From 7492333859774bea1b842e66c26ccfc6e4813f68 Mon Sep 17 00:00:00 2001 From: Timur Sufiev Date: Wed, 4 Dec 2013 18:12:02 +0400 Subject: [PATCH] Use code from AdvNetworking for MS SQL Cluster IP validation. Use code borrowed from murano-conductor for generation of subnets against which both MS SQL Cluster IPs should be validated (they should belong to that subnet). Old validation of static IPs is no longer sufficient because CIDR's are generated by murano-conductor during deployment. Valid subnet, once generated in dashboard for validation purposes, is stored in murano-api environment for later use by murano-conductor (DRY). So this commit comes in a team with 2 following commits: https://review.openstack.org/#/c/59982/ and https://review.openstack.org/#/c/59983/ Change-Id: I02c1b1905dd15535f807bcbc49b4f8fc2ad435d7 --- muranodashboard/environments/api.py | 5 +- .../environments/services/fields.py | 84 ++++++++++--- .../environments/services/network.py | 116 ++++++++++++++++++ muranodashboard/environments/views.py | 5 +- muranodashboard/settings.py | 9 ++ setup-centos.sh | 2 + setup.sh | 2 + 7 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 muranodashboard/environments/services/network.py diff --git a/muranodashboard/environments/api.py b/muranodashboard/environments/api.py index 8651dfe01..2f50206e2 100644 --- a/muranodashboard/environments/api.py +++ b/muranodashboard/environments/api.py @@ -207,8 +207,9 @@ def environment_deploy(request, environment_id): return env -def environment_update(request, environment_id, name): - return muranoclient(request).environments.update(environment_id, name) +def environment_update(request, environment_id, name, **kwargs): + return muranoclient(request).environments.update( + environment_id, name, **kwargs) def get_environment_name(request, environment_id): diff --git a/muranodashboard/environments/services/fields.py b/muranodashboard/environments/services/fields.py index 16aa692c6..13e64de30 100644 --- a/muranodashboard/environments/services/fields.py +++ b/muranodashboard/environments/services/fields.py @@ -16,9 +16,10 @@ import re import json from django import forms from django.core.validators import RegexValidator, validate_ipv4_address -from netaddr import all_matching_cidrs +from netaddr import all_matching_cidrs, IPNetwork, IPAddress from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text +from django.conf import settings from muranodashboard.environments import api from horizon import exceptions, messages from openstack_dashboard.api import glance @@ -32,6 +33,8 @@ import horizon.tables as tables import floppyforms from django.template.loader import render_to_string +from .network import NeutronSubnetGetter + log = logging.getLogger(__name__) @@ -53,6 +56,10 @@ def with_request(func): """ def update(self, initial, request=None, **kwargs): initial_request = initial.get('request') + for key, value in initial.iteritems(): + if key != 'request' and key not in kwargs: + kwargs[key] = value + if initial_request: log.debug("Using 'request' value from initial dictionary") func(self, initial_request, **kwargs) @@ -548,8 +555,12 @@ class BooleanField(forms.BooleanField, CustomPropertiesField): class ClusterIPField(CharField): + existing_subnet = None + network_topology = None + router_id = None + @staticmethod - def validate_cluster_ip(request, ip_ranges): + def make_nova_validator(request, ip_ranges): def perform_checking(ip): validate_ipv4_address(ip) if not all_matching_cidrs(ip, ip_ranges) and ip_ranges: @@ -576,19 +587,64 @@ class ClusterIPField(CharField): _('Specified Cluster Static IP is already in use')) return perform_checking + def update_network_params(self, request, environment_id): + env = api.environment_get(request, environment_id) + net_info = getattr(env, 'networking', {}) + self.network_topology = getattr(settings, 'NETWORK_TOPOLOGY', 'routed') + + if net_info: + self.existing_subnet = net_info.get('cidr') + topology = net_info.get('topology') + if topology: + self.network_topology = topology + self.router_id = net_info.get('routerId') + + if self.network_topology != 'nova' and not self.existing_subnet: + getter = NeutronSubnetGetter(request.user.tenant_id, + self.router_id, + request.user.token.id) + self.existing_subnet = getter.get_subnet(environment_id) + if self.existing_subnet: + networking = { + 'createNetwork': True, + 'cidr': self.existing_subnet + } + name = api.get_environment_name(request, environment_id) + api.environment_update(request, environment_id, name, + networking=networking) + else: + raise RuntimeError('Cannot get subnet') + + def make_neutron_validator(self): + def perform_checking(ip): + validate_ipv4_address(ip) + if not IPAddress(ip) in IPNetwork(self.existing_subnet): + raise forms.ValidationError( + _('Specified IP address should belong to {0} ' + 'subnet'.format(self.existing_subnet))) + + return perform_checking + @with_request - def update(self, request, **kwargs): - try: - network_list = novaclient(request).networks.list() - ip_ranges = [network.cidr for network in network_list] - ranges = ', '.join(ip_ranges) - except StandardError: - ip_ranges, ranges = [], '' - if ip_ranges: - self.help_text = _('Select IP from available range: ' + ranges) - else: - self.help_text = _('Specify valid fixed IP') - self.validators = [self.validate_cluster_ip(request, ip_ranges)] + def update(self, request, environment_id, **kwargs): + self.update_network_params(request, environment_id) + + if self.network_topology == 'nova': + try: + network_list = novaclient(request).networks.list() + ip_ranges = [network.cidr for network in network_list] + ranges = ', '.join(ip_ranges) + except StandardError: + ip_ranges, ranges = [], '' + if ip_ranges: + self.help_text = _('Select IP from available range: ' + ranges) + else: + self.help_text = _('Specify valid fixed IP') + self.validators = [self.make_nova_validator(request, ip_ranges)] + elif self.network_topology == 'routed': + self.validators = [self.make_neutron_validator()] + else: # 'flat' topology + raise NotImplementedError('Flat topology is not implemented yet') self.error_messages['invalid'] = validate_ipv4_address.message diff --git a/muranodashboard/environments/services/network.py b/muranodashboard/environments/services/network.py new file mode 100644 index 000000000..68f7223c1 --- /dev/null +++ b/muranodashboard/environments/services/network.py @@ -0,0 +1,116 @@ +# 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 +from django.conf import settings +from keystoneclient.v2_0 import client as ksclient +import netaddr +from netaddr.strategy import ipv4 +from neutronclient.v2_0 import client as neutronclient + + +class NeutronSubnetGetter(object): + def __init__(self, tenant_id, router_id, token): + conf = settings.ADVANCED_NETWORKING_CONFIG + self.env_count = conf.get('max_environments') + self.host_count = conf.get('max_hosts') + self.address = conf.get('env_ip_template') + + self.tenant_id = tenant_id + self.router_id = router_id + + cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) + insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + endpoint_type = getattr( + settings, 'OPENSTACK_ENDPOINT_TYPE', 'publicURL') + + keystone_client = ksclient.Client( + auth_url=settings.OPENSTACK_KEYSTONE_URL, + tenant_id=tenant_id, + token=token, + cacert=cacert, + insecure=insecure) + + if not keystone_client.authenticate(): + raise ksclient.exceptions.Unauthorized() + + neutron_url = keystone_client.service_catalog.url_for( + service_type='network', endpoint_type=endpoint_type) + self.neutron = neutronclient.Client(endpoint_url=neutron_url, + token=token, + ca_cert=cacert, + insecure=insecure) + + def _get_router_id(self): + routers = self.neutron.list_routers(tenant_id=self.tenant_id).\ + get("routers") + if not len(routers): + router_id = None + else: + router_id = routers[0]["id"] + + if len(routers) > 1: + for router in routers: + if "murano" in router["name"].lower(): + router_id = router["id"] + break + + return router_id + + def _get_subnet(self, router_id=None, count=1): + if router_id: + taken_cidrs = self._get_taken_cidrs_by_router(router_id) + 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, router_id): + 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 = [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 get_subnet(self, environment_id): + # TODO: should use it for getting cidr in future + assert environment_id + router_id = self.router_id or self._get_router_id() + return self._get_subnet(router_id)[0] diff --git a/muranodashboard/environments/views.py b/muranodashboard/environments/views.py index 2af527163..4059a2d97 100644 --- a/muranodashboard/environments/views.py +++ b/muranodashboard/environments/views.py @@ -132,7 +132,10 @@ class Wizard(ModalFormMixin, LazyWizard): def get_form_initial(self, step): init_dict = {} if step != 'service_choice': - init_dict['request'] = self.request + init_dict.update({ + 'request': self.request, + 'environment_id': self.kwargs.get('environment_id') + }) return self.initial_dict.get(step, init_dict) def get_context_data(self, form, **kwargs): diff --git a/muranodashboard/settings.py b/muranodashboard/settings.py index e54c3e0f7..5fd892d76 100644 --- a/muranodashboard/settings.py +++ b/muranodashboard/settings.py @@ -158,3 +158,12 @@ except ImportError: if DEBUG: logging.basicConfig(level=logging.DEBUG) + +ADVANCED_NETWORKING_CONFIG = { + # Maximum number of environments that can be processed simultaneously + 'max_environments': 100, + # 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/setup-centos.sh b/setup-centos.sh index d4645bcba..9414044d4 100644 --- a/setup-centos.sh +++ b/setup-centos.sh @@ -119,6 +119,8 @@ LOGGING['loggers']['muranoclient'] = {'handlers': ['murano-file'], 'level': 'ERR #MURANO_METADATA_URL = "http://localhost:8084/v1" #if murano-api set up with ssl uncomment next strings #MURANO_API_INSECURE = True +ADVANCED_NETWORKING_CONFIG = {'max_environments': 100, 'max_hosts': 250, 'env_ip_template': '10.0.0.0'} +NETWORK_TOPOLOGY = 'routed' #END_MURANO_DASHBOARD EOF if [ $? -ne 0 ];then diff --git a/setup.sh b/setup.sh index 51719bb79..eb33f2d9f 100644 --- a/setup.sh +++ b/setup.sh @@ -99,6 +99,8 @@ LOGGING['loggers']['muranoclient'] = {'handlers': ['murano-file'], 'level': 'ERR #MURANO_METADATA_URL = "http://localhost:8084/v1" #if murano-api set up with ssl uncomment next strings #MURANO_API_INSECURE = True +ADVANCED_NETWORKING_CONFIG = {'max_environments': 100, 'max_hosts': 250, 'env_ip_template': '10.0.0.0'} +NETWORK_TOPOLOGY = 'routed' #END_MURANO_DASHBOARD EOF if [ $? -ne 0 ];then