From fcd86987db8f7f7440809a01b7046ffdaf652458 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Mon, 1 Aug 2016 18:19:09 -0700 Subject: [PATCH 01/11] Initial python wrapper for ansible playbooks and provisioning --- bin/evolve | 199 ++++++++++++++++++++ lib/tasks/provision.py | 23 +++ lib/yeoman/templates/bin/evolve | 8 + lib/yeoman/templates/lib/ansible/galaxy.yml | 2 + 4 files changed, 232 insertions(+) create mode 100755 bin/evolve create mode 100644 lib/tasks/provision.py create mode 100644 lib/yeoman/templates/bin/evolve diff --git a/bin/evolve b/bin/evolve new file mode 100755 index 00000000..8e8ad48b --- /dev/null +++ b/bin/evolve @@ -0,0 +1,199 @@ +#!/usr/bin/env python + +from __future__ import print_function +import datetime +import getpass +import getopt +import json +import os +import os.path +import pipes +import socket +import subprocess +import sys +import termcolor + +class Evolve: + def __init__(self): + # separate any command line opts from args + try: + opts, args = getopt.getopt(sys.argv[1:], '') + except getopt.GetoptError as err: + pass + + # usage statement when missing args + if len(args) < 2: + print('Usage: ./bin/evolve [args...]') + sys.exit(1) + + # seperate arguments and expose them for dependency injection + self.stage = args.pop(0) + self.action = args.pop(0) + self.arguments = args + + self.announce('running "%s" task in the "%s" stage' % (self.action, self.stage)) + + # determine paths + self.working_path = os.getcwd() + self.ansible_path = os.path.join(self.working_path, 'lib/ansible') + self.tasks_path = os.path.join(os.path.dirname(__file__), '../lib/tasks') + + # install galaxy roles + # todo: bypass --force unless roles are older than X? + self.call(['ansible-galaxy', 'install', '-r', os.path.join(self.ansible_path, 'galaxy.yml'), '--force']) + + # import and instantiate action + sys.path.append(self.tasks_path) + try: + self.import_action(self.action) + except ImportError as err: + print(termcolor.colored('Unknown action "%s"' % self.action, 'red'), file=sys.stderr) + sys.exit(2) + except Exception as err: + print(termcolor.colored('The "%s" action failed: %s' % (self.action, err), 'red'), file=sys.stderr) + self.log_action(False, ' '.join([self.action] + args)) + else: + self.log_action(True, ' '.join([self.action] + args)) + + def announce(self, message): + ''' + Prints a colored announcement across the terminal. + ''' + print(termcolor.colored("\n! %s" % message, 'green')) + + def call(self, *args): + ''' + Prints and runs a command, or piped series of commands, inheriting standard input/output and returning after completion. + Throws subprocess.CalledProcessError if returncode was nonzero. + ''' + command = self._normalize_commands(args) + print(termcolor.colored("$ %s\n" % command, 'yellow')) + return subprocess.check_call(command, shell=True) + + def bg_call(self, *args): + ''' + Runs a command, or piped series of commands, ignoring standard input/output and returning a returncode after completion. + ''' + command = self._normalize_commands(args) + # TODO: instead of subprocess.PIPE, should open & provide file descriptors (to temp files?) + # TODO: alternatively, use subprocess.Popen with Popen.communicate? + return subprocess.call(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + def _normalize_commands(self, args): + ''' + Normalizes and escapes one or more commands (as lists), piping each into the command following it. + ''' + groups = [] + + for command in args: + # remove any None args + command = filter(lambda a: a != None, command) + # quote each arg for shell metacharacters + command = map(pipes.quote, command) + # flatten args to string command + groups.append(' '.join(command)) + + # flatten commands to pipe chain + return ' | '.join(groups) + + def single_input(self, prompt, cb): + ''' + Prompts for a single line of input, validating with a given callback. + ''' + built_prompt = "\n? %s: " % prompt + while True: + result = raw_input(termcolor.colored(built_prompt, 'cyan')) + + try: + if cb(result): + return result + except: + pass + + def multi_input(self, prompt, choices): + ''' + Prompts from a given list of choices. + ''' + built_prompt = termcolor.colored("\n? %s:\n" % prompt, 'cyan') + + for i, choice in enumerate(choices): + choices[i] = choice.lower() + built_prompt += " %s %s\n" % ( + termcolor.colored("[%i]" % (i+1), 'grey', 'on_cyan'), + termcolor.colored(choice.lower(), 'cyan') + ) + + built_prompt += termcolor.colored("> ", 'cyan') + + while True: + result = raw_input(built_prompt) + + try: + test_index = int(result) + if 1 <= test_index <= len(choices): + return choices[test_index-1] + except (ValueError, IndexError, TypeError): + pass + + if result.lower() in choices: + return result.lower() + + def password_input(self, prompt): + ''' + Prompts for a password (which is not shown). + ''' + built_prompt = "\n\a? %s: " % prompt + return getpass.getpass(termcolor.colored(built_prompt, 'white', 'on_blue')) + + def import_action(self, action): + ''' + Imports an module (expected to be in the tasks path), and instantiates its ActionClass. + ''' + action_module = __import__(action) + action_module.ActionClass(self) + + def playbook(self, playbook, extra_vars=None, extra_args=None): + ''' + Runs an ansible playbook, with optional json encoded extra_vars and optional extra arguments. + ''' + call_args = ['ansible-playbook'] + + # add extra_vars encoded as json + if extra_vars is not None: + call_args += ['-e', json.dumps(extra_vars)] + + # add extra args... + if extra_args is not None: + # append, if a scalar string + if isinstance(extra_args, basestring): + call_args.append(extra_args) + # otherwise add as a list-like + else: + call_args += extra_args + + # append playbook filepath + call_args.append(os.path.join(self.ansible_path, playbook)) + + self.call(call_args) + + def log_action(self, success, message): + ''' + Writes a log entry to a remote server, via a specialized ansible playbook. + ''' + local_user = getpass.getuser() + local_host = socket.gethostname() + local_time = datetime.datetime.today().strftime('%c') + status = 'Success' if success else 'Failure' + + extra_vars = { + 'stage': self.stage, + 'log_message': '\t'.join([local_time, local_user, message, status]) + } + + try: + self.playbook('log.yml', extra_vars) + except subprocess.CalledProcessError as err: + print(termcolor.colored('Failed to log action to remote: %s' % err, 'red'), file=sys.stderr) + +if __name__ == "__main__": + Evolve() diff --git a/lib/tasks/provision.py b/lib/tasks/provision.py new file mode 100644 index 00000000..d256beb4 --- /dev/null +++ b/lib/tasks/provision.py @@ -0,0 +1,23 @@ +import os.path +import re +import subprocess + +class ActionClass: + def __init__(self, evolve): + # TODO: implement `invoke "evolve:prepare_key"` + + extra_vars = { + 'stage': evolve.stage + } + + try: + evolve.playbook('provision.yml', extra_vars, '--user=deploy') + except subprocess.CalledProcessError: + evolve.announce('Unable to provision with SSH publickey for deploy user') + + # connect as intermediate user, and set up deploy user + username = evolve.single_input('user to provision as (root)', lambda u: not u or re.match(r"^[a-z_][a-z0-9_-]*[$]?", u, re.I)) or 'root' + evolve.playbook('user.yml', extra_vars, ['--user=%s' % username, '--ask-pass', '--ask-sudo-pass']) + + # provision as usual + evolve.playbook('provision.yml', extra_vars, '--user=deploy') diff --git a/lib/yeoman/templates/bin/evolve b/lib/yeoman/templates/bin/evolve new file mode 100644 index 00000000..60015852 --- /dev/null +++ b/lib/yeoman/templates/bin/evolve @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +IMPORTER_PATH="$( dirname "$0" )/../bower_components/evolution-wordpress" +IMPORTER_SCRIPT="$IMPORTER_PATH/bin/evolve" + +chmod +x "$IMPORTER_SCRIPT" + +$IMPORTER_SCRIPT diff --git a/lib/yeoman/templates/lib/ansible/galaxy.yml b/lib/yeoman/templates/lib/ansible/galaxy.yml index c93bbb46..f147d672 100644 --- a/lib/yeoman/templates/lib/ansible/galaxy.yml +++ b/lib/yeoman/templates/lib/ansible/galaxy.yml @@ -1 +1,3 @@ +- src: carlosbuenosvinos.ansistrano-deploy +- src: carlosbuenosvinos.ansistrano-rollback <% if (props.datadog) { %>- src: Datadog.datadog<% } %> From cb4452aaa128394613d8aff8a552249ad22ab832 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Mon, 1 Aug 2016 18:38:32 -0700 Subject: [PATCH 02/11] Added custom log playbook --- lib/yeoman/templates/lib/ansible/log.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 lib/yeoman/templates/lib/ansible/log.yml diff --git a/lib/yeoman/templates/lib/ansible/log.yml b/lib/yeoman/templates/lib/ansible/log.yml new file mode 100644 index 00000000..33c607f4 --- /dev/null +++ b/lib/yeoman/templates/lib/ansible/log.yml @@ -0,0 +1,20 @@ +--- +- hosts: "{{ stage }}" + gather_facts: yes + + vars: + log_dir: "/var/log/evolution" + log_file: "{{ log_dir }}/wordpress.log" + + tasks: + - name: Ensure log directory exists and is owned by remote user + file: path={{ log_dir }} state=directory mode=0644 owner={{ ansible_ssh_user }} + sudo: yes + + - name: Ensure log file exists and is owned by remote user + file: path={{ log_file }} state=touch mode=0644 owner={{ ansible_ssh_user }} + sudo: yes + + - name: Append message to log file + lineinfile: insertafter=EOF dest={{ log_file }} line={{ log_message }} + sudo: yes From 62bf4f9a74f9d8dff2fad1b89e2d1219ff5e7602 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Wed, 10 Aug 2016 14:50:30 -0700 Subject: [PATCH 03/11] Retooled wrapper significantly, added ansistrano deploy/rollback support --- bin/evolve | 99 +++++++++++++------ lib/actions/deploy.py | 27 +++++ lib/{tasks => actions}/provision.py | 1 - lib/actions/rollback.py | 13 +++ lib/ansistrano/after-update-code-tasks.yml | 5 + lib/ansistrano/before-setup-tasks.yml | 15 +++ lib/yeoman/templates/bin/evolve | 2 +- lib/yeoman/templates/lib/ansible/deploy.yml | 6 ++ .../templates/lib/ansible/group_vars/all | 10 ++ 9 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 lib/actions/deploy.py rename lib/{tasks => actions}/provision.py (98%) create mode 100644 lib/actions/rollback.py create mode 100644 lib/ansistrano/after-update-code-tasks.yml create mode 100644 lib/ansistrano/before-setup-tasks.yml create mode 100644 lib/yeoman/templates/lib/ansible/deploy.yml diff --git a/bin/evolve b/bin/evolve index 8e8ad48b..70b0917d 100755 --- a/bin/evolve +++ b/bin/evolve @@ -1,13 +1,14 @@ #!/usr/bin/env python from __future__ import print_function +import argparse import datetime import getpass -import getopt import json import os import os.path import pipes +import re import socket import subprocess import sys @@ -15,45 +16,51 @@ import termcolor class Evolve: def __init__(self): - # separate any command line opts from args - try: - opts, args = getopt.getopt(sys.argv[1:], '') - except getopt.GetoptError as err: - pass - - # usage statement when missing args - if len(args) < 2: - print('Usage: ./bin/evolve [args...]') - sys.exit(1) + # handle arguments & flags + parser = argparse.ArgumentParser(prog='./bin/evolve') + parser.add_argument('stage', help='environment against which to run given action') + parser.add_argument('action', help='action to run against given stage') + parser.add_argument('-v','--verbose', action='count', help='increase verbosity') + parser.add_argument('-e','--extra-vars', action='append', help='provide key=value pairs or JSON document') + self.arguments = parser.parse_args() + self._extra_vars() - # seperate arguments and expose them for dependency injection - self.stage = args.pop(0) - self.action = args.pop(0) - self.arguments = args - - self.announce('running "%s" task in the "%s" stage' % (self.action, self.stage)) + self.announce_verbose('Parsed args:', self.arguments) # determine paths self.working_path = os.getcwd() self.ansible_path = os.path.join(self.working_path, 'lib/ansible') - self.tasks_path = os.path.join(os.path.dirname(__file__), '../lib/tasks') + self.actions_path = os.path.join(os.path.dirname(__file__), '../lib/actions') + + self.announce_verbose('Paths:', { + 'working_path': self.working_path, + 'ansible_path': self.ansible_path, + 'actions_path': self.actions_path, + }) + + # expose positional arguments for simpler dependency injection + self.stage = self.arguments.stage + self.action = self.arguments.action + + self.announce('Loading "%s" action in the "%s" stage' % (self.action, self.stage)) + try: + action_module = self.import_action(self.action) + except ImportError as err: + print(termcolor.colored('Unknown action "%s"' % self.action, 'red'), file=sys.stderr) + sys.exit(1) # install galaxy roles # todo: bypass --force unless roles are older than X? self.call(['ansible-galaxy', 'install', '-r', os.path.join(self.ansible_path, 'galaxy.yml'), '--force']) # import and instantiate action - sys.path.append(self.tasks_path) try: - self.import_action(self.action) - except ImportError as err: - print(termcolor.colored('Unknown action "%s"' % self.action, 'red'), file=sys.stderr) - sys.exit(2) + self.init_action(action_module) except Exception as err: print(termcolor.colored('The "%s" action failed: %s' % (self.action, err), 'red'), file=sys.stderr) - self.log_action(False, ' '.join([self.action] + args)) + self.log_action(False, self.action) else: - self.log_action(True, ' '.join([self.action] + args)) + self.log_action(True, self.action) def announce(self, message): ''' @@ -61,6 +68,10 @@ class Evolve: ''' print(termcolor.colored("\n! %s" % message, 'green')) + def announce_verbose(self, label, *args): + if self.arguments.verbose > 3: + print(termcolor.colored(label, 'magenta'), *args) + def call(self, *args): ''' Prints and runs a command, or piped series of commands, inheriting standard input/output and returning after completion. @@ -75,9 +86,15 @@ class Evolve: Runs a command, or piped series of commands, ignoring standard input/output and returning a returncode after completion. ''' command = self._normalize_commands(args) - # TODO: instead of subprocess.PIPE, should open & provide file descriptors (to temp files?) - # TODO: alternatively, use subprocess.Popen with Popen.communicate? - return subprocess.call(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + self.announce_verbose('Background process:', termcolor.colored("$ %s" % command, 'yellow')) + + proc = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output = proc.communicate() + + result = { 'stdout': output[0], 'returncode': proc.returncode } + self.announce_verbose('Background result:', result) + + return result def _normalize_commands(self, args): ''' @@ -145,11 +162,33 @@ class Evolve: built_prompt = "\n\a? %s: " % prompt return getpass.getpass(termcolor.colored(built_prompt, 'white', 'on_blue')) + def _extra_vars(self): + if self.arguments.extra_vars: + extra_vars = {} + for pair in self.arguments.extra_vars: + m = re.match(r'^([\w-]+)=([^=]+)$', pair) + if m: + extra_vars[m.group(1)] = m.group(2) + else: + try: + parsed = json.loads(pair) + except ValueError as err: + raise ValueError('%s: %s' % (err, pair)) + extra_vars.update(parsed) + self.arguments.extra_vars = extra_vars + def import_action(self, action): ''' - Imports an module (expected to be in the tasks path), and instantiates its ActionClass. + Imports an module (adding actions path to system paths if necessary) and returns it. + ''' + if self.actions_path not in sys.path: + sys.path.append(self.actions_path) + return __import__(action) + + def init_action(self, action_module): + ''' + Initializes an ActionClass from the given module. ''' - action_module = __import__(action) action_module.ActionClass(self) def playbook(self, playbook, extra_vars=None, extra_args=None): diff --git a/lib/actions/deploy.py b/lib/actions/deploy.py new file mode 100644 index 00000000..0735b506 --- /dev/null +++ b/lib/actions/deploy.py @@ -0,0 +1,27 @@ +import re +import subprocess +import sys + +class ActionClass: + def __init__(self, evolve): + extra_vars = { + 'stage': evolve.stage, + 'deploy_mode': 'deploy' + } + + # use branch from extra_vars, if given + if evolve.arguments.extra_vars and 'branch' in evolve.arguments.extra_vars: + extra_vars['branch'] = evolve.arguments.extra_vars['branch'] + # otherwise, for non-prod stages, infer currently checked out branch + elif evolve.stage != 'production': + result = evolve.bg_call(['git', 'branch']) + matched = re.search(r'^[*] (\S+)\s', result['stdout'], re.M) + if matched and matched.group(1) != 'master': + extra_vars['branch'] = matched.group(1) + + # infer git remote from currently checked out repo + git_remote = evolve.bg_call(['git', 'config', '--get', 'remote.origin.url']) + extra_vars['git_remote'] = git_remote['stdout'].strip(), + + # kick off ansistrano + evolve.playbook('deploy.yml', extra_vars, ['--user=deploy', '--limit=%s' % evolve.stage]) diff --git a/lib/tasks/provision.py b/lib/actions/provision.py similarity index 98% rename from lib/tasks/provision.py rename to lib/actions/provision.py index d256beb4..590c045b 100644 --- a/lib/tasks/provision.py +++ b/lib/actions/provision.py @@ -1,4 +1,3 @@ -import os.path import re import subprocess diff --git a/lib/actions/rollback.py b/lib/actions/rollback.py new file mode 100644 index 00000000..e2f50a75 --- /dev/null +++ b/lib/actions/rollback.py @@ -0,0 +1,13 @@ +import re +import subprocess +import sys + +class ActionClass: + def __init__(self, evolve): + extra_vars = { + 'stage': evolve.stage, + 'deploy_mode': 'rollback' + } + + # kick off ansistrano + evolve.playbook('deploy.yml', extra_vars, ['--user=deploy', '--limit=%s' % evolve.stage]) diff --git a/lib/ansistrano/after-update-code-tasks.yml b/lib/ansistrano/after-update-code-tasks.yml new file mode 100644 index 00000000..d700a7af --- /dev/null +++ b/lib/ansistrano/after-update-code-tasks.yml @@ -0,0 +1,5 @@ +--- +- name: Install bower dependencies + command: bower install --config.interactive=false + args: + chdir: "{{ ansistrano_release_path.stdout_lines[0] }}" diff --git a/lib/ansistrano/before-setup-tasks.yml b/lib/ansistrano/before-setup-tasks.yml new file mode 100644 index 00000000..041bf0dd --- /dev/null +++ b/lib/ansistrano/before-setup-tasks.yml @@ -0,0 +1,15 @@ +--- +- name: Use branch fom extra_vars, when explicitly provided + set_fact: + ansistrano_git_branch: "{{ branch }}" + when: branch is defined + +- name: Use git_remote from extra_vars, if the default was not overriden + set_fact: + ansistrano_git_repo: "{{ git_remote }}" + when: ansistrano_git_repo.find('USERNAME/REPO.git') != -1 + +- name: Clear deploy key for https remotes + set_fact: + ansistrano_git_identity_key_path: '' + when: ansistrano_git_repo.startswith('https://') diff --git a/lib/yeoman/templates/bin/evolve b/lib/yeoman/templates/bin/evolve index 60015852..1df52f73 100644 --- a/lib/yeoman/templates/bin/evolve +++ b/lib/yeoman/templates/bin/evolve @@ -5,4 +5,4 @@ IMPORTER_SCRIPT="$IMPORTER_PATH/bin/evolve" chmod +x "$IMPORTER_SCRIPT" -$IMPORTER_SCRIPT +$IMPORTER_SCRIPT $@ diff --git a/lib/yeoman/templates/lib/ansible/deploy.yml b/lib/yeoman/templates/lib/ansible/deploy.yml new file mode 100644 index 00000000..920c659b --- /dev/null +++ b/lib/yeoman/templates/lib/ansible/deploy.yml @@ -0,0 +1,6 @@ +--- +- hosts: "{{ stage }}" + gather_facts: yes + + roles: + - { role: "carlosbuenosvinos.ansistrano-{{ deploy_mode }}" } diff --git a/lib/yeoman/templates/lib/ansible/group_vars/all b/lib/yeoman/templates/lib/ansible/group_vars/all index 59c7517a..c0daf024 100644 --- a/lib/yeoman/templates/lib/ansible/group_vars/all +++ b/lib/yeoman/templates/lib/ansible/group_vars/all @@ -35,3 +35,13 @@ mail__postmaster: no-reply@<%= props.domain %> # swap__path: /swapfile # swap__swappiness: 10 # swap__vfs_cache_pressure: 50 + +ansistrano_deploy_to: "/var/www/{{ domain }}/{{ stage }}/{{ ansistrano_git_branch }}" +ansistrano_shared_paths: ['web/wp-content/uploads'] +ansistrano_keep_releases: 2 +ansistrano_deploy_via: "git" +ansistrano_allow_anonymous_stats: no +ansistrano_git_identity_key_path: "files/ssh/id_rsa" +ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/../../bower_components/evolution-wordpress/lib/ansistrano/before-setup-tasks.yml" +ansistrano_after_update_code_tasks_file: "{{ playbook_dir }}/../../bower_components/evolution-wordpress/lib/ansistrano/after-update-code-tasks.yml" +ansistrano_remove_rolled_back: yes From 5c1e80dde2a87d280faf373aa418adfbffcdbaf8 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Sat, 13 Aug 2016 12:42:40 -0700 Subject: [PATCH 04/11] Another round of `bin/evolve` retooling... * provision action prompts for user reprovision only in event of host failure (instead of ANY ansible failure) * now loads actions and playbooks from multiple paths, in both the project site and bower-installed evolution --- bin/evolve | 35 +++++++++++++++---- lib/actions/provision.py | 18 ++++++---- .../ansible => ansible/playbooks}/deploy.yml | 0 .../lib/ansible => ansible/playbooks}/log.yml | 0 4 files changed, 40 insertions(+), 13 deletions(-) rename lib/{yeoman/templates/lib/ansible => ansible/playbooks}/deploy.yml (100%) rename lib/{yeoman/templates/lib/ansible => ansible/playbooks}/log.yml (100%) diff --git a/bin/evolve b/bin/evolve index 70b0917d..77cf7919 100755 --- a/bin/evolve +++ b/bin/evolve @@ -29,8 +29,14 @@ class Evolve: # determine paths self.working_path = os.getcwd() - self.ansible_path = os.path.join(self.working_path, 'lib/ansible') - self.actions_path = os.path.join(os.path.dirname(__file__), '../lib/actions') + self.ansible_path = [ + os.path.join(self.working_path, 'lib/ansible'), + os.path.join(os.path.dirname(__file__), '../lib/ansible/playbooks'), + ] + self.actions_path = [ + os.path.join(self.working_path, 'lib/actions'), + os.path.join(os.path.dirname(__file__), '../lib/actions'), + ] self.announce_verbose('Paths:', { 'working_path': self.working_path, @@ -51,7 +57,11 @@ class Evolve: # install galaxy roles # todo: bypass --force unless roles are older than X? - self.call(['ansible-galaxy', 'install', '-r', os.path.join(self.ansible_path, 'galaxy.yml'), '--force']) + requirements_path = self.playbook_path('galaxy.yml') + if requirements_path: + self.call(['ansible-galaxy', 'install', '-r', requirements_path, '--force']) + else: + raise RuntimeError('Could not find requirements file %s in paths: %s' % (playbook, os.pathsep.join(self.ansible_path))) # import and instantiate action try: @@ -181,8 +191,9 @@ class Evolve: ''' Imports an module (adding actions path to system paths if necessary) and returns it. ''' - if self.actions_path not in sys.path: - sys.path.append(self.actions_path) + for path in self.actions_path: + if path not in sys.path: + sys.path.append(path) return __import__(action) def init_action(self, action_module): @@ -211,10 +222,22 @@ class Evolve: call_args += extra_args # append playbook filepath - call_args.append(os.path.join(self.ansible_path, playbook)) + playbook_path = self.playbook_path(playbook) + if playbook_path: + call_args.append(playbook_path) + else: + raise RuntimeError('Could not find playbook %s in paths: %s' % (playbook, os.pathsep.join(self.ansible_path))) self.call(call_args) + def playbook_path(self, playbook): + ''' + Finds the given playbook file in ansible paths, and returns it. + ''' + for path in self.ansible_path: + if os.path.isfile(os.path.join(path, playbook)): + return os.path.join(path, playbook) + def log_action(self, success, message): ''' Writes a log entry to a remote server, via a specialized ansible playbook. diff --git a/lib/actions/provision.py b/lib/actions/provision.py index 590c045b..53408a05 100644 --- a/lib/actions/provision.py +++ b/lib/actions/provision.py @@ -11,12 +11,16 @@ def __init__(self, evolve): try: evolve.playbook('provision.yml', extra_vars, '--user=deploy') - except subprocess.CalledProcessError: - evolve.announce('Unable to provision with SSH publickey for deploy user') + except subprocess.CalledProcessError as err: + # only prompt for user reprovisioning if host failed or unreachable + if any(x in err for x in ['returned non-zero exit status 3', 'returned non-zero exit status 2']): + evolve.announce('Unable to provision with SSH publickey for deploy user') - # connect as intermediate user, and set up deploy user - username = evolve.single_input('user to provision as (root)', lambda u: not u or re.match(r"^[a-z_][a-z0-9_-]*[$]?", u, re.I)) or 'root' - evolve.playbook('user.yml', extra_vars, ['--user=%s' % username, '--ask-pass', '--ask-sudo-pass']) + # connect as intermediate user, and set up deploy user + username = evolve.single_input('user to provision as (root)', lambda u: not u or re.match(r"^[a-z_][a-z0-9_-]*[$]?", u, re.I)) or 'root' + evolve.playbook('user.yml', extra_vars, ['--user=%s' % username, '--ask-pass', '--ask-sudo-pass']) - # provision as usual - evolve.playbook('provision.yml', extra_vars, '--user=deploy') + # provision as usual + evolve.playbook('provision.yml', extra_vars, '--user=deploy') + else: + raise err diff --git a/lib/yeoman/templates/lib/ansible/deploy.yml b/lib/ansible/playbooks/deploy.yml similarity index 100% rename from lib/yeoman/templates/lib/ansible/deploy.yml rename to lib/ansible/playbooks/deploy.yml diff --git a/lib/yeoman/templates/lib/ansible/log.yml b/lib/ansible/playbooks/log.yml similarity index 100% rename from lib/yeoman/templates/lib/ansible/log.yml rename to lib/ansible/playbooks/log.yml From dabed82ce3a6a693837acb3d0b7992ff262437bc Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Sun, 14 Aug 2016 10:40:33 -0700 Subject: [PATCH 05/11] Tweaks to `evolve` wrapper, `mock` wrapper, generator, and group vars In `evolve` wrapper: * resolving paths to absolute * using pre-initialized extra_vars dict in actions In `mock` wrapper, overriding repo url In generator, updated fixPermissions to ensure binaries are executable In group vars: * updated deploy path * fixed ansistrano before/after playbook paths --- bin/evolve | 33 +++++++++++++++---- bin/mock | 5 ++- lib/actions/deploy.py | 16 ++------- lib/actions/provision.py | 7 ++-- lib/actions/rollback.py | 6 ++-- lib/yeoman/index.js | 2 ++ .../templates/lib/ansible/group_vars/all | 6 ++-- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/bin/evolve b/bin/evolve index 77cf7919..8b3e61f2 100755 --- a/bin/evolve +++ b/bin/evolve @@ -30,12 +30,12 @@ class Evolve: # determine paths self.working_path = os.getcwd() self.ansible_path = [ - os.path.join(self.working_path, 'lib/ansible'), - os.path.join(os.path.dirname(__file__), '../lib/ansible/playbooks'), + os.path.abspath(os.path.join(self.working_path, 'lib/ansible')), + os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib/ansible/playbooks')), ] self.actions_path = [ - os.path.join(self.working_path, 'lib/actions'), - os.path.join(os.path.dirname(__file__), '../lib/actions'), + os.path.abspath(os.path.join(self.working_path, 'lib/actions')), + os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib/actions')), ] self.announce_verbose('Paths:', { @@ -173,8 +173,11 @@ class Evolve: return getpass.getpass(termcolor.colored(built_prompt, 'white', 'on_blue')) def _extra_vars(self): + ''' + Initializes a dictionary of extra variables, to be passed on to ansible. + ''' + extra_vars = {} if self.arguments.extra_vars: - extra_vars = {} for pair in self.arguments.extra_vars: m = re.match(r'^([\w-]+)=([^=]+)$', pair) if m: @@ -185,7 +188,25 @@ class Evolve: except ValueError as err: raise ValueError('%s: %s' % (err, pair)) extra_vars.update(parsed) - self.arguments.extra_vars = extra_vars + + extra_vars['stage'] = self.arguments.stage + + # infer branch currently checked out (for non prod stages), defaulting to master + if 'branch' not in extra_vars: + extra_vars['branch'] = 'master' + if extra_vars['stage'] != 'production': + result = self.bg_call(['git', 'branch']) + matched = re.search(r'^[*] (\S+)\s', result['stdout'], re.M) + if matched and matched.group(1) != 'master': + extra_vars['branch'] = matched.group(1) + + self.arguments.extra_vars = extra_vars + + def get_extra_vars(self): + ''' + Returns a copy of the extra_vars dictionary. + ''' + return self.arguments.extra_vars.copy() def import_action(self, action): ''' diff --git a/bin/mock b/bin/mock index b566066a..b0216f28 100755 --- a/bin/mock +++ b/bin/mock @@ -77,10 +77,9 @@ helpers.testDirectory(outputDir, function(err) { fs.writeFileSync(certPath, cert); } - fs.appendFileSync(outputDir + '/lib/capistrano/deploy.rb', [ + fs.appendFileSync(outputDir + '/lib/ansible/group_vars/all', [ '', - '# Added for testing purposes', - 'set :repo_url, ' + ( + 'ansistrano_git_repo: ' + ( isCI ? '"file:///vagrant/.git"' : '"https://github.com/evolution/wordpress-example.git"' diff --git a/lib/actions/deploy.py b/lib/actions/deploy.py index 0735b506..9d74c25f 100644 --- a/lib/actions/deploy.py +++ b/lib/actions/deploy.py @@ -4,20 +4,8 @@ class ActionClass: def __init__(self, evolve): - extra_vars = { - 'stage': evolve.stage, - 'deploy_mode': 'deploy' - } - - # use branch from extra_vars, if given - if evolve.arguments.extra_vars and 'branch' in evolve.arguments.extra_vars: - extra_vars['branch'] = evolve.arguments.extra_vars['branch'] - # otherwise, for non-prod stages, infer currently checked out branch - elif evolve.stage != 'production': - result = evolve.bg_call(['git', 'branch']) - matched = re.search(r'^[*] (\S+)\s', result['stdout'], re.M) - if matched and matched.group(1) != 'master': - extra_vars['branch'] = matched.group(1) + extra_vars = evolve.get_extra_vars() + extra_vars['deploy_mode'] = 'deploy' # infer git remote from currently checked out repo git_remote = evolve.bg_call(['git', 'config', '--get', 'remote.origin.url']) diff --git a/lib/actions/provision.py b/lib/actions/provision.py index 53408a05..f1d80722 100644 --- a/lib/actions/provision.py +++ b/lib/actions/provision.py @@ -4,16 +4,13 @@ class ActionClass: def __init__(self, evolve): # TODO: implement `invoke "evolve:prepare_key"` - - extra_vars = { - 'stage': evolve.stage - } + extra_vars = evolve.get_extra_vars() try: evolve.playbook('provision.yml', extra_vars, '--user=deploy') except subprocess.CalledProcessError as err: # only prompt for user reprovisioning if host failed or unreachable - if any(x in err for x in ['returned non-zero exit status 3', 'returned non-zero exit status 2']): + if 'returned non-zero exit status 3' in str(err): evolve.announce('Unable to provision with SSH publickey for deploy user') # connect as intermediate user, and set up deploy user diff --git a/lib/actions/rollback.py b/lib/actions/rollback.py index e2f50a75..85f6ba33 100644 --- a/lib/actions/rollback.py +++ b/lib/actions/rollback.py @@ -4,10 +4,8 @@ class ActionClass: def __init__(self, evolve): - extra_vars = { - 'stage': evolve.stage, - 'deploy_mode': 'rollback' - } + extra_vars = evolve.get_extra_vars() + extra_vars['deploy_mode'] = 'rollback' # kick off ansistrano evolve.playbook('deploy.yml', extra_vars, ['--user=deploy', '--limit=%s' % evolve.stage]) diff --git a/lib/yeoman/index.js b/lib/yeoman/index.js index 308f2c6e..9f09972c 100644 --- a/lib/yeoman/index.js +++ b/lib/yeoman/index.js @@ -700,6 +700,8 @@ var WordpressGenerator = yeoman.generators.Base.extend({ fixPermissions: function() { fs.chmodSync(path.join(this.env.cwd, 'lib', 'ansible', 'files', 'ssh', 'id_rsa'), '600'); + fs.chmodSync(path.join(this.env.cwd, 'bin', 'evolve'), '755'); + fs.chmodSync(path.join(this.env.cwd, 'bin', 'import'), '755'); }, installGems: function() { diff --git a/lib/yeoman/templates/lib/ansible/group_vars/all b/lib/yeoman/templates/lib/ansible/group_vars/all index c0daf024..fe613013 100644 --- a/lib/yeoman/templates/lib/ansible/group_vars/all +++ b/lib/yeoman/templates/lib/ansible/group_vars/all @@ -36,12 +36,12 @@ mail__postmaster: no-reply@<%= props.domain %> # swap__swappiness: 10 # swap__vfs_cache_pressure: 50 -ansistrano_deploy_to: "/var/www/{{ domain }}/{{ stage }}/{{ ansistrano_git_branch }}" +ansistrano_deploy_to: "/var/www/{{ domain }}/{{ stage }}/{{ branch }}" ansistrano_shared_paths: ['web/wp-content/uploads'] ansistrano_keep_releases: 2 ansistrano_deploy_via: "git" ansistrano_allow_anonymous_stats: no ansistrano_git_identity_key_path: "files/ssh/id_rsa" -ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/../../bower_components/evolution-wordpress/lib/ansistrano/before-setup-tasks.yml" -ansistrano_after_update_code_tasks_file: "{{ playbook_dir }}/../../bower_components/evolution-wordpress/lib/ansistrano/after-update-code-tasks.yml" +ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/../../ansistrano/before-setup-tasks.yml" +ansistrano_after_update_code_tasks_file: "{{ playbook_dir }}/../../ansistrano/after-update-code-tasks.yml" ansistrano_remove_rolled_back: yes From e20771c0427d990105a3a18257db35b0a520344d Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Mon, 15 Aug 2016 18:27:33 -0700 Subject: [PATCH 06/11] Updated gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 50ac54ee..bdaf6e12 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ test/temp/ vendor/ composer.phar *.pyc +lib/ansible/playbooks/*.retry +lib/ansible/roles/carlosbuenosvinos.ansistrano-* From 760c81e289fe1871d6575ad75489ac33e65c309b Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Wed, 31 Aug 2016 10:34:23 -0700 Subject: [PATCH 07/11] Load any group_vars available into evolve wrapper --- bin/evolve | 18 ++++++++++++++++++ .../templates/lib/ansible/group_vars/all | 2 ++ .../templates/lib/ansible/group_vars/local | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 lib/yeoman/templates/lib/ansible/group_vars/local diff --git a/bin/evolve b/bin/evolve index 8b3e61f2..2ed88d58 100755 --- a/bin/evolve +++ b/bin/evolve @@ -13,6 +13,7 @@ import socket import subprocess import sys import termcolor +import yaml class Evolve: def __init__(self): @@ -48,6 +49,9 @@ class Evolve: self.stage = self.arguments.stage self.action = self.arguments.action + # load ansible group_vars/all and group_vars/{{stage}} (if exists) + self.load_group_vars() + self.announce('Loading "%s" action in the "%s" stage' % (self.action, self.stage)) try: action_module = self.import_action(self.action) @@ -208,6 +212,20 @@ class Evolve: ''' return self.arguments.extra_vars.copy() + def load_group_vars(self): + self.group_vars = {} + vars_files = [ + os.path.join(self.ansible_path[0], 'group_vars/all'), + os.path.join(self.ansible_path[0], 'group_vars/%s' % self.stage), + ] + + for vars_file in vars_files: + if os.path.isfile(vars_file): + f = open(vars_file) + loaded = yaml.safe_load(f) + f.close() + self.group_vars.update(loaded) + def import_action(self, action): ''' Imports an module (adding actions path to system paths if necessary) and returns it. diff --git a/lib/yeoman/templates/lib/ansible/group_vars/all b/lib/yeoman/templates/lib/ansible/group_vars/all index fe613013..e8134464 100644 --- a/lib/yeoman/templates/lib/ansible/group_vars/all +++ b/lib/yeoman/templates/lib/ansible/group_vars/all @@ -45,3 +45,5 @@ ansistrano_git_identity_key_path: "files/ssh/id_rsa" ansistrano_before_setup_tasks_file: "{{ playbook_dir }}/../../ansistrano/before-setup-tasks.yml" ansistrano_after_update_code_tasks_file: "{{ playbook_dir }}/../../ansistrano/after-update-code-tasks.yml" ansistrano_remove_rolled_back: yes + +wp_path: "{{ ansistrano_deploy_to }}/current/web/wp" diff --git a/lib/yeoman/templates/lib/ansible/group_vars/local b/lib/yeoman/templates/lib/ansible/group_vars/local new file mode 100644 index 00000000..9c7fdb82 --- /dev/null +++ b/lib/yeoman/templates/lib/ansible/group_vars/local @@ -0,0 +1,2 @@ +--- +wp_path: "/vagrant/web/wp" From cd19299859658d83ddeb640f9e1c5ac110738b74 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Wed, 31 Aug 2016 10:38:09 -0700 Subject: [PATCH 08/11] Added ability to load actions nested within packages * load actions with importlib instead of __import__ * preserve raw sys args --- bin/evolve | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/bin/evolve b/bin/evolve index 2ed88d58..6ef87bdc 100755 --- a/bin/evolve +++ b/bin/evolve @@ -4,6 +4,7 @@ from __future__ import print_function import argparse import datetime import getpass +import importlib import json import os import os.path @@ -17,6 +18,8 @@ import yaml class Evolve: def __init__(self): + raw_args = list(sys.argv) + # handle arguments & flags parser = argparse.ArgumentParser(prog='./bin/evolve') parser.add_argument('stage', help='environment against which to run given action') @@ -26,6 +29,7 @@ class Evolve: self.arguments = parser.parse_args() self._extra_vars() + self.announce_verbose('Raw args:', raw_args) self.announce_verbose('Parsed args:', self.arguments) # determine paths @@ -72,9 +76,9 @@ class Evolve: self.init_action(action_module) except Exception as err: print(termcolor.colored('The "%s" action failed: %s' % (self.action, err), 'red'), file=sys.stderr) - self.log_action(False, self.action) + self.log_action(False, ' '.join(raw_args[1:])) else: - self.log_action(True, self.action) + self.log_action(True, ' '.join(raw_args[1:])) def announce(self, message): ''' @@ -233,7 +237,20 @@ class Evolve: for path in self.actions_path: if path not in sys.path: sys.path.append(path) - return __import__(action) + + if '.' in action: + package, module = action.rsplit('.', 1) + module = '.%s' % module + else: + module = action + package = None + + self.announce_verbose('From package %s %s' % ( + termcolor.colored(package, 'yellow'), + termcolor.colored(module, 'green') + )) + + return importlib.import_module(module, package) def init_action(self, action_module): ''' @@ -241,7 +258,7 @@ class Evolve: ''' action_module.ActionClass(self) - def playbook(self, playbook, extra_vars=None, extra_args=None): + def playbook(self, playbook, extra_vars=None, extra_args=None, bg=False): ''' Runs an ansible playbook, with optional json encoded extra_vars and optional extra arguments. ''' @@ -267,7 +284,10 @@ class Evolve: else: raise RuntimeError('Could not find playbook %s in paths: %s' % (playbook, os.pathsep.join(self.ansible_path))) - self.call(call_args) + if bg: + self.bg_call(call_args) + else: + self.call(call_args) def playbook_path(self, playbook): ''' @@ -292,7 +312,7 @@ class Evolve: } try: - self.playbook('log.yml', extra_vars) + self.playbook('log.yml', extra_vars, bg=True) except subprocess.CalledProcessError as err: print(termcolor.colored('Failed to log action to remote: %s' % err, 'red'), file=sys.stderr) From e45d69357781f20b00c4e5f3b860ee38a1d53df9 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Wed, 31 Aug 2016 10:41:43 -0700 Subject: [PATCH 09/11] Added ssh action... * added optional command arg * added method for exec()ing a new process * fixed passing of args from bash to python --- bin/evolve | 18 ++++++++++++++++++ lib/actions/ssh.py | 26 ++++++++++++++++++++++++++ lib/yeoman/templates/bin/evolve | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 lib/actions/ssh.py diff --git a/bin/evolve b/bin/evolve index 6ef87bdc..267172fc 100755 --- a/bin/evolve +++ b/bin/evolve @@ -24,6 +24,7 @@ class Evolve: parser = argparse.ArgumentParser(prog='./bin/evolve') parser.add_argument('stage', help='environment against which to run given action') parser.add_argument('action', help='action to run against given stage') + parser.add_argument('command', nargs='?', default=None, help='optional (string quoted) subcommand for given action') parser.add_argument('-v','--verbose', action='count', help='increase verbosity') parser.add_argument('-e','--extra-vars', action='append', help='provide key=value pairs or JSON document') self.arguments = parser.parse_args() @@ -114,6 +115,23 @@ class Evolve: return result + def exec_process(self, command): + executable = command[0] + + if os.access(executable, os.X_OK): + resolved = executable + else: + for path in os.environ['PATH'].split(os.pathsep): + joined = os.path.join(path, executable) + if os.access(joined, os.X_OK): + resolved = joined + break + + if resolved: + os.execv(resolved, command) + else: + raise RuntimeError('Could not find %s in PATH %s' % (executable, os.environ['PATH'])) + def _normalize_commands(self, args): ''' Normalizes and escapes one or more commands (as lists), piping each into the command following it. diff --git a/lib/actions/ssh.py b/lib/actions/ssh.py new file mode 100644 index 00000000..5c2fa568 --- /dev/null +++ b/lib/actions/ssh.py @@ -0,0 +1,26 @@ +import datetime +import os +import os.path +import re +import subprocess +import sys + +class ActionClass: + def __init__(self, evolve): + extra_vars = evolve.get_extra_vars() + + command = [ + 'ssh', + '-i', + os.path.join(evolve.ansible_path[0], 'files/ssh/id_rsa'), + 'deploy@%s.%s' % (extra_vars['stage'], evolve.group_vars['domain']), + ] + + if evolve.arguments.command: + command.append(evolve.arguments.command) + try: + evolve.call(command) + except subprocess.CalledProcessError as err: + evolve.announce(err) + else: + evolve.exec_process(command) diff --git a/lib/yeoman/templates/bin/evolve b/lib/yeoman/templates/bin/evolve index 1df52f73..34744671 100644 --- a/lib/yeoman/templates/bin/evolve +++ b/lib/yeoman/templates/bin/evolve @@ -5,4 +5,4 @@ IMPORTER_SCRIPT="$IMPORTER_PATH/bin/evolve" chmod +x "$IMPORTER_SCRIPT" -$IMPORTER_SCRIPT $@ +$IMPORTER_SCRIPT "$@" From 113c01eee13d900a97f0c355814973c283592f1a Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Sun, 4 Sep 2016 23:03:44 -0700 Subject: [PATCH 10/11] Added wp action, and misc cleanup --- bin/evolve | 3 ++- lib/actions/ssh.py | 7 ++----- lib/actions/wp.py | 24 +++++++++++++++++++++++ lib/yeoman/templates/_gitignore | 1 + lib/yeoman/templates/lib/actions/_gitkeep | 0 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 lib/actions/wp.py create mode 100644 lib/yeoman/templates/lib/actions/_gitkeep diff --git a/bin/evolve b/bin/evolve index 267172fc..24a8df45 100755 --- a/bin/evolve +++ b/bin/evolve @@ -53,6 +53,7 @@ class Evolve: # expose positional arguments for simpler dependency injection self.stage = self.arguments.stage self.action = self.arguments.action + self.command = self.arguments.command # load ansible group_vars/all and group_vars/{{stage}} (if exists) self.load_group_vars() @@ -250,7 +251,7 @@ class Evolve: def import_action(self, action): ''' - Imports an module (adding actions path to system paths if necessary) and returns it. + Imports a module (adding actions path to system paths if necessary) and returns it. ''' for path in self.actions_path: if path not in sys.path: diff --git a/lib/actions/ssh.py b/lib/actions/ssh.py index 5c2fa568..e7e6f3fb 100644 --- a/lib/actions/ssh.py +++ b/lib/actions/ssh.py @@ -1,9 +1,6 @@ -import datetime import os import os.path -import re import subprocess -import sys class ActionClass: def __init__(self, evolve): @@ -16,8 +13,8 @@ def __init__(self, evolve): 'deploy@%s.%s' % (extra_vars['stage'], evolve.group_vars['domain']), ] - if evolve.arguments.command: - command.append(evolve.arguments.command) + if evolve.command: + command.append(evolve.command) try: evolve.call(command) except subprocess.CalledProcessError as err: diff --git a/lib/actions/wp.py b/lib/actions/wp.py new file mode 100644 index 00000000..1d299803 --- /dev/null +++ b/lib/actions/wp.py @@ -0,0 +1,24 @@ +import datetime +import os +import os.path +import re +import subprocess +import sys + +class ActionClass: + def __init__(self, evolve): + extra_vars = evolve.get_extra_vars() + + if not evolve.command: + raise RuntimeError('wp action requires a command string'); + + ssh_module = evolve.import_action('ssh') + + evolve.command = ' '.join([ + '/usr/local/bin/wp', + evolve.command, + '--path="%s"' % evolve.group_vars['wp_path'], + '--url="http://%s.%s/"' % (extra_vars['stage'], evolve.group_vars['domain']), + ]) + + evolve.init_action(ssh_module) diff --git a/lib/yeoman/templates/_gitignore b/lib/yeoman/templates/_gitignore index 970ad645..44721816 100644 --- a/lib/yeoman/templates/_gitignore +++ b/lib/yeoman/templates/_gitignore @@ -6,6 +6,7 @@ lib/ansible/files/ssl/ <% } %> # Application files error_log +lib/ansible/*.retry # WordPress files **/wp-content/cache/** diff --git a/lib/yeoman/templates/lib/actions/_gitkeep b/lib/yeoman/templates/lib/actions/_gitkeep new file mode 100644 index 00000000..e69de29b From c85273f6f84be4bd02bae45ccd18ddb054523451 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Tue, 6 Sep 2016 20:39:16 -0700 Subject: [PATCH 11/11] Added version action, and misc cleanup --- lib/actions/version.py | 17 +++++++++++++++++ lib/actions/wp.py | 7 +------ lib/ansible/playbooks/version.yml | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 lib/actions/version.py create mode 100644 lib/ansible/playbooks/version.yml diff --git a/lib/actions/version.py b/lib/actions/version.py new file mode 100644 index 00000000..32bcbc6e --- /dev/null +++ b/lib/actions/version.py @@ -0,0 +1,17 @@ +import json +import os +import os.path + +class ActionClass: + def __init__(self, evolve): + extra_vars = evolve.get_extra_vars() + + # inject generated version (from group vars) + extra_vars['generated_version'] = evolve.group_vars['evolve_version'] + + # inject installed version (from bower_components) + with open(os.path.join(evolve.working_path, 'bower_components/evolution-wordpress/package.json'), 'r') as package_json: + extra_vars['local_version'] = json.load(package_json)['version'] + + # call playbook to gather and display all three versions + evolve.playbook('version.yml', extra_vars=extra_vars) diff --git a/lib/actions/wp.py b/lib/actions/wp.py index 1d299803..45508e72 100644 --- a/lib/actions/wp.py +++ b/lib/actions/wp.py @@ -1,9 +1,3 @@ -import datetime -import os -import os.path -import re -import subprocess -import sys class ActionClass: def __init__(self, evolve): @@ -19,6 +13,7 @@ def __init__(self, evolve): evolve.command, '--path="%s"' % evolve.group_vars['wp_path'], '--url="http://%s.%s/"' % (extra_vars['stage'], evolve.group_vars['domain']), + '--color', ]) evolve.init_action(ssh_module) diff --git a/lib/ansible/playbooks/version.yml b/lib/ansible/playbooks/version.yml new file mode 100644 index 00000000..56fe6a75 --- /dev/null +++ b/lib/ansible/playbooks/version.yml @@ -0,0 +1,17 @@ +--- +- hosts: "{{ stage }}" + + tasks: + - name: Retrieve service version + command: service evolution-wordpress version + register: provisioned_version + sudo: yes + + - debug: + msg: "Last generated with: {{generated_version}}" + + - debug: + msg: "Currently bower installed with: {{local_version}}" + + - debug: + msg: "Stage '{{stage}}' last provisioned with: {{provisioned_version.stdout|regex_replace('\\A[\\s\\S]*?Evolution Wordpress ([\\w.-]+)[\\s\\S]*?\\Z', '\\1')}}"