[gandelman-a] Merge of changes for ODS keynotes including NVP support and service restart on db changes.

This commit is contained in:
James Page 2013-11-17 21:47:29 +00:00
commit 957ffb3216
23 changed files with 450 additions and 82 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>nova-compute</name>
<name>nvp-nova-compute</name>
<comment></comment>
<projects>
</projects>

View File

@ -3,7 +3,7 @@
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/nova-compute/hooks</path>
<path>/nova-compute/unit_tests</path>
<path>/nvp-nova-compute/hooks</path>
<path>/nvp-nova-compute/unit_tests</path>
</pydev_pathproperty>
</pydev_project>

View File

@ -69,4 +69,7 @@ def get_certificate():
def full_restart():
''' Full restart and reload of openvswitch '''
service('force-reload-kmod', 'openvswitch-switch')
if os.path.exists('/etc/init/openvswitch-force-reload-kmod.conf'):
service('start', 'openvswitch-force-reload-kmod')
else:
service('force-reload-kmod', 'openvswitch-switch')

View File

@ -0,0 +1,17 @@
''' Helper for managing alternatives for file conflict resolution '''
import subprocess
import shutil
import os
def install_alternative(name, target, source, priority=50):
''' Install alternative configuration '''
if (os.path.exists(target) and not os.path.islink(target)):
# Move existing file/directory away before installing
shutil.move(target, '{}.bak'.format(target))
cmd = [
'update-alternatives', '--force', '--install',
target, name, source, str(priority)
]
subprocess.check_call(cmd)

View File

@ -385,16 +385,33 @@ class NeutronContext(object):
def ovs_ctxt(self):
driver = neutron_plugin_attribute(self.plugin, 'driver',
self.network_manager)
config = neutron_plugin_attribute(self.plugin, 'config',
self.network_manager)
ovs_ctxt = {
'core_plugin': driver,
'neutron_plugin': 'ovs',
'neutron_security_groups': self.neutron_security_groups,
'local_ip': unit_private_ip(),
'config': config
}
return ovs_ctxt
def nvp_ctxt(self):
driver = neutron_plugin_attribute(self.plugin, 'driver',
self.network_manager)
config = neutron_plugin_attribute(self.plugin, 'config',
self.network_manager)
nvp_ctxt = {
'core_plugin': driver,
'neutron_plugin': 'nvp',
'neutron_security_groups': self.neutron_security_groups,
'local_ip': unit_private_ip(),
'config': config
}
return nvp_ctxt
def __call__(self):
self._ensure_packages()
@ -408,6 +425,8 @@ class NeutronContext(object):
if self.plugin == 'ovs':
ctxt.update(self.ovs_ctxt())
elif self.plugin == 'nvp':
ctxt.update(self.nvp_ctxt())
self._save_flag_file()
return ctxt

View File

@ -34,13 +34,23 @@ def quantum_plugins():
'services': ['quantum-plugin-openvswitch-agent'],
'packages': [[headers_package(), 'openvswitch-datapath-dkms'],
['quantum-plugin-openvswitch-agent']],
'server_packages': ['quantum-server',
'quantum-plugin-openvswitch'],
'server_services': ['quantum-server']
},
'nvp': {
'config': '/etc/quantum/plugins/nicira/nvp.ini',
'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.'
'QuantumPlugin.NvpPluginV2',
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
'services': [],
'packages': [],
'server_packages': ['quantum-server',
'quantum-plugin-nicira'],
'server_services': ['quantum-server']
}
}
@ -60,13 +70,23 @@ def neutron_plugins():
'services': ['neutron-plugin-openvswitch-agent'],
'packages': [[headers_package(), 'openvswitch-datapath-dkms'],
['quantum-plugin-openvswitch-agent']],
'server_packages': ['neutron-server',
'neutron-plugin-openvswitch'],
'server_services': ['neutron-server']
},
'nvp': {
'config': '/etc/neutron/plugins/nicira/nvp.ini',
'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.'
'NeutronPlugin.NvpPluginV2',
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
'services': [],
'packages': [],
'server_packages': ['neutron-server',
'neutron-plugin-nicira'],
'server_services': ['neutron-server']
}
}

View File

