d639c2bea8d499795f00934c45d8323dd17f85da
[snaps.git] / snaps / openstack / create_network.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 import logging
16
17 import enum
18 from neutronclient.common.exceptions import NetworkNotFoundClient
19
20 from snaps.openstack.openstack_creator import OpenStackNetworkObject
21 from snaps.openstack.utils import keystone_utils, neutron_utils
22
23 __author__ = 'spisarski'
24
25 logger = logging.getLogger('OpenStackNetwork')
26
27
28 class OpenStackNetwork(OpenStackNetworkObject):
29     """
30     Class responsible for managing a network in OpenStack
31     """
32
33     def __init__(self, os_creds, network_settings):
34         """
35         Constructor - all parameters are required
36         :param os_creds: The credentials to connect with OpenStack
37         :param network_settings: The settings used to create a network
38         """
39         super(self.__class__, self).__init__(os_creds)
40
41         self.network_settings = network_settings
42
43         # Attributes instantiated on create()
44         self.__network = None
45
46     def initialize(self):
47         """
48         Loads the existing OpenStack network/subnet
49         :return: The Network domain object or None
50         """
51         super(self.__class__, self).initialize()
52
53         self.__network = neutron_utils.get_network(
54             self._neutron, network_settings=self.network_settings,
55             project_id=self.network_settings.get_project_id(self._os_creds))
56
57         return self.__network
58
59     def create(self):
60         """
61         Responsible for creating not only the network but then a private
62         subnet, router, and an interface to the router.
63         :return: the Network domain object
64         """
65         self.initialize()
66
67         if not self.__network:
68             self.__network = neutron_utils.create_network(
69                 self._neutron, self._os_creds, self.network_settings)
70             logger.debug(
71                 'Network [%s] created successfully' % self.__network.id)
72
73         return self.__network
74
75     def clean(self):
76         """
77         Removes and deletes all items created in reverse order.
78         """
79         if self.__network:
80             try:
81                 neutron_utils.delete_network(self._neutron, self.__network)
82             except NetworkNotFoundClient:
83                 pass
84             self.__network = None
85
86     def get_network(self):
87         """
88         Returns the created OpenStack network object
89         :return: the OpenStack network object
90         """
91         return self.__network
92
93
94 class NetworkSettings:
95     """
96     Class representing a network configuration
97     """
98
99     def __init__(self, **kwargs):
100         """
101         Constructor - all parameters are optional
102         :param name: The network name.
103         :param admin_state_up: The administrative status of the network.
104                                True = up / False = down (default True)
105         :param shared: Boolean value indicating whether this network is shared
106                        across all projects/tenants. By default, only
107                        administrative users can change this value.
108         :param project_name: Admin-only. The name of the project that will own
109                              the network. This project can be different from
110                              the project that makes the create network request.
111                              However, only administrative users can specify a
112                              project ID other than their own. You cannot change
113                              this value through authorization policies.
114         :param external: when true, will setup an external network
115                          (default False).
116         :param network_type: the type of network (i.e. vlan|flat).
117         :param physical_network: the name of the physical network
118                                  (required when network_type is 'flat')
119         :param segmentation_id: the id of the segmentation
120                                  (this is required when network_type is 'vlan')
121         :param subnets or subnet_settings: List of SubnetSettings objects.
122         :return:
123         """
124
125         self.project_id = None
126
127         self.name = kwargs.get('name')
128         if kwargs.get('admin_state_up') is not None:
129             self.admin_state_up = bool(kwargs['admin_state_up'])
130         else:
131             self.admin_state_up = True
132
133         if kwargs.get('shared') is not None:
134             self.shared = bool(kwargs['shared'])
135         else:
136             self.shared = None
137
138         self.project_name = kwargs.get('project_name')
139
140         if kwargs.get('external') is not None:
141             self.external = bool(kwargs.get('external'))
142         else:
143             self.external = False
144
145         self.network_type = kwargs.get('network_type')
146         self.physical_network = kwargs.get('physical_network')
147         self.segmentation_id = kwargs.get('segmentation_id')
148
149         self.subnet_settings = list()
150         subnet_settings = kwargs.get('subnets')
151         if not subnet_settings:
152             subnet_settings = kwargs.get('subnet_settings', list())
153         if subnet_settings:
154             for subnet_config in subnet_settings:
155                 if isinstance(subnet_config, SubnetSettings):
156                     self.subnet_settings.append(subnet_config)
157                 else:
158                     self.subnet_settings.append(
159                         SubnetSettings(**subnet_config['subnet']))
160
161         if not self.name or len(self.name) < 1:
162             raise NetworkSettingsError('Name required for networks')
163
164     def get_project_id(self, os_creds):
165         """
166         Returns the project ID for a given project_name or None
167         :param os_creds: the credentials required for keystone client retrieval
168         :return: the ID or None
169         """
170         if self.project_id:
171             return self.project_id
172         else:
173             if self.project_name:
174                 keystone = keystone_utils.keystone_client(os_creds)
175                 project = keystone_utils.get_project(
176                     keystone=keystone, project_name=self.project_name)
177                 if project:
178                     return project.id
179
180         return None
181
182     def dict_for_neutron(self, os_creds):
183         """
184         Returns a dictionary object representing this object.
185         This is meant to be converted into JSON designed for use by the Neutron
186         API
187         TODO - expand automated testing to exercise all parameters
188
189         :param os_creds: the OpenStack credentials
190         :return: the dictionary object
191         """
192         out = dict()
193
194         if self.name:
195             out['name'] = self.name
196         if self.admin_state_up is not None:
197             out['admin_state_up'] = self.admin_state_up
198         if self.shared:
199             out['shared'] = self.shared
200         if self.project_name:
201             project_id = self.get_project_id(os_creds)
202             if project_id:
203                 out['tenant_id'] = project_id
204             else:
205                 raise NetworkSettingsError(
206                     'Could not find project ID for project named - ' +
207                     self.project_name)
208         if self.network_type:
209             out['provider:network_type'] = self.network_type
210         if self.physical_network:
211             out['provider:physical_network'] = self.physical_network
212         if self.segmentation_id:
213             out['provider:segmentation_id'] = self.segmentation_id
214         if self.external:
215             out['router:external'] = self.external
216         return {'network': out}
217
218
219 class NetworkSettingsError(Exception):
220     """
221     Exception to be thrown when networks settings attributes are incorrect
222     """
223
224
225 class IPv6Mode(enum.Enum):
226     """
227     A rule's direction
228     """
229     slaac = 'slaac'
230     stateful = 'dhcpv6-stateful'
231     stateless = 'dhcpv6-stateless'
232
233
234 class SubnetSettings:
235     """
236     Class representing a subnet configuration
237     """
238
239     def __init__(self, **kwargs):
240         """
241         Constructor - all parameters are optional except cidr (subnet mask)
242         :param name: The subnet name (required)
243         :param cidr: The CIDR (required)
244         :param ip_version: The IP version, which is 4 or 6 (required)
245         :param project_name: The name of the project who owns the network.
246                              Only administrative users can specify a project ID
247                              other than their own. You cannot change this value
248                              through authorization policies (optional)
249         :param start: The start address for the allocation pools (optional)
250         :param end: The end address for the allocation pools (optional)
251         :param gateway_ip: The gateway IP address (optional)
252         :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
253                             disabled (optional)
254         :param dns_nameservers: A list of DNS name servers for the subnet.
255                                 Specify each name server as an IP address
256                                 and separate multiple entries with a space.
257                                 For example [8.8.8.7 8.8.8.8]
258                                 (default '8.8.8.8')
259         :param host_routes: A list of host route dictionaries for the subnet.
260                             For example:
261                                 "host_routes":[
262                                     {
263                                         "destination":"0.0.0.0/0",
264                                         "nexthop":"123.456.78.9"
265                                     },
266                                     {
267                                         "destination":"192.168.0.0/24",
268                                         "nexthop":"192.168.0.1"
269                                     }
270                                 ]
271         :param destination: The destination for static route (optional)
272         :param nexthop: The next hop for the destination (optional)
273         :param ipv6_ra_mode: an instance of the IPv6Mode enum
274                              (optional when enable_dhcp is True)
275         :param ipv6_address_mode: an instance of the IPv6Mode enum
276                                   (optional when enable_dhcp is True)
277         :raise: SubnetSettingsError when config does not have or cidr values
278                 are None
279         """
280         self.cidr = kwargs.get('cidr')
281         if kwargs.get('ip_version'):
282             self.ip_version = kwargs['ip_version']
283         else:
284             self.ip_version = 4
285
286         # Optional attributes that can be set after instantiation
287         self.name = kwargs.get('name')
288         self.project_name = kwargs.get('project_name')
289         self.start = kwargs.get('start')
290         self.end = kwargs.get('end')
291         self.gateway_ip = kwargs.get('gateway_ip')
292         self.enable_dhcp = kwargs.get('enable_dhcp')
293
294         if 'dns_nameservers' in kwargs:
295             self.dns_nameservers = kwargs.get('dns_nameservers')
296         else:
297             if self.ip_version == 4:
298                 self.dns_nameservers = ['8.8.8.8']
299             else:
300                 self.dns_nameservers = list()
301
302         self.host_routes = kwargs.get('host_routes')
303         self.destination = kwargs.get('destination')
304         self.nexthop = kwargs.get('nexthop')
305         self.ipv6_ra_mode = map_mode(kwargs.get('ipv6_ra_mode'))
306         self.ipv6_address_mode = map_mode(kwargs.get('ipv6_address_mode'))
307
308         if not self.name or not self.cidr:
309             raise SubnetSettingsError('Name and cidr required for subnets')
310
311     def dict_for_neutron(self, os_creds, network=None):
312         """
313         Returns a dictionary object representing this object.
314         This is meant to be converted into JSON designed for use by the Neutron
315         API
316         :param os_creds: the OpenStack credentials
317         :param network: The network object on which the subnet will be created
318                         (optional)
319         :return: the dictionary object
320         """
321         out = {
322             'cidr': self.cidr,
323             'ip_version': self.ip_version,
324         }
325
326         if network:
327             out['network_id'] = network.id
328         if self.name:
329             out['name'] = self.name
330         if self.project_name:
331             keystone = keystone_utils.keystone_client(os_creds)
332             project = keystone_utils.get_project(
333                 keystone=keystone, project_name=self.project_name)
334             project_id = None
335             if project:
336                 project_id = project.id
337             if project_id:
338                 out['tenant_id'] = project_id
339             else:
340                 raise SubnetSettingsError(
341                     'Could not find project ID for project named - ' +
342                     self.project_name)
343         if self.start and self.end:
344             out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
345         if self.gateway_ip:
346             out['gateway_ip'] = self.gateway_ip
347         if self.enable_dhcp is not None:
348             out['enable_dhcp'] = self.enable_dhcp
349         if self.dns_nameservers and len(self.dns_nameservers) > 0:
350             out['dns_nameservers'] = self.dns_nameservers
351         if self.host_routes and len(self.host_routes) > 0:
352             out['host_routes'] = self.host_routes
353         if self.destination:
354             out['destination'] = self.destination
355         if self.nexthop:
356             out['nexthop'] = self.nexthop
357         if self.ipv6_ra_mode:
358             out['ipv6_ra_mode'] = self.ipv6_ra_mode.value
359         if self.ipv6_address_mode:
360             out['ipv6_address_mode'] = self.ipv6_address_mode.value
361         return out
362
363
364 def map_mode(mode):
365     """
366     Takes a the direction value maps it to the Direction enum. When None return
367     None
368     :param mode: the mode value
369     :return: the IPv6Mode enum object
370     :raise: SubnetSettingsError if value is invalid
371     """
372     if not mode:
373         return None
374     if isinstance(mode, IPv6Mode):
375         return mode
376     else:
377         mode_str = str(mode)
378         if mode_str == 'slaac':
379             return IPv6Mode.slaac
380         elif mode_str == 'dhcpv6-stateful':
381             return IPv6Mode.stateful
382         elif mode_str == 'stateful':
383             return IPv6Mode.stateful
384         elif mode_str == 'dhcpv6-stateless':
385             return IPv6Mode.stateless
386         elif mode_str == 'stateless':
387             return IPv6Mode.stateless
388         else:
389             raise SubnetSettingsError('Invalid mode - ' + mode_str)
390
391
392 class SubnetSettingsError(Exception):
393     """
394     Exception to be thrown when subnet settings attributes are incorrect
395     """
396
397
398 class PortSettings:
399     """
400     Class representing a port configuration
401     """
402
403     def __init__(self, **kwargs):
404         """
405         Constructor
406         :param name: A symbolic name for the port (optional).
407         :param network_name: The name of the network on which to create the
408                              port (required).
409         :param admin_state_up: A boolean value denoting the administrative
410                                status of the port (default = True)
411         :param project_name: The name of the project who owns the network.
412                              Only administrative users can specify a project ID
413                              other than their own. You cannot change this value
414                              through authorization policies (optional)
415         :param mac_address: The MAC address. If you specify an address that is
416                             not valid, a Bad Request (400) status code is
417                             returned. If you do not specify a MAC address,
418                             OpenStack Networking tries to allocate one. If a
419                             failure occurs, a Service Unavailable (503) status
420                             code is returned (optional)
421         :param ip_addrs: A list of dict objects where each contains two keys
422                          'subnet_name' and 'ip' values which will get mapped to
423                          self.fixed_ips. These values will be directly
424                          translated into the fixed_ips dict (optional)
425         :param security_groups: One or more security group IDs.
426         :param allowed_address_pairs: A dictionary containing a set of zero or
427                                       more allowed address pairs. An address
428                                       pair contains an IP address and MAC
429                                       address (optional)
430         :param opt_value: The extra DHCP option value (optional)
431         :param opt_name: The extra DHCP option name (optional)
432         :param device_owner: The ID of the entity that uses this port.
433                              For example, a DHCP agent (optional)
434         :param device_id: The ID of the device that uses this port.
435                           For example, a virtual server (optional)
436         :return:
437         """
438         if 'port' in kwargs:
439             kwargs = kwargs['port']
440
441         self.name = kwargs.get('name')
442         self.network_name = kwargs.get('network_name')
443
444         if kwargs.get('admin_state_up') is not None:
445             self.admin_state_up = bool(kwargs['admin_state_up'])
446         else:
447             self.admin_state_up = True
448
449         self.project_name = kwargs.get('project_name')
450         self.mac_address = kwargs.get('mac_address')
451         self.ip_addrs = kwargs.get('ip_addrs')
452         self.security_groups = kwargs.get('security_groups')
453         self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
454         self.opt_value = kwargs.get('opt_value')
455         self.opt_name = kwargs.get('opt_name')
456         self.device_owner = kwargs.get('device_owner')
457         self.device_id = kwargs.get('device_id')
458
459         if not self.network_name:
460             raise PortSettingsError(
461                 'The attribute network_name is required')
462
463     def __get_fixed_ips(self, neutron):
464         """
465         Sets the self.fixed_ips value
466         :param neutron: the Neutron client
467         :return: None
468         """
469
470         fixed_ips = list()
471         if self.ip_addrs:
472
473             for ip_addr_dict in self.ip_addrs:
474                 subnet = neutron_utils.get_subnet(
475                     neutron, subnet_name=ip_addr_dict['subnet_name'])
476                 if subnet and 'ip' in ip_addr_dict:
477                     fixed_ips.append({'ip_address': ip_addr_dict['ip'],
478                                       'subnet_id': subnet.id})
479                 else:
480                     raise PortSettingsError(
481                         'Invalid port configuration, subnet does not exist '
482                         'with name - ' + ip_addr_dict['subnet_name'])
483
484         return fixed_ips
485
486     def dict_for_neutron(self, neutron, os_creds):
487         """
488         Returns a dictionary object representing this object.
489         This is meant to be converted into JSON designed for use by the Neutron
490         API
491
492         TODO - expand automated testing to exercise all parameters
493         :param neutron: the Neutron client
494         :param os_creds: the OpenStack credentials
495         :return: the dictionary object
496         """
497
498         out = dict()
499
500         project_id = None
501         if self.project_name:
502             keystone = keystone_utils.keystone_client(os_creds)
503             project = keystone_utils.get_project(
504                 keystone=keystone, project_name=self.project_name)
505             if project:
506                 project_id = project.id
507
508         network = neutron_utils.get_network(
509             neutron, network_name=self.network_name, project_id=project_id)
510         if not network:
511             raise PortSettingsError(
512                 'Cannot locate network with name - ' + self.network_name)
513
514         out['network_id'] = network.id
515
516         if self.admin_state_up is not None:
517             out['admin_state_up'] = self.admin_state_up
518         if self.name:
519             out['name'] = self.name
520         if self.project_name:
521             if project_id:
522                 out['tenant_id'] = project_id
523             else:
524                 raise PortSettingsError(
525                     'Could not find project ID for project named - ' +
526                     self.project_name)
527         if self.mac_address:
528             out['mac_address'] = self.mac_address
529
530         fixed_ips = self.__get_fixed_ips(neutron)
531         if fixed_ips and len(fixed_ips) > 0:
532             out['fixed_ips'] = fixed_ips
533
534         if self.security_groups:
535             out['security_groups'] = self.security_groups
536         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
537             out['allowed_address_pairs'] = self.allowed_address_pairs
538         if self.opt_value:
539             out['opt_value'] = self.opt_value
540         if self.opt_name:
541             out['opt_name'] = self.opt_name
542         if self.device_owner:
543             out['device_owner'] = self.device_owner
544         if self.device_id:
545             out['device_id'] = self.device_id
546         return {'port': out}
547
548     def __eq__(self, other):
549         return (self.name == other.name and
550                 self.network_name == other.network_name and
551                 self.admin_state_up == other.admin_state_up and
552                 self.project_name == other.project_name and
553                 self.mac_address == other.mac_address and
554                 self.ip_addrs == other.ip_addrs and
555                 # self.fixed_ips == other.fixed_ips and
556                 self.security_groups == other.security_groups and
557                 self.allowed_address_pairs == other.allowed_address_pairs and
558                 self.opt_value == other.opt_value and
559                 self.opt_name == other.opt_name and
560                 self.device_owner == other.device_owner and
561                 self.device_id == other.device_id)
562
563
564 class PortSettingsError(Exception):
565     """
566     Exception to be thrown when port settings attributes are incorrect
567     """