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