Add CaaS networks related validations 43/743/15
authorKrisztian Lengyel <krisztian.lengyel@nokia.com>
Fri, 17 May 2019 21:03:42 +0000 (17:03 -0400)
committerKrisztian Lengyel <krisztian.lengyel@nokia.com>
Mon, 17 Jun 2019 14:49:08 +0000 (16:49 +0200)
- Add CaaS cluster and tenant network related validations

- Modified baremetal deployment Architecture, so that nodes and their Datastructures are not created in playbook. They are now coming from Inventory, generated using CM Inventory handler. This way we don't have to iterate through nodes list. One thread per node is forked, but all are executed on installation controller using delegate_to ansible keyword. This saves baremetal provisioning time, and improves code readability

- Add REC specific REC userconfighandler for network profiles

Change-Id: I299fea47177a2ad1a24e0715e9f2d5f37c3d2e08
Signed-off-by: Krisztian Lengyel <krisztian.lengyel@nokia.com>
inventoryhandlers/baremetal-node-inventory/zbaremetalnodeinventory.py [new file with mode: 0644]
recuserconfighandlers.spec [new file with mode: 0644]
recuserconfighandlers/recnetworkprofileshandler/recnetworkprofileshandler.py [new file with mode: 0644]
validators/src/CaasValidation.py
validators/src/NetworkProfilesValidation.py
validators/src/VersionValidation.py

