340 lines
12 KiB
Python
340 lines
12 KiB
Python
# Copyright 2013 Canonical Ltd.
|
|
#
|
|
# Authors:
|
|
# Charm Helpers Developers <juju@lists.ubuntu.com>
|
|
import mock
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import tempfile
|
|
import unittest
|
|
import yaml
|
|
|
|
|
|
import charmhelpers.contrib.ansible
|
|
from charmhelpers.core import hookenv
|
|
|
|
|
|
class InstallAnsibleSupportTestCase(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
super(InstallAnsibleSupportTestCase, self).setUp()
|
|
|
|
patcher = mock.patch('charmhelpers.fetch')
|
|
self.mock_fetch = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
patcher = mock.patch('charmhelpers.core')
|
|
self.mock_core = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
hosts_file = tempfile.NamedTemporaryFile()
|
|
self.ansible_hosts_path = hosts_file.name
|
|
self.addCleanup(hosts_file.close)
|
|
patcher = mock.patch.object(charmhelpers.contrib.ansible,
|
|
'ansible_hosts_path',
|
|
self.ansible_hosts_path)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def test_adds_ppa_by_default(self):
|
|
charmhelpers.contrib.ansible.install_ansible_support()
|
|
|
|
self.mock_fetch.add_source.assert_called_once_with(
|
|
'ppa:ansible/ansible')
|
|
self.mock_fetch.apt_update.assert_called_once_with(fatal=True)
|
|
self.mock_fetch.apt_install.assert_called_once_with(
|
|
'ansible')
|
|
|
|
def test_no_ppa(self):
|
|
charmhelpers.contrib.ansible.install_ansible_support(
|
|
from_ppa=False)
|
|
|
|
self.assertEqual(self.mock_fetch.add_source.call_count, 0)
|
|
self.mock_fetch.apt_install.assert_called_once_with(
|
|
'ansible')
|
|
|
|
def test_writes_ansible_hosts(self):
|
|
with open(self.ansible_hosts_path) as hosts_file:
|
|
self.assertEqual(hosts_file.read(), '')
|
|
|
|
charmhelpers.contrib.ansible.install_ansible_support()
|
|
|
|
with open(self.ansible_hosts_path) as hosts_file:
|
|
self.assertEqual(hosts_file.read(),
|
|
'localhost ansible_connection=local '
|
|
'ansible_remote_tmp=/root/.ansible/tmp')
|
|
|
|
|
|
class ApplyPlaybookTestCases(unittest.TestCase):
|
|
|
|
unit_data = {
|
|
'private-address': '10.0.3.2',
|
|
'public-address': '123.123.123.123',
|
|
}
|
|
|
|
def setUp(self):
|
|
super(ApplyPlaybookTestCases, self).setUp()
|
|
|
|
# Hookenv patches (a single patch to hookenv doesn't work):
|
|
patcher = mock.patch('charmhelpers.core.hookenv.config')
|
|
self.mock_config = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
Serializable = charmhelpers.core.hookenv.Serializable
|
|
self.mock_config.return_value = Serializable({})
|
|
patcher = mock.patch('charmhelpers.core.hookenv.relation_get')
|
|
self.mock_relation_get = patcher.start()
|
|
self.mock_relation_get.return_value = {}
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch('charmhelpers.core.hookenv.relations')
|
|
self.mock_relations = patcher.start()
|
|
self.mock_relations.return_value = {
|
|
'wsgi-file': {},
|
|
'website': {},
|
|
'nrpe-external-master': {},
|
|
}
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch('charmhelpers.core.hookenv.relations_of_type')
|
|
self.mock_relations_of_type = patcher.start()
|
|
self.mock_relations_of_type.return_value = []
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch('charmhelpers.core.hookenv.relation_type')
|
|
self.mock_relation_type = patcher.start()
|
|
self.mock_relation_type.return_value = None
|
|
self.addCleanup(patcher.stop)
|
|
patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
|
|
self.mock_local_unit = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
self.mock_local_unit.return_value = {}
|
|
|
|
def unit_get_data(argument):
|
|
"dummy unit_get that accesses dummy unit data"
|
|
return self.unit_data[argument]
|
|
|
|
patcher = mock.patch(
|
|
'charmhelpers.core.hookenv.unit_get', unit_get_data)
|
|
self.mock_unit_get = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
patcher = mock.patch('charmhelpers.contrib.ansible.subprocess')
|
|
self.mock_subprocess = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
etc_dir = tempfile.mkdtemp()
|
|
self.addCleanup(shutil.rmtree, etc_dir)
|
|
self.vars_path = os.path.join(etc_dir, 'ansible', 'vars.yaml')
|
|
patcher = mock.patch.object(charmhelpers.contrib.ansible,
|
|
'ansible_vars_path', self.vars_path)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
patcher = mock.patch.object(charmhelpers.contrib.ansible.os,
|
|
'environ', {})
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def test_calls_ansible_playbook(self):
|
|
charmhelpers.contrib.ansible.apply_playbook(
|
|
'playbooks/dependencies.yaml')
|
|
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'playbooks/dependencies.yaml'],
|
|
env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
def test_writes_vars_file(self):
|
|
self.assertFalse(os.path.exists(self.vars_path))
|
|
self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
|
|
'group_code_owner': 'webops_deploy',
|
|
'user_code_runner': 'ubunet',
|
|
'private-address': '10.10.10.10',
|
|
})
|
|
self.mock_relation_type.return_value = 'wsgi-file'
|
|
self.mock_relation_get.return_value = {
|
|
'relation_key1': 'relation_value1',
|
|
'relation-key2': 'relation_value2',
|
|
}
|
|
|
|
charmhelpers.contrib.ansible.apply_playbook(
|
|
'playbooks/dependencies.yaml')
|
|
|
|
self.assertTrue(os.path.exists(self.vars_path))
|
|
stats = os.stat(self.vars_path)
|
|
self.assertEqual(
|
|
stats.st_mode & stat.S_IRWXU,
|
|
stat.S_IRUSR | stat.S_IWUSR)
|
|
self.assertEqual(stats.st_mode & stat.S_IRWXG, 0)
|
|
self.assertEqual(stats.st_mode & stat.S_IRWXO, 0)
|
|
with open(self.vars_path, 'r') as vars_file:
|
|
result = yaml.safe_load(vars_file.read())
|
|
self.assertEqual({
|
|
"group_code_owner": "webops_deploy",
|
|
"user_code_runner": "ubunet",
|
|
"private_address": "10.10.10.10",
|
|
"charm_dir": "",
|
|
"local_unit": {},
|
|
'current_relation': {
|
|
'relation_key1': 'relation_value1',
|
|
'relation-key2': 'relation_value2',
|
|
},
|
|
'relations_full': {
|
|
'nrpe-external-master': {},
|
|
'website': {},
|
|
'wsgi-file': {},
|
|
},
|
|
'relations': {
|
|
'nrpe-external-master': [],
|
|
'website': [],
|
|
'wsgi-file': [],
|
|
},
|
|
"wsgi_file__relation_key1": "relation_value1",
|
|
"wsgi_file__relation_key2": "relation_value2",
|
|
"unit_private_address": "10.0.3.2",
|
|
"unit_public_address": "123.123.123.123",
|
|
}, result)
|
|
|
|
def test_calls_with_tags(self):
|
|
charmhelpers.contrib.ansible.apply_playbook(
|
|
'playbooks/complete-state.yaml', tags=['install', 'somethingelse'])
|
|
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'playbooks/complete-state.yaml',
|
|
'--tags', 'install,somethingelse'], env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
@mock.patch.object(hookenv, 'config')
|
|
def test_calls_with_extra_vars(self, config):
|
|
charmhelpers.contrib.ansible.apply_playbook(
|
|
'playbooks/complete-state.yaml', tags=['install', 'somethingelse'],
|
|
extra_vars={'a': 'b'})
|
|
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'playbooks/complete-state.yaml',
|
|
'--tags', 'install,somethingelse', '--extra-vars', '{"a": "b"}'],
|
|
env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
@mock.patch.object(hookenv, 'config')
|
|
def test_calls_with_extra_vars_path(self, config):
|
|
charmhelpers.contrib.ansible.apply_playbook(
|
|
'playbooks/complete-state.yaml', tags=['install', 'somethingelse'],
|
|
extra_vars='@myvars.json')
|
|
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'playbooks/complete-state.yaml',
|
|
'--tags', 'install,somethingelse', '--extra-vars', '"@myvars.json"'],
|
|
env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
@mock.patch.object(hookenv, 'config')
|
|
def test_calls_with_extra_vars_dict(self, config):
|
|
charmhelpers.contrib.ansible.apply_playbook(
|
|
'playbooks/complete-state.yaml', tags=['install', 'somethingelse'],
|
|
extra_vars={'pkg': {'a': 'present', 'b': 'absent'}})
|
|
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'playbooks/complete-state.yaml',
|
|
'--tags', 'install,somethingelse', '--extra-vars',
|
|
'{"pkg": {"a": "present", "b": "absent"}}'],
|
|
env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
@mock.patch.object(hookenv, 'config')
|
|
def test_hooks_executes_playbook_with_tag(self, config):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
|
|
foo = mock.MagicMock()
|
|
hooks.register('foo', foo)
|
|
|
|
hooks.execute(['foo'])
|
|
|
|
self.assertEqual(foo.call_count, 1)
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
|
|
'--tags', 'foo'], env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
@mock.patch.object(hookenv, 'config')
|
|
def test_specifying_ansible_handled_hooks(self, config):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks(
|
|
'my/playbook.yaml', default_hooks=['start', 'stop'])
|
|
|
|
hooks.execute(['start'])
|
|
|
|
self.mock_subprocess.check_call.assert_called_once_with([
|
|
'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
|
|
'--tags', 'start'], env={'PYTHONUNBUFFERED': '1'})
|
|
|
|
|
|
class TestActionDecorator(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
p = mock.patch('charmhelpers.contrib.ansible.apply_playbook')
|
|
self.apply_playbook = p.start()
|
|
self.addCleanup(p.stop)
|
|
|
|
def test_action_no_args(self):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('playbook.yaml')
|
|
|
|
@hooks.action()
|
|
def test():
|
|
return {}
|
|
|
|
hooks.execute(['test'])
|
|
self.apply_playbook.assert_called_once_with(
|
|
'playbook.yaml', tags=['test'], extra_vars={})
|
|
|
|
def test_action_required_arg_keyword(self):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('playbook.yaml')
|
|
|
|
@hooks.action()
|
|
def test(x):
|
|
return locals()
|
|
|
|
hooks.execute(['test', 'x=a'])
|
|
self.apply_playbook.assert_called_once_with(
|
|
'playbook.yaml', tags=['test'], extra_vars={'x': 'a'})
|
|
|
|
def test_action_required_arg_missing(self):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('playbook.yaml')
|
|
|
|
@hooks.action()
|
|
def test(x):
|
|
"""Requires x"""
|
|
return locals()
|
|
|
|
try:
|
|
hooks.execute(['test'])
|
|
self.fail("should have thrown TypeError")
|
|
except TypeError as e:
|
|
self.assertEqual(e.args[1], "Requires x")
|
|
|
|
def test_action_required_unknown_arg(self):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('playbook.yaml')
|
|
|
|
@hooks.action()
|
|
def test(x='a'):
|
|
"""Requires x"""
|
|
return locals()
|
|
|
|
try:
|
|
hooks.execute(['test', 'z=c'])
|
|
self.fail("should have thrown TypeError")
|
|
except TypeError as e:
|
|
self.assertEqual(e.args[1], "Requires x")
|
|
|
|
def test_action_default_arg(self):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('playbook.yaml')
|
|
|
|
@hooks.action()
|
|
def test(x='b'):
|
|
return locals()
|
|
|
|
hooks.execute(['test'])
|
|
self.apply_playbook.assert_called_once_with(
|
|
'playbook.yaml', tags=['test'], extra_vars={'x': 'b'})
|
|
|
|
def test_action_mutliple(self):
|
|
hooks = charmhelpers.contrib.ansible.AnsibleHooks('playbook.yaml')
|
|
|
|
@hooks.action()
|
|
def test(x, y='b'):
|
|
return locals()
|
|
|
|
hooks.execute(['test', 'x=a', 'y=b'])
|
|
self.apply_playbook.assert_called_once_with(
|
|
'playbook.yaml', tags=['test'], extra_vars={'x': 'a', 'y': 'b'})
|