Rationalize endpoint generation, rework tests, add support for multiple network support

This commit is contained in:
James Page 2014-06-27 12:55:45 +01:00
parent faf2d1d655
commit b082dcf12f
18 changed files with 300 additions and 100 deletions

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>neutron-api</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

9
.pydevproject Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<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>/neutron-api/hooks</path>
<path>/neutron-api/unit_tests</path>
</pydev_pathproperty>
</pydev_project>

View File

@ -1,4 +1,4 @@
branch: lp:charm-helpers branch: lp:~james-page/charm-helpers/network-splits
destination: hooks/charmhelpers destination: hooks/charmhelpers
include: include:
- core - core
@ -8,3 +8,4 @@ include:
- contrib.network.ovs - contrib.network.ovs
- contrib.storage.linux - contrib.storage.linux
- payload.execd - payload.execd
- contrib.network.ip

View File

@ -89,3 +89,27 @@ options:
default: False default: False
type: boolean type: boolean
description: Enable verbose logging description: Enable verbose logging
# Network configuration options
# by default all access is over 'private-address'
os-admin-network:
type: string
description: |
The IP address and netmask of the OpenStack Admin network (e.g.,
192.168.0.0/24)
.
This network will be used for admin endpoints.
os-internal-network:
type: string
description: |
The IP address and netmask of the OpenStack Internal network (e.g.,
192.168.0.0/24)
.
This network will be used for internal endpoints.
os-public-network:
type: string
description: |
The IP address and netmask of the OpenStack Public network (e.g.,
192.168.0.0/24)
.
This network will be used for public endpoints.

View File

@ -163,13 +163,14 @@ def get_hacluster_config():
return conf return conf
def canonical_url(configs, vip_setting='vip'): def canonical_url(configs, vip_setting='vip', address=None):
''' '''
Returns the correct HTTP URL to this host given the state of HTTPS Returns the correct HTTP URL to this host given the state of HTTPS
configuration and hacluster. configuration and hacluster.
:configs : OSTemplateRenderer: A config tempating object to inspect for :configs : OSTemplateRenderer: A config tempating object to inspect for
a complete https context. a complete https context.
:vip_setting: str: Setting in charm config that specifies :vip_setting: str: Setting in charm config that specifies
VIP address. VIP address.
''' '''
@ -179,5 +180,5 @@ def canonical_url(configs, vip_setting='vip'):
if is_clustered(): if is_clustered():
addr = config_get(vip_setting) addr = config_get(vip_setting)
else: else:
addr = unit_get('private-address') addr = address or unit_get('private-address')
return '%s://%s' % (scheme, addr) return '%s://%s' % (scheme, addr)

View File

@ -0,0 +1,69 @@
import sys
from charmhelpers.fetch import apt_install
from charmhelpers.core.hookenv import (
ERROR, log,
)
try:
import netifaces
except ImportError:
apt_install('python-netifaces')
import netifaces
try:
import netaddr
except ImportError:
apt_install('python-netaddr')
import netaddr
def _validate_cidr(network):
try:
netaddr.IPNetwork(network)
except (netaddr.core.AddrFormatError, ValueError):
raise ValueError("Network (%s) is not in CIDR presentation format" %
network)
def get_address_in_network(network, fallback=None, fatal=False):
"""
Get an IPv4 address within the network from the host.
:param network (str): CIDR presentation format. For example,
'192.168.1.0/24'.
:param fallback (str): If no address is found, return fallback.
:param fatal (boolean): If no address is found, fallback is not
set and fatal is True then exit(1).
"""
def not_found_error_out():
log("No IP address found in network: %s" % network,
level=ERROR)
sys.exit(1)
if network is None:
if fallback is not None:
return fallback
else:
if fatal:
not_found_error_out()
_validate_cidr(network)
for iface in netifaces.interfaces():
addresses = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addresses:
addr = addresses[netifaces.AF_INET][0]['addr']
netmask = addresses[netifaces.AF_INET][0]['netmask']
cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
if cidr in netaddr.IPNetwork(network):
return str(cidr.ip)
if fallback is not None:
return fallback
if fatal:
not_found_error_out()
return None

View File

