kolla-ansible/ansible/library/kolla_toolbox.py
Mark Goddard 28273411a8 Fix kolla_toolbox error handling
Backport only.

The kolla_toolbox Ansible module executes as-hoc ansible commands in the
kolla_toolbox container, and parses the output to make it look as if
ansible-playbook executed the command. Currently however, this module
sometimes fails to catch failures of the underlying command, and also
sometimes shows tasks as 'ok' when the underlying command was changed.

This change introduces a check on the status field for non-JSON output,
and assumes the module has changes on success.

NOTE: The original patch on master
(https://review.opendev.org/#/c/682340/) cannot be backported as it
relies on Ansible 2.5+ in the kolla_toolbox container.

Change-Id: Ib1e33f8e5c60d98b76a4eba9f405af89431d60bc
Closes-Bug: 1844114
2019-09-24 16:32:22 +00:00

172 lines
5.2 KiB
Python

#!/usr/bin/env python
# Copyright 2016 99cloud Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import docker
import json
import re
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = '''
---
module: kolla_toolbox
short_description: >
Module for invoking ansible module in kolla_toolbox container.
description:
- A module targerting at invoking ansible module in kolla_toolbox
container as used by Kolla project.
options:
module_name:
description:
- The module name to invoke
required: True
type: str
module_args:
description:
- The module args use by the module
required: False
type: str or dict
module_extra_vars:
description:
- The extra variables used by the module
required: False
type: str or dict
api_version:
description:
- The version of the API for docker-py to use when contacting Docker
required: False
type: str
default: auto
timeout:
description:
- The default timeout for docker-py client when contacting Docker API
required: False
type: int
default: 180
author: Jeffrey Zhang
'''
EXAMPLES = '''
- hosts: controller
tasks:
- name: Ensure the direct absent
kolla_toolbox:
module_name: file
module_args: path=/tmp/a state=absent
- name: Create mysql database
kolla_toolbox:
module_name: mysql_db
module_args:
login_host: 192.168.1.10
login_user: root
login_password: admin
name: testdb
- name: Creating default user role
kolla_toolbox:
module_name: os_keystone_role
module_args:
name: _member_
auth: "{{ '{{ openstack_keystone_auth }}' }}"
module_extra_vars:
openstack_keystone_auth:
auth_url: http://127.0.0.1:5000
username: admin
password: password
project_name: "admin"
domain_name: "default"
'''
JSON_REG = re.compile('^(?P<host>\w+) \| (?P<status>\w+)!? =>(?P<stdout>.*)$',
re.MULTILINE | re.DOTALL)
NON_JSON_REG = re.compile(('^(?P<host>\w+) \| (?P<status>\w+)!? \| '
'rc=(?P<exit_code>\d+) >>\n(?P<stdout>.*)\n$'),
re.MULTILINE | re.DOTALL)
def gen_commandline(params):
command = ['ansible', 'localhost']
if params.get('module_name'):
command.extend(['-m', params.get('module_name')])
if params.get('module_args'):
module_args = params.get('module_args')
if isinstance(module_args, dict):
module_args = ' '.join("{}='{}'".format(key, value)
for key, value in module_args.items())
command.extend(['-a', module_args])
if params.get('module_extra_vars'):
extra_vars = params.get('module_extra_vars')
if isinstance(extra_vars, dict):
extra_vars = json.dumps(extra_vars)
command.extend(['--extra-vars', extra_vars])
return command
def get_docker_client():
return docker.APIClient
def main():
specs = dict(
module_name=dict(required=True, type='str'),
module_args=dict(type='str'),
module_extra_vars=dict(type='json'),
api_version=dict(required=False, type='str', default='auto'),
timeout=dict(required=False, type='int', default=180),
)
module = AnsibleModule(argument_spec=specs, bypass_checks=True)
client = get_docker_client()(
version=module.params.get('api_version'),
timeout=module.params.get('timeout'))
command_line = gen_commandline(module.params)
kolla_toolbox = client.containers(filters=dict(name='kolla_toolbox',
status='running'))
if not kolla_toolbox:
module.fail_json(msg='kolla_toolbox container is not running.')
kolla_toolbox = kolla_toolbox[0]
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)
if __name__ == "__main__":
main()