Merge "Initial patch to begin adding support for Magnum."
[snaps.git] / snaps / openstack / create_network.py
index 8357313..d639c2b 100644 (file)
 # limitations under the License.
 import logging
 
-from neutronclient.common.exceptions import NotFound
+import enum
+from neutronclient.common.exceptions import NetworkNotFoundClient
+
+from snaps.openstack.openstack_creator import OpenStackNetworkObject
 from snaps.openstack.utils import keystone_utils, neutron_utils
 
 __author__ = 'spisarski'
@@ -22,9 +25,9 @@ __author__ = 'spisarski'
 logger = logging.getLogger('OpenStackNetwork')
 
 
-class OpenStackNetwork:
+class OpenStackNetwork(OpenStackNetworkObject):
     """
-    Class responsible for creating a network in OpenStack
+    Class responsible for managing a network in OpenStack
     """
 
     def __init__(self, os_creds, network_settings):
@@ -33,59 +36,39 @@ class OpenStackNetwork:
         :param os_creds: The credentials to connect with OpenStack
         :param network_settings: The settings used to create a network
         """
-        self.__os_creds = os_creds
+        super(self.__class__, self).__init__(os_creds)
+
         self.network_settings = network_settings
-        self.__neutron = None
 
         # Attributes instantiated on create()
         self.__network = None
-        self.__subnets = list()
 
-    def create(self, cleanup=False):
+    def initialize(self):
+        """
+        Loads the existing OpenStack network/subnet
+        :return: The Network domain object or None
+        """
+        super(self.__class__, self).initialize()
+
+        self.__network = neutron_utils.get_network(
+            self._neutron, network_settings=self.network_settings,
+            project_id=self.network_settings.get_project_id(self._os_creds))
+
+        return self.__network
+
+    def create(self):
         """
         Responsible for creating not only the network but then a private
         subnet, router, and an interface to the router.
-        :param cleanup: When true, only perform lookups for OpenStack objects.
-        :return: the created network object or None
+        :return: the Network domain object
         """
-        self.__neutron = neutron_utils.neutron_client(self.__os_creds)
-
-        logger.info(
-            'Creating neutron network %s...' % self.network_settings.name)
-        net_inst = neutron_utils.get_network(
-            self.__neutron, self.network_settings.name,
-            self.network_settings.get_project_id(self.__os_creds))
-        if net_inst:
-            self.__network = net_inst
-        else:
-            if not cleanup:
-                self.__network = neutron_utils.create_network(
-                    self.__neutron, self.__os_creds, self.network_settings)
-            else:
-                logger.info(
-                    'Network does not exist and will not create as in cleanup'
-                    ' mode')
-                return
-        logger.debug(
-            "Network '%s' created successfully" % self.__network['network'][
-                'id'])
-
-        logger.debug('Creating Subnets....')
-        for subnet_setting in self.network_settings.subnet_settings:
-            sub_inst = neutron_utils.get_subnet_by_name(self.__neutron,
-                                                        subnet_setting.name)
-            if sub_inst:
-                self.__subnets.append(sub_inst)
-                logger.debug(
-                    "Subnet '%s' created successfully" % sub_inst['subnet'][
-                        'id'])
-            else:
-                if not cleanup:
-                    self.__subnets.append(
-                        neutron_utils.create_subnet(self.__neutron,
-                                                    subnet_setting,
-                                                    self.__os_creds,
-                                                    self.__network))
+        self.initialize()
+
+        if not self.__network:
+            self.__network = neutron_utils.create_network(
+                self._neutron, self._os_creds, self.network_settings)
+            logger.debug(
+                'Network [%s] created successfully' % self.__network.id)
 
         return self.__network
 
@@ -93,23 +76,11 @@ class OpenStackNetwork:
         """
         Removes and deletes all items created in reverse order.
         """
-        for subnet in self.__subnets:
-            try:
-                logger.info(
-                    'Deleting subnet with name ' + subnet['subnet']['name'])
-                neutron_utils.delete_subnet(self.__neutron, subnet)
-            except NotFound as e:
-                logger.warning(
-                    'Error deleting subnet with message - ' + str(e))
-                pass
-        self.__subnets = list()
-
         if self.__network:
             try:
-                neutron_utils.delete_network(self.__neutron, self.__network)
-            except NotFound:
+                neutron_utils.delete_network(self._neutron, self.__network)
+            except NetworkNotFoundClient:
                 pass
-
             self.__network = None
 
     def get_network(self):
@@ -119,13 +90,6 @@ class OpenStackNetwork:
         """
         return self.__network
 
