Merge "Adds ansible actions"
This commit is contained in:
commit
fa0fdb16a9
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds actions for calling ansible and ansible playbook executables from a
|
||||||
|
workflow.
|
||||||
|
deprecations:
|
||||||
|
- |
|
||||||
|
The actions for calling ansible and ansible playbook executables from a
|
||||||
|
workflow will be removed in the Queens release as they are intended to
|
||||||
|
be migrated to the mistral-extra project.
|
@ -110,4 +110,6 @@ mistral.actions =
|
|||||||
tripleo.validations.run_validation = tripleo_common.actions.validations:RunValidationAction
|
tripleo.validations.run_validation = tripleo_common.actions.validations:RunValidationAction
|
||||||
tripleo.validations.verify_profiles = tripleo_common.actions.validations:VerifyProfilesAction
|
tripleo.validations.verify_profiles = tripleo_common.actions.validations:VerifyProfilesAction
|
||||||
# deprecated for pike release, will be removed in queens
|
# deprecated for pike release, will be removed in queens
|
||||||
|
tripleo.ansible = tripleo_common.actions.ansible:AnsibleAction
|
||||||
|
tripleo.ansible-playbook = tripleo_common.actions.ansible:AnsiblePlaybookAction
|
||||||
tripleo.templates.upload_default = tripleo_common.actions.templates:UploadTemplatesAction
|
tripleo.templates.upload_default = tripleo_common.actions.templates:UploadTemplatesAction
|
||||||
|
308
tripleo_common/actions/ansible.py
Normal file
308
tripleo_common/actions/ansible.py
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
# Copyright 2017 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 json
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from mistral_lib import actions
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
|
||||||
|
|
||||||
|
def _write_data(data, suffix=''):
|
||||||
|
temp_data = tempfile.NamedTemporaryFile(suffix=suffix)
|
||||||
|
temp_data.write(data)
|
||||||
|
temp_data.flush()
|
||||||
|
return temp_data
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleAction(actions.Action):
|
||||||
|
"""Executes ansible module"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._kwargs_for_run = kwargs
|
||||||
|
self.hosts = self._kwargs_for_run.pop('hosts', None)
|
||||||
|
self.module = self._kwargs_for_run.pop('module', None)
|
||||||
|
self.module_args = self._kwargs_for_run.pop('module_args', None)
|
||||||
|
if self.module_args:
|
||||||
|
self.module_args = json.dumps(self.module_args)
|
||||||
|
self.limit_hosts = self._kwargs_for_run.pop('limit_hosts', None)
|
||||||
|
self.remote_user = self._kwargs_for_run.pop('remote_user', None)
|
||||||
|
self.become = self._kwargs_for_run.pop('become', None)
|
||||||
|
self.become_user = self._kwargs_for_run.pop('become_user', None)
|
||||||
|
self.extra_vars = self._kwargs_for_run.pop('extra_vars', None)
|
||||||
|
if self.extra_vars:
|
||||||
|
self.extra_vars = json.dumps(self.extra_vars)
|
||||||
|
self._inventory = self._kwargs_for_run.pop('inventory', None)
|
||||||
|
self.verbosity = self._kwargs_for_run.pop('verbosity', 5)
|
||||||
|
self._ssh_private_key = self._kwargs_for_run.pop(
|
||||||
|
'ssh_private_key', None)
|
||||||
|
self.forks = self._kwargs_for_run.pop('forks', None)
|
||||||
|
self.timeout = self._kwargs_for_run.pop('timeout', None)
|
||||||
|
self.ssh_extra_args = self._kwargs_for_run.pop('ssh_extra_args', None)
|
||||||
|
if self.ssh_extra_args:
|
||||||
|
self.ssh_extra_args = json.dumps(self.ssh_extra_args)
|
||||||
|
self.ssh_common_args = self._kwargs_for_run.pop(
|
||||||
|
'ssh_common_args', None)
|
||||||
|
if self.ssh_common_args:
|
||||||
|
self.ssh_common_args = json.dumps(self.ssh_common_args)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inventory(self):
|
||||||
|
if not self._inventory:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NOTE(flaper87): if it's a path, use it
|
||||||
|
if (isinstance(self._inventory, six.string_types) and
|
||||||
|
os.path.exists(self._inventory)):
|
||||||
|
return open(self._inventory)
|
||||||
|
else:
|
||||||
|
self._inventory = yaml.safe_dump(self._inventory)
|
||||||
|
|
||||||
|
# NOTE(flaper87):
|
||||||
|
# We could probably catch parse errors here
|
||||||
|
# but if we do, they won't be propagated and
|
||||||
|
# we should not move forward with the action
|
||||||
|
# if the inventory generation failed
|
||||||
|
return _write_data(self._inventory, suffix='.yaml')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ssh_private_key(self):
|
||||||
|
if not self._ssh_private_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NOTE(flaper87): if it's a path, use it
|
||||||
|
if (isinstance(self._ssh_private_key, six.string_types) and
|
||||||
|
os.path.exists(self._ssh_private_key)):
|
||||||
|
return open(self._ssh_private_key)
|
||||||
|
|
||||||
|
# NOTE(flaper87):
|
||||||
|
# We could probably catch parse errors here
|
||||||
|
# but if we do, they won't be propagated and
|
||||||
|
# we should not move forward with the action
|
||||||
|
# if the playbook generation failed
|
||||||
|
return _write_data(self._ssh_private_key)
|
||||||
|
|
||||||
|
def run(self, context):
|
||||||
|
|
||||||
|
if 0 < self.verbosity < 6:
|
||||||
|
verbosity_option = '-' + ('v' * self.verbosity)
|
||||||
|
command = ['ansible', self.hosts, verbosity_option, ]
|
||||||
|
else:
|
||||||
|
command = ['ansible-playbook', self.hosts, ]
|
||||||
|
|
||||||
|
if self.module:
|
||||||
|
command.extend(['--module-name', self.module])
|
||||||
|
|
||||||
|
if self.module_args:
|
||||||
|
command.extend(['--args', self.module_args])
|
||||||
|
|
||||||
|
if self.limit_hosts:
|
||||||
|
command.extend(['--limit', self.limit_hosts])
|
||||||
|
|
||||||
|
if self.remote_user:
|
||||||
|
command.extend(['--user', self.remote_user])
|
||||||
|
|
||||||
|
if self.become:
|
||||||
|
command.extend(['--become'])
|
||||||
|
|
||||||
|
if self.become_user:
|
||||||
|
command.extend(['--become-user', self.become_user])
|
||||||
|
|
||||||
|
if self.extra_vars:
|
||||||
|
command.extend(['--extra-vars', self.extra_vars])
|
||||||
|
|
||||||
|
if self.forks:
|
||||||
|
command.extend(['--forks', self.forks])
|
||||||
|
|
||||||
|
if self.ssh_common_args:
|
||||||
|
command.extend(['--ssh-common-args', self.ssh_common_args])
|
||||||
|
|
||||||
|
if self.ssh_extra_args:
|
||||||
|
command.extend(['--ssh-extra-args', self.ssh_extra_args])
|
||||||
|
|
||||||
|
if self.timeout:
|
||||||
|
command.extend(['--timeout', self.timeout])
|
||||||
|
|
||||||
|
inventory_file = self.inventory
|
||||||
|
if inventory_file:
|
||||||
|
command.extend(['--inventory-file', inventory_file.name])
|
||||||
|
|
||||||
|
ssh_priv_key_file = self.ssh_private_key
|
||||||
|
if ssh_priv_key_file:
|
||||||
|
command.extend(['--private-key', ssh_priv_key_file.name])
|
||||||
|
|
||||||
|
try:
|
||||||
|
stderr, stdout = processutils.execute(
|
||||||
|
*command, log_errors=processutils.LogErrors.ALL)
|
||||||
|
return {"stderr": stderr, "stdout": stdout}
|
||||||
|
finally:
|
||||||
|
# NOTE(flaper87): Close the file
|
||||||
|
# this is important as it'll also cleanup
|
||||||
|
# temporary files
|
||||||
|
if inventory_file:
|
||||||
|
inventory_file.close()
|
||||||
|
|
||||||
|
if ssh_priv_key_file:
|
||||||
|
ssh_priv_key_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
class AnsiblePlaybookAction(actions.Action):
|
||||||
|
"""Executes ansible playbook"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._kwargs_for_run = kwargs
|
||||||
|
self._playbook = self._kwargs_for_run.pop('playbook', None)
|
||||||
|
self.limit_hosts = self._kwargs_for_run.pop('limit_hosts', None)
|
||||||
|
self.remote_user = self._kwargs_for_run.pop('remote_user', None)
|
||||||
|
self.become = self._kwargs_for_run.pop('become', None)
|
||||||
|
self.become_user = self._kwargs_for_run.pop('become_user', None)
|
||||||
|
self.extra_vars = self._kwargs_for_run.pop('extra_vars', None)
|
||||||
|
if self.extra_vars:
|
||||||
|
self.extra_vars = json.dumps(self.extra_vars)
|
||||||
|
self._inventory = self._kwargs_for_run.pop('inventory', None)
|
||||||
|
self.verbosity = self._kwargs_for_run.pop('verbosity', 5)
|
||||||
|
self._ssh_private_key = self._kwargs_for_run.pop(
|
||||||
|
'ssh_private_key', None)
|
||||||
|
self.flush_cache = self._kwargs_for_run.pop('flush_cache', None)
|
||||||
|
self.forks = self._kwargs_for_run.pop('forks', None)
|
||||||
|
self.timeout = self._kwargs_for_run.pop('timeout', None)
|
||||||
|
self.ssh_extra_args = self._kwargs_for_run.pop('ssh_extra_args', None)
|
||||||
|
if self.ssh_extra_args:
|
||||||
|
self.ssh_extra_args = json.dumps(self.ssh_extra_args)
|
||||||
|
self.ssh_common_args = self._kwargs_for_run.pop(
|
||||||
|
'ssh_common_args', None)
|
||||||
|
if self.ssh_common_args:
|
||||||
|
self.ssh_common_args = json.dumps(self.ssh_common_args)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inventory(self):
|
||||||
|
if not self._inventory:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NOTE(flaper87): if it's a path, use it
|
||||||
|
if (isinstance(self._inventory, six.string_types) and
|
||||||
|
os.path.exists(self._inventory)):
|
||||||
|
return open(self._inventory)
|
||||||
|
else:
|
||||||
|
self._inventory = yaml.safe_dump(self._inventory)
|
||||||
|
|
||||||
|
# NOTE(flaper87):
|
||||||
|
# We could probably catch parse errors here
|
||||||
|
# but if we do, they won't be propagated and
|
||||||
|
# we should not move forward with the action
|
||||||
|
# if the inventory generation failed
|
||||||
|
return _write_data(self._inventory, suffix='.yaml')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playbook(self):
|
||||||
|
if not self._playbook:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NOTE(flaper87): if it's a path, use it
|
||||||
|
if (isinstance(self._playbook, six.string_types) and
|
||||||
|
os.path.exists(self._playbook)):
|
||||||
|
return open(self._playbook)
|
||||||
|
else:
|
||||||
|
self._playbook = yaml.safe_dump(self._playbook)
|
||||||
|
|
||||||
|
# NOTE(flaper87):
|
||||||
|
# We could probably catch parse errors here
|
||||||
|
# but if we do, they won't be propagated and
|
||||||
|
# we should not move forward with the action
|
||||||
|
# if the playbook generation failed
|
||||||
|
return _write_data(self._playbook, suffix='.yaml')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ssh_private_key(self):
|
||||||
|
if not self._ssh_private_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NOTE(flaper87): if it's a path, use it
|
||||||
|
if (isinstance(self._ssh_private_key, six.string_types) and
|
||||||
|
os.path.exists(self._ssh_private_key)):
|
||||||
|
return open(self._ssh_private_key)
|
||||||
|
|
||||||
|
# NOTE(flaper87):
|
||||||
|
# We could probably catch parse errors here
|
||||||
|
# but if we do, they won't be propagated and
|
||||||
|
# we should not move forward with the action
|
||||||
|
# if the playbook generation failed
|
||||||
|
return _write_data(self._ssh_private_key)
|
||||||
|
|
||||||
|
def run(self, context):
|
||||||
|
playbook_file = self.playbook
|
||||||
|
if 0 < self.verbosity < 6:
|
||||||
|
verbosity_option = '-' + ('v' * self.verbosity)
|
||||||
|
command = ['ansible-playbook', verbosity_option,
|
||||||
|
playbook_file.name]
|
||||||
|
else:
|
||||||
|
command = ['ansible-playbook', playbook_file.name]
|
||||||
|
|
||||||
|
if self.limit_hosts:
|
||||||
|
command.extend(['--limit', self.limit_hosts])
|
||||||
|
|
||||||
|
if self.remote_user:
|
||||||
|
command.extend(['--user', self.remote_user])
|
||||||
|
|
||||||
|
if self.become:
|
||||||
|
command.extend(['--become'])
|
||||||
|
|
||||||
|
if self.become_user:
|
||||||
|
command.extend(['--become-user', self.become_user])
|
||||||
|
|
||||||
|
if self.extra_vars:
|
||||||
|
command.extend(['--extra-vars', self.extra_vars])
|
||||||
|
|
||||||
|
if self.flush_cache:
|
||||||
|
command.extend(['--flush-cache'])
|
||||||
|
|
||||||
|
if self.forks:
|
||||||
|
command.extend(['--forks', self.forks])
|
||||||
|
|
||||||
|
if self.ssh_common_args:
|
||||||
|
command.extend(['--ssh-common-args', self.ssh_common_args])
|
||||||
|
|
||||||
|
if self.ssh_extra_args:
|
||||||
|
command.extend(['--ssh-extra-args', self.ssh_extra_args])
|
||||||
|
|
||||||
|
if self.timeout:
|
||||||
|
command.extend(['--timeout', self.timeout])
|
||||||
|
|
||||||
|
inventory_file = self.inventory
|
||||||
|
if inventory_file:
|
||||||
|
command.extend(['--inventory-file', inventory_file.name])
|
||||||
|
|
||||||
|
ssh_priv_key_file = self.ssh_private_key
|
||||||
|
if ssh_priv_key_file:
|
||||||
|
command.extend(['--private-key', ssh_priv_key_file.name])
|
||||||
|
|
||||||
|
try:
|
||||||
|
stderr, stdout = processutils.execute(
|
||||||
|
*command, log_errors=processutils.LogErrors.ALL)
|
||||||
|
return {"stderr": stderr, "stdout": stdout}
|
||||||
|
finally:
|
||||||
|
# NOTE(flaper87): Close the file
|
||||||
|
# this is important as it'll also cleanup
|
||||||
|
# temporary files
|
||||||
|
if inventory_file:
|
||||||
|
inventory_file.close()
|
||||||
|
|
||||||
|
if ssh_priv_key_file:
|
||||||
|
ssh_priv_key_file.close()
|
||||||
|
|
||||||
|
playbook_file.close()
|
88
tripleo_common/tests/actions/test_ansible.py
Normal file
88
tripleo_common/tests/actions/test_ansible.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Copyright 2017 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 json
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
|
||||||
|
from tripleo_common.actions import ansible
|
||||||
|
from tripleo_common.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleActionTest(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(AnsibleActionTest, self).setUp()
|
||||||
|
|
||||||
|
self.hosts = "127.0.0.2"
|
||||||
|
self.module = "foo"
|
||||||
|
self.remote_user = 'fido'
|
||||||
|
self.become = True
|
||||||
|
self.become_user = 'root'
|
||||||
|
self.ctx = mock.MagicMock()
|
||||||
|
|
||||||
|
@mock.patch("oslo_concurrency.processutils.execute")
|
||||||
|
def test_run(self, mock_execute):
|
||||||
|
|
||||||
|
mock_execute.return_value = ('', '')
|
||||||
|
|
||||||
|
action = ansible.AnsibleAction(
|
||||||
|
hosts=self.hosts, module=self.module, remote_user=self.remote_user,
|
||||||
|
become=self.become, become_user=self.become_user)
|
||||||
|
action.run(self.ctx)
|
||||||
|
|
||||||
|
mock_execute.assert_called_once_with(
|
||||||
|
'ansible', self.hosts, '-vvvvv', '--module-name',
|
||||||
|
self.module, '--user', self.remote_user, '--become',
|
||||||
|
'--become-user', self.become_user,
|
||||||
|
log_errors=processutils.LogErrors.ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnsiblePlaybookActionTest(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(AnsiblePlaybookActionTest, self).setUp()
|
||||||
|
|
||||||
|
self.playbook = "myplaybook"
|
||||||
|
self.limit_hosts = None
|
||||||
|
self.remote_user = 'fido'
|
||||||
|
self.become = True
|
||||||
|
self.become_user = 'root'
|
||||||
|
self.extra_vars = {"var1": True, "var2": 0}
|
||||||
|
self.verbosity = 1
|
||||||
|
self.ctx = mock.MagicMock()
|
||||||
|
|
||||||
|
@mock.patch("tripleo_common.actions.ansible._write_data")
|
||||||
|
@mock.patch("oslo_concurrency.processutils.execute")
|
||||||
|
def test_run(self, mock_execute, mock_temp):
|
||||||
|
|
||||||
|
mock_execute.return_value = ('', '')
|
||||||
|
mock_file = mock.MagicMock()
|
||||||
|
mock_file.name = self.playbook
|
||||||
|
mock_temp.return_value = mock_file
|
||||||
|
|
||||||
|
action = ansible.AnsiblePlaybookAction(
|
||||||
|
playbook=self.playbook, limit_hosts=self.limit_hosts,
|
||||||
|
remote_user=self.remote_user, become=self.become,
|
||||||
|
become_user=self.become_user, extra_vars=self.extra_vars,
|
||||||
|
verbosity=self.verbosity)
|
||||||
|
action.run(self.ctx)
|
||||||
|
|
||||||
|
mock_execute.assert_called_once_with(
|
||||||
|
'ansible-playbook', '-v', self.playbook, '--user',
|
||||||
|
self.remote_user, '--become', '--become-user', self.become_user,
|
||||||
|
'--extra-vars', json.dumps(self.extra_vars),
|
||||||
|
log_errors=processutils.LogErrors.ALL)
|
Loading…
Reference in New Issue
Block a user