From 7d0cae73d393a254ce4cbd94f15aa604f904937f Mon Sep 17 00:00:00 2001 From: Alexander Tivelkov Date: Fri, 1 Nov 2013 18:45:43 +0400 Subject: [PATCH] Advanced Networking Implemented Change-Id: Ibebc5de57cdbba10ebeb0cbc600e2fade96d96b4 --- doc/source/index.rst | 26 +++- etc/conductor.conf | 18 +++ muranoconductor/app.py | 1 + muranoconductor/commands/dispatcher.py | 10 +- muranoconductor/commands/network.py | 196 +++++++++++++++++++++++++ muranoconductor/config.py | 10 ++ muranoconductor/network.py | 95 ++++++++++++ muranoconductor/xml_code_engine.py | 6 + requirements.txt | 1 + test-requirements.txt | 1 - 10 files changed, 358 insertions(+), 6 deletions(-) create mode 100644 muranoconductor/commands/network.py create mode 100644 muranoconductor/network.py diff --git a/doc/source/index.rst b/doc/source/index.rst index 6cd8e02..002e142 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -59,7 +59,23 @@ Configure # Directory where conductor's data directory located. # "data" must be subdirectory to this. - data_dir = /etc/murano-conductor + data_dir = /etc/murano-conductor/metadata-cache + + # Provide url to Murano Metadata repository + murano_metadata_url = http://localhost:8084 + + # Maximum number of environments that can be processed simultaneously + 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 + + # Enforces flat network topology by default. + # If set to "False", routed topology will be used + flat_by_default = False [keystone] # URL of OpenStack KeyStone service REST API. @@ -89,6 +105,14 @@ Configure # Valid endpoint types: publicURL (default), internalURL, adminURL endpoint_type = publicURL + [neutron] + # Optional CA cert file to use in SSL connections + #ca_cert = + # Allow self signed server certificate + insecure = False + # Valid endpoint types: publicURL (default), internalURL, adminURL + endpoint_type = publicURL + [rabbitmq] # Connection parameters to RabbitMQ service diff --git a/etc/conductor.conf b/etc/conductor.conf index 97de261..30c615b 100644 --- a/etc/conductor.conf +++ b/etc/conductor.conf @@ -23,6 +23,16 @@ murano_metadata_url = http://localhost:8084 # Maximum number of environments that can be processed simultaneously 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 + +# Enforces flat network topology by default. +# If set to "False", routed topology will be used +flat_by_default = False + [keystone] # URL of OpenStack KeyStone service REST API. # Typically only hostname (or IP) needs to be changed @@ -51,6 +61,14 @@ insecure = False # Valid endpoint types: publicURL (default), internalURL, adminURL endpoint_type = publicURL +[neutron] +# Optional CA cert file to use in SSL connections +#ca_cert = +# Allow self signed server certificate +insecure = False +# Valid endpoint types: publicURL (default), internalURL, adminURL +endpoint_type = publicURL + [rabbitmq] # Connection parameters to RabbitMQ service diff --git a/muranoconductor/app.py b/muranoconductor/app.py index 6310606..82d7b0f 100644 --- a/muranoconductor/app.py +++ b/muranoconductor/app.py @@ -30,6 +30,7 @@ from muranocommon.helpers.token_sanitizer import TokenSanitizer from muranoconductor import metadata import vm_agent import cloud_formation +import network log = logging.getLogger(__name__) diff --git a/muranoconductor/commands/dispatcher.py b/muranoconductor/commands/dispatcher.py index bb98520..5f612fb 100644 --- a/muranoconductor/commands/dispatcher.py +++ b/muranoconductor/commands/dispatcher.py @@ -15,17 +15,19 @@ import command import cloud_formation +import network import vm_agent class CommandDispatcher(command.CommandBase): - def __init__(self, environment, rmqclient, token, - tenant_id, reporter): + def __init__(self, environment, rmqclient, token, tenant_id, reporter): self._command_map = { 'cf': cloud_formation.HeatExecutor(environment, token, tenant_id, reporter), - 'agent': vm_agent.VmAgentExecutor(environment, rmqclient, - reporter) + 'agent': vm_agent.VmAgentExecutor( + environment, rmqclient, reporter), + + 'net': network.NeutronExecutor(tenant_id, token) } def execute(self, name, **kwargs): diff --git a/muranoconductor/commands/network.py b/muranoconductor/commands/network.py new file mode 100644 index 0000000..8ad935c --- /dev/null +++ b/muranoconductor/commands/network.py @@ -0,0 +1,196 @@ +# Copyright (c) 2013 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 muranoconductor.config +from keystoneclient.v2_0 import client as ksclient +import netaddr +from netaddr.strategy import ipv4 +from neutronclient.v2_0 import client as client +from muranoconductor.commands.command import CommandBase + + +class NeutronExecutor(CommandBase): + def __init__(self, tenant_id, token): + keystone_settings = muranoconductor.config.CONF.keystone + neutron_settings = muranoconductor.config.CONF.neutron + + self.env_count = muranoconductor.config.CONF.max_environments + self.host_count = muranoconductor.config.CONF.max_hosts + self.address = muranoconductor.config.CONF.env_ip_template + + self.cidr_waiting_per_router = {} + self.router_requests = [] + self.network_requests = [] + + 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=tenant_id, + token=token): + raise client.exceptions.Unauthorized() + + neutron_url = keystone_client.service_catalog.url_for( + service_type='network', + endpoint_type=neutron_settings.endpoint_type) + self.neutron = client.Client(endpoint_url=neutron_url, + token=token, + ca_cert=neutron_settings.ca_cert or None, + insecure=neutron_settings.insecure) + + self.command_map = { + "get_subnet": self._schedule_get_subnet, + "get_router": self._schedule_get_router, + "get_network": self._schedule_get_network + } + + def execute(self, command, callback, **kwargs): + if command in self.command_map: + self.command_map[command](callback, **kwargs) + + def has_pending_commands(self): + return len(self.cidr_waiting_per_router) + len( + self.router_requests) + len(self.network_requests) > 0 + + def execute_pending(self): + r1 = self._execute_pending_cidr_requests() + r2 = self._execute_pending_net_requests() + r3 = self._execute_pending_router_requests() + return r1 or r2 or r3 + + def _execute_pending_cidr_requests(self): + if not len(self.cidr_waiting_per_router): + return False + for router, callbacks in self.cidr_waiting_per_router.items(): + results = self._get_subnet(router, len(callbacks)) + for callback, result in zip(callbacks, results): + callback(result) + self.cidr_waiting_per_router = {} + return True + + def _execute_pending_router_requests(self): + if not len(self.router_requests): + return False + + routers = self.neutron.list_routers().get("routers") + if not len(routers): + routerId = None + else: + routerId = routers[0]["id"] + + if len(routers) > 1: + for router in routers: + if "murano" in router["name"].lower(): + routerId = router["id"] + break + for callback in self.router_requests: + callback(routerId) + self.router_requests = [] + return True + + def _execute_pending_net_requests(self): + if not len(self.network_requests): + return False + + nets = self.neutron.list_networks()["networks"] + if not len(nets): + netId = None + else: + netId = nets[0]["id"] + if len(nets) > 1: + murano_id = None + ext_id = None + shared_id = None + for net in nets: + if "murano" in net.get("name").lower(): + murano_id = net["id"] + break + if net.get("router:external") and not ext_id: + ext_id = net["id"] + if net.get("shared") and not shared_id: + shared_id = net["id"] + if murano_id: + netId = murano_id + elif ext_id: + netId = ext_id + elif shared_id: + netId = shared_id + for callback in self.network_requests: + callback(netId) + self.network_requests = [] + return True + + def _get_subnet(self, routerId, count): + if routerId == "*": + routerId = None + if routerId: + taken_cidrs = self._get_taken_cidrs_by_router(routerId) + else: + taken_cidrs = self._get_all_taken_cidrs() + results = [] + for i in range(0, count): + res = self._generate_cidr(taken_cidrs) + results.append(res) + taken_cidrs.append(res) + return results + + def _get_taken_cidrs_by_router(self, routerId): + ports = self.neutron.list_ports(device_id=routerId)["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 = [subnet["cidr"] for subnet in all_subnets if + subnet["id"] in subnet_ids] + + return filtered_cidrs + + def _get_all_taken_cidrs(self): + return [subnet["cidr"] for subnet in + self.neutron.list_subnets()["subnets"]] + + def _generate_cidr(self, taken_cidrs): + bits_for_envs = int(math.ceil(math.log(self.env_count, 2))) + bits_for_hosts = int(math.ceil(math.log(self.host_count, 2))) + width = ipv4.width + mask_width = width - bits_for_hosts - bits_for_envs + net = netaddr.IPNetwork(self.address + "/" + str(mask_width)) + for subnet in net.subnet(width - bits_for_hosts): + if str(subnet) in taken_cidrs: + continue + return str(subnet) + return None + + def _schedule_get_subnet(self, callback, **kwargs): + routerId = kwargs.get("routerId") + if not routerId: + routerId = "*" + if routerId in self.cidr_waiting_per_router: + self.cidr_waiting_per_router[routerId].append(callback) + else: + self.cidr_waiting_per_router[routerId] = [callback] + + def _schedule_get_router(self, callback, **kwargs): + self.router_requests.append(callback) + + def _schedule_get_network(self, callback, **kwargs): + self.network_requests.append(callback) diff --git a/muranoconductor/config.py b/muranoconductor/config.py index eadaeeb..6b2e86c 100644 --- a/muranoconductor/config.py +++ b/muranoconductor/config.py @@ -58,6 +58,12 @@ heat_opts = [ cfg.StrOpt('endpoint_type', default='publicURL') ] +neutron_opts = [ + cfg.BoolOpt('insecure', default=False), + cfg.StrOpt('ca_cert'), + cfg.StrOpt('endpoint_type', default='publicURL') +] + keystone_opts = [ cfg.StrOpt('auth_url'), cfg.BoolOpt('insecure', default=False), @@ -70,6 +76,7 @@ CONF = cfg.CONF CONF.register_opts(paste_deploy_opts, group='paste_deploy') CONF.register_opts(rabbit_opts, group='rabbitmq') CONF.register_opts(heat_opts, group='heat') +CONF.register_opts(neutron_opts, group='neutron') CONF.register_opts(keystone_opts, group='keystone') CONF.register_opts(directories) CONF.register_opt(cfg.StrOpt('file_server')) @@ -77,6 +84,9 @@ CONF.register_cli_opt(cfg.StrOpt('murano_metadata_url')) CONF.register_opt(cfg.IntOpt('max_environments', default=20)) +CONF.register_opt(cfg.IntOpt('max_hosts', default=250)) +CONF.register_opt(cfg.StrOpt('env_ip_template', default='10.0.0.0')) +CONF.register_opt(cfg.BoolOpt('flat_by_default', default=False)) CONF.import_opt('verbose', 'muranoconductor.openstack.common.log') diff --git a/muranoconductor/network.py b/muranoconductor/network.py new file mode 100644 index 0000000..af3a10c --- /dev/null +++ b/muranoconductor/network.py @@ -0,0 +1,95 @@ +# Copyright (c) 2013 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. +from muranoconductor import xml_code_engine +import muranoconductor.config + +from openstack.common import log as logging + +log = logging.getLogger(__name__) + + +def get_available_subnet(engine, context, body, routerId=None, result=None): + command_dispatcher = context['/commandDispatcher'] + + def callback(result_value): + if result is not None: + context[result] = {"cidr": result_value} + + success_handler = body.find('success') + if success_handler is not None: + engine.evaluate_content(success_handler, context) + + command_dispatcher.execute( + name="net", + command="get_subnet", + routerId=routerId, + callback=callback) + + +def get_default_router(engine, context, body, result=None): + command_dispatcher = context['/commandDispatcher'] + + def callback(result_value): + if result is not None: + context[result] = {"routerId": result_value} + + success_handler = body.find('success') + if success_handler is not None: + engine.evaluate_content(success_handler, context) + + command_dispatcher.execute( + name="net", + command="get_router", + callback=callback) + + +def get_default_network(engine, context, body, result=None): + command_dispatcher = context['/commandDispatcher'] + + def callback(result_value): + if result is not None: + context[result] = {"networkId": result_value} + + success_handler = body.find('success') + if success_handler is not None: + engine.evaluate_content(success_handler, context) + + command_dispatcher.execute( + name="net", + command="get_network", + callback=callback) + + +def get_network_topology(engine, context, body, result=None): + if muranoconductor.config.CONF.flat_by_default: + return "flat" + else: + return "routed" + + +xml_code_engine.XmlCodeEngine.register_function( + get_available_subnet, "get-cidr") + +xml_code_engine.XmlCodeEngine.register_function( + get_default_router, "get-default-router-id") + +xml_code_engine.XmlCodeEngine.register_function( + get_default_network, "get-default-network-id") + +xml_code_engine.XmlCodeEngine.register_function( + get_default_router, "get-default-router-id") + +xml_code_engine.XmlCodeEngine.register_function( + get_network_topology, "get-net-topology") diff --git a/muranoconductor/xml_code_engine.py b/muranoconductor/xml_code_engine.py index 8661ead..61601d6 100644 --- a/muranoconductor/xml_code_engine.py +++ b/muranoconductor/xml_code_engine.py @@ -12,6 +12,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +import uuid import xml.etree.ElementTree as etree import types @@ -127,6 +128,10 @@ def _false_func(**kwargs): return False +def _gen_id(**kwargs): + return uuid.uuid4().hex + + XmlCodeEngine.register_function(_dict_func, "map") XmlCodeEngine.register_function(_array_func, "list") XmlCodeEngine.register_function(_text_func, "text") @@ -135,3 +140,4 @@ XmlCodeEngine.register_function(_function_func, "function") XmlCodeEngine.register_function(_null_func, "null") XmlCodeEngine.register_function(_true_func, "true") XmlCodeEngine.register_function(_false_func, "false") +XmlCodeEngine.register_function(_gen_id, "uuid") diff --git a/requirements.txt b/requirements.txt index 6b28786..59793b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ deep murano-common>=0.2.2 PyYAML>=3.1.0 murano-metadataclient==0.4.a13.gd65dfd2 +python-neutronclient>=2.3.1 diff --git a/test-requirements.txt b/test-requirements.txt index 1026727..7b09f4f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,4 +7,3 @@ nosexcover pep8 sphinx>=1.1.2 mockfs -