ansible/web_infrastructure/deploy_helper.py

342 lines
13 KiB
Python
Raw Normal View History

2014-11-16 22:40:37 +01:00
#!/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()