-    def get_subnets(self):
-        """
-        Returns the OpenStack subnet objects
-        :return:
-        """
-        return self.__subnets
-
 
 class NetworkSettings:
     """
@@ -151,7 +115,9 @@ class NetworkSettings:
                          (default False).
         :param network_type: the type of network (i.e. vlan|flat).
         :param physical_network: the name of the physical network
-                                 (this is required when network_type is 'flat')
+                                 (required when network_type is 'flat')
+        :param segmentation_id: the id of the segmentation
+                                 (this is required when network_type is 'vlan')
         :param subnets or subnet_settings: List of SubnetSettings objects.
         :return:
         """
@@ -178,11 +144,12 @@ class NetworkSettings:
 
         self.network_type = kwargs.get('network_type')
         self.physical_network = kwargs.get('physical_network')
+        self.segmentation_id = kwargs.get('segmentation_id')
 
         self.subnet_settings = list()
         subnet_settings = kwargs.get('subnets')
         if not subnet_settings:
-            subnet_settings = kwargs.get('subnet_settings')
+            subnet_settings = kwargs.get('subnet_settings', list())
         if subnet_settings:
             for subnet_config in subnet_settings:
                 if isinstance(subnet_config, SubnetSettings):
@@ -192,7 +159,7 @@ class NetworkSettings:
                         SubnetSettings(**subnet_config['subnet']))
 
         if not self.name or len(self.name) < 1:
-            raise Exception('Name required for networks')
+            raise NetworkSettingsError('Name required for networks')
 
     def get_project_id(self, os_creds):
         """
@@ -205,8 +172,8 @@ class NetworkSettings:
         else:
             if self.project_name:
                 keystone = keystone_utils.keystone_client(os_creds)
-                project = keystone_utils.get_project(keystone,
-                                                     self.project_name)
+                project = keystone_utils.get_project(
+                    keystone=keystone, project_name=self.project_name)
                 if project:
                     return project.id
 
@@ -233,20 +200,37 @@ class NetworkSettings:
         if self.project_name:
             project_id = self.get_project_id(os_creds)
             if project_id:
-                out['project_id'] = project_id
+                out['tenant_id'] = project_id
             else:
-                raise Exception(
+                raise NetworkSettingsError(
                     'Could not find project ID for project named - ' +
                     self.project_name)
         if self.network_type:
             out['provider:network_type'] = self.network_type
         if self.physical_network:
             out['provider:physical_network'] = self.physical_network
+        if self.segmentation_id:
+            out['provider:segmentation_id'] = self.segmentation_id
         if self.external:
             out['router:external'] = self.external
         return {'network': out}
 
 
+class NetworkSettingsError(Exception):
+    """
+    Exception to be thrown when networks settings attributes are incorrect
+    """
+
+
+class IPv6Mode(enum.Enum):
+    """
+    A rule's direction
+    """
+    slaac = 'slaac'
+    stateful = 'dhcpv6-stateful'
+    stateless = 'dhcpv6-stateless'
+
+
 class SubnetSettings:
     """
     Class representing a subnet configuration
