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