From 149590eb35592fca1497611f03ab8872fc0bd282 Mon Sep 17 00:00:00 2001 From: Wim Nap Date: Wed, 8 Aug 2018 15:43:03 +0200 Subject: [PATCH] some changes to nova-compute docker --- neutron_controller_ovs/Dockerfile | 31 + neutron_controller_ovs/README.md | 21 + neutron_controller_ovs/bootstrap.sh | 76 + neutron_controller_ovs/db_base_plugin_v2.py | 1388 +++++++++++++++++++ neutron_controller_ovs/run.sh | 43 + neutron_controller_ovs/write_conf.sh | 100 ++ nova_compute/write_conf.sh | 4 +- 7 files changed, 1661 insertions(+), 2 deletions(-) create mode 100644 neutron_controller_ovs/Dockerfile create mode 100644 neutron_controller_ovs/README.md create mode 100755 neutron_controller_ovs/bootstrap.sh create mode 100644 neutron_controller_ovs/db_base_plugin_v2.py create mode 100755 neutron_controller_ovs/run.sh create mode 100755 neutron_controller_ovs/write_conf.sh diff --git a/neutron_controller_ovs/Dockerfile b/neutron_controller_ovs/Dockerfile new file mode 100644 index 0000000..e0a6ff2 --- /dev/null +++ b/neutron_controller_ovs/Dockerfile @@ -0,0 +1,31 @@ +FROM ubuntu:16.04 +# install packages + +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5EDB1B62EC4926EA + +RUN set -x \ + && echo "deb http://ubuntu-cloud.archive.canonical.com/ubuntu xenial-updates/ocata main" > /etc/apt/sources.list.d/ocata.list \ + && apt-get -y update \ + && apt-get -y install ubuntu-cloud-keyring \ + && apt-get -y update \ + && apt-get -y install \ + mysql-client \ + python-mysqldb \ + python-openstackclient \ + neutron-server \ + neutron-plugin-ml2 \ + neutron-linuxbridge-agent \ + neutron-l3-agent \ + neutron-metadata-agent \ + && apt-get -y clean + +# add bootstrap script and make it executable +COPY bootstrap.sh /etc/bootstrap.sh + +# Workaround for vlan_transparent parameter set to None. +COPY db_base_plugin_v2.py /usr/lib/python2.7/dist-packages/neutron/db/db_base_plugin_v2.py + +COPY run.sh /etc/run.sh +COPY write_conf.sh /etc/write_conf.sh + +CMD ["/etc/run.sh"] diff --git a/neutron_controller_ovs/README.md b/neutron_controller_ovs/README.md new file mode 100644 index 0000000..6661a20 --- /dev/null +++ b/neutron_controller_ovs/README.md @@ -0,0 +1,21 @@ +# ubuntu 16.04 openstack ocata neutron controler node + +## How to build the docker image. +``` +docker build . -t="hpc/neutroncontroller" +``` + +## How to bootstrap the service. +Before we can take the container into service we need accounts in keystone. +We also need an initial database. Both of these tasks are performed by the bootstrap script. +``` +docker run --rm --it --add-host="controller:" hpc/neutroncontroler /etc/bootstrap.sh +``` + +## How to run +This image needs a lot of environment variables. It should be run via the `hpc-cloud` ansible repository. + + +## Notes +This image is designed to be deployed from the [hpc-cloud repo](https://git.webhosting.rug.nl/HPC/hpc-cloud) +The -p option is added to the run command to make the container accessible from (containers on ) other hosts than the container host. diff --git a/neutron_controller_ovs/bootstrap.sh b/neutron_controller_ovs/bootstrap.sh new file mode 100755 index 0000000..065a6c6 --- /dev/null +++ b/neutron_controller_ovs/bootstrap.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# +# This script sets up the openstack users and regions.. +# as well as the database for the nova controller. +# This guide was used: +# https://docs.openstack.org/ocata/install-guide-ubuntu/nova-controller-install. + +# Create admin-openrc.sh from secrets that are in the environment during bootstrap. +cat << EOF > /root/admin-openrc.sh +#!/bin/bash +export OS_TENANT_NAME=admin +export OS_USERNAME=admin +export OS_PASSWORD=${OS_PASSWORD} +export OS_AUTH_URL=http://${KEYSTONE_HOST}:35357/v3 +export OS_IDENTITY_API_VERSION=3 + +export OS_PROJECT_DOMAIN_NAME=default +export OS_USER_DOMAIN_NAME=default +export OS_PROJECT_NAME=admin +export OS_IMAGE_API_VERSION=2 + +EOF + +source /root/admin-openrc.sh + +# create database for neutron. +SQL_SCRIPT=/root/neutron.sql +mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -h "$MYSQL_HOST" << EOF +DROP DATABASE IF EXISTS neutron; +CREATE DATABASE neutron; + +GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'localhost' \ + IDENTIFIED BY "${NEUTRON_PASSWORD}"; + +GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'%' \ + IDENTIFIED BY "${NEUTRON_PASSWORD}"; +EOF + +openstack user create "$NEUTRON_USER" --domain default --password "$NEUTRON_PASSWORD" +openstack role add --project service --user neutron admin +openstack service create --name neutron --description "OpenStack Networking" network + +# neutron endpoints +openstack endpoint create --region RegionOne \ + network public http://$MY_IP:9696 + +openstack endpoint create --region RegionOne \ + network internal http://$MY_IP:9696 + +openstack endpoint create --region RegionOne \ + network admin http://$MY_IP:9696 + +# population of the database requires complete server and plug-in configuration files. +/etc/write_conf.sh + +# Ugly hacks to prevent the manage command from failing +sed -i "/ op.drop_column('networks', 'shared')/ s/^#*/#/" /usr/lib/python2.7/dist-packages/neutron/db/migration/alembic_migrations/versions/liberty/contract/4ffceebfada_rbac_network.py +sed -i "/ op.drop_column('subnets', 'shared')/ s/^#*/#/" /usr/lib/python2.7/dist-packages/neutron/db/migration/alembic_migrations/versions/liberty/contract/4ffceebfada_rbac_network.py +sed -i "/ op.drop_column('qos_policies', 'shared')/ s/^#*/#/" /usr/lib/python2.7/dist-packages/neutron/db/migration/alembic_migrations/versions/mitaka/contract/c6c112992c9_rbac_qos_policy.py + +neutron-db-manage --config-file /etc/neutron/neutron.conf \ + --config-file /etc/neutron/plugins/ml2/ml2_conf.ini upgrade head + +# And now we drop the colums and constraints that the ORM fails to drop. +mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -h "$MYSQL_HOST" neutron << EOF + +ALTER TABLE networks DROP CONSTRAINT CONSTRAINT_2; +alter table networks drop column shared; + +ALTER TABLE subnets DROP CONSTRAINT CONSTRAINT_2; +ALTER TABLE subnets DROP COLUMN shared; + +ALTER TABLE qos_policies DROP CONSTRAINT CONSTRAINT_1; +ALTER TABLE qos_policies drop column shared + +EOF diff --git a/neutron_controller_ovs/db_base_plugin_v2.py b/neutron_controller_ovs/db_base_plugin_v2.py new file mode 100644 index 0000000..865eaad --- /dev/null +++ b/neutron_controller_ovs/db_base_plugin_v2.py @@ -0,0 +1,1388 @@ +# Copyright (c) 2012 OpenStack Foundation. +# All Rights Reserved. +# +# 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 functools + +import netaddr +from neutron_lib.api import validators +from neutron_lib import constants +from neutron_lib import exceptions as exc +from neutron_lib.plugins import directory +from oslo_config import cfg +from oslo_db import exception as os_db_exc +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import uuidutils +from sqlalchemy import and_ +from sqlalchemy import exc as sql_exc +from sqlalchemy import not_ + +from neutron._i18n import _, _LE, _LI +from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api +from neutron.api.v2 import attributes +from neutron.callbacks import events +from neutron.callbacks import exceptions +from neutron.callbacks import registry +from neutron.callbacks import resources +from neutron.common import constants as n_const +from neutron.common import exceptions as n_exc +from neutron.common import ipv6_utils +from neutron.common import utils +from neutron import context as ctx +from neutron.db import _utils as ndb_utils +from neutron.db import api as db_api +from neutron.db import db_base_plugin_common +from neutron.db import ipam_pluggable_backend +from neutron.db import models_v2 +from neutron.db import rbac_db_mixin as rbac_mixin +from neutron.db import rbac_db_models as rbac_db +from neutron.db import standardattrdescription_db as stattr_db +from neutron.extensions import ip_allocation as ipa +from neutron.extensions import l3 +from neutron import ipam +from neutron.ipam import exceptions as ipam_exc +from neutron.ipam import subnet_alloc +from neutron import neutron_plugin_base_v2 +from neutron.objects import base as base_obj +from neutron.objects import subnetpool as subnetpool_obj + + +LOG = logging.getLogger(__name__) + +# Ports with the following 'device_owner' values will not prevent +# network deletion. If delete_network() finds that all ports on a +# network have these owners, it will explicitly delete each port +# and allow network deletion to continue. Similarly, if delete_subnet() +# finds out that all existing IP Allocations are associated with ports +# with these owners, it will allow subnet deletion to proceed with the +# IP allocations being cleaned up by cascade. +AUTO_DELETE_PORT_OWNERS = [constants.DEVICE_OWNER_DHCP] + +DNS_DOMAIN_DEFAULT = 'openstacklocal.' +FQDN_MAX_LEN = 255 + + +def _check_subnet_not_used(context, subnet_id): + try: + kwargs = {'context': context, 'subnet_id': subnet_id} + registry.notify( + resources.SUBNET, events.BEFORE_DELETE, None, **kwargs) + except exceptions.CallbackFailure as e: + raise exc.SubnetInUse(subnet_id=subnet_id, reason=e) + + +def _update_subnetpool_dict(orig_pool, new_pool): + updated = dict((k, v) for k, v in orig_pool.to_dict().items() + if k not in orig_pool.synthetic_fields) + + new_pool = new_pool.copy() + new_prefixes = new_pool.pop('prefixes', constants.ATTR_NOT_SPECIFIED) + for k, v in new_pool.items(): + if k not in orig_pool.fields_no_update: + updated[k] = v + if new_prefixes is not constants.ATTR_NOT_SPECIFIED: + orig_ip_set = netaddr.IPSet(orig_pool.prefixes) + new_ip_set = netaddr.IPSet(new_prefixes) + if not orig_ip_set.issubset(new_ip_set): + msg = _("Existing prefixes must be " + "a subset of the new prefixes") + raise n_exc.IllegalSubnetPoolPrefixUpdate(msg=msg) + new_ip_set.compact() + updated['prefixes'] = [str(prefix.cidr) + for prefix in new_ip_set.iter_cidrs()] + else: + updated['prefixes'] = [str(prefix) + for prefix in orig_pool.prefixes] + return updated + + +class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, + neutron_plugin_base_v2.NeutronPluginBaseV2, + rbac_mixin.RbacPluginMixin, + stattr_db.StandardAttrDescriptionMixin): + """V2 Neutron plugin interface implementation using SQLAlchemy models. + + Whenever a non-read call happens the plugin will call an event handler + class method (e.g., network_created()). The result is that this class + can be sub-classed by other classes that add custom behaviors on certain + events. + """ + + # This attribute specifies whether the plugin supports or not + # bulk/pagination/sorting operations. Name mangling is used in + # order to ensure it is qualified by class + __native_bulk_support = True + __native_pagination_support = True + __native_sorting_support = True + + def __init__(self): + self.set_ipam_backend() + if cfg.CONF.notify_nova_on_port_status_changes: + # Import nova conditionally to support the use case of Neutron + # being used outside of an OpenStack context. + from neutron.notifiers import nova + # NOTE(arosen) These event listeners are here to hook into when + # port status changes and notify nova about their change. + self.nova_notifier = nova.Notifier.get_instance() + db_api.sqla_listen(models_v2.Port, 'after_insert', + self.nova_notifier.send_port_status) + db_api.sqla_listen(models_v2.Port, 'after_update', + self.nova_notifier.send_port_status) + db_api.sqla_listen(models_v2.Port.status, 'set', + self.nova_notifier.record_port_status_changed) + for e in (events.BEFORE_CREATE, events.BEFORE_UPDATE, + events.BEFORE_DELETE): + registry.subscribe(self.validate_network_rbac_policy_change, + rbac_mixin.RBAC_POLICY, e) + + @db_api.retry_if_session_inactive() + def validate_network_rbac_policy_change(self, resource, event, trigger, + context, object_type, policy, + **kwargs): + """Validates network RBAC policy changes. + + On creation, verify that the creator is an admin or that it owns the + network it is sharing. + + On update and delete, make sure the tenant losing access does not have + resources that depend on that access. + """ + if object_type != 'network' or policy['action'] != 'access_as_shared': + # we only care about shared network policies + return + # The object a policy targets cannot be changed so we can look + # at the original network for the update event as well. + net = self._get_network(context, policy['object_id']) + if event in (events.BEFORE_CREATE, events.BEFORE_UPDATE): + # we still have to verify that the caller owns the network because + # _get_network will succeed on a shared network + if not context.is_admin and net['tenant_id'] != context.tenant_id: + msg = _("Only admins can manipulate policies on networks " + "they do not own") + raise exc.InvalidInput(error_message=msg) + + tenant_to_check = None + self_sharing = policy['target_tenant'] == net['tenant_id'] + if self_sharing: + return + if event == events.BEFORE_UPDATE: + new_tenant = kwargs['policy_update']['target_tenant'] + if policy['target_tenant'] != new_tenant: + tenant_to_check = policy['target_tenant'] + + if event == events.BEFORE_DELETE: + tenant_to_check = policy['target_tenant'] + + if tenant_to_check: + self.ensure_no_tenant_ports_on_network(net['id'], net['tenant_id'], + tenant_to_check) + + def ensure_no_tenant_ports_on_network(self, network_id, net_tenant_id, + tenant_id): + ctx_admin = ctx.get_admin_context() + rb_model = rbac_db.NetworkRBAC + other_rbac_entries = self._model_query(ctx_admin, rb_model).filter( + and_(rb_model.object_id == network_id, + rb_model.action == 'access_as_shared')) + ports = self._model_query(ctx_admin, models_v2.Port).filter( + models_v2.Port.network_id == network_id) + if tenant_id == '*': + # for the wildcard we need to get all of the rbac entries to + # see if any allow the remaining ports on the network. + other_rbac_entries = other_rbac_entries.filter( + rb_model.target_tenant != tenant_id) + # any port with another RBAC entry covering it or one belonging to + # the same tenant as the network owner is ok + allowed_tenants = [entry['target_tenant'] + for entry in other_rbac_entries] + allowed_tenants.append(net_tenant_id) + ports = ports.filter( + ~models_v2.Port.tenant_id.in_(allowed_tenants)) + else: + # if there is a wildcard rule, we can return early because it + # allows any ports + query = other_rbac_entries.filter(rb_model.target_tenant == '*') + if query.count(): + return + ports = ports.filter(models_v2.Port.tenant_id == tenant_id) + if ports.count(): + raise n_exc.InvalidSharedSetting(network=network_id) + + def set_ipam_backend(self): + self.ipam = ipam_pluggable_backend.IpamPluggableBackend() + + def _validate_host_route(self, route, ip_version): + try: + netaddr.IPNetwork(route['destination']) + netaddr.IPAddress(route['nexthop']) + except netaddr.core.AddrFormatError: + err_msg = _("Invalid route: %s") % route + raise exc.InvalidInput(error_message=err_msg) + except ValueError: + # netaddr.IPAddress would raise this + err_msg = _("Invalid route: %s") % route + raise exc.InvalidInput(error_message=err_msg) + self._validate_ip_version(ip_version, route['nexthop'], 'nexthop') + self._validate_ip_version(ip_version, route['destination'], + 'destination') + + def _validate_shared_update(self, context, id, original, updated): + # The only case that needs to be validated is when 'shared' + # goes from True to False + if updated['shared'] == original.shared or updated['shared']: + return + ports = self._model_query( + context, models_v2.Port).filter(models_v2.Port.network_id == id) + ports = ports.filter(not_(models_v2.Port.device_owner.startswith( + constants.DEVICE_OWNER_NETWORK_PREFIX))) + subnets = self._model_query( + context, models_v2.Subnet).filter( + models_v2.Subnet.network_id == id) + tenant_ids = set([port['tenant_id'] for port in ports] + + [subnet['tenant_id'] for subnet in subnets]) + # raise if multiple tenants found or if the only tenant found + # is not the owner of the network + if (len(tenant_ids) > 1 or len(tenant_ids) == 1 and + tenant_ids.pop() != original.tenant_id): + raise n_exc.InvalidSharedSetting(network=original.name) + + def _validate_ipv6_attributes(self, subnet, cur_subnet): + if cur_subnet: + self._validate_ipv6_update_dhcp(subnet, cur_subnet) + return + ra_mode_set = validators.is_attr_set(subnet.get('ipv6_ra_mode')) + address_mode_set = validators.is_attr_set( + subnet.get('ipv6_address_mode')) + self._validate_ipv6_dhcp(ra_mode_set, address_mode_set, + subnet['enable_dhcp']) + if ra_mode_set and address_mode_set: + self._validate_ipv6_combination(subnet['ipv6_ra_mode'], + subnet['ipv6_address_mode']) + if address_mode_set or ra_mode_set: + self._validate_eui64_applicable(subnet) + + def _validate_eui64_applicable(self, subnet): + # Per RFC 4862, section 5.5.3, prefix length and interface + # id together should be equal to 128. Currently neutron supports + # EUI64 interface id only, thus limiting the prefix + # length to be 64 only. + if ipv6_utils.is_auto_address_subnet(subnet): + if netaddr.IPNetwork(subnet['cidr']).prefixlen != 64: + msg = _('Invalid CIDR %s for IPv6 address mode. ' + 'OpenStack uses the EUI-64 address format, ' + 'which requires the prefix to be /64') + raise exc.InvalidInput( + error_message=(msg % subnet['cidr'])) + + def _validate_ipv6_combination(self, ra_mode, address_mode): + if ra_mode != address_mode: + msg = _("ipv6_ra_mode set to '%(ra_mode)s' with ipv6_address_mode " + "set to '%(addr_mode)s' is not valid. " + "If both attributes are set, they must be the same value" + ) % {'ra_mode': ra_mode, 'addr_mode': address_mode} + raise exc.InvalidInput(error_message=msg) + + def _validate_ipv6_dhcp(self, ra_mode_set, address_mode_set, enable_dhcp): + if (ra_mode_set or address_mode_set) and not enable_dhcp: + msg = _("ipv6_ra_mode or ipv6_address_mode cannot be set when " + "enable_dhcp is set to False") + raise exc.InvalidInput(error_message=msg) + + def _validate_ipv6_update_dhcp(self, subnet, cur_subnet): + if ('enable_dhcp' in subnet and not subnet['enable_dhcp']): + msg = _("Cannot disable enable_dhcp with " + "ipv6 attributes set") + + ra_mode_set = validators.is_attr_set(subnet.get('ipv6_ra_mode')) + address_mode_set = validators.is_attr_set( + subnet.get('ipv6_address_mode')) + + if ra_mode_set or address_mode_set: + raise exc.InvalidInput(error_message=msg) + + old_ra_mode_set = validators.is_attr_set( + cur_subnet.get('ipv6_ra_mode')) + old_address_mode_set = validators.is_attr_set( + cur_subnet.get('ipv6_address_mode')) + + if old_ra_mode_set or old_address_mode_set: + raise exc.InvalidInput(error_message=msg) + + def _create_bulk(self, resource, context, request_items): + objects = [] + collection = "%ss" % resource + items = request_items[collection] + context.session.begin(subtransactions=True) + try: + for item in items: + obj_creator = getattr(self, 'create_%s' % resource) + objects.append(obj_creator(context, item)) + context.session.commit() + except Exception: + context.session.rollback() + with excutils.save_and_reraise_exception(): + LOG.error(_LE("An exception occurred while creating " + "the %(resource)s:%(item)s"), + {'resource': resource, 'item': item}) + return objects + + @db_api.retry_if_session_inactive() + def create_network_bulk(self, context, networks): + return self._create_bulk('network', context, networks) + + @db_api.retry_if_session_inactive() + def create_network(self, context, network): + """Handle creation of a single network.""" + net_db = self.create_network_db(context, network) + return self._make_network_dict(net_db, process_extensions=False, + context=context) + + def create_network_db(self, context, network): + # single request processing + n = network['network'] + # NOTE(jkoelker) Get the tenant_id outside of the session to avoid + # unneeded db action if the operation raises + tenant_id = n['tenant_id'] + with context.session.begin(subtransactions=True): + args = {'tenant_id': tenant_id, + 'id': n.get('id') or uuidutils.generate_uuid(), + 'name': n['name'], + 'admin_state_up': n['admin_state_up'], + 'status': n.get('status', constants.NET_STATUS_ACTIVE), + # Hack by egon + 'vlan_transparent': False, + 'description': n.get('description')} + network = models_v2.Network(**args) + if n['shared']: + entry = rbac_db.NetworkRBAC( + network=network, action='access_as_shared', + target_tenant='*', tenant_id=network['tenant_id']) + context.session.add(entry) + context.session.add(network) + return network + + @db_api.retry_if_session_inactive() + def update_network(self, context, id, network): + n = network['network'] + with context.session.begin(subtransactions=True): + network = self._get_network(context, id) + # validate 'shared' parameter + if 'shared' in n: + entry = None + for item in network.rbac_entries: + if (item.action == 'access_as_shared' and + item.target_tenant == '*'): + entry = item + break + setattr(network, 'shared', True if entry else False) + self._validate_shared_update(context, id, network, n) + update_shared = n.pop('shared') + if update_shared and not entry: + entry = rbac_db.NetworkRBAC( + network=network, action='access_as_shared', + target_tenant='*', tenant_id=network['tenant_id']) + context.session.add(entry) + elif not update_shared and entry: + context.session.delete(entry) + context.session.expire(network, ['rbac_entries']) + # The filter call removes attributes from the body received from + # the API that are logically tied to network resources but are + # stored in other database tables handled by extensions + network.update( + ndb_utils.filter_non_model_columns(n, models_v2.Network)) + return self._make_network_dict(network, context=context) + + @db_api.retry_if_session_inactive() + def delete_network(self, context, id): + with context.session.begin(subtransactions=True): + network = self._get_network(context, id) + + auto_delete_ports = context.session.query( + models_v2.Port).filter_by(network_id=id).filter( + models_v2.Port.device_owner.in_(AUTO_DELETE_PORT_OWNERS)) + for port in auto_delete_ports: + context.session.delete(port) + + port_in_use = context.session.query(models_v2.Port).filter_by( + network_id=id).first() + + if port_in_use: + raise exc.NetworkInUse(net_id=id) + + # clean up subnets + subnets = self._get_subnets_by_network(context, id) + for subnet in subnets: + self.delete_subnet(context, subnet['id']) + + context.session.delete(network) + + @db_api.retry_if_session_inactive() + def get_network(self, context, id, fields=None): + network = self._get_network(context, id) + return self._make_network_dict(network, fields, context=context) + + @db_api.retry_if_session_inactive() + def get_networks(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + marker_obj = self._get_marker_obj(context, 'network', limit, marker) + make_network_dict = functools.partial(self._make_network_dict, + context=context) + return self._get_collection(context, models_v2.Network, + make_network_dict, + filters=filters, fields=fields, + sorts=sorts, + limit=limit, + marker_obj=marker_obj, + page_reverse=page_reverse) + + @db_api.retry_if_session_inactive() + def get_networks_count(self, context, filters=None): + return self._get_collection_count(context, models_v2.Network, + filters=filters) + + @db_api.retry_if_session_inactive() + def create_subnet_bulk(self, context, subnets): + return self._create_bulk('subnet', context, subnets) + + def _validate_ip_version(self, ip_version, addr, name): + """Check IP field of a subnet match specified ip version.""" + ip = netaddr.IPNetwork(addr) + if ip.version != ip_version: + data = {'name': name, + 'addr': addr, + 'ip_version': ip_version} + msg = _("%(name)s '%(addr)s' does not match " + "the ip_version '%(ip_version)s'") % data + raise exc.InvalidInput(error_message=msg) + + def _validate_subnet(self, context, s, cur_subnet=None): + """Validate a subnet spec.""" + + # This method will validate attributes which may change during + # create_subnet() and update_subnet(). + # The method requires the subnet spec 's' has 'ip_version' field. + # If 's' dict does not have 'ip_version' field in an API call + # (e.g., update_subnet()), you need to set 'ip_version' field + # before calling this method. + + ip_ver = s['ip_version'] + + if validators.is_attr_set(s.get('cidr')): + self._validate_ip_version(ip_ver, s['cidr'], 'cidr') + + # TODO(watanabe.isao): After we found a way to avoid the re-sync + # from the agent side, this restriction could be removed. + if cur_subnet: + dhcp_was_enabled = cur_subnet.enable_dhcp + else: + dhcp_was_enabled = False + if s.get('enable_dhcp') and not dhcp_was_enabled: + subnet_prefixlen = netaddr.IPNetwork(s['cidr']).prefixlen + error_message = _("Subnet has a prefix length that is " + "incompatible with DHCP service enabled") + if ((ip_ver == 4 and subnet_prefixlen > 30) or + (ip_ver == 6 and subnet_prefixlen > 126)): + raise exc.InvalidInput(error_message=error_message) + + net = netaddr.IPNetwork(s['cidr']) + if net.is_multicast(): + error_message = _("Multicast IP subnet is not supported " + "if enable_dhcp is True") + raise exc.InvalidInput(error_message=error_message) + elif net.is_loopback(): + error_message = _("Loopback IP subnet is not supported " + "if enable_dhcp is True") + raise exc.InvalidInput(error_message=error_message) + + if validators.is_attr_set(s.get('gateway_ip')): + self._validate_ip_version(ip_ver, s['gateway_ip'], 'gateway_ip') + is_gateway_not_valid = ( + ipam.utils.check_gateway_invalid_in_subnet( + s['cidr'], s['gateway_ip'])) + if is_gateway_not_valid: + error_message = _("Gateway is not valid on subnet") + raise exc.InvalidInput(error_message=error_message) + # Ensure the gateway IP is not assigned to any port + # skip this check in case of create (s parameter won't have id) + # NOTE(salv-orlando): There is slight chance of a race, when + # a subnet-update and a router-interface-add operation are + # executed concurrently + if cur_subnet and not ipv6_utils.is_ipv6_pd_enabled(s): + ipal = models_v2.IPAllocation + alloc_qry = context.session.query(ipal) + alloc_qry = alloc_qry.join("port", "routerport") + allocated = alloc_qry.filter( + ipal.ip_address == cur_subnet['gateway_ip'], + ipal.subnet_id == cur_subnet['id']).first() + if allocated and allocated['port_id']: + raise n_exc.GatewayIpInUse( + ip_address=cur_subnet['gateway_ip'], + port_id=allocated['port_id']) + + if validators.is_attr_set(s.get('dns_nameservers')): + if len(s['dns_nameservers']) > cfg.CONF.max_dns_nameservers: + raise n_exc.DNSNameServersExhausted( + subnet_id=s.get('id', _('new subnet')), + quota=cfg.CONF.max_dns_nameservers) + for dns in s['dns_nameservers']: + try: + netaddr.IPAddress(dns) + except Exception: + raise exc.InvalidInput( + error_message=(_("Error parsing dns address %s") % + dns)) + self._validate_ip_version(ip_ver, dns, 'dns_nameserver') + + if validators.is_attr_set(s.get('host_routes')): + if len(s['host_routes']) > cfg.CONF.max_subnet_host_routes: + raise n_exc.HostRoutesExhausted( + subnet_id=s.get('id', _('new subnet')), + quota=cfg.CONF.max_subnet_host_routes) + # check if the routes are all valid + for rt in s['host_routes']: + self._validate_host_route(rt, ip_ver) + + if ip_ver == 4: + if validators.is_attr_set(s.get('ipv6_ra_mode')): + raise exc.InvalidInput( + error_message=(_("ipv6_ra_mode is not valid when " + "ip_version is 4"))) + if validators.is_attr_set(s.get('ipv6_address_mode')): + raise exc.InvalidInput( + error_message=(_("ipv6_address_mode is not valid when " + "ip_version is 4"))) + if ip_ver == 6: + self._validate_ipv6_attributes(s, cur_subnet) + + def _validate_subnet_for_pd(self, subnet): + """Validates that subnet parameters are correct for IPv6 PD""" + if (subnet.get('ip_version') != constants.IP_VERSION_6): + reason = _("Prefix Delegation can only be used with IPv6 " + "subnets.") + raise exc.BadRequest(resource='subnets', msg=reason) + + mode_list = [constants.IPV6_SLAAC, + constants.DHCPV6_STATELESS] + + ra_mode = subnet.get('ipv6_ra_mode') + if ra_mode not in mode_list: + reason = _("IPv6 RA Mode must be SLAAC or Stateless for " + "Prefix Delegation.") + raise exc.BadRequest(resource='subnets', msg=reason) + + address_mode = subnet.get('ipv6_address_mode') + if address_mode not in mode_list: + reason = _("IPv6 Address Mode must be SLAAC or Stateless for " + "Prefix Delegation.") + raise exc.BadRequest(resource='subnets', msg=reason) + + def _update_router_gw_ports(self, context, network, subnet): + l3plugin = directory.get_plugin(constants.L3) + if l3plugin: + gw_ports = self._get_router_gw_ports_by_network(context, + network['id']) + router_ids = [p['device_id'] for p in gw_ports] + for id in router_ids: + try: + self._update_router_gw_port(context, id, network, subnet) + except l3.RouterNotFound: + LOG.debug("Router %(id)s was concurrently deleted while " + "updating GW port for subnet %(s)s", + {'id': id, 's': subnet}) + + def _update_router_gw_port(self, context, router_id, network, subnet): + l3plugin = directory.get_plugin(constants.L3) + ctx_admin = context.elevated() + ext_subnets_dict = {s['id']: s for s in network['subnets']} + router = l3plugin.get_router(ctx_admin, router_id) + external_gateway_info = router['external_gateway_info'] + # Get all stateful (i.e. non-SLAAC/DHCPv6-stateless) fixed ips + fips = [f for f in external_gateway_info['external_fixed_ips'] + if not ipv6_utils.is_auto_address_subnet( + ext_subnets_dict[f['subnet_id']])] + num_fips = len(fips) + # Don't add the fixed IP to the port if it already + # has a stateful fixed IP of the same IP version + if num_fips > 1: + return + if num_fips == 1 and netaddr.IPAddress( + fips[0]['ip_address']).version == subnet['ip_version']: + return + external_gateway_info['external_fixed_ips'].append( + {'subnet_id': subnet['id']}) + info = {'router': {'external_gateway_info': + external_gateway_info}} + l3plugin.update_router(context, router_id, info) + + @db_api.retry_if_session_inactive() + def _create_subnet_postcommit(self, context, result, network, ipam_subnet): + if hasattr(network, 'external') and network.external: + self._update_router_gw_ports(context, + network, + result) + # If this subnet supports auto-addressing, then update any + # internal ports on the network with addresses for this subnet. + if ipv6_utils.is_auto_address_subnet(result): + updated_ports = self.ipam.add_auto_addrs_on_network_ports(context, + result, ipam_subnet) + for port_id in updated_ports: + port_info = {'port': {'id': port_id}} + try: + self.update_port(context, port_id, port_info) + except exc.PortNotFound: + LOG.debug("Port %(p)s concurrently deleted while adding " + "address for new subnet %(s)s.", {'p': port_id, + 's': result}) + + def _get_subnetpool_id(self, context, subnet): + """Return the subnetpool id for this request + + :param subnet: The subnet dict from the request + """ + use_default_subnetpool = subnet.get('use_default_subnetpool') + if use_default_subnetpool == constants.ATTR_NOT_SPECIFIED: + use_default_subnetpool = False + subnetpool_id = subnet.get('subnetpool_id') + if subnetpool_id == constants.ATTR_NOT_SPECIFIED: + subnetpool_id = None + + if use_default_subnetpool and subnetpool_id: + msg = _('subnetpool_id and use_default_subnetpool cannot both be ' + 'specified') + raise exc.BadRequest(resource='subnets', msg=msg) + + if subnetpool_id: + return subnetpool_id + + if not use_default_subnetpool: + return + + cidr = subnet.get('cidr') + if validators.is_attr_set(cidr): + ip_version = netaddr.IPNetwork(cidr).version + else: + ip_version = subnet.get('ip_version') + if not validators.is_attr_set(ip_version): + msg = _('ip_version must be specified in the absence of ' + 'cidr and subnetpool_id') + raise exc.BadRequest(resource='subnets', msg=msg) + + if ip_version == 6 and cfg.CONF.ipv6_pd_enabled: + return constants.IPV6_PD_POOL_ID + + subnetpool = self.get_default_subnetpool(context, ip_version) + if subnetpool: + return subnetpool['id'] + + msg = _('No default subnetpool found for IPv%s') % ip_version + raise exc.BadRequest(resource='subnets', msg=msg) + + @db_api.retry_if_session_inactive() + def create_subnet(self, context, subnet): + result, net, ipam_sub = self._create_subnet_precommit(context, subnet) + self._create_subnet_postcommit(context, result, net, ipam_sub) + return result + + def _create_subnet_precommit(self, context, subnet): + """Creates subnet in DB, returns result, network, and ipam_subnet.""" + s = subnet['subnet'] + cidr = s.get('cidr', constants.ATTR_NOT_SPECIFIED) + prefixlen = s.get('prefixlen', constants.ATTR_NOT_SPECIFIED) + has_cidr = validators.is_attr_set(cidr) + has_prefixlen = validators.is_attr_set(prefixlen) + + if has_cidr and has_prefixlen: + msg = _('cidr and prefixlen must not be supplied together') + raise exc.BadRequest(resource='subnets', msg=msg) + + if has_cidr: + # turn the CIDR into a proper subnet + net = netaddr.IPNetwork(s['cidr']) + subnet['subnet']['cidr'] = '%s/%s' % (net.network, net.prefixlen) + + subnetpool_id = self._get_subnetpool_id(context, s) + if not subnetpool_id and not has_cidr: + msg = _('a subnetpool must be specified in the absence of a cidr') + raise exc.BadRequest(resource='subnets', msg=msg) + + if subnetpool_id: + self.ipam.validate_pools_with_subnetpool(s) + if subnetpool_id == constants.IPV6_PD_POOL_ID: + if has_cidr: + # We do not currently support requesting a specific + # cidr with IPv6 prefix delegation. Set the subnetpool_id + # to None and allow the request to continue as normal. + subnetpool_id = None + self._validate_subnet(context, s) + else: + prefix = n_const.PROVISIONAL_IPV6_PD_PREFIX + subnet['subnet']['cidr'] = prefix + self._validate_subnet_for_pd(s) + else: + if not has_cidr: + msg = _('A cidr must be specified in the absence of a ' + 'subnet pool') + raise exc.BadRequest(resource='subnets', msg=msg) + self._validate_subnet(context, s) + + with context.session.begin(subtransactions=True): + network = self._get_network(context, + subnet['subnet']['network_id']) + subnet, ipam_subnet = self.ipam.allocate_subnet(context, + network, + subnet['subnet'], + subnetpool_id) + result = self._make_subnet_dict(subnet, context=context) + return result, network, ipam_subnet + + def _update_allocation_pools(self, subnet): + """Gets new allocation pools and formats them correctly""" + allocation_pools = self.ipam.generate_pools(subnet['cidr'], + subnet['gateway_ip']) + return [{'start': str(netaddr.IPAddress(p.first, + subnet['ip_version'])), + 'end': str(netaddr.IPAddress(p.last, subnet['ip_version']))} + for p in allocation_pools] + + @db_api.retry_if_session_inactive() + def update_subnet(self, context, id, subnet): + """Update the subnet with new info. + + The change however will not be realized until the client renew the + dns lease or we support gratuitous DHCP offers + """ + orig = self.get_subnet(context, id) + result = self._update_subnet_precommit(context, id, subnet) + return self._update_subnet_postcommit(context, orig, result) + + def _update_subnet_precommit(self, context, id, subnet): + """All subnet update operations safe to enclose in a transaction. + + :param context: neutron api request context + :param id: subnet id + :param subnet: API request dictionary + """ + s = subnet['subnet'] + new_cidr = s.get('cidr') + db_subnet = self._get_subnet(context, id) + # Fill 'ip_version' and 'allocation_pools' fields with the current + # value since _validate_subnet() expects subnet spec has 'ip_version' + # and 'allocation_pools' fields. + s['ip_version'] = db_subnet.ip_version + s['cidr'] = db_subnet.cidr + s['id'] = db_subnet.id + s['tenant_id'] = db_subnet.tenant_id + s['subnetpool_id'] = db_subnet.subnetpool_id + self._validate_subnet(context, s, cur_subnet=db_subnet) + db_pools = [netaddr.IPRange(p['first_ip'], p['last_ip']) + for p in db_subnet.allocation_pools] + + if new_cidr and ipv6_utils.is_ipv6_pd_enabled(s): + # This is an ipv6 prefix delegation-enabled subnet being given an + # updated cidr by the process_prefix_update RPC + s['cidr'] = new_cidr + net = netaddr.IPNetwork(s['cidr'], s['ip_version']) + # Update gateway_ip and allocation pools based on new cidr + s['gateway_ip'] = utils.get_first_host_ip(net, s['ip_version']) + s['allocation_pools'] = self._update_allocation_pools(s) + + range_pools = None + if s.get('allocation_pools') is not None: + # Convert allocation pools to IPRange to simplify future checks + range_pools = self.ipam.pools_to_ip_range(s['allocation_pools']) + self.ipam.validate_allocation_pools(range_pools, s['cidr']) + s['allocation_pools'] = range_pools + + # If either gateway_ip or allocation_pools were specified + gateway_ip = s.get('gateway_ip', db_subnet.gateway_ip) + gateway_ip_changed = gateway_ip != db_subnet.gateway_ip + if gateway_ip_changed or s.get('allocation_pools') is not None: + pools = range_pools if range_pools is not None else db_pools + if gateway_ip: + self.ipam.validate_gw_out_of_pools(gateway_ip, pools) + + if gateway_ip_changed: + # Provide pre-update notification not to break plugins that don't + # support gateway ip change + kwargs = {'context': context, 'subnet_id': id, + 'network_id': db_subnet.network_id} + registry.notify(resources.SUBNET_GATEWAY, events.BEFORE_UPDATE, + self, **kwargs) + + with context.session.begin(subtransactions=True): + subnet, changes = self.ipam.update_db_subnet(context, id, s, + db_pools) + # we expire here since ipam may have made changes to relationships + # that will be stale on any subsequent lookups while the subnet object + # is in the session otherwise. + context.session.expire(subnet) + return self._make_subnet_dict(subnet, context=context) + + def _update_subnet_postcommit(self, context, orig, result): + """Subnet update operations that happen after transaction completes. + + :param context: neutron api request context + :param orig: subnet dictionary representing state before update + :param result: subnet dictionary representing state after update + """ + update_ports_needed = (result['cidr'] != orig['cidr'] and + ipv6_utils.is_ipv6_pd_enabled(result)) + if update_ports_needed: + # Find ports that have not yet been updated + # with an IP address by Prefix Delegation, and update them + filters = {'fixed_ips': {'subnet_id': [result['id']]}} + ports = self.get_ports(context, filters=filters) + routers = [] + for port in ports: + for ip in port['fixed_ips']: + if ip['subnet_id'] == result['id']: + if (port['device_owner'] in + constants.ROUTER_INTERFACE_OWNERS): + routers.append(port['device_id']) + ip['ip_address'] = result['gateway_ip'] + else: + # We remove ip_address and pass only PD subnet_id + # in port's fixed_ip for port_update. Later, IPAM + # drivers will allocate eui64 address with new + # prefix when they find PD subnet_id in port's + # fixed_ip. + ip.pop('ip_address', None) + self.update_port(context, port['id'], {'port': port}) + # Send router_update to l3_agent + if routers: + l3_rpc_notifier = l3_rpc_agent_api.L3AgentNotifyAPI() + l3_rpc_notifier.routers_updated(context, routers) + + if orig['gateway_ip'] != result['gateway_ip']: + kwargs = {'context': context, 'subnet_id': result['id'], + 'network_id': result['network_id']} + registry.notify(resources.SUBNET_GATEWAY, events.AFTER_UPDATE, + self, **kwargs) + + return result + + def _subnet_get_user_allocation(self, context, subnet_id): + """Check if there are any user ports on subnet and return first.""" + # need to join with ports table as IPAllocation's port + # is not joined eagerly and thus producing query which yields + # incorrect results + return (context.session.query(models_v2.IPAllocation). + filter_by(subnet_id=subnet_id).join(models_v2.Port). + filter(~models_v2.Port.device_owner. + in_(AUTO_DELETE_PORT_OWNERS)).first()) + + def _subnet_check_ip_allocations_internal_router_ports(self, context, + subnet_id): + # Do not delete the subnet if IP allocations for internal + # router ports still exist + allocs = context.session.query(models_v2.IPAllocation).filter_by( + subnet_id=subnet_id).join(models_v2.Port).filter( + models_v2.Port.device_owner.in_( + constants.ROUTER_INTERFACE_OWNERS) + ).first() + if allocs: + LOG.debug("Subnet %s still has internal router ports, " + "cannot delete", subnet_id) + raise exc.SubnetInUse(subnet_id=subnet_id) + + @db_api.retry_if_session_inactive() + def _remove_subnet_from_port(self, context, sub_id, port_id, auto_subnet): + try: + fixed = [f for f in self.get_port(context, port_id)['fixed_ips'] + if f['subnet_id'] != sub_id] + if auto_subnet: + # special flag to avoid re-allocation on auto subnets + fixed.append({'subnet_id': sub_id, 'delete_subnet': True}) + data = {attributes.PORT: {'fixed_ips': fixed}} + self.update_port(context, port_id, data) + except exc.PortNotFound: + # port is gone + return + except exc.SubnetNotFound as e: + # another subnet in the fixed ips was concurrently removed. retry + raise os_db_exc.RetryRequest(e) + + def _ensure_no_user_ports_on_subnet(self, context, id): + alloc = self._subnet_get_user_allocation(context, id) + if alloc: + LOG.info(_LI("Found port (%(port_id)s, %(ip)s) having IP " + "allocation on subnet " + "%(subnet)s, cannot delete"), + {'ip': alloc.ip_address, + 'port_id': alloc.port_id, + 'subnet': id}) + raise exc.SubnetInUse(subnet_id=id) + + @db_api.retry_if_session_inactive() + def _remove_subnet_ip_allocations_from_ports(self, context, id): + # Do not allow a subnet to be deleted if a router is attached to it + self._subnet_check_ip_allocations_internal_router_ports( + context, id) + subnet = self._get_subnet(context, id) + is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet) + if not is_auto_addr_subnet: + # we only automatically remove IP addresses from user ports if + # the IPs come from auto allocation subnets. + self._ensure_no_user_ports_on_subnet(context, id) + net_allocs = (context.session.query(models_v2.IPAllocation.port_id). + filter_by(subnet_id=id)) + port_ids_on_net = [ipal.port_id for ipal in net_allocs] + for port_id in port_ids_on_net: + self._remove_subnet_from_port(context, id, port_id, + auto_subnet=is_auto_addr_subnet) + + @db_api.retry_if_session_inactive() + def delete_subnet(self, context, id): + LOG.debug("Deleting subnet %s", id) + # Make sure the subnet isn't used by other resources + _check_subnet_not_used(context, id) + self._remove_subnet_ip_allocations_from_ports(context, id) + # retry integrity errors to catch ip allocation races + with db_api.exc_to_retry(sql_exc.IntegrityError), \ + context.session.begin(subtransactions=True): + subnet_db = self._get_subnet(context, id) + subnet = self._make_subnet_dict(subnet_db, context=context) + registry.notify(resources.SUBNET, events.PRECOMMIT_DELETE, + self, context=context, subnet_id=id) + context.session.delete(subnet_db) + # Delete related ipam subnet manually, + # since there is no FK relationship + self.ipam.delete_subnet(context, id) + registry.notify(resources.SUBNET, events.AFTER_DELETE, + self, context=context, subnet=subnet) + + @db_api.retry_if_session_inactive() + def get_subnet(self, context, id, fields=None): + subnet = self._get_subnet(context, id) + return self._make_subnet_dict(subnet, fields, context=context) + + @db_api.retry_if_session_inactive() + def get_subnets(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + return self._get_subnets(context, filters, fields, sorts, limit, + marker, page_reverse) + + @db_api.retry_if_session_inactive() + def get_subnets_count(self, context, filters=None): + return self._get_collection_count(context, models_v2.Subnet, + filters=filters) + + @db_api.retry_if_session_inactive() + def get_subnets_by_network(self, context, network_id): + return [self._make_subnet_dict(subnet_db) for subnet_db in + self._get_subnets_by_network(context, network_id)] + + def _validate_address_scope_id(self, context, address_scope_id, + subnetpool_id, sp_prefixes, ip_version): + """Validate the address scope before associating. + + Subnetpool can associate with an address scope if + - the tenant user is the owner of both the subnetpool and + address scope + - the admin is associating the subnetpool with the shared + address scope + - there is no prefix conflict with the existing subnetpools + associated with the address scope. + - the address family of the subnetpool and address scope + are the same + """ + if not validators.is_attr_set(address_scope_id): + return + + if not self.is_address_scope_owned_by_tenant(context, + address_scope_id): + raise n_exc.IllegalSubnetPoolAssociationToAddressScope( + subnetpool_id=subnetpool_id, address_scope_id=address_scope_id) + + as_ip_version = self.get_ip_version_for_address_scope(context, + address_scope_id) + + if ip_version != as_ip_version: + raise n_exc.IllegalSubnetPoolIpVersionAssociationToAddressScope( + subnetpool_id=subnetpool_id, address_scope_id=address_scope_id, + ip_version=as_ip_version) + + subnetpools = subnetpool_obj.SubnetPool.get_objects( + context, address_scope_id=address_scope_id) + + new_set = netaddr.IPSet(sp_prefixes) + for sp in subnetpools: + if sp.id == subnetpool_id: + continue + sp_set = netaddr.IPSet(sp.prefixes) + if sp_set.intersection(new_set): + raise n_exc.AddressScopePrefixConflict() + + def _check_subnetpool_update_allowed(self, context, subnetpool_id, + address_scope_id): + """Check if the subnetpool can be updated or not. + + If the subnetpool is associated to a shared address scope not owned + by the tenant, then the subnetpool cannot be updated. + """ + + if not self.is_address_scope_owned_by_tenant(context, + address_scope_id): + msg = _("subnetpool %(subnetpool_id)s cannot be updated when" + " associated with shared address scope " + "%(address_scope_id)s") % { + 'subnetpool_id': subnetpool_id, + 'address_scope_id': address_scope_id} + raise n_exc.IllegalSubnetPoolUpdate(reason=msg) + + def _check_default_subnetpool_exists(self, context, ip_version): + """Check if a default already exists for the given IP version. + + There can only be one default subnetpool for each IP family. Raise an + InvalidInput error if a default has already been set. + """ + if self.get_default_subnetpool(context, ip_version): + msg = _("A default subnetpool for this IP family has already " + "been set. Only one default may exist per IP family") + raise exc.InvalidInput(error_message=msg) + + @db_api.retry_if_session_inactive() + def create_subnetpool(self, context, subnetpool): + sp = subnetpool['subnetpool'] + sp_reader = subnet_alloc.SubnetPoolReader(sp) + if sp_reader.is_default: + self._check_default_subnetpool_exists(context, + sp_reader.ip_version) + self._validate_address_scope_id(context, sp_reader.address_scope_id, + id, sp_reader.prefixes, + sp_reader.ip_version) + pool_args = {'project_id': sp['tenant_id'], + 'id': sp_reader.id, + 'name': sp_reader.name, + 'ip_version': sp_reader.ip_version, + 'default_prefixlen': + sp_reader.default_prefixlen, + 'min_prefixlen': sp_reader.min_prefixlen, + 'max_prefixlen': sp_reader.max_prefixlen, + 'is_default': sp_reader.is_default, + 'shared': sp_reader.shared, + 'default_quota': sp_reader.default_quota, + 'address_scope_id': sp_reader.address_scope_id, + 'description': sp_reader.description, + 'prefixes': sp_reader.prefixes} + subnetpool = subnetpool_obj.SubnetPool(context, **pool_args) + subnetpool.create() + + return self._make_subnetpool_dict(subnetpool.db_obj) + + @db_api.retry_if_session_inactive() + def update_subnetpool(self, context, id, subnetpool): + new_sp = subnetpool['subnetpool'] + + with context.session.begin(subtransactions=True): + orig_sp = self._get_subnetpool(context, id=id) + updated = _update_subnetpool_dict(orig_sp, new_sp) + reader = subnet_alloc.SubnetPoolReader(updated) + if reader.is_default and not orig_sp.is_default: + self._check_default_subnetpool_exists(context, + reader.ip_version) + if orig_sp.address_scope_id: + self._check_subnetpool_update_allowed(context, id, + orig_sp.address_scope_id) + + self._validate_address_scope_id(context, reader.address_scope_id, + id, reader.prefixes, + reader.ip_version) + address_scope_changed = ( + orig_sp.address_scope_id != reader.address_scope_id) + + orig_sp.update_fields(reader.subnetpool) + orig_sp.update() + + if address_scope_changed: + # Notify about the update of subnetpool's address scope + kwargs = {'context': context, 'subnetpool_id': id} + registry.notify(resources.SUBNETPOOL_ADDRESS_SCOPE, + events.AFTER_UPDATE, + self.update_subnetpool, + **kwargs) + + for key in ['min_prefixlen', 'max_prefixlen', 'default_prefixlen']: + updated['key'] = str(updated[key]) + self._apply_dict_extend_functions(attributes.SUBNETPOOLS, + updated, orig_sp.db_obj) + return updated + + @db_api.retry_if_session_inactive() + def get_subnetpool(self, context, id, fields=None): + subnetpool = self._get_subnetpool(context, id) + return self._make_subnetpool_dict(subnetpool.db_obj, fields) + + @db_api.retry_if_session_inactive() + def get_subnetpools(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pager = base_obj.Pager(sorts, limit, page_reverse, marker) + filters = filters or {} + subnetpools = subnetpool_obj.SubnetPool.get_objects( + context, _pager=pager, validate_filters=False, **filters) + return [ + self._make_subnetpool_dict(pool.db_obj, fields) + for pool in subnetpools + ] + + @db_api.retry_if_session_inactive() + def get_default_subnetpool(self, context, ip_version): + """Retrieve the default subnetpool for the given IP version.""" + filters = {'is_default': True, + 'ip_version': ip_version} + subnetpool = self.get_subnetpools(context, filters=filters) + if subnetpool: + return subnetpool[0] + + @db_api.retry_if_session_inactive() + def delete_subnetpool(self, context, id): + with context.session.begin(subtransactions=True): + subnetpool = self._get_subnetpool(context, id=id) + subnets = self._get_subnets_by_subnetpool(context, id) + if subnets: + reason = _("Subnet pool has existing allocations") + raise n_exc.SubnetPoolDeleteError(reason=reason) + subnetpool.delete() + + def _check_mac_addr_update(self, context, port, new_mac, device_owner): + if (device_owner and + device_owner.startswith(constants.DEVICE_OWNER_NETWORK_PREFIX)): + raise n_exc.UnsupportedPortDeviceOwner( + op=_("mac address update"), port_id=id, + device_owner=device_owner) + + @db_api.retry_if_session_inactive() + def create_port_bulk(self, context, ports): + return self._create_bulk('port', context, ports) + + def _create_db_port_obj(self, context, port_data): + mac_address = port_data.pop('mac_address', None) + if mac_address: + if self._is_mac_in_use(context, port_data['network_id'], + mac_address): + raise exc.MacAddressInUse(net_id=port_data['network_id'], + mac=mac_address) + else: + mac_address = self._generate_mac() + db_port = models_v2.Port(mac_address=mac_address, **port_data) + context.session.add(db_port) + return db_port + + @db_api.retry_if_session_inactive() + def create_port(self, context, port): + db_port = self.create_port_db(context, port) + return self._make_port_dict(db_port, process_extensions=False) + + def create_port_db(self, context, port): + p = port['port'] + port_id = p.get('id') or uuidutils.generate_uuid() + network_id = p['network_id'] + # NOTE(jkoelker) Get the tenant_id outside of the session to avoid + # unneeded db action if the operation raises + tenant_id = p['tenant_id'] + if p.get('device_owner'): + self._enforce_device_owner_not_router_intf_or_device_id( + context, p.get('device_owner'), p.get('device_id'), tenant_id) + + port_data = dict(tenant_id=tenant_id, + name=p['name'], + id=port_id, + network_id=network_id, + admin_state_up=p['admin_state_up'], + status=p.get('status', constants.PORT_STATUS_ACTIVE), + device_id=p['device_id'], + device_owner=p['device_owner'], + description=p.get('description')) + if p.get('mac_address') is not constants.ATTR_NOT_SPECIFIED: + port_data['mac_address'] = p.get('mac_address') + with context.session.begin(subtransactions=True): + # Ensure that the network exists. + self._get_network(context, network_id) + + # Create the port + db_port = self._create_db_port_obj(context, port_data) + p['mac_address'] = db_port['mac_address'] + + try: + self.ipam.allocate_ips_for_port_and_store( + context, port, port_id) + db_port['ip_allocation'] = ipa.IP_ALLOCATION_IMMEDIATE + except ipam_exc.DeferIpam: + db_port['ip_allocation'] = ipa.IP_ALLOCATION_DEFERRED + fixed_ips = p['fixed_ips'] + if validators.is_attr_set(fixed_ips) and not fixed_ips: + # [] was passed explicitly as fixed_ips. An unaddressed port. + db_port['ip_allocation'] = ipa.IP_ALLOCATION_NONE + + return db_port + + def _validate_port_for_update(self, context, db_port, new_port, new_mac): + changed_owner = 'device_owner' in new_port + current_owner = (new_port.get('device_owner') or + db_port['device_owner']) + changed_device_id = new_port.get('device_id') != db_port['device_id'] + current_device_id = new_port.get('device_id') or db_port['device_id'] + + if current_owner and changed_device_id or changed_owner: + self._enforce_device_owner_not_router_intf_or_device_id( + context, current_owner, current_device_id, + db_port['tenant_id']) + + if new_mac and new_mac != db_port['mac_address']: + self._check_mac_addr_update(context, db_port, + new_mac, current_owner) + + @db_api.retry_if_session_inactive() + def update_port(self, context, id, port): + new_port = port['port'] + + with context.session.begin(subtransactions=True): + db_port = self._get_port(context, id) + new_mac = new_port.get('mac_address') + self._validate_port_for_update(context, db_port, new_port, new_mac) + # Note: _make_port_dict is called here to load extension data + # (specifically host binding). The IPAM plugin is separate from + # the core plugin, so extensions are not loaded. + # + # The IPAM code could cheat and get it directly from db_port but it + # would have to know about the implementation (remember ml2 has its + # own port binding schema that differs from the generic one) + # + # This code could extract just the port binding host here and pass + # that in. The problem is that db_base_plugin_common shouldn't + # know anything about port binding. This compromise sends IPAM a + # port_dict with all of the extension data loaded. + try: + self.ipam.update_port( + context, + old_port_db=db_port, + old_port=self._make_port_dict(db_port), + new_port=new_port) + except ipam_exc.IpAddressAllocationNotFound as e: + # If a port update and a subnet delete interleave, there is a + # chance that the IPAM update operation raises this exception. + # Rather than throwing that up to the user under some sort of + # conflict, bubble up a retry instead that should bring things + # back to sanity. + raise os_db_exc.RetryRequest(e) + result = self._make_port_dict(db_port) + return result + + @db_api.retry_if_session_inactive() + def delete_port(self, context, id): + with context.session.begin(subtransactions=True): + self.ipam.delete_port(context, id) + + def delete_ports_by_device_id(self, context, device_id, network_id=None): + query = (context.session.query(models_v2.Port.id) + .enable_eagerloads(False) + .filter(models_v2.Port.device_id == device_id)) + if network_id: + query = query.filter(models_v2.Port.network_id == network_id) + port_ids = [p[0] for p in query] + for port_id in port_ids: + try: + self.delete_port(context, port_id) + except exc.PortNotFound: + # Don't raise if something else concurrently deleted the port + LOG.debug("Ignoring PortNotFound when deleting port '%s'. " + "The port has already been deleted.", + port_id) + + @db_api.retry_if_session_inactive() + def get_port(self, context, id, fields=None): + port = self._get_port(context, id) + return self._make_port_dict(port, fields) + + def _get_ports_query(self, context, filters=None, *args, **kwargs): + Port = models_v2.Port + IPAllocation = models_v2.IPAllocation + + filters = filters or {} + fixed_ips = filters.pop('fixed_ips', {}) + query = self._get_collection_query(context, Port, filters=filters, + *args, **kwargs) + ip_addresses = fixed_ips.get('ip_address') + subnet_ids = fixed_ips.get('subnet_id') + if ip_addresses: + query = query.filter( + Port.fixed_ips.any(IPAllocation.ip_address.in_(ip_addresses))) + if subnet_ids: + query = query.filter( + Port.fixed_ips.any(IPAllocation.subnet_id.in_(subnet_ids))) + return query + + @db_api.retry_if_session_inactive() + def get_ports(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + marker_obj = self._get_marker_obj(context, 'port', limit, marker) + query = self._get_ports_query(context, filters=filters, + sorts=sorts, limit=limit, + marker_obj=marker_obj, + page_reverse=page_reverse) + items = [self._make_port_dict(c, fields) for c in query] + if limit and page_reverse: + items.reverse() + return items + + @db_api.retry_if_session_inactive() + def get_ports_count(self, context, filters=None): + return self._get_ports_query(context, filters).count() + + def _enforce_device_owner_not_router_intf_or_device_id(self, context, + device_owner, + device_id, + tenant_id): + """Prevent tenants from replacing the device id of router ports with + a router uuid belonging to another tenant. + """ + if device_owner not in constants.ROUTER_INTERFACE_OWNERS: + return + if not context.is_admin: + # check to make sure device_id does not match another tenants + # router. + if device_id: + if hasattr(self, 'get_router'): + try: + ctx_admin = context.elevated() + router = self.get_router(ctx_admin, device_id) + except l3.RouterNotFound: + return + else: + l3plugin = directory.get_plugin(constants.L3) + if l3plugin: + try: + ctx_admin = context.elevated() + router = l3plugin.get_router(ctx_admin, + device_id) + except l3.RouterNotFound: + return + else: + # raise as extension doesn't support L3 anyways. + raise n_exc.DeviceIDNotOwnedByTenant( + device_id=device_id) + if tenant_id != router['tenant_id']: + raise n_exc.DeviceIDNotOwnedByTenant(device_id=device_id) + + db_base_plugin_common.DbBasePluginCommon.register_model_query_hook( + models_v2.Port, + "port", + None, + '_port_filter_hook', + None) diff --git a/neutron_controller_ovs/run.sh b/neutron_controller_ovs/run.sh new file mode 100755 index 0000000..9c282ca --- /dev/null +++ b/neutron_controller_ovs/run.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# start neutron services + +/etc/write_conf.sh + +/usr/bin/neutron-server \ +--config-file /etc/neutron/neutron.conf \ +--config-file /etc/neutron/plugins/ml2/ml2_conf.ini \ +--config-file /etc/neutron/plugins/ml2/linuxbridge_agent.ini \ +--config-file /etc/neutron/metadata_agent.ini \ +--config-dir /etc/neutron/ \ +-v -d & + +sleep 3 +/usr/bin/neutron-linuxbridge-agent \ +--config-file /etc/neutron/neutron.conf \ +--config-file /etc/neutron/plugins/ml2/ml2_conf.ini \ +--config-file /etc/neutron/plugins/ml2/linuxbridge_agent.ini \ +--config-file /etc/neutron/metadata_agent.ini \ +--config-dir /etc/neutron/ \ +-v -d & + +sleep 3 +neutron-metadata-agent \ +--config-file /etc/neutron/neutron.conf \ +--config-file /etc/neutron/plugins/ml2/ml2_conf.ini \ +--config-file /etc/neutron/plugins/ml2/linuxbridge_agent.ini \ +--config-file /etc/neutron/metadata_agent.ini \ +--config-dir /etc/neutron/ \ +-v -d & + +sleep 3 +neutron-l3-agent \ +--config-file /etc/neutron/l3_agent.ini \ +--config-file /etc/neutron/neutron.conf \ +--config-file /etc/neutron/metadata_agent.ini \ +--config-dir /etc/neutron/ \ +-v -d & +# If any process fails, kill the rest. +# This ensures the container stops and systemd will restart it. + +wait -n +pkill -P $$ diff --git a/neutron_controller_ovs/write_conf.sh b/neutron_controller_ovs/write_conf.sh new file mode 100755 index 0000000..153ada7 --- /dev/null +++ b/neutron_controller_ovs/write_conf.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# +# Generate config files from environments values. +# These are to be passed to the docker container using -e + +cat << EOF > /etc/neutron/neutron.conf + +[DEFAULT] +core_plugin = ml2 +service_plugins = router +allow_overlapping_ips = True +transport_url = rabbit://$RABBIT_USER:$RABBIT_PASSWORD@$MY_IP +auth_strategy = keystone +notify_nova_on_port_status_changes = true +notify_nova_on_port_data_changes = true +dhcp_agents_per_network = 2 + +[agent] +root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf + +[database] +connection = mysql+pymysql://$NEUTRON_USER:$NEUTRON_PASSWORD@mariadb/neutron + +[keystone_authtoken] +auth_uri = http://$KEYSTONE_HOST:5000 +auth_url = http://$KEYSTONE_HOST:35357 +memcached_servers = $MEMCACHED_HOST:11211 +auth_type = password +project_domain_name = Default +user_domain_name = Default +project_name = service +username = $NEUTRON_USER +password = $NEUTRON_PASSWORD + +[nova] +auth_url = http://$KEYSTONE_HOST:35357 +auth_type = password +project_domain_name = Default +user_domain_name = Default +region_name = RegionOne +project_name = service +username = $NOVA_USER +password = $NOVA_PASSWORD + +EOF + +cat << EOF > /etc/neutron/plugins/ml2/ml2_conf.ini + +[ml2] +type_drivers = flat,vlan,vxlan +tenant_network_types = vxlan +mechanism_drivers = linuxbridge,l2population +extension_drivers = port_security + +[ml2_type_vlan] +network_vlan_ranges = provider + +[ml2_type_flat] +flat_networks = provider + +[ml2_type_vxlan] +vni_ranges = 1:1000 + +[securitygroup] +enable_ipset = true + +EOF + +cat << EOF > /etc/neutron/metadata_agent.ini + +[DEFAULT] +nova_metadata_ip = $MY_IP +metadata_proxy_shared_secret = $METADATA_SECRET + +EOF + + +cat << EOF > /etc/neutron/plugins/ml2/linuxbridge_agent.ini + +[linux_bridge] +physical_interface_mappings = $PHYSICAL_INTERFACE_MAPPINGS + +[vxlan] +enable_vxlan = True +l2_population = True +local_ip = $OVERLAY_IP + +[securitygroup] +enable_security_group = true +firewall_driver = iptables + +EOF + +cat << EOF > /etc/neutron/l3_agent.ini + +[DEFAULT] +interface_driver = linuxbridge +external_network_bridge = + +EOF diff --git a/nova_compute/write_conf.sh b/nova_compute/write_conf.sh index 96ed86d..715480b 100755 --- a/nova_compute/write_conf.sh +++ b/nova_compute/write_conf.sh @@ -124,9 +124,9 @@ allow_resize_to_same_host = True [libvirt] virt_type = kvm images_type = rbd -images_rbd_pool = volumes +images_rbd_pool = compute images_rbd_ceph_conf = /etc/ceph/ceph.conf -rbd_user = volumes +rbd_user = compute rbd_secret_uuid = d0db6ba7-a0c9-4da6-b0bc-aa7846325333 [vnc]