@ -1,4 +1,7 @@
import logging import logging
import os
import time
import urllib
import glanceclient.v1.client as glance_client import glanceclient.v1.client as glance_client
import keystoneclient.v2_0 as keystone_client import keystoneclient.v2_0 as keystone_client
@ -149,3 +152,58 @@ class OpenStackAmuletUtils(AmuletUtils):
endpoint_type='publicURL') endpoint_type='publicURL')
return nova_client.Client(username=user, api_key=password, return nova_client.Client(username=user, api_key=password,
project_id=tenant, auth_url=ep) project_id=tenant, auth_url=ep)
def create_cirros_image(self, glance, image_name):
"""Download the latest cirros image and upload it to glance."""
http_proxy = os.getenv('AMULET_HTTP_PROXY')
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
if http_proxy:
proxies = {'http': http_proxy}
opener = urllib.FancyURLopener(proxies)
else:
opener = urllib.FancyURLopener()
f = opener.open("http://download.cirros-cloud.net/version/released")
version = f.read().strip()
cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
if not os.path.exists(cirros_img):
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
version, cirros_img)
opener.retrieve(cirros_url, cirros_img)
f.close()
with open(cirros_img) as f:
image = glance.images.create(name=image_name, is_public=True,
disk_format='qcow2',
container_format='bare', data=f)
return image
def delete_image(self, glance, image):
"""Delete the specified image."""
glance.images.delete(image)
def create_instance(self, nova, image_name, instance_name, flavor):
"""Create the specified instance."""
image = nova.images.find(name=image_name)
flavor = nova.flavors.find(name=flavor)
instance = nova.servers.create(name=instance_name, image=image,
flavor=flavor)
count = 1
status = instance.status
while status == 'BUILD' and count < 10:
time.sleep(5)
instance = nova.servers.get(instance.id)
status = instance.status
self.log.debug('instance status: {}'.format(status))
count += 1
if status == 'BUILD':
return None
return instance
def delete_instance(self, nova, instance):
"""Delete the specified instance."""
nova.servers.delete(instance)

View File

