Fixed test names.
[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 - all parameters are optional
398         :param name: A symbolic name for the port.
399         :param network_name: The name of the network on which to create the
400                              port.
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.name or not self.network_name:
457             raise PortSettingsError(
458                 'The attributes neutron, name, and network_name are required '
459                 'for PortSettings')
460
461     def __set_fixed_ips(self, neutron):
462         """
463         Sets the self.fixed_ips value
464         :param neutron: the Neutron client
465         :return: None
466         """
467         if not self.fixed_ips and self.ip_addrs:
468             self.fixed_ips = list()
469
470             for ip_addr_dict in self.ip_addrs:
471                 subnet = neutron_utils.get_subnet(
472                     neutron, subnet_name=ip_addr_dict['subnet_name'])
473                 if subnet and 'ip' in ip_addr_dict:
474                     self.fixed_ips.append({'ip_address': ip_addr_dict['ip'],
475                                            'subnet_id': subnet.id})
476                 else:
477                     raise PortSettingsError(
478                         'Invalid port configuration, subnet does not exist '
479                         'with name - ' + ip_addr_dict['subnet_name'])
480
481     def dict_for_neutron(self, neutron, os_creds):
482         """
483         Returns a dictionary object representing this object.
484         This is meant to be converted into JSON designed for use by the Neutron
485         API
486
487         TODO - expand automated testing to exercise all parameters
488         :param neutron: the Neutron client
489         :param os_creds: the OpenStack credentials
490         :return: the dictionary object
491         """
492         self.__set_fixed_ips(neutron)
493
494         out = dict()
495
496         project_id = None
497         if self.project_name:
498             keystone = keystone_utils.keystone_client(os_creds)
499             project = keystone_utils.get_project(
500                 keystone=keystone, project_name=self.project_name)
501             if project:
502                 project_id = project.id
503
504         if not self.network:
505             self.network = neutron_utils.get_network(
506                 neutron, network_name=self.network_name, project_id=project_id)
507         if not self.network:
508             raise PortSettingsError(
509                 'Cannot locate network with name - ' + self.network_name)
510
511         out['network_id'] = self.network.id
512
513         if self.admin_state_up is not None:
514             out['admin_state_up'] = self.admin_state_up
515         if self.name:
516             out['name'] = self.name
517         if self.project_name:
518             if project_id:
519                 out['tenant_id'] = project_id
520             else:
521                 raise PortSettingsError(
522                     'Could not find project ID for project named - ' +
523                     self.project_name)
524         if self.mac_address:
525             out['mac_address'] = self.mac_address
526         if self.fixed_ips and len(self.fixed_ips) > 0:
527             out['fixed_ips'] = self.fixed_ips
528         if self.security_groups:
529             out['security_groups'] = self.security_groups
530         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
531             out['allowed_address_pairs'] = self.allowed_address_pairs
532         if self.opt_value:
533             out['opt_value'] = self.opt_value
534         if self.opt_name:
535             out['opt_name'] = self.opt_name
536         if self.device_owner:
537             out['device_owner'] = self.device_owner
538         if self.device_id:
539             out['device_id'] = self.device_id
540         return {'port': out}
541
542     def __eq__(self, other):
543         return (self.name == other.name and
544                 self.network_name == other.network_name and
545                 self.admin_state_up == other.admin_state_up and
546                 self.project_name == other.project_name and
547                 self.mac_address == other.mac_address and
548                 self.ip_addrs == other.ip_addrs and
549                 self.fixed_ips == other.fixed_ips and
550                 self.security_groups == other.security_groups and
551                 self.allowed_address_pairs == other.allowed_address_pairs and
552                 self.opt_value == other.opt_value and
553                 self.opt_name == other.opt_name and
554                 self.device_owner == other.device_owner and
555                 self.device_id == other.device_id)
556
557
558 class PortSettingsError(Exception):
559     """
560     Exception to be thrown when port settings attributes are incorrect
561     """