Merge "Do not link with Ansible code"

This commit is contained in:
Zuul 2018-11-26 14:14:04 +00:00 committed by Gerrit Code Review
commit c26860948e
5 changed files with 101 additions and 237 deletions

View File

@ -18,6 +18,13 @@ IPMI driver, Universal driver).
Installation
------------
Requirements
~~~~~~~~~~~~
Ansible is required and should be installed manually system-wide or in virtual
environment. Please refer to [https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html]
for installation instructions.
Regular installation::
pip install os-faults
@ -292,3 +299,22 @@ Terminate a container on a random node:
container = cloud_management.get_container(name='neutron_ovs_agent')
nodes = container.get_nodes().pick()
container.restart(nodes)
License notes
-------------
Ansible is distributed under GPL-3.0 license and thus all programs
that link with its code are subject to GPL restrictions [1].
However these restrictions are not applied to os-faults library
since it invokes Ansible as process [2][3].
Ansible modules are provided with Apache license (compatible to GPL) [4].
Those modules import part of Ansible runtime (modules API) and executed
on remote hosts. os-faults library does not import these module
neither static nor dynamic.
[1] https://www.gnu.org/licenses/gpl-faq.html#GPLModuleLicense
[2] https://www.gnu.org/licenses/gpl-faq.html#GPLPlugins
[3] https://www.gnu.org/licenses/gpl-faq.html#MereAggregation
[4] https://www.apache.org/licenses/GPL-compatibility.html

View File

@ -11,25 +11,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
import collections
import copy
import json
import logging
import os
import shlex
import tempfile
from ansible.executor import task_queue_manager
from ansible.parsing import dataloader
from ansible.playbook import play
from ansible.plugins import callback as callback_pkg
try:
from ansible.inventory.manager import InventoryManager as Inventory
from ansible.vars.manager import VariableManager
PRE_24_ANSIBLE = False
except ImportError:
# pre-2.4 Ansible
from ansible.inventory import Inventory
from ansible.vars import VariableManager
PRE_24_ANSIBLE = True
from oslo_concurrency import processutils
import yaml
from os_faults.api import error
@ -61,37 +54,13 @@ AnsibleExecutionRecord = collections.namedtuple(
'AnsibleExecutionRecord', ['host', 'status', 'task', 'payload'])
class MyCallback(callback_pkg.CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'myown'
def __init__(self, storage, display=None):
super(MyCallback, self).__init__(display)
self.storage = storage
def _store(self, result, status):
record = AnsibleExecutionRecord(
host=result._host.get_name(), status=status,
task=result._task.get_name(), payload=result._result)
self.storage.append(record)
def v2_runner_on_failed(self, result, ignore_errors=False):
super(MyCallback, self).v2_runner_on_failed(result)
self._store(result, STATUS_FAILED)
def v2_runner_on_ok(self, result):
super(MyCallback, self).v2_runner_on_ok(result)
self._store(result, STATUS_OK)
def v2_runner_on_skipped(self, result):
super(MyCallback, self).v2_runner_on_skipped(result)
self._store(result, STATUS_SKIPPED)
def v2_runner_on_unreachable(self, result):
super(MyCallback, self).v2_runner_on_unreachable(result)
self._store(result, STATUS_UNREACHABLE)
def find_ansible():
stdout, stderr = processutils.execute(
*shlex.split('which ansible-playbook'), check_exit_code=[0, 1])
if not stdout:
raise AnsibleExecutionException(
'Ansible executable is not found in $PATH')
return stdout[:-1]
def resolve_relative_path(file_name):
@ -122,14 +91,10 @@ def add_module_paths(paths):
def make_module_path_option():
if PRE_24_ANSIBLE:
# it was a string of colon-separated directories
module_path = os.pathsep.join(get_module_paths())
else:
# now it is a list of strings (MUST have > 1 element)
module_path = list(get_module_paths())
if len(module_path) == 1:
module_path += [module_path[0]]
# now it is a list of strings (MUST have > 1 element)
module_path = list(get_module_paths())
if len(module_path) == 1:
module_path += [module_path[0]]
return module_path
@ -166,6 +131,7 @@ class AnsibleRunner(object):
become=become, become_method='sudo', become_user='root',
verbosity=100, check=False, diff=None)
self.serial = serial or 10
self.ansible = find_ansible()
@staticmethod
def _build_proxy_arg(jump_user, jump_host, private_key_file=None):
@ -176,55 +142,67 @@ class AnsibleRunner(object):
host=jump_host, ssh_args=SSH_COMMON_ARGS))
def _run_play(self, play_source, host_vars):
host_list = play_source['hosts']
loader = dataloader.DataLoader()
# FIXME(jpena): we need to behave differently if we are using
# Ansible >= 2.4.0.0. Remove when only versions > 2.4 are supported
if PRE_24_ANSIBLE:
variable_manager = VariableManager()
inventory_inst = Inventory(loader=loader,
variable_manager=variable_manager,
host_list=host_list)
variable_manager.set_inventory(inventory_inst)
else:
inventory_inst = Inventory(loader=loader,
sources=','.join(host_list) + ',')
variable_manager = VariableManager(loader=loader,
inventory=inventory_inst)
inventory = {}
for host, variables in host_vars.items():
host_inst = inventory_inst.get_host(host)
host_vars = {}
for var_name, value in variables.items():
if value is not None:
variable_manager.set_host_variable(
host_inst, var_name, value)
host_vars[var_name] = value
inventory[host] = host_vars
storage = []
callback = MyCallback(storage)
inventory[host]['ansible_ssh_common_args'] = (
self.options.ssh_common_args)
inventory[host]['ansible_connection'] = self.options.connection
tqm = task_queue_manager.TaskQueueManager(
inventory=inventory_inst,
variable_manager=variable_manager,
loader=loader,
options=self.options,
passwords=self.passwords,
stdout_callback=callback,
)
full_inventory = {'all': {'hosts': inventory}}
# create play
play_inst = play.Play().load(play_source,
variable_manager=variable_manager,
loader=loader)
temp_dir = tempfile.mkdtemp(prefix='os-faults')
inventory_file_name = os.path.join(temp_dir, 'inventory')
playbook_file_name = os.path.join(temp_dir, 'playbook')
# actually run it
try:
tqm.run(play_inst)
finally:
tqm.cleanup()
with open(inventory_file_name, 'w') as fd:
print(yaml.safe_dump(full_inventory, default_flow_style=False),
file=fd)
return storage
play = {
'hosts': 'all',
'gather_facts': 'no',
'tasks': play_source['tasks'],
}
with open(playbook_file_name, 'w') as fd:
print(yaml.safe_dump([play], default_flow_style=False), file=fd)
cmd = ('%(ansible)s --module-path %(module_path)s '
'-i %(inventory)s %(playbook)s' %
{'ansible': self.ansible,
'module_path': ':'.join(self.options.module_path),
'inventory': inventory_file_name,
'playbook': playbook_file_name})
logging.info('Executing %s' % cmd)
command_stdout, command_stderr = processutils.execute(
*shlex.split(cmd),
env_variables={'ANSIBLE_STDOUT_CALLBACK': 'json'},
check_exit_code=False)
d = json.loads(command_stdout[command_stdout.find('{'):])
h = d['plays'][0]['tasks'][0]['hosts']
recs = []
for h, hv in h.items():
if hv.get('unreachable'):
status = STATUS_UNREACHABLE
elif hv.get('failed'):
status = STATUS_FAILED
else:
status = STATUS_OK
r = AnsibleExecutionRecord(host=h, status=status, task='',
payload=hv)
recs.append(r)
return recs
def run_playbook(self, playbook, host_vars):
result = []

