c3fb5752bdd56a90403d2614d15352d91e9f3d5c
[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 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
194     def get_project_id(self, os_creds):
195         """
196         Returns the project ID for a given project_name or None
197         :param os_creds: the credentials required for keystone client retrieval
198         :return: the ID or None
199         """
200         if self.project_id:
201             return self.project_id
202         else:
203             if self.project_name:
204                 keystone = keystone_utils.keystone_client(os_creds)
205                 project = keystone_utils.get_project(
206                     keystone=keystone, project_name=self.project_name)
207                 if project:
208                     return project.id
209
210         return None
211
212     def dict_for_neutron(self, os_creds):
213         """
214         Returns a dictionary object representing this object.
215         This is meant to be converted into JSON designed for use by the Neutron
216         API
217         TODO - expand automated testing to exercise all parameters
218
219         :param os_creds: the OpenStack credentials
220         :return: the dictionary object
221         """
222         out = dict()
223
224         if self.name:
225             out['name'] = self.name
226         if self.admin_state_up is not None:
227             out['admin_state_up'] = self.admin_state_up
228         if self.shared:
229             out['shared'] = self.shared
230         if self.project_name:
231             project_id = self.get_project_id(os_creds)
232             if project_id:
233                 out['tenant_id'] = project_id
234             else:
235                 raise NetworkSettingsError(
236                     'Could not find project ID for project named - ' +
237                     self.project_name)
238         if self.network_type:
239             out['provider:network_type'] = self.network_type
240         if self.physical_network:
241             out['provider:physical_network'] = self.physical_network
242         if self.external:
243             out['router:external'] = self.external
244         return {'network': out}
245
246
247 class NetworkSettingsError(Exception):
248     """
249     Exception to be thrown when networks settings attributes are incorrect
250     """
251
252
253 class SubnetSettings:
254     """
255     Class representing a subnet configuration
256     """
257
258     def __init__(self, **kwargs):
259         """
260         Constructor - all parameters are optional except cidr (subnet mask)
261         :param cidr: The CIDR. REQUIRED if config parameter is None
262         :param ip_version: The IP version, which is 4 or 6.
263         :param name: The subnet name.
264         :param project_name: The name of the project who owns the network.
265                              Only administrative users can specify a project ID
266                              other than their own. You cannot change this value
267                              through authorization policies.
268         :param start: The start address for the allocation pools.
269         :param end: The end address for the allocation pools.
270         :param gateway_ip: The gateway IP address.
271         :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
272                             disabled.
273         :param dns_nameservers: A list of DNS name servers for the subnet.
274                                 Specify each name server as an IP address
275                                 and separate multiple entries with a space.
276                                 For example [8.8.8.7 8.8.8.8].
277         :param host_routes: A list of host route dictionaries for the subnet.
278                             For example:
279                                 "host_routes":[
280                                     {
281                                         "destination":"0.0.0.0/0",
282                                         "nexthop":"123.456.78.9"
283                                     },
284                                     {
285                                         "destination":"192.168.0.0/24",
286                                         "nexthop":"192.168.0.1"
287                                     }
288                                 ]
289         :param destination: The destination for static route
290         :param nexthop: The next hop for the destination.
291         :param ipv6_ra_mode: A valid value is dhcpv6-stateful,
292                              dhcpv6-stateless, or slaac.
293         :param ipv6_address_mode: A valid value is dhcpv6-stateful,
294                                   dhcpv6-stateless, or slaac.
295         :raise: SubnetSettingsError when config does not have or cidr values
296                 are None
297         """
298         self.cidr = kwargs.get('cidr')
299         if kwargs.get('ip_version'):
300             self.ip_version = kwargs['ip_version']
301         else:
302             self.ip_version = 4
303
304         # Optional attributes that can be set after instantiation
305         self.name = kwargs.get('name')
306         self.project_name = kwargs.get('project_name')
307         self.start = kwargs.get('start')
308         self.end = kwargs.get('end')
309         self.gateway_ip = kwargs.get('gateway_ip')
310         self.enable_dhcp = kwargs.get('enable_dhcp')
311
312         if kwargs.get('dns_nameservers'):
313             self.dns_nameservers = kwargs.get('dns_nameservers')
314         else:
315             self.dns_nameservers = ['8.8.8.8']
316
317         self.host_routes = kwargs.get('host_routes')
318         self.destination = kwargs.get('destination')
319         self.nexthop = kwargs.get('nexthop')
320         self.ipv6_ra_mode = kwargs.get('ipv6_ra_mode')
321         self.ipv6_address_mode = kwargs.get('ipv6_address_mode')
322
323         if not self.name or not self.cidr:
324             raise SubnetSettingsError('Name and cidr required for subnets')
325
326     def dict_for_neutron(self, os_creds, network=None):
327         """
328         Returns a dictionary object representing this object.
329         This is meant to be converted into JSON designed for use by the Neutron
330         API
331         :param os_creds: the OpenStack credentials
332         :param network: The network object on which the subnet will be created
333                         (optional)
334         :return: the dictionary object
335         """
336         out = {
337             'cidr': self.cidr,
338             'ip_version': self.ip_version,
339         }
340
341         if network:
342             out['network_id'] = network.id
343         if self.name:
344             out['name'] = self.name
345         if self.project_name:
346             keystone = keystone_utils.keystone_client(os_creds)
347             project = keystone_utils.get_project(
348                 keystone=keystone, project_name=self.project_name)
349             project_id = None
350             if project:
351                 project_id = project.id
352             if project_id:
353                 out['tenant_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(
467                     neutron, subnet_name=ip_addr_dict['subnet_name'])
468                 if subnet and 'ip' in ip_addr_dict:
469                     self.fixed_ips.append({'ip_address': ip_addr_dict['ip'],
470                                            'subnet_id': subnet.id})
471                 else:
472                     raise PortSettingsError(
473                         'Invalid port configuration, subnet does not exist '
474                         'with name - ' + ip_addr_dict['subnet_name'])
475
476     def dict_for_neutron(self, neutron, os_creds):
477         """
478         Returns a dictionary object representing this object.
479         This is meant to be converted into JSON designed for use by the Neutron
480         API
481
482         TODO - expand automated testing to exercise all parameters
483         :param neutron: the Neutron client
484         :param os_creds: the OpenStack credentials
485         :return: the dictionary object
486         """
487         self.__set_fixed_ips(neutron)
488
489         out = dict()
490
491         project_id = None
492         if self.project_name:
493             keystone = keystone_utils.keystone_client(os_creds)
494             project = keystone_utils.get_project(
495                 keystone=keystone, project_name=self.project_name)
496             if project:
497                 project_id = project.id
498
499         if not self.network:
500             self.network = neutron_utils.get_network(
501                 neutron, network_name=self.network_name, project_id=project_id)
502         if not self.network:
503             raise PortSettingsError(
504                 'Cannot locate network with name - ' + self.network_name)
505
506         out['network_id'] = self.network.id
507
508         if self.admin_state_up is not None:
509             out['admin_state_up'] = self.admin_state_up
510         if self.name:
511             out['name'] = self.name
512         if self.project_name:
513             if project_id:
514                 out['tenant_id'] = project_id
515             else:
516                 raise PortSettingsError(
517                     'Could not find project ID for project named - ' +
518                     self.project_name)
519         if self.mac_address:
520             out['mac_address'] = self.mac_address
521         if self.fixed_ips and len(self.fixed_ips) > 0:
522             out['fixed_ips'] = self.fixed_ips
523         if self.security_groups:
524             out['security_groups'] = self.security_groups
525         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
526             out['allowed_address_pairs'] = self.allowed_address_pairs
527         if self.opt_value:
528             out['opt_value'] = self.opt_value
529         if self.opt_name:
530             out['opt_name'] = self.opt_name
531         if self.device_owner:
532             out['device_owner'] = self.device_owner
533         if self.device_id:
534             out['device_id'] = self.device_id
535         return {'port': out}
536
537     def __eq__(self, other):
538         return (self.name == other.name and
539                 self.network_name == other.network_name and
540                 self.admin_state_up == other.admin_state_up and
541                 self.project_name == other.project_name and
542                 self.mac_address == other.mac_address and
543                 self.ip_addrs == other.ip_addrs and
544                 self.fixed_ips == other.fixed_ips and
545                 self.security_groups == other.security_groups and
546                 self.allowed_address_pairs == other.allowed_address_pairs and
547                 self.opt_value == other.opt_value and
548                 self.opt_name == other.opt_name and
549                 self.device_owner == other.device_owner and
550                 self.device_id == other.device_id)
551
552
553 class PortSettingsError(Exception):
554     """
555     Exception to be thrown when port settings attributes are incorrect
556     """