adding the deploy_helper module
This commit is contained in:
parent
a1dbb83075
commit
7841bf9c91
1 changed files with 341 additions and 0 deletions
341
lib/ansible/modules/extras/web_infrastructure/deploy_helper.py
Normal file
341
lib/ansible/modules/extras/web_infrastructure/deploy_helper.py
Normal file
|
@ -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()
|
Loading…
Reference in a new issue