@@ -255,22 +239,23 @@ class SubnetSettings:
     def __init__(self, **kwargs):
         """
         Constructor - all parameters are optional except cidr (subnet mask)
-        :param cidr: The CIDR. REQUIRED if config parameter is None
-        :param ip_version: The IP version, which is 4 or 6.
-        :param name: The subnet name.
+        :param name: The subnet name (required)
+        :param cidr: The CIDR (required)
+        :param ip_version: The IP version, which is 4 or 6 (required)
         :param project_name: The name of the project who owns the network.
                              Only administrative users can specify a project ID
                              other than their own. You cannot change this value
-                             through authorization policies.
-        :param start: The start address for the allocation pools.
-        :param end: The end address for the allocation pools.
-        :param gateway_ip: The gateway IP address.
+                             through authorization policies (optional)
+        :param start: The start address for the allocation pools (optional)
+        :param end: The end address for the allocation pools (optional)
+        :param gateway_ip: The gateway IP address (optional)
         :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
-                            disabled.
+                            disabled (optional)
         :param dns_nameservers: A list of DNS name servers for the subnet.
                                 Specify each name server as an IP address
                                 and separate multiple entries with a space.
-                                For example [8.8.8.7 8.8.8.8].
+                                For example [8.8.8.7 8.8.8.8]
+                                (default '8.8.8.8')
         :param host_routes: A list of host route dictionaries for the subnet.
                             For example:
                                 "host_routes":[
@@ -283,15 +268,16 @@ class SubnetSettings:
                                         "nexthop":"192.168.0.1"
                                     }
                                 ]
-        :param destination: The destination for static route
-        :param nexthop: The next hop for the destination.
-        :param ipv6_ra_mode: A valid value is dhcpv6-stateful,
-                             dhcpv6-stateless, or slaac.
-        :param ipv6_address_mode: A valid value is dhcpv6-stateful,
-                                  dhcpv6-stateless, or slaac.
-        :raise: Exception when config does not have or cidr values are None
+        :param destination: The destination for static route (optional)
+        :param nexthop: The next hop for the destination (optional)
+        :param ipv6_ra_mode: an instance of the IPv6Mode enum
+                             (optional when enable_dhcp is True)
+        :param ipv6_address_mode: an instance of the IPv6Mode enum
+                                  (optional when enable_dhcp is True)
+        :raise: SubnetSettingsError when config does not have or cidr values
+                are None
         """
-        self.cidr = kwargs['cidr']
+        self.cidr = kwargs.get('cidr')
         if kwargs.get('ip_version'):
             self.ip_version = kwargs['ip_version']
         else:
@@ -305,19 +291,22 @@ class SubnetSettings:
         self.gateway_ip = kwargs.get('gateway_ip')
         self.enable_dhcp = kwargs.get('enable_dhcp')
 
-        if kwargs.get('dns_nameservers'):
+        if 'dns_nameservers' in kwargs:
             self.dns_nameservers = kwargs.get('dns_nameservers')
         else:
-            self.dns_nameservers = ['8.8.8.8']
+            if self.ip_version == 4:
+                self.dns_nameservers = ['8.8.8.8']
+            else:
+                self.dns_nameservers = list()
 
         self.host_routes = kwargs.get('host_routes')
         self.destination = kwargs.get('destination')
         self.nexthop = kwargs.get('nexthop')
-        self.ipv6_ra_mode = kwargs.get('ipv6_ra_mode')
-        self.ipv6_address_mode = kwargs.get('ipv6_address_mode')
+        self.ipv6_ra_mode = map_mode(kwargs.get('ipv6_ra_mode'))
+        self.ipv6_address_mode = map_mode(kwargs.get('ipv6_address_mode'))
 
         if not self.name or not self.cidr:
-            raise Exception('Name and cidr required for subnets')
+            raise SubnetSettingsError('Name and cidr required for subnets')
 
     def dict_for_neutron(self, os_creds, network=None):
         """
@@ -335,19 +324,20 @@ class SubnetSettings:
         }
 
         if network:
-            out['network_id'] = network['network']['id']
+            out['network_id'] = network.id
         if self.name:
             out['name'] = self.name
         if self.project_name:
             keystone = keystone_utils.keystone_client(os_creds)
-            project = keystone_utils.get_project(keystone, self.project_name)
+            project = keystone_utils.get_project(
+                keystone=keystone, project_name=self.project_name)
             project_id = None
             if project:
                 project_id = project.id
             if project_id:
-                out['project_id'] = project_id
+                out['tenant_id'] = project_id
             else:
-                raise Exception(
+                raise SubnetSettingsError(
                     'Could not find project ID for project named - ' +
                     self.project_name)
         if self.start and self.end:
@@ -365,12 +355,46 @@ class SubnetSettings:
         if self.nexthop:
             out['nexthop'] = self.nexthop
         if self.ipv6_ra_mode:
-            out['ipv6_ra_mode'] = self.ipv6_ra_mode
+            out['ipv6_ra_mode'] = self.ipv6_ra_mode.value
         if self.ipv6_address_mode:
-            out['ipv6_address_mode'] = self.ipv6_address_mode
+            out['ipv6_address_mode'] = self.ipv6_address_mode.value
         return out
 
 
+def map_mode(mode):
+    """
+    Takes a the direction value maps it to the Direction enum. When None return
+    None
+    :param mode: the mode value
+    :return: the IPv6Mode enum object
+    :raise: SubnetSettingsError if value is invalid
+    """
+    if not mode:
+        return None
+    if isinstance(mode, IPv6Mode):
+        return mode
+    else:
+        mode_str = str(mode)
+        if mode_str == 'slaac':
+            return IPv6Mode.slaac
+        elif mode_str == 'dhcpv6-stateful':
+            return IPv6Mode.stateful
+        elif mode_str == 'stateful':
+            return IPv6Mode.stateful
+        elif mode_str == 'dhcpv6-stateless':
+            return IPv6Mode.stateless
+        elif mode_str == 'stateless':
+            return IPv6Mode.stateless
+        else:
+            raise SubnetSettingsError('Invalid mode - ' + mode_str)
+
+
+class SubnetSettingsError(Exception):
+    """
+    Exception to be thrown when subnet settings attributes are incorrect
+    """
+
+
 class PortSettings:
     """
     Class representing a port configuration
@@ -378,42 +402,41 @@ class PortSettings:
 
     def __init__(self, **kwargs):
         """
-        Constructor - all parameters are optional
-        :param name: A symbolic name for the port.
+        Constructor
+        :param name: A symbolic name for the port (optional).
         :param network_name: The name of the network on which to create the
-                             port.
+                             port (required).
         :param admin_state_up: A boolean value denoting the administrative
-                               status of the port. True = up / False = down
+                               status of the port (default = True)
         :param project_name: The name of the project who owns the network.
                              Only administrative users can specify a project ID
                              other than their own. You cannot change this value
-                             through authorization policies.
+                             through authorization policies (optional)
         :param mac_address: The MAC address. If you specify an address that is
                             not valid, a Bad Request (400) status code is
                             returned. If you do not specify a MAC address,
                             OpenStack Networking tries to allocate one. If a
                             failure occurs, a Service Unavailable (503) status
-                            code is returned.
+                            code is returned (optional)
         :param ip_addrs: A list of dict objects where each contains two keys
                          'subnet_name' and 'ip' values which will get mapped to
                          self.fixed_ips. These values will be directly
-                         translated into the fixed_ips dict
-        :param fixed_ips: A dict where the key is the subnet IDs and value is
-                          the IP address to assign to the port
+                         translated into the fixed_ips dict (optional)
         :param security_groups: One or more security group IDs.
         :param allowed_address_pairs: A dictionary containing a set of zero or
                                       more allowed address pairs. An address
                                       pair contains an IP address and MAC
-                                      address.
-        :param opt_value: The extra DHCP option value.
-        :param opt_name: The extra DHCP option name.
+                                      address (optional)
+        :param opt_value: The extra DHCP option value (optional)
+        :param opt_name: The extra DHCP option name (optional)
         :param device_owner: The ID of the entity that uses this port.
-                             For example, a DHCP agent.
+                             For example, a DHCP agent (optional)
         :param device_id: The ID of the device that uses this port.
-                          For example, a virtual server.
+                          For example, a virtual server (optional)
         :return:
         """
-        self.network = None
+        if 'port' in kwargs:
+            kwargs = kwargs['port']
 
         self.name = kwargs.get('name')
         self.network_name = kwargs.get('network_name')
@@ -426,7 +449,6 @@ class PortSettings:
         self.project_name = kwargs.get('project_name')
         self.mac_address = kwargs.get('mac_address')
         self.ip_addrs = kwargs.get('ip_addrs')
-        self.fixed_ips = kwargs.get('fixed_ips')
         self.security_groups = kwargs.get('security_groups')
         self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
         self.opt_value = kwargs.get('opt_value')
@@ -434,33 +456,33 @@ class PortSettings:
         self.device_owner = kwargs.get('device_owner')
         self.device_id = kwargs.get('device_id')
 
-        if not self.name or not self.network_name:
-            raise Exception(
-                'The attributes neutron, name, and network_name are required '
-                'for PortSettings')
+        if not self.network_name:
+            raise PortSettingsError(
+                'The attribute network_name is required')
 
