|
| 1 | +import contextlib |
| 2 | +import json |
| 3 | +import os.path |
| 4 | +import uuid |
| 5 | + |
| 6 | +from rrmngmnt.common import CommandReader |
| 7 | +from rrmngmnt.resource import Resource |
| 8 | +from rrmngmnt.service import Service |
| 9 | + |
| 10 | + |
| 11 | +class PlaybookRunner(Service): |
| 12 | + """ |
| 13 | + Class for working with and especially executing Ansible playbooks on hosts. |
| 14 | + On your Host instance, it might be accessed (similar to other services) by |
| 15 | + playbook property. |
| 16 | +
|
| 17 | + Example: |
| 18 | + rc,out, err = my_host.playbook.run('long_task.yml') |
| 19 | +
|
| 20 | + In such case, the default logger of this class (called PlaybookRunner) will |
| 21 | + be used to log playbook's output. It will propagate events to handlers of |
| 22 | + ancestor loggers. However, PlaybookRunner might also be directly |
| 23 | + instantiated with instance of logging.Logger passed to logger parameter. |
| 24 | +
|
| 25 | + Example: |
| 26 | + my_runner = PlaybookRunner(my_host, logging.getLogger('playbook')) |
| 27 | + rc, out, err = my_runner.run('long_task.yml') |
| 28 | +
|
| 29 | + In that case, custom provided logger will be used instead. In both cases, |
| 30 | + each log record will be prefixed with UUID generated specifically for that |
| 31 | + one playbook execution. |
| 32 | + """ |
| 33 | + class LoggerAdapter(Resource.LoggerAdapter): |
| 34 | + |
| 35 | + def process(self, msg, kwargs): |
| 36 | + return "[%s] %s" % (self.extra['self'].short_run_uuid, msg), kwargs |
| 37 | + |
| 38 | + tmp_dir = "/tmp" |
| 39 | + binary = "ansible-playbook" |
| 40 | + extra_vars_file = "extra_vars.json" |
| 41 | + default_inventory_name = "inventory" |
| 42 | + default_inventory_content = "localhost ansible_connection=local" |
| 43 | + ssh_common_args_param = "--ssh-common-args" |
| 44 | + check_mode_param = "--check" |
| 45 | + |
| 46 | + def __init__(self, host, logger=None): |
| 47 | + """ |
| 48 | + Args: |
| 49 | + host (rrmngmnt.Host): Underlying host for this service |
| 50 | + logger (logging.Logger): Alternate logger for Ansible output |
| 51 | + """ |
| 52 | + super(PlaybookRunner, self).__init__(host) |
| 53 | + if logger: |
| 54 | + self.set_logger(logger) |
| 55 | + self.run_uuid = uuid.uuid4() |
| 56 | + self.short_run_uuid = str(self.run_uuid).split('-')[0] |
| 57 | + self.tmp_exec_dir = None |
| 58 | + self.cmd = [self.binary] |
| 59 | + self.rc = None |
| 60 | + self.out = None |
| 61 | + self.err = None |
| 62 | + |
| 63 | + @contextlib.contextmanager |
| 64 | + def _exec_dir(self): |
| 65 | + """ |
| 66 | + Context manager that makes sure that for each execution of playbook, |
| 67 | + temporary directory (whose name is the same as run's UUID) is created |
| 68 | + on the host and removed afterwards. |
| 69 | + """ |
| 70 | + exec_dir_path = os.path.join(self.tmp_dir, self.short_run_uuid) |
| 71 | + self.host.fs.rmdir(exec_dir_path) |
| 72 | + self.host.fs.mkdir(exec_dir_path) |
| 73 | + self.tmp_exec_dir = exec_dir_path |
| 74 | + try: |
| 75 | + yield |
| 76 | + finally: |
| 77 | + self.tmp_exec_dir = None |
| 78 | + self.host.fs.rmdir(exec_dir_path) |
| 79 | + |
| 80 | + def _upload_file(self, file_): |
| 81 | + file_path_on_host = os.path.join( |
| 82 | + self.tmp_exec_dir, os.path.basename(file_) |
| 83 | + ) |
| 84 | + self.host.fs.put(path_src=file_, path_dst=file_path_on_host) |
| 85 | + return file_path_on_host |
| 86 | + |
| 87 | + def _dump_vars_to_json_file(self, vars_): |
| 88 | + file_path_on_host = os.path.join( |
| 89 | + self.tmp_exec_dir, self.extra_vars_file |
| 90 | + ) |
| 91 | + self.host.fs.create_file( |
| 92 | + content=json.dumps(vars_), path=file_path_on_host |
| 93 | + ) |
| 94 | + return file_path_on_host |
| 95 | + |
| 96 | + def _generate_default_inventory(self): |
| 97 | + file_path_on_host = os.path.join( |
| 98 | + self.tmp_exec_dir, self.default_inventory_name |
| 99 | + ) |
| 100 | + self.host.fs.create_file( |
| 101 | + content=self.default_inventory_content, |
| 102 | + path=file_path_on_host |
| 103 | + ) |
| 104 | + return file_path_on_host |
| 105 | + |
| 106 | + def run( |
| 107 | + self, playbook, extra_vars=None, vars_files=None, inventory=None, |
| 108 | + verbose_level=1, run_in_check_mode=False, ssh_common_args=None, |
| 109 | + ): |
| 110 | + """ |
| 111 | + Run Ansible playbook on host |
| 112 | +
|
| 113 | + Args: |
| 114 | + playbook (str): Path to playbook you want to execute (on your |
| 115 | + machine) |
| 116 | + extra_vars (dict): Dictionary of extra variables that are to be |
| 117 | + passed to playbook execution. They will be dumped to JSON file |
| 118 | + and included using -e@ parameter |
| 119 | + vars_files (list): List of additional variable files to be included |
| 120 | + using -e@ parameter. If one variable is specified both in |
| 121 | + extra_vars and in one of the vars_files, the one in vars_files |
| 122 | + takes precedence. |
| 123 | + inventory (str): Path to an inventory file (on your machine) to be |
| 124 | + used for playbook execution. If none is provided, default |
| 125 | + inventory including only localhost will be generated and used |
| 126 | + verbose_level (int): How much should playbook be verbose. Possible |
| 127 | + values are 1 through 5 with 1 being the most quiet and 5 being |
| 128 | + the most verbose |
| 129 | + run_in_check_mode (bool): If True, playbook will not actually be |
| 130 | + executed, but instead run with --check parameter |
| 131 | + ssh_common_args (list): List of options that will extend (not |
| 132 | + replace) the list of default options that Ansible uses when |
| 133 | + calling ssh/sftp/scp. Example: ["-o StrictHostKeyChecking=no", |
| 134 | + "-o UserKnownHostsFile=/dev/null"] |
| 135 | +
|
| 136 | + Returns: |
| 137 | + tuple: tuple of (rc, out, err) |
| 138 | + """ |
| 139 | + self.logger.info( |
| 140 | + "Running playbook {} on {}".format( |
| 141 | + os.path.basename(playbook), |
| 142 | + self.host.fqdn |
| 143 | + ) |
| 144 | + ) |
| 145 | + |
| 146 | + with self._exec_dir(): |
| 147 | + |
| 148 | + if extra_vars: |
| 149 | + self.cmd.append( |
| 150 | + "-e@{}".format(self._dump_vars_to_json_file(extra_vars)) |
| 151 | + ) |
| 152 | + |
| 153 | + if vars_files: |
| 154 | + for f in vars_files: |
| 155 | + self.cmd.append("-e@{}".format(self._upload_file(f))) |
| 156 | + |
| 157 | + self.cmd.append("-i") |
| 158 | + if inventory: |
| 159 | + self.cmd.append(self._upload_file(inventory)) |
| 160 | + else: |
| 161 | + self.cmd.append(self._generate_default_inventory()) |
| 162 | + |
| 163 | + self.cmd.append("-{}".format("v" * verbose_level)) |
| 164 | + |
| 165 | + if run_in_check_mode: |
| 166 | + self.cmd.append(self.check_mode_param) |
| 167 | + |
| 168 | + if ssh_common_args: |
| 169 | + self.cmd.append( |
| 170 | + "{}={}".format( |
| 171 | + self.ssh_common_args_param, " ".join(ssh_common_args) |
| 172 | + ) |
| 173 | + ) |
| 174 | + |
| 175 | + self.cmd.append(self._upload_file(playbook)) |
| 176 | + |
| 177 | + self.logger.debug("Executing: {}".format(" ".join(self.cmd))) |
| 178 | + |
| 179 | + playbook_reader = CommandReader(self.host.executor(), self.cmd) |
| 180 | + for line in playbook_reader.read_lines(): |
| 181 | + self.logger.debug(line) |
| 182 | + self.rc, self.out, self.err = ( |
| 183 | + playbook_reader.rc, |
| 184 | + playbook_reader.out, |
| 185 | + playbook_reader.err |
| 186 | + ) |
| 187 | + |
| 188 | + self.logger.debug( |
| 189 | + "Ansible playbook finished with RC: {}".format(self.rc) |
| 190 | + ) |
| 191 | + |
| 192 | + return self.rc, self.out, self.err |
0 commit comments