From 8d25b306f55481d295061f50c3face729f1117a3 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Mon, 30 Sep 2019 13:30:00 +0100 Subject: [PATCH] Fall back to regexes in kolla_toolbox with Docker API 1.24 Since https://opendev.org/openstack/kolla-ansible/commit/70b515bf1225e56b7df81677043d75be4bbb1ab4 was merged, we implicitly require Docker API version 1.25 (https://docs.docker.com/engine/api/v1.25/) to support passing environment variables to docker exec. The version of docker we deployed before the Docker CE upgrade was 1.12.0, which is Docker API version 1.24, and so does not support this. We get the following error: Setting environment for exec is not supported in API < 1.25 This change modifies the kolla_toolbox module to use the new JSON method for parsing Ansible's output when Docker API 1.25 is available, falling back to the old regex-based method otherwise. This change can be reverted when we require a minimum Docker API version of 1.25+. Change-Id: Ie671624ecca5b43d7bd8fbd959d701d9e21d66b3 Closes-Bug: #1845681 --- ansible/library/kolla_toolbox.py | 118 ++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/ansible/library/kolla_toolbox.py b/ansible/library/kolla_toolbox.py index 8873ea2156..4d9dd1c02f 100644 --- a/ansible/library/kolla_toolbox.py +++ b/ansible/library/kolla_toolbox.py @@ -14,8 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from distutils.version import StrictVersion import docker import json +import re from ansible.module_utils.basic import AnsibleModule @@ -89,6 +91,13 @@ EXAMPLES = ''' ''' +JSON_REG = re.compile('^(?P\w+) \| (?P\w+)!? =>(?P.*)$', + re.MULTILINE | re.DOTALL) +NON_JSON_REG = re.compile(('^(?P\w+) \| (?P\w+)!? \| ' + 'rc=(?P\d+) >>\n(?P.*)\n$'), + re.MULTILINE | re.DOTALL) + + def gen_commandline(params): command = ['ansible', 'localhost'] if params.get('module_name'): @@ -111,6 +120,11 @@ def get_docker_client(): return docker.APIClient +def docker_supports_environment_in_exec(client): + docker_version = StrictVersion(client.api_version) + return docker_version >= StrictVersion('1.25') + + def main(): specs = dict( module_name=dict(required=True, type='str'), @@ -130,43 +144,79 @@ def main(): module.fail_json(msg='kolla_toolbox container is not running.') kolla_toolbox = kolla_toolbox[0] - # Use the JSON output formatter, so that we can parse it. - environment = {"ANSIBLE_STDOUT_CALLBACK": "json", - "ANSIBLE_LOAD_CALLBACK_PLUGINS": "True"} - job = client.exec_create(kolla_toolbox, command_line, - environment=environment) - json_output = client.exec_start(job) - try: - output = json.loads(json_output) - except Exception as e: - module.fail_json( - msg='Can not parse the inner module output: %s' % json_output) + # NOTE(mgoddard): Docker 1.12 has API version 1.24, and was installed by + # kolla-ansible bootstrap-servers on Rocky and earlier releases. This API + # version does not have support for specifying environment variables for + # exec jobs, which is necessary to use the Ansible JSON output formatter. + # While we continue to support this version of Docker, fall back to the old + # regex-based method for API version 1.24 and earlier. + # TODO(mgoddard): Remove this conditional (keep the if) when we require + # Docker API version 1.25+. + if docker_supports_environment_in_exec(client): + # Use the JSON output formatter, so that we can parse it. + environment = {"ANSIBLE_STDOUT_CALLBACK": "json", + "ANSIBLE_LOAD_CALLBACK_PLUGINS": "True"} + job = client.exec_create(kolla_toolbox, command_line, + environment=environment) + json_output = client.exec_start(job) - # Expected format is the following: - # { - # "plays": [ - # { - # "tasks": [ - # { - # "hosts": { - # "localhost": { - # - # } - # } - # } - # ] - # { - # ] - # } - try: - ret = output['plays'][0]['tasks'][0]['hosts']['localhost'] - except (KeyError, IndexError) as e: - module.fail_json( - msg='Ansible JSON output has unexpected format: %s' % output) + try: + output = json.loads(json_output) + except Exception as e: + module.fail_json( + msg='Can not parse the inner module output: %s' % json_output) - # Remove Ansible's internal variables from returned fields. - ret.pop('_ansible_no_log', None) + # Expected format is the following: + # { + # "plays": [ + # { + # "tasks": [ + # { + # "hosts": { + # "localhost": { + # + # } + # } + # } + # ] + # { + # ] + # } + try: + ret = output['plays'][0]['tasks'][0]['hosts']['localhost'] + except (KeyError, IndexError) as e: + module.fail_json( + msg='Ansible JSON output has unexpected format: %s' % output) + + # Remove Ansible's internal variables from returned fields. + ret.pop('_ansible_no_log', None) + else: + job = client.exec_create(kolla_toolbox, command_line) + output = client.exec_start(job) + + for exp in [JSON_REG, NON_JSON_REG]: + m = exp.match(output) + if m: + inner_output = m.groupdict().get('stdout') + status = m.groupdict().get('status') + break + else: + module.fail_json( + msg='Can not parse the inner module output: %s' % output) + + ret = dict() + try: + ret = json.loads(inner_output) + except ValueError: + # Some modules (e.g. command) do not produce a JSON output. + # Instead, check the status, and assume changed on success. + ret['stdout'] = inner_output + if status != "SUCCESS": + ret['failed'] = True + else: + # No way to know whether changed - assume yes. + ret['changed'] = True module.exit_json(**ret)