adding the deploy_helper module
This commit is contained in:
parent
205115ea1f
commit
559a7e7a32
1 changed files with 341 additions and 0 deletions
341
web_infrastructure/deploy_helper.py
Normal file
341
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