mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-23 19:01:26 -07:00
Unflatmap community.general (#5461)
* Move files. * Update imports and references. * Move wrongly placed files. * Reverse redirects, deprecate long → short name redirects. * Simplify contribution guidelines for new modules. * Rewrite BOTMETA. * Add changelog fragment. * Fix ignore.txt files.
This commit is contained in:
parent
2b0bebc8fc
commit
b531ecdc9b
1033 changed files with 4802 additions and 1989 deletions
527
plugins/modules/deploy_helper.py
Normal file
527
plugins/modules/deploy_helper.py
Normal file
|
@ -0,0 +1,527 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014, Jasper N. Brouwer <jasper@nerdsweide.nl>
|
||||
# Copyright (c) 2014, Ramon de la Fuente <ramon@delafuente.nl>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: deploy_helper
|
||||
author: "Ramon de la Fuente (@ramondelafuente)"
|
||||
short_description: Manages some of the steps common in deploying projects.
|
||||
description:
|
||||
- The Deploy Helper manages some of the steps common in deploying software.
|
||||
It creates a folder structure, manages a symlink for the current release
|
||||
and cleans up old releases.
|
||||
- "Running it with the I(state=query) or I(state=present) will return the C(deploy_helper) fact.
|
||||
C(project_path), whatever you set in the I(path) parameter,
|
||||
C(current_path), the path to the symlink that points to the active release,
|
||||
C(releases_path), the path to the folder to keep releases in,
|
||||
C(shared_path), the path to the folder to keep shared resources in,
|
||||
C(unfinished_filename), the file to check for to recognize unfinished builds,
|
||||
C(previous_release), the release the 'current' symlink is pointing to,
|
||||
C(previous_release_path), the full path to the 'current' symlink target,
|
||||
C(new_release), either the 'release' parameter or a generated timestamp,
|
||||
C(new_release_path), the path to the new release folder (not created by the module)."
|
||||
|
||||
options:
|
||||
path:
|
||||
type: path
|
||||
required: true
|
||||
aliases: ['dest']
|
||||
description:
|
||||
- The root path of the project.
|
||||
Returned in the C(deploy_helper.project_path) fact.
|
||||
|
||||
state:
|
||||
type: str
|
||||
description:
|
||||
- The state of the project.
|
||||
C(query) will only gather facts,
|
||||
C(present) will create the project I(root) folder, and in it the I(releases) and I(shared) folders,
|
||||
C(finalize) will remove the unfinished_filename file, create a symlink to the newly
|
||||
deployed release and optionally clean old releases,
|
||||
C(clean) will remove failed & old releases,
|
||||
C(absent) will remove the project folder (synonymous to the M(ansible.builtin.file) module with I(state=absent)).
|
||||
choices: [ present, finalize, absent, clean, query ]
|
||||
default: present
|
||||
|
||||
release:
|
||||
type: str
|
||||
description:
|
||||
- The release version that is being deployed. Defaults to a timestamp format %Y%m%d%H%M%S (i.e. '20141119223359').
|
||||
This parameter is optional during I(state=present), but needs to be set explicitly for I(state=finalize).
|
||||
You can use the generated fact I(release={{ deploy_helper.new_release }}).
|
||||
|
||||
releases_path:
|
||||
type: str
|
||||
description:
|
||||
- The name of the folder that will hold the releases. This can be relative to I(path) or absolute.
|
||||
Returned in the C(deploy_helper.releases_path) fact.
|
||||
default: releases
|
||||
|
||||
shared_path:
|
||||
type: path
|
||||
description:
|
||||
- The name of the folder that will hold the shared resources. This can be relative to I(path) or absolute.
|
||||
If this is set to an empty string, no shared folder will be created.
|
||||
Returned in the C(deploy_helper.shared_path) fact.
|
||||
default: shared
|
||||
|
||||
current_path:
|
||||
type: path
|
||||
description:
|
||||
- The name of the symlink that is created when the deploy is finalized. Used in I(finalize) and I(clean).
|
||||
Returned in the C(deploy_helper.current_path) fact.
|
||||
default: current
|
||||
|
||||
unfinished_filename:
|
||||
type: str
|
||||
description:
|
||||
- The name of the file that indicates a deploy has not finished. All folders in the I(releases_path) that
|
||||
contain this file will be deleted on I(state=finalize) with I(clean=True), or I(state=clean). This file is
|
||||
automatically deleted from the I(new_release_path) during I(state=finalize).
|
||||
default: DEPLOY_UNFINISHED
|
||||
|
||||
clean:
|
||||
description:
|
||||
- Whether to run the clean procedure in case of I(state=finalize).
|
||||
type: bool
|
||||
default: true
|
||||
|
||||
keep_releases:
|
||||
type: int
|
||||
description:
|
||||
- The number of old releases to keep when cleaning. Used in I(finalize) and I(clean). Any unfinished builds
|
||||
will be deleted first, so only correct releases will count. The current version will not count.
|
||||
default: 5
|
||||
|
||||
notes:
|
||||
- Facts are only returned for I(state=query) and I(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 I(state=clean), the releases are ordered by I(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 I(release). Due to the nature of deploying software, this should not
|
||||
be much of a problem.
|
||||
extends_documentation_fragment: files
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
||||
# General explanation, starting with an example folder structure for a project:
|
||||
|
||||
# root:
|
||||
# releases:
|
||||
# - 20140415234508
|
||||
# - 20140415235146
|
||||
# - 20140416082818
|
||||
#
|
||||
# shared:
|
||||
# - sessions
|
||||
# - uploads
|
||||
#
|
||||
# current: releases/20140416082818
|
||||
|
||||
|
||||
# The 'releases' folder holds all the available releases. A release is a complete build of the application being
|
||||
# deployed. This can be a clone of a repository for example, or a sync of a local folder on your filesystem.
|
||||
# Having timestamped folders is one way of having distinct releases, but you could choose your own strategy like
|
||||
# git tags or commit hashes.
|
||||
#
|
||||
# During a deploy, a new folder should be created in the releases folder and any build steps required should be
|
||||
# performed. Once the new build is ready, the deploy procedure is 'finalized' by replacing the 'current' symlink
|
||||
# with a link to this build.
|
||||
#
|
||||
# The 'shared' folder holds any resource that is shared between releases. Examples of this are web-server
|
||||
# session files, or files uploaded by users of your application. It's quite common to have symlinks from a release
|
||||
# folder pointing to a shared/subfolder, and creating these links would be automated as part of the build steps.
|
||||
#
|
||||
# The 'current' symlink points to one of the releases. Probably the latest one, unless a deploy is in progress.
|
||||
# The web-server's root for the project will go through this symlink, so the 'downtime' when switching to a new
|
||||
# release is reduced to the time it takes to switch the link.
|
||||
#
|
||||
# To distinguish between successful builds and unfinished ones, a file can be placed in the folder of the release
|
||||
# that is currently in progress. The existence of this file will mark it as unfinished, and allow an automated
|
||||
# procedure to remove it during cleanup.
|
||||
|
||||
|
||||
# Typical usage
|
||||
- name: Initialize the deploy root and gather facts
|
||||
community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
- name: Clone the project to the new release folder
|
||||
ansible.builtin.git:
|
||||
repo: ansible.builtin.git://foosball.example.org/path/to/repo.git
|
||||
dest: '{{ deploy_helper.new_release_path }}'
|
||||
version: v1.1.1
|
||||
- name: Add an unfinished file, to allow cleanup on successful finalize
|
||||
ansible.builtin.file:
|
||||
path: '{{ deploy_helper.new_release_path }}/{{ deploy_helper.unfinished_filename }}'
|
||||
state: touch
|
||||
- name: Perform some build steps, like running your dependency manager for example
|
||||
composer:
|
||||
command: install
|
||||
working_dir: '{{ deploy_helper.new_release_path }}'
|
||||
- name: Create some folders in the shared folder
|
||||
ansible.builtin.file:
|
||||
path: '{{ deploy_helper.shared_path }}/{{ item }}'
|
||||
state: directory
|
||||
with_items:
|
||||
- sessions
|
||||
- uploads
|
||||
- name: Add symlinks from the new release to the shared folder
|
||||
ansible.builtin.file:
|
||||
path: '{{ deploy_helper.new_release_path }}/{{ item.path }}'
|
||||
src: '{{ deploy_helper.shared_path }}/{{ item.src }}'
|
||||
state: link
|
||||
with_items:
|
||||
- path: app/sessions
|
||||
src: sessions
|
||||
- path: web/uploads
|
||||
src: uploads
|
||||
- name: Finalize the deploy, removing the unfinished file and switching the symlink
|
||||
community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
release: '{{ deploy_helper.new_release }}'
|
||||
state: finalize
|
||||
|
||||
# Retrieving facts before running a deploy
|
||||
- name: Run 'state=query' to gather facts without changing anything
|
||||
community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
state: query
|
||||
# Remember to set the 'release' parameter when you actually call 'state=present' later
|
||||
- name: Initialize the deploy root
|
||||
community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
release: '{{ deploy_helper.new_release }}'
|
||||
state: present
|
||||
|
||||
# all paths can be absolute or relative (to the 'path' parameter)
|
||||
- community.general.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 for releases (a version tag in this case):
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
release: v1.1.1
|
||||
state: present
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
release: '{{ deploy_helper.new_release }}'
|
||||
state: finalize
|
||||
|
||||
# Using a different unfinished_filename:
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
unfinished_filename: README.md
|
||||
release: '{{ deploy_helper.new_release }}'
|
||||
state: finalize
|
||||
|
||||
# Postponing the cleanup of older builds:
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
release: '{{ deploy_helper.new_release }}'
|
||||
state: finalize
|
||||
clean: false
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
state: clean
|
||||
# Or running the cleanup ahead of the new deploy
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
state: clean
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
state: present
|
||||
|
||||
# Keeping more old releases:
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
release: '{{ deploy_helper.new_release }}'
|
||||
state: finalize
|
||||
keep_releases: 10
|
||||
# Or, if you use 'clean=false' on finalize:
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
state: clean
|
||||
keep_releases: 10
|
||||
|
||||
# Removing the entire project root folder
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
state: absent
|
||||
|
||||
# Debugging the facts returned by the module
|
||||
- community.general.deploy_helper:
|
||||
path: /path/to/root
|
||||
- ansible.builtin.debug:
|
||||
var: deploy_helper
|
||||
'''
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
|
||||
class DeployHelper(object):
|
||||
|
||||
def __init__(self, module):
|
||||
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")
|
||||
|
||||
if self.release:
|
||||
new_release_path = os.path.join(releases_path, self.release)
|
||||
else:
|
||||
new_release_path = None
|
||||
|
||||
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 as e:
|
||||
self.module.fail_json(msg="rmtree failed: %s" % to_native(e), exception=traceback.format_exc())
|
||||
|
||||
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 os.path.islink(link_name):
|
||||
norm_link = os.path.normpath(os.path.realpath(link_name))
|
||||
norm_source = os.path.normpath(os.path.realpath(source))
|
||||
if norm_link == norm_source:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
if not self.module.check_mode:
|
||||
if not os.path.lexists(source):
|
||||
self.module.fail_json(msg="the symlink target %s doesn't exists" % source)
|
||||
tmp_link_name = link_name + '.' + self.unfinished_filename
|
||||
if os.path.islink(tmp_link_name):
|
||||
os.unlink(tmp_link_name)
|
||||
os.symlink(source, tmp_link_name)
|
||||
os.rename(tmp_link_name, link_name)
|
||||
else:
|
||||
changed = True
|
||||
if not self.module.check_mode:
|
||||
os.symlink(source, link_name)
|
||||
|
||||
return changed
|
||||
|
||||
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 remove_unfinished_link(self, path):
|
||||
changed = False
|
||||
|
||||
if not self.release:
|
||||
return changed
|
||||
|
||||
tmp_link_name = os.path.join(path, self.release + '.' + self.unfinished_filename)
|
||||
if not self.module.check_mode and os.path.exists(tmp_link_name):
|
||||
changed = True
|
||||
os.remove(tmp_link_name)
|
||||
|
||||
return changed
|
||||
|
||||
def cleanup(self, releases_path, reserve_version):
|
||||
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))]
|
||||
try:
|
||||
releases.remove(reserve_version)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
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='path'),
|
||||
release=dict(type='str'),
|
||||
releases_path=dict(type='str', default='releases'),
|
||||
shared_path=dict(type='path', default='shared'),
|
||||
current_path=dict(type='path', default='current'),
|
||||
keep_releases=dict(type='int', default=5),
|
||||
clean=dict(type='bool', default=True),
|
||||
unfinished_filename=dict(type='str', default='DEPLOY_UNFINISHED'),
|
||||
state=dict(choices=['present', 'absent', 'clean', 'finalize', 'query'], default='present')
|
||||
),
|
||||
required_if=[
|
||||
('state', 'finalize', ['release']),
|
||||
],
|
||||
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 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_link(facts['project_path'])
|
||||
changes += deploy_helper.remove_unfinished_builds(facts['releases_path'])
|
||||
changes += deploy_helper.cleanup(facts['releases_path'], facts['new_release'])
|
||||
|
||||
elif deploy_helper.state == 'clean':
|
||||
changes += deploy_helper.remove_unfinished_link(facts['project_path'])
|
||||
changes += deploy_helper.remove_unfinished_builds(facts['releases_path'])
|
||||
changes += deploy_helper.cleanup(facts['releases_path'], facts['new_release'])
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue