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