Do not link with Ansible code
Ansible is distributed under GPL-3.0 license and certain restrictions are applied when its code is imported as Python library. The only safe way to call GPL code is via general interface, e.g. CLI. This patch removes all direct linking of Ansible code and executes all actions via command line. It is now user responsibility to install Ansible executable on the system. Change-Id: If879e4ce59bcdac84bc51ea0ac9277783777c80b
This commit is contained in:
parent
7924e4ccf4
commit
b2ca946296
26
README.rst
26
README.rst
@ -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
|
||||
@ -291,3 +298,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
|
||||
|
@ -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 = []
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user