4f27eec11840529d3e9cf5346a6ba26993b0b74e
[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, self.network_settings.name,
57             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_by_name(
75                 self.__neutron, subnet_setting.name)
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 subnets or subnet_settings: List of SubnetSettings objects.
153         :return:
154         """
155
156         self.project_id = None
157
158         self.name = kwargs.get('name')
159         if kwargs.get('admin_state_up') is not None:
160             self.admin_state_up = bool(kwargs['admin_state_up'])
161         else:
162             self.admin_state_up = True
163
164         if kwargs.get('shared') is not None:
165             self.shared = bool(kwargs['shared'])
166         else:
167             self.shared = None
168
169         self.project_name = kwargs.get('project_name')
170
171         if kwargs.get('external') is not None:
172             self.external = bool(kwargs.get('external'))
173         else:
174             self.external = False
175
176         self.network_type = kwargs.get('network_type')
177         self.physical_network = kwargs.get('physical_network')
178
179         self.subnet_settings = list()
180         subnet_settings = kwargs.get('subnets')
181         if not subnet_settings:
182             subnet_settings = kwargs.get('subnet_settings')
183         if subnet_settings:
184             for subnet_config in subnet_settings:
185                 if isinstance(subnet_config, SubnetSettings):
186                     self.subnet_settings.append(subnet_config)
187                 else:
188                     self.subnet_settings.append(
189                         SubnetSettings(**subnet_config['subnet']))
190
191         if not self.name or len(self.name) < 1:
192             raise NetworkSettingsError('Name required for networks')
193             raise NetworkSettingsError('Name required for networks')
194
195     def get_project_id(self, os_creds):
196         """
197         Returns the project ID for a given project_name or None
198         :param os_creds: the credentials required for keystone client retrieval
199         :return: the ID or None
200         """
201         if self.project_id:
202             return self.project_id
203         else:
204             if self.project_name:
205                 keystone = keystone_utils.keystone_client(os_creds)
206                 project = keystone_utils.get_project(keystone,
207                                                      self.project_name)
208                 if project:
209                     return project.id
210
211         return None
212
213     def dict_for_neutron(self, os_creds):
214         """
215         Returns a dictionary object representing this object.
216         This is meant to be converted into JSON designed for use by the Neutron
217         API
218         TODO - expand automated testing to exercise all parameters
219
220         :param os_creds: the OpenStack credentials
221         :return: the dictionary object
222         """
223         out = dict()
224
225         if self.name:
226             out['name'] = self.name
227         if self.admin_state_up is not None:
228             out['admin_state_up'] = self.admin_state_up
229         if self.shared:
230             out['shared'] = self.shared
231         if self.project_name:
232             project_id = self.get_project_id(os_creds)
233             if project_id:
234                 out['project_id'] = project_id
235             else:
236                 raise NetworkSettingsError(
237                     'Could not find project ID for project named - ' +
238                     self.project_name)
239         if self.network_type:
240             out['provider:network_type'] = self.network_type
241         if self.physical_network:
242             out['provider:physical_network'] = self.physical_network
243         if self.external:
244             out['router:external'] = self.external
245         return {'network': out}
246
247
248 class NetworkSettingsError(Exception):
249     """
250     Exception to be thrown when networks settings attributes are incorrect
251     """
252
253
254 class SubnetSettings:
255     """
256     Class representing a subnet configuration
257     """
258
259     def __init__(self, **kwargs):
260         """
261         Constructor - all parameters are optional except cidr (subnet mask)
262         :param cidr: The CIDR. REQUIRED if config parameter is None
263         :param ip_version: The IP version, which is 4 or 6.
264         :param name: The subnet name.
265         :param project_name: The name of the project who owns the network.
266                              Only administrative users can specify a project ID
267                              other than their own. You cannot change this value
268                              through authorization policies.
269         :param start: The start address for the allocation pools.
270         :param end: The end address for the allocation pools.
271         :param gateway_ip: The gateway IP address.
272         :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
273                             disabled.
274         :param dns_nameservers: A list of DNS name servers for the subnet.
275                                 Specify each name server as an IP address
276                                 and separate multiple entries with a space.
277                                 For example [8.8.8.7 8.8.8.8].
278         :param host_routes: A list of host route dictionaries for the subnet.
279                             For example:
280                                 "host_routes":[
281                                     {
282                                         "destination":"0.0.0.0/0",
283                                         "nexthop":"123.456.78.9"
284                                     },
285                                     {
286                                         "destination":"192.168.0.0/24",
287                                         "nexthop":"192.168.0.1"
288                                     }
289                                 ]
290         :param destination: The destination for static route
291         :param nexthop: The next hop for the destination.
292         :param ipv6_ra_mode: A valid value is dhcpv6-stateful,
293                              dhcpv6-stateless, or slaac.
294         :param ipv6_address_mode: A valid value is dhcpv6-stateful,
295                                   dhcpv6-stateless, or slaac.
296         :raise: SubnetSettingsError when config does not have or cidr values
297                 are None
298         """
299         self.cidr = kwargs.get('cidr')
300         if kwargs.get('ip_version'):
301             self.ip_version = kwargs['ip_version']
302         else:
303             self.ip_version = 4
304
305         # Optional attributes that can be set after instantiation
306         self.name = kwargs.get('name')
307         self.project_name = kwargs.get('project_name')
308         self.start = kwargs.get('start')
309         self.end = kwargs.get('end')
310         self.gateway_ip = kwargs.get('gateway_ip')
311         self.enable_dhcp = kwargs.get('enable_dhcp')
312
313         if kwargs.get('dns_nameservers'):
314             self.dns_nameservers = kwargs.get('dns_nameservers')
315         else:
316             self.dns_nameservers = ['8.8.8.8']
317
318         self.host_routes = kwargs.get('host_routes')
319         self.destination = kwargs.get('destination')
320         self.nexthop = kwargs.get('nexthop')
321         self.ipv6_ra_mode = kwargs.get('ipv6_ra_mode')
322         self.ipv6_address_mode = kwargs.get('ipv6_address_mode')
323
324         if not self.name or not self.cidr:
325             raise SubnetSettingsError('Name and cidr required for subnets')
326
327     def dict_for_neutron(self, os_creds, network=None):
328         """
329         Returns a dictionary object representing this object.
330         This is meant to be converted into JSON designed for use by the Neutron
331         API
332         :param os_creds: the OpenStack credentials
333         :param network: The network object on which the subnet will be created
334                         (optional)
335         :return: the dictionary object
336         """
337         out = {
338             'cidr': self.cidr,
339             'ip_version': self.ip_version,
340         }
341
342         if network:
343             out['network_id'] = network.id
344         if self.name:
345             out['name'] = self.name
346         if self.project_name:
347             keystone = keystone_utils.keystone_client(os_creds)
348             project = keystone_utils.get_project(keystone, self.project_name)
349             project_id = None
350             if project:
351                 project_id = project.id
352             if project_id:
353                 out['project_id'] = project_id
354             else:
355                 raise SubnetSettingsError(
356                     'Could not find project ID for project named - ' +
357                     self.project_name)
358         if self.start and self.end:
359             out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
360         if self.gateway_ip:
361             out['gateway_ip'] = self.gateway_ip
362         if self.enable_dhcp is not None:
363             out['enable_dhcp'] = self.enable_dhcp
364         if self.dns_nameservers and len(self.dns_nameservers) > 0:
365             out['dns_nameservers'] = self.dns_nameservers
366         if self.host_routes and len(self.host_routes) > 0:
367             out['host_routes'] = self.host_routes
368         if self.destination:
369             out['destination'] = self.destination
370         if self.nexthop:
371             out['nexthop'] = self.nexthop
372         if self.ipv6_ra_mode:
373             out['ipv6_ra_mode'] = self.ipv6_ra_mode
374         if self.ipv6_address_mode:
375             out['ipv6_address_mode'] = self.ipv6_address_mode
376         return out
377
378
379 class SubnetSettingsError(Exception):
380     """
381     Exception to be thrown when subnet settings attributes are incorrect
382     """
383
384
385 class PortSettings:
386     """
387     Class representing a port configuration
388     """
389
390     def __init__(self, **kwargs):
391         """
392         Constructor - all parameters are optional
393         :param name: A symbolic name for the port.
394         :param network_name: The name of the network on which to create the
395                              port.
396         :param admin_state_up: A boolean value denoting the administrative
397                                status of the port. True = up / False = down
398         :param project_name: The name of the project who owns the network.
399                              Only administrative users can specify a project ID
400                              other than their own. You cannot change this value
401                              through authorization policies.
402         :param mac_address: The MAC address. If you specify an address that is
403                             not valid, a Bad Request (400) status code is
404                             returned. If you do not specify a MAC address,
405                             OpenStack Networking tries to allocate one. If a
406                             failure occurs, a Service Unavailable (503) status
407                             code is returned.
408         :param ip_addrs: A list of dict objects where each contains two keys
409                          'subnet_name' and 'ip' values which will get mapped to
410                          self.fixed_ips. These values will be directly
411                          translated into the fixed_ips dict
412         :param fixed_ips: A dict where the key is the subnet IDs and value is
413                           the IP address to assign to the port
414         :param security_groups: One or more security group IDs.
415         :param allowed_address_pairs: A dictionary containing a set of zero or
416                                       more allowed address pairs. An address
417                                       pair contains an IP address and MAC
418                                       address.
419         :param opt_value: The extra DHCP option value.
420         :param opt_name: The extra DHCP option name.
421         :param device_owner: The ID of the entity that uses this port.
422                              For example, a DHCP agent.
423         :param device_id: The ID of the device that uses this port.
424                           For example, a virtual server.
425         :return:
426         """
427         if 'port' in kwargs:
428             kwargs = kwargs['port']
429
430         self.network = None
431
432         self.name = kwargs.get('name')
433         self.network_name = kwargs.get('network_name')
434
435         if kwargs.get('admin_state_up') is not None:
436             self.admin_state_up = bool(kwargs['admin_state_up'])
437         else:
438             self.admin_state_up = True
439
440         self.project_name = kwargs.get('project_name')
441         self.mac_address = kwargs.get('mac_address')
442         self.ip_addrs = kwargs.get('ip_addrs')
443         self.fixed_ips = kwargs.get('fixed_ips')
444         self.security_groups = kwargs.get('security_groups')
445         self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
446         self.opt_value = kwargs.get('opt_value')
447         self.opt_name = kwargs.get('opt_name')
448         self.device_owner = kwargs.get('device_owner')
449         self.device_id = kwargs.get('device_id')
450
451         if not self.name or not self.network_name:
452             raise PortSettingsError(
453                 'The attributes neutron, name, and network_name are required '
454                 'for PortSettings')
455
456     def __set_fixed_ips(self, neutron):
457         """
458         Sets the self.fixed_ips value
459         :param neutron: the Neutron client
460         :return: None
461         """
462         if not self.fixed_ips and self.ip_addrs:
463             self.fixed_ips = list()
464
465             for ip_addr_dict in self.ip_addrs:
466                 subnet = neutron_utils.get_subnet_by_name(neutron,
467                                                           ip_addr_dict[
468                                                               'subnet_name'])
469                 if subnet:
470                     self.fixed_ips.append({'ip_address': ip_addr_dict['ip'],
471                                            'subnet_id': subnet.id})
472                 else:
473                     raise PortSettingsError(
474                         'Invalid port configuration, subnet does not exist '
475                         'with name - ' + ip_addr_dict['subnet_name'])
476
477     def dict_for_neutron(self, neutron, os_creds):
478         """
479         Returns a dictionary object representing this object.
480         This is meant to be converted into JSON designed for use by the Neutron
481         API
482
483         TODO - expand automated testing to exercise all parameters
484         :param neutron: the Neutron client
485         :param os_creds: the OpenStack credentials
486         :return: the dictionary object
487         """
488         self.__set_fixed_ips(neutron)
489
490         out = dict()
491
492         project_id = None
493         if self.project_name:
494             keystone = keystone_utils.keystone_client(os_creds)
495             project = keystone_utils.get_project(keystone, self.project_name)
496             if project:
497                 project_id = project.id
498
499         if not self.network:
500             self.network = neutron_utils.get_network(neutron,
501                                                      self.network_name,
502                                                      project_id)
503         if not self.network:
504             raise PortSettingsError(
505                 'Cannot locate network with name - ' + self.network_name)
506
507         out['network_id'] = self.network.id
508
509         if self.admin_state_up is not None:
510             out['admin_state_up'] = self.admin_state_up
511         if self.name:
512             out['name'] = self.name
513         if self.project_name:
514             if project_id:
515                 out['project_id'] = project_id
516             else:
517                 raise PortSettingsError(
518                     'Could not find project ID for project named - ' +
519                     self.project_name)
520         if self.mac_address:
521             out['mac_address'] = self.mac_address
522         if self.fixed_ips and len(self.fixed_ips) > 0:
523             out['fixed_ips'] = self.fixed_ips
524         if self.security_groups:
525             out['security_groups'] = self.security_groups
526         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
527             out['allowed_address_pairs'] = self.allowed_address_pairs
528         if self.opt_value:
529             out['opt_value'] = self.opt_value
530         if self.opt_name:
531             out['opt_name'] = self.opt_name
532         if self.device_owner:
533             out['device_owner'] = self.device_owner
534         if self.device_id:
535             out['device_id'] = self.device_id
536         return {'port': out}
537
538     def __eq__(self, other):
539         return (self.name == other.name and
540                 self.network_name == other.network_name and
541                 self.admin_state_up == other.admin_state_up and
542                 self.project_name == other.project_name and
543                 self.mac_address == other.mac_address and
544                 self.ip_addrs == other.ip_addrs and
545                 self.fixed_ips == other.fixed_ips and
546                 self.security_groups == other.security_groups and
547                 self.allowed_address_pairs == other.allowed_address_pairs and
548                 self.opt_value == other.opt_value and
549                 self.opt_name == other.opt_name and
550                 self.device_owner == other.device_owner and
551                 self.device_id == other.device_id)
552
553
554 class PortSettingsError(Exception):
555     """
556     Exception to be thrown when port settings attributes are incorrect
557     """