charm-neutron-api/hooks/neutron_api_context.py

604 lines
21 KiB
Python

# Copyright 2016 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ast
import re
from collections import OrderedDict
from charmhelpers.core.hookenv import (
config,
relation_ids,
related_units,
relation_get,
log,
DEBUG,
ERROR,
)
from charmhelpers.contrib.openstack import context
from charmhelpers.contrib.hahelpers.cluster import (
determine_api_port,
determine_apache_port,
)
from charmhelpers.contrib.openstack.utils import (
os_release,
CompareOpenStackReleases,
)
VLAN = 'vlan'
VXLAN = 'vxlan'
GRE = 'gre'
FLAT = 'flat'
LOCAL = 'local'
OVERLAY_NET_TYPES = [VXLAN, GRE]
NON_OVERLAY_NET_TYPES = [VLAN, FLAT, LOCAL]
TENANT_NET_TYPES = [VXLAN, GRE, VLAN, FLAT, LOCAL]
EXTENSION_DRIVER_PORT_SECURITY = 'port_security'
EXTENSION_DRIVER_DNS = 'dns'
# Domain name validation regex which is used to certify that
# the domain-name consists only of valid characters, is not
# longer than 63 characters in length for any name segment,
# and each segment does not begin or end with a hyphen.
DOMAIN_NAME_REGEX = re.compile(r'^(?!-)[A-Z\d-]{1,63}(?<!-)$',
re.IGNORECASE)
def get_l2population():
plugin = config('neutron-plugin')
return config('l2-population') if plugin == "ovs" else False
def _get_overlay_network_type():
overlay_networks = config('overlay-network-type').split()
for overlay_net in overlay_networks:
if overlay_net not in OVERLAY_NET_TYPES:
raise ValueError('Unsupported overlay-network-type %s'
% overlay_net)
return overlay_networks
def get_overlay_network_type():
return ','.join(_get_overlay_network_type())
def _get_tenant_network_types():
default_tenant_network_type = config('default-tenant-network-type')
tenant_network_types = _get_overlay_network_type()
tenant_network_types.extend(NON_OVERLAY_NET_TYPES)
if default_tenant_network_type:
if (default_tenant_network_type in TENANT_NET_TYPES and
default_tenant_network_type in tenant_network_types):
tenant_network_types[:0] = [default_tenant_network_type]
else:
raise ValueError('Unsupported or unconfigured '
'default-tenant-network-type'
' {}'.format(default_tenant_network_type))
# Dedupe list but preserve order
return list(OrderedDict.fromkeys(tenant_network_types))
def get_tenant_network_types():
'''Get the configured tenant network types
@return: comma delimited string of configured tenant
network types.
'''
return ','.join(_get_tenant_network_types())
def get_l3ha():
if config('enable-l3ha'):
if CompareOpenStackReleases(os_release('neutron-server')) < 'juno':
log('Disabling L3 HA, enable-l3ha is not valid before Juno')
return False
if get_l2population():
log('Disabling L3 HA, l2-population must be disabled with L3 HA')
return False
return True
else:
return False
def get_dvr():
if config('enable-dvr'):
release = os_release('neutron-server')
if CompareOpenStackReleases(release) < 'juno':
log('Disabling DVR, enable-dvr is not valid before Juno')
return False
if CompareOpenStackReleases(release) == 'juno':
if VXLAN not in config('overlay-network-type').split():
log('Disabling DVR, enable-dvr requires the use of the vxlan '
'overlay network for OpenStack Juno')
return False
if get_l3ha():
log('Disabling DVR, enable-l3ha must be disabled with dvr')
return False
if not get_l2population():
log('Disabling DVR, l2-population must be enabled to use dvr')
return False
return True
else:
return False
def get_dns_domain():
if not config('enable-ml2-dns'):
log('ML2 DNS Extensions are not enabled.', DEBUG)
return ""
dns_domain = config('dns-domain')
if not dns_domain:
log('No dns-domain has been configured', DEBUG)
return dns_domain
release = os_release('neutron-server')
if CompareOpenStackReleases(release) < 'mitaka':
log('Internal DNS resolution is not supported before Mitaka')
return ""
# Strip any trailing . at the end
if dns_domain[-1] == '.':
dns_domain = dns_domain[:-1]
# Ensure that the dns name is only a valid name. Valid entries include
# a-z, A-Z, 0-9, ., and -. No particular name may be longer than 63
# characters, each part cannot begin/end with a -. Validate this here in
# order to prevent other chaos which may prevent neutron services from
# functioning properly.
# Note: intentionally not validating the length of the domain name because
# this is practically difficult to validate reasonably well.
for level in dns_domain.split('.'):
if not DOMAIN_NAME_REGEX.match(level):
msg = "dns-domain '%s' is an invalid domain name." % dns_domain
log(msg, ERROR)
raise ValueError(msg)
# Make sure it ends with a .
dns_domain += '.'
return dns_domain
class ApacheSSLContext(context.ApacheSSLContext):
interfaces = ['https']
external_ports = []
service_namespace = 'neutron'
def __call__(self):
# late import to work around circular dependency
from neutron_api_utils import determine_ports
self.external_ports = determine_ports()
return super(ApacheSSLContext, self).__call__()
class IdentityServiceContext(context.IdentityServiceContext):
def __call__(self):
ctxt = super(IdentityServiceContext, self).__call__()
if not ctxt:
return
ctxt['region'] = config('region')
return ctxt
class NeutronCCContext(context.NeutronContext):
interfaces = []
@property
def network_manager(self):
return 'neutron'
@property
def plugin(self):
return config('neutron-plugin')
@property
def neutron_security_groups(self):
return config('neutron-security-groups')
@property
def neutron_l2_population(self):
return get_l2population()
@property
def neutron_tenant_network_types(self):
return get_tenant_network_types()
@property
def neutron_overlay_network_type(self):
return get_overlay_network_type()
@property
def neutron_dvr(self):
return get_dvr()
@property
def neutron_l3ha(self):
return get_l3ha()
# Do not need the plugin agent installed on the api server
def _ensure_packages(self):
pass
# Do not need the flag on the api server
def _save_flag_file(self):
pass
def get_neutron_api_rel_settings(self):
settings = {}
for rid in relation_ids('neutron-api'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
cell_type = rdata.get('cell_type')
settings['nova_url'] = rdata.get('nova_url')
settings['restart_trigger'] = rdata.get('restart_trigger')
# If there are multiple nova-cloud-controllers joined to this
# service in a cell deployment then ignore the non-api cell
# ones
if cell_type and not cell_type == "api":
continue
if settings['nova_url']:
return settings
return settings
def __call__(self):
from neutron_api_utils import api_port
ctxt = super(NeutronCCContext, self).__call__()
if config('neutron-plugin') == 'nsx':
ctxt['nsx_username'] = config('nsx-username')
ctxt['nsx_password'] = config('nsx-password')
ctxt['nsx_tz_uuid'] = config('nsx-tz-uuid')
ctxt['nsx_l3_uuid'] = config('nsx-l3-uuid')
if 'nsx-controllers' in config():
ctxt['nsx_controllers'] = \
','.join(config('nsx-controllers').split())
ctxt['nsx_controllers_list'] = \
config('nsx-controllers').split()
if config('neutron-plugin') == 'plumgrid':
ctxt['pg_username'] = config('plumgrid-username')
ctxt['pg_password'] = config('plumgrid-password')
ctxt['virtual_ip'] = config('plumgrid-virtual-ip')
elif config('neutron-plugin') == 'midonet':
ctxt.update(MidonetContext()())
identity_context = IdentityServiceContext(service='neutron',
service_user='neutron')()
if identity_context is not None:
ctxt.update(identity_context)
ctxt['l2_population'] = self.neutron_l2_population
ctxt['enable_dvr'] = self.neutron_dvr
ctxt['l3_ha'] = self.neutron_l3ha
if self.neutron_l3ha:
max_agents = config('max-l3-agents-per-router')
min_agents = config('min-l3-agents-per-router')
if max_agents < min_agents:
raise ValueError("max-l3-agents-per-router ({}) must be >= "
"min-l3-agents-per-router "
"({})".format(max_agents, min_agents))
ctxt['max_l3_agents_per_router'] = max_agents
ctxt['min_l3_agents_per_router'] = min_agents
ctxt['dhcp_agents_per_network'] = config('dhcp-agents-per-network')
ctxt['tenant_network_types'] = self.neutron_tenant_network_types
ctxt['overlay_network_type'] = self.neutron_overlay_network_type
ctxt['external_network'] = config('neutron-external-network')
release = os_release('neutron-server')
cmp_release = CompareOpenStackReleases(release)
if config('neutron-plugin') in ['vsp']:
_config = config()
for k, v in _config.iteritems():
if k.startswith('vsd'):
ctxt[k.replace('-', '_')] = v
for rid in relation_ids('vsd-rest-api'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
vsd_ip = rdata.get('vsd-ip-address')
if cmp_release >= 'kilo':
cms_id_value = rdata.get('nuage-cms-id')
log('relation data:cms_id required for'
' nuage plugin: {}'.format(cms_id_value))
if cms_id_value is not None:
ctxt['vsd_cms_id'] = cms_id_value
log('relation data:vsd-ip-address: {}'.format(vsd_ip))
if vsd_ip is not None:
ctxt['vsd_server'] = '{}:8443'.format(vsd_ip)
if 'vsd_server' not in ctxt:
ctxt['vsd_server'] = '1.1.1.1:8443'
ctxt['verbose'] = config('verbose')
ctxt['debug'] = config('debug')
ctxt['neutron_bind_port'] = \
determine_api_port(api_port('neutron-server'),
singlenode_mode=True)
ctxt['quota_security_group'] = config('quota-security-group')
ctxt['quota_security_group_rule'] = \
config('quota-security-group-rule')
ctxt['quota_network'] = config('quota-network')
ctxt['quota_subnet'] = config('quota-subnet')
ctxt['quota_port'] = config('quota-port')
ctxt['quota_vip'] = config('quota-vip')
ctxt['quota_pool'] = config('quota-pool')
ctxt['quota_member'] = config('quota-member')
ctxt['quota_health_monitors'] = config('quota-health-monitors')
ctxt['quota_router'] = config('quota-router')
ctxt['quota_floatingip'] = config('quota-floatingip')
n_api_settings = self.get_neutron_api_rel_settings()
if n_api_settings:
ctxt.update(n_api_settings)
flat_providers = config('flat-network-providers')
if flat_providers:
ctxt['network_providers'] = ','.join(flat_providers.split())
vlan_ranges = config('vlan-ranges')
if vlan_ranges:
ctxt['vlan_ranges'] = ','.join(vlan_ranges.split())
vni_ranges = config('vni-ranges')
if vni_ranges:
ctxt['vni_ranges'] = ','.join(vni_ranges.split())
extension_drivers = []
if config('enable-ml2-port-security'):
extension_drivers.append(EXTENSION_DRIVER_PORT_SECURITY)
dns_domain = get_dns_domain()
if dns_domain:
extension_drivers.append(EXTENSION_DRIVER_DNS)
ctxt['dns_domain'] = dns_domain
if extension_drivers:
ctxt['extension_drivers'] = ','.join(extension_drivers)
ctxt['enable_sriov'] = config('enable-sriov')
if cmp_release == 'kilo' or cmp_release >= 'mitaka':
ctxt['enable_hyperv'] = True
else:
ctxt['enable_hyperv'] = False
if cmp_release >= 'mitaka':
if config('global-physnet-mtu'):
ctxt['global_physnet_mtu'] = config('global-physnet-mtu')
if config('path-mtu'):
ctxt['path_mtu'] = config('path-mtu')
else:
ctxt['path_mtu'] = config('global-physnet-mtu')
return ctxt
class HAProxyContext(context.HAProxyContext):
interfaces = ['ceph']
def __call__(self):
'''
Extends the main charmhelpers HAProxyContext with a port mapping
specific to this charm.
Also used to extend nova.conf context with correct api_listening_ports
'''
from neutron_api_utils import api_port
ctxt = super(HAProxyContext, self).__call__()
# Apache ports
a_neutron_api = determine_apache_port(api_port('neutron-server'),
singlenode_mode=True)
port_mapping = {
'neutron-server': [
api_port('neutron-server'), a_neutron_api]
}
ctxt['neutron_bind_port'] = determine_api_port(
api_port('neutron-server'),
singlenode_mode=True,
)
# for haproxy.conf
ctxt['service_ports'] = port_mapping
return ctxt
class EtcdContext(context.OSContextGenerator):
interfaces = ['etcd-proxy']
def __call__(self):
ctxt = {'cluster': ''}
cluster_string = ''
if not config('neutron-plugin') == 'Calico':
return ctxt
for rid in relation_ids('etcd-proxy'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
cluster_string = rdata.get('cluster')
if cluster_string:
break
ctxt['cluster'] = cluster_string
return ctxt
class NeutronApiSDNContext(context.SubordinateConfigContext):
interfaces = 'neutron-plugin-api-subordinate'
def __init__(self):
super(NeutronApiSDNContext, self).__init__(
interface='neutron-plugin-api-subordinate',
service='neutron-api',
config_file='/etc/neutron/neutron.conf')
def __call__(self):
ctxt = super(NeutronApiSDNContext, self).__call__()
defaults = {
'core-plugin': {
'templ_key': 'core_plugin',
'value': 'neutron.plugins.ml2.plugin.Ml2Plugin',
},
'neutron-plugin-config': {
'templ_key': 'neutron_plugin_config',
'value': '/etc/neutron/plugins/ml2/ml2_conf.ini',
},
'service-plugins': {
'templ_key': 'service_plugins',
'value': 'router,firewall,lbaas,vpnaas,metering',
},
'restart-trigger': {
'templ_key': 'restart_trigger',
'value': '',
},
'quota-driver': {
'templ_key': 'quota_driver',
'value': '',
},
'api-extensions-path': {
'templ_key': 'api_extensions_path',
'value': '',
},
}
for rid in relation_ids('neutron-plugin-api-subordinate'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
plugin = rdata.get('neutron-plugin')
if not plugin:
continue
ctxt['neutron_plugin'] = plugin
for key in defaults.keys():
remote_value = rdata.get(key)
ctxt_key = defaults[key]['templ_key']
if remote_value:
ctxt[ctxt_key] = remote_value
else:
ctxt[ctxt_key] = defaults[key]['value']
return ctxt
return ctxt
class NeutronApiSDNConfigFileContext(context.OSContextGenerator):
interfaces = ['neutron-plugin-api-subordinate']
def __call__(self):
for rid in relation_ids('neutron-plugin-api-subordinate'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
neutron_server_plugin_conf = rdata.get('neutron-plugin-config')
if neutron_server_plugin_conf:
return {'config': neutron_server_plugin_conf}
return {'config': '/etc/neutron/plugins/ml2/ml2_conf.ini'}
class NeutronApiApiPasteContext(context.OSContextGenerator):
interfaces = ['neutron-plugin-api-subordinate']
def __validate_middleware(self, middleware):
'''
Accepts a list of dicts of the following format:
{
'type': 'middleware_type',
'name': 'middleware_name',
'config': {
option_1: value_1,
# ...
option_n: value_n
}
This validator was meant to be minimalistic - PasteDeploy's
validator will take care of the rest while our purpose here
is mainly config rendering - not imposing additional validation
logic which does not belong here.
'''
# types taken from PasteDeploy's wsgi loader
VALID_TYPES = ['filter', 'filter-app',
'app', 'application',
'composite', 'composit', 'pipeline']
def types_valid(t, n, c):
return all((type(t) is str,
type(n) is str,
type(c is dict)))
def mtype_valid(t):
return t in VALID_TYPES
for m in middleware:
t, n, c = [m.get(v) for v in ['type', 'name', 'config']]
# note that dict has to be non-empty
if not types_valid(t, n, c):
raise ValueError('Extra middleware key type(s) are'
' invalid: {}'.format(repr(m)))
if not mtype_valid(t):
raise ValueError('Extra middleware type key is not'
' a valid PasteDeploy middleware '
'type {}'.format(repr(t)))
if not c:
raise ValueError('Extra middleware config dictionary'
' is empty')
def __process_unit(self, rid, unit):
rdata = relation_get(rid=rid, unit=unit)
# update extra middleware for all possible plugins
rdata_middleware = rdata.get('extra_middleware')
if rdata_middleware:
try:
middleware = ast.literal_eval(rdata_middleware)
except:
import traceback
log(traceback.format_exc())
raise ValueError('Invalid extra middleware data'
' - check the subordinate charm')
if middleware:
return middleware
else:
log('extra_middleware specified but not'
'populated by unit {}, '
'relation: {}, value: {}'.format(
unit, rid, repr(middleware)))
raise ValueError('Invalid extra middleware'
'specified by a subordinate')
# no extra middleware
return list()
def __call__(self):
extra_middleware = []
for rid in relation_ids('neutron-plugin-api-subordinate'):
for unit in related_units(rid):
extra_middleware.extend(self.__process_unit(rid, unit))
self.__validate_middleware(extra_middleware)
return {'extra_middleware': extra_middleware}\
if extra_middleware else {}
class MidonetContext(context.OSContextGenerator):
def __init__(self, rel_name='midonet'):
self.rel_name = rel_name
self.interfaces = [rel_name]
def __call__(self):
for rid in relation_ids(self.rel_name):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
ctxt = {
'midonet_api_ip': rdata.get('host'),
'midonet_api_port': rdata.get('port'),
}
if self.context_complete(ctxt):
return ctxt
return {}