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