From 961818d505d68942fa55922e7ef6353b9db8b506 Mon Sep 17 00:00:00 2001 From: Alexander Tivelkov Date: Thu, 24 Apr 2014 20:36:45 +0400 Subject: [PATCH] Added NetworkExplorer engine object class Added a NetworkExplorer class (maps to io.murano.system.NetworkExplorer) to explore the Network Environment of an active tenant. The class is able to locate the default router (if present) and allocate available CIDR range for the selected router. The latter requires some configuration options (for proper CIDR segmentation) This commit adds python-neutronclient to the requirements, as NetworkExplorer has to directly interact with Neutron This classes is a crucial part of AdvancedNetworking implementation Partial-Bug: #1308921 Change-Id: Ib9daa1b1521d9bc17a53d7e131be6c9f43faa013 --- etc/murano/murano-api.conf.sample | 10 ++ muranoapi/common/config.py | 7 ++ muranoapi/engine/system/net_explorer.py | 128 ++++++++++++++++++++++ muranoapi/engine/system/system_objects.py | 2 + requirements.txt | 1 + 5 files changed, 148 insertions(+) create mode 100644 muranoapi/engine/system/net_explorer.py diff --git a/etc/murano/murano-api.conf.sample b/etc/murano/murano-api.conf.sample index eb81cd216..7589801b2 100644 --- a/etc/murano/murano-api.conf.sample +++ b/etc/murano/murano-api.conf.sample @@ -159,3 +159,13 @@ url = http://localhost:8082 #key_file = # If set then the server's certificate will not be verified insecure = False + +[networking] +# Maximum number of environments that use a single router per tenant +max_environments = 20 + +# Maximum number of VMs per environment +max_hosts = 250 + +# Template IP address for generating environment subnet cidrs +env_ip_template = 10.0.0.0 diff --git a/muranoapi/common/config.py b/muranoapi/common/config.py index 4e12870bf..2af1d9bd2 100644 --- a/muranoapi/common/config.py +++ b/muranoapi/common/config.py @@ -87,6 +87,12 @@ murano_opts = [ cfg.StrOpt('endpoint_type', default='publicURL') ] +networking_opts = [ + cfg.IntOpt('max_environments', default=20), + cfg.IntOpt('max_hosts', default=250), + cfg.StrOpt('env_ip_template', default='10.0.0.0'), +] + stats_opt = [ cfg.IntOpt('period', default=5, help=_('Statistics collection interval in minutes.' @@ -115,6 +121,7 @@ CONF.register_cli_opt(metadata_dir) CONF.register_cli_opt(packages_cache) CONF.register_cli_opt(package_size_limit) CONF.register_opts(stats_opt, group='stats') +CONF.register_opts(networking_opts, group='networking') CONF.import_opt('connection', 'muranoapi.openstack.common.db.options', diff --git a/muranoapi/engine/system/net_explorer.py b/muranoapi/engine/system/net_explorer.py new file mode 100644 index 000000000..d2fa778d2 --- /dev/null +++ b/muranoapi/engine/system/net_explorer.py @@ -0,0 +1,128 @@ +# Copyright (c) 2014 Mirantis Inc. +# +# 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 math + +import keystoneclient.apiclient.exceptions as ks_exc +import keystoneclient.v2_0.client as ksclient +import netaddr +from netaddr.strategy import ipv4 +import neutronclient.v2_0.client as nclient + +import muranoapi.common.config as config +import muranoapi.dsl.helpers as helpers +import muranoapi.dsl.murano_class as murano_class +import muranoapi.dsl.murano_object as murano_object + + +@murano_class.classname('io.murano.system.NetworkExplorer') +class NetworkExplorer(murano_object.MuranoObject): + # noinspection PyAttributeOutsideInit + def initialize(self, _context): + environment = helpers.get_environment(_context) + self._tenant_id = environment.tenant_id + keystone_settings = config.CONF.keystone + neutron_settings = config.CONF.neutron + self._settings = config.CONF.networking + + keystone_client = ksclient.Client( + endpoint=keystone_settings.auth_url, + cacert=keystone_settings.ca_file or None, + cert=keystone_settings.cert_file or None, + key=keystone_settings.key_file or None, + insecure=keystone_settings.insecure) + + if not keystone_client.authenticate( + auth_url=keystone_settings.auth_url, + tenant_id=environment.tenant_id, + token=environment.token): + raise ks_exc.AuthorizationFailure() + + neutron_url = keystone_client.service_catalog.url_for( + service_type='network', + endpoint_type=neutron_settings.endpoint_type) + + self._neutron = \ + nclient.Client(endpoint_url=neutron_url, + token=environment.token, + ca_cert=neutron_settings.ca_cert or None, + insecure=neutron_settings.insecure) + + self._available_cidrs = self._generate_possible_cidrs() + + # noinspection PyPep8Naming + def getDefaultRouter(self): + routers = self._neutron.list_routers(tenant_id=self._tenant_id).\ + get("routers") + if len(routers) == 0: + return "NOT_FOUND" + else: + router_id = routers[0]["id"] + + if len(routers) > 1: + for router in routers: + if "murano" in router["name"].lower(): + return router["id"] + + return router_id + + # noinspection PyPep8Naming + def getAvailableCidr(self, routerId, netId): + """ + Uses hash of network IDs to minimize the collisions: + different nets will attempt to pick different cidrs out of available + range. + If the cidr is taken will pick another one + """ + taken_cidrs = self._get_cidrs_taken_by_router(routerId) + id_hash = hash(netId) + num_fails = 0 + while num_fails < len(self._available_cidrs): + cidr = self._available_cidrs[ + (id_hash + num_fails) % len(self._available_cidrs)] + if any(self._cidrs_overlap(cidr, taken_cidr) for taken_cidr in + taken_cidrs): + num_fails += 1 + else: + return str(cidr) + return None + + def _get_cidrs_taken_by_router(self, router_id): + if not router_id: + return [] + ports = self._neutron.list_ports(device_id=router_id)["ports"] + subnet_ids = [] + for port in ports: + for fixed_ip in port["fixed_ips"]: + subnet_ids.append(fixed_ip["subnet_id"]) + + all_subnets = self._neutron.list_subnets()["subnets"] + filtered_cidrs = [netaddr.IPNetwork(subnet["cidr"]) for subnet in + all_subnets if subnet["id"] in subnet_ids] + + return filtered_cidrs + + @staticmethod + def _cidrs_overlap(cidr1, cidr2): + return (cidr1 in cidr2) or (cidr2 in cidr1) + + def _generate_possible_cidrs(self): + bits_for_envs = int( + math.ceil(math.log(self._settings.max_environments, 2))) + bits_for_hosts = int(math.ceil(math.log(self._settings.max_hosts, 2))) + width = ipv4.width + mask_width = width - bits_for_hosts - bits_for_envs + net = netaddr.IPNetwork( + "{0}/{1}".format(self._settings.env_ip_template, mask_width)) + return list(net.subnet(width - bits_for_hosts)) diff --git a/muranoapi/engine/system/system_objects.py b/muranoapi/engine/system/system_objects.py index 36283cb24..80ab51ff8 100644 --- a/muranoapi/engine/system/system_objects.py +++ b/muranoapi/engine/system/system_objects.py @@ -20,6 +20,7 @@ from muranoapi.engine.system import agent from muranoapi.engine.system import agent_listener from muranoapi.engine.system import heat_stack from muranoapi.engine.system import instance_reporter +from muranoapi.engine.system import net_explorer from muranoapi.engine.system import resource_manager from muranoapi.engine.system import status_reporter @@ -50,3 +51,4 @@ def register(class_loader, package_loader): class_loader.import_class(ResourceManagerWrapper) class_loader.import_class(instance_reporter.InstanceReportNotifier) class_loader.import_class(status_reporter.StatusReporter) + class_loader.import_class(net_explorer.NetworkExplorer) diff --git a/requirements.txt b/requirements.txt index f7b5b2fa5..6e3705f16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,7 @@ passlib jsonschema>=2.0.0,<3.0.0 python-keystoneclient>=0.7.0 python-heatclient>=0.2.3 +python-neutronclient>=2.3.4,<3 oslo.config>=1.2.0 oslo.messaging>=1.3.0a9