View File

@ -19,87 +19,6 @@ from os_faults.api import node_collection
from os_faults.tests.unit import test
class MyCallbackTestCase(test.TestCase):
def test__store(self,):
ex = executor.MyCallback(mock.Mock())
my_host = 'my_host'
my_task = 'my_task'
my_result = 'my_result'
r = mock.Mock()
r._host.get_name.return_value = my_host
r._task.get_name.return_value = my_task
r._result = my_result
stat = 'OK'
ex._store(r, stat)
ex.storage.append.assert_called_once_with(
executor.AnsibleExecutionRecord(host=my_host, status=stat,
task=my_task, payload=my_result))
@mock.patch('ansible.plugins.callback.CallbackBase.v2_runner_on_failed')
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_failed_super(self, mock_store, mock_callback):
ex = executor.MyCallback(mock.Mock())
result = mock.Mock()
ex.v2_runner_on_failed(result)
mock_callback.assert_called_once_with(result)
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_failed(self, mock_store):
result = mock.Mock()
ex = executor.MyCallback(mock.Mock())
ex.v2_runner_on_failed(result)
mock_store.assert_called_once_with(result, executor.STATUS_FAILED)
@mock.patch('ansible.plugins.callback.CallbackBase.v2_runner_on_ok')
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_ok_super(self, mock_store, mock_callback):
ex = executor.MyCallback(mock.Mock())
result = mock.Mock()
ex.v2_runner_on_ok(result)
mock_callback.assert_called_once_with(result)
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_ok(self, mock_store):
result = mock.Mock()
ex = executor.MyCallback(mock.Mock())
ex.v2_runner_on_ok(result)
mock_store.assert_called_once_with(result, executor.STATUS_OK)
@mock.patch('ansible.plugins.callback.CallbackBase.v2_runner_on_skipped')
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_skipped_super(self, mock_store, mock_callback):
ex = executor.MyCallback(mock.Mock())
result = mock.Mock()
ex.v2_runner_on_skipped(result)
mock_callback.assert_called_once_with(result)
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_skipped(self, mock_store):
result = mock.Mock()
ex = executor.MyCallback(mock.Mock())
ex.v2_runner_on_skipped(result)
mock_store.assert_called_once_with(result, executor.STATUS_SKIPPED)
@mock.patch(
'ansible.plugins.callback.CallbackBase.v2_runner_on_unreachable')
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_unreachable_super(self, mock_store, mock_callback):
ex = executor.MyCallback(mock.Mock())
result = mock.Mock()
ex.v2_runner_on_unreachable(result)
mock_callback.assert_called_once_with(result)
@mock.patch('os_faults.ansible.executor.MyCallback._store')
def test_v2_runner_on_unreachable(self, mock_store):
result = mock.Mock()
ex = executor.MyCallback(mock.Mock())
ex.v2_runner_on_unreachable(result)
mock_store.assert_called_once_with(result, executor.STATUS_UNREACHABLE)
@ddt.ddt
class AnsibleRunnerTestCase(test.TestCase):
@ -191,59 +110,6 @@ class AnsibleRunnerTestCase(test.TestCase):
**options_args)
self.assertEqual(passwords, runner.passwords)
@mock.patch.object(executor.task_queue_manager, 'TaskQueueManager')
@mock.patch('ansible.playbook.play.Play.load')
@mock.patch('os_faults.ansible.executor.Inventory')
@mock.patch('os_faults.ansible.executor.VariableManager')
@mock.patch('ansible.parsing.dataloader.DataLoader')
def test__run_play(self, mock_dataloader, mock_vmanager, mock_inventory,
mock_play_load, mock_taskqm):
mock_play_load.return_value = 'my_load'
variable_manager = mock_vmanager.return_value
host_inst = mock_inventory.return_value.get_host.return_value
host_vars = {
'0.0.0.0': {
'ansible_user': 'foo',
'ansible_ssh_pass': 'bar',
'ansible_become': True,
'ansible_ssh_private_key_file': None,
'ansible_ssh_common_args': '-o Option=yes',
}
}
ex = executor.AnsibleRunner()
ex._run_play({'hosts': ['0.0.0.0']}, host_vars)
mock_taskqm.assert_called_once()
self.assertEqual(mock_taskqm.mock_calls[1], mock.call().run('my_load'))
self.assertEqual(mock_taskqm.mock_calls[2], mock.call().cleanup())
variable_manager.set_host_variable.assert_has_calls((
mock.call(host_inst, 'ansible_user', 'foo'),
mock.call(host_inst, 'ansible_ssh_pass', 'bar'),
mock.call(host_inst, 'ansible_become', True),
mock.call(host_inst, 'ansible_ssh_common_args', '-o Option=yes'),
), any_order=True)
@mock.patch.object(executor.task_queue_manager, 'TaskQueueManager')
@mock.patch('ansible.playbook.play.Play.load')
@mock.patch('os_faults.ansible.executor.Inventory')
@mock.patch('os_faults.ansible.executor.VariableManager')
@mock.patch('ansible.parsing.dataloader.DataLoader')
def test__run_play_no_host_vars(
self, mock_dataloader, mock_vmanager, mock_inventory,
mock_play_load, mock_taskqm):
mock_play_load.return_value = 'my_load'
variable_manager = mock_vmanager.return_value
host_vars = {}
ex = executor.AnsibleRunner()
ex._run_play({'hosts': ['0.0.0.0']}, host_vars)
mock_taskqm.assert_called_once()
self.assertEqual(mock_taskqm.mock_calls[1], mock.call().run('my_load'))
self.assertEqual(mock_taskqm.mock_calls[2], mock.call().cleanup())
self.assertEqual(0, variable_manager.set_host_variable.call_count)
@mock.patch('os_faults.ansible.executor.AnsibleRunner._run_play')
def test_run_playbook(self, mock_run_play):
ex = executor.AnsibleRunner()
@ -404,22 +270,13 @@ class AnsibleRunnerTestCase(test.TestCase):
))
@mock.patch('os_faults.executor.get_module_paths')
@mock.patch('os_faults.executor.PRE_24_ANSIBLE', False)
def test_make_module_path_option_ansible_24(self, mock_mp):
mock_mp.return_value = ['/path/one', 'path/two']
self.assertEqual(['/path/one', 'path/two'],
executor.make_module_path_option())
@mock.patch('os_faults.executor.get_module_paths')
@mock.patch('os_faults.executor.PRE_24_ANSIBLE', False)
def test_make_module_path_option_ansible_24_one_item(self, mock_mp):
mock_mp.return_value = ['/path/one']
self.assertEqual(['/path/one', '/path/one'],
executor.make_module_path_option())
@mock.patch('os_faults.executor.get_module_paths')
@mock.patch('os_faults.executor.PRE_24_ANSIBLE', True)
def test_make_module_path_option_ansible_pre24(self, mock_mp):
mock_mp.return_value = ['/path/one', 'path/two']
self.assertEqual('/path/one:path/two',
executor.make_module_path_option())

View File

@ -4,11 +4,11 @@
pbr>=2.0.0 # Apache-2.0
ansible>=2.2
appdirs>=1.3.0 # MIT License
click>=6.7 # BSD
iso8601>=0.1.11 # MIT
jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
oslo.concurrency>=3.0.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.utils>=3.20.0 # Apache-2.0

View File

@ -21,5 +21,8 @@ testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
# used for testing only
ansible # GPL-3.0
# releasenotes
reno>=1.8.0 # Apache-2.0