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