-    def __set_fixed_ips(self, neutron):
+    def __get_fixed_ips(self, neutron):
         """
         Sets the self.fixed_ips value
         :param neutron: the Neutron client
         :return: None
         """
-        if not self.fixed_ips and self.ip_addrs:
-            self.fixed_ips = list()
+
+        fixed_ips = list()
+        if self.ip_addrs:
 
             for ip_addr_dict in self.ip_addrs:
-                subnet = neutron_utils.get_subnet_by_name(neutron,
-                                                          ip_addr_dict[
-                                                              'subnet_name'])
-                if subnet:
-                    self.fixed_ips.append({'ip_address': ip_addr_dict['ip'],
-                                           'subnet_id': subnet['subnet'][
-                                               'id']})
+                subnet = neutron_utils.get_subnet(
+                    neutron, subnet_name=ip_addr_dict['subnet_name'])
+                if subnet and 'ip' in ip_addr_dict:
+                    fixed_ips.append({'ip_address': ip_addr_dict['ip'],
+                                      'subnet_id': subnet.id})
                 else:
-                    raise Exception(
+                    raise PortSettingsError(
                         'Invalid port configuration, subnet does not exist '
                         'with name - ' + ip_addr_dict['subnet_name'])
 
+        return fixed_ips
+
     def dict_for_neutron(self, neutron, os_creds):
         """
         Returns a dictionary object representing this object.
@@ -472,26 +494,24 @@ class PortSettings:
         :param os_creds: the OpenStack credentials
         :return: the dictionary object
         """
-        self.__set_fixed_ips(neutron)
 
         out = dict()
 
         project_id = None
         if self.project_name:
             keystone = keystone_utils.keystone_client(os_creds)
-            project = keystone_utils.get_project(keystone, self.project_name)
+            project = keystone_utils.get_project(
+                keystone=keystone, project_name=self.project_name)
             if project:
                 project_id = project.id
 
-        if not self.network:
-            self.network = neutron_utils.get_network(neutron,
-                                                     self.network_name,
-                                                     project_id)
-        if not self.network:
-            raise Exception(
+        network = neutron_utils.get_network(
+            neutron, network_name=self.network_name, project_id=project_id)
+        if not network:
+            raise PortSettingsError(
                 'Cannot locate network with name - ' + self.network_name)
 
-        out['network_id'] = self.network['network']['id']
+        out['network_id'] = network.id
 
         if self.admin_state_up is not None:
             out['admin_state_up'] = self.admin_state_up
@@ -499,15 +519,18 @@ class PortSettings:
             out['name'] = self.name
         if self.project_name:
             if project_id:
-                out['project_id'] = project_id
+                out['tenant_id'] = project_id
             else:
-                raise Exception(
+                raise PortSettingsError(
                     'Could not find project ID for project named - ' +
                     self.project_name)
         if self.mac_address:
             out['mac_address'] = self.mac_address
-        if self.fixed_ips and len(self.fixed_ips) > 0:
-            out['fixed_ips'] = self.fixed_ips
+
+        fixed_ips = self.__get_fixed_ips(neutron)
+        if fixed_ips and len(fixed_ips) > 0:
+            out['fixed_ips'] = fixed_ips
+
         if self.security_groups:
             out['security_groups'] = self.security_groups
         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
@@ -521,3 +544,24 @@ class PortSettings:
         if self.device_id:
             out['device_id'] = self.device_id
         return {'port': out}
+
+    def __eq__(self, other):
+        return (self.name == other.name and
+                self.network_name == other.network_name and
+                self.admin_state_up == other.admin_state_up and
+                self.project_name == other.project_name and
+                self.mac_address == other.mac_address and
+                self.ip_addrs == other.ip_addrs and
+                # self.fixed_ips == other.fixed_ips and
+                self.security_groups == other.security_groups and
+                self.allowed_address_pairs == other.allowed_address_pairs and
+                self.opt_value == other.opt_value and
+                self.opt_name == other.opt_name and
+                self.device_owner == other.device_owner and
+                self.device_id == other.device_id)
+
+
+class PortSettingsError(Exception):
+    """
+    Exception to be thrown when port settings attributes are incorrect
+    """