diff --git a/configutilities/configutilities/configutilities/__init__.py b/configutilities/configutilities/configutilities/__init__.py index bd7610e820..0717792c47 100755 --- a/configutilities/configutilities/configutilities/__init__.py +++ b/configutilities/configutilities/configutilities/__init__.py @@ -29,4 +29,5 @@ from configutilities.common.utils import validate_address # noqa: F401 from configutilities.common.utils import ip_version_to_string # noqa: F401 from configutilities.common.utils import lag_mode_to_str # noqa: F401 from configutilities.common.utils import validate_openstack_password # noqa: F401 +from configutilities.common.utils import validate_nameserver_address_str # noqa: F401 from configutilities.common.utils import extract_openstack_password_rules_from_file # noqa: F401 diff --git a/configutilities/configutilities/configutilities/common/validator.py b/configutilities/configutilities/configutilities/common/validator.py index 2bba43351e..3045459bf0 100644 --- a/configutilities/configutilities/configutilities/common/validator.py +++ b/configutilities/configutilities/configutilities/common/validator.py @@ -23,6 +23,7 @@ from configutilities.common.utils import is_mtu_valid from configutilities.common.utils import get_service from configutilities.common.utils import get_optional from configutilities.common.utils import validate_address_str +from configutilities.common.utils import validate_nameserver_address_str from configutilities.common.exceptions import ConfigFail from configutilities.common.exceptions import ValidateFail @@ -944,8 +945,25 @@ class ConfigValidator(object): raise ConfigFail("SDN Configuration is no longer supported") def validate_dns(self): - if self.conf.has_section('DNS'): - raise ConfigFail("DNS Configuration is no longer supported") + # DNS config is optional + if not self.conf.has_section('DNS'): + return + if self.cgcs_conf is not None: + self.cgcs_conf.add_section('cDNS') + for x in range(0, 3): + if self.conf.has_option('DNS', 'NAMESERVER_' + str(x + 1)): + dns_address_str = self.conf.get('DNS', 'NAMESERVER_' + str( + x + 1)) + try: + dns_address = validate_nameserver_address_str( + dns_address_str) + if self.cgcs_conf is not None: + self.cgcs_conf.set('cDNS', 'NAMESERVER_' + str(x + 1), + str(dns_address)) + except ValidateFail as e: + raise ConfigFail( + "Invalid DNS NAMESERVER value of %s.\nReason: %s" % + (dns_address_str, e)) def validate_ntp(self): if self.conf.has_section('NTP'): diff --git a/controllerconfig/controllerconfig/controllerconfig/configassistant.py b/controllerconfig/controllerconfig/controllerconfig/configassistant.py index 1a224ba90c..b1c1b5db71 100644 --- a/controllerconfig/controllerconfig/controllerconfig/configassistant.py +++ b/controllerconfig/controllerconfig/controllerconfig/configassistant.py @@ -28,6 +28,7 @@ from configutilities import validate_network_str from configutilities import validate_address_str from configutilities import validate_address from configutilities import ip_version_to_string +from configutilities import validate_nameserver_address_str from configutilities import validate_openstack_password from configutilities import DEFAULT_DOMAIN_NAME from netaddr import IPNetwork @@ -463,6 +464,10 @@ class ConfigAssistant(): # SDN config self.enable_sdn = False + + # DNS config + self.nameserver_addresses = ["8.8.8.8", "8.8.4.4", ""] + # HTTPS self.enable_https = False # Network config @@ -2666,6 +2671,64 @@ class ConfigAssistant(): """ Cluster host interface configuration complete""" self.cluster_host_interface_configured = True + def get_dns_servers(self): + """Produce a comma separated list of DNS servers.""" + servers = [str(s) for s in self.nameserver_addresses if s] + return ",".join(servers) + + def input_dns_config(self): + """Allow user to input DNS config and perform validation.""" + + print("\nDomain Name System (DNS):") + print("-------------------------\n") + print(textwrap.fill( + "Configuring DNS servers accessible through the external " + "OAM network allows domain names to be mapped to IP " + "addresses.", 80)) + print(textwrap.fill( + "The configuration of at least one DNS server is mandatory. To " + "skip the configuration of one or more nameservers (1 to 3 are " + "allowed), enter C to continue to the next configuration item.", + 80)) + print('') + + if self.external_oam_subnet.version == 6: + self.nameserver_addresses = ["2001:4860:4860::8888", + "2001:4860:4860::8844", ""] + + for server in range(0, len(self.nameserver_addresses)): + while True: + user_input = raw_input( + "Nameserver " + str(server + 1) + " [" + + str(self.nameserver_addresses[server]) + "]: ") + if user_input.lower() == 'q': + raise UserQuit + elif user_input.lower() == 'c': + if server == 0: + print("At least one DNS server is required.") + continue + for x in range(server, len(self.nameserver_addresses)): + self.nameserver_addresses[x] = "" + return + elif user_input == "": + user_input = self.nameserver_addresses[server] + # Pressing enter with a blank default will continue + if user_input == "": + return + + try: + try: + ip_input = validate_nameserver_address_str( + user_input, self.external_oam_subnet.version) + except ValidateFail as e: + print('{}'.format(e)) + continue + self.nameserver_addresses[server] = ip_input + break + except (AddrFormatError, ValueError): + print("Invalid address - please enter a valid IPv4 " + "address") + def input_authentication_config(self): """Allow user to input authentication config and perform validation. """ @@ -2711,6 +2774,8 @@ class ConfigAssistant(): self.management_interface_configured = True self.external_oam_interface_configured = True self.default_pxeboot_config() + if not self.kubernetes: + self.nameserver_addresses = ["", "", ""] if utils.is_cpe(): self.system_mode = sysinv_constants.SYSTEM_MODE_DUPLEX @@ -2747,6 +2812,8 @@ class ConfigAssistant(): if self.kubernetes: self.input_cluster_host_config() self.input_external_oam_config() + if self.kubernetes: + self.input_dns_config() self.input_authentication_config() def is_valid_management_multicast_subnet(self, ip_subnet): @@ -3079,6 +3146,19 @@ class ConfigAssistant(): self.external_oam_interface_configured = True + # DNS configuration + if self.kubernetes: + if config.has_section('cDNS'): + self.nameserver_addresses = ["", "", ""] + for x in range(0, len(self.nameserver_addresses)): + if config.has_option('cDNS', + 'NAMESERVER_' + str(x + 1)): + cvalue = config.get('cDNS', + 'NAMESERVER_' + str(x + 1)) + if cvalue != "NC" and cvalue != "": + self.nameserver_addresses[x] = \ + IPAddress(cvalue) + # SDN Network configuration if config.has_option('cSDN', 'ENABLE_SDN'): raise ConfigFail("The option ENABLE_SDN is no longer " @@ -3511,6 +3591,18 @@ class ConfigAssistant(): else: print("External OAM address: " + str(self.external_oam_address_0)) + if self.kubernetes: + print("\nDNS Configuration") + print("-----------------") + dns_config = False + for x in range(0, len(self.nameserver_addresses)): + if self.nameserver_addresses[x]: + print("Nameserver " + str(x + 1) + ": " + + str(self.nameserver_addresses[x])) + dns_config = True + if not dns_config: + print("External DNS servers not configured") + if self.region_config: print("\nRegion Configuration") print("--------------------") @@ -3796,6 +3888,17 @@ class ConfigAssistant(): f.write("EXTERNAL_OAM_1_ADDRESS=" + str(self.external_oam_address_1) + "\n") + if self.kubernetes: + # DNS configuration + f.write("\n[cDNS]") + f.write("\n# DNS Configuration\n") + for x in range(0, len(self.nameserver_addresses)): + if self.nameserver_addresses[x]: + f.write("NAMESERVER_" + str(x + 1) + "=" + + str(self.nameserver_addresses[x]) + "\n") + else: + f.write("NAMESERVER_" + str(x + 1) + "=NC" + "\n") + # Network configuration f.write("\n[cNETWORK]") f.write("\n# Data Network Configuration\n") @@ -5188,6 +5291,17 @@ class ConfigAssistant(): "required_patches": "N/A"} client.sysinv.load.create(**patch) + def _populate_dns_config(self, client): + # Retrieve the list of dns servers to get the uuid + dns_list = client.sysinv.idns.list() + dns_record = dns_list[0] + values = { + 'nameservers': self.get_dns_servers(), + 'action': 'apply' + } + patch = sysinv.dict_to_patch(values) + client.sysinv.idns.update(dns_record.uuid, patch) + def populate_initial_config(self): """Populate initial system inventory configuration""" try: @@ -5195,6 +5309,8 @@ class ConfigAssistant(): self._populate_system_config(client) self._populate_load_config(client) self._populate_network_config(client) + if self.kubernetes: + self._populate_dns_config(client) controller = self._populate_controller_config(client) # ceph_mon config requires controller host to be created self._inventory_config_complete_wait(client, controller) diff --git a/controllerconfig/controllerconfig/controllerconfig/tests/files/cgcs_config.kubernetes b/controllerconfig/controllerconfig/controllerconfig/tests/files/cgcs_config.kubernetes new file mode 100755 index 0000000000..24cb70edd6 --- /dev/null +++ b/controllerconfig/controllerconfig/controllerconfig/tests/files/cgcs_config.kubernetes @@ -0,0 +1,86 @@ +[cSYSTEM] +# System Configuration +SYSTEM_MODE=duplex +TIMEZONE=UTC + +[cPXEBOOT] +# PXEBoot Network Support Configuration +PXECONTROLLER_FLOATING_HOSTNAME=pxecontroller + +[cMGMT] +# Management Network Configuration +MANAGEMENT_INTERFACE_NAME=eth1 +MANAGEMENT_INTERFACE=eth1 +MANAGEMENT_MTU=1500 +MANAGEMENT_SUBNET=192.168.204.0/24 +LAG_MANAGEMENT_INTERFACE=no +CONTROLLER_FLOATING_ADDRESS=192.168.204.2 +CONTROLLER_0_ADDRESS=192.168.204.3 +CONTROLLER_1_ADDRESS=192.168.204.4 +NFS_MANAGEMENT_ADDRESS_1=192.168.204.5 +NFS_MANAGEMENT_ADDRESS_2=192.168.204.6 +CONTROLLER_FLOATING_HOSTNAME=controller +CONTROLLER_HOSTNAME_PREFIX=controller- +OAMCONTROLLER_FLOATING_HOSTNAME=oamcontroller +DYNAMIC_ADDRESS_ALLOCATION=yes +MANAGEMENT_MULTICAST_SUBNET=239.1.1.0/28 + +[cINFRA] +# Infrastructure Network Configuration +INFRASTRUCTURE_INTERFACE_NAME=NC +INFRASTRUCTURE_INTERFACE=NC +INFRASTRUCTURE_VLAN=NC +INFRASTRUCTURE_MTU=NC +INFRASTRUCTURE_SUBNET=NC +LAG_INFRASTRUCTURE_INTERFACE=no +INFRASTRUCTURE_BOND_MEMBER_0=NC +INFRASTRUCTURE_BOND_MEMBER_1=NC +INFRASTRUCTURE_BOND_POLICY=NC +CONTROLLER_0_INFRASTRUCTURE_ADDRESS=NC +CONTROLLER_1_INFRASTRUCTURE_ADDRESS=NC +NFS_INFRASTRUCTURE_ADDRESS_1=NC +STORAGE_0_INFRASTRUCTURE_ADDRESS=NC +STORAGE_1_INFRASTRUCTURE_ADDRESS=NC +CONTROLLER_INFRASTRUCTURE_HOSTNAME_SUFFIX=NC +INFRASTRUCTURE_START_ADDRESS=NC +INFRASTRUCTURE_END_ADDRESS=NC + +[cCLUSTER] +# Cluster Host Network Configuration +CLUSTER_INTERFACE_NAME=eth1 +CLUSTER_INTERFACE=eth1 +CLUSTER_VLAN=NC +CLUSTER_MTU=1500 +CLUSTER_SUBNET=192.168.206.0/24 +LAG_CLUSTER_INTERFACE=no + +[cEXT_OAM] +# External OAM Network Configuration +EXTERNAL_OAM_INTERFACE_NAME=eth0 +EXTERNAL_OAM_INTERFACE=eth0 +EXTERNAL_OAM_VLAN=NC +EXTERNAL_OAM_MTU=1500 +LAG_EXTERNAL_OAM_INTERFACE=no +EXTERNAL_OAM_SUBNET=10.10.10.0/24 +EXTERNAL_OAM_GATEWAY_ADDRESS=10.10.10.1 +EXTERNAL_OAM_FLOATING_ADDRESS=10.10.10.2 +EXTERNAL_OAM_0_ADDRESS=10.10.10.3 +EXTERNAL_OAM_1_ADDRESS=10.10.10.4 + +[cDNS] +# DNS Configuration +NAMESERVER_1=1.2.3.4 +NAMESERVER_2=5.6.7.8 +NAMESERVER_3=NC + +[cNETWORK] +# Data Network Configuration +VSWITCH_TYPE=ovs-dpdk + +[cSECURITY] +[cREGION] +# Region Configuration +REGION_CONFIG=False + +[cAUTHENTICATION] +ADMIN_PASSWORD=Li69nux* diff --git a/controllerconfig/controllerconfig/controllerconfig/tests/files/system_config.cluster b/controllerconfig/controllerconfig/controllerconfig/tests/files/system_config.kubernetes similarity index 94% rename from controllerconfig/controllerconfig/controllerconfig/tests/files/system_config.cluster rename to controllerconfig/controllerconfig/controllerconfig/tests/files/system_config.kubernetes index 6117b7d4b4..d14364b7ca 100755 --- a/controllerconfig/controllerconfig/controllerconfig/tests/files/system_config.cluster +++ b/controllerconfig/controllerconfig/controllerconfig/tests/files/system_config.kubernetes @@ -44,6 +44,11 @@ CIDR=10.10.10.0/24 GATEWAY=10.10.10.1 LOGICAL_INTERFACE=LOGICAL_INTERFACE_2 +[DNS] +# DNS Configuration +NAMESERVER_1=1.2.3.4 +NAMESERVER_2=5.6.7.8 + ;[PXEBOOT_NETWORK] ;PXEBOOT_CIDR=192.168.203.0/24 diff --git a/controllerconfig/controllerconfig/controllerconfig/tests/test_answerfile.py b/controllerconfig/controllerconfig/controllerconfig/tests/test_answerfile.py index e0562d50e7..d87c735caa 100644 --- a/controllerconfig/controllerconfig/controllerconfig/tests/test_answerfile.py +++ b/controllerconfig/controllerconfig/controllerconfig/tests/test_answerfile.py @@ -19,13 +19,14 @@ import controllerconfig.common.constants as constants def _test_answerfile(tmpdir, filename, mock_get_net_device_list, mock_get_rootfs_node, - compare_results=True): + compare_results=True, + ca_options={}): """ Test import and generation of answerfile """ mock_get_net_device_list.return_value = \ ['eth0', 'eth1', 'eth2'] mock_get_rootfs_node.return_value = '/dev/sda' - assistant = ca.ConfigAssistant() + assistant = ca.ConfigAssistant(**ca_options) # Create the path to the answerfile answerfile = os.path.join( @@ -93,3 +94,10 @@ def test_answerfile_region_nuage_vrs(tmpdir): """ Test import of answerfile with region values for nuage_vrs""" _test_answerfile(tmpdir, "cgcs_config.region_nuage_vrs") + + +def test_answerfile_kubernetes(tmpdir): + """ Test import of answerfile with kubernetes values """ + + _test_answerfile(tmpdir, "cgcs_config.kubernetes", + ca_options={"kubernetes": True}) diff --git a/controllerconfig/controllerconfig/controllerconfig/tests/test_system_config.py b/controllerconfig/controllerconfig/controllerconfig/tests/test_system_config.py index eda2692230..22f2224ddf 100644 --- a/controllerconfig/controllerconfig/controllerconfig/tests/test_system_config.py +++ b/controllerconfig/controllerconfig/controllerconfig/tests/test_system_config.py @@ -42,7 +42,7 @@ def _test_system_config(filename): cr.create_cgcs_config_file(None, system_config, None, None, None, 0, validate_only=True) - # Validate the region config file. + # Validate the system config file. # Using onboard validation since the validator's reference version number # is only set at build-time when validating offboard validate(system_config, DEFAULT_CONFIG, None, False) @@ -508,14 +508,6 @@ def test_system_config_validation(): with pytest.raises(exceptions.ConfigFail): validate(system_config, DEFAULT_CONFIG, None, False) - # Test detection of unsupported DNS NAMESERVER - system_config = cr.parse_system_config(simple_systemfile) - system_config.add_section('DNS') - system_config.set('DNS', 'NAMESERVER_1', '8.8.8.8') - with pytest.raises(exceptions.ConfigFail): - cr.create_cgcs_config_file(None, system_config, None, None, None, 0, - validate_only=True) - # Test detection of unsupported NTP NTP_SERVER system_config = cr.parse_system_config(simple_systemfile) system_config.add_section('NTP') @@ -606,12 +598,13 @@ def test_pxeboot_range(): validate(system_config, DEFAULT_CONFIG, None, False) -def test_cluster_network(): - """ Test import of system_config file for cluster network address """ +def test_kubernetes(): + """ Test import of system_config file for kubernetes """ # Create the path to the system_config file systemfile = os.path.join( - os.getcwd(), "controllerconfig/tests/files/", "system_config.cluster") + os.getcwd(), "controllerconfig/tests/files/", + "system_config.kubernetes") # Test import and generation of answer file _test_system_config(systemfile) @@ -647,3 +640,10 @@ def test_cluster_network(): validate_only=True) with pytest.raises(exceptions.ConfigFail): validate(system_config, DEFAULT_CONFIG, None, False) + + # Test absence of optional DNS configuration + system_config = cr.parse_system_config(systemfile) + system_config.remove_section('DNS') + cr.create_cgcs_config_file(None, system_config, None, None, None, 0, + validate_only=True) + validate(system_config, DEFAULT_CONFIG, None, False) diff --git a/devstack/lib/stx-config b/devstack/lib/stx-config index bc5a0851e0..489c374997 100644 --- a/devstack/lib/stx-config +++ b/devstack/lib/stx-config @@ -176,7 +176,6 @@ function install_sysinv { sudo install -d -m 755 $SYSINV_CONF_DIR sudo install -p -D -m 755 $SYSINV_DIR/etc/sysinv/policy.json $SYSINV_CONF_DIR/policy.json sudo install -p -D -m 640 $SYSINV_DIR/etc/sysinv/profileSchema.xsd $SYSINV_CONF_DIR/profileSchema.xsd - sudo install -p -D -m 655 $SYSINV_DIR/etc/sysinv/crushmap.bin $SYSINV_CONF_DIR/crushmap.bin sudo install -d -m 755 $SYSINV_ETC_MOTDD sudo install -p -D -m 755 $SYSINV_DIR/etc/sysinv/motd-system $SYSINV_ETC_MOTDD/10-system sudo install -d -m 755 $SYSINV_CONF_DIR/upgrades diff --git a/puppet-manifests/src/modules/platform/manifests/dns.pp b/puppet-manifests/src/modules/platform/manifests/dns.pp index 786c89a7c8..5ea7a9a4e3 100644 --- a/puppet-manifests/src/modules/platform/manifests/dns.pp +++ b/puppet-manifests/src/modules/platform/manifests/dns.pp @@ -93,8 +93,14 @@ class platform::dns::resolv ( class platform::dns { - include ::platform::dns::resolv - include ::platform::dns::dnsmasq + Anchor['platform::networking'] -> Class[$name] + + # The "contain" ensures that the resolv and dnsmasq classes are not applied + # until the dns class is begun, which will wait for networking to be + # complete, as per the anchor dependency above. This is necessary because + # the networking configuration can wipe the /etc/resolv.conf file. + contain ::platform::dns::resolv + contain ::platform::dns::dnsmasq } diff --git a/puppet-manifests/src/modules/platform/manifests/kubernetes.pp b/puppet-manifests/src/modules/platform/manifests/kubernetes.pp index b6b6a74b15..f30dbd5da6 100644 --- a/puppet-manifests/src/modules/platform/manifests/kubernetes.pp +++ b/puppet-manifests/src/modules/platform/manifests/kubernetes.pp @@ -58,15 +58,8 @@ class platform::kubernetes::master::init # For initial controller install, configure kubernetes from scratch. $resolv_conf = '/etc/resolv.conf' - # Add a DNS server to allow access to kubernetes repo. This will no longer - # be required once we are using our own internal repo. - file_line { "${resolv_conf} nameserver 8.8.8.8": - path => $resolv_conf, - line => 'nameserver 8.8.8.8', - } - # Configure the master node. - -> file { '/etc/kubernetes/kubeadm.yaml': + file { '/etc/kubernetes/kubeadm.yaml': ensure => file, content => template('platform/kubeadm.yaml.erb'), } diff --git a/sysinv/cgts-client/centos/build_srpm.data b/sysinv/cgts-client/centos/build_srpm.data index f4cf7d398b..c2971c945f 100644 --- a/sysinv/cgts-client/centos/build_srpm.data +++ b/sysinv/cgts-client/centos/build_srpm.data @@ -1,2 +1,2 @@ SRC_DIR="cgts-client" -TIS_PATCH_VER=61 +TIS_PATCH_VER=62 diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py index e337974531..e702ace96c 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py @@ -25,6 +25,7 @@ from cgtsclient.v1 import ceph_mon from cgtsclient.v1 import certificate from cgtsclient.v1 import cluster from cgtsclient.v1 import controller_fs +from cgtsclient.v1 import datanetwork from cgtsclient.v1 import drbdconfig from cgtsclient.v1 import ethernetport from cgtsclient.v1 import fernet @@ -42,6 +43,7 @@ from cgtsclient.v1 import iinterface from cgtsclient.v1 import ilvg from cgtsclient.v1 import imemory from cgtsclient.v1 import inode +from cgtsclient.v1 import interface_datanetwork from cgtsclient.v1 import interface_network from cgtsclient.v1 import intp from cgtsclient.v1 import iprofile @@ -132,6 +134,9 @@ class Client(http.HTTPClient): self.load = load.LoadManager(self) self.upgrade = upgrade.UpgradeManager(self) self.network = network.NetworkManager(self) + self.datanetwork = datanetwork.DataNetworkManager(self) + self.interface_datanetwork = \ + interface_datanetwork.InterfaceDataNetworkManager(self) self.interface_network = interface_network.InterfaceNetworkManager(self) self.service_parameter = service_parameter.ServiceParameterManager(self) self.cluster = cluster.ClusterManager(self) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/datanetwork.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/datanetwork.py new file mode 100644 index 0000000000..840b53a49a --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/datanetwork.py @@ -0,0 +1,78 @@ +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# -*- encoding: utf-8 -*- +# + +from cgtsclient.common import base +from cgtsclient.common import utils +from cgtsclient import exc + + +CREATION_ATTRIBUTES = [ + 'network_type', 'name', 'description', 'mtu', + 'multicast_group', 'port_num', 'ttl', 'mode'] + + +class DataNetwork(base.Resource): + def __repr__(self): + return "" % self._info + + +class DataNetworkManager(base.Manager): + resource_class = DataNetwork + + def list(self): + path = '/v1/datanetworks' + return self._list(path, "datanetworks") + + def get(self, datanetwork_id): + path = '/v1/datanetworks/%s' % datanetwork_id + try: + return self._list(path)[0] + except IndexError: + return None + + def create(self, **kwargs): + path = '/v1/datanetworks' + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exc.InvalidAttribute('%s' % key) + return self._create(path, new) + + def update(self, datanetwork_id, patch): + path = '/v1/datanetworks/%s' % datanetwork_id + return self._update(path, patch) + + def delete(self, datanetwork_id): + path = '/v1/datanetworks/%s' % datanetwork_id + return self._delete(path) + + +def _find_datanetwork(cc, datanetwork): + if datanetwork.isdigit() and not utils.is_uuid_like(datanetwork): + datanetwork_list = cc.datanetwork.list() + for n in datanetwork_list: + if str(n.id) == datanetwork: + return n + else: + raise exc.CommandError('datanetwork not found: %s' % datanetwork) + elif utils.is_uuid_like(datanetwork): + try: + h = cc.datanetwork.get(datanetwork) + except exc.HTTPNotFound: + raise exc.CommandError('datanetwork not found: %s' % datanetwork) + else: + return h + else: + datanetwork_list = cc.datanetwork.list() + for n in datanetwork_list: + if n.name == datanetwork: + return n + else: + raise exc.CommandError('datanetwork not found: %s' % datanetwork) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/datanetwork_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/datanetwork_shell.py new file mode 100644 index 0000000000..834a5eafd5 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/datanetwork_shell.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# All Rights Reserved. +# + +from cgtsclient.common import utils +from cgtsclient import exc + +DATANETWORK_TYPE_VXLAN = "vxlan" + + +def _print_datanetwork_show(obj): + fields = ['id', 'uuid', 'name', 'network_type', 'mtu', + 'description'] + + if obj.network_type == DATANETWORK_TYPE_VXLAN: + fields.append('multicast_group') + fields.append('port_num') + fields.append('ttl') + fields.append('mode') + + data = [(f, getattr(obj, f, '')) for f in fields] + utils.print_tuple_list(data) + + +@utils.arg('datanetwork_id', + metavar='', + help="UUID or name of datanetwork") +def do_datanetwork_show(cc, args): + """Show datanetwork details.""" + + datanetwork = cc.datanetwork.get(args.datanetwork_id) + _print_datanetwork_show(datanetwork) + + +def do_datanetwork_list(cc, args): + """List datanetworks.""" + + labels = ['uuid', 'name', 'network_type', 'mtu'] + fields = ['uuid', 'name', 'network_type', 'mtu'] + datanetworks = cc.datanetwork.list() + utils.print_list(datanetworks, fields, labels, sortby=1) + + +@utils.arg('name', + metavar='', + help="Name of the datanetwork [REQUIRED]") +@utils.arg('network_type', + metavar='', + choices=['flat', 'vlan', 'vxlan'], + help="Type of the datanetwork [REQUIRED]") +@utils.arg('-d', '--description', + metavar='', + help='User description of the datanetwork') +@utils.arg('-m', '--mtu', + metavar='', + default=1500, + help='MTU of the datanetwork') +@utils.arg('-p', '--port_num', + metavar='', + help='port_num of the datanetwork') +@utils.arg('-g', '--multicast_group', + metavar='', + help='multicast_group of the datanetwork') +@utils.arg('-t', '--ttl', + metavar='', + help='time-to-live of the datanetwork') +@utils.arg('-M', '--mode', + metavar='', + choices=['dynamic', 'static'], + default='dynamic', + help='mode of the datanetwork') +def do_datanetwork_add(cc, args): + """Add a datanetwork.""" + + field_list = ['name', 'network_type', 'mtu', 'description', + 'multicast_group', 'port_num', 'ttl', 'mode'] + + # Prune input fields down to required/expected values + data = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + + datanetwork = cc.datanetwork.create(**data) + uuid = getattr(datanetwork, 'uuid', '') + try: + datanetwork = cc.datanetwork.get(uuid) + except exc.HTTPNotFound: + raise exc.CommandError('Created DataNetwork UUID not found: %s' % uuid) + _print_datanetwork_show(datanetwork) + + +@utils.arg('datanetwork_id', + metavar='', + help="Name of the datanetwork [REQUIRED]") +@utils.arg('-m', '--mtu', + metavar='', + help='MTU of the datanetwork') +@utils.arg('-d', '--description', + metavar='', + help='User description of the datanetwork') +def do_datanetwork_modify(cc, args): + """Modify a datanetwork.""" + + rwfields = ['mtu', 'description'] + + user_specified_fields = dict((k, v) for (k, v) in vars(args).items() + if k in rwfields and not (v is None)) + + patch = [] + for (k, v) in user_specified_fields.items(): + patch.append({'op': 'replace', 'path': '/' + k, 'value': v}) + + datanetwork = cc.datanetwork.update(args.datanetwork_id, patch) + _print_datanetwork_show(datanetwork) + + +@utils.arg('datanetwork_uuid', + metavar='', + help="UUID of datanetwork entry") +def do_datanetwork_delete(cc, args): + """Delete a datanetwork.""" + + cc.datanetwork.delete(args.datanetwork_uuid) + print('Deleted DataNetwork: %s' % args.datanetwork_uuid) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface.py index 54f31950c1..9206a2a5d4 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface.py @@ -14,7 +14,7 @@ from cgtsclient.v1 import port CREATION_ATTRIBUTES = ['ifname', 'iftype', 'ihost_uuid', 'imtu', 'ifclass', 'networks', 'network_uuid', 'networktype', 'aemode', 'txhashpolicy', - 'providernetworks', 'providernetworksdict', 'ifcapabilities', 'ports', 'imac', + 'providernetworks', 'datanetworks', 'ifcapabilities', 'ports', 'imac', 'vlan_id', 'uses', 'used_by', 'ipv4_mode', 'ipv6_mode', 'ipv4_pool', 'ipv6_pool', 'sriov_numvfs'] diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface_shell.py index 7e243aed75..0b8dd6f8ee 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iinterface_shell.py @@ -17,7 +17,7 @@ from cgtsclient.v1 import network as network_utils def _print_iinterface_show(cc, iinterface): - fields = ['ifname', 'iftype', 'ports', 'providernetworks', + fields = ['ifname', 'iftype', 'ports', 'datanetworks', 'imac', 'imtu', 'ifclass', 'networks', 'aemode', 'schedpolicy', 'txhashpolicy', 'uuid', 'ihost_uuid', @@ -95,9 +95,12 @@ def do_host_if_list(cc, args): attr_str = "%s,accelerated=True" % attr_str setattr(i, 'attrs', attr_str) - field_labels = ['uuid', 'name', 'class', 'type', 'vlan id', 'ports', 'uses i/f', 'used by i/f', 'attributes', 'provider networks'] - fields = ['uuid', 'ifname', 'ifclass', 'iftype', 'vlan_id', 'ports', 'uses', 'used_by', 'attrs', 'providernetworks'] - utils.print_list(iinterfaces, fields, field_labels, sortby=0, no_wrap_fields=['ports']) + field_labels = ['uuid', 'name', 'class', 'type', 'vlan id', 'ports', + 'uses i/f', 'used by i/f', 'attributes', 'data networks'] + fields = ['uuid', 'ifname', 'ifclass', 'iftype', 'vlan_id', 'ports', + 'uses', 'used_by', 'attrs', 'datanetworks'] + utils.print_list( + iinterfaces, fields, field_labels, sortby=0, no_wrap_fields=['ports']) @utils.arg('hostnameorid', @@ -125,11 +128,11 @@ def do_host_if_delete(cc, args): choices=['ae', 'vlan'], nargs='?', help="Type of the interface") -@utils.arg('providernetworks', - metavar='', +@utils.arg('datanetworks', + metavar='', nargs='?', default=None, - help=('The provider network attached to the interface ' + help=('The data network attached to the interface ' '(default: %(default)s) ' '[REQUIRED when interface class is data or pci-passthrough')) @utils.arg('-a', '--aemode', @@ -175,7 +178,7 @@ def do_host_if_add(cc, args): """Add an interface.""" field_list = ['ifname', 'iftype', 'imtu', 'ifclass', 'networks', 'aemode', - 'txhashpolicy', 'providernetworks', 'vlan_id', + 'txhashpolicy', 'datanetworks', 'vlan_id', 'ipv4_mode', 'ipv6_mode', 'ipv4_pool', 'ipv6_pool'] ihost = ihost_utils._find_ihost(cc, args.hostnameorid) @@ -194,10 +197,10 @@ def do_host_if_add(cc, args): user_specified_fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) - if 'providernetworks' in user_specified_fields.keys(): - user_specified_fields['providernetworks'] = user_specified_fields['providernetworks'].replace(" ", "") - if 'none' in user_specified_fields['providernetworks']: - del user_specified_fields['providernetworks'] + if 'datanetworks' in user_specified_fields.keys(): + user_specified_fields['datanetworks'] = \ + user_specified_fields['datanetworks'].split(',') + if 'networks' in user_specified_fields.keys(): network = network_utils._find_network(cc, args.networks) user_specified_fields['networks'] = [str(network.id)] @@ -230,7 +233,10 @@ def do_host_if_add(cc, args): help='The MTU of the interface') @utils.arg('-p', '--providernetworks', metavar='', - help='The provider network attached to the interface [REQUIRED]') + help='[DEPRECATED] The provider network attached to the interface') +@utils.arg('-d', '--datanetworks', + metavar='', + help='The data network attached to the interface') @utils.arg('-a', '--aemode', metavar='', choices=['balanced', 'active_standby', '802.3ad'], @@ -267,7 +273,7 @@ def do_host_if_modify(cc, args): """Modify interface attributes.""" rwfields = ['iftype', 'ifname', 'imtu', 'aemode', 'txhashpolicy', - 'providernetworks', 'ports', 'ifclass', 'networks', + 'datanetworks', 'providernetworks', 'ports', 'ifclass', 'networks', 'ipv4_mode', 'ipv6_mode', 'ipv4_pool', 'ipv6_pool', 'sriov_numvfs'] @@ -277,7 +283,13 @@ def do_host_if_modify(cc, args): if k in rwfields and not (v is None)) if 'providernetworks' in user_specified_fields.keys(): - user_specified_fields['providernetworks'] = user_specified_fields['providernetworks'].replace(" ", "") + user_specified_fields['datanetworks'] = \ + user_specified_fields['providernetworks'] + del user_specified_fields['providernetworks'] + + elif 'datanetworks' in user_specified_fields.keys(): + user_specified_fields['datanetworks'] = \ + user_specified_fields['datanetworks'] interface = _find_interface(cc, ihost, args.ifnameoruuid) fields = interface.__dict__ @@ -297,7 +309,7 @@ def do_host_if_modify(cc, args): user_specified_fields['ifname'] = p break if interface.ifclass == 'data': - user_specified_fields['providernetworks'] = 'none' + user_specified_fields['datanetworks'] = 'none' patch = [] for (k, v) in user_specified_fields.items(): diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/interface_datanetwork.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/interface_datanetwork.py new file mode 100755 index 0000000000..36f6fc9302 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/interface_datanetwork.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# -*- encoding: utf-8 -*- +# + +from cgtsclient.common import base +from cgtsclient import exc + + +CREATION_ATTRIBUTES = [ + 'interface_uuid', 'datanetwork_uuid' +] + + +class InterfaceDataNetwork(base.Resource): + def __repr__(self): + return "" % self._info + + +class InterfaceDataNetworkManager(base.Manager): + resource_class = InterfaceDataNetwork + + def list(self): + path = '/v1/interface_datanetworks' + return self._list(path, "interface_datanetworks") + + def list_by_host(self, host_uuid): + path = '/v1/ihosts/%s/interface_datanetworks' % host_uuid + return self._list(path, "interface_datanetworks") + + def list_by_interface(self, interface_uuid): + path = '/v1/iinterfaces/%s/interface_datanetworks' % interface_uuid + return self._list(path, "interface_datanetworks") + + def get(self, interface_datanetwork_uuid): + path = '/v1/interface_datanetworks/%s' % interface_datanetwork_uuid + try: + return self._list(path)[0] + except IndexError: + return None + + def assign(self, **kwargs): + path = '/v1/interface_datanetworks' + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exc.InvalidAttribute('%s' % key) + return self._create(path, new) + + def remove(self, interface_datanetwork_uuid): + path = '/v1/interface_datanetworks/%s' % interface_datanetwork_uuid + return self._delete(path) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/interface_datanetwork_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/interface_datanetwork_shell.py new file mode 100755 index 0000000000..7189dca4f0 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/interface_datanetwork_shell.py @@ -0,0 +1,102 @@ +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# -*- encoding: utf-8 -*- +# + +from cgtsclient.common import utils +from cgtsclient import exc +from cgtsclient.v1 import datanetwork as datanetwork_utils +from cgtsclient.v1 import ihost as ihost_utils +from cgtsclient.v1 import iinterface as iinterface_utils + + +def _print_interface_datanetwork_show(cc, obj): + fields = ['hostname', 'uuid', 'ifname', 'datanetwork_name'] + # Add a hostname column using the forihostid field + host_id = str(getattr(obj, 'forihostid', '')) + ihost = ihost_utils._find_ihost(cc, host_id) + setattr(obj, 'hostname', ihost.hostname) + data = [(f, getattr(obj, f, '')) for f in fields] + utils.print_tuple_list(data) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host") +@utils.arg('ifnameoruuid', + metavar='', + nargs='?', + help="Name or UUID of interface") +def do_interface_datanetwork_list(cc, args): + """List datanetwork interfaces.""" + fields = ['hostname', 'uuid', 'ifname', 'datanetwork_name'] + ihost = ihost_utils._find_ihost(cc, args.hostnameorid) + if args.ifnameoruuid is None: + interface_datanetworks = \ + cc.interface_datanetwork.list_by_host(ihost.uuid) + else: + interface = \ + iinterface_utils._find_interface(cc, ihost, args.ifnameoruuid) + interface_datanetworks = \ + cc.interface_datanetwork.list_by_interface(interface.uuid) + # Add a hostname column using the forihostid field + for i in interface_datanetworks[:]: + host_id = str(getattr(i, 'forihostid', '')) + ihost = ihost_utils._find_ihost(cc, host_id) + setattr(i, 'hostname', ihost.hostname) + utils.print_list(interface_datanetworks, fields, fields, sortby=1) + + +@utils.arg('interface_datanetwork_uuid', + metavar='', + help="UUID of interface datanetwork entry") +def do_interface_datanetwork_show(cc, args): + """Show interface datanetwork details.""" + interface_datanetwork = \ + cc.interface_datanetwork.get(args.interface_datanetwork_uuid) + _print_interface_datanetwork_show(cc, interface_datanetwork) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host [REQUIRED]") +@utils.arg('ifnameoruuid', + metavar='', + help="Name or UUID of interface [REQUIRED]") +@utils.arg('datanetnameoruuid', + metavar='', + help="Name of UUID of datanetwork [REQUIRED]") +def do_interface_datanetwork_assign(cc, args): + """Assign a datanetwork to an interface.""" + # Determine host, interface, and datanetwork using the given arguments + ihost = ihost_utils._find_ihost(cc, args.hostnameorid) + interface = \ + iinterface_utils._find_interface(cc, ihost, args.ifnameoruuid) + datanetwork = \ + datanetwork_utils._find_datanetwork(cc, args.datanetnameoruuid) + + data = dict() + data['interface_uuid'] = interface.uuid + data['datanetwork_uuid'] = datanetwork.uuid + + interface_datanetwork = cc.interface_datanetwork.assign(**data) + uuid = getattr(interface_datanetwork, 'uuid', '') + try: + interface_datanetwork = cc.interface_datanetwork.get(uuid) + except exc.HTTPNotFound: + raise exc.CommandError('Created Interface DataNetwork ' + 'UUID not found: %s' % uuid) + _print_interface_datanetwork_show(cc, interface_datanetwork) + + +@utils.arg('interface_datanetwork_uuid', + metavar='', + help="UUID of interface datanetwork entry") +def do_interface_datanetwork_remove(cc, args): + """Remove an assigned datanetwork from an interface.""" + cc.interface_datanetwork.remove(args.interface_datanetwork_uuid) + print('Deleted Interface DataNetwork: %s' % args.interface_datanetwork_uuid) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iprofile_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iprofile_shell.py index e812b7e7e8..5544bb5bee 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iprofile_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iprofile_shell.py @@ -63,7 +63,7 @@ def get_interfaceconfig(iprofile): for interface in iprofile.interfaces: istr = istr + "%s: %s" % (interface.ifname, interface.networktype) if interface.networktype == 'data': - istr = istr + "( %s )" % interface.providernetworks + istr = istr + "( %s )" % interface.datanetworks _get_interface_ports_interfaces(iprofile, interface) if interface.ports: istr = istr + " | %s | PORTS = %s" % (interface.iftype, interface.ports) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/isystem_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/isystem_shell.py index 52003df00a..dbe96e970a 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/isystem_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/isystem_shell.py @@ -60,7 +60,7 @@ def do_show(cc, args): help='The name of the system') @utils.arg('-s', '--sdn_enabled', metavar='', - choices=['true', 'false'], + choices=['true', 'false', 'True', 'False'], help='The SDN enabled or disabled flag') @utils.arg('-t', '--timezone', metavar='', @@ -79,7 +79,7 @@ def do_show(cc, args): help='The location of the system') @utils.arg('-p', '--https_enabled', metavar='', - choices=['true', 'false'], + choices=['true', 'false', 'True', 'False'], help='The HTTPS enabled or disabled flag') @utils.arg('-v', '--vswitch_type', metavar='', diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/network_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/network_shell.py index 1750257069..3487fd8713 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/network_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/network_shell.py @@ -47,7 +47,7 @@ def do_network_list(cc, args): help="Type of network [REQUIRED]") @utils.arg('dynamic', metavar='', - choices=['true', 'false'], + choices=['true', 'false', 'True', 'False'], help="dynamic [REQUIRED]") @utils.arg('pool_uuid', metavar='', @@ -57,6 +57,10 @@ def do_network_add(cc, args): field_list = ['name', 'type', 'dynamic', 'pool_uuid'] + # make sure dynamic is lower + if args.dynamic is not None: + args.dynamic = args.dynamic.lower() + # Prune input fields down to required/expected values data = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py index f742f3d7c4..2521930b59 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py @@ -13,6 +13,7 @@ from cgtsclient.v1 import ceph_mon_shell from cgtsclient.v1 import certificate_shell from cgtsclient.v1 import cluster_shell from cgtsclient.v1 import controller_fs_shell +from cgtsclient.v1 import datanetwork_shell from cgtsclient.v1 import drbdconfig_shell from cgtsclient.v1 import ethernetport_shell from cgtsclient.v1 import firewallrules_shell @@ -29,6 +30,7 @@ from cgtsclient.v1 import iinfra_shell from cgtsclient.v1 import iinterface_shell from cgtsclient.v1 import ilvg_shell from cgtsclient.v1 import imemory_shell +from cgtsclient.v1 import interface_datanetwork_shell from cgtsclient.v1 import interface_network_shell from cgtsclient.v1 import intp_shell from cgtsclient.v1 import iprofile_shell @@ -100,6 +102,8 @@ COMMAND_MODULES = [ upgrade_shell, network_shell, interface_network_shell, + datanetwork_shell, + interface_datanetwork_shell, service_parameter_shell, cluster_shell, lldp_agent_shell, diff --git a/sysinv/sysinv/centos/build_srpm.data b/sysinv/sysinv/centos/build_srpm.data index c17ad79259..fb9c4ef5cd 100644 --- a/sysinv/sysinv/centos/build_srpm.data +++ b/sysinv/sysinv/centos/build_srpm.data @@ -1,2 +1,2 @@ SRC_DIR="sysinv" -TIS_PATCH_VER=295 +TIS_PATCH_VER=296 diff --git a/sysinv/sysinv/sysinv/etc/sysinv/profileSchema.xsd b/sysinv/sysinv/sysinv/etc/sysinv/profileSchema.xsd index cbf7eb07a0..25c5ce351e 100644 --- a/sysinv/sysinv/sysinv/etc/sysinv/profileSchema.xsd +++ b/sysinv/sysinv/sysinv/etc/sysinv/profileSchema.xsd @@ -133,7 +133,7 @@ - + @@ -200,7 +200,7 @@ - + @@ -237,7 +237,7 @@ - + @@ -262,7 +262,7 @@ - + diff --git a/sysinv/sysinv/sysinv/etc/sysinv/sampleProfile.xml b/sysinv/sysinv/sysinv/etc/sysinv/sampleProfile.xml index 25e2d7dcc6..e75382c757 100644 --- a/sysinv/sysinv/sysinv/etc/sysinv/sampleProfile.xml +++ b/sysinv/sysinv/sysinv/etc/sysinv/sampleProfile.xml @@ -213,8 +213,8 @@ be combined. --> - - + + @@ -232,7 +232,7 @@ linkLocal, and static--> - + @@ -267,13 +267,13 @@ - + - + @@ -322,8 +322,8 @@ - - @@ -336,7 +336,7 @@ - + diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py index e1cd7f2b61..70a67b8ad7 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py @@ -29,6 +29,8 @@ from sysinv.api.controllers.v1 import community from sysinv.api.controllers.v1 import controller_fs from sysinv.api.controllers.v1 import cpu from sysinv.api.controllers.v1 import disk +from sysinv.api.controllers.v1 import datanetwork +from sysinv.api.controllers.v1 import interface_datanetwork from sysinv.api.controllers.v1 import dns from sysinv.api.controllers.v1 import drbdconfig from sysinv.api.controllers.v1 import ethernet_port @@ -193,6 +195,12 @@ class V1(base.APIBase): networks = [link.Link] "Links to the network resource" + datanetworks = [link.Link] + "Links to the datanetwork resource" + + interface_datanetworks = [link.Link] + "Links to the interface datanetwork resource" + interface_networks = [link.Link] "Links to the network interface resource" @@ -751,6 +759,21 @@ class V1(base.APIBase): 'apps', '', bookmark=True)] + v1.datanetworks = [link.Link.make_link('self', pecan.request.host_url, + 'datanetworks', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'datanetworks', '', + bookmark=True)] + + v1.interface_datanetworks = [ + link.Link.make_link('self', pecan.request.host_url, + 'interface_datanetworks', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'interface_datanetworks', '', + bookmark=True)] + return v1 @@ -817,6 +840,8 @@ class Controller(rest.RestController): labels = label.LabelController() fernet_repo = fernet_repo.FernetKeyController() apps = kube_app.KubeAppController() + datanetworks = datanetwork.DataNetworkController() + interface_datanetworks = interface_datanetwork.InterfaceDataNetworkController() @wsme_pecan.wsexpose(V1) def get(self): diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/datanetwork.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/datanetwork.py new file mode 100644 index 0000000000..7d225aa174 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/datanetwork.py @@ -0,0 +1,365 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 UnitedStack Inc. +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +import jsonpatch +import pecan +from pecan import rest +import six +import wsme +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from sysinv.api.controllers.v1 import base +from sysinv.api.controllers.v1 import collection +from sysinv.api.controllers.v1 import types +from sysinv.api.controllers.v1 import utils +from sysinv.common import constants +from sysinv.common import exception +from sysinv.common import utils as cutils +from sysinv import objects +from sysinv.openstack.common import log +from sysinv.openstack.common.gettextutils import _ + +LOG = log.getLogger(__name__) + + +ALLOWED_DATANETWORK_TYPES = [ + constants.DATANETWORK_TYPE_FLAT, + constants.DATANETWORK_TYPE_VLAN, + constants.DATANETWORK_TYPE_VXLAN, +] + +VXLAN_DYNAMIC_REQUIRED_PARAMS = ['multicast_group', 'port_num', 'ttl'] +VXLAN_STATIC_REQUIRED_PARAMS = ['port_num', 'ttl'] + + +class DataNetworkPatchType(types.JsonPatchType): + @staticmethod + def mandatory_attrs(): + return [] + + +class DataNetwork(base.APIBase): + """API representation of an datanetwork. + + This class enforces type checking and value constraints, and converts + between the internal object model and the API representation of a + datanetwork. + """ + + id = int + "Unique ID for this datanetwork" + + uuid = types.uuid + "Unique UUID for this datanetwork" + + network_type = wtypes.text + "Represent the datanetwork type for the datanetwork" + + name = wtypes.text + "Unique name for this datanetwork" + + description = wtypes.text + "Represent the user description for the datanetwork" + + mtu = int + "Represent the MTU size (bytes) of the datanetwork" + + multicast_group = wtypes.text + "Multicast group for this datanetwork. VxLan only" + + port_num = int + "Vxlan Port for this datanetwork. VxLan only" + + ttl = int + "Time To Live for this datanetwork. VxLan only" + + mode = wtypes.text + "Mode for this datanetwork. VxLan only" + + def __init__(self, **kwargs): + self.fields = objects.datanetwork.fields.keys() + for k in self.fields: + if not hasattr(self, k): + continue + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @classmethod + def convert_with_links(cls, rpc_datanetwork, expand=True): + datanetwork = DataNetwork(**rpc_datanetwork.as_dict()) + if not expand: + datanetwork.unset_fields_except( + ['id', 'uuid', 'network_type', 'name', + 'description', 'mtu', + 'multicast_group', 'port_num', 'ttl', 'mode']) + + return datanetwork + + def _validate_network_type(self): + if self.network_type not in ALLOWED_DATANETWORK_TYPES: + raise ValueError(_("DataNetwork type %s not supported") % + self.network_type) + + def validate_syntax(self): + """ + Validates the syntax of each field. + """ + self._validate_network_type() + + +class DataNetworkCollection(collection.Collection): + """API representation of a collection of datanetworks.""" + + datanetworks = [DataNetwork] + "A list containing DataNetwork objects" + + def __init__(self, **kwargs): + self._type = 'datanetworks' + + @classmethod + def convert_with_links(cls, rpc_datanetworks, limit, url=None, + expand=False, **kwargs): + collection = DataNetworkCollection() + collection.datanetworks = [DataNetwork.convert_with_links(n, expand) + for n in rpc_datanetworks] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +LOCK_NAME = 'DataNetworkController' + + +class DataNetworkController(rest.RestController): + """REST controller for DataNetworks.""" + + def __init__(self, parent=None, **kwargs): + self._parent = parent + + def _get_datanetwork_collection( + self, marker=None, limit=None, sort_key=None, + sort_dir=None, expand=False, resource_url=None): + + limit = utils.validate_limit(limit) + sort_dir = utils.validate_sort_dir(sort_dir) + marker_obj = None + + if marker: + marker_obj = objects.datanetwork.get_by_uuid( + pecan.request.context, marker) + + datanetworks = pecan.request.dbapi.datanetworks_get_all( + limit=limit, marker=marker_obj, + sort_key=sort_key, sort_dir=sort_dir) + + return DataNetworkCollection.convert_with_links( + datanetworks, limit, url=resource_url, expand=expand, + sort_key=sort_key, sort_dir=sort_dir) + + def _get_one(self, datanetwork_uuid): + rpc_datanetwork = objects.datanetwork.get_by_uuid( + pecan.request.context, datanetwork_uuid) + return DataNetwork.convert_with_links(rpc_datanetwork) + + @staticmethod + def _check_network_type(datanetwork): + if 'network_type' not in datanetwork: + raise wsme.exc.ClientSideError( + _('DataNetwork network_type is required.')) + + network_type = datanetwork['network_type'] + if network_type not in ALLOWED_DATANETWORK_TYPES: + raise ValueError(_("DataNetwork type %s is not supported") % + network_type) + + @staticmethod + def _check_datanetwork_name(datanetwork): + if 'name' not in datanetwork: + raise wsme.exc.ClientSideError( + _('DataNetwork name is required.')) + + name = datanetwork['name'] + if name.lower() == constants.DATANETWORK_TYPE_NONE: + raise ValueError(_("DataNetwork name '%s' is not allowed") % name) + + @staticmethod + def _check_new_datanetwork_mtu_or_set_default(datanetwork): + if 'mtu' not in datanetwork: + datanetwork['mtu'] = constants.DEFAULT_MTU + utils.validate_mtu(datanetwork['mtu']) + + @staticmethod + def _check_datanetwork_vxlan(datanetwork): + if datanetwork['network_type'] != constants.DATANETWORK_TYPE_VXLAN: + return + + mode = datanetwork.get('mode', constants.DATANETWORK_MODE_DYNAMIC) + if mode == constants.DATANETWORK_MODE_STATIC: + required_vxlan_params = VXLAN_STATIC_REQUIRED_PARAMS + else: + required_vxlan_params = VXLAN_DYNAMIC_REQUIRED_PARAMS + + missing = set(required_vxlan_params).difference(datanetwork.keys()) + if missing: + raise wsme.exc.ClientSideError( + _("VxLan parameters '%s' are required for '%s' mode.") % + (list(missing), mode)) + + multicast_group = datanetwork.get('multicast_group') + if mode == constants.DATANETWORK_MODE_STATIC: + if multicast_group: + raise wsme.exc.ClientSideError( + _('VxLan of mode %s does not support multicast_group.') % + mode) + else: + if not cutils.validate_ip_multicast_address(multicast_group): + raise wsme.exc.ClientSideError( + _("multicast group '%s' is not a valid " + "multicast ip address.") % + multicast_group) + + def _check_datanetwork(self, datanetwork): + self._check_network_type(datanetwork) + self._check_datanetwork_name(datanetwork) + self._check_new_datanetwork_mtu_or_set_default(datanetwork) + self._check_datanetwork_vxlan(datanetwork) + + @staticmethod + def _check_update_mtu(rpc_datanetwork): + # Check interfaces using this datanetwork + ifdns = pecan.request.dbapi.interface_datanetwork_get_by_datanetwork( + rpc_datanetwork.uuid) + + for ifdn in ifdns: + interface_obj = pecan.request.dbapi.iinterface_get( + ifdn.interface_uuid) + if interface_obj.imtu < rpc_datanetwork.mtu: + msg = _("The datanetwork MTU '%s' must be smaller than " + "assigned interface MTU '%s'." % + (rpc_datanetwork.mtu, interface_obj.imtu)) + raise wsme.exc.ClientSideError(msg) + + def _create_datanetwork(self, datanetwork): + # Perform syntactic validation + datanetwork.validate_syntax() + + # Perform semantic validation + datanetwork = datanetwork.as_dict() + self._check_datanetwork(datanetwork) + + result = pecan.request.dbapi.datanetwork_create(datanetwork) + + return DataNetwork.convert_with_links(result) + + @wsme_pecan.wsexpose(DataNetworkCollection, + types.uuid, int, wtypes.text, wtypes.text) + def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'): + """Retrieve a list of DataNetworks.""" + + return self._get_datanetwork_collection(marker, limit, + sort_key=sort_key, + sort_dir=sort_dir) + + @wsme_pecan.wsexpose(DataNetwork, wtypes.text) + def get_one(self, datanetwork_id): + """Retrieve a single DataNetwork.""" + + return self._get_one(datanetwork_id) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(DataNetwork, body=DataNetwork) + def post(self, datanetwork): + """Create a new Data Network.""" + + return self._create_datanetwork(datanetwork) + + @cutils.synchronized(LOCK_NAME) + @wsme.validate(six.text_type, [DataNetworkPatchType]) + @wsme_pecan.wsexpose(DataNetwork, six.text_type, + body=[DataNetworkPatchType]) + def patch(self, datanetwork_id, patch): + """Update an existing datanetwork.""" + + rpc_datanetwork = \ + objects.datanetwork.get_by_uuid( + pecan.request.context, datanetwork_id) + + utils.validate_patch(patch) + patch_obj = jsonpatch.JsonPatch(patch) + LOG.info("datanetwork patch_obj=%s" % patch_obj) + + try: + datanetwork = DataNetwork(**jsonpatch.apply_patch( + rpc_datanetwork.as_dict(), patch_obj)) + + except utils.JSONPATCH_EXCEPTIONS as e: + raise exception.PatchError(patch=patch, reason=e) + + LOG.info("rpc_datanetwork=%s datanetwork=%s" % + (rpc_datanetwork.as_dict(), datanetwork)) + + fields = objects.datanetwork.fields + + for field in fields: + if (field in rpc_datanetwork and + rpc_datanetwork[field] != getattr(datanetwork, field)): + rpc_datanetwork[field] = getattr(datanetwork, field) + + delta = rpc_datanetwork.obj_what_changed() + if not delta: + return DataNetwork.convert_with_links(rpc_datanetwork) + + delta_list = list(delta) + + allowed_updates = ['mtu', 'description'] + if not set(delta_list).issubset(allowed_updates): + extra = set(allowed_updates).difference(delta_list) + raise wsme.exc.ClientSideError( + _("DataNetwork '%s' attributes '%s' may not be modified ") % + (rpc_datanetwork.uuid, extra)) + + values = {} + if 'mtu' in delta_list: + self._check_update_mtu(rpc_datanetwork) + values.update({'mtu': rpc_datanetwork.mtu}) + + if 'description' in delta_list: + values.update({'description': rpc_datanetwork.description}) + + rpc_datanetwork.save() + + return DataNetwork.convert_with_links(rpc_datanetwork) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(None, types.uuid, status_code=204) + def delete(self, datanetwork_uuid): + """Delete a Data Network.""" + + # Only allow delete if there are no associated interfaces + ifdns = pecan.request.dbapi.interface_datanetwork_get_by_datanetwork( + datanetwork_uuid) + if ifdns: + raise wsme.exc.ClientSideError( + _("DataNetwork '%s' is still assigned to interfaces. " + "Check interface-datanetwork.") % datanetwork_uuid) + + pecan.request.dbapi.datanetwork_destroy(datanetwork_uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py index 78ada95f2d..3c943e9d42 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py @@ -80,6 +80,7 @@ from sysinv.api.controllers.v1 import state from sysinv.api.controllers.v1 import types from sysinv.api.controllers.v1 import utils from sysinv.api.controllers.v1 import interface_network +from sysinv.api.controllers.v1 import interface_datanetwork from sysinv.api.controllers.v1 import vim_api from sysinv.api.controllers.v1 import patch_api @@ -1081,6 +1082,10 @@ class HostController(rest.RestController): parent="ihosts") "Expose interface_networks as a sub-element of ihosts" + interface_datanetworks = interface_datanetwork.InterfaceDataNetworkController( + parent="ihosts") + "Expose interface_datanetworks as a sub-element of ihosts" + _custom_actions = { 'detail': ['GET'], 'bulk_add': ['POST'], @@ -3179,62 +3184,61 @@ class HostController(rest.RestController): raise wsme.exc.ClientSideError(msg) @staticmethod - def _semantic_check_interface_providernets(ihost, interface): + def _semantic_check_interface_datanets(interface): """ - Perform provider network semantics on a specific interface to ensure - that any provider networks that have special requirements on the - interface has been statisfied. + Perform data network semantics on a specific interface to ensure + that any data networks that have special requirements on the + interface have been satisfied. """ - networktype = [] - if interface.networktype: - networktype = [network.strip() for network in interface.networktype.split(",")] - if constants.NETWORK_TYPE_DATA not in networktype: + + if interface.ifclass != constants.NETWORK_TYPE_DATA: return - # Fetch the list of provider networks from neutron - providernets = pecan.request.rpcapi.iinterface_get_providernets( - pecan.request.context) - # Cleanup the list of provider networks stored on the interface - values = interface.providernetworks.strip() - values = re.sub(',,+', ',', values) - providernet_names = values.split(',') - # Check for VXLAN provider networks that require IP addresses - for providernet_name in providernet_names: - providernet = providernets.get(providernet_name) - if not providernet: - msg = (_("Interface %(ifname)s is associated to provider " - "network %(name)s which does not exist") % - {'ifname': interface.ifname, 'name': providernet_name}) - raise wsme.exc.ClientSideError(msg) - if providernet['type'] != "vxlan": + + ifdatanets = \ + pecan.request.dbapi.interface_datanetwork_get_by_interface( + interface.uuid) + + # Check for VXLAN data networks that require IP addresses + for ifdn in ifdatanets: + if ifdn.datanetwork_network_type != \ + constants.DATANETWORK_TYPE_VXLAN: continue - for r in providernet['ranges']: - if r['vxlan']['group'] is None: - continue # static range; fallback to generic check - # Check for address family specific ranges - address = netaddr.IPAddress(r['vxlan']['group']) - if ((address.version == constants.IPV4_FAMILY) and - (interface.ipv4_mode == constants.IPV4_DISABLED)): - msg = (_("Interface %(ifname)s is associated to VXLAN " - "provider network %(name)s which requires an " - "IPv4 address") % - {'ifname': interface.ifname, - 'name': providernet_name}) - raise wsme.exc.ClientSideError(msg) - if ((address.version == constants.IPV6_FAMILY) and - (interface.ipv6_mode == constants.IPV6_DISABLED)): - msg = (_("Interface %(ifname)s is associated to VXLAN " - "provider network %(name)s which requires an " - "IPv6 address") % - {'ifname': interface.ifname, - 'name': providernet_name}) - raise wsme.exc.ClientSideError(msg) + + dn = pecan.request.dbapi.datanetwork_get(ifdn.datanetwork_uuid) + if not dn.multicast_group: + # static range; fallback to generic check + continue + + # Check for address family specific ranges + address = netaddr.IPAddress(dn.multicast_group) + if ((address.version == constants.IPV4_FAMILY) and + (interface.ipv4_mode == constants.IPV4_DISABLED or not + interface.ipv4_mode)): + msg = (_("Interface %(ifname)s is associated to VXLAN " + "data network %(name)s which requires an " + "IPv4 address") % + {'ifname': interface.ifname, + 'name': ifdn.datanetwork_name}) + raise wsme.exc.ClientSideError(msg) + if ((address.version == constants.IPV6_FAMILY) and + (interface.ipv6_mode == constants.IPV6_DISABLED or not + interface.ipv6_mode)): + msg = (_("Interface %(ifname)s is associated to VXLAN " + "data network %(name)s which requires an " + "IPv6 address") % + {'ifname': interface.ifname, + 'name': ifdn.datanetwork_name}) + raise wsme.exc.ClientSideError(msg) + # Check for at least 1 address if no ranges exist yet if ((interface.ipv4_mode == constants.IPV4_DISABLED) and - (interface.ipv6_mode == constants.IPV6_DISABLED)): + (interface.ipv6_mode == constants.IPV6_DISABLED) or + (not interface.ipv4_mode and not interface.ipv6_mode)): msg = (_("Interface %(ifname)s is associated to VXLAN " - "provider network %(name)s which requires an IP " + "data network %(name)s which requires an IP " "address") % - {'ifname': interface.ifname, 'name': providernet_name}) + {'ifname': interface.ifname, + 'name': ifdn.datanetwork_name}) raise wsme.exc.ClientSideError(msg) @staticmethod @@ -3263,11 +3267,11 @@ class HostController(rest.RestController): if ((vswitch_type == constants.VSWITCH_TYPE_OVS_DPDK) and (iif.ifclass == constants.INTERFACE_CLASS_DATA)): self._semantic_check_non_accelerated_interface_support(iif) - self._semantic_check_interface_providernets(ihost, iif) + self._semantic_check_interface_datanets(iif) self._semantic_check_interface_addresses(ihost, iif) - if not iif.networktype: + if not iif.ifclass: continue - if any(n in [constants.NETWORK_TYPE_DATA] for n in iif.networktype.split(",")): + if iif.ifclass == constants.NETWORK_TYPE_DATA: data_interface_configured = True if not data_interface_configured: diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface.py index e20d772549..23c7cc1809 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface.py @@ -16,7 +16,9 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2013-2016 Wind River Systems, Inc. +# Copyright (c) 2013-2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 # @@ -42,6 +44,7 @@ from sysinv.api.controllers.v1 import route from sysinv.api.controllers.v1 import types from sysinv.api.controllers.v1 import utils from sysinv.api.controllers.v1 import interface_network +from sysinv.api.controllers.v1 import interface_datanetwork from sysinv.common import constants from sysinv.common import exception from sysinv.common import utils as cutils @@ -101,8 +104,8 @@ DATA_NETWORK_TYPES = [constants.NETWORK_TYPE_DATA] MAX_IFNAME_LEN = 10 MAX_VLAN_ID_LEN = 5 -# Maximum number of characters in provider network list -MAX_PROVIDERNETWORK_LEN = 255 +# Maximum number of characters in data network list +MAX_DATANETWORK_LEN = 255 DEFAULT_MTU = 1500 @@ -152,12 +155,8 @@ class Interface(base.APIBase): txhashpolicy = wtypes.text "Represent the txhashpolicy of the interface" - providernetworks = wtypes.text - "Represent the providernetworks of the interface" - - providernetworksdict = {wtypes.text: utils.ValidTypes(wtypes.text, - six.integer_types)} - "Represent the providernetworksdict of the interface" + datanetworks = [wtypes.text] + "Represent the datanetworks of the interface" ifcapabilities = {wtypes.text: utils.ValidTypes(wtypes.text, six.integer_types)} @@ -219,15 +218,19 @@ class Interface(base.APIBase): # fields = ['uuid', 'address'] if not expand else None # interface = iinterface.from_rpc_object(rpc_interface, fields) - interface = Interface(**rpc_interface.as_dict()) + kwargs = rpc_interface.as_dict() + datanetworks_list = kwargs.pop('datanetworks') + + interface = Interface(**kwargs) if not expand: interface.unset_fields_except(['uuid', 'ifname', 'iftype', 'imac', 'imtu', 'ifclass', 'networktype', 'networks', + 'ihost_uuid', 'forihostid', 'aemode', 'schedpolicy', 'txhashpolicy', - 'providernetworks', 'ihost_uuid', 'forihostid', 'vlan_id', 'uses', 'usesmodify', 'used_by', 'ipv4_mode', 'ipv6_mode', 'ipv4_pool', 'ipv6_pool', - 'sriov_numvfs']) + 'sriov_numvfs', + 'datanetworks']) # never expose the ihost_id attribute interface.ihost_id = wtypes.Unset @@ -282,6 +285,13 @@ class Interface(base.APIBase): if interface.ipv6_mode != constants.IPV6_POOL: interface.ipv6_pool = wtypes.Unset + datanetworks_names_list = [] + for dn in datanetworks_list: + dn = pecan.request.dbapi.datanetwork_get(dn) + datanetworks_names_list.append(dn.name) + + interface.datanetworks = datanetworks_names_list + return interface @@ -323,6 +333,11 @@ class InterfaceController(rest.RestController): parent="iinterfaces") "Expose interface_networks as a sub-element of interface" + interface_datanetworks = \ + interface_datanetwork.InterfaceDataNetworkController( + parent="iinterfaces") + "Expose interface_datanetworks as a sub-element of interface" + _custom_actions = { 'detail': ['GET'], } @@ -442,10 +457,13 @@ class InterfaceController(rest.RestController): networks = [] networks_to_add = [] interface_networks_to_remove = [] + datanetworks = [] + datanetworks_to_add = [] + interface_datanetworks_to_remove = [] patches_to_remove = [] for p in patch: if '/ifclass' == p['path']: - if p['value'] == 'none': + if p['value'] == constants.INTERFACE_CLASS_NONE: p['value'] = None elif '/usesmodify' == p['path']: uses = p['value'].split(',') @@ -462,6 +480,15 @@ class InterfaceController(rest.RestController): elif '/interface_networks_to_remove' == p['path']: interface_networks_to_remove = p['value'].split(',') patches_to_remove.append(p) + elif '/datanetworks' == p['path']: + datanetworks = p['value'].split(',') + patches_to_remove.append(p) + elif '/datanetworks_to_add' == p['path']: + datanetworks_to_add = p['value'].split(',') + patches_to_remove.append(p) + elif '/interface_datanetworks_to_remove' == p['path']: + interface_datanetworks_to_remove = p['value'].split(',') + patches_to_remove.append(p) if uses: patch.append(dict(path='/uses', value=uses, op='replace')) @@ -524,6 +551,7 @@ class InterfaceController(rest.RestController): # Process updates vlan_id = None delete_addressing = False + delete_ifdn = False for p in patch: if '/vlan_id' in p['path']: @@ -569,6 +597,7 @@ class InterfaceController(rest.RestController): interface['ipv4_mode'] = None interface['ipv6_mode'] = None delete_addressing = True + delete_ifdn = True else: # Otherwise make sure that appropriate defaults are set. interface = _set_defaults(interface) @@ -581,7 +610,8 @@ class InterfaceController(rest.RestController): interface = _check("modify", interface, ports=ports, ifaces=uses, - existing_interface=rpc_interface.as_dict()) + existing_interface=rpc_interface.as_dict(), + datanetworks=datanetworks) if uses: # Update MAC address if uses list changed @@ -623,6 +653,40 @@ class InterfaceController(rest.RestController): if _is_ipv6_address_mode_updated(interface, rpc_interface): _update_ipv6_address_mode(interface) + # Update interface-datanetworks + if datanetworks_to_add: + for datanetwork_id in datanetworks_to_add: + values = {'interface_id': interface['id'], + 'datanetwork_id': datanetwork_id} + try: + pecan.request.dbapi.interface_datanetwork_create(values) + except exception.InterfaceDataNetworkAlreadyExists: + pass + elif datanetworks: + _update_interface_datanetworks( + ihost['uuid'], interface, datanetworks, delete_ifdn) + + try: + # Remove old datanetworks from the interface + if interface_datanetworks_to_remove: + for ifdatanet_id in interface_datanetworks_to_remove: + pecan.request.dbapi.interface_datanetwork_destroy( + ifdatanet_id) + elif (orig_ifclass == constants.INTERFACE_CLASS_DATA and + (not ifclass or + ifclass != constants.INTERFACE_CLASS_DATA)): + # data networks apply only for DATA + ifdatanets = \ + pecan.request.dbapi.interface_datanetwork_get_by_interface( + rpc_interface['uuid']) + for ifdatanet in ifdatanets: + pecan.request.dbapi.interface_datanetwork_destroy(ifdatanet.uuid) + except Exception as e: + LOG.exception(e) + msg = _("Failed to remove interface datanetwork association for " + "interface %s" % (interface['ifname'])) + raise wsme.exc.ClientSideError(msg) + # Commit operation with neutron if (interface['ifclass'] and interface['ifclass'] in NEUTRON_INTERFACE_CLASS): @@ -755,6 +819,65 @@ class InterfaceController(rest.RestController): # UTILS ############## +def _update_interface_datanetworks(host_uuid, interface, + datanetworks=None, + delete_ifdn=False): + + pns = [] + + if datanetworks: + # remove 'none' from datanetworks + datanetworks = \ + [x for x in datanetworks if x != constants.DATANETWORK_TYPE_NONE] + for datanetwork_id in datanetworks: + dn = pecan.request.dbapi.datanetwork_get(datanetwork_id) + pns.append(dn.name) + elif 'datanetworks' in interface: + pns = interface['datanetworks'] + + LOG.info("_update_interface_datanetworks interface=%s datanetworks=%s pns=%s" % + (interface, datanetworks, pns)) + + # remove from the interface datanetworks not in list + ifdns = \ + pecan.request.dbapi.interface_datanetwork_get_by_host( + host_uuid) + for ifdn in ifdns: + # if this is not this interface, continue + if_uuid = interface.get('uuid', None) + if if_uuid: + if if_uuid != ifdn.interface_uuid: + continue + elif ifdn.ifname != interface.get('ifname'): + continue + + LOG.debug("_update_interface_datanetworks host_uuid %s " + "interface=%s ifdn=%s" % + (host_uuid, interface, ifdn.as_dict())) + if (pns and ifdn.datanetwork_name not in pns) or delete_ifdn: + LOG.info("interface_datanetwork_destroy %s %s delete_ifdn=%s" % + (ifdn.uuid, ifdn.ifname, delete_ifdn)) + pecan.request.dbapi.interface_datanetwork_destroy( + ifdn.uuid) + + for pn in pns: + dn = pecan.request.dbapi.datanetwork_get(pn) + values = {'interface_id': interface['id'], + 'datanetwork_id': dn.id} + try: + ifdn = pecan.request.dbapi.interface_datanetwork_create(values) + except exception.InterfaceDataNetworkAlreadyExists: + pass + except Exception as e: + LOG.exception(e) + msg = _("Failed to create interface datanetwork " + "assignment for interface %s" % + (interface['ifname'])) + raise wsme.exc.ClientSideError(msg) + + return ifdns + + def _dynamic_address_allocation(): mgmt_network = pecan.request.dbapi.network_get_by_type( constants.NETWORK_TYPE_MGMT) @@ -1258,11 +1381,140 @@ def _check_networks(interface): raise wsme.exc.ClientSideError(msg) -def _check_interface_data(op, interface, ihost, existing_interface): +def _check_datanetworks(ihost, + interface, + interface_list, + existing_interface, + networktypelist, + datanetworks=None): + + if 'id' in interface: + this_interface_id = interface['id'] + else: + this_interface_id = 0 + + ifclass = interface['ifclass'] + iftype = interface['iftype'] + + if not datanetworks: + datanetworks = interface.get('datanetworks') or [] + + # remove 'none' from datanetworks + datanetworks = \ + [x for x in datanetworks if x != constants.DATANETWORK_TYPE_NONE] + + LOG.debug("_check_datanetworks datanetworks interface=%s datanetworks=%s" % + (interface, datanetworks)) + + # Get all provisioned datanetworks + all_datanetworks = {} + db_datanetworks = pecan.request.dbapi.datanetworks_get_all() + for db in db_datanetworks: + all_datanetworks[db.name] = { + 'network_type': db.network_type} + + # Ensure a valid datanetwork is specified + # Ensure at least one datanetwork is selected for 'data', + # and none for 'oam', 'mgmt' and 'infra' + # Ensure uniqueness of the datanetworks + + datanetworks_list = [] + for datanetwork in datanetworks: + if datanetwork == constants.DATANETWORK_TYPE_NONE: + continue + dn = pecan.request.dbapi.datanetwork_get(datanetwork) + datanetworks_list.append(dn.name) + + if interface['ifclass'] in NEUTRON_INTERFACE_CLASS: + if not datanetworks: + msg = _("At least one data network must be selected.") + raise wsme.exc.ClientSideError(msg) + if len(datanetworks) > MAX_DATANETWORK_LEN: + msg = _("Data network list must not exceed %d characters." % + MAX_DATANETWORK_LEN) + raise wsme.exc.ClientSideError(msg) + + for pn in [n.strip() for n in datanetworks_list]: + if pn not in all_datanetworks.keys(): + msg = _("Data network '%s' does not exist." % pn) + raise wsme.exc.ClientSideError(msg) + if datanetworks_list.count(pn) > 1: + msg = (_("Specifying duplicate data network '%(name)s' " + "is not permitted") % {'name': pn}) + raise wsme.exc.ClientSideError(msg) + datanet = all_datanetworks[pn] + if iftype == constants.INTERFACE_TYPE_VLAN: + if datanet['network_type'] == \ + constants.DATANETWORK_TYPE_VLAN: + msg = _("VLAN based data network '%s' cannot be " + "assigned to a VLAN interface" % pn) + raise wsme.exc.ClientSideError(msg) + + # If pxeboot, Mgmt, Infra network types are consolidated + # with a data network type on the same interface, + # in which case, they would be the primary network + # type. Ensure that the only data type that + # can be assigned is VLAN. + if (datanet['network_type'] != constants.DATANETWORK_TYPE_VLAN and + ifclass not in NEUTRON_NETWORK_TYPES): + msg = _("Data network '%s' of type '%s' cannot be assigned " + "to an interface with interface class '%s'" + % (pn, datanet['network_type'], ifclass)) + raise wsme.exc.ClientSideError(msg) + + # This ensures that a specific data network type can + # only be assigned to 1 data interface. Such as the case of + # when only 1 vxlan data is required when SDN is enabled + if constants.NETWORK_TYPE_DATA in networktypelist and interface_list: + for pn in [n.strip() for n in datanetworks_list]: + for i in interface_list: + if i.id == this_interface_id: + continue + if not i.ifclass or not i.datanetworks: + continue + if constants.NETWORK_TYPE_DATA != i.ifclass: + continue + + other_datanetworks = [] + for datanetwork in i.datanetworks: + dn = pecan.request.dbapi.datanetwork_get(datanetwork) + other_datanetworks.append(dn.name) + if pn in other_datanetworks: + msg = _("Data interface %(ifname)s is already " + "attached to this Data Network: " + "%(datanetwork)s." % + {'ifname': i.ifname, 'datanetwork': pn}) + raise wsme.exc.ClientSideError(msg) + + elif (not _neutron_providernet_extension_supported() and + any(nt in PCI_NETWORK_TYPES for nt in networktypelist)): + # When the neutron implementation is not our own and it does not + # support our data network extension we still want to do minimal + # validation of the data network list but we cannot do more + # complex validation because we do not have any additional information + # about the data networks. + if not datanetworks: + msg = _("At least one data network must be selected.") + raise wsme.exc.ClientSideError(msg) + + elif (interface['ifclass'] and + interface['ifclass'] not in NEUTRON_INTERFACE_CLASS and + not existing_interface): + if datanetworks: + msg = _("Data network(s) not supported " + "for non-data interfaces. (%s) (%s)" % + (interface['ifclass'], str(existing_interface))) + raise wsme.exc.ClientSideError(msg) + elif (_neutron_providernet_extension_supported() or + interface['ifclass'] not in NEUTRON_INTERFACE_CLASS): + interface['datanetworks'] = None + + +def _check_interface_data(op, interface, ihost, existing_interface, + datanetworks=None): # Get data ihost_id = interface['forihostid'] ihost_uuid = interface['ihost_uuid'] - providernetworks = interface['providernetworks'] ifclass = interface['ifclass'] networktypelist = [] if ifclass == constants.INTERFACE_CLASS_PLATFORM: @@ -1274,9 +1526,6 @@ def _check_interface_data(op, interface, ihost, existing_interface): else: networktypelist.append(constants.INTERFACE_CLASS_NONE) - # Get providernet dict - all_providernetworks = _neutron_providernet_list() - # Check interface name for validity _check_interface_name(op, interface, ihost, existing_interface) @@ -1448,96 +1697,13 @@ def _check_interface_data(op, interface, ihost, existing_interface): host_port, networktypelist) - # Ensure a valid providernetwork is specified - # Ensure at least one providernetwork is selected for 'data', - # or interface (when SDN L3 services are enabled) - # and none for 'oam', 'mgmt' and 'infra' - # Ensure uniqueness wrt the providernetworks - if (_neutron_providernet_extension_supported() and - interface['ifclass'] in NEUTRON_INTERFACE_CLASS): - if not providernetworks: - msg = _("At least one provider network must be selected.") - raise wsme.exc.ClientSideError(msg) - if len(providernetworks) > MAX_PROVIDERNETWORK_LEN: - msg = _("Provider network list must not exceed %d characters." % - MAX_PROVIDERNETWORK_LEN) - raise wsme.exc.ClientSideError(msg) - providernetworks_list = providernetworks.split(',') - for pn in [n.strip() for n in providernetworks_list]: - if pn not in all_providernetworks.keys(): - msg = _("Provider network '%s' does not exist." % pn) - raise wsme.exc.ClientSideError(msg) - if providernetworks_list.count(pn) > 1: - msg = (_("Specifying duplicate provider network '%(name)s' " - "is not permitted") % {'name': pn}) - raise wsme.exc.ClientSideError(msg) - providernet = all_providernetworks[pn] - if iftype == constants.INTERFACE_TYPE_VLAN: - if providernet['type'] == 'vlan': - msg = _("VLAN based provider network '%s' cannot be " - "assigned to a VLAN interface" % pn) - raise wsme.exc.ClientSideError(msg) - - # If pxeboot, Mgmt, Infra network types are consolidated - # with a data network type on the same interface, - # in which case, they would be the primary network - # type. Ensure that the only provider type that - # can be assigned is VLAN. - if (providernet['type'] != constants.NEUTRON_PROVIDERNET_VLAN and - ifclass not in NEUTRON_NETWORK_TYPES): - msg = _("Provider network '%s' of type '%s' cannot be assigned " - "to an interface with interface class '%s'" - % (pn, providernet['type'], ifclass)) - raise wsme.exc.ClientSideError(msg) - - # This ensures that a specific provider network type can - # only be assigned to 1 data interface. Such as the case of - # when only 1 vxlan provider is required when SDN is enabled - if constants.NETWORK_TYPE_DATA in networktypelist and interface_list: - for pn in [n.strip() for n in providernetworks.split(',')]: - for i in interface_list: - if i.id == this_interface_id: - continue - if not i.ifclass or not i.providernetworks: - continue - if constants.NETWORK_TYPE_DATA != i.ifclass: - continue - other_providernetworks = i.providernetworks.split(',') - if pn in other_providernetworks: - msg = _("Data interface %(ifname)s is already " - "attached to this Provider Network: " - "%(network)s." % - {'ifname': i.ifname, 'network': pn}) - raise wsme.exc.ClientSideError(msg) - - # Send the interface and provider network details to neutron for - # additional validation. - _neutron_bind_interface(ihost, interface, test=True) - # Send the shared data interface(s) and provider networks details to - # neutron for additional validation, if required - _update_shared_interface_neutron_bindings(ihost, interface, test=True) - - elif (not _neutron_providernet_extension_supported() and - any(nt in PCI_NETWORK_TYPES for nt in networktypelist)): - # When the neutron implementation is not our own and it does not - # support our provider network extension we still want to do minimal - # validation of the provider network list but we cannot do more - # complex validation because we do not have any additional information - # about the provider networks. - if not providernetworks: - msg = _("At least one provider network must be selected.") - raise wsme.exc.ClientSideError(msg) - - elif (interface['ifclass'] and - interface['ifclass'] not in NEUTRON_INTERFACE_CLASS and - not existing_interface): - if providernetworks is not None: - msg = _("Provider network(s) not supported " - "for non-data interfaces. (%s) (%s)" % (interface['ifclass'], str(existing_interface))) - raise wsme.exc.ClientSideError(msg) - elif (_neutron_providernet_extension_supported() or - interface['ifclass'] not in NEUTRON_INTERFACE_CLASS): - interface['providernetworks'] = None + # Check datanetworks (formerly known as providernetworks) + _check_datanetworks(ihost, + interface, + interface_list, + existing_interface, + networktypelist, + datanetworks) # check MTU if interface['iftype'] == constants.INTERFACE_TYPE_VLAN: @@ -1894,31 +2060,6 @@ def _update_host_cluster_address(host, interface): address_name) -def _clean_providernetworks(providernetworks): - pn = [','.join(p['name']) for p in providernetworks] - return pn - - -""" -Params: - pn_all: all providernets stored in neutron - pn_names: providernets specified for this interface - -Return: - pn_dict: a dictionary of providernets specified - for this interface: item format {name:body} -""" - - -def _get_providernetworksdict(pn_all, pn_names): - pn_dict = {} - if pn_names: - for name, body in pn_all.items(): - if name in pn_names.split(','): - pn_dict.update({name: body}) - return pn_dict - - def _get_interface_vlans(ihost_uuid, interface): """ Retrieve the VLAN id values (if any) that are dependent on this @@ -2053,6 +2194,38 @@ def _update_shared_interface_neutron_bindings(ihost, interface, test=False): _neutron_bind_interface(ihost, shared_interface, test) +def _datanetworks_get_by_interface(interface_uuid): + ifdatanets = pecan.request.dbapi.interface_datanetwork_get_by_interface( + interface_uuid) + + LOG.debug("_datanetworks_get_by_interface %s ifdnets=%s" % + (interface_uuid, ifdatanets)) + + datanetworks = [] + for ifdatanet in ifdatanets: + datanetworks.append(ifdatanet.datanetwork_uuid) + + datanetworks_list = [] + datanetworks_names_list = [] + for datanetwork in datanetworks: + dn = pecan.request.dbapi.datanetwork_get(datanetwork) + datanetwork_dict = \ + {'name': dn.name, + 'uuid': dn.uuid, + 'network_type': dn.network_type, + 'mtu': dn.mtu} + datanetworks_names_list.append(dn.name) + if dn.network_type == constants.DATANETWORK_TYPE_VXLAN: + datanetwork_dict.update( + {'port_num': dn.port_num, + 'multicast_group': dn.multicast_group, + 'ttl': dn.ttl, + 'mode': dn.mode}) + datanetworks_list.append(datanetwork_dict) + + return datanetworks_names_list, datanetworks_list + + def _neutron_bind_interface(ihost, interface, test=False): """ Send a request to neutron to bind the interface to the specified @@ -2082,7 +2255,13 @@ def _neutron_bind_interface(ihost, interface, test=False): raise wsme.exc.ClientSideError(msg) interface_uuid = interface['uuid'] - providernetworks = interface.get('providernetworks', '') + datanetworks_names_list, _dl = \ + _datanetworks_get_by_interface(interface_uuid) + + providernetworks = ",".join([str(x) for x in datanetworks_names_list]) + LOG.info("_neutron_bind_interface uuid=%s datanetworks_names=%s" % + (interface_uuid, providernetworks)) + vlans = _get_interface_vlans(ihost_uuid, interface) try: # Send the request to neutron @@ -2210,6 +2389,8 @@ def _create(interface, from_profile=False): else: forihostid = ihostId + datanetworks = interface.get('datanetworks') + LOG.debug("iinterface post interfaces ihostid: %s" % forihostid) interface.update({'forihostid': ihost['id'], @@ -2267,6 +2448,9 @@ def _create(interface, from_profile=False): forihostid, interface) + # Create interface-datanetworks + _update_interface_datanetworks(ihost['uuid'], new_interface, datanetworks) + # Create network-interface try: if (new_interface['ifclass'] and @@ -2382,7 +2566,7 @@ def _create(interface, from_profile=False): def _check(op, interface, ports=None, ifaces=None, from_profile=False, - existing_interface=None): + existing_interface=None, datanetworks=None): # Semantic checks ihost = pecan.request.dbapi.ihost_get(interface['ihost_uuid']).as_dict() _check_host(ihost) @@ -2414,9 +2598,11 @@ def _check(op, interface, ports=None, ifaces=None, from_profile=False, if 'txhashpolicy' not in iface: iface['txhashpolicy'] = None - _check_interface_data("modify", iface, ihost, existing_iface) + _check_interface_data( + "modify", iface, ihost, existing_iface, datanetworks) - interface = _check_interface_data(op, interface, ihost, existing_interface) + interface = _check_interface_data( + op, interface, ihost, existing_interface, datanetworks) return interface diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_datanetwork.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_datanetwork.py new file mode 100644 index 0000000000..1c37683682 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/interface_datanetwork.py @@ -0,0 +1,286 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# Copyright 2013 UnitedStack Inc. +# 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. +# +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import uuid +import wsme +import pecan +from pecan import rest +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from sysinv.api.controllers.v1 import base +from sysinv.api.controllers.v1 import collection +from sysinv.api.controllers.v1 import types +from sysinv.api.controllers.v1 import utils +from sysinv.common import utils as cutils +from sysinv.common import constants +from sysinv.common import exception +from sysinv.openstack.common.gettextutils import _ + +from sysinv import objects + + +class InterfaceDataNetwork(base.APIBase): + + id = int + "Unique ID for this interface data network" + + uuid = types.uuid + "Unique UUID for this interface data network" + + forihostid = int + "The ID of the host the interface data network belongs to" + + interface_uuid = types.uuid + "Unique UUID of the parent interface" + + ifname = wtypes.text + "User defined name of the interface" + + datanetwork_id = int + "Unique ID of the parent datanetwork" + + datanetwork_uuid = types.uuid + "Unique UUID of the parent datanetwork" + + datanetwork_name = wtypes.text + "User defined name of the datanetwork" + + network_type = wtypes.text + "Represents the type for the datanetwork" + + def __init__(self, **kwargs): + self.fields = objects.interface_datanetwork.fields.keys() + for k in self.fields: + if not hasattr(self, k): + continue + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @classmethod + def convert_with_links(cls, rpc_interface_datanetwork, expand=True): + interface_datanetwork = InterfaceDataNetwork( + **rpc_interface_datanetwork.as_dict()) + if not expand: + interface_datanetwork.unset_fields_except([ + 'forihostid', 'id', 'uuid', 'interface_uuid', 'ifname', + 'datanetwork_id', 'datanetwork_uuid', + 'datanetwork_name', 'network_type' + ]) + return interface_datanetwork + + +class InterfaceDataNetworkCollection(collection.Collection): + """API representation of a collection of IP addresses.""" + + interface_datanetworks = [InterfaceDataNetwork] + "A list containing Interface Data Network objects" + + def __init__(self, **kwargs): + self._type = 'interface_datanetworks' + + @classmethod + def convert_with_links(cls, rpc_interface_datanetwork, limit, url=None, + expand=False, **kwargs): + collection = InterfaceDataNetworkCollection() + collection.interface_datanetworks = [ + InterfaceDataNetwork.convert_with_links(p, expand) + for p in rpc_interface_datanetwork] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +LOCK_NAME = 'InterfaceDataNetworkController' + + +class InterfaceDataNetworkController(rest.RestController): + + def __init__(self, parent=None): + self._parent = parent + + def _create_interface_datanetwork(self, interface_datanetwork): + interface_datanetwork_dict = interface_datanetwork.as_dict() + interface_datanetwork_dict['uuid'] = str(uuid.uuid4()) + + # Remove UUIDs from dict to be replaced with IDs + interface_uuid = interface_datanetwork_dict.pop('interface_uuid') + datanetwork_uuid = interface_datanetwork_dict.pop('datanetwork_uuid') + + interface_id = self._get_interface_id(interface_uuid) + + try: + datanetwork_obj = \ + pecan.request.dbapi.datanetwork_get(datanetwork_uuid) + except exception.DataNetworkNotFound: + msg = _("DataNetwork with uuid '%s' does not exist. " % + datanetwork_uuid) + raise wsme.exc.ClientSideError(msg) + + datanetwork_id = datanetwork_obj['id'] + + interface_datanetwork_dict['interface_id'] = interface_id + interface_datanetwork_dict['datanetwork_id'] = datanetwork_id + + interface_obj = pecan.request.dbapi.iinterface_get(interface_uuid) + self._check_host(interface_obj.ihost_uuid) + + self._check_interface_class(interface_obj) + self._check_interface_mtu(interface_obj, datanetwork_obj) + self._check_duplicate_interface_datanetwork(interface_datanetwork_dict) + + result = pecan.request.dbapi.interface_datanetwork_create( + interface_datanetwork_dict) + + return InterfaceDataNetwork.convert_with_links(result) + + def _get_interface_datanetwork_collection( + self, parent_uuid=None, marker=None, limit=None, sort_key=None, + sort_dir=None, expand=False, resource_url=None): + limit = utils.validate_limit(limit) + sort_dir = utils.validate_sort_dir(sort_dir) + marker_obj = None + + if marker: + marker_obj = objects.interface_datanetwork.get_by_uuid( + pecan.request.context, marker) + + if self._parent == "ihosts": + interface_datanetworks = \ + pecan.request.dbapi.interface_datanetwork_get_by_host( + parent_uuid, + limit=limit, marker=marker_obj, + sort_key=sort_key, sort_dir=sort_dir) + elif self._parent == "iinterfaces": + interface_datanetworks = \ + pecan.request.dbapi.interface_datanetwork_get_by_interface( + parent_uuid, limit=limit, marker=marker_obj, + sort_key=sort_key, sort_dir=sort_dir) + else: + interface_datanetworks = \ + pecan.request.dbapi.interface_datanetwork_get_all( + limit=limit, marker=marker_obj, + sort_key=sort_key, sort_dir=sort_dir) + + return InterfaceDataNetworkCollection.convert_with_links( + interface_datanetworks, limit, url=resource_url, expand=expand, + sort_key=sort_key, sort_dir=sort_dir) + + @staticmethod + def _get_one(interface_datanetwork_uuid): + rpc_interface_datanetwork = objects.interface_datanetwork.get_by_uuid( + pecan.request.context, interface_datanetwork_uuid) + return InterfaceDataNetwork.convert_with_links( + rpc_interface_datanetwork) + + @staticmethod + def _check_interface_class(interface_obj): + if (not interface_obj.ifclass or + interface_obj.ifclass == constants.INTERFACE_CLASS_NONE): + values = {'ifclass': constants.INTERFACE_CLASS_DATA} + pecan.request.dbapi.iinterface_update(interface_obj.uuid, values) + return + else: + # Allow ifclass data to assign another; disallow other ifclass + if interface_obj.ifclass != constants.INTERFACE_CLASS_DATA: + msg = _("An interface with interface class '%s' " + "cannot assign datanetworks." % + interface_obj.ifclass) + raise wsme.exc.ClientSideError(msg) + + @staticmethod + def _check_host(host_uuid): + host = pecan.request.dbapi.ihost_get(host_uuid) + if host.administrative != constants.ADMIN_LOCKED: + msg = _("Operation Rejected: Host '%s' is adminstrative '%s' " % + (host.hostname, host.administrative)) + raise wsme.exc.ClientSideError(msg) + + @staticmethod + def _check_interface_mtu(interface_obj, datanetwork_obj): + if datanetwork_obj.network_type == constants.DATANETWORK_TYPE_VXLAN: + overhead = constants.VXLAN_MTU_OVERHEAD + else: + overhead = 0 + + if interface_obj.imtu < datanetwork_obj.mtu + overhead: + msg = _("The interface MTU %s must be larger than the '%s' " + "datanetwork MTU requirement." % + (interface_obj.imtu, datanetwork_obj.mtu)) + raise wsme.exc.ClientSideError(msg) + + @staticmethod + def _query_interface_datanetwork(interface_datanetwork): + try: + result = pecan.request.dbapi.interface_datanetwork_query( + interface_datanetwork) + except exception.InterfaceDataNetworkNotFoundByKeys: + return None + return result + + def _check_duplicate_interface_datanetwork(self, interface_datanetwork): + result = self._query_interface_datanetwork(interface_datanetwork) + if not result: + return + msg = _("Interface '%s' assignment with Data Network '%s' " + "already exists." + % (interface_datanetwork['interface_id'], + interface_datanetwork['datanetwork_id'])) + raise wsme.exc.ClientSideError(msg) + + @staticmethod + def _get_interface_id(interface_uuid): + interface = pecan.request.dbapi.iinterface_get(interface_uuid) + return interface['id'] + + @staticmethod + def _get_datanetwork_id_and_type(datanetwork_uuid): + datanetwork = pecan.request.dbapi.datanetwork_get(datanetwork_uuid) + return datanetwork['id'], datanetwork['network_type'] + + @wsme_pecan.wsexpose(InterfaceDataNetwork, types.uuid) + def get_one(self, interface_datanetwork_uuid): + return self._get_one(interface_datanetwork_uuid) + + @wsme_pecan.wsexpose(InterfaceDataNetworkCollection, + wtypes.text, types.uuid, int, + wtypes.text, wtypes.text) + def get_all(self, parent_uuid=None, marker=None, + limit=None, sort_key='id', sort_dir='asc'): + return self._get_interface_datanetwork_collection( + parent_uuid, marker, limit, sort_key, sort_dir) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(InterfaceDataNetwork, body=InterfaceDataNetwork) + def post(self, interface_datanetwork): + return self._create_interface_datanetwork(interface_datanetwork) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(None, types.uuid, status_code=204) + def delete(self, interface_datanetwork_uuid): + ifdn_obj = pecan.request.dbapi.interface_datanetwork_get( + interface_datanetwork_uuid) + interface_obj = pecan.request.dbapi.iinterface_get( + ifdn_obj.interface_uuid) + self._check_host(interface_obj.ihost_uuid) + pecan.request.dbapi.interface_datanetwork_destroy( + interface_datanetwork_uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile.py index 438c0127fb..b867313f5a 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile.py @@ -74,7 +74,7 @@ CONF.import_opt('journal_default_size', # Defines the fields that must be copied in/out of interface profiles INTERFACE_PROFILE_FIELDS = ['ifname', 'iftype', 'imtu', 'networktype', 'ifclass', 'aemode', 'networks', - 'txhashpolicy', 'forihostid', 'providernetworks', + 'txhashpolicy', 'forihostid', 'datanetworks', 'vlan_id', 'ipv4_mode', 'ipv6_mode', 'ipv4_pool', 'ipv6_pool', 'sriov_numvfs'] @@ -1350,7 +1350,7 @@ def _create_if_profile(profile_name, profile_node): 'imtu': ethIf.mtu, 'networktype': nt, 'forihostid': iprofile_id, - 'providernetworks': providernets, + 'datanetworks': providernets, 'ipv4_mode': ipv4_mode['mode'], 'ipv6_mode': ipv6_mode['mode'], 'ipv4_pool': ipv4_mode['pool'], @@ -1390,7 +1390,7 @@ def _create_if_profile(profile_name, profile_node): 'aemode': aeIf.aeMode, 'txhashpolicy': aeIf.txPolicy, 'forihostid': iprofile_id, - 'providernetworks': providernets, + 'datanetworks': providernets, 'ipv4_mode': ipv4_mode, 'ipv6_mode': ipv6_mode, 'ipv4_pool': ipv4_pool, @@ -1417,7 +1417,7 @@ def _create_if_profile(profile_name, profile_node): 'networktype': nt, 'vlan_id': vlanIf.vlanId, 'forihostid': iprofile_id, - 'providernetworks': providernets, + 'datanetworks': providernets, 'ipv4_mode': ipv4_mode, 'ipv6_mode': ipv6_mode, 'ipv4_pool': ipv4_pool, diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile_utils.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile_utils.py index d59ee5808c..e605173ed9 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile_utils.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/profile_utils.py @@ -54,13 +54,13 @@ class Network(object): raise InvalidProfileData("At least one provider network must be selected.") -class DataNetwork(Network): +class DataclassNetwork(Network): def __init__(self, node): - super(DataNetwork, self).__init__(node, constants.NETWORK_TYPE_DATA) - self.ipv4Mode = DataNetwork.getIpMode(node, "ipv4") - self.ipv6Mode = DataNetwork.getIpMode(node, "ipv6") - self.routes = DataNetwork.getRoutes(node) + super(DataclassNetwork, self).__init__(node, constants.NETWORK_TYPE_DATA) + self.ipv4Mode = DataclassNetwork.getIpMode(node, "ipv4") + self.ipv6Mode = DataclassNetwork.getIpMode(node, "ipv6") + self.routes = DataclassNetwork.getRoutes(node) @staticmethod def getRoutes(node): @@ -288,7 +288,7 @@ class EthInterface(Interface): def getNetworkMap(self): return { - 'dataNetwork': lambda node: DataNetwork(node), + 'dataclassNetwork': lambda node: DataclassNetwork(node), 'infraNetwork': lambda node: ExternalNetwork(node, constants.NETWORK_TYPE_INFRA), 'oamNetwork': lambda node: ExternalNetwork(node, constants.NETWORK_TYPE_OAM), 'mgmtNetwork': lambda node: ExternalNetwork(node, constants.NETWORK_TYPE_MGMT), @@ -330,7 +330,7 @@ class AeInterface(Interface): def getNetworkMap(self): return { - 'dataNetwork': lambda node: DataNetwork(node), + 'dataclassNetwork': lambda node: DataclassNetwork(node), 'infraNetwork': lambda node: ExternalNetwork(node, constants.NETWORK_TYPE_INFRA), 'oamNetwork': lambda node: ExternalNetwork(node, constants.NETWORK_TYPE_OAM), 'mgmtNetwork': lambda node: ExternalNetwork(node, constants.NETWORK_TYPE_MGMT) @@ -365,7 +365,7 @@ class VlanInterface(Interface): def getNetworkMap(self): return { - 'dataNetwork': lambda (node): DataNetwork(node), + 'dataclassNetwork': lambda (node): DataclassNetwork(node), 'infraNetwork': lambda (node): ExternalNetwork(node, constants.NETWORK_TYPE_INFRA), 'oamNetwork': lambda (node): ExternalNetwork(node, constants.NETWORK_TYPE_OAM), 'mgmtNetwork': lambda (node): ExternalNetwork(node, constants.NETWORK_TYPE_MGMT) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/system.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/system.py index 7431c7910b..2f84d6fdf4 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/system.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/system.py @@ -422,11 +422,11 @@ class SystemController(rest.RestController): timezone)) if p['path'] == '/sdn_enabled': - sdn_enabled = p['value'] + sdn_enabled = p['value'].lower() patch.remove(p) if p['path'] == '/https_enabled': - https_enabled = p['value'] + https_enabled = p['value'].lower() patch.remove(p) if p['path'] == '/distributed_cloud_role': diff --git a/sysinv/sysinv/sysinv/sysinv/common/constants.py b/sysinv/sysinv/sysinv/sysinv/common/constants.py index 87e072f82f..0927e82a3f 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/common/constants.py @@ -239,10 +239,42 @@ HWMON_PORT = 2212 NEUTRON_HOST_ALIAS = "host" NEUTRON_WRS_PROVIDER_ALIAS = "wrs-provider" -# Neutron provider networks -NEUTRON_PROVIDERNET_FLAT = "flat" -NEUTRON_PROVIDERNET_VXLAN = "vxlan" -NEUTRON_PROVIDERNET_VLAN = "vlan" +# Data Networks +DATANETWORK_TYPE_NONE = "none" +DATANETWORK_TYPE_FLAT = "flat" +DATANETWORK_TYPE_VLAN = "vlan" +DATANETWORK_TYPE_VXLAN = "vxlan" + +DATANETWORK_MODE_DYNAMIC = "dynamic" +DATANETWORK_MODE_STATIC = "static" + +DATANETWORK_VXLAN_MODES = [ + DATANETWORK_MODE_DYNAMIC, + DATANETWORK_MODE_STATIC +] + +# Represents the number of bytes added to a tenant packet when it is carried +# by a VXLAN based provider network. We start by assuming a tenant network +# with an MTU of 1500 bytes. This means that at the host vswitch the +# ethernet frame will be 1514 bytes (+4 if VLAN tagged) not including the FCS +# trailer. To get this packet on to the provider network it must be +# encapsulated as-is with a {IPv4|IPv6}+UDP+VXLAN headers. The ETH+VLAN +# headers are not included because they themselves are not included in the +# provider network MTU (i.e., the VXLAN packet must fit within the ethernet +# payload of the provider interface). +# Therefore the maximum overhead, assuming a VLAN tagged provider network, is: +# +# IPv4 = 20 + 8 + 8 = 36 +# IPv6 = 40 + 8 + 8 = 56 +# +# This brings the maximum tenant packet size to: +# IPv4 = 36 + 1518 = 1554 +# IPv6 = 56 + 1518 = 1574 +# +# Therefore to support an tenant MTU of 1500 the underlying physical +# interface must support an MTU of 1574 bytes. +# +VXLAN_MTU_OVERHEAD = 74 # Supported worker node vswitch types VSWITCH_TYPE_OVS_DPDK = "ovs-dpdk" diff --git a/sysinv/sysinv/sysinv/sysinv/common/exception.py b/sysinv/sysinv/sysinv/sysinv/common/exception.py index 034f3b3e88..b675ee862b 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/exception.py +++ b/sysinv/sysinv/sysinv/sysinv/common/exception.py @@ -1293,3 +1293,57 @@ class LocalManagementIpNotFound(NotFound): class InvalidHelmDockerImageSource(Invalid): message = _("Invalid docker image source: %(source)s. Must be one of %(valid_srcs)s") + + +# DataNetwork +class UnsupportedInterfaceDataNetworkType(Conflict): + message = _("Interface with datanetwork type '%(datanetworktype)s' " + "is not supported.") + + +class DataNetworkNotFound(NotFound): + message = _("DataNetwork %(datanetwork_uuid)s could not be found.") + + +class DataNetworkTypeNotFound(NotFound): + message = _("DataNetwork of type %(network_type)s could not be found.") + + +class DataNetworkIDNotFound(NotFound): + message = _("DataNetwork with id %(id)s could not be found.") + + +class DataNetworkNameNotFound(NotFound): + message = _("DataNetwork with name %(name)s could not be found.") + + +class DataNetworkAlreadyExists(Conflict): + message = _("DataNetwork of name %(name)s already exists.") + + +class DataNetworkTypeUnsupported(Conflict): + message = _("DataNetwork of type %(network_type)s is not supported.") + + +class InterfaceDataNetworkNotFound(NotFound): + message = _("Interface datanetwork %(uuid)s could not be found.") + + +class InterfaceDataNetworkAlreadyExists(Conflict): + message = _("Interface datanetwork with interface ID %(interface_id)s " + "and datanetwork ID %(datanetwork_id)s already exists.") + + +class InterfaceDataNetworkNotFoundByKeys(NotFound): + message = _("Interface datanetwork with interface ID %(interface_id)s " + "and datanetwork ID %(datanetwork_id)s not found") + + +class UnsupportedAssignedInterfaceDataNetworkType(Conflict): + message = _("Cannot assign datanetwork with type '%(network_type)s' " + "to an interface.") + + +class UnsupportedRemovedInterfaceDataNetworkType(Conflict): + message = _("Cannot remove datanetwork with type '%(network_type)s' " + "from an interface.") diff --git a/sysinv/sysinv/sysinv/sysinv/common/utils.py b/sysinv/sysinv/sysinv/sysinv/common/utils.py index bd5cb5b7c9..4a9af7e5d8 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/common/utils.py @@ -78,7 +78,6 @@ utils_opts = [ default=None, help='Explicitly specify the temporary working directory'), ] - CONF = cfg.CONF CONF.register_opts(utils_opts) @@ -425,6 +424,18 @@ def is_valid_ipv6_cidr(address): return False +def validate_ip_multicast_address(address, valid_values=None): + """ + Validates that an IP address is a multicast address. + """ + try: + return netaddr.IPAddress(address).is_multicast() + except Exception: + msg = _("'%s' is not a valid multicast IP address") % address + LOG.debug(msg) + return False + + def get_shortened_ipv6(address): addr = netaddr.IPAddress(address, version=6) return str(addr.ipv6()) diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/openstack.py b/sysinv/sysinv/sysinv/sysinv/conductor/openstack.py index 441a386262..d1c1d2d7e7 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/openstack.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/openstack.py @@ -451,6 +451,14 @@ class OpenStackOperator(object): (ihost.hostname, agg_add_to)) raise + def _get_interface_datanetworks(self, interface): + ifdatanets = self.dbapi.interface_datanetwork_get_by_interface( + interface.uuid) + names = [x.datanetwork_name for x in ifdatanets] + names_csv = ",".join(str(x) for x in names) + + return names_csv + def nova_host_available(self, ihost_uuid): """ Perform sysinv driven nova operations for an available ihost @@ -500,22 +508,22 @@ class OpenStackOperator(object): availability_zone = None aggregate_name_prefix = 'provider_' - ihost_providernets = [] + ihost_datanets = [] - ihost_aggset_provider = set() + host_aggset_datanet = set() nova_aggset_provider = set() - # determine which providernets are on this ihost + # determine which datanets are on this host try: iinterfaces = self.try_interface_get_by_host(ihost_uuid) for interface in iinterfaces: if interface['ifclass'] == constants.INTERFACE_CLASS_DATA: - providernets = interface.providernetworks - for providernet in providernets.split(',') if providernets else []: - ihost_aggset_provider.add(aggregate_name_prefix + - providernet) + datanets = self._get_interface_datanetworks(interface) + for datanet in datanets.split(',') if datanets else []: + host_aggset_datanet.add(aggregate_name_prefix + + datanet) - ihost_providernets = list(ihost_aggset_provider) + ihost_datanets = list(host_aggset_datanet) except Exception: LOG.exception("AGG iinterfaces_get failed for %s." % ihost_uuid) @@ -529,8 +537,8 @@ class OpenStackOperator(object): for aggregate in aggregates: nova_aggset_provider.add(aggregate.name) - if ihost_providernets: - agglist_missing = list(ihost_aggset_provider - nova_aggset_provider) + if ihost_datanets: + agglist_missing = list(host_aggset_datanet - nova_aggset_provider) LOG.debug("AGG agglist_missing = %s." % agglist_missing) for i in agglist_missing: @@ -538,8 +546,8 @@ class OpenStackOperator(object): # use None for the availability zone # cs.aggregates.create(args.name, args.availability_zone) try: - aggregate = self._get_novaclient().aggregates.create(i, - availability_zone) + aggregate = self._get_novaclient().aggregates.create( + i, availability_zone) aggregates.append(aggregate) LOG.debug("AGG6 aggregate= %s. aggregates= %s" % (aggregate, aggregates)) @@ -577,7 +585,7 @@ class OpenStackOperator(object): ihost = self.dbapi.ihost_get(ihost_uuid) for i in aggregates: - if i.name in ihost_providernets: + if i.name in ihost_datanets: metadata = self._get_novaclient().aggregates.get(int(i.id)) nhosts = [] @@ -596,7 +604,7 @@ class OpenStackOperator(object): % (i.id, ihost.hostname)) return False else: - LOG.warn("AGG ihost_providernets empty %s." % ihost_uuid) + LOG.warn("AGG ihost_datanets empty %s." % ihost_uuid) def nova_host_offline(self, ihost_uuid): """ @@ -618,22 +626,21 @@ class OpenStackOperator(object): # aggregate_name_prefix = 'provider_' - ihost_providernets = [] + ihost_datanets = [] - ihost_aggset_provider = set() + host_aggset_datanet = set() nova_aggset_provider = set() - # determine which providernets are on this ihost + # determine which datanets are on this ihost try: iinterfaces = self.try_interface_get_by_host(ihost_uuid) for interface in iinterfaces: if interface['ifclass'] == constants.INTERFACE_CLASS_DATA: - providernets = interface.providernetworks - for providernet in ( - providernets.split(',') if providernets else []): - ihost_aggset_provider.add(aggregate_name_prefix + - providernet) - ihost_providernets = list(ihost_aggset_provider) + datanets = self._get_interface_datanetworks(interface) + for datanet in (datanets.split(',') if datanets else []): + host_aggset_datanet.add(aggregate_name_prefix + + datanet) + ihost_datanets = list(host_aggset_datanet) except Exception: LOG.exception("AGG iinterfaces_get failed for %s." % ihost_uuid) @@ -643,11 +650,11 @@ class OpenStackOperator(object): self.nova_client = None # password may have updated aggregates = self._get_novaclient().aggregates.list() - if ihost_providernets: + if ihost_datanets: for aggregate in aggregates: nova_aggset_provider.add(aggregate.name) else: - LOG.debug("AGG ihost_providernets empty %s." % ihost_uuid) + LOG.debug("AGG ihost_datanets empty %s." % ihost_uuid) # setup the valid set of storage aggregates for host removal aggset_storage = set([ @@ -661,7 +668,7 @@ class OpenStackOperator(object): ihost = self.dbapi.ihost_get(ihost_uuid) for aggregate in aggregates: - if aggregate.name in ihost_providernets or \ + if aggregate.name in ihost_datanets or \ aggregate.name in aggset_storage: # or just do it for all aggs try: LOG.debug("AGG10 remove aggregate id = %s ihost= %s." % diff --git a/sysinv/sysinv/sysinv/sysinv/db/api.py b/sysinv/sysinv/sysinv/sysinv/db/api.py index 769ea3e37a..cb5ea3d4a8 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/api.py @@ -836,7 +836,6 @@ class Connection(object): 'aemode': 'balanced', 'schedpolicy': 'xor', 'txhashpolicy': 'L2', - 'providernetworks': 'physnet0, physnet1' 'extra': { ... }, } :returns: An iinterface. diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py index 9345ea7745..9dbb30b86e 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py @@ -404,6 +404,26 @@ def add_interface_filter_by_ihost(query, value): return query.filter(models.ihost.uuid == value) +def add_datanetwork_filter(query, value): + """Adds a datanetwork-specific filter to a query. + + :param query: Initial query to add filter to. + :param value: Value for filtering results by. + :return: Modified query. + """ + + if uuidutils.is_uuid_like(value): + return query.filter(or_(models.DataNetworksFlat.uuid == value, + models.DataNetworksVlan.uuid == value, + models.DataNetworksVXlan.uuid == value)) + elif utils.is_int_like(value): + return query.filter(or_(models.DataNetworksFlat.id == value, + models.DataNetworksVlan.id == value, + models.DataNetworksVXlan.id == value)) + else: + return add_identity_filter(query, value, use_name=True) + + def add_port_filter_by_numa_node(query, nodeid): """Adds a port-specific numa node filter to a query. @@ -534,9 +554,9 @@ def add_port_filter_by_host_interface(query, hostid, interfaceid): elif utils.is_uuid_like(hostid) and utils.is_uuid_like(interfaceid): query = query.join(models.ihost, - models.iinterface) + models.Interface) return query.filter(models.ihost.uuid == hostid, - models.iinterface.uuid == interfaceid) + models.Interface.uuid == interfaceid) LOG.debug("port_filter_by_host_iinterface: " "No match for supplied filter ids (%s, %s)" @@ -1325,9 +1345,6 @@ class Connection(api.Connection): model_query(models.imemory, read_deleted="no").\ filter_by(forihostid=server_id).\ delete() - model_query(models.iinterface, read_deleted="no").\ - filter_by(forihostid=server_id).\ - delete() model_query(models.idisk, read_deleted="no").\ filter_by(forihostid=server_id).\ delete() @@ -2063,7 +2080,6 @@ class Connection(api.Connection): return query.all() def _iinterface_get(self, iinterface_id, ihost=None, network=None): - # query = model_query(models.iinterface) entity = with_polymorphic(models.Interfaces, '*') query = model_query(entity) query = add_interface_filter(query, iinterface_id) @@ -2215,16 +2231,13 @@ class Connection(api.Connection): if obj.id is None: obj.id = temp_id - # Ensure networktype and providernetworks results are None when they + # Ensure networktype results are None when they # are specified as 'none'. Otherwise the 'none' value is written to # the database which causes issues with checks that expects it to be # the None type if getattr(obj, 'networktype', None) == constants.NETWORK_TYPE_NONE: setattr(obj, 'networktype', None) - if getattr(obj, 'providernetworks', None) == 'none': - setattr(obj, 'providernetworks', None) - try: session.add(obj) session.flush() @@ -2301,7 +2314,7 @@ class Connection(api.Connection): for k, v in values.items(): if k == 'networktype' and v == constants.NETWORK_TYPE_NONE: v = None - if k == 'providernetworks' and v == 'none': + if k == 'datanetworks' and v == 'none': v = None if k == 'uses': del obj.uses[:] @@ -7579,3 +7592,263 @@ class Connection(api.Connection): except NoResultFound: raise exception.KubeAppNotFound(name) query.delete() + + def _datanetwork_get(self, model_class, datanetwork_id, obj=None): + session = None + if obj: + session = inspect(obj).session + query = model_query(model_class, session=session) + + query = add_datanetwork_filter(query, datanetwork_id) + + try: + result = query.one() + except NoResultFound: + raise exception.DataNetworkNotFound( + datanetwork_uuid=datanetwork_id) + except MultipleResultsFound: + raise exception.InvalidParameterValue( + err="Multiple entries found for datanetwork %s" % datanetwork_id) + return result + + def _datanetwork_get_one(self, datanetwork_id, datanetwork=None): + entity = with_polymorphic(models.DataNetworks, '*') + query = model_query(entity) + query = add_datanetwork_filter(query, datanetwork_id) + if datanetwork is not None: + query = query.filter_by(network_type=datanetwork) + try: + result = query.one() + except NoResultFound: + raise exception.DataNetworkNotFound( + datanetwork_uuid=datanetwork_id) + except MultipleResultsFound: + raise exception.InvalidParameterValue( + err="Multiple entries found for datanetwork %s" % datanetwork_id) + + return result + + def _datanetwork_create(self, obj, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + with _session_for_write() as session: + # The id is null for ae interfaces with more than one member interface + temp_id = obj.id + obj.update(values) + if obj.id is None: + obj.id = temp_id + + try: + session.add(obj) + session.flush() + except db_exc.DBDuplicateEntry: + LOG.error("Failed to add datanetwork (uuid: %s), " + "name %s already exists." % + (values['uuid'], values.get('name'))) + + raise exception.DataNetworkAlreadyExists( + name=values.get('name')) + + return self._datanetwork_get(type(obj), values['uuid']) + + @objects.objectify(objects.datanetwork) + def datanetwork_create(self, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + network_type = values.get('network_type') + if network_type == constants.DATANETWORK_TYPE_FLAT: + datanetwork = models.DataNetworksFlat() + elif network_type == constants.DATANETWORK_TYPE_VLAN: + datanetwork = models.DataNetworksVlan() + elif network_type == constants.DATANETWORK_TYPE_VXLAN: + datanetwork = models.DataNetworksVXlan() + else: + raise exception.DataNetworkTypeUnsupported( + network_type=network_type) + return self._datanetwork_create(datanetwork, values) + + @objects.objectify(objects.datanetwork) + def datanetwork_get(self, datanetwork_id): + return self._datanetwork_get_one(datanetwork_id) + + def _add_datanetworks_filters(self, query, filters): + if filters is None: + filters = dict() + supported_filters = {'network_type', + 'name', + } + unsupported_filters = set(filters).difference(supported_filters) + if unsupported_filters: + msg = _("SqlAlchemy API does not support " + "filtering by %s") % ', '.join(unsupported_filters) + raise ValueError(msg) + + for field in supported_filters: + if field in filters: + query = query.filter_by(**{field: filters[field]}) + + return query + + @objects.objectify(objects.datanetwork) + def datanetworks_get_all(self, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + + with _session_for_read() as session: + datanetworks = with_polymorphic(models.DataNetworks, '*') + query = model_query(datanetworks, session=session) + query = self._add_datanetworks_filters(query, filters) + + return _paginate_query(models.DataNetworks, limit, marker, + sort_key, sort_dir, query) + + @objects.objectify(objects.datanetwork) + def datanetwork_update(self, datanetwork_uuid, values): + with _session_for_write() as session: + query = model_query(models.DataNetworks, session=session) + query = add_identity_filter(query, datanetwork_uuid) + + count = query.update(values, synchronize_session='fetch') + if count != 1: + raise exception.DataNetworkNotFound( + datanetwork_uuid=datanetwork_uuid) + return query.one() + + def datanetwork_destroy(self, datanetwork_uuid): + query = model_query(models.DataNetworks) + query = add_identity_filter(query, datanetwork_uuid) + try: + query.one() + except NoResultFound: + raise exception.DataNetworkNotFound( + datanetwork_uuid=datanetwork_uuid) + query.delete() + + def _interface_datanetwork_get(self, uuid, session=None): + query = model_query(models.InterfaceDataNetworks, session=session) + query = add_identity_filter(query, uuid) + try: + result = query.one() + except NoResultFound: + raise exception.InterfaceDataNetworkNotFound(uuid=uuid) + return result + + def _interface_datanetwork_get_all( + self, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.InterfaceDataNetworks) + return _paginate_query( + models.InterfaceDataNetworks, limit, marker, + sort_key, sort_dir, query) + + def _interface_datanetwork_get_by_host( + self, host_uuid, limit=None, marker=None, + sort_key=None, sort_dir=None): + + query = model_query(models.InterfaceDataNetworks) + query = (query. + join(models.Interfaces). + join(models.ihost, + models.ihost.id == models.Interfaces.forihostid)) + query, field = add_filter_by_many_identities( + query, models.ihost, [host_uuid]) + return _paginate_query( + models.InterfaceDataNetworks, limit, marker, + sort_key, sort_dir, query) + + def _interface_datanetwork_get_by_interface( + self, interface_uuid, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.InterfaceDataNetworks) + query = (query.join(models.Interfaces)) + query, field = add_filter_by_many_identities( + query, models.Interfaces, [interface_uuid]) + return _paginate_query(models.InterfaceDataNetworks, + limit, marker, sort_key, sort_dir, query) + + def _interface_datanetwork_get_by_datanetwork( + self, datanetwork_uuid, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.InterfaceDataNetworks) + query = (query.join(models.DataNetworks)) + query, field = add_filter_by_many_identities( + query, models.DataNetworks, [datanetwork_uuid]) + return _paginate_query(models.InterfaceDataNetworks, + limit, marker, sort_key, sort_dir, query) + + def _interface_datanetwork_query(self, values): + query = model_query(models.InterfaceDataNetworks) + query = (query. + filter(models.InterfaceDataNetworks.interface_id == + values['interface_id']). + filter(models.InterfaceDataNetworks.datanetwork_id == + values['datanetwork_id'])) + try: + result = query.one() + except NoResultFound: + raise exception.InterfaceDataNetworkNotFoundByKeys( + interface_id=values['interface_id'], + datanetwork_id=values['datanetwork_id']) + return result + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_create(self, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + interface_datanetwork = models.InterfaceDataNetworks(**values) + with _session_for_write() as session: + try: + session.add(interface_datanetwork) + session.flush() + except db_exc.DBDuplicateEntry: + raise exception.InterfaceDataNetworkAlreadyExists( + interface_id=values['interface_id'], + datanetwork_id=values['datanetwork_id']) + return self._interface_datanetwork_get(values['uuid'], session) + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_get(self, uuid): + return self._interface_datanetwork_get(uuid) + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_get_all( + self, limit=None, marker=None, + sort_key=None, sort_dir=None): + return self._interface_datanetwork_get_all( + limit, marker, sort_key, sort_dir) + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_get_by_host( + self, host_id, limit=None, marker=None, + sort_key=None, sort_dir=None): + return self._interface_datanetwork_get_by_host( + host_id, limit, marker, sort_key, sort_dir) + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_get_by_interface( + self, interface_id, limit=None, marker=None, + sort_key=None, sort_dir=None): + return self._interface_datanetwork_get_by_interface( + interface_id, limit, marker, sort_key, sort_dir) + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_get_by_datanetwork( + self, datanetwork_id, limit=None, marker=None, + sort_key=None, sort_dir=None): + return self._interface_datanetwork_get_by_datanetwork( + datanetwork_id, limit, marker, sort_key, sort_dir) + + def interface_datanetwork_destroy(self, uuid): + query = model_query(models.InterfaceDataNetworks) + query = add_identity_filter(query, uuid) + try: + query.one() + except NoResultFound: + raise exception.InterfaceDataNetworkNotFound(uuid=uuid) + query.delete() + + @objects.objectify(objects.interface_datanetwork) + def interface_datanetwork_query(self, values): + return self._interface_datanetwork_query(values) diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/084_data_networks.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/084_data_networks.py new file mode 100644 index 0000000000..93a0a74e70 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/084_data_networks.py @@ -0,0 +1,144 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sqlalchemy import Column, MetaData, Table +from sqlalchemy import DateTime, Integer, String +from sqlalchemy import ForeignKey, UniqueConstraint +from sysinv.common import constants + +ENGINE = 'InnoDB' +CHARSET = 'utf8' + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + Table('interfaces', meta, autoload=True) + + datanetworks = Table( + 'datanetworks', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + Column('name', String(255), unique=True), + Column('network_type', String(255)), + Column('description', String(255)), + Column('mtu', Integer, nullable=False), + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + datanetworks_flat = Table( + 'datanetworks_flat', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, + ForeignKey('datanetworks.id', ondelete="CASCADE"), + primary_key=True, nullable=False), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + datanetworks_vlan = Table( + 'datanetworks_vlan', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, + ForeignKey('datanetworks.id', ondelete="CASCADE"), + primary_key=True, nullable=False), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + datanetworks_vxlan = Table( + 'datanetworks_vxlan', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, + ForeignKey('datanetworks.id', ondelete="CASCADE"), + primary_key=True, nullable=False), + + Column('multicast_group', String(64), nullable=True), + Column('port_num', Integer, nullable=False), + Column('ttl', Integer, nullable=False), + Column('mode', String(32), nullable=False, + default=constants.DATANETWORK_MODE_DYNAMIC), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + interface_datanetworks = Table( + 'interface_datanetworks', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + + Column('interface_id', Integer, + ForeignKey('interfaces.id', ondelete='CASCADE')), + Column('datanetwork_id', Integer, + ForeignKey('datanetworks.id', ondelete='CASCADE')), + + UniqueConstraint('interface_id', 'datanetwork_id', + name='u_interface_id@datanetwork_id'), + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + tables = ( + datanetworks, + datanetworks_flat, + datanetworks_vlan, + datanetworks_vxlan, + interface_datanetworks, + ) + + for index, table in enumerate(tables): + try: + table.create() + except Exception: + # If an error occurs, drop all tables created so far to return + # to the previously existing state. + meta.drop_all(tables=tables[:index]) + raise + + ethernet_interfaces = Table('ethernet_interfaces', meta, autoload=True) + ethernet_interfaces.drop_column('providernetworks') + ethernet_interfaces.drop_column('providernetworksdict') + + ae_interfaces = Table('ae_interfaces', meta, autoload=True) + ae_interfaces.drop_column('providernetworks') + ae_interfaces.drop_column('providernetworksdict') + + vlan_interfaces = Table('vlan_interfaces', meta, autoload=True) + vlan_interfaces.drop_column('providernetworks') + vlan_interfaces.drop_column('providernetworksdict') + + virtual_interfaces = Table('virtual_interfaces', meta, autoload=True) + virtual_interfaces.drop_column('providernetworks') + virtual_interfaces.drop_column('providernetworksdict') + + +def downgrade(migrate_engine): + # As per other openstack components, downgrade is + # unsupported in this release. + raise NotImplementedError('SysInv database downgrade is unsupported.') diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py index 80f02b8053..5aa47d91b1 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py @@ -17,7 +17,8 @@ # # Copyright (c) 2013-2018 Wind River Systems, Inc. # - +# SPDX-License-Identifier: Apache-2.0 +# """ SQLAlchemy models for sysinv data. """ @@ -26,6 +27,7 @@ import json from six.moves.urllib.parse import urlparse from oslo_config import cfg +from oslo_db.sqlalchemy import models from sqlalchemy import Column, ForeignKey, Integer, BigInteger, Boolean from sqlalchemy import Enum, UniqueConstraint, String, Table, Text, Float @@ -35,7 +37,7 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.types import TypeDecorator, VARCHAR from sqlalchemy.orm import relationship, backref -from oslo_db.sqlalchemy import models +from sysinv.common import constants sql_opts = [ cfg.StrOpt('mysql_engine', @@ -321,32 +323,6 @@ class imemory(Base): UniqueConstraint('forihostid', 'forinodeid', name='u_hostnode') -class iinterface(Base): - __tablename__ = 'i_interface' - - id = Column(Integer, primary_key=True, nullable=False) - uuid = Column(String(36)) - - ifname = Column(String(255)) - iftype = Column(String(255)) - imac = Column(String(255), unique=True) - imtu = Column(Integer) - networktype = Column(String(255)) - aemode = Column(String(255)) # e.g. balanced, active_standby - aedict = Column(JSONEncodedDict) # e.g. 802.3ad parameters - txhashpolicy = Column(String(255)) # e.g. L2, L2L3, L3L4 - providernetworks = Column(String(255)) # ['physnet0','physnet1'] - providernetworksdict = Column(JSONEncodedDict) - schedpolicy = Column(String(255)) - ifcapabilities = Column(JSONEncodedDict) - sriov_numvfs = Column(Integer) - # JSON{'mode':"xor", 'bond':'false'} - - farend = Column(JSONEncodedDict) - forihostid = Column(Integer, ForeignKey('i_host.id', ondelete='CASCADE')) - UniqueConstraint('ifname', 'forihostid', name='u_ifnameihost') - - interfaces_to_interfaces = Table("interfaces_to_interfaces", Base.metadata, Column("used_by_id", Integer, ForeignKey("interfaces.id", ondelete='CASCADE'), primary_key=True), Column("uses_id", Integer, ForeignKey("interfaces.id", ondelete='CASCADE'), primary_key=True) @@ -408,8 +384,6 @@ class EthernetCommon(object): imac = Column(String(255)) imtu = Column(Integer) - providernetworks = Column(String(255)) # ['physnet0','physnet1'] - providernetworksdict = Column(JSONEncodedDict) class EthernetInterfaces(EthernetCommon, Interfaces): @@ -1180,6 +1154,85 @@ class InterfaceNetworks(Base): UniqueConstraint('interface_id', 'network_id', name='u_interface_id@network_id') +class DataNetworks(Base): + __tablename__ = 'datanetworks' + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(36), unique=True) + name = Column(String(255), unique=True) + network_type = Column(String(255)) + description = Column(String(255)) + mtu = Column(Integer) + + __mapper_args__ = { + 'polymorphic_identity': 'datanetwork', + 'polymorphic_on': network_type + } + + +class DataNetworksCommon(object): + @declared_attr + def id(cls): + return Column(Integer, + ForeignKey('datanetworks.id', ondelete="CASCADE"), + primary_key=True, nullable=False) + + +class DataNetworksFlat(DataNetworksCommon, DataNetworks): + __tablename__ = 'datanetworks_flat' + + __mapper_args__ = { + 'polymorphic_identity': 'flat', + } + + +class DataNetworksVlan(DataNetworksCommon, DataNetworks): + __tablename__ = 'datanetworks_vlan' + + __mapper_args__ = { + 'polymorphic_identity': 'vlan', + } + + +class DataNetworksVXlan(DataNetworksCommon, DataNetworks): + __tablename__ = 'datanetworks_vxlan' + + # IP address of the multicast group + multicast_group = Column(String(64), nullable=True) + + # Destination DP port for all instances + port_num = Column(Integer, nullable=False) + + # Time-to-live value for all instances + ttl = Column(Integer, nullable=False) + + # defines dynamic learning with multicast enable/disabled + mode = Column(String(32), nullable=False, + default=constants.DATANETWORK_MODE_DYNAMIC) + + __mapper_args__ = { + 'polymorphic_identity': 'vxlan', + } + + +class InterfaceDataNetworks(Base): + __tablename__ = 'interface_datanetworks' + + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(36), unique=True) + + interface_id = Column( + Integer, ForeignKey('interfaces.id', ondelete='CASCADE')) + datanetwork_id = Column( + Integer, ForeignKey('datanetworks.id', ondelete='CASCADE')) + + interface = relationship( + "Interfaces", lazy="joined", backref="interface_datanetworks") + datanetwork = relationship( + "DataNetworks", lazy="joined", backref="interface_datanetworks") + UniqueConstraint( + 'interface_id', 'datanetwork_id', name='u_interface_id@datanetwork_id') + + class SensorGroups(Base): __tablename__ = 'i_sensorgroups' diff --git a/sysinv/sysinv/sysinv/sysinv/helm/neutron.py b/sysinv/sysinv/sysinv/sysinv/helm/neutron.py index 3450fe7231..dbaef7ce1c 100644 --- a/sysinv/sysinv/sysinv/sysinv/helm/neutron.py +++ b/sysinv/sysinv/sysinv/sysinv/helm/neutron.py @@ -198,15 +198,17 @@ class NeutronHelm(openstack.OpenstackBaseHelm): # obtain the assigned bridge for interface brname = 'br-phy%d' % index if brname: - providernets = self._get_interface_providernets(iface) - for providernet in providernets: + datanets = self._get_interface_datanets(iface) + for datanet in datanets: + LOG.info("_get_dynamic_ovs_agent_config datanet %s" % + datanet) address = self._get_interface_primary_address( self.context, host, iface) if address: local_ip = address - tunnel_types = constants.NEUTRON_PROVIDERNET_VXLAN + tunnel_types = constants.DATANETWORK_TYPE_VXLAN else: - bridge_mappings += ('%s:%s,' % (providernet, brname)) + bridge_mappings += ('%s:%s,' % (datanet, brname)) index += 1 agent = {} @@ -223,6 +225,12 @@ class NeutronHelm(openstack.OpenstackBaseHelm): if bridge_mappings: ovs['bridge_mappings'] = str(bridge_mappings) + # https://access.redhat.com/documentation/en-us/ + # red_hat_enterprise_linux_openstack_platform/7/html/ + # networking_guide/bridge-mappings + # required for vlan, not flat, vxlan: + # ovs['network_vlan_ranges'] = physnet1:10:20,physnet2:21:25 + return { 'agent': agent, 'ovs': ovs, @@ -236,11 +244,11 @@ class NeutronHelm(openstack.OpenstackBaseHelm): for iface in sorted(self.dbapi.iinterface_get_by_ihost(host.id), key=self._interface_sort_key): if self._is_sriov_network_type(iface): - # obtain the assigned providernets for interface - providernets = self._get_interface_providernets(iface) + # obtain the assigned datanets for interface + datanets = self._get_interface_datanets(iface) port_name = self._get_interface_port_name(iface) - for providernet in providernets: - physical_device_mappings += ('%s:%s,' % (providernet, port_name)) + for datanet in datanets: + physical_device_mappings += ('%s:%s,' % (datanet, port_name)) sriov_nic = { 'physical_device_mappings': str(physical_device_mappings), } @@ -281,6 +289,9 @@ class NeutronHelm(openstack.OpenstackBaseHelm): 'policy_file': '/etc/neutron/policy.json', 'service_plugins': 'router', 'dns_domain': 'openstacklocal', + 'enable_new_agents': False, + 'allow_automatic_dhcp_failover': True, + 'allow_automatic_l3agent_failover': True, }, 'vhost': { 'vhost_user_enabled': True, @@ -292,6 +303,15 @@ class NeutronHelm(openstack.OpenstackBaseHelm): return neutron_config + def _get_ml2_physical_network_mtus(self): + ml2_physical_network_mtus = [] + datanetworks = self.dbapi.datanetworks_get_all() + for datanetwork in datanetworks: + dn_str = str(datanetwork.name) + ":" + str(datanetwork.mtu) + ml2_physical_network_mtus.append(dn_str) + + return ",".join(ml2_physical_network_mtus) + def _get_neutron_ml2_config(self): ml2_config = { 'ml2': { @@ -299,12 +319,14 @@ class NeutronHelm(openstack.OpenstackBaseHelm): 'tenant_network_types': 'vlan,vxlan', 'mechanism_drivers': 'openvswitch,sriovnicswitch,l2population', 'path_mtu': 0, + 'physical_network_mtus': self._get_ml2_physical_network_mtus() }, 'securitygroup': { 'firewall_driver': 'noop', }, } + LOG.info("_get_neutron_ml2_config=%s" % ml2_config) return ml2_config def _is_data_network_type(self, iface): @@ -315,14 +337,14 @@ class NeutronHelm(openstack.OpenstackBaseHelm): networktypelist = utils.get_network_type_list(iface) return bool(any(n in SRIOV_NETWORK_TYPES for n in networktypelist)) - def _get_interface_providernets(self, iface): + def _get_interface_datanets(self, iface): """ - Return the provider networks of the supplied interface as a list. + Return the data networks of the supplied interface as a list. """ - providernetworks = iface['providernetworks'] - if not providernetworks: - return [] - return [x.strip() for x in providernetworks.split(',')] + + ifdatanets = self.dbapi.interface_datanetwork_get_by_interface( + iface.uuid) + return [ifdn['datanetwork_name'].strip() for ifdn in ifdatanets] def _get_interface_port_name(self, iface): """ diff --git a/sysinv/sysinv/sysinv/sysinv/objects/__init__.py b/sysinv/sysinv/sysinv/sysinv/objects/__init__.py index 5003794329..a7b754799b 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/__init__.py @@ -27,6 +27,7 @@ from sysinv.objects import cluster from sysinv.objects import community from sysinv.objects import controller_fs from sysinv.objects import cpu +from sysinv.objects import datanetwork from sysinv.objects import disk from sysinv.objects import firewallrules from sysinv.objects import partition @@ -41,6 +42,7 @@ from sysinv.objects import network_infra from sysinv.objects import interface from sysinv.objects import interface_ae from sysinv.objects import interface_ethernet +from sysinv.objects import interface_datanetwork from sysinv.objects import interface_network from sysinv.objects import interface_virtual from sysinv.objects import interface_vlan @@ -128,6 +130,7 @@ ae_interface = interface_ae.AEInterface virtual_interface = interface_virtual.VirtualInterface vlan_interface = interface_vlan.VLANInterface interface_network = interface_network.InterfaceNetwork +interface_datanetwork = interface_datanetwork.InterfaceDataNetwork port = port.Port ethernet_port = port_ethernet.EthernetPort disk = disk.Disk @@ -183,6 +186,7 @@ storage_ceph_external = storage_ceph_external.StorageCephExternal helm_overrides = helm_overrides.HelmOverrides label = label.Label kube_app = kube_app.KubeApp +datanetwork = datanetwork.DataNetwork __all__ = (system, cluster, @@ -251,6 +255,8 @@ __all__ = (system, storage_ceph_external, helm_overrides, kube_app, + datanetwork, + interface_network, # alias objects for RPC compatibility ihost, ilvg, diff --git a/sysinv/sysinv/sysinv/sysinv/objects/datanetwork.py b/sysinv/sysinv/sysinv/sysinv/objects/datanetwork.py new file mode 100644 index 0000000000..0d61839ed5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/datanetwork.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class DataNetwork(base.SysinvObject): + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = {'id': int, + 'uuid': utils.uuid_or_none, + 'network_type': utils.str_or_none, + 'name': utils.str_or_none, + 'description': utils.str_or_none, + 'mtu': utils.int_or_none, + 'multicast_group': utils.str_or_none, + 'port_num': utils.int_or_none, + 'ttl': utils.int_or_none, + 'mode': utils.str_or_none, + } + + _optional_fields = {'port_num', + 'multicast_group', + 'ttl', + 'mode'} + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.datanetwork_get(uuid) + + def save_changes(self, context, updates): + self.dbapi.datanetwork_update(self.uuid, updates) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/interface.py b/sysinv/sysinv/sysinv/sysinv/objects/interface.py index 26855361d1..1db20e1727 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/interface.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/interface.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2016 Wind River Systems, Inc. +# Copyright (c) 2013-2019 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -88,6 +88,15 @@ def get_networks(field, db_object): return result +def get_datanetworks(field, db_object): + result = [] + if hasattr(db_object, 'interface_datanetworks'): + for entry in getattr(db_object, 'interface_datanetworks', []): + id_str = str(entry.datanetwork_id) + result.append(id_str) + return result + + class Interface(base.SysinvObject): # VERSION 1.0: Initial version # VERSION 1.1: Added VLAN and uses/used_by interface support @@ -110,9 +119,8 @@ class Interface(base.SysinvObject): 'aemode': utils.str_or_none, 'schedpolicy': utils.str_or_none, 'txhashpolicy': utils.str_or_none, - 'providernetworks': utils.str_or_none, - 'providernetworksdict': utils.dict_or_none, 'networks': utils.list_of_strings_or_none, + 'datanetworks': utils.list_of_strings_or_none, 'ifcapabilities': utils.dict_or_none, @@ -136,7 +144,8 @@ class Interface(base.SysinvObject): 'ipv4_pool': get_ipv4_address_pool, 'ipv6_pool': get_ipv6_address_pool, 'ihost_uuid': get_host_uuid, - 'networks': get_networks} + 'networks': get_networks, + 'datanetworks': get_datanetworks} _optional_fields = ['aemode', 'txhashpolicy', 'schedpolicy', 'vlan_id', 'vlan_type'] diff --git a/sysinv/sysinv/sysinv/sysinv/objects/interface_datanetwork.py b/sysinv/sysinv/sysinv/sysinv/objects/interface_datanetwork.py new file mode 100644 index 0000000000..5fc8a85936 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/interface_datanetwork.py @@ -0,0 +1,110 @@ +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + +from oslo_log import log + +LOG = log.getLogger(__name__) + + +def _get_mtu(field, db_object): + mtu = None + datanetwork = getattr(db_object, 'datanetwork', None) + if hasattr(datanetwork, 'mtu'): + mtu = datanetwork.mtu + return mtu + + +def _get_multicast_group(field, db_object): + multicast_group = None + datanetwork = getattr(db_object, 'datanetwork', None) + if hasattr(datanetwork, 'multicast_group'): + multicast_group = datanetwork.multicast_group + return multicast_group + + +def _get_port_num(field, db_object): + port_num = None + datanetwork = getattr(db_object, 'datanetwork', None) + if hasattr(datanetwork, 'port_num'): + port_num = datanetwork.port_num + return port_num + + +def _get_ttl(field, db_object): + ttl = None + datanetwork = getattr(db_object, 'datanetwork', None) + if hasattr(datanetwork, 'ttl'): + ttl = datanetwork.ttl + return ttl + + +def _get_mode(field, db_object): + mode = None + datanetwork = getattr(db_object, 'datanetwork', None) + if hasattr(datanetwork, 'mode'): + mode = datanetwork.mode + return mode + + +class InterfaceDataNetwork(base.SysinvObject): + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': int, + 'uuid': utils.uuid_or_none, + 'forihostid': utils.int_or_none, + 'interface_id': utils.int_or_none, + 'interface_uuid': utils.uuid_or_none, + 'ifname': utils.str_or_none, + 'datanetwork_id': utils.int_or_none, + 'datanetwork_uuid': utils.uuid_or_none, + 'datanetwork_name': utils.str_or_none, + 'datanetwork_network_type': utils.str_or_none, + 'datanetwork_description': utils.str_or_none, + 'datanetwork_mtu': utils.int_or_none, + 'datanetwork_port_num': utils.int_or_none, + 'datanetwork_multicast_group': utils.str_or_none, + 'datanetwork_ttl': utils.int_or_none, + 'datanetwork_mode': utils.str_or_none, + } + + _foreign_fields = { + 'forihostid': 'interface:forihostid', + 'interface_id': 'interface:id', + 'interface_uuid': 'interface:uuid', + 'ifname': 'interface:ifname', + 'datanetwork_uuid': 'datanetwork:uuid', + 'datanetwork_id': 'datanetwork:id', + 'datanetwork_name': 'datanetwork:name', + 'datanetwork_network_type': 'datanetwork:network_type', + 'datanetwork_description': 'datanetwork:description', + 'datanetwork_mtu': _get_mtu, + 'datanetwork_port_num': _get_port_num, + 'datanetwork_multicast_group': _get_multicast_group, + 'datanetwork_ttl': _get_ttl, + 'datanetwork_mode': _get_mode, + } + + _optional_fields = { + 'datanetwork_port_num', + 'datanetwork_multicast_group', + 'datanetwork_ttl', + 'datanetwork_mode', + } + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.interface_datanetwork_get(uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/interface_ethernet.py b/sysinv/sysinv/sysinv/sysinv/objects/interface_ethernet.py index 257ae4ed5f..470fedd72d 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/interface_ethernet.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/interface_ethernet.py @@ -18,8 +18,6 @@ class EthernetInterface(interface_base.InterfaceBase): fields = dict({ 'imtu': utils.int_or_none, 'imac': utils.str_or_none, - 'providernetworks': utils.str_or_none, - 'providernetworksdict': utils.dict_or_none, }, **interface_base.InterfaceBase.fields) @base.remotable_classmethod diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/interface.py b/sysinv/sysinv/sysinv/sysinv/puppet/interface.py index e33362d516..2af759f40b 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/interface.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/interface.py @@ -122,13 +122,14 @@ class InterfacePuppet(base.BasePuppet): 'system_mode': self._get_system().system_mode, 'ports': self._get_port_interface_id_index(host), 'interfaces': self._get_interface_name_index(host), + 'interfaces_datanets': self._get_interface_name_datanets(host), 'devices': self._get_port_pciaddr_index(host), 'addresses': self._get_address_interface_name_index(host), 'routes': self._get_routes_interface_name_index(host), 'networks': self._get_network_type_index(), 'gateways': self._get_gateway_index(), 'floatingips': self._get_floating_ip_index(), - 'providernets': self._get_provider_networks(host), + 'datanets': self._get_datanetworks(host), } return context @@ -160,6 +161,41 @@ class InterfacePuppet(base.BasePuppet): interfaces = {} for iface in self.dbapi.iinterface_get_by_ihost(host.id): interfaces[iface.ifname] = iface + + return interfaces + + def _get_interface_name_datanets(self, host): + """ + Builds a dictionary of datanets indexed by interface name. + """ + interfaces = {} + for iface in self.dbapi.iinterface_get_by_ihost(host.id): + ifdatanets = self.dbapi.interface_datanetwork_get_by_interface( + iface.uuid) + + datanetworks = [] + for ifdatanet in ifdatanets: + datanetworks.append(ifdatanet.datanetwork_uuid) + + datanetworks_list = [] + for datanetwork in datanetworks: + dn = self.dbapi.datanetwork_get(datanetwork) + datanetwork_dict = \ + {'name': dn.name, + 'uuid': dn.uuid, + 'network_type': dn.network_type, + 'mtu': dn.mtu} + if dn.network_type == constants.DATANETWORK_TYPE_VXLAN: + datanetwork_dict.update( + {'multicast_group': dn.multicast_group, + 'port_num': dn.port_num, + 'ttl': dn.ttl, + 'mode': dn.mode}) + datanetworks_list.append(datanetwork_dict) + interfaces[iface.ifname] = datanetworks_list + + LOG.debug("_get_interface_name_datanets ifdatanet=%s" % interfaces) + return interfaces def _get_port_pciaddr_index(self, host): @@ -277,17 +313,11 @@ class InterfacePuppet(base.BasePuppet): return floating_ips - def _get_provider_networks(self, host): - # TODO(alegacy): this will not work as intended for upgrades of AIO-SX - # and -DX. The call to get_providernetworksdict will return an empty - # dictionary because the neutron endpoint is not available yet. Since - # we do not currently support SDN/OVS over upgrades we will need to - # deal with this in a later commit. - pnets = {} - if (self.openstack and - constants.WORKER in utils.get_personalities(host)): - pnets = self.openstack.get_providernetworksdict(quiet=True) - return pnets + def _get_datanetworks(self, host): + dnets = {} + if constants.WORKER in utils.get_personalities(host): + dnets = self.dbapi.datanetworks_get_all() + return dnets def is_platform_network_type(iface): @@ -461,14 +491,11 @@ def get_interface_mtu(context, iface): return iface['imtu'] -def get_interface_providernets(iface): +def get_interface_datanets(context, iface): """ - Return the provider networks of the supplied interface as a list. + Return the list of data networks of the supplied interface """ - providernetworks = iface['providernetworks'] - if not providernetworks: - return [] - return [x.strip() for x in providernetworks.split(',')] + return context['interfaces_datanets'][iface.ifname] def get_interface_port(context, iface): diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/neutron.py b/sysinv/sysinv/sysinv/sysinv/puppet/neutron.py index 15ee1e3999..ddd3cb5338 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/neutron.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/neutron.py @@ -10,6 +10,9 @@ from sysinv.common import utils from sysinv.puppet import interface from sysinv.puppet import openstack +from oslo_log import log +LOG = log.getLogger(__name__) + class NeutronPuppet(openstack.OpenstackBasePuppet): """Class to encapsulate puppet operations for neutron configuration""" @@ -164,9 +167,14 @@ class NeutronPuppet(openstack.OpenstackBasePuppet): for iface in self.context['interfaces'].values(): if (iface['ifclass'] in [constants.INTERFACE_CLASS_PCI_SRIOV]): port = interface.get_interface_port(self.context, iface) - providernets = interface.get_interface_providernets(iface) - for net in providernets: - device_mappings.append("%s:%s" % (net, port['name'])) + + datanets = interface.get_interface_datanets( + self.context, iface) + for dnet in datanets: + device_mappings.append( + "%s:%s" % (dnet['name'], port['name'])) + LOG.debug("get_host_config device_mappings=%s" % + device_mappings) config = { 'neutron::agents::ml2::sriov::physical_device_mappings': diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/nova.py b/sysinv/sysinv/sysinv/sysinv/puppet/nova.py index 90ca787825..80412b25c4 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/nova.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/nova.py @@ -17,6 +17,9 @@ from sysinv.common import utils from sysinv.puppet import openstack from sysinv.puppet import interface +from oslo_log import log +LOG = log.getLogger(__name__) + SCHEDULER_FILTERS_COMMON = [ 'RetryFilter', @@ -580,6 +583,13 @@ class NovaPuppet(openstack.OpenstackBasePuppet): return "\"%s\"" % ','.join( "%r:%r" % (node, cpu) for node, cpu in cpu_map.items()) + def _get_datanetwork_names(self, iface): + dnets = interface.get_interface_datanets( + self.context, iface) + dnames_list = [dnet['name'] for dnet in dnets] + dnames = ",".join(dnames_list) + return dnames + def _get_pci_pt_whitelist(self, host): # Process all configured PCI passthrough interfaces and add them to # the list of devices to whitelist @@ -587,10 +597,13 @@ class NovaPuppet(openstack.OpenstackBasePuppet): for iface in self.context['interfaces'].values(): if iface['ifclass'] in [constants.INTERFACE_CLASS_PCI_PASSTHROUGH]: port = interface.get_interface_port(self.context, iface) + + dnames = self._get_datanetwork_names(iface) device = { 'address': port['pciaddr'], - 'physical_network': iface['providernetworks'] + 'physical_network': dnames } + LOG.debug("_get_pci_pt_whitelist device=%s" % device) devices.append(device) # Process all enabled PCI devices configured for PT and SRIOV and @@ -616,11 +629,13 @@ class NovaPuppet(openstack.OpenstackBasePuppet): for iface in self.context['interfaces'].values(): if iface['ifclass'] in [constants.INTERFACE_CLASS_PCI_SRIOV]: port = interface.get_interface_port(self.context, iface) + dnames = self._get_datanetwork_names(iface) device = { 'address': port['pciaddr'], - 'physical_network': iface['providernetworks'], + 'physical_network': dnames, 'sriov_numvfs': iface['sriov_numvfs'] } + LOG.info("_get_pci_sriov_whitelist device=%s" % device) devices.append(device) return json.dumps(devices) if devices else None diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/ovs.py b/sysinv/sysinv/sysinv/sysinv/puppet/ovs.py index ced5410d35..4f932ec4e7 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/ovs.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/ovs.py @@ -4,12 +4,15 @@ # SPDX-License-Identifier: Apache-2.0 # +from oslo_log import log from sysinv.common import constants from sysinv.common import utils from sysinv.puppet import base from sysinv.puppet import interface +LOG = log.getLogger(__name__) + class OVSPuppet(base.BasePuppet): """Class to encapsulate puppet operations for vswitch configuration""" @@ -90,12 +93,10 @@ class OVSPuppet(base.BasePuppet): index += 1 - # currently only one provider network is supported per - # interface, therefore obtain first entry - providernet = interface.get_interface_providernets(iface)[0] + datanets = interface.get_interface_datanets(self.context, iface) # setup tunnel address if assigned provider network is vxlan - if self._is_vxlan_providernet(providernet): + if datanets and self._is_vxlan_datanet(datanets[0]): address = interface.get_interface_primary_address( self.context, iface) if address: @@ -105,7 +106,7 @@ class OVSPuppet(base.BasePuppet): 'prefixlen': address['prefix'], } - return { + ovs_dict = { 'platform::vswitch::ovs::devices': ovs_devices, 'platform::vswitch::ovs::bridges': ovs_bridges, 'platform::vswitch::ovs::ports': ovs_ports, @@ -113,6 +114,10 @@ class OVSPuppet(base.BasePuppet): 'platform::vswitch::ovs::flows': ovs_flows, } + LOG.debug("_get_port_config=%s" % ovs_dict) + + return ovs_dict + def _get_ethernet_device(self, iface): if interface.is_a_mellanox_device(self.context, iface): # Mellanox devices are not bound to the DPDK driver @@ -370,6 +375,9 @@ class OVSPuppet(base.BasePuppet): }) return config + def _is_vxlan_datanet(self, datanet): + return datanet.get('network_type') == constants.DATANETWORK_TYPE_VXLAN + def _get_neutron_config(self, host): local_ip = None tunnel_types = set() @@ -379,24 +387,29 @@ class OVSPuppet(base.BasePuppet): # obtain the assigned bridge for interface brname = iface.get('_ovs_bridge') if brname: - providernets = interface.get_interface_providernets(iface) - for providernet in providernets: - if self._is_vxlan_providernet(providernet): - address = interface.get_interface_primary_address( - self.context, iface) + datanets = interface.get_interface_datanets( + self.context, iface) + for datanet in datanets: + if self._is_vxlan_datanet(datanet): + address = \ + interface.get_interface_primary_address( + self.context, iface) if address: local_ip = address['address'] tunnel_types.add( - constants.NEUTRON_PROVIDERNET_VXLAN) + constants.DATANETWORK_TYPE_VXLAN) else: bridge_mappings.append('%s:%s' % - (providernet, brname)) + (datanet['name'], brname)) - return { + neutron_dict = { 'neutron::agents::ml2::ovs::local_ip': local_ip, 'neutron::agents::ml2::ovs::tunnel_types': list(tunnel_types), 'neutron::agents::ml2::ovs::bridge_mappings': bridge_mappings } + LOG.debug("OVS get_neutron_config neutron_dict=%s" % neutron_dict) + + return neutron_dict def _get_providernet_type(self, name): if name in self.context['providernets']: diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_interface.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_interface.py index 2313cbe2d0..0256a69d0b 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/api/test_interface.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_interface.py @@ -12,6 +12,7 @@ Tests for the API /interfaces/ methods. """ import mock +import testtools from six.moves import http_client from sysinv.api.controllers.v1 import interface as api_if_v1 @@ -107,7 +108,7 @@ providernet_list = { "port": 8472, "ttl": 10}}], "vlan_transparent": False, "type": "vxlan", - "id": "da9f7bb1-2114-4ffd-8a4c-9ca215d98fa2", + "id": "da9f7bb1-2114-4ffd-8a4c-9ca215d98fa4", "name": "group0-ext2"}, 'group0-ext3': { "status": "ACTIVE", "description": None, @@ -121,7 +122,7 @@ providernet_list = { "port": 8472, "ttl": 10}}], "vlan_transparent": False, "type": "vxlan", - "id": "da9f7bb1-2114-4ffd-8a4c-9ca215d98fa2", + "id": "da9f7bb1-2114-4ffd-8a4c-9ca215d98fa5", "name": "group0-ext3"}, 'group0-flat': { "status": "ACTIVE", "description": None, @@ -130,13 +131,12 @@ providernet_list = { "id": "72f21b11-6d17-486e-a4e6-4eaf5f00f23e", "name": "group0-flat-r0-0", "tenant_id": None, "maximum": 4, - "tenant_id": None, "maximum": 4, "shared": True, "vxlan": {"group": "239.0.2.1", "port": 8472, "ttl": 10}}], "vlan_transparent": False, "type": "flat", - "id": "da9f7bb1-2114-4ffd-8a4c-9ca215d98fa3", + "id": "da9f7bb1-2114-4ffd-8a4c-9ca215d98fa6", "name": "group0-flat"} } @@ -261,8 +261,26 @@ class InterfaceTestCase(base.FunctionalTest): self.worker = host return + def _create_datanetworks(self): + for name, v in providernet_list.items(): + dn_values = { + 'name': name, + 'uuid': v.get('id', None), + 'network_type': v['type'], + 'mtu': v['mtu']} + if v['type'] == constants.DATANETWORK_TYPE_VXLAN: + for r in v['ranges']: + dn_values.update( + {'multicast_group': r['vxlan'].get('group'), + 'port_num': r['vxlan'].get('port'), + 'ttl': r['vxlan'].get('ttl'), + 'mode': r['vxlan'].get('mode', 'dynamic'), + }) + + dbutils.create_test_datanetwork(**dn_values) + def _create_ethernet(self, ifname=None, networktype=None, ifclass=None, - providernetworks=None, host=None, expect_errors=False): + datanetworks=None, host=None, expect_errors=False): if not isinstance(networktype, list): networktypelist = [networktype] else: @@ -308,7 +326,7 @@ class InterfaceTestCase(base.FunctionalTest): ifclass=ifclass, networktype=networktype, networks=networks, - providernetworks=providernetworks, + datanetworks=datanetworks, forihostid=host.id, ihost_uuid=host.uuid) response = self._post_and_check(interface, expect_errors) @@ -322,7 +340,7 @@ class InterfaceTestCase(base.FunctionalTest): return port, interface def _create_bond(self, ifname, networktype=None, ifclass=None, - providernetworks=None, host=None, expect_errors=False): + datanetworks=None, host=None, expect_errors=False): if not isinstance(networktype, list): networktypelist = [networktype] else: @@ -357,7 +375,7 @@ class InterfaceTestCase(base.FunctionalTest): networks=networks, uses=[iface1['ifname'], iface2['ifname']], txhashpolicy='layer2', - providernetworks=providernetworks, + datanetworks=datanetworks, forihostid=host.id, ihost_uuid=host.uuid) lacp_types = [constants.NETWORK_TYPE_MGMT, @@ -378,12 +396,12 @@ class InterfaceTestCase(base.FunctionalTest): return interface def _create_worker_bond(self, ifname, networktype=None, ifclass=None, - providernetworks=None, expect_errors=False): - return self._create_bond(ifname, networktype, ifclass, providernetworks, + datanetworks=None, expect_errors=False): + return self._create_bond(ifname, networktype, ifclass, datanetworks, self.worker, expect_errors) def _create_vlan(self, ifname, networktype, ifclass, vlan_id, - lower_iface=None, providernetworks=None, host=None, + lower_iface=None, datanetworks=None, host=None, expect_errors=False): if not isinstance(networktype, list): networktypelist = [networktype] @@ -417,7 +435,7 @@ class InterfaceTestCase(base.FunctionalTest): networks=networks, vlan_id=vlan_id, uses=[lower_iface['ifname']], - providernetworks=providernetworks, + datanetworks=datanetworks, forihostid=host.id, ihost_uuid=host.uuid) self._post_and_check(interface, expect_errors) @@ -425,11 +443,11 @@ class InterfaceTestCase(base.FunctionalTest): return interface def _create_worker_vlan(self, ifname, networktype, ifclass, vlan_id, - lower_iface=None, providernetworks=None, + lower_iface=None, datanetworks=None, host=None, expect_errors=False): return self._create_vlan(ifname, networktype, ifclass, vlan_id, lower_iface, - providernetworks, self.worker, expect_errors) + datanetworks, self.worker, expect_errors) def _post_and_check_success(self, ndict): response = self.post_json('%s' % self._get_path(), ndict) @@ -585,6 +603,7 @@ class InterfaceComputeEthernet(InterfaceTestCase): # Setup a sample configuration where the personality is set to a # worker and all interfaces are ethernet interfaces. self._create_host(constants.CONTROLLER, admin=constants.ADMIN_UNLOCKED) + self._create_datanetworks() self._create_ethernet('oam', constants.NETWORK_TYPE_OAM) self._create_ethernet('mgmt', constants.NETWORK_TYPE_MGMT) self._create_ethernet('infra', constants.NETWORK_TYPE_INFRA) @@ -642,6 +661,7 @@ class InterfaceComputeVlanOverEthernet(InterfaceTestCase): # controller and all interfaces are vlan interfaces over ethernet # interfaces. self._create_host(constants.CONTROLLER) + self._create_datanetworks() port, iface = self._create_ethernet( 'pxeboot', constants.NETWORK_TYPE_PXEBOOT) self._create_vlan('oam', constants.NETWORK_TYPE_OAM, @@ -663,7 +683,7 @@ class InterfaceComputeVlanOverEthernet(InterfaceTestCase): constants.INTERFACE_CLASS_PLATFORM, 3) self._create_worker_vlan('data', constants.INTERFACE_CLASS_DATA, constants.NETWORK_TYPE_DATA, 5, - providernetworks='group0-ext0') + datanetworks='group0-ext0') self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, constants.INTERFACE_CLASS_PCI_SRIOV, @@ -686,6 +706,7 @@ class InterfaceComputeBond(InterfaceTestCase): # Setup a sample configuration where all platform interfaces are # aggregated ethernet interfaces. self._create_host(constants.CONTROLLER, admin=constants.ADMIN_UNLOCKED) + self._create_datanetworks() self._create_bond('oam', constants.NETWORK_TYPE_OAM) self._create_bond('mgmt', constants.NETWORK_TYPE_MGMT) self._create_bond('infra', constants.NETWORK_TYPE_INFRA) @@ -698,7 +719,7 @@ class InterfaceComputeBond(InterfaceTestCase): self._create_worker_bond('data', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0') + datanetworks='group0-data0') self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, constants.INTERFACE_CLASS_PCI_SRIOV, @@ -719,6 +740,7 @@ class InterfaceComputeVlanOverBond(InterfaceTestCase): def _setup_configuration(self): self._create_host(constants.CONTROLLER) + self._create_datanetworks() bond = self._create_bond('pxeboot', constants.NETWORK_TYPE_PXEBOOT, constants.INTERFACE_CLASS_PLATFORM) self._create_vlan('oam', constants.NETWORK_TYPE_OAM, @@ -745,7 +767,7 @@ class InterfaceComputeVlanOverBond(InterfaceTestCase): constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 5, bond2, - providernetworks='group0-ext0') + datanetworks='group0-ext0') self._create_worker_bond('bond3', constants.NETWORK_TYPE_NONE) @@ -769,6 +791,7 @@ class InterfaceComputeVlanOverDataEthernet(InterfaceTestCase): def _setup_configuration(self): self._create_host(constants.CONTROLLER) + self._create_datanetworks() bond = self._create_bond('pxeboot', constants.NETWORK_TYPE_PXEBOOT) self._create_vlan('oam', constants.NETWORK_TYPE_OAM, constants.INTERFACE_CLASS_PLATFORM, 1, bond) @@ -790,7 +813,7 @@ class InterfaceComputeVlanOverDataEthernet(InterfaceTestCase): host=self.worker) self._create_worker_vlan('data2', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 5, - iface, providernetworks='group0-ext0') + iface, datanetworks='group0-ext0') self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, constants.INTERFACE_CLASS_PCI_SRIOV, @@ -815,6 +838,7 @@ class InterfaceCpeEthernet(InterfaceTestCase): # ethernet interfaces. self._create_host(constants.CONTROLLER, constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() self._create_ethernet('oam', constants.NETWORK_TYPE_OAM) self._create_ethernet('mgmt', constants.NETWORK_TYPE_MGMT) self._create_ethernet('infra', constants.NETWORK_TYPE_INFRA) @@ -857,6 +881,7 @@ class InterfaceCpeVlanOverEthernet(InterfaceTestCase): # vlan interfaces over ethernet interfaces. self._create_host(constants.CONTROLLER, constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() port, iface = self._create_ethernet( 'pxeboot', constants.NETWORK_TYPE_PXEBOOT) self._create_vlan('oam', constants.NETWORK_TYPE_OAM, @@ -867,7 +892,7 @@ class InterfaceCpeVlanOverEthernet(InterfaceTestCase): constants.INTERFACE_CLASS_PLATFORM, 3) self._create_ethernet('data', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, - providernetworks='group0-ext0') + datanetworks='group0-ext0') self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, constants.INTERFACE_CLASS_PCI_SRIOV, 'group0-ext1') @@ -891,18 +916,19 @@ class InterfaceCpeBond(InterfaceTestCase): self._create_host(constants.CONTROLLER, subfunction=constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() self._create_bond('oam', constants.NETWORK_TYPE_OAM) self._create_bond('mgmt', constants.NETWORK_TYPE_MGMT) self._create_bond('infra', constants.NETWORK_TYPE_INFRA) self._create_bond('data', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0') + datanetworks='group0-data0') self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, constants.INTERFACE_CLASS_PCI_SRIOV, - providernetworks='group0-ext0') + datanetworks='group0-ext0') self._create_ethernet('pthru', constants.NETWORK_TYPE_PCI_PASSTHROUGH, constants.INTERFACE_CLASS_PCI_PASSTHROUGH, - providernetworks='group0-ext1') + datanetworks='group0-ext1') def setUp(self): super(InterfaceCpeBond, self).setUp() @@ -919,6 +945,7 @@ class InterfaceCpeVlanOverBond(InterfaceTestCase): # vlan interfaces over aggregated ethernet interfaces. self._create_host(constants.CONTROLLER, constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() bond = self._create_bond('pxeboot', constants.NETWORK_TYPE_PXEBOOT) self._create_vlan('oam', constants.NETWORK_TYPE_OAM, constants.INTERFACE_CLASS_PLATFORM, 1, bond) @@ -930,7 +957,7 @@ class InterfaceCpeVlanOverBond(InterfaceTestCase): self._create_vlan('data', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 5, bond2, - providernetworks='group0-ext0') + datanetworks='group0-ext0') self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, constants.INTERFACE_CLASS_PCI_SRIOV, 'group0-ext1') @@ -954,6 +981,7 @@ class InterfaceCpeVlanOverDataEthernet(InterfaceTestCase): # vlan interfaces over data ethernet interfaces. self._create_host(constants.CONTROLLER, constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() port, iface = ( self._create_ethernet('data', constants.NETWORK_TYPE_DATA, @@ -971,15 +999,15 @@ class InterfaceCpeVlanOverDataEthernet(InterfaceTestCase): self._create_vlan('data2', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 5, iface, - providernetworks='group0-ext0', + datanetworks='group0-ext0', expect_errors=False) self._create_ethernet('sriov', constants.NETWORK_TYPE_PCI_SRIOV, ifclass=constants.INTERFACE_CLASS_PCI_SRIOV, - providernetworks='group0-ext1', + datanetworks='group0-ext1', expect_errors=False) self._create_ethernet('pthru', constants.NETWORK_TYPE_PCI_PASSTHROUGH, ifclass=constants.INTERFACE_CLASS_PCI_PASSTHROUGH, - providernetworks='group0-ext2', + datanetworks='group0-ext2', expect_errors=False) def setUp(self): @@ -1008,6 +1036,7 @@ class TestPatch(InterfaceTestCase): super(TestPatch, self).setUp() self._create_host(constants.CONTROLLER) self._create_host(constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() def test_modify_ifname(self): interface = dbutils.create_test_interface(forihostid='1') @@ -1030,7 +1059,7 @@ class TestPatch(InterfaceTestCase): def test_interface_usesmodify_success(self): data_bond = self._create_bond('data', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0', + datanetworks='group0-data0', host=self.worker) port, new_ethernet = self._create_ethernet( @@ -1055,7 +1084,7 @@ class TestPatch(InterfaceTestCase): networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, iftype=constants.INTERFACE_TYPE_ETHERNET, - providernetworks='group0-data0', + datanetworks='group0-data0', aemode='balanced', txhashpolicy='layer2', uses=['pxeboot'], @@ -1080,7 +1109,7 @@ class TestPatch(InterfaceTestCase): ifclass=constants.INTERFACE_CLASS_DATA, iftype=constants.INTERFACE_TYPE_VLAN, vlan_id=100, - providernetworks='group0-ext0', + datanetworks='group0-ext0', aemode='balanced', txhashpolicy='layer2', uses=['pxeboot'], @@ -1111,6 +1140,7 @@ class TestPost(InterfaceTestCase): super(TestPost, self).setUp() self._create_host(constants.CONTROLLER) self._create_host(constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() # Expected error: The oam network type is only supported on controller nodes def test_invalid_oam_on_worker(self): @@ -1130,28 +1160,28 @@ class TestPost(InterfaceTestCase): def test_invalid_network_type_on_nonworker(self): self._create_ethernet('data0', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-ext0', + datanetworks='group0-ext0', expect_errors=True) # Expected error: Interface name cannot be whitespace. def test_invalid_whitespace_interface_name(self): self._create_ethernet(' ', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-ext0', + datanetworks='group0-ext0', expect_errors=True) # Expected error: Interface name must be in lower case. def test_invalid_uppercase_interface_name(self): self._create_ethernet('miXedCaSe', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-ext0', + datanetworks='group0-ext0', expect_errors=True) # Expected error: Cannot use special characters in interface name. def test_invalid_character_interface_name(self): self._create_ethernet('bad-name', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-ext0', + datanetworks='group0-ext0', expect_errors=True) # Expected error: Interface ___ has name length greater than 10. @@ -1163,11 +1193,11 @@ class TestPost(InterfaceTestCase): def test_create_duplicate_interface_name(self): self._create_ethernet('data0', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0', + datanetworks='group0-data0', host=self.worker) self._create_ethernet('data0', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-ext0', + datanetworks='group0-ext0', host=self.worker, expect_errors=True) @@ -1303,7 +1333,7 @@ class TestPost(InterfaceTestCase): def test_aemode_invalid_iftype(self): ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-data0', + datanetworks='group0-data0', ifname='name', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1317,7 +1347,7 @@ class TestPost(InterfaceTestCase): def test_aemode_no_txhash(self): ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-data0', + datanetworks='group0-data0', ifname='name', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1344,7 +1374,7 @@ class TestPost(InterfaceTestCase): def test_aemode_invalid_txhash_none(self): ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-data0', + datanetworks='group0-data0', ifname='name', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1355,7 +1385,7 @@ class TestPost(InterfaceTestCase): ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-data0', + datanetworks='group0-data0', ifname='name', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1369,7 +1399,7 @@ class TestPost(InterfaceTestCase): def test_aemode_invalid_mgmt(self): ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-data0', + datanetworks='group0-data0', ifname='name', networktype=constants.NETWORK_TYPE_MGMT, networks=['1'], @@ -1385,7 +1415,7 @@ class TestPost(InterfaceTestCase): def test_aemode_invalid_data(self): ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-data0', + datanetworks='group0-data0', ifname='name', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1460,12 +1490,12 @@ class TestPost(InterfaceTestCase): bond_iface = self._create_worker_bond('bond0', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0') + datanetworks='group0-data0') port, iface1 = self._create_ethernet() ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-ext1', + datanetworks='group0-ext1', ifname='bond1', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1480,7 +1510,7 @@ class TestPost(InterfaceTestCase): self._create_worker_vlan('vlan0', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, vlan_id=4095, - providernetworks='group0-ext0', + datanetworks='group0-ext0', expect_errors=True) # Expected message: Interface eth0 is already used by another VLAN @@ -1490,12 +1520,12 @@ class TestPost(InterfaceTestCase): 'vlan0', constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - vlan_id=10, providernetworks='group0-ext0') + vlan_id=10, datanetworks='group0-ext0') port, iface1 = self._create_ethernet() ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-ext1', + datanetworks='group0-ext1', ifname='bond0', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1510,12 +1540,12 @@ class TestPost(InterfaceTestCase): bond_iface = self._create_worker_bond('bond0', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0') + datanetworks='group0-data0') port, iface1 = self._create_ethernet() ndict = dbutils.post_get_test_interface( ihost_uuid=self.worker.uuid, - providernetworks='group0-ext1', + datanetworks='group0-ext1', ifname='bond1', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1531,13 +1561,13 @@ class TestPost(InterfaceTestCase): vlan_iface = self._create_worker_vlan( 'vlan1', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 1, - providernetworks='group0-ext0') + datanetworks='group0-ext0') self._create_worker_vlan('vlan2', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, vlan_id=2, lower_iface=vlan_iface, - providernetworks='group0-ext1', + datanetworks='group0-ext1', expect_errors=True) # Expected message: data VLAN cannot be created over a LAG interface with @@ -1549,7 +1579,7 @@ class TestPost(InterfaceTestCase): self._create_worker_vlan( 'vlan2', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 2, - lower_iface=bond_iface, providernetworks='group0-ext1', + lower_iface=bond_iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: data VLAN cannot be created over a LAG interface with @@ -1561,7 +1591,7 @@ class TestPost(InterfaceTestCase): self._create_worker_vlan( 'vlan2', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, 2, - lower_iface=bond_iface, providernetworks='group0-ext1', + lower_iface=bond_iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: mgmt VLAN cannot be created over a LAG interface with @@ -1569,20 +1599,20 @@ class TestPost(InterfaceTestCase): def test_create_mgmt_vlan_over_data_lag(self): bond_iface = self._create_worker_bond( 'data', constants.NETWORK_TYPE_DATA, - constants.INTERFACE_CLASS_DATA, providernetworks='group0-ext1') + constants.INTERFACE_CLASS_DATA, datanetworks='group0-ext1') self._create_worker_vlan( 'mgmt', constants.NETWORK_TYPE_MGMT, constants.INTERFACE_CLASS_PLATFORM, 2, - lower_iface=bond_iface, providernetworks='group0-ext1', + lower_iface=bond_iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: - # Provider network(s) not supported for non-data interfaces. - def test_create_nondata_provider_network(self): + # Data network(s) not supported for non-data interfaces. + def test_create_nondata_data_network(self): self._create_worker_bond( 'pxeboot', constants.NETWORK_TYPE_PXEBOOT, constants.INTERFACE_CLASS_PLATFORM, - providernetworks='group0-data0', expect_errors=True) + datanetworks='group0-data0', expect_errors=True) # Expected message: Name must be unique def test_create_invalid_ae_name(self): @@ -1607,7 +1637,7 @@ class TestPost(InterfaceTestCase): self._create_ethernet('shared', networktype=[constants.NETWORK_TYPE_MGMT, constants.NETWORK_TYPE_DATA], - providernetworks='group0-data0', + datanetworks='group0-data0', host=self.worker, expect_errors=True) @@ -1618,7 +1648,7 @@ class TestPost(InterfaceTestCase): self._create_ethernet('shared', networktype=[constants.NETWORK_TYPE_DATA, constants.NETWORK_TYPE_PXEBOOT], - providernetworks='group0-data0', + datanetworks='group0-data0', host=self.worker, expect_errors=True) @@ -1628,6 +1658,7 @@ class TestCpePost(InterfaceTestCase): super(TestCpePost, self).setUp() self._create_host(constants.CONTROLLER, constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() # Expected message: # Network type list may only contain at most one type @@ -1635,7 +1666,7 @@ class TestCpePost(InterfaceTestCase): self._create_bond('bond0', networktype=[constants.NETWORK_TYPE_DATA, constants.NETWORK_TYPE_PXEBOOT], - providernetworks='group0-data0', expect_errors=True) + datanetworks='group0-data0', expect_errors=True) # Expected message: # Network type list may only contain at most one type @@ -1643,7 +1674,7 @@ class TestCpePost(InterfaceTestCase): self._create_bond('shared', networktype=[constants.NETWORK_TYPE_INFRA, constants.NETWORK_TYPE_DATA], - providernetworks='group0-data0', + datanetworks='group0-data0', expect_errors=True) # Expected message: oam VLAN cannot be created over an interface with @@ -1651,11 +1682,11 @@ class TestCpePost(InterfaceTestCase): def test_create_oam_vlan_over_data_lag(self): bond_iface = self._create_bond( 'data', constants.NETWORK_TYPE_DATA, - constants.INTERFACE_CLASS_DATA, providernetworks='group0-ext1') + constants.INTERFACE_CLASS_DATA, datanetworks='group0-ext1') self._create_vlan( 'oam', constants.NETWORK_TYPE_OAM, constants.INTERFACE_CLASS_PLATFORM, 2, - lower_iface=bond_iface, providernetworks='group0-ext1', + lower_iface=bond_iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: infra VLAN cannot be created over an interface with @@ -1663,11 +1694,11 @@ class TestCpePost(InterfaceTestCase): def test_create_infra_vlan_over_data_lag(self): bond_iface = self._create_bond( 'data', constants.NETWORK_TYPE_DATA, - constants.INTERFACE_CLASS_DATA, providernetworks='group0-ext1') + constants.INTERFACE_CLASS_DATA, datanetworks='group0-ext1') self._create_vlan( 'infra', constants.NETWORK_TYPE_INFRA, constants.INTERFACE_CLASS_PLATFORM, 2, - lower_iface=bond_iface, providernetworks='group0-ext1', + lower_iface=bond_iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: mgmt VLAN cannot be created over an interface with @@ -1675,11 +1706,11 @@ class TestCpePost(InterfaceTestCase): def test_create_mgmt_vlan_over_data_ethernet(self): port, iface = self._create_ethernet( 'data', constants.NETWORK_TYPE_DATA, - constants.INTERFACE_CLASS_DATA, providernetworks='group0-ext1') + constants.INTERFACE_CLASS_DATA, datanetworks='group0-ext1') self._create_vlan( 'mgmt', constants.NETWORK_TYPE_MGMT, constants.INTERFACE_CLASS_PLATFORM, 2, - lower_iface=iface, providernetworks='group0-ext1', + lower_iface=iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: An interface with \'oam\' network type is already @@ -1694,10 +1725,10 @@ class TestCpePost(InterfaceTestCase): port, iface = self._create_ethernet('eth1', constants.NETWORK_TYPE_NONE) self._create_vlan('vlan1', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, vlan_id=1, - lower_iface=iface, providernetworks='group0-ext0') + lower_iface=iface, datanetworks='group0-ext0') self._create_vlan('vlan2', constants.NETWORK_TYPE_DATA, constants.INTERFACE_CLASS_DATA, vlan_id=1, - lower_iface=iface, providernetworks='group0-ext1', + lower_iface=iface, datanetworks='group0-ext1', expect_errors=True) # Expected message: Network type list may only contain at most one type @@ -1718,13 +1749,14 @@ class TestCpePost(InterfaceTestCase): # Expected error: VLAN based provider network group0-data0 cannot be # assigned to a VLAN interface - def test_create_invalid_vlan_with_vlan_provider_network(self): + def test_create_invalid_vlan_with_vlan_data_network(self): port, lower = self._create_ethernet('eth1', constants.NETWORK_TYPE_NONE) self._create_vlan('vlan2', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0', + datanetworks='group0-data0', vlan_id=2, lower_iface=lower, expect_errors=True) + @testtools.skip("deprecate neutron bind interface") @mock.patch.object(dbsql_api.Connection, 'iinterface_destroy') @mock.patch.object(rpcapi.ConductorAPI, 'neutron_bind_interface') def test_create_neutron_bind_failed(self, mock_neutron_bind_interface, @@ -1738,7 +1770,7 @@ class TestCpePost(InterfaceTestCase): ndict = dbutils.post_get_test_interface( forihostid=self.controller.id, ihost_uuid=self.controller.uuid, - providernetworks='group0-ext1', + datanetworks='group0-ext1', ifname='data1', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, @@ -1751,39 +1783,39 @@ class TestCpePost(InterfaceTestCase): mock_iinterface_destroy.assert_called_once_with(mock.ANY) # Expected error: At least one provider network must be selected. - def test_create_invalid_no_provider_network(self): + def test_create_invalid_no_data_network(self): self._create_ethernet('data', networktype=constants.NETWORK_TYPE_DATA, expect_errors=True) # Expected error: Data interface data0 is already attached to this - # Provider Network: group0-data0. - def test_create_invalid_provider_network_used(self): + # Data Network: group0-data0. + def test_create_invalid_data_network_used(self): self._create_ethernet('data0', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0') + datanetworks='group0-data0') self._create_ethernet('data1', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0', + datanetworks='group0-data0', expect_errors=True) - # Expected error: Provider network \'group0-dataXX\' does not exist. - def test_create_invalid_provider_network_not_exist(self): + # Expected error: Data network \'group0-dataXX\' does not exist. + def test_create_invalid_data_network_not_exist(self): self._create_ethernet('data0', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-dataXX', + datanetworks='group0-dataXX', expect_errors=True) # Expected error: Specifying duplicate provider network 'group0-data1' # is not permitted - def test_create_invalid_duplicate_provider_network(self): + def test_create_invalid_duplicate_data_network(self): self._create_ethernet('data0', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data1,group0-data1', + datanetworks='group0-data1,group0-data1', expect_errors=True) @@ -1792,20 +1824,22 @@ class TestCpePatch(InterfaceTestCase): super(TestCpePatch, self).setUp() self._create_host(constants.CONTROLLER, constants.WORKER, admin=constants.ADMIN_LOCKED) + self._create_datanetworks() def test_create_invalid_infra_data_ethernet(self): self._create_ethernet('shared', networktype=[constants.NETWORK_TYPE_INFRA, constants.NETWORK_TYPE_DATA], - providernetworks='group0-data0', + datanetworks='group0-data0', expect_errors=True) + @testtools.skip("deprecate neutron bind interface") @mock.patch.object(rpcapi.ConductorAPI, 'neutron_bind_interface') def test_patch_neutron_bind_failed(self, mock_neutron_bind_interface): port, interface = self._create_ethernet( 'data0', networktype=constants.NETWORK_TYPE_DATA, ifclass=constants.INTERFACE_CLASS_DATA, - providernetworks='group0-data0') + datanetworks='group0-data0') mock_neutron_bind_interface.side_effect = [ None, diff --git a/sysinv/sysinv/sysinv/sysinv/tests/db/sqlalchemy/test_migrations.py b/sysinv/sysinv/sysinv/sysinv/tests/db/sqlalchemy/test_migrations.py index 5a15633607..afcabbe948 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/db/sqlalchemy/test_migrations.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/db/sqlalchemy/test_migrations.py @@ -770,7 +770,6 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin): 'created_at': 'DateTime', 'updated_at': 'DateTime', 'ifname': 'String', 'iftype': 'String', 'imac': 'String', 'imtu': 'Integer', 'networktype': 'String', 'aemode': 'String', 'txhashpolicy': 'String', - 'providernetworks': 'String', 'providernetworksdict': 'Text', 'schedpolicy': 'String', 'ifcapabilities': 'Text', 'farend': 'Text', 'forihostid': 'Integer', } @@ -1016,7 +1015,6 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin): ethernet_interfaces_col = { 'id': 'Integer', 'deleted_at': 'DateTime', 'created_at': 'DateTime', 'updated_at': 'DateTime', 'imac': 'String', 'imtu': 'Integer', - 'providernetworks': 'String', 'providernetworksdict': 'Text', } for col, coltype in ethernet_interfaces_col.items(): self.assertTrue(isinstance(ethernet_interfaces.c[col].type, @@ -1027,7 +1025,7 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin): 'id': 'Integer', 'deleted_at': 'DateTime', 'created_at': 'DateTime', 'updated_at': 'DateTime', 'aemode': 'String', 'aedict': 'Text', 'txhashpolicy': 'String', 'schedpolicy': 'String', 'imac': 'String', - 'imtu': 'Integer', 'providernetworks': 'String', 'providernetworksdict': 'Text', + 'imtu': 'Integer', } for col, coltype in ae_interfaces_col.items(): self.assertTrue(isinstance(ae_interfaces.c[col].type, @@ -1037,8 +1035,7 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin): vlan_interfaces_col = { 'id': 'Integer', 'deleted_at': 'DateTime', 'created_at': 'DateTime', 'updated_at': 'DateTime', 'vlan_id': 'String', 'vlan_type': 'String', - 'imac': 'String', 'imtu': 'Integer', 'providernetworks': 'String', - 'providernetworksdict': 'Text', + 'imac': 'String', 'imtu': 'Integer', } for col, coltype in vlan_interfaces_col.items(): self.assertTrue(isinstance(vlan_interfaces.c[col].type, @@ -1708,8 +1705,6 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin): 'id': 'Integer', 'imac': 'String', 'imtu': 'Integer', - 'providernetworks': 'String', - 'providernetworksdict': 'Text', } for col, coltype in virtual_interfaces_col.items(): self.assertTrue(isinstance(virtual_interfaces.c[col].type, diff --git a/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py b/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py index 5a33ef01ff..f3831e58ca 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py @@ -609,6 +609,39 @@ def get_test_ethernet_port(**kw): return ethernet_port +def get_test_datanetwork(**kw): + datanetwork = { + 'uuid': kw.get('uuid', '60d41820-a4a0-4c25-a6a0-2a3b98746640'), + 'name': kw.get('name'), + 'network_type': kw.get('network_type', 'vxlan'), + 'mtu': kw.get('mtu', '1500'), + 'multicast_group': kw.get('multicast_group', '239.0.2.1'), + 'port_num': kw.get('port_num', 8472), + 'ttl': kw.get('ttl', 10), + 'mode': kw.get('mode', 'dynamic'), + } + return datanetwork + + +def create_test_datanetwork(**kw): + """Create test datanetwork entry in DB and return datanetwork DB object. + Function to be used to create test datanetwork objects in the database. + :param kw: kwargs with overriding values for datanework attributes. + :returns: Test datanetwork DB object. + """ + datanetwork = get_test_datanetwork(**kw) + + if kw['network_type'] != constants.DATANETWORK_TYPE_VXLAN: + # Remove DB fields which are specific to VXLAN + del datanetwork['multicast_group'] + del datanetwork['port_num'] + del datanetwork['ttl'] + del datanetwork['mode'] + + dbapi = db_api.get_instance() + return dbapi.datanetwork_create(datanetwork) + + def create_test_ethernet_port(**kw): """Create test ethernet port entry in DB and return ethernet port DB object. Function to be used to create test ethernet port objects in the database. @@ -624,6 +657,13 @@ def create_test_ethernet_port(**kw): def post_get_test_interface(**kw): + datanetworks = kw.get('datanetworks') or "" + + if datanetworks: + datanetworks_list = datanetworks.split(',') + else: + datanetworks_list = [] + interface = { 'forihostid': kw.get('forihostid'), 'ihost_uuid': kw.get('ihost_uuid'), @@ -636,7 +676,7 @@ def post_get_test_interface(**kw): 'networks': kw.get('networks', []), 'aemode': kw.get('aemode', 'balanced'), 'txhashpolicy': kw.get('txhashpolicy', 'layer2'), - 'providernetworks': kw.get('providernetworks'), + 'datanetworks': datanetworks_list, 'vlan_id': kw.get('vlan_id'), 'uses': kw.get('uses', None), 'used_by': kw.get('used_by', []), @@ -650,6 +690,13 @@ def post_get_test_interface(**kw): def get_test_interface(**kw): + + datanetworks = kw.get('datanetworks') or "" + if datanetworks: + datanetworks_list = datanetworks.split(',') + else: + datanetworks_list = [] + interface = { 'id': kw.get('id'), 'uuid': kw.get('uuid'), @@ -664,7 +711,7 @@ def get_test_interface(**kw): 'networks': kw.get('networks', []), 'aemode': kw.get('aemode'), 'txhashpolicy': kw.get('txhashpolicy', None), - 'providernetworks': kw.get('providernetworks'), + 'datanetworks': datanetworks_list, 'vlan_id': kw.get('vlan_id', None), 'uses': kw.get('uses', []), 'used_by': kw.get('used_by', []), @@ -683,13 +730,31 @@ def create_test_interface(**kw): :param kw: kwargs with overriding values for interface's attributes. :returns: Test Interface DB object. """ + interface = get_test_interface(**kw) + datanetworks_list = interface.get('datanetworks') or [] + # Let DB generate ID if it isn't specified explicitly if 'id' not in kw: del interface['id'] + + if 'datanetworks' in interface: + del interface['datanetworks'] + dbapi = db_api.get_instance() forihostid = kw.get('forihostid') - return dbapi.iinterface_create(forihostid, interface) + interface_obj = dbapi.iinterface_create(forihostid, interface) + + # assign the interface to the datanetwork + for datanetwork in datanetworks_list: + if not datanetwork: + continue + dn = dbapi.datanetwork_get(datanetwork) + values = {'interface_id': interface_obj.id, + 'datanetwork_id': dn.id} + dbapi.interface_datanetwork_create(values) + + return interface_obj def create_test_interface_network(**kw): diff --git a/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py b/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py index 90e8bcdfb7..8961d365ac 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py @@ -20,6 +20,7 @@ import hashlib import os import os.path import tempfile +import wsme import mox import netaddr @@ -28,6 +29,7 @@ from oslo_config import cfg from six import StringIO from six.moves import builtins from sysinv.common import exception +from sysinv.common import service_parameter from sysinv.common import utils from sysinv.tests import base @@ -367,3 +369,44 @@ class IntLikeTestCase(base.TestCase): self.assertFalse( utils.is_int_like("0cc3346e-9fef-4445-abe6-5d2b2690ec64")) self.assertFalse(utils.is_int_like("a1")) + + +class LDAPTestCase(base.TestCase): + + def test_ldapurl(self): + # Bad Network address is not acceptable as ldap url + ldap_url = 'ldap://127' + self.assertRaises(wsme.exc.ClientSideError, + service_parameter._validate_ldap_url, + 'foo', + ldap_url) + + # loopback is not acceptable as ldap url + ldap_url = 'ldap://127.0.0.1' + self.assertRaises(wsme.exc.ClientSideError, + service_parameter._validate_ldap_url, + 'foo', + ldap_url) + + # localhost is not acceptable as ldap url + ldap_url = 'ldap://localhost:1234' + self.assertRaises(wsme.exc.ClientSideError, + service_parameter._validate_ldap_url, + 'foo', + ldap_url) + + # A valid ldap URL should not raise an exception + ldap_url = 'ldap://dns.example.com:389' + service_parameter._validate_ldap_url('foo', ldap_url) + + def test_ldap_dn(self): + # A poorly formatted ldap DN will raise a ClientSideError + ldap_dn = 'this is not a valid ldap dn' + self.assertRaises(wsme.exc.ClientSideError, + service_parameter._validate_ldap_dn, + 'foo', + ldap_dn) + + # A valid DN will not raise a ClientSideError + ldap_dn = 'uid=john.doe,ou=People,dc=example,dc=com' + service_parameter._validate_ldap_dn('foo', ldap_dn) diff --git a/sysinv/sysinv/sysinv/test-requirements.txt b/sysinv/sysinv/sysinv/test-requirements.txt index 4990bf5ab3..92eb395efb 100644 --- a/sysinv/sysinv/sysinv/test-requirements.txt +++ b/sysinv/sysinv/sysinv/test-requirements.txt @@ -28,7 +28,7 @@ libvirt-python>=1.2.5 migrate python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 python-cephclient -python-ldap>=2.4.22,<3.0.0 +python-ldap>=3.1.0 markupsafe docker # Babel>=0.9.6