diff --git a/inventoryhandlers/baremetal-node-inventory/zbaremetalnodeinventory.py b/inventoryhandlers/baremetal-node-inventory/zbaremetalnodeinventory.py
new file mode 100644 (file)
index 0000000..249a5c0
--- /dev/null
@@ -0,0 +1,332 @@
+# Copyright 2019 Nokia
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+from jinja2 import Environment
+
+from cmframework.apis import cmansibleinventoryconfig
+from cmdatahandlers.api import utils
+
+
+nics_json_txt = """
+[ {%- if 'mgmt_mac' in all_vars['hosts'][host] %}
+      {%- for mac_members in all_vars['hosts'][host]['mgmt_mac'] %}
+          {
+          "mac": "{{ mac_members }}"
+          }
+          {%- if not loop.last %},{%- endif %}
+      {%- endfor %}
+  {%- else: %}
+      {
+      "mac": "{{ all_vars['hw_inventory_details'][host]['mgmt_mac'] }}"
+      }
+  {%- endif %}
+]
+"""
+
+
+class zbaremetalnodeinventory(cmansibleinventoryconfig.CMAnsibleInventoryConfigPlugin):
+    def __init__(self, confman, inventory, ownhost):
+        super(zbaremetalnodeinventory, self).__init__(confman, inventory, ownhost)
+
+    def handle_bootstrapping(self):
+        pass
+
+    def handle_provisioning(self):
+        self.handle()
+
+    def handle_postconfig(self):
+        self.handle()
+
+    def handle_setup(self):
+        pass
+
+    @staticmethod
+    def _check_host_single_nic(host_network_profile_value, host_interface_net_mapping):
+        if 'provider_network_interfaces' in host_network_profile_value:
+            host_provider_network_interfaces = host_network_profile_value['provider_network_interfaces']
+            if len(host_interface_net_mapping) == 1 and len(host_provider_network_interfaces) == 1:
+                if host_interface_net_mapping.keys()[0] == host_provider_network_interfaces.keys()[0]:
+                    return True
+        return False
+
+    @staticmethod
+    def _generate_linux_bonding_options(options):
+        mode_mapping = {'active-backup': 'active-backup', 'lacp': '802.3ad'}
+        default_options = {'active-backup': 'miimon=100',
+                           'lacp': 'lacp_rate=fast miimon=100'}
+        for i in options.split():
+            key, value = i.split('=')
+            if key == 'mode':
+                if default_options[value]:
+                    return 'mode=' + mode_mapping[value] + ' ' + default_options[value]
+                return 'mode=' + mode_mapping[value]
+
+    @staticmethod
+    def _generate_ovs_bonding_options(options):
+        mode_mapping = {'active-backup': 'active-backup', 'lacp': 'balance-slb',
+                        'lacp-layer34': 'balance-tcp'}
+        default_options = {'active-backup': '',
+                           'lacp': 'lacp=active other_config:lacp-time=fast other_config:bond-detect-mode=carrier',
+                           'lacp-layer34': 'lacp=active other_config:lacp-time=fast other_config:bond-detect-mode=carrier'}
+        for i in options.split():
+            key, value = i.split('=')
+            if key == 'mode':
+                if default_options[value]:
+                    return 'bond_mode=' + mode_mapping[value] + ' ' + default_options[value]
+                return 'bond_mode=' + mode_mapping[value]
+
+    @staticmethod
+    def _add_static_routes(routes):
+        routes_list = []
+        for route in routes:
+            routes_list.append({"ip_netmask": route["to"], "next_hop": route["via"]})
+        return routes_list
+
+    def handle(self):
+        usersconf = self.confman.get_users_config_handler()
+        admin_user = usersconf.get_admin_user()
+        self.add_global_var("home_dir", "/home/" + admin_user)
+        all_vars = self.inventory['all']['vars']
+        host_locals = self.inventory['_meta']['hostvars']
+        nfs_server_ip = host_locals[all_vars['installation_controller']]['networking']['infra_external']['ip']
+
+        for host, hostvars in host_locals.iteritems():
+            host_hdd_mapping = hostvars['by_path_disks']
+            host_networking = hostvars['networking']
+            host_network_profiles_list = all_vars['hosts'][host]['network_profiles']
+            host_network_profile_value = all_vars['network_profiles'][host_network_profiles_list[0]]
+            host_interface_net_mapping = host_network_profile_value['interface_net_mapping']
+
+            infra_bond = {'in_use': False}
+            host_bonding_interfaces = host_network_profile_value.get('bonding_interfaces', {})
+            default_mtu = all_vars['networking'].get('mtu', 1500)
+
+            sriov_mtus = {}
+            if 'sriov_provider_networks' in host_network_profile_value:
+                sriov_nets = host_network_profile_value['sriov_provider_networks']
+                prov_infos = host_networking.get('provider_networks', {})
+                for net_name, sriov_info in sriov_nets.iteritems():
+                    if prov_infos.get(net_name):
+                        prov_info = prov_infos[net_name]
+                        sriov_mtu = prov_info.get('mtu', default_mtu)
+                        for iface in sriov_info['interfaces']:
+                            sriov_mtus[iface] = sriov_mtu
+
+            mtu = default_mtu
+            if 'mtu' in all_vars['networking']['infra_internal']:
+                mtu = all_vars['networking']['infra_internal']['mtu']
+
+            phys_iface_mtu = 1500
+            if 'vlan' in host_networking['infra_internal']:
+                for iface, infras in host_interface_net_mapping.iteritems():
+                    if 'infra_internal' in infras:
+                        for infra in infras:
+                            tmp_mtu = default_mtu
+                            if 'mtu' in all_vars['networking'][infra]:
+                                tmp_mtu = all_vars['networking'][infra]['mtu']
+                            if infra == 'cloud_tenant':
+                                tmp_mtu = tmp_mtu + 50
+                            if tmp_mtu > phys_iface_mtu:
+                                phys_iface_mtu = tmp_mtu
+                        if 'bond' in iface:
+                            if host_bonding_interfaces.get(iface):
+                                for slave in host_bonding_interfaces[iface]:
+                                    if slave in sriov_mtus and sriov_mtus[slave] > phys_iface_mtu:
+                                        phys_iface_mtu = sriov_mtus[slave]
+                        elif iface in sriov_mtus and sriov_mtus[iface] > phys_iface_mtu:
+                            phys_iface_mtu = sriov_mtus[iface]
+                        break
+
+            properties = {
+                "capabilities": "boot_option:local",
+                "cpu_arch": "x86_64",
+                "cpus": 8,
+                "disk_size": 40,
+                "ram": 16384
+            }
+
+            power = {
+                "provisioning_server": nfs_server_ip,
+                "virtmedia_deploy_iso": "file:///opt/images/ironic-deploy.iso",
+            }
+
+            if utils.is_virtualized():
+                driver = "ssh_virtmedia"
+                properties["root_device"] = {"by_path": host_hdd_mapping['os']}
+                power["ssh_address"] = all_vars['hosts'][host]['hwmgmt']['address']
+                power["ssh_username"] = all_vars['hosts'][host]['hwmgmt']['user']
+                power["ipmi_port"] = all_vars['hosts'][host]['vbmc_port']
+                power["ipmi_username"] = "admin"
+                power["ipmi_password"] = "password"
+                power["ssh_key_contents"] = "{{ lookup('file', '/etc/userconfig/id_rsa') }}"
+                power["ipmi_address"] = host_locals[all_vars['installation_controller']]['networking']['infra_internal']['ip']
+            else:
+                driver = "ipmi_virtmedia"
+                power["ipmi_address"] = all_vars['hosts'][host]['hwmgmt']['address']
+                power["ipmi_password"] = all_vars['hosts'][host]['hwmgmt']['password']
+                power["ipmi_username"] = all_vars['hosts'][host]['hwmgmt']['user']
+                power["product_family"] = all_vars['hw_inventory_details'][host]['product_family']
+                power["vendor"] = all_vars['hw_inventory_details'][host]['vendor']
+
+                if host_hdd_mapping['os'] != "/dev/sda":
+                    properties["root_device"] = {"by_path": host_hdd_mapping['os']}
+                else:
+                    properties["root_device"] = {"name": host_hdd_mapping['os']}
+
+            nics_text = Environment().from_string(nics_json_txt).render(all_vars=all_vars, host=host)
+            nics_inventory = json.loads(nics_text)
+
+            driver_info = {}
+            driver_info["power"] = power
+            #####################################################
+            network_config = []
+            if 'interface' in host_networking['infra_internal']:
+                if not self._check_host_single_nic(host_network_profile_value, host_interface_net_mapping):
+                    if 'bonding_interfaces' in host_network_profile_value:
+                        for net_key, net_value in host_interface_net_mapping.iteritems():
+                            bond_contents = {}
+                            if "bond" in net_key and "infra_internal" in net_value:
+                                members = []
+                                for member in host_bonding_interfaces[net_key]:
+                                    member_element = {}
+                                    if 'bond' in host_networking['infra_internal']['interface']:
+                                        member_element["mtu"] = mtu
+                                    else:
+                                        member_element["mtu"] = phys_iface_mtu
+                                    member_element["name"] = member
+                                    member_element["type"] = "interface"
+                                    member_element["use_dhcp"] = False
+                                    members.append(member_element)
+
+                                bond_contents = {
+                                    "type": "linux_bond",
+                                    "use_dhcp": False
+                                }
+                                bond_contents["name"] = net_key
+                                bond_contents["members"] = members
+
+                                if 'linux_bonding_options' in host_network_profile_value:
+                                    bond_contents["bonding_options"] = self._generate_linux_bonding_options(host_network_profile_value['linux_bonding_options'])
+                                if 'bond' in host_networking['infra_internal']['interface']:
+                                    bond_contents["addresses"] = [{"ip_netmask": "%s/%s" % (host_networking['infra_internal']['ip'], host_networking['infra_internal']['mask'])}]
+                                    bond_contents["mtu"] = mtu
+                                    if 'routes' in host_networking['infra_internal']:
+                                        routes = host_networking['infra_internal']['routes']
+                                        bond_contents["routes"] = self._add_static_routes(routes)
+                                else:
+                                    bond_contents["mtu"] = phys_iface_mtu
+
+                                infra_bond.update({'in_use': True})
+
+                                network_config.append(bond_contents)
+                    if 'vlan' in host_networking['infra_internal']:
+                        vlan_contents = {
+                            "type": "vlan",
+                            "use_dhcp": False
+                            }
+                        vlan_contents["addresses"] = [{"ip_netmask": "%s/%s" % (host_networking['infra_internal']['ip'], host_networking['infra_internal']['mask'])}]
+                        vlan_contents["vlan_id"] = host_networking['infra_internal']['vlan']
+                        for net_key, net_value in host_interface_net_mapping.iteritems():
+                            if "infra_internal" in net_value:
+                                vlan_contents["device"] = net_key
+                        vlan_contents["mtu"] = mtu
+                        if 'routes' in host_networking['infra_internal']:
+                            routes = host_networking['infra_internal']['routes']
+                            vlan_contents["routes"] = []
+                            for route in routes:
+                                vlan_contents["routes"].append({"ip_netmask": route["to"], "next_hop": route["via"]})
+                        if not infra_bond["in_use"]:
+                            vlan_phy_contents = {
+                                "type": "interface",
+                                "use_dhcp": False,
+                                "mtu": phys_iface_mtu
+                                }
+                            for net_key, net_value in host_interface_net_mapping.iteritems():
+                                if "infra_internal" in net_value:
+                                    vlan_phy_contents["name"] = net_key
+                            network_config.append(vlan_phy_contents)
+
+                        network_config.append(vlan_contents)
+
+                    elif not infra_bond["in_use"]:
+                        phy_contents = {
+                            "name": host_networking['infra_internal']['interface'],
+                            "type": "interface",
+                            "mtu": mtu,
+                            "use_dhcp": False
+                            }
+                        phy_contents["addresses"] = [{"ip_netmask": "%s/%s" % (host_networking['infra_internal']['ip'], host_networking['infra_internal']['mask'])}]
+                        if 'routes' in host_networking['infra_internal']:
+                            routes = host_networking['infra_internal']['routes']
+                            phy_contents["routes"] = self._add_static_routes(routes)
+
+                        network_config.append(phy_contents)
+
+                # --> single_nic_setup <-- #
+                else:
+                    single_nic_contents = {
+                        "name": "br-pro0",
+                        "type": "ovs_bridge",
+                        "members": []
+                        }
+                    member_elements = {"mtu": phys_iface_mtu, "use_dhcp": False}
+                    iface = host_interface_net_mapping.keys()[0]
+                    if 'bond' in iface:
+                        for bond_iface, bond_value in host_bonding_interfaces.iteritems():
+                            if bond_iface == iface:
+                                if 'ovs_bonding_options' in host_network_profile_value:
+                                    member_elements["ovs_options"] = self._generate_ovs_bonding_options(host_network_profile_value['ovs_bonding_options'])
+                                member_elements["name"] = iface
+                                member_elements["type"] = "ovs_bond"
+                                member_elements["members"] = []
+                                for member in bond_value:
+                                    ovs_bond_member = {
+                                        "name": member,
+                                        "type": "interface",
+                                        "mtu": phys_iface_mtu,
+                                        "use_dhcp": False
+                                        }
+                                    member_elements["members"].append(ovs_bond_member)
+                            single_nic_contents["members"].append(member_elements)
+                    else:
+                        member_elements["name"] = iface
+                        member_elements["type"] = "interface"
+                        single_nic_contents["members"].append(member_elements)
+
+                    infra_elements = {}
+                    infra = host_networking['infra_internal']
+                    infra_elements["use_dhcp"] = False
+                    infra_elements["type"] = "vlan"
+                    infra_elements["vlan_id"] = infra['vlan']
+                    infra_elements["mtu"] = mtu
+                    infra_elements["addresses"] = [{"ip_netmask": "%s/%s" % (infra['ip'], infra['mask'])}]
+                    if 'routes' in infra:
+                        routes = infra['routes']
+                        infra_elements["routes"] = self._add_static_routes(routes)
+
+                    single_nic_contents["members"].append(infra_elements)
+                    network_config.append(single_nic_contents)
+            #####################################################
+            driver_info["power"]["os_net_config"] = {"network_config": network_config}
+
+            ironic_node_details = {
+                "name": host,
+                "driver": driver,
+                "network_interface": "noop",
+                "nics": nics_inventory,
+                "properties": properties,
+                "driver_info": driver_info
+            }
+            self.add_host_var(host, 'ironic_node_details', ironic_node_details)
diff --git a/recuserconfighandlers.spec b/recuserconfighandlers.spec
new file mode 100644 (file)
index 0000000..c70b09a
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2019 Nokia
+#
+# 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.
+
+Name:       recuserconfighandlers
+Version:    %{_version}
+Release:    1%{?dist}
+Summary:    REC user configuration handlers
+License:    %{_platform_licence}
+Source0:    %{name}-%{version}.tar.gz
+Vendor:     %{_platform_vendor}
+
+BuildArch:      noarch
+
+%define PKG_BASE_DIR /opt/cmframework/userconfighandlers
+
+%description
+REC user configuration handlers
+
+
+%prep
+%autosetup
+
+%build
+
+%install
+mkdir -p %{buildroot}/%{PKG_BASE_DIR}/
+find recuserconfighandlers -name '*.py' -exec cp {} %{buildroot}/%{PKG_BASE_DIR}/ \;
+
+%files
+%defattr(0755,root,root,0755)
+%{PKG_BASE_DIR}/*.py*
+
+%preun
+
+
+%postun
+
+%clean
+rm -rf ${buildroot}
diff --git a/recuserconfighandlers/recnetworkprofileshandler/recnetworkprofileshandler.py b/recuserconfighandlers/recnetworkprofileshandler/recnetworkprofileshandler.py
new file mode 100644 (file)
index 0000000..9bcbf4f
--- /dev/null
@@ -0,0 +1,64 @@
+#! /usr/bin/python
+# Copyright 2019 Nokia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from cmframework.apis import cmuserconfig
+from cmframework.apis import cmerror
+from cmdatahandlers.api import configerror
+from cmdatahandlers.network_profiles.config import config as net_prof_config
+
+"""
+This plugin is used to handle REC specific network profiles configs. Currently
+its sole purpuse is to set default type for any provider networks.
+"""
+
+
+class recnetworkprofileshandler(cmuserconfig.CMUserConfigPlugin):
+    defaul_sriov_net_type = 'caas'
+    defaul_provider_interface_type = 'caas'
+
+    def __init__(self):
+        super(recnetworkprofileshandler, self).__init__()
+
+    def handle(self, confman):
+        try:
+            self._set_default_type_for_provider_networks(confman)
+        except configerror.ConfigError as exp:
+            raise cmerror.CMError(str(exp))
+
+    def _set_default_type_for_provider_networks(self, confman):
+        netprofconf = confman.get_network_profiles_config_handler()
+        network_profiles = netprofconf.get_network_profiles()
+        for profile in network_profiles:
+            try:
+                self._set_default_provider_network_type(netprofconf, profile)
+                self._set_default_sriov_provider_network_type(netprofconf, profile)
+            except (net_prof_config.MissingProviderNetworkInterfaces, net_prof_config.MissingSriovProviderNetworks):
+                pass
+
+    def _set_default_provider_network_type(self, netprofconf, profile):
+        for interface in netprofconf.get_profile_provider_network_interfaces(profile):
+            try:
+                netprofconf.get_profile_provider_network_interface_type(
+                    profile, interface)
+            except net_prof_config.MissingProviderNetworkInterfaceType:
+                netprofconf.set_profile_provider_network_interface_type(
+                        profile, interface, self.defaul_provider_interface_type)
+
+    def _set_default_sriov_provider_network_type(self, netprofconf, profile):
+        for sriov_net in netprofconf.get_profile_sriov_provider_networks(profile):
+            if not netprofconf.get_profile_sriov_provider_network_type(
+                    profile, sriov_net):
+                netprofconf.set_profile_sriov_provider_network_type(
+                        profile, sriov_net, self.defaul_sriov_net_type)
index 9bb0b8c..e72a598 100644 (file)
@@ -67,18 +67,18 @@ class CaasValidationUtils(object):
 
 
 class CaasValidation(cmvalidator.CMValidator):
-
-    SUBSCRIPTION = r'^cloud\.caas|cloud\.hosts|cloud\.networking$'
+    SUBSCRIPTION = r'^cloud\.caas|cloud\.hosts|cloud\.networking|cloud\.network_profiles$'
     CAAS_DOMAIN = 'cloud.caas'
-    NETW_DOMAIN = 'cloud.networking'
     HOSTS_DOMAIN = 'cloud.hosts'
+    NETW_DOMAIN = 'cloud.networking'
+    NETPROF_DOMAIN = 'cloud.network_profiles'
 
     SERV_PROF = 'service_profiles'
     CAAS_PROFILE_PATTERN = 'caas_master|caas_worker'
     CIDR = 'cidr'
 
-    DOCKER_SIZE_QOUTA = "docker_size_quota"
-    DOCKER_SIZE_QOUTA_PATTERN = r"^\d*[G,M,K]$"
+    DOCKER_SIZE_QUOTA = "docker_size_quota"
+    DOCKER_SIZE_QUOTA_PATTERN = r"^\d*[G,M,K]$"
 
     HELM_OP_TIMEOUT = "helm_operation_timeout"
 
@@ -89,8 +89,8 @@ class CaasValidation(cmvalidator.CMValidator):
     ENCRYPTED_CA = "encrypted_ca"
     ENCRYPTED_CA_KEY = "encrypted_ca_key"
 
-    DOMAIN_NAME = "dns_domain"
-    DOMAIN_NAME_PATTERN = "^[a-z0-9]([a-z0-9-\.]{0,253}[a-z0-9])?$"
+    CLUSTER_NETS = 'cluster_networks'
+    TENANT_NETS = 'tenant_networks'
 
     def __init__(self):
         cmvalidator.CMValidator.__init__(self)
@@ -114,10 +114,19 @@ class CaasValidation(cmvalidator.CMValidator):
         self.validate_instantiation_timeout()
         self.validate_encrypted_ca(self.ENCRYPTED_CA)
         self.validate_encrypted_ca(self.ENCRYPTED_CA_KEY)
-        self.validate_dns_domain()
+        self.validate_networks(props)
+
+    def _get_conf(self, props, domain):
+        if props.get(domain):
+            conf_str = props[domain]
+        else:
+            conf_str = self.get_plugin_client().get_property(domain)
+        return json.loads(conf_str)
 
     def is_caas_mandatory(self, props):
-        hosts_conf = json.loads(props[self.HOSTS_DOMAIN])
+        if not isinstance(props, dict):
+            raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
+        hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
         service_profiles = self.caas_utils.get_every_key_occurrence(hosts_conf, self.SERV_PROF)
         pattern = re.compile(self.CAAS_PROFILE_PATTERN)
         for profile in service_profiles:
@@ -126,31 +135,26 @@ class CaasValidation(cmvalidator.CMValidator):
         return False
 
     def props_pre_check(self, props):
-        if not isinstance(props, dict):
-            raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
-        if self.CAAS_DOMAIN not in props:
-            raise CaasValidationError(
-                '{} configuration is missing from {}!'.format(self.CAAS_DOMAIN, props))
-        self.caas_conf = json.loads(props[self.CAAS_DOMAIN])
+        self.caas_conf = self._get_conf(props, self.CAAS_DOMAIN)
         self.conf = {self.CAAS_DOMAIN: self.caas_conf}
         if not self.caas_conf:
             raise CaasValidationError('{} is an empty dictionary!'.format(self.conf))
 
     def validate_docker_size_quota(self):
-        if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QOUTA, self.caas_conf):
+        if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QUOTA, self.caas_conf):
             return
-        if not re.match(self.DOCKER_SIZE_QOUTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QOUTA]):
-            raise CaasValidationError('{} is not a valid {}!'.format(
-                self.caas_conf[self.DOCKER_SIZE_QOUTA],
-                self.DOCKER_SIZE_QOUTA))
+        if not re.match(self.DOCKER_SIZE_QUOTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QUOTA]):
+            raise CaasValidationError(
+                '{} is not a valid {}!'.format(self.caas_conf[self.DOCKER_SIZE_QUOTA],
+                                               self.DOCKER_SIZE_QUOTA))
 
     def validate_helm_operation_timeout(self):
         if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
             return
         if not isinstance(self.caas_conf[self.HELM_OP_TIMEOUT], int):
-            raise CaasValidationError('{}:{} is not an integer'.format(
-                self.HELM_OP_TIMEOUT,
-                self.caas_conf[self.HELM_OP_TIMEOUT]))
+            raise CaasValidationError(
+                '{}:{} is not an integer'.format(self.HELM_OP_TIMEOUT,
+                                                 self.caas_conf[self.HELM_OP_TIMEOUT]))
 
     def get_docker0_cidr_netw_obj(self, subnet):
         try:
@@ -160,14 +164,14 @@ class CaasValidation(cmvalidator.CMValidator):
                 self.DOCKER0_CIDR, exc))
 
     def check_docker0_cidr_overlaps_with_netw_subnets(self, docker0_cidr, props):
-        netw_conf = json.loads(props[self.NETW_DOMAIN])
+        netw_conf = self._get_conf(props, self.NETW_DOMAIN)
         cidrs = self.caas_utils.get_every_key_occurrence(netw_conf, self.CIDR)
         for cidr in cidrs:
             if docker0_cidr.overlaps(ipaddr.IPNetwork(cidr)):
                 raise CaasValidationError(
                     'CIDR configured for {} shall be an unused IP range, '
-                    'but it overlaps with {} from {}.'.format(
-                        self.DOCKER0_CIDR, cidr, self.NETW_DOMAIN))
+                    'but it overlaps with {} from {}.'.format(self.DOCKER0_CIDR, cidr,
+                                                              self.NETW_DOMAIN))
 
     def validate_docker0_cidr(self, props):
         if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
@@ -181,8 +185,7 @@ class CaasValidation(cmvalidator.CMValidator):
             return
         if not isinstance(self.caas_conf[self.INSTANTIATION_TIMEOUT], int):
             raise CaasValidationError('{}:{} is not an integer'.format(
-                self.INSTANTIATION_TIMEOUT,
-                self.caas_conf[self.INSTANTIATION_TIMEOUT]))
+                self.INSTANTIATION_TIMEOUT, self.caas_conf[self.INSTANTIATION_TIMEOUT]))
 
     def validate_encrypted_ca(self, enc_ca):
         self.caas_utils.check_key_in_dict(enc_ca, self.caas_conf)
@@ -194,11 +197,54 @@ class CaasValidation(cmvalidator.CMValidator):
         except TypeError as exc:
             raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
 
-    def validate_dns_domain(self):
-        domain = self.caas_conf[self.DOMAIN_NAME]
-        if not self.caas_utils.is_optional_param_present(self.DOMAIN_NAME, self.caas_conf):
-            return
-        if not re.match(self.DOMAIN_NAME_PATTERN, domain):
-            raise CaasValidationError('{} is not a valid {} !'.format(
-                domain,
-                self.DOMAIN_NAME))
+    def validate_networks(self, props):
+        caas_nets = []
+        for nets_key in [self.CLUSTER_NETS, self.TENANT_NETS]:
+            if self.caas_utils.is_optional_param_present(nets_key, self.caas_conf):
+                if not isinstance(self.caas_conf[nets_key], list):
+                    raise CaasValidationError('{} is not a list'.format(nets_key))
+                if len(set(self.caas_conf[nets_key])) != len(self.caas_conf[nets_key]):
+                    raise CaasValidationError('{} has duplicate entries'.format(nets_key))
+                caas_nets.extend(self.caas_conf[nets_key])
+        if len(set(caas_nets)) != len(caas_nets):
+            raise CaasValidationError('{} and {} must be distinct, but same entries are '
+                                      'found from both lists'.format(self.CLUSTER_NETS,
+                                                                     self.TENANT_NETS))
+        self._validate_homogenous_net_setup(props, caas_nets)
+
+    def _validate_homogenous_net_setup(self, props, caas_nets):
+        # Validate homogenous CaaS provider network setup
+        # pylint: disable=too-many-locals,too-many-nested-blocks
+        hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
+        netprof_conf = self._get_conf(props, self.NETPROF_DOMAIN)
+        net_iface_map = {}
+        for net in caas_nets:
+            net_iface_map[net] = None
+            for host, host_conf in hosts_conf.iteritems():
+                # Validate only nodes that can host containerized workloads
+                if ('caas_worker' in host_conf[self.SERV_PROF] or
+                        ('caas_master' in host_conf[self.SERV_PROF] and
+                         'compute' not in host_conf[self.SERV_PROF])):
+                    # Validating CaaS network 'net' mapping in 'host'
+                    is_caas_network_present = False
+                    profiles = host_conf.get('network_profiles')
+                    if isinstance(profiles, list) and profiles:
+                        net_prof = netprof_conf.get(profiles[0])
+                    if net_prof is not None:
+                        ifaces = net_prof.get('provider_network_interfaces', {})
+                        for iface, data in ifaces.iteritems():
+                            net_type = data.get('type')
+                            networks = data.get('provider_networks', [])
+                            if net in networks and net_type == 'caas':
+                                is_caas_network_present = True
+                                if net_iface_map[net] is None:
+                                    net_iface_map[net] = iface
+                                elif net_iface_map[net] != iface:
+                                    msg = 'CaaS network {} mapped to interface {} in one host '
+                                    msg += 'and interface {} in another host'
+                                    raise CaasValidationError(msg.format(net, iface,
+                                                                         net_iface_map[net]))
+                                break
+                    if not is_caas_network_present:
+                        raise CaasValidationError('CaaS network {} missing from host {}'
+                                                  .format(net, host))
index 875d5cb..ae8ad1a 100644 (file)
@@ -43,11 +43,19 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
     LINUX_BONDING_OPTIONS = 'linux_bonding_options'
     OVS_BONDING_OPTIONS = 'ovs_bonding_options'
 
+    TYPE_CAAS = 'caas'
+    TYPE_OPENSTACK = 'openstack'
     TYPE_OVS = 'ovs'
     TYPE_OVS_DPDK = 'ovs-dpdk'
     TYPE_OVS_OFFLOAD_SRIOV = "ovs-offload-sriov"
     TYPE_OVS_OFFLOAD_VIRTIO = "ovs-offload-virtio"
-    VALID_TYPES = [TYPE_OVS, TYPE_OVS_DPDK, TYPE_OVS_OFFLOAD_SRIOV, TYPE_OVS_OFFLOAD_VIRTIO]
+    VALID_PROVIDER_TYPES = [TYPE_CAAS,
+                            TYPE_OVS,
+                            TYPE_OVS_DPDK,
+                            TYPE_OVS_OFFLOAD_SRIOV,
+                            TYPE_OVS_OFFLOAD_VIRTIO]
+    SINGLE_NIC_UNSUPPORTED_TYPES = [TYPE_CAAS, TYPE_OVS_DPDK]
+    VALID_SRIOV_TYPES = [TYPE_CAAS, TYPE_OPENSTACK]
 
     MODE_LACP = 'mode=lacp'
     MODE_LACP_LAYER34 = 'mode=lacp-layer34'
@@ -82,8 +90,13 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
     ERR_UNTAGGED_MTU_SIZE = 'Untagged network {1} in {0} has too small MTU, ' + \
                             'VLAN tagged networks with bigger MTU exists on the same interface'
 
-    ERR_INVALID_TYPE = 'Invalid provider network type for interface {}, valid types: %s' % \
-        VALID_TYPES
+    ERR_INVALID_PROVIDER_TYPE = \
+        'Invalid provider network type for interface {}, valid types: %s' % \
+        VALID_PROVIDER_TYPES
+    ERR_INVALID_SRIOV_TYPE = \
+        'Invalid sr-iov network type for network {}, valid types: %s' % \
+        VALID_SRIOV_TYPES
+
     ERR_DPDK_MAX_RX_QUEUES = 'Invalid %s value {}, must be positive integer' % DPDK_MAX_RX_QUEUES
     ERR_MISSPLACED_MTU = 'Missplaced MTU inside %s interface {}' % PROVIDER_NETWORK_INTERFACES
     ERR_OVS_TYPE_CONFLICT = 'Cannot have both %s and %s types of provider networks in {}' % \
@@ -110,11 +123,11 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
     ERR_SRIOV_PROVIDER_VLAN_CONFLICT = \
         'SR-IOV network {} vlan range is conflicting with other provider network vlan'
     ERR_SINGLE_NIC_VIOLATION = \
-        'Provider and infa networks on the same interface in {}: ' + \
+        'Provider and infra networks on the same interface in {}: ' + \
         'Supported only if all networks on the same interface'
-    ERR_SINGLE_NIC_DPDK = \
-        'Provider and infa networks on the same interface in {}: ' + \
-        'Not supported for %s type of provider networks' % TYPE_OVS_DPDK
+    ERR_SINGLE_NIC_PROVIDER_TYPE = \
+        'Provider and infra networks on the same interface in {0}: ' + \
+        'Not supported for {1} type of provider networks'
     ERR_INFRA_PROVIDER_VLAN_CONFLICT = \
         'Provider network {} vlan range is conflicting with infra network vlan'
     ERR_INFRA_PROVIDER_UNTAGGED_CONFLICT = \
@@ -180,7 +193,12 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
 
     @staticmethod
     def err_provnet_type(iface):
-        err = NetworkProfilesValidation.ERR_INVALID_TYPE.format(iface)
+        err = NetworkProfilesValidation.ERR_INVALID_PROVIDER_TYPE.format(iface)
+        raise validation.ValidationError(err)
+
+    @staticmethod
+    def err_sriov_type(network):
+        err = NetworkProfilesValidation.ERR_INVALID_SRIOV_TYPE.format(network)
         raise validation.ValidationError(err)
 
     @staticmethod
@@ -285,8 +303,8 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
         raise validation.ValidationError(err)
 
     @staticmethod
-    def err_single_nic_dpdk(profile):
-        err = NetworkProfilesValidation.ERR_SINGLE_NIC_DPDK.format(profile)
+    def err_single_nic_provider_type(profile, provider_type):
+        err = NetworkProfilesValidation.ERR_SINGLE_NIC_PROVIDER_TYPE.format(profile, provider_type)
         raise validation.ValidationError(err)
 
     @staticmethod
@@ -582,6 +600,9 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
                 if (self.key_exists(networks[network], self.TRUSTED) and
                         not self.val_is_bool(networks[network], self.TRUSTED)):
                     self.err_not_bool(network, self.TRUSTED)
+                if (self.key_exists(networks[network], self.TYPE) and
+                        networks[network][self.TYPE] not in self.VALID_SRIOV_TYPES):
+                    self.err_sriov_type(network)
                 self.must_be_list(networks, network, self.INTERFACES)
                 for iface in networks[network][self.INTERFACES]:
                     sriov_ifaces.append(iface)
@@ -616,8 +637,8 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
                         if (len(self.conf[profile_name][self.PROVIDER_NETWORK_INTERFACES]) > 1 or
                                 len(self.conf[profile_name][self.INTERFACE_NET_MAPPING]) > 1):
                             self.err_single_nic_violation(profile_name)
-                        if iface_info[self.TYPE] == self.TYPE_OVS_DPDK:
-                            self.err_single_nic_dpdk(profile_name)
+                        if iface_info[self.TYPE] in self.SINGLE_NIC_UNSUPPORTED_TYPES:
+                            self.err_single_nic_provider_type(profile_name, iface_info[self.TYPE])
                         self.validate_shared_infra_provider(network, infra_info, vlan_ranges)
                 for idx, ranges1 in enumerate(vlan_ranges_list):
                     for ranges2 in vlan_ranges_list[(idx+1):]:
@@ -626,10 +647,12 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
 
     def validate_not_part_of_lacp(self, profile_conf, iface):
         if self.key_exists(profile_conf, self.PROVIDER_NETWORK_INTERFACES):
-            for provider_iface in profile_conf[self.PROVIDER_NETWORK_INTERFACES]:
-                if self.is_bond_iface(provider_iface):
-                    if iface in profile_conf[self.BONDING_INTERFACES][provider_iface]:
-                        if profile_conf[self.OVS_BONDING_OPTIONS] == self.MODE_LACP:
+            for prov_iface, prov in profile_conf[self.PROVIDER_NETWORK_INTERFACES].iteritems():
+                if self.is_bond_iface(prov_iface):
+                    if iface in profile_conf[self.BONDING_INTERFACES][prov_iface]:
+                        bonding_type = self.OVS_BONDING_OPTIONS \
+                            if prov[self.TYPE] != self.TYPE_CAAS else self.LINUX_BONDING_OPTIONS
+                        if profile_conf[bonding_type] == self.MODE_LACP:
                             self.err_sriov_lacp_conflict()
                         # part of ovs bonding
                         # do not check linux bonding options even if shared with infra networks
@@ -751,7 +774,7 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
 
     def validate_provider_net_type(self, provnet_ifaces_conf, iface):
         self.must_be_str(provnet_ifaces_conf, iface, self.TYPE)
-        if provnet_ifaces_conf[iface][self.TYPE] not in self.VALID_TYPES:
+        if provnet_ifaces_conf[iface][self.TYPE] not in self.VALID_PROVIDER_TYPES:
             self.err_provnet_type(iface)
 
     def validate_provider_net_vf_count(self, provnet_ifaces_conf, iface):
@@ -785,7 +808,10 @@ class NetworkProfilesValidation(cmvalidator.CMValidator):
         provider_ifaces = []
         if self.key_exists(self.conf[profile_name], self.PROVIDER_NETWORK_INTERFACES):
             for iface in self.conf[profile_name][self.PROVIDER_NETWORK_INTERFACES]:
-                self.validate_net_iface_integrity(profile_name, iface, self.OVS_BONDING_OPTIONS)
+                iface_data = self.conf[profile_name][self.PROVIDER_NETWORK_INTERFACES][iface]
+                bonding_type = self.OVS_BONDING_OPTIONS \
+                    if iface_data[self.TYPE] != self.TYPE_CAAS else self.LINUX_BONDING_OPTIONS
+                self.validate_net_iface_integrity(profile_name, iface, bonding_type)
                 provider_ifaces.append(iface)
         for iface in self.conf[profile_name][self.INTERFACE_NET_MAPPING]:
             if iface not in provider_ifaces:
index 5c92b49..f6e891a 100644 (file)
@@ -22,10 +22,10 @@ from cmdatahandlers.api import validation
 
 class VersionValidation(cmvalidator.CMValidator):
     domain = 'cloud.version'
-    version = [2, 0, 0]
+    version = [2, 0, 2]
 
     # Should be same as 'version' in release build
-    devel_version = [2, 0, 0]
+    devel_version = [2, 0, 2]
 
     # Example:
     # {1: 'This is the first change requiring new template version (1.1.0)',