@ -13,19 +13,28 @@ from charmhelpers.core.hookenv import (
config,
log as juju_log,
charm_dir,
ERROR,
INFO
)
from charmhelpers.core.host import (
lsb_release,
from charmhelpers.contrib.storage.linux.lvm import (
deactivate_lvm_volume_group,
is_lvm_physical_volume,
remove_lvm_physical_volume,
)
from charmhelpers.fetch import (
apt_install,
)
from charmhelpers.core.host import lsb_release, mounts, umount
from charmhelpers.fetch import apt_install
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
'restricted main multiverse universe')
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
('oneiric', 'diablo'),
('precise', 'essex'),
@ -57,6 +66,8 @@ SWIFT_CODENAMES = OrderedDict([
('1.9.0', 'havana'),
])
DEFAULT_LOOPBACK_SIZE = '5G'
def error_out(msg):
juju_log("FATAL ERROR: %s" % msg, level='ERROR')
@ -67,7 +78,7 @@ def get_os_codename_install_source(src):
'''Derive OpenStack release codename from a given installation source.'''
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
rel = ''
if src == 'distro':
if src in ['distro', 'distro-proposed']:
try:
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
except KeyError:
@ -202,6 +213,10 @@ def configure_installation_source(rel):
'''Configure apt installation source.'''
if rel == 'distro':
return
elif rel == 'distro-proposed':
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
f.write(DISTRO_PROPOSED % ubuntu_rel)
elif rel[:4] == "ppa:":
src = rel
subprocess.check_call(["add-apt-repository", "-y", src])
@ -299,6 +314,62 @@ def openstack_upgrade_available(package):
return apt.version_compare(available_vers, cur_vers) == 1
def ensure_block_device(block_device):
'''
Confirm block_device, create as loopback if necessary.
:param block_device: str: Full path of block device to ensure.
:returns: str: Full path of ensured block device.
'''
_none = ['None', 'none', None]
if (block_device in _none):
error_out('prepare_storage(): Missing required input: '
'block_device=%s.' % block_device, level=ERROR)
if block_device.startswith('/dev/'):
bdev = block_device
elif block_device.startswith('/'):
_bd = block_device.split('|')
if len(_bd) == 2:
bdev, size = _bd
else:
bdev = block_device
size = DEFAULT_LOOPBACK_SIZE
bdev = ensure_loopback_device(bdev, size)
else:
bdev = '/dev/%s' % block_device
if not is_block_device(bdev):
error_out('Failed to locate valid block device at %s' % bdev,
level=ERROR)
return bdev
def clean_storage(block_device):
'''
Ensures a block device is clean. That is:
- unmounted
- any lvm volume groups are deactivated
- any lvm physical device signatures removed
- partition table wiped
:param block_device: str: Full path to block device to clean.
'''
for mp, d in mounts():
if d == block_device:
juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
(d, mp), level=INFO)
umount(mp, persist=True)
if is_lvm_physical_volume(block_device):
deactivate_lvm_volume_group(block_device)
remove_lvm_physical_volume(block_device)
else:
zap_disk(block_device)
def is_ip(address):
"""
Returns True if address is a valid IP address.

View File

@ -102,8 +102,12 @@ def get_osds(service):
Return a list of all Ceph Object Storage Daemons
currently in the cluster
'''
return json.loads(check_output(['ceph', '--id', service,
'osd', 'ls', '--format=json']))
version = ceph_version()
if version and version >= '0.56':
return json.loads(check_output(['ceph', '--id', service,
'osd', 'ls', '--format=json']))
else:
return None
def create_pool(service, name, replicas=2):
@ -114,7 +118,13 @@ def create_pool(service, name, replicas=2):
return
# Calculate the number of placement groups based
# on upstream recommended best practices.
pgnum = (len(get_osds(service)) * 100 / replicas)
osds = get_osds(service)
if osds:
pgnum = (len(osds) * 100 / replicas)
else:
# NOTE(james-page): Default to 200 for older ceph versions
# which don't support OSD query from cli
pgnum = 200
cmd = [
'ceph', '--id', service,
'osd', 'pool', 'create',
@ -357,3 +367,17 @@ def ensure_ceph_keyring(service, user=None, group=None):
if user and group:
check_call(['chown', '%s.%s' % (user, group), keyring])
return True
def ceph_version():
''' Retrieve the local version of ceph '''
if os.path.exists('/usr/bin/ceph'):
cmd = ['ceph', '-v']
output = check_output(cmd)
output = output.split()
if len(output) > 3:
return output[2]
else:
return None
else:
return None

View File

@ -9,6 +9,7 @@ import json
import yaml
import subprocess
import UserDict
from subprocess import CalledProcessError
CRITICAL = "CRITICAL"
ERROR = "ERROR"
@ -21,7 +22,7 @@ cache = {}
def cached(func):
''' Cache return values for multiple executions of func + args
"""Cache return values for multiple executions of func + args
For example:
@ -32,7 +33,7 @@ def cached(func):
unit_get('test')
will cache the result of unit_get + 'test' for future calls.
'''
"""
def wrapper(*args, **kwargs):
global cache
key = str((func, args, kwargs))
@ -46,8 +47,8 @@ def cached(func):
def flush(key):
''' Flushes any entries from function cache where the
key is found in the function+args '''
"""Flushes any entries from function cache where the
key is found in the function+args """
flush_list = []
for item in cache:
if key in item:
@ -57,7 +58,7 @@ def flush(key):
def log(message, level=None):
"Write a message to the juju log"
"""Write a message to the juju log"""
command = ['juju-log']
if level:
command += ['-l', level]
@ -66,7 +67,7 @@ def log(message, level=None):
class Serializable(UserDict.IterableUserDict):
"Wrapper, an object that can be serialized to yaml or json"
"""Wrapper, an object that can be serialized to yaml or json"""
def __init__(self, obj):
# wrap the object
@ -96,11 +97,11 @@ class Serializable(UserDict.IterableUserDict):
self.data = state
def json(self):
"Serialize the object to json"
"""Serialize the object to json"""
return json.dumps(self.data)
def yaml(self):
"Serialize the object to yaml"
"""Serialize the object to yaml"""
return yaml.dump(self.data)
@ -119,38 +120,38 @@ def execution_environment():
def in_relation_hook():
"Determine whether we're running in a relation hook"
"""Determine whether we're running in a relation hook"""
return 'JUJU_RELATION' in os.environ
def relation_type():
"The scope for the current relation hook"
"""The scope for the current relation hook"""
return os.environ.get('JUJU_RELATION', None)
def relation_id():
"The relation ID for the current relation hook"
"""The relation ID for the current relation hook"""
return os.environ.get('JUJU_RELATION_ID', None)
def local_unit():
"Local unit ID"
"""Local unit ID"""
return os.environ['JUJU_UNIT_NAME']
def remote_unit():
"The remote unit for the current relation hook"
"""The remote unit for the current relation hook"""
return os.environ['JUJU_REMOTE_UNIT']
def service_name():
"The name service group this unit belongs to"
"""The name service group this unit belongs to"""
return local_unit().split('/')[0]
@cached
def config(scope=None):
"Juju charm configuration"
"""Juju charm configuration"""
config_cmd_line = ['config-get']
if scope is not None:
config_cmd_line.append(scope)
@ -163,6 +164,7 @@ def config(scope=None):
@cached
def relation_get(attribute=None, unit=None, rid=None):
"""Get relation information"""
_args = ['relation-get', '--format=json']
if rid:
_args.append('-r')
@ -174,9 +176,14 @@ def relation_get(attribute=None, unit=None, rid=None):
return json.loads(subprocess.check_output(_args))
except ValueError:
return None
except CalledProcessError, e:
if e.returncode == 2:
return None
raise
def relation_set(relation_id=None, relation_settings={}, **kwargs):
"""Set relation information for the current unit"""
relation_cmd_line = ['relation-set']
if relation_id is not None:
relation_cmd_line.extend(('-r', relation_id))
@ -192,7 +199,7 @@ def relation_set(relation_id=None, relation_settings={}, **kwargs):
@cached
def relation_ids(reltype=None):
"A list of relation_ids"
"""A list of relation_ids"""
reltype = reltype or relation_type()
relid_cmd_line = ['relation-ids', '--format=json']
if reltype is not None:
@ -203,7 +210,7 @@ def relation_ids(reltype=None):
@cached
def related_units(relid=None):
"A list of related units"
"""A list of related units"""
relid = relid or relation_id()
units_cmd_line = ['relation-list', '--format=json']
if relid is not None:
@ -213,7 +220,7 @@ def related_units(relid=None):
@cached
def relation_for_unit(unit=None, rid=None):
"Get the json represenation of a unit's relation"
"""Get the json represenation of a unit's relation"""
unit = unit or remote_unit()
relation = relation_get(unit=unit, rid=rid)
for key in relation:
@ -225,7 +232,7 @@ def relation_for_unit(unit=None, rid=None):
@cached
def relations_for_id(relid=None):
"Get relations of a specific relation ID"
"""Get relations of a specific relation ID"""
relation_data = []
relid = relid or relation_ids()
for unit in related_units(relid):
@ -237,7 +244,7 @@ def relations_for_id(relid=None):
@cached
def relations_of_type(reltype=None):
"Get relations of a specific type"
"""Get relations of a specific type"""
relation_data = []
reltype = reltype or relation_type()
for relid in relation_ids(reltype):
@ -249,7 +256,7 @@ def relations_of_type(reltype=None):
@cached
def relation_types():
"Get a list of relation types supported by this charm"
"""Get a list of relation types supported by this charm"""
charmdir = os.environ.get('CHARM_DIR', '')
mdf = open(os.path.join(charmdir, 'metadata.yaml'))
md = yaml.safe_load(mdf)
@ -264,6 +271,7 @@ def relation_types():
@cached
def relations():
"""Get a nested dictionary of relation data for all related units"""
rels = {}
for reltype in relation_types():
relids = {}
@ -277,15 +285,35 @@ def relations():
return rels
@cached
def is_relation_made(relation, keys='private-address'):
'''
Determine whether a relation is established by checking for
presence of key(s). If a list of keys is provided, they
must all be present for the relation to be identified as made
'''
if isinstance(keys, str):
keys = [keys]
for r_id in relation_ids(relation):
for unit in related_units(r_id):
context = {}
for k in keys:
context[k] = relation_get(k, rid=r_id,
unit=unit)
if None not in context.values():
return True
return False
def open_port(port, protocol="TCP"):
"Open a service network port"
"""Open a service network port"""
_args = ['open-port']
_args.append('{}/{}'.format(port, protocol))
subprocess.check_call(_args)
def close_port(port, protocol="TCP"):
"Close a service network port"
"""Close a service network port"""
_args = ['close-port']
_args.append('{}/{}'.format(port, protocol))
subprocess.check_call(_args)
@ -293,6 +321,7 @@ def close_port(port, protocol="TCP"):
@cached
def unit_get(attribute):
"""Get the unit ID for the remote unit"""
_args = ['unit-get', '--format=json', attribute]
try:
return json.loads(subprocess.check_output(_args))
@ -301,22 +330,46 @@ def unit_get(attribute):
def unit_private_ip():
"""Get this unit's private IP address"""
return unit_get('private-address')
class UnregisteredHookError(Exception):
"""Raised when an undefined hook is called"""
pass
class Hooks(object):
"""A convenient handler for hook functions.
Example:
hooks = Hooks()
# register a hook, taking its name from the function name
@hooks.hook()
def install():
...
# register a hook, providing a custom hook name
@hooks.hook("config-changed")
def config_changed():
...
if __name__ == "__main__":
# execute a hook based on the name the program is called by
hooks.execute(sys.argv)
"""
def __init__(self):
super(Hooks, self).__init__()
self._hooks = {}
def register(self, name, function):
"""Register a hook"""
self._hooks[name] = function
def execute(self, args):
"""Execute a registered hook based on args[0]"""
hook_name = os.path.basename(args[0])
if hook_name in self._hooks:
self._hooks[hook_name]()
@ -324,6 +377,7 @@ class Hooks(object):
raise UnregisteredHookError(hook_name)
def hook(self, *hook_names):
"""Decorator, registering them as hooks"""
def wrapper(decorated):
for hook_name in hook_names:
self.register(hook_name, decorated)
@ -337,4 +391,5 @@ class Hooks(object):
def charm_dir():
"""Return the root directory of the current charm"""
return os.environ.get('CHARM_DIR')

View File

@ -19,18 +19,22 @@ from hookenv import log
def service_start(service_name):
"""Start a system service"""
return service('start', service_name)
def service_stop(service_name):
"""Stop a system service"""
return service('stop', service_name)
def service_restart(service_name):
"""Restart a system service"""
return service('restart', service_name)
def service_reload(service_name, restart_on_failure=False):
"""Reload a system service, optionally falling back to restart if reload fails"""
service_result = service('reload', service_name)
if not service_result and restart_on_failure:
service_result = service('restart', service_name)
@ -38,11 +42,13 @@ def service_reload(service_name, restart_on_failure=False):
def service(action, service_name):
"""Control a system service"""
cmd = ['service', service_name, action]
return subprocess.call(cmd) == 0
def service_running(service):
"""Determine whether a system service is running"""
try:
output = subprocess.check_output(['service', service, 'status'])
except subprocess.CalledProcessError:
@ -55,7 +61,7 @@ def service_running(service):
def adduser(username, password=None, shell='/bin/bash', system_user=False):
"""Add a user"""
"""Add a user to the system"""
try:
user_info = pwd.getpwnam(username)
log('user {0} already exists!'.format(username))
@ -138,7 +144,7 @@ def write_file(path, content, owner='root', group='root', perms=0444):
def mount(device, mountpoint, options=None, persist=False):
'''Mount a filesystem'''
"""Mount a filesystem at a particular mountpoint"""
cmd_args = ['mount']
if options is not None:
cmd_args.extend(['-o', options])
@ -155,7 +161,7 @@ def mount(device, mountpoint, options=None, persist=False):
def umount(mountpoint, persist=False):
'''Unmount a filesystem'''
"""Unmount a filesystem"""
cmd_args = ['umount', mountpoint]
try:
subprocess.check_output(cmd_args)
@ -169,7 +175,7 @@ def umount(mountpoint, persist=False):
def mounts():
'''List of all mounted volumes as [[mountpoint,device],[...]]'''
"""Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
with open('/proc/mounts') as f:
# [['/mount/point','/dev/path'],[...]]
system_mounts = [m[1::-1] for m in [l.strip().split()
@ -178,7 +184,7 @@ def mounts():
def file_hash(path):
''' Generate a md5 hash of the contents of 'path' or None if not found '''
"""Generate a md5 hash of the contents of 'path' or None if not found """
if os.path.exists(path):
h = hashlib.md5()
with open(path, 'r') as source:
@ -189,7 +195,7 @@ def file_hash(path):
def restart_on_change(restart_map):
''' Restart services based on configuration files changing
"""Restart services based on configuration files changing
This function is used a decorator, for example
@ -202,7 +208,7 @@ def restart_on_change(restart_map):
In this example, the cinder-api and cinder-volume services
would be restarted if /etc/ceph/ceph.conf is changed by the
ceph_client_changed function.
'''
"""
def wrap(f):
def wrapped_f(*args):
checksums = {}
@ -220,7 +226,7 @@ def restart_on_change(restart_map):
def lsb_release():
'''Return /etc/lsb-release in a dict'''
"""Return /etc/lsb-release in a dict"""
d = {}
with open('/etc/lsb-release', 'r') as lsb:
for l in lsb:
@ -230,7 +236,7 @@ def lsb_release():
def pwgen(length=None):
'''Generate a random pasword.'''
"""Generate a random pasword."""
if length is None:
length = random.choice(range(35, 45))
alphanumeric_chars = [

View File

@ -20,6 +20,32 @@ deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
PROPOSED_POCKET = """# Proposed
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
"""
CLOUD_ARCHIVE_POCKETS = {
# Folsom
'folsom': 'precise-updates/folsom',
'precise-folsom': 'precise-updates/folsom',
'precise-folsom/updates': 'precise-updates/folsom',
'precise-updates/folsom': 'precise-updates/folsom',
'folsom/proposed': 'precise-proposed/folsom',
'precise-folsom/proposed': 'precise-proposed/folsom',
'precise-proposed/folsom': 'precise-proposed/folsom',
# Grizzly
'grizzly': 'precise-updates/grizzly',
'precise-grizzly': 'precise-updates/grizzly',
'precise-grizzly/updates': 'precise-updates/grizzly',
'precise-updates/grizzly': 'precise-updates/grizzly',
'grizzly/proposed': 'precise-proposed/grizzly',
'precise-grizzly/proposed': 'precise-proposed/grizzly',
'precise-proposed/grizzly': 'precise-proposed/grizzly',
# Havana
'havana': 'precise-updates/havana',
'precise-havana': 'precise-updates/havana',
'precise-havana/updates': 'precise-updates/havana',
'precise-updates/havana': 'precise-updates/havana',
'havana/proposed': 'precise-proposed/havana',
'precies-havana/proposed': 'precise-proposed/havana',
'precise-proposed/havana': 'precise-proposed/havana',
}
def filter_installed_packages(packages):
@ -79,16 +105,35 @@ def apt_purge(packages, fatal=False):
subprocess.call(cmd)
def apt_hold(packages, fatal=False):
"""Hold one or more packages"""
cmd = ['apt-mark', 'hold']
if isinstance(packages, basestring):
cmd.append(packages)
else:
cmd.extend(packages)
log("Holding {}".format(packages))
if fatal:
subprocess.check_call(cmd)
else:
subprocess.call(cmd)
def add_source(source, key=None):
if ((source.startswith('ppa:') or
source.startswith('http:'))):
if (source.startswith('ppa:') or
source.startswith('http:') or
source.startswith('deb ') or
source.startswith('cloud-archive:')):
subprocess.check_call(['add-apt-repository', '--yes', source])
elif source.startswith('cloud:'):
apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
fatal=True)
pocket = source.split(':')[-1]
if pocket not in CLOUD_ARCHIVE_POCKETS:
raise SourceConfigError('Unsupported cloud: source option %s' % pocket)
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
apt.write(CLOUD_ARCHIVE.format(pocket))
apt.write(CLOUD_ARCHIVE.format(actual_pocket))
elif source == 'proposed':
release = lsb_release()['DISTRIB_CODENAME']
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
@ -118,8 +163,11 @@ def configure_sources(update=False,
Note that 'null' (a.k.a. None) should not be quoted.
"""
sources = safe_load(config(sources_var))
keys = safe_load(config(keys_var))
if isinstance(sources, basestring) and isinstance(keys, basestring):
keys = config(keys_var)
if keys is not None:
keys = safe_load(keys)
if isinstance(sources, basestring) and (
keys is None or isinstance(keys, basestring)):
add_source(sources, keys)
else:
if not len(sources) == len(keys):

View File

@ -12,6 +12,7 @@ except ImportError:
apt_install("python-bzrlib")
from bzrlib.branch import Branch
class BzrUrlFetchHandler(BaseFetchHandler):
"""Handler for bazaar branches via generic and lp URLs"""
def can_handle(self, source):
@ -46,4 +47,3 @@ class BzrUrlFetchHandler(BaseFetchHandler):
except OSError as e:
raise UnhandledSource(e.strerror)
return dest_dir

View File

@ -273,6 +273,14 @@ class CloudComputeContext(context.OSContextGenerator):
self.network_manager)
return ctxt
def restart_trigger(self):
rt = None
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
rt = relation_get('restart_trigger', rid=rid, unit=unit)
if rt:
return rt
def __call__(self):
rids = relation_ids('cloud-compute')
if not rids:
@ -289,6 +297,9 @@ class CloudComputeContext(context.OSContextGenerator):
if vol_service:
ctxt['volume_service'] = vol_service
if self.restart_trigger():
ctxt['restart_trigger'] = self.restart_trigger()
return ctxt

View File

@ -48,7 +48,7 @@ from nova_compute_utils import (
register_configs,
NOVA_CONF,
QUANTUM_CONF, NEUTRON_CONF,
CEPH_CONF, CEPH_SECRET
ceph_config_file, CEPH_SECRET
)
from nova_compute_context import CEPH_SECRET_UUID
@ -94,9 +94,10 @@ def amqp_changed():
log('amqp relation incomplete. Peer not ready?')
return
CONFIGS.write(NOVA_CONF)
if network_manager() == 'quantum':
if network_manager() == 'quantum' and neutron_plugin() == 'ovs':
CONFIGS.write(QUANTUM_CONF)
if network_manager() == 'neutron':
if network_manager() == 'neutron' and neutron_plugin() == 'ovs':
CONFIGS.write(NEUTRON_CONF)
@ -106,7 +107,8 @@ def db_joined(rid=None):
nova_database=config('database'),
nova_username=config('database-user'),
nova_hostname=unit_get('private-address'))
if network_manager() in ['quantum', 'neutron']:
if (network_manager() in ['quantum', 'neutron']
and neutron_plugin() == 'ovs'):
# XXX: Renaming relations from quantum_* to neutron_* here.
relation_set(relation_id=rid,
neutron_database=config('neutron-database'),
@ -122,8 +124,8 @@ def db_changed():
return
CONFIGS.write(NOVA_CONF)
nm = network_manager()
if nm in ['quantum', 'neutron']:
plugin = neutron_plugin()
plugin = neutron_plugin()
if nm in ['quantum', 'neutron'] and plugin == 'ovs':
CONFIGS.write(neutron_plugin_attribute(plugin, 'config', nm))
@ -157,7 +159,8 @@ def compute_changed():
CONFIGS.write_all()
import_authorized_keys()
import_keystone_ca_cert()
if network_manager() in ['quantum', 'neutron']:
if (network_manager() in ['quantum', 'neutron']
and neutron_plugin() == 'ovs'):
# in case we already have a database relation, need to request
# access to the additional neutron database.
[db_joined(rid) for rid in relation_ids('shared-db')]
@ -179,7 +182,7 @@ def ceph_changed():
if not ensure_ceph_keyring(service=svc):
log('Could not create ceph keyring: peer not ready?')
return
CONFIGS.write(CEPH_CONF)
CONFIGS.write(ceph_config_file())
CONFIGS.write(CEPH_SECRET)
CONFIGS.write(NOVA_CONF)

View File

@ -6,7 +6,7 @@ from copy import deepcopy
from subprocess import check_call, check_output
from charmhelpers.fetch import apt_update, apt_install
from charmhelpers.core.host import mkdir
from charmhelpers.core.hookenv import (
config,
log,
@ -14,10 +14,12 @@ from charmhelpers.core.hookenv import (
relation_ids,
relation_get,
DEBUG,
service_name
)
from charmhelpers.contrib.openstack.neutron import neutron_plugin_attribute
from charmhelpers.contrib.openstack import templating, context
from charmhelpers.contrib.openstack.alternatives import install_alternative
from charmhelpers.contrib.openstack.utils import (
configure_installation_source,
@ -72,13 +74,10 @@ BASE_RESOURCE_MAP = {
}
CEPH_CONF = '/etc/ceph/ceph.conf'
CHARM_CEPH_CONF = '/var/lib/charm/{}/ceph.conf'
CEPH_SECRET = '/etc/ceph/secret.xml'
CEPH_RESOURCES = {
CEPH_CONF: {
'contexts': [NovaComputeCephContext()],
'services': [],
},
CEPH_SECRET: {
'contexts': [NovaComputeCephContext()],
'services': [],
@ -114,6 +113,10 @@ VIRT_TYPES = {
}
def ceph_config_file():
return CHARM_CEPH_CONF.format(service_name())
def resource_map():
'''
Dynamically generate a map of resources that will be managed for a single
@ -122,6 +125,7 @@ def resource_map():
# TODO: Cache this on first call?
resource_map = deepcopy(BASE_RESOURCE_MAP)
net_manager = network_manager()
plugin = neutron_plugin()
# Network manager gets set late by the cloud-compute interface.
# FlatDHCPManager only requires some extra packages.
@ -133,17 +137,15 @@ def resource_map():
# Neutron/quantum requires additional contexts, as well as new resources
# depending on the plugin used.
# NOTE(james-page): only required for ovs plugin right now
if net_manager in ['neutron', 'quantum']:
if net_manager == 'quantum':
nm_rsc = QUANTUM_RESOURCES
if net_manager == 'neutron':
nm_rsc = NEUTRON_RESOURCES
resource_map.update(nm_rsc)
if plugin == 'ovs':
if net_manager == 'quantum':
nm_rsc = QUANTUM_RESOURCES
if net_manager == 'neutron':
nm_rsc = NEUTRON_RESOURCES
resource_map.update(nm_rsc)
resource_map[NOVA_CONF]['contexts'].append(NeutronComputeContext())
plugin = neutron_plugin()
if plugin:
conf = neutron_plugin_attribute(plugin, 'config', net_manager)
svcs = neutron_plugin_attribute(plugin, 'services', net_manager)
ctxts = (neutron_plugin_attribute(plugin, 'contexts', net_manager)
@ -156,7 +158,25 @@ def resource_map():
# associate the plugin agent with main network manager config(s)
[resource_map[nmc]['services'].extend(svcs) for nmc in nm_rsc]
resource_map[NOVA_CONF]['contexts'].append(NeutronComputeContext())
if relation_ids('ceph'):
# Add charm ceph configuration to resources and
# ensure directory actually exists
mkdir(os.path.dirname(ceph_config_file()))
mkdir(os.path.dirname(CEPH_CONF))
# Install ceph config as an alternative for co-location with
# ceph and ceph-osd charms - nova-compute ceph.conf will be
# lower priority that both of these but thats OK
if not os.path.exists(ceph_config_file()):
# touch file for pre-templated generation
open(ceph_config_file(), 'w').close()
install_alternative(os.path.basename(CEPH_CONF),
CEPH_CONF, ceph_config_file())
CEPH_RESOURCES[ceph_config_file()] = {
'contexts': [NovaComputeCephContext()],
'services': [],
}
resource_map.update(CEPH_RESOURCES)
return resource_map

View File

@ -1,6 +1,9 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
{% if restart_trigger -%}
# restart trigger: {{ restart_trigger }}
{% endif -%}
###############################################################################
--dhcpbridge_flagfile=/etc/nova/nova.conf
--dhcpbridge=/usr/bin/nova-dhcpbridge

View File

@ -2,6 +2,9 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
{% if restart_trigger -%}
# restart trigger: {{ restart_trigger }}
{% endif -%}
###############################################################################
[DEFAULT]
dhcpbridge_flagfile=/etc/nova/nova.conf

View File

@ -2,6 +2,9 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
{% if restart_trigger -%}
# restart trigger: {{ restart_trigger }}
{% endif -%}
###############################################################################
[DEFAULT]
dhcpbridge_flagfile=/etc/nova/nova.conf
@ -52,6 +55,8 @@ firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% if neutron_plugin and neutron_plugin == 'nvp' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtOpenVswitchVirtualPortDriver
security_group_api = quantum
firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% endif -%}
{% if network_manager_config -%}

View File

@ -2,6 +2,9 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
{% if restart_trigger -%}
# restart trigger: {{ restart_trigger }}
{% endif -%}
###############################################################################
[DEFAULT]
dhcpbridge_flagfile=/etc/nova/nova.conf
@ -52,6 +55,8 @@ firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% if neutron_plugin and neutron_plugin == 'nvp' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtOpenVswitchVirtualPortDriver
security_group_api = neutron
firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% endif -%}
{% if network_manager_config -%}

View File

@ -55,6 +55,7 @@ def fake_log(msg, level=None):
class NovaComputeContextTests(CharmTestCase):
def setUp(self):
super(NovaComputeContextTests, self).setUp(context, TO_PATCH)
self.relation_get.side_effect = self.test_relation.get
@ -66,6 +67,20 @@ class NovaComputeContextTests(CharmTestCase):
cloud_compute = context.CloudComputeContext()
self.assertEquals({}, cloud_compute())
@patch.object(context, '_network_manager')
def test_cloud_compute_context_restart_trigger(self, nm):
nm.return_value = None
cloud_compute = context.CloudComputeContext()
with patch.object(cloud_compute, 'restart_trigger') as rt:
rt.return_value = 'footrigger'
ctxt = cloud_compute()
self.assertEquals(ctxt.get('restart_trigger'), 'footrigger')
with patch.object(cloud_compute, 'restart_trigger') as rt:
rt.return_value = None
ctxt = cloud_compute()
self.assertEquals(ctxt.get('restart_trigger'), None)
@patch.object(context, '_network_manager')
def test_cloud_compute_volume_context_cinder(self, netman):
netman.return_value = None

View File

@ -30,7 +30,7 @@ TO_PATCH = [
'apt_update',
'filter_installed_packages',
'restart_on_change',
#charmhelpers.contrib.openstack.utils
# charmhelpers.contrib.openstack.utils
'configure_installation_source',
'openstack_upgrade_available',
# nova_compute_utils
@ -59,6 +59,7 @@ def fake_filter(packages):
class NovaComputeRelationsTests(CharmTestCase):
def setUp(self):
super(NovaComputeRelationsTests, self).setUp(hooks,
TO_PATCH)
@ -105,8 +106,9 @@ class NovaComputeRelationsTests(CharmTestCase):
def test_amqp_joined(self):
hooks.amqp_joined()
self.relation_set.assert_called_with(username='nova', vhost='openstack',
relation_id=None)
self.relation_set.assert_called_with(
username='nova', vhost='openstack',
relation_id=None)
@patch.object(hooks, 'CONFIGS')
def test_amqp_changed_missing_relation_data(self, configs):
@ -123,6 +125,7 @@ class NovaComputeRelationsTests(CharmTestCase):
configs.write = MagicMock()
if quantum:
self.network_manager.return_value = 'quantum'
self.neutron_plugin.return_value = 'ovs'
hooks.amqp_changed()
@patch.object(hooks, 'CONFIGS')
@ -147,9 +150,10 @@ class NovaComputeRelationsTests(CharmTestCase):
nova_hostname='nova.foohost.com')
self.unit_get.assert_called_with('private-address')
def test_db_joined_quantum(self):
def test_db_joined_quantum_ovs(self):
self.unit_get.return_value = 'nova.foohost.com'
self.network_manager.return_value = 'quantum'
self.neutron_plugin.return_value = 'ovs'
hooks.db_joined(rid='shared-db:0')
calls = [call(nova_database='nova',
nova_username='nova',
@ -163,6 +167,21 @@ class NovaComputeRelationsTests(CharmTestCase):
for c in calls]
self.unit_get.assert_called_with('private-address')
def test_db_joined_quantum_nvp(self):
self.unit_get.return_value = 'nova.foohost.com'
self.network_manager.return_value = 'quantum'
self.neutron_plugin.return_value = 'nvp'
hooks.db_joined(rid='shared-db:0')
calls = [call(nova_database='nova',
nova_username='nova',
nova_hostname='nova.foohost.com',
relation_id='shared-db:0')]
# NVP plugin requires no DB access - check it was not
# requested
[self.assertIn(c, self.relation_set.call_args_list)
for c in calls]
self.unit_get.assert_called_with('private-address')
@patch.object(hooks, 'CONFIGS')
def test_db_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
@ -187,12 +206,22 @@ class NovaComputeRelationsTests(CharmTestCase):
configs.write.call_args_list)
@patch.object(hooks, 'CONFIGS')
def test_db_changed_with_data_and_quantum(self, configs):
def test_db_changed_with_data_and_quantum_ovs(self, configs):
self.neutron_plugin_attribute.return_value = '/etc/quantum/plugin.conf'
self.neutron_plugin.return_value = 'ovs'
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(hooks, 'CONFIGS')
def test_db_changed_with_data_and_quantum_nvp(self, configs):
self.neutron_plugin_attribute.return_value = '/etc/quantum/plugin.conf'
self.neutron_plugin.return_value = 'nvp'
self._shared_db_test(configs, quantum=True)
ex = [call('/etc/nova/nova.conf')]
# NVP has no compute agent for neutron; check no config files generated
self.assertEquals(ex, configs.write.call_args_list)
@patch.object(hooks, 'CONFIGS')
def test_image_service_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
@ -264,15 +293,18 @@ class NovaComputeRelationsTests(CharmTestCase):
'Could not create ceph keyring: peer not ready?'
)
@patch.object(utils, 'service_name')
@patch.object(hooks, 'CONFIGS')
def test_ceph_changed_with_key_and_relation_data(self, configs):
def test_ceph_changed_with_key_and_relation_data(self, configs,
service_name):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['ceph']
configs.write = MagicMock()
service_name.return_value = 'nova-compute'
self.ensure_ceph_keyring.return_value = True
hooks.ceph_changed()
ex = [
call('/etc/ceph/ceph.conf'),
call('/var/lib/charm/nova-compute/ceph.conf'),
call('/etc/ceph/secret.xml'),
call('/etc/nova/nova.conf'),
]

View File

@ -13,6 +13,9 @@ TO_PATCH = [
'related_units',
'relation_ids',
'relation_get',
'service_name',
'mkdir',
'install_alternative'
]
OVS_PKGS = [
@ -22,9 +25,11 @@ OVS_PKGS = [
class NovaComputeUtilsTests(CharmTestCase):
def setUp(self):
super(NovaComputeUtilsTests, self).setUp(utils, TO_PATCH)
self.config.side_effect = self.test_config.get
self.service_name.return_value = 'nova-compute'
@patch.object(utils, 'network_manager')
def test_determine_packages_nova_network(self, net_man):
@ -143,7 +148,7 @@ class NovaComputeUtilsTests(CharmTestCase):
'/etc/quantum/quantum.conf': {
'contexts': [],
'services': ['quantum-plugin-openvswitch-agent']}
}
}
self.assertEquals(ex, result)

View File

@ -45,6 +45,7 @@ def get_default_config():
class CharmTestCase(unittest.TestCase):
def setUp(self, obj, patches):
super(CharmTestCase, self).setUp()
self.patches = patches
@ -65,6 +66,7 @@ class CharmTestCase(unittest.TestCase):
class TestConfig(object):
def __init__(self):
self.config = get_default_config()
@ -86,6 +88,7 @@ class TestConfig(object):
class TestRelation(object):
def __init__(self, relation_data={}):
self.relation_data = relation_data