@ -243,23 +243,31 @@ class IdentityServiceContext(OSContextGenerator):
class AMQPContext(OSContextGenerator): class AMQPContext(OSContextGenerator):
interfaces = ['amqp']
def __init__(self, ssl_dir=None): def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
self.ssl_dir = ssl_dir self.ssl_dir = ssl_dir
self.rel_name = rel_name
self.relation_prefix = relation_prefix
self.interfaces = [rel_name]
def __call__(self): def __call__(self):
log('Generating template context for amqp') log('Generating template context for amqp')
conf = config() conf = config()
user_setting = 'rabbit-user'
vhost_setting = 'rabbit-vhost'
if self.relation_prefix:
user_setting = self.relation_prefix + '-rabbit-user'
vhost_setting = self.relation_prefix + '-rabbit-vhost'
try: try:
username = conf['rabbit-user'] username = conf[user_setting]
vhost = conf['rabbit-vhost'] vhost = conf[vhost_setting]
except KeyError as e: except KeyError as e:
log('Could not generate shared_db context. ' log('Could not generate shared_db context. '
'Missing required charm config options: %s.' % e) 'Missing required charm config options: %s.' % e)
raise OSContextError raise OSContextError
ctxt = {} ctxt = {}
for rid in relation_ids('amqp'): for rid in relation_ids(self.rel_name):
ha_vip_only = False ha_vip_only = False
for unit in related_units(rid): for unit in related_units(rid):
if relation_get('clustered', rid=rid, unit=unit): if relation_get('clustered', rid=rid, unit=unit):
@ -332,10 +340,12 @@ class CephContext(OSContextGenerator):
use_syslog = str(config('use-syslog')).lower() use_syslog = str(config('use-syslog')).lower()
for rid in relation_ids('ceph'): for rid in relation_ids('ceph'):
for unit in related_units(rid): for unit in related_units(rid):
mon_hosts.append(relation_get('private-address', rid=rid,
unit=unit))
auth = relation_get('auth', rid=rid, unit=unit) auth = relation_get('auth', rid=rid, unit=unit)
key = relation_get('key', rid=rid, unit=unit) key = relation_get('key', rid=rid, unit=unit)
ceph_addr = \
relation_get('ceph-public-address', rid=rid, unit=unit) or \
relation_get('private-address', rid=rid, unit=unit)
mon_hosts.append(ceph_addr)
ctxt = { ctxt = {
'mon_hosts': ' '.join(mon_hosts), 'mon_hosts': ' '.join(mon_hosts),
@ -418,7 +428,8 @@ class ApacheSSLContext(OSContextGenerator):
""" """
Generates a context for an apache vhost configuration that configures Generates a context for an apache vhost configuration that configures
HTTPS reverse proxying for one or many endpoints. Generated context HTTPS reverse proxying for one or many endpoints. Generated context
looks something like: looks something like::
{ {
'namespace': 'cinder', 'namespace': 'cinder',
'private_address': 'iscsi.mycinderhost.com', 'private_address': 'iscsi.mycinderhost.com',
@ -633,7 +644,7 @@ class SubordinateConfigContext(OSContextGenerator):
The subordinate interface allows subordinates to export their The subordinate interface allows subordinates to export their
configuration requirements to the principle for multiple config configuration requirements to the principle for multiple config
files and multiple serivces. Ie, a subordinate that has interfaces files and multiple serivces. Ie, a subordinate that has interfaces
to both glance and nova may export to following yaml blob as json: to both glance and nova may export to following yaml blob as json::
glance: glance:
/etc/glance/glance-api.conf: /etc/glance/glance-api.conf:
@ -652,7 +663,8 @@ class SubordinateConfigContext(OSContextGenerator):
It is then up to the principle charms to subscribe this context to It is then up to the principle charms to subscribe this context to
the service+config file it is interestd in. Configuration data will the service+config file it is interestd in. Configuration data will
be available in the template context, in glance's case, as: be available in the template context, in glance's case, as::
ctxt = { ctxt = {
... other context ... ... other context ...
'subordinate_config': { 'subordinate_config': {

View File

@ -30,15 +30,15 @@ def get_loader(templates_dir, os_release):
loading dir. loading dir.
A charm may also ship a templates dir with this module A charm may also ship a templates dir with this module
and it will be appended to the bottom of the search list, eg: and it will be appended to the bottom of the search list, eg::
hooks/charmhelpers/contrib/openstack/templates.
:param templates_dir: str: Base template directory containing release hooks/charmhelpers/contrib/openstack/templates
:param templates_dir (str): Base template directory containing release
sub-directories. sub-directories.
:param os_release : str: OpenStack release codename to construct template :param os_release (str): OpenStack release codename to construct template
loader. loader.
:returns: jinja2.ChoiceLoader constructed with a list of
:returns : jinja2.ChoiceLoader constructed with a list of
jinja2.FilesystemLoaders, ordered in descending jinja2.FilesystemLoaders, ordered in descending
order by OpenStack release. order by OpenStack release.
""" """
@ -111,7 +111,8 @@ class OSConfigRenderer(object):
and ease the burden of managing config templates across multiple OpenStack and ease the burden of managing config templates across multiple OpenStack
releases. releases.
Basic usage: Basic usage::
# import some common context generates from charmhelpers # import some common context generates from charmhelpers
from charmhelpers.contrib.openstack import context from charmhelpers.contrib.openstack import context
@ -131,10 +132,8 @@ class OSConfigRenderer(object):
# write out all registered configs # write out all registered configs
configs.write_all() configs.write_all()
Details: **OpenStack Releases and template loading**
OpenStack Releases and template loading
---------------------------------------
When the object is instantiated, it is associated with a specific OS When the object is instantiated, it is associated with a specific OS
release. This dictates how the template loader will be constructed. release. This dictates how the template loader will be constructed.
@ -144,8 +143,8 @@ class OSConfigRenderer(object):
- the base templates_dir - the base templates_dir
- a template directory shipped in the charm with this helper file. - a template directory shipped in the charm with this helper file.
For the example above, '/tmp/templates' contains the following structure::
For the example above, '/tmp/templates' contains the following structure:
/tmp/templates/nova.conf /tmp/templates/nova.conf
/tmp/templates/api-paste.ini /tmp/templates/api-paste.ini
/tmp/templates/grizzly/api-paste.ini /tmp/templates/grizzly/api-paste.ini
@ -169,8 +168,8 @@ class OSConfigRenderer(object):
$CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
us to ship common templates (haproxy, apache) with the helpers. us to ship common templates (haproxy, apache) with the helpers.
Context generators **Context generators**
---------------------------------------
Context generators are used to generate template contexts during hook Context generators are used to generate template contexts during hook
execution. Doing so may require inspecting service relations, charm execution. Doing so may require inspecting service relations, charm
config, etc. When registered, a config file is associated with a list config, etc. When registered, a config file is associated with a list

View File

@ -84,6 +84,8 @@ def get_os_codename_install_source(src):
'''Derive OpenStack release codename from a given installation source.''' '''Derive OpenStack release codename from a given installation source.'''
ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
rel = '' rel = ''
if src is None:
return rel
if src in ['distro', 'distro-proposed']: if src in ['distro', 'distro-proposed']:
try: try:
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel] rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]

View File

@ -25,7 +25,7 @@ cache = {}
def cached(func): def cached(func):
"""Cache return values for multiple executions of func + args """Cache return values for multiple executions of func + args
For example: For example::
@cached @cached
def unit_get(attribute): def unit_get(attribute):
@ -445,18 +445,19 @@ class UnregisteredHookError(Exception):
class Hooks(object): class Hooks(object):
"""A convenient handler for hook functions. """A convenient handler for hook functions.
Example: Example::
hooks = Hooks() hooks = Hooks()
# register a hook, taking its name from the function name # register a hook, taking its name from the function name
@hooks.hook() @hooks.hook()
def install(): def install():
... pass # your code here
# register a hook, providing a custom hook name # register a hook, providing a custom hook name
@hooks.hook("config-changed") @hooks.hook("config-changed")
def config_changed(): def config_changed():
... pass # your code here
if __name__ == "__main__": if __name__ == "__main__":
# execute a hook based on the name the program is called by # execute a hook based on the name the program is called by

View File

@ -211,13 +211,13 @@ def file_hash(path):
def restart_on_change(restart_map, stopstart=False): def restart_on_change(restart_map, stopstart=False):
"""Restart services based on configuration files changing """Restart services based on configuration files changing
This function is used a decorator, for example This function is used a decorator, for example::
@restart_on_change({ @restart_on_change({
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
}) })
def ceph_client_changed(): def ceph_client_changed():
... pass # your code here
In this example, the cinder-api and cinder-volume services In this example, the cinder-api and cinder-volume services
would be restarted if /etc/ceph/ceph.conf is changed by the would be restarted if /etc/ceph/ceph.conf is changed by the
@ -313,9 +313,11 @@ def get_nic_hwaddr(nic):
def cmp_pkgrevno(package, revno, pkgcache=None): def cmp_pkgrevno(package, revno, pkgcache=None):
'''Compare supplied revno with the revno of the installed package '''Compare supplied revno with the revno of the installed package
1 => Installed revno is greater than supplied arg
0 => Installed revno is the same as supplied arg * 1 => Installed revno is greater than supplied arg
-1 => Installed revno is less than supplied arg * 0 => Installed revno is the same as supplied arg
* -1 => Installed revno is less than supplied arg
''' '''
import apt_pkg import apt_pkg
if not pkgcache: if not pkgcache:

View File

@ -235,31 +235,39 @@ def configure_sources(update=False,
sources_var='install_sources', sources_var='install_sources',
keys_var='install_keys'): keys_var='install_keys'):
""" """
Configure multiple sources from charm configuration Configure multiple sources from charm configuration.
The lists are encoded as yaml fragments in the configuration.
The frament needs to be included as a string.
Example config: Example config:
install_sources: install_sources: |
- "ppa:foo" - "ppa:foo"
- "http://example.com/repo precise main" - "http://example.com/repo precise main"
install_keys: install_keys: |
- null - null
- "a1b2c3d4" - "a1b2c3d4"
Note that 'null' (a.k.a. None) should not be quoted. Note that 'null' (a.k.a. None) should not be quoted.
""" """
sources = safe_load(config(sources_var)) sources = safe_load((config(sources_var) or '').strip()) or []
keys = config(keys_var) keys = safe_load((config(keys_var) or '').strip()) or None
if keys is not None:
keys = safe_load(keys) if isinstance(sources, basestring):
if isinstance(sources, basestring) and ( sources = [sources]
keys is None or isinstance(keys, basestring)):
add_source(sources, keys) if keys is None:
for source in sources:
add_source(source, None)
else: else:
if not len(sources) == len(keys): if isinstance(keys, basestring):
msg = 'Install sources and keys lists are different lengths' keys = [keys]
raise SourceConfigError(msg)
for src_num in range(len(sources)): if len(sources) != len(keys):
add_source(sources[src_num], keys[src_num]) raise SourceConfigError(
'Install sources and keys lists are different lengths')
for source, key in zip(sources, keys):
add_source(source, key)
if update: if update:
apt_update(fatal=True) apt_update(fatal=True)

View File

@ -33,7 +33,6 @@ from charmhelpers.contrib.openstack.neutron import (
) )
from neutron_api_utils import ( from neutron_api_utils import (
determine_endpoints,
determine_packages, determine_packages,
determine_ports, determine_ports,
register_configs, register_configs,
@ -51,6 +50,7 @@ from charmhelpers.contrib.hahelpers.cluster import (
) )
from charmhelpers.payload.execd import execd_preinstall from charmhelpers.payload.execd import execd_preinstall
from charmhelpers.contrib.network.ip import get_address_in_network
hooks = Hooks() hooks = Hooks()
CONFIGS = register_configs() CONFIGS = register_configs()
@ -152,8 +152,35 @@ def relation_broken():
@hooks.hook('identity-service-relation-joined') @hooks.hook('identity-service-relation-joined')
def identity_joined(rid=None): def identity_joined(rid=None):
base_url = canonical_url(CONFIGS) public_url = '{}:{}'.format(canonical_url(CONFIGS,
relation_set(relation_id=rid, **determine_endpoints(base_url)) address=get_address_in_network(
config('os-public-network'),
unit_get('public-address')
)),
api_port('neutron-server'))
admin_url = '{}:{}'.format(canonical_url(CONFIGS,
address=get_address_in_network(
config('os-admin-network'),
unit_get('private-address')
)),
api_port('neutron-server'))
internal_url = '{}:{}'.format(canonical_url(CONFIGS,
address=get_address_in_network(
config('os-internal-network'),
unit_get('private-address')
)),
api_port('neutron-server'))
endpoints = {
'quantum_service': 'quantum',
'quantum_region': config('region'),
'quantum_public_url': public_url,
'quantum_admin_url': admin_url,
'quantum_internal_url': internal_url,
}
relation_set(relation_id=rid, relation_settings=endpoints)
@hooks.hook('identity-service-relation-changed') @hooks.hook('identity-service-relation-changed')

View File

@ -72,23 +72,6 @@ def api_port(service):
return API_PORTS[service] return API_PORTS[service]
def determine_endpoints(url):
'''Generates a dictionary containing all relevant endpoints to be
passed to keystone as relation settings.'''
region = config('region')
neutron_url = '%s:%s' % (url, api_port('neutron-server'))
endpoints = ({
'quantum_service': 'quantum',
'quantum_region': region,
'quantum_public_url': neutron_url,
'quantum_admin_url': neutron_url,
'quantum_internal_url': neutron_url,
})
return endpoints
def determine_packages(): def determine_packages():
# currently all packages match service names # currently all packages match service names
packages = [] + BASE_PACKAGES packages = [] + BASE_PACKAGES

View File

@ -25,7 +25,6 @@ TO_PATCH = [
'config', 'config',
'CONFIGS', 'CONFIGS',
'configure_installation_source', 'configure_installation_source',
'determine_endpoints',
'determine_packages', 'determine_packages',
'determine_ports', 'determine_ports',
'do_openstack_upgrade', 'do_openstack_upgrade',
@ -170,7 +169,10 @@ class NeutronAPIHooksTests(CharmTestCase):
self.assertTrue(self.CONFIGS.write_all.called) self.assertTrue(self.CONFIGS.write_all.called)
def test_identity_joined(self): def test_identity_joined(self):
_neutron_url = 'http://127.0.0.1:1234' self.canonical_url.return_value = 'http://127.0.0.1'
self.api_port.return_value = '9696'
self.test_config.set('region','region1')
_neutron_url = 'http://127.0.0.1:9696'
_endpoints = { _endpoints = {
'quantum_service': 'quantum', 'quantum_service': 'quantum',
'quantum_region': 'region1', 'quantum_region': 'region1',
@ -178,11 +180,10 @@ class NeutronAPIHooksTests(CharmTestCase):
'quantum_admin_url': _neutron_url, 'quantum_admin_url': _neutron_url,
'quantum_internal_url': _neutron_url, 'quantum_internal_url': _neutron_url,
} }
self.determine_endpoints.return_value = _endpoints
self._call_hook('identity-service-relation-joined') self._call_hook('identity-service-relation-joined')
self.relation_set.assert_called_with( self.relation_set.assert_called_with(
relation_id=None, relation_id=None,
**_endpoints relation_settings=_endpoints
) )
def test_identity_changed_partial_ctxt(self): def test_identity_changed_partial_ctxt(self):

View File

@ -61,20 +61,6 @@ class TestNeutronAPIUtils(CharmTestCase):
port = nutils.api_port('neutron-server') port = nutils.api_port('neutron-server')
self.assertEqual(port, nutils.API_PORTS['neutron-server']) self.assertEqual(port, nutils.API_PORTS['neutron-server'])
def test_determine_endpoints(self):
test_url = 'http://127.0.0.1'
endpoints = nutils.determine_endpoints(test_url)
neutron_url = '%s:%s' % (test_url,
nutils.api_port('neutron-server'))
expect = {
'quantum_service': 'quantum',
'quantum_region': 'region101',
'quantum_public_url': neutron_url,
'quantum_admin_url': neutron_url,
'quantum_internal_url': neutron_url,
}
self.assertEqual(endpoints, expect)
def test_determine_packages(self): def test_determine_packages(self):
pkg_list = nutils.determine_packages() pkg_list = nutils.determine_packages()
expect = nutils.BASE_PACKAGES expect = nutils.BASE_PACKAGES