forked from ansible/ansible
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a1dbb83
commit 7841bf9
Showing
1 changed file
with
341 additions
and
0 deletions.
There are no files selected for viewing
341 changes: 341 additions & 0 deletions
341
lib/ansible/modules/extras/web_infrastructure/deploy_helper.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,341 @@ | ||
#!/usr/bin/python | ||
|
||
DOCUMENTATION = ''' | ||
--- | ||
module: deploy_helper | ||
version_added: "1.8" | ||
author: Ramon de la Fuente, Jasper N. Brouwer | ||
short_description: Manages the folders for deploy of a project | ||
description: | ||
- Manages some of the steps common in deploying projects. | ||
It creates a folder structure, cleans up old releases and manages a symlink for the current release. | ||
For more information, see the :doc:`guide_deploy_helper` | ||
options: | ||
path: | ||
required: true | ||
aliases: ['dest'] | ||
description: | ||
- the root path of the project. Alias I(dest). | ||
state: | ||
required: false | ||
choices: [ present, finalize, absent, clean, query ] | ||
default: present | ||
description: | ||
- the state of the project. | ||
C(query) will only gather facts, | ||
C(present) will create the project, | ||
C(finalize) will create a symlink to the newly deployed release, | ||
C(clean) will remove failed & old releases, | ||
C(absent) will remove the project folder (synonymous to M(file) with state=absent) | ||
release: | ||
required: false | ||
description: | ||
- the release version that is being deployed (defaults to a timestamp %Y%m%d%H%M%S). This parameter is | ||
optional during C(state=present), but needs to be set explicitly for C(state=finalize). You can use the | ||
generated fact C(release={{ deploy_helper.new_release }}) | ||
releases_path: | ||
required: false | ||
default: releases | ||
description: | ||
- the name of the folder that will hold the releases. This can be relative to C(path) or absolute. | ||
shared_path: | ||
required: false | ||
default: shared | ||
description: | ||
- the name of the folder that will hold the shared resources. This can be relative to C(path) or absolute. | ||
If this is set to an empty string, no shared folder will be created. | ||
current_path: | ||
required: false | ||
default: current | ||
description: | ||
- the name of the symlink that is created when the deploy is finalized. Used in C(finalize) and C(clean). | ||
unfinished_filename: | ||
required: false | ||
default: DEPLOY_UNFINISHED | ||
description: | ||
- the name of the file that indicates a deploy has not finished. All folders in the releases_path that | ||
contain this file will be deleted on C(state=finalize) with clean=True, or C(state=clean). This file is | ||
automatically deleted from the I(new_release_path) during C(state=finalize). | ||
clean: | ||
required: false | ||
default: True | ||
description: | ||
- Whether to run the clean procedure in case of C(state=finalize). | ||
keep_releases: | ||
required: false | ||
default: 5 | ||
description: | ||
- the number of old releases to keep when cleaning. Used in C(finalize) and C(clean). Any unfinished builds | ||
will be deleted first, so only correct releases will count. | ||
notes: | ||
- Facts are only returned for C(state=query) and C(state=present). If you use both, you should pass any overridden | ||
parameters to both calls, otherwise the second call will overwrite the facts of the first one. | ||
- When using C(state=clean), the releases are ordered by creation date. You should be able to switch to a | ||
new naming strategy without problems. | ||
- Because of the default behaviour of generating the I(new_release) fact, this module will not be idempotent | ||
unless you pass your own release name with C(release). Due to the nature of deploying software, this should not | ||
be much of a problem. | ||
''' | ||
|
||
EXAMPLES = ''' | ||
Example usage for the deploy_helper module. | ||
tasks: | ||
# Typical usage: | ||
- deploy_helper: path=/path/to/root state=present | ||
...some_build_steps_here, like a git clone to {{ deploy_helper.new_release_path }} for example... | ||
- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize | ||
# Gather information only | ||
- deploy_helper: path=/path/to/root state=query | ||
# Remember to set the 'release=' when you actually call state=present later | ||
- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=present | ||
# all paths can be absolute or relative (to 'path') | ||
- deploy_helper: path=/path/to/root | ||
releases_path=/var/www/project/releases | ||
shared_path=/var/www/shared | ||
current_path=/var/www/active | ||
# Using your own naming strategy: | ||
- deploy_helper: path=/path/to/root release=v1.1.1 state=present | ||
- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize | ||
# Postponing the cleanup of older builds: | ||
- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize clean=False | ||
...anything you do before actually deleting older releases... | ||
- deploy_helper: path=/path/to/root state=clean | ||
# Keeping more old releases: | ||
- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize keep_releases=10 | ||
# Or: | ||
- deploy_helper: path=/path/to/root state=clean keep_releases=10 | ||
# Using a different unfinished_filename: | ||
- deploy_helper: path=/path/to/root unfinished_filename=README.md release={{ deploy_helper.new_release }} state=finalize | ||
''' | ||
|
||
class DeployHelper(object): | ||
|
||
def __init__(self, module): | ||
module.params['path'] = os.path.expanduser(module.params['path']) | ||
|
||
self.module = module | ||
self.file_args = module.load_file_common_arguments(module.params) | ||
|
||
self.clean = module.params['clean'] | ||
self.current_path = module.params['current_path'] | ||
self.keep_releases = module.params['keep_releases'] | ||
self.path = module.params['path'] | ||
self.release = module.params['release'] | ||
self.releases_path = module.params['releases_path'] | ||
self.shared_path = module.params['shared_path'] | ||
self.state = module.params['state'] | ||
self.unfinished_filename = module.params['unfinished_filename'] | ||
|
||
def gather_facts(self): | ||
current_path = os.path.join(self.path, self.current_path) | ||
releases_path = os.path.join(self.path, self.releases_path) | ||
if self.shared_path: | ||
shared_path = os.path.join(self.path, self.shared_path) | ||
else: | ||
shared_path = None | ||
|
||
previous_release, previous_release_path = self._get_last_release(current_path) | ||
|
||
if not self.release and (self.state == 'query' or self.state == 'present'): | ||
self.release = time.strftime("%Y%m%d%H%M%S") | ||
|
||
new_release_path = os.path.join(releases_path, self.release) | ||
|
||
return { | ||
'project_path': self.path, | ||
'current_path': current_path, | ||
'releases_path': releases_path, | ||
'shared_path': shared_path, | ||
'previous_release': previous_release, | ||
'previous_release_path': previous_release_path, | ||
'new_release': self.release, | ||
'new_release_path': new_release_path, | ||
'unfinished_filename': self.unfinished_filename | ||
} | ||
|
||
def delete_path(self, path): | ||
if not os.path.lexists(path): | ||
return False | ||
|
||
if not os.path.isdir(path): | ||
self.module.fail_json(msg="%s exists but is not a directory" % path) | ||
|
||
if not self.module.check_mode: | ||
try: | ||
shutil.rmtree(path, ignore_errors=False) | ||
except Exception, e: | ||
self.module.fail_json(msg="rmtree failed: %s" % str(e)) | ||
|
||
return True | ||
|
||
def create_path(self, path): | ||
changed = False | ||
|
||
if not os.path.lexists(path): | ||
changed = True | ||
if not self.module.check_mode: | ||
os.makedirs(path) | ||
|
||
elif not os.path.isdir(path): | ||
self.module.fail_json(msg="%s exists but is not a directory" % path) | ||
|
||
changed += self.module.set_directory_attributes_if_different(self._get_file_args(path), changed) | ||
|
||
return changed | ||
|
||
def check_link(self, path): | ||
if os.path.lexists(path): | ||
if not os.path.islink(path): | ||
self.module.fail_json(msg="%s exists but is not a symbolic link" % path) | ||
|
||
def create_link(self, source, link_name): | ||
if not self.module.check_mode: | ||
if os.path.islink(link_name): | ||
os.unlink(link_name) | ||
os.symlink(source, link_name) | ||
|
||
return True | ||
|
||
def remove_unfinished_file(self, new_release_path): | ||
changed = False | ||
unfinished_file_path = os.path.join(new_release_path, self.unfinished_filename) | ||
if os.path.lexists(unfinished_file_path): | ||
changed = True | ||
if not self.module.check_mode: | ||
os.remove(unfinished_file_path) | ||
|
||
return changed | ||
|
||
def remove_unfinished_builds(self, releases_path): | ||
changes = 0 | ||
|
||
for release in os.listdir(releases_path): | ||
if (os.path.isfile(os.path.join(releases_path, release, self.unfinished_filename))): | ||
if self.module.check_mode: | ||
changes += 1 | ||
else: | ||
changes += self.delete_path(os.path.join(releases_path, release)) | ||
|
||
return changes | ||
|
||
def cleanup(self, releases_path): | ||
changes = 0 | ||
|
||
if os.path.lexists(releases_path): | ||
releases = [ f for f in os.listdir(releases_path) if os.path.isdir(os.path.join(releases_path,f)) ] | ||
|
||
if not self.module.check_mode: | ||
releases.sort( key=lambda x: os.path.getctime(os.path.join(releases_path,x)), reverse=True) | ||
for release in releases[self.keep_releases:]: | ||
changes += self.delete_path(os.path.join(releases_path, release)) | ||
elif len(releases) > self.keep_releases: | ||
changes += (len(releases) - self.keep_releases) | ||
|
||
return changes | ||
|
||
def _get_file_args(self, path): | ||
file_args = self.file_args.copy() | ||
file_args['path'] = path | ||
return file_args | ||
|
||
def _get_last_release(self, current_path): | ||
previous_release = None | ||
previous_release_path = None | ||
|
||
if os.path.lexists(current_path): | ||
previous_release_path = os.path.realpath(current_path) | ||
previous_release = os.path.basename(previous_release_path) | ||
|
||
return previous_release, previous_release_path | ||
|
||
def main(): | ||
|
||
module = AnsibleModule( | ||
argument_spec = dict( | ||
path = dict(aliases=['dest'], required=True, type='str'), | ||
release = dict(required=False, type='str', default=''), | ||
releases_path = dict(required=False, type='str', default='releases'), | ||
shared_path = dict(required=False, type='str', default='shared'), | ||
current_path = dict(required=False, type='str', default='current'), | ||
keep_releases = dict(required=False, type='int', default=5), | ||
clean = dict(required=False, type='bool', default=True), | ||
unfinished_filename = dict(required=False, type='str', default='DEPLOY_UNFINISHED'), | ||
state = dict(required=False, choices=['present', 'absent', 'clean', 'finalize', 'query'], default='present') | ||
), | ||
add_file_common_args = True, | ||
supports_check_mode = True | ||
) | ||
|
||
deploy_helper = DeployHelper(module) | ||
facts = deploy_helper.gather_facts() | ||
|
||
result = { | ||
'state': deploy_helper.state | ||
} | ||
|
||
changes = 0 | ||
|
||
if deploy_helper.state == 'query': | ||
result['ansible_facts'] = { 'deploy_helper': facts } | ||
|
||
elif deploy_helper.state == 'present': | ||
deploy_helper.check_link(facts['current_path']) | ||
changes += deploy_helper.create_path(facts['project_path']) | ||
changes += deploy_helper.create_path(facts['releases_path']) | ||
if deploy_helper.shared_path: | ||
changes += deploy_helper.create_path(facts['shared_path']) | ||
|
||
result['ansible_facts'] = { 'deploy_helper': facts } | ||
|
||
elif deploy_helper.state == 'finalize': | ||
if not deploy_helper.release: | ||
module.fail_json(msg="'release' is a required parameter for state=finalize (try the 'deploy_helper.new_release' fact)") | ||
if deploy_helper.keep_releases <= 0: | ||
module.fail_json(msg="'keep_releases' should be at least 1") | ||
|
||
changes += deploy_helper.remove_unfinished_file(facts['new_release_path']) | ||
changes += deploy_helper.create_link(facts['new_release_path'], facts['current_path']) | ||
if deploy_helper.clean: | ||
changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) | ||
changes += deploy_helper.cleanup(facts['releases_path']) | ||
|
||
elif deploy_helper.state == 'clean': | ||
changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) | ||
changes += deploy_helper.cleanup(facts['releases_path']) | ||
|
||
elif deploy_helper.state == 'absent': | ||
# destroy the facts | ||
result['ansible_facts'] = { 'deploy_helper': [] } | ||
changes += deploy_helper.delete_path(facts['project_path']) | ||
|
||
if changes > 0: | ||
result['changed'] = True | ||
else: | ||
result['changed'] = False | ||
|
||
module.exit_json(**result) | ||
|
||
|
||
# import module snippets | ||
from ansible.module_utils.basic import * | ||
|
||
main() |