Construct package list and restart_map dynamically. Check-in tests.

This commit is contained in:
Adam Gandelman 2013-07-18 21:41:07 -07:00
parent 8139e43440
commit 4b969f7722
8 changed files with 610 additions and 22 deletions

10
charm-helpers.yaml Normal file
View File

@ -0,0 +1,10 @@
branch: lp:charm-helpers
destination: hooks/charmhelpers
include:
- core
- contrib.openstack|inc=*
- contrib.storage
- contrib.hahelpers:
- apache
- ceph
- cluster

1
hooks/install Symbolic link
View File

@ -0,0 +1 @@
nova_compute_relations.py

View File

@ -24,8 +24,7 @@ from charmhelpers.contrib.openstack.utils import (
)
from nova_compute_utils import (
PACKAGES,
RESTART_MAP,
determine_packages,
import_authorized_keys,
import_keystone_ca_cert,
migration_enabled,
@ -33,9 +32,11 @@ from nova_compute_utils import (
configure_network_service,
configure_volume_service,
do_openstack_upgrade,
quantum_attribute,
quantum_enabled,
quantum_plugin_config,
quantum_plugin,
public_ssh_key,
restart_map,
register_configs,
)
@ -51,11 +52,11 @@ CONFIGS = register_configs()
def install():
configure_installation_source(config('openstack-origin'))
apt_update()
apt_install(PACKAGES, fatal=True)
apt_install(determine_packages(), fatal=True)
@hooks.hook('config-changed')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def config_changed():
if openstack_upgrade_available('nova-common'):
do_openstack_upgrade()
@ -68,13 +69,13 @@ def config_changed():
@hooks.hook('amqp-relation-joined')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def amqp_joined():
relation_set(username=config('rabbit-user'), vhost=config('rabbit-vhost'))
@hooks.hook('amqp-relation-changed')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def amqp_changed():
if 'amqp' not in CONFIGS.complete_contexts():
log('amqp relation incomplete. Peer not ready?')
@ -91,18 +92,19 @@ def db_joined():
@hooks.hook('shared-db-relation-changed')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def db_changed():
if 'shared-db' not in CONFIGS.complete_contexts():
log('shared-db relation incomplete. Peer not ready?')
return
CONFIGS.write('/etc/nova/nova.conf')
if quantum_enabled():
CONFIGS.write(quantum_plugin_config())
plugin = quantum_plugin()
CONFIGS.write(quantum_attribute(plugin, 'config'))
@hooks.hook('image-service-relation-changed')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def image_service_changed():
if 'image-service' not in CONFIGS.complete_contexts():
log('image-service relation incomplete. Peer not ready?')
@ -124,7 +126,7 @@ def compute_joined(rid=None):
@hooks.hook('cloud-compute-relation-changed')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def compute_changed():
configure_network_service()
configure_volume_service()
@ -133,7 +135,7 @@ def compute_changed():
@hooks.hook('ceph-relation-joined')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def ceph_joined():
if not os.path.isdir('/etc/ceph'):
os.mkdir('/etc/ceph')
@ -141,7 +143,7 @@ def ceph_joined():
@hooks.hook('ceph-relation-changed')
@restart_on_change(RESTART_MAP)
@restart_on_change(restart_map())
def ceph_changed():
if 'ceph' not in CONFIGS.complete_contexts():
log('ceph relation incomplete. Peer not ready?')

View File

@ -1,12 +1,47 @@
import copy
from charmhelpers.core.hookenv import (
config,
log,
related_units,
relation_ids,
relation_get,
ERROR,
)
PACKAGES = []
BASE_PACKAGES = [
'nova-compute',
'genisoimage', # was missing as a package dependency until raring.
]
RESTART_MAP = {
BASE_RESTART_MAP = {
'/etc/libvirt/qemu.conf': ['libvirt-bin'],
'/etc/default/libvirt-bin': ['libvirt-bin']
'/etc/default/libvirt-bin': ['libvirt-bin'],
'/etc/nova/nova.conf': ['nova-compute'],
'/etc/nova/nova-compute.conf': ['nova-compute'],
}
QUANTUM_PLUGINS = {
'ovs': {
'config': '/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini',
'services': ['quantum-plugin-openvswitch-agent'],
'packages': ['quantum-plugin-openvswitch-agent',
'openvswitch-datapath-dkms'],
},
'nvp': {
'config': '/etc/quantum/plugins/nicira/nvp.ini',
'services': [],
'packages': ['quantum-plugin-nicira'],
}
}
# Maps virt-type config to a compute package(s).
VIRT_TYPES = {
'kvm': ['nova-compute-kvm'],
'qemu': ['nova-compute-qemu'],
'xen': ['nova-compute-xen'],
'uml': ['nova-compute-uml'],
'lxc': ['nova-compute-lxc'],
}
# This is just a label and it must be consistent across
@ -14,6 +49,58 @@ RESTART_MAP = {
CEPH_SECRET_UUID = '514c9fca-8cbe-11e2-9c52-3bc8c7819472'
def restart_map():
'''
Constructs a restart map based on charm config settings and relation
state.
'''
_restart_map = copy.copy(BASE_RESTART_MAP)
net_manager = network_manager()
if (net_manager in ['FlatManager', 'FlatDHCPManager'] and
config('multi-host').lower() == 'yes'):
_restart_map['/etc/nova/nova.conf'].extend(
['nova-api', 'nova-network']
)
elif net_manager == 'Quantum':
plugin = quantum_plugin()
if plugin:
conf = quantum_attribute(plugin, 'config')
svcs = quantum_attribute(plugin, 'services')
_restart_map[conf] = svcs
_restart_map['/etc/quantum/quantum.conf'] = svcs
return _restart_map
def determine_packages():
packages = [] + BASE_PACKAGES
net_manager = network_manager()
if (net_manager in ['FlatManager', 'FlatDHCPManager'] and
config('multi-host').lower() == 'yes'):
packages.extend(['nova-api', 'nova-network'])
elif net_manager == 'Quantum':
plugin = quantum_plugin()
packages.extend(quantum_attribute(plugin, 'packages'))
if relation_ids('ceph'):
packages.append('ceph-common')
virt_type = config('virt-type')
try:
packages.extend(VIRT_TYPES[virt_type])
except KeyError:
log('Unsupported virt-type configured: %s' % virt_type)
raise
return packages
def register_configs():
pass
def migration_enabled():
return config('enable-live-migration').lower() == 'true'
@ -22,8 +109,37 @@ def quantum_enabled():
return config('network-manager').lower() == 'quantum'
def quantum_plugin_config():
pass
def _network_config():
'''
Obtain all relevant network configuration settings from nova-c-c via
cloud-compute interface.
'''
settings = ['network_manager', 'quantum_plugin']
net_config = {}
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
for setting in settings:
value = relation_get(setting, rid=rid, unit=unit)
if value:
net_config[setting] = value
return net_config
def quantum_plugin():
return _network_config().get('quantum_plugin')
def network_manager():
return _network_config().get('network_manager')
def quantum_attribute(plugin, attr):
try:
_plugin = QUANTUM_PLUGINS[plugin]
except KeyError:
log('Unrecognised plugin for quantum: %s' % plugin, level=ERROR)
raise
return _plugin[attr]
def public_ssh_key(user='root'):
@ -59,10 +175,6 @@ def do_openstack_upgrade():
pass
def register_configs():
pass
def import_keystone_ca_cert():
pass

0
tests/__init__.py Normal file
View File

View File

@ -0,0 +1,257 @@
from mock import call, patch, MagicMock
from tests.test_utils import CharmTestCase
import hooks.nova_compute_utils as utils
_reg = utils.register_configs
_map = utils.restart_map
utils.register_configs = MagicMock()
utils.restart_map = MagicMock()
import hooks.nova_compute_relations as relations
utils.register_configs = _reg
utils.restart_map = _map
TO_PATCH = [
# charmhelpers.core.hookenv
'Hooks',
'config',
'log',
'relation_ids',
'relation_set',
'service_name',
'unit_get',
# charmhelpers.core.host
'apt_install',
'apt_update',
'restart_on_change',
#charmhelpers.contrib.openstack.utils
'configure_installation_source',
'openstack_upgrade_available',
# nova_compute_utils
#'PACKAGES',
'restart_map',
'determine_packages',
'import_authorized_keys',
'import_keystone_ca_cert',
'migration_enabled',
'configure_live_migration',
'configure_network_service',
'configure_volume_service',
'do_openstack_upgrade',
'quantum_attribute',
'quantum_enabled',
'quantum_plugin',
'public_ssh_key',
'register_configs',
# misc_utils
'ensure_ceph_keyring',
]
class NovaComputeRelationsTests(CharmTestCase):
def setUp(self):
super(NovaComputeRelationsTests, self).setUp(relations,
TO_PATCH)
self.config.side_effect = self.test_config.get
def test_install_hook(self):
repo = 'cloud:precise-grizzly'
self.test_config.set('openstack-origin', repo)
self.determine_packages.return_value = ['foo', 'bar']
relations.install()
self.configure_installation_source.assert_called_with(repo)
self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
def test_config_changed_with_upgrade(self):
self.openstack_upgrade_available.return_value = True
relations.config_changed()
self.assertTrue(self.do_openstack_upgrade.called)
@patch.object(relations, 'compute_joined')
def test_config_changed_with_migration(self, compute_joined):
self.migration_enabled.return_value = True
self.test_config.set('migration-auth-type', 'ssh')
self.relation_ids.return_value = [
'cloud-compute:0',
'cloud-compute:1'
]
relations.config_changed()
ex = [
call('cloud-compute:0'),
call('cloud-compute:1'),
]
self.assertEquals(ex, compute_joined.call_args_list)
@patch.object(relations, 'compute_joined')
def test_config_changed_no_upgrade_no_migration(self, compute_joined):
self.openstack_upgrade_available.return_value = False
self.migration_enabled.return_value = False
relations.config_changed()
self.assertFalse(self.do_openstack_upgrade.called)
self.assertTrue(self.configure_live_migration)
self.assertFalse(compute_joined.called)
def test_amqp_joined(self):
relations.amqp_joined()
self.relation_set.assert_called_with(username='nova', vhost='nova')
@patch.object(relations, 'CONFIGS')
def test_amqp_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
relations.amqp_changed()
self.log.assert_called_with(
'amqp relation incomplete. Peer not ready?'
)
def _amqp_test(self, configs, quantum=False):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['amqp']
configs.write = MagicMock()
self.quantum_enabled.return_value = quantum
relations.amqp_changed()
@patch.object(relations, 'CONFIGS')
def test_amqp_changed_with_data_no_quantum(self, configs):
self._amqp_test(configs, quantum=False)
self.assertEquals([call('/etc/nova/nova.conf')],
configs.write.call_args_list)
@patch.object(relations, 'CONFIGS')
def test_amqp_changed_with_data_and_quantum(self, configs):
self._amqp_test(configs, quantum=True)
self.assertEquals([call('/etc/nova/nova.conf'),
call('/etc/quantum/quantum.conf')],
configs.write.call_args_list)
def test_db_joined(self):
self.unit_get.return_value = 'nova.foohost.com'
relations.db_joined()
self.relation_set.assert_called_with(database='nova', username='nova',
hostname='nova.foohost.com')
self.unit_get.assert_called_with('private-address')
@patch.object(relations, 'CONFIGS')
def test_db_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
relations.db_changed()
self.log.assert_called_with(
'shared-db relation incomplete. Peer not ready?'
)
def _shared_db_test(self, configs, quantum=False):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['shared-db']
configs.write = MagicMock()
self.quantum_enabled.return_value = quantum
relations.db_changed()
@patch.object(relations, 'CONFIGS')
def test_db_changed_with_data_no_quantum(self, configs):
self._shared_db_test(configs, quantum=False)
self.assertEquals([call('/etc/nova/nova.conf')],
configs.write.call_args_list)
@patch.object(relations, 'CONFIGS')
def test_db_changed_with_data_and_quantum(self, configs):
self.quantum_attribute.return_value = '/etc/quantum/plugin.conf'
self._shared_db_test(configs, quantum=True)
ex = [call('/etc/nova/nova.conf'), call('/etc/quantum/plugin.conf')]
self.assertEquals(ex, configs.write.call_args_list)
@patch.object(relations, 'CONFIGS')
def test_image_service_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
relations.image_service_changed()
self.log.assert_called_with(
'image-service relation incomplete. Peer not ready?'
)
@patch.object(relations, 'CONFIGS')
def test_image_service_with_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.write = MagicMock()
configs.complete_contexts.return_value = ['image-service']
relations.image_service_changed()
configs.write.assert_called_with('/etc/nova/nova.conf')
def test_compute_joined_no_migration(self):
self.migration_enabled.return_value = False
relations.compute_joined()
self.assertFalse(self.relation_set.called)
def test_compute_joined_with_ssh_migration(self):
self.migration_enabled.return_value = True
self.test_config.set('migration-auth-type', 'ssh')
self.public_ssh_key.return_value = 'foo'
relations.compute_joined()
self.relation_set.assert_called_with(
relation_id=None,
ssh_public_key='foo',
migration_auth_type='ssh'
)
relations.compute_joined(rid='cloud-compute:2')
self.relation_set.assert_called_with(
relation_id='cloud-compute:2',
ssh_public_key='foo',
migration_auth_type='ssh'
)
def test_compute_changed(self):
relations.compute_changed()
expected_funcs = [
self.configure_network_service,
self.configure_volume_service,
self.import_authorized_keys,
self.import_keystone_ca_cert,
]
for func in expected_funcs:
self.assertTrue(func.called)
@patch('os.mkdir')
@patch('os.path.isdir')
def test_ceph_joined(self, isdir, mkdir):
isdir.return_value = False
relations.ceph_joined()
mkdir.assert_called_with('/etc/ceph')
self.apt_install.assert_called_with('ceph-common')
@patch.object(relations, 'CONFIGS')
def test_ceph_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
relations.ceph_changed()
self.log.assert_called_with(
'ceph relation incomplete. Peer not ready?'
)
@patch.object(relations, 'CONFIGS')
def test_ceph_changed_no_keyring(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['ceph']
self.ensure_ceph_keyring.return_value = False
relations.ceph_changed()
self.log.assert_called_with(
'Could not create ceph keyring: peer not ready?'
)
@patch.object(relations, 'CONFIGS')
def test_ceph_changed_with_key_and_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['ceph']
configs.write = MagicMock()
self.ensure_ceph_keyring.return_value = True
relations.ceph_changed()
ex = [
call('/etc/ceph/ceph.conf'),
call('/etc/ceph/secret.xml'),
call('/etc/nova/nova.conf'),
]
self.assertEquals(ex, configs.write.call_args_list)

View File

@ -0,0 +1,105 @@
from mock import patch
from tests.test_utils import CharmTestCase
import hooks.nova_compute_utils as utils
TO_PATCH = [
'config',
'log',
'related_units',
'relation_ids',
'relation_get',
]
class NovaComputeUtilsTests(CharmTestCase):
def setUp(self):
super(NovaComputeUtilsTests, self).setUp(utils, TO_PATCH)
self.config.side_effect = self.test_config.get
@patch.object(utils, 'network_manager')
def test_determine_packages_nova_network(self, net_man):
net_man.return_value = 'FlatDHCPManager'
self.relation_ids.return_value = []
result = utils.determine_packages()
ex = utils.BASE_PACKAGES + [
'nova-api',
'nova-network',
'nova-compute-kvm'
]
self.assertEquals(ex, result)
@patch.object(utils, 'quantum_plugin')
@patch.object(utils, 'network_manager')
def test_determine_packages_quantum(self, net_man, q_plugin):
net_man.return_value = 'Quantum'
q_plugin.return_value = 'ovs'
self.relation_ids.return_value = []
result = utils.determine_packages()
ex = utils.BASE_PACKAGES + [
'quantum-plugin-openvswitch-agent',
'openvswitch-datapath-dkms',
'nova-compute-kvm'
]
self.assertEquals(ex, result)
@patch.object(utils, 'quantum_plugin')
@patch.object(utils, 'network_manager')
def test_determine_packages_quantum_ceph(self, net_man, q_plugin):
net_man.return_value = 'Quantum'
q_plugin.return_value = 'ovs'
self.relation_ids.return_value = ['ceph:0']
result = utils.determine_packages()
ex = utils.BASE_PACKAGES + [
'quantum-plugin-openvswitch-agent',
'openvswitch-datapath-dkms',
'ceph-common',
'nova-compute-kvm'
]
self.assertEquals(ex, result)
# NOTE: These tests faill if run together, something is holding
# a reference to BASE_RESOURCE_MAP ?
@patch.object(utils, 'network_manager')
def test_resource_map_nova_network_no_multihost(self, net_man):
self.test_config.set('multi-host', 'no')
net_man.return_value = 'FlatDHCPManager'
result = utils.restart_map()
ex = {
'/etc/default/libvirt-bin': ['libvirt-bin'],
'/etc/libvirt/qemu.conf': ['libvirt-bin'],
'/etc/nova/nova-compute.conf': ['nova-compute'],
'/etc/nova/nova.conf': ['nova-compute']
}
self.assertEquals(ex, result)
@patch.object(utils, 'network_manager')
def test_resource_map_nova_network(self, net_man):
net_man.return_value = 'FlatDHCPManager'
result = utils.restart_map()
ex = {
'/etc/default/libvirt-bin': ['libvirt-bin'],
'/etc/libvirt/qemu.conf': ['libvirt-bin'],
'/etc/nova/nova-compute.conf': ['nova-compute'],
'/etc/nova/nova.conf': ['nova-compute', 'nova-api', 'nova-network']
}
self.assertEquals(ex, result)
@patch.object(utils, 'quantum_plugin')
@patch.object(utils, 'network_manager')
def test_resource_map_quantum_ovs(self, net_man, _plugin):
net_man.return_value = 'Quantum'
_plugin.return_value = 'ovs'
result = utils.restart_map()
ex = {
'/etc/default/libvirt-bin': ['libvirt-bin'],
'/etc/libvirt/qemu.conf': ['libvirt-bin'],
'/etc/nova/nova-compute.conf': ['nova-compute'],
'/etc/nova/nova.conf': ['nova-compute'],
'/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini':
['quantum-plugin-openvswitch-agent'],
'/etc/quantum/quantum.conf': ['quantum-plugin-openvswitch-agent']
}
self.assertEquals(ex, result)

101
tests/test_utils.py Normal file
View File

@ -0,0 +1,101 @@
import logging
import unittest
import os
import yaml
from mock import patch
def load_config():
'''
Walk backwords from __file__ looking for config.yaml, load and return the
'options' section'
'''
config = None
f = __file__
while config is None:
d = os.path.dirname(f)
if os.path.isfile(os.path.join(d, 'config.yaml')):
config = os.path.join(d, 'config.yaml')
break
f = d
if not config:
logging.error('Could not find config.yaml in any parent directory '
'of %s. ' % file)
raise Exception
return yaml.safe_load(open(config).read())['options']
def get_default_config():
'''
Load default charm config from config.yaml return as a dict.
If no default is set in config.yaml, its value is None.
'''
default_config = {}
config = load_config()
for k, v in config.iteritems():
if 'default' in v:
default_config[k] = v['default']
else:
default_config[k] = None
return default_config
class CharmTestCase(unittest.TestCase):
def setUp(self, obj, patches):
super(CharmTestCase, self).setUp()
self.patches = patches
self.obj = obj
self.test_config = TestConfig()
self.test_relation = TestRelation()
self.patch_all()
def patch(self, method):
_m = patch.object(self.obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_all(self):
for method in self.patches:
setattr(self, method, self.patch(method))
class TestConfig(object):
def __init__(self):
self.config = get_default_config()
def get(self, attr=None):
if not attr:
return self.get_all()
try:
return self.config[attr]
except KeyError:
return None
def get_all(self):
return self.config
def set(self, attr, value):
if attr not in self.config:
raise KeyError
self.config[attr] = value
class TestRelation(object):
def __init__(self, relation_data={}):
self.relation_data = relation_data
def set(self, relation_data):
self.relation_data = relation_data
def get(self, attr=None, unit=None, rid=None):
if attr == None:
return self.relation_data
elif attr in self.relation_data:
return self.relation_data[attr]
return None