Created domain classes for networks and subnets.
[snaps.git] / snaps / openstack / create_network.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 import logging
16
17 from neutronclient.common.exceptions import NotFound
18 from snaps.openstack.utils import keystone_utils, neutron_utils
19
20 __author__ = 'spisarski'
21
22 logger = logging.getLogger('OpenStackNetwork')
23
24
25 class OpenStackNetwork:
26     """
27     Class responsible for creating a network in OpenStack
28     """
29
30     def __init__(self, os_creds, network_settings):
31         """
32         Constructor - all parameters are required
33         :param os_creds: The credentials to connect with OpenStack
34         :param network_settings: The settings used to create a network
35         """
36         self.__os_creds = os_creds
37         self.network_settings = network_settings
38         self.__neutron = None
39
40         # Attributes instantiated on create()
41         self.__network = None
42         self.__subnets = list()
43
44     def create(self, cleanup=False):
45         """
46         Responsible for creating not only the network but then a private
47         subnet, router, and an interface to the router.
48         :param cleanup: When true, only perform lookups for OpenStack objects.
49         :return: the created network object or None
50         """
51         self.__neutron = neutron_utils.neutron_client(self.__os_creds)
52
53         logger.info(
54             'Creating neutron network %s...' % self.network_settings.name)
55         net_inst = neutron_utils.get_network(
56             self.__neutron, self.network_settings.name,
57             self.network_settings.get_project_id(self.__os_creds))
58         if net_inst:
59             self.__network = net_inst
60         else:
61             if not cleanup:
62                 self.__network = neutron_utils.create_network(
63                     self.__neutron, self.__os_creds, self.network_settings)
64             else:
65                 logger.info(
66                     'Network does not exist and will not create as in cleanup'
67                     ' mode')
68                 return
69         logger.debug(
70             "Network '%s' created successfully" % self.__network.id)
71
72         logger.debug('Creating Subnets....')
73         for subnet_setting in self.network_settings.subnet_settings:
74             sub_inst = neutron_utils.get_subnet_by_name(
75                 self.__neutron, subnet_setting.name)
76             if sub_inst:
77                 self.__subnets.append(sub_inst)
78                 logger.debug(
79                     "Subnet '%s' created successfully" % sub_inst.id)
80             else:
81                 if not cleanup:
82                     self.__subnets.append(
83                         neutron_utils.create_subnet(
84                             self.__neutron, subnet_setting, self.__os_creds,
85                             self.__network))
86
87         return self.__network
88
89     def clean(self):
90         """
91         Removes and deletes all items created in reverse order.
92         """
93         for subnet in self.__subnets:
94             try:
95                 logger.info(
96                     'Deleting subnet with name ' + subnet.name)
97                 neutron_utils.delete_subnet(self.__neutron, subnet)
98             except NotFound as e:
99                 logger.warning(
100                     'Error deleting subnet with message - ' + str(e))
101                 pass
102         self.__subnets = list()
103
104         if self.__network:
105             try:
106                 neutron_utils.delete_network(self.__neutron, self.__network)
107             except NotFound:
108                 pass
109
110             self.__network = None
111
112     def get_network(self):
113         """
114         Returns the created OpenStack network object
115         :return: the OpenStack network object
116         """
117         return self.__network
118
119     def get_subnets(self):
120         """
121         Returns the OpenStack subnet objects
122         :return:
123         """
124         return self.__subnets
125
126
127 class NetworkSettings:
128     """
129     Class representing a network configuration
130     """
131
132     def __init__(self, **kwargs):
133         """
134         Constructor - all parameters are optional
135         :param name: The network name.
136         :param admin_state_up: The administrative status of the network.
137                                True = up / False = down (default True)
138         :param shared: Boolean value indicating whether this network is shared
139                        across all projects/tenants. By default, only
140                        administrative users can change this value.
141         :param project_name: Admin-only. The name of the project that will own
142                              the network. This project can be different from
143                              the project that makes the create network request.
144                              However, only administrative users can specify a
145                              project ID other than their own. You cannot change
146                              this value through authorization policies.
147         :param external: when true, will setup an external network
148                          (default False).
149         :param network_type: the type of network (i.e. vlan|flat).
150         :param physical_network: the name of the physical network
151                                  (this is required when network_type is 'flat')
152         :param subnets or subnet_settings: List of SubnetSettings objects.
153         :return:
154         """
155
156         self.project_id = None
157
158         self.name = kwargs.get('name')
159         if kwargs.get('admin_state_up') is not None:
160             self.admin_state_up = bool(kwargs['admin_state_up'])
161         else:
162             self.admin_state_up = True
163
164         if kwargs.get('shared') is not None:
165             self.shared = bool(kwargs['shared'])
166         else:
167             self.shared = None
168
169         self.project_name = kwargs.get('project_name')
170
171         if kwargs.get('external') is not None:
172             self.external = bool(kwargs.get('external'))
173         else:
174             self.external = False
175
176         self.network_type = kwargs.get('network_type')
177         self.physical_network = kwargs.get('physical_network')
178
179         self.subnet_settings = list()
180         subnet_settings = kwargs.get('subnets')
181         if not subnet_settings:
182             subnet_settings = kwargs.get('subnet_settings')
183         if subnet_settings:
184             for subnet_config in subnet_settings:
185                 if isinstance(subnet_config, SubnetSettings):
186                     self.subnet_settings.append(subnet_config)
187                 else:
188                     self.subnet_settings.append(
189                         SubnetSettings(**subnet_config['subnet']))
190
191         if not self.name or len(self.name) < 1:
192             raise Exception('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(keystone,
206                                                      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['project_id'] = project_id
234             else:
235                 raise Exception(
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 SubnetSettings:
248     """
249     Class representing a subnet configuration
250     """
251
252     def __init__(self, **kwargs):
253         """
254         Constructor - all parameters are optional except cidr (subnet mask)
255         :param cidr: The CIDR. REQUIRED if config parameter is None
256         :param ip_version: The IP version, which is 4 or 6.
257         :param name: The subnet name.
258         :param project_name: The name of the project who owns the network.
259                              Only administrative users can specify a project ID
260                              other than their own. You cannot change this value
261                              through authorization policies.
262         :param start: The start address for the allocation pools.
263         :param end: The end address for the allocation pools.
264         :param gateway_ip: The gateway IP address.
265         :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
266                             disabled.
267         :param dns_nameservers: A list of DNS name servers for the subnet.
268                                 Specify each name server as an IP address
269                                 and separate multiple entries with a space.
270                                 For example [8.8.8.7 8.8.8.8].
271         :param host_routes: A list of host route dictionaries for the subnet.
272                             For example:
273                                 "host_routes":[
274                                     {
275                                         "destination":"0.0.0.0/0",
276                                         "nexthop":"123.456.78.9"
277                                     },
278                                     {
279                                         "destination":"192.168.0.0/24",
280                                         "nexthop":"192.168.0.1"
281                                     }
282                                 ]
283         :param destination: The destination for static route
284         :param nexthop: The next hop for the destination.
285         :param ipv6_ra_mode: A valid value is dhcpv6-stateful,
286                              dhcpv6-stateless, or slaac.
287         :param ipv6_address_mode: A valid value is dhcpv6-stateful,
288                                   dhcpv6-stateless, or slaac.
289         :raise: Exception when config does not have or cidr values are None
290         """
291         self.cidr = kwargs['cidr']
292         if kwargs.get('ip_version'):
293             self.ip_version = kwargs['ip_version']
294         else:
295             self.ip_version = 4
296
297         # Optional attributes that can be set after instantiation
298         self.name = kwargs.get('name')
299         self.project_name = kwargs.get('project_name')
300         self.start = kwargs.get('start')
301         self.end = kwargs.get('end')
302         self.gateway_ip = kwargs.get('gateway_ip')
303         self.enable_dhcp = kwargs.get('enable_dhcp')
304
305         if kwargs.get('dns_nameservers'):
306             self.dns_nameservers = kwargs.get('dns_nameservers')
307         else:
308             self.dns_nameservers = ['8.8.8.8']
309
310         self.host_routes = kwargs.get('host_routes')
311         self.destination = kwargs.get('destination')
312         self.nexthop = kwargs.get('nexthop')
313         self.ipv6_ra_mode = kwargs.get('ipv6_ra_mode')
314         self.ipv6_address_mode = kwargs.get('ipv6_address_mode')
315
316         if not self.name or not self.cidr:
317             raise Exception('Name and cidr required for subnets')
318
319     def dict_for_neutron(self, os_creds, network=None):
320         """
321         Returns a dictionary object representing this object.
322         This is meant to be converted into JSON designed for use by the Neutron
323         API
324         :param os_creds: the OpenStack credentials
325         :param network: The network object on which the subnet will be created
326                         (optional)
327         :return: the dictionary object
328         """
329         out = {
330             'cidr': self.cidr,
331             'ip_version': self.ip_version,
332         }
333
334         if network:
335             out['network_id'] = network.id
336         if self.name:
337             out['name'] = self.name
338         if self.project_name:
339             keystone = keystone_utils.keystone_client(os_creds)
340             project = keystone_utils.get_project(keystone, self.project_name)
341             project_id = None
342             if project:
343                 project_id = project.id
344             if project_id:
345                 out['project_id'] = project_id
346             else:
347                 raise Exception(
348                     'Could not find project ID for project named - ' +
349                     self.project_name)
350         if self.start and self.end:
351             out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
352         if self.gateway_ip:
353             out['gateway_ip'] = self.gateway_ip
354         if self.enable_dhcp is not None:
355             out['enable_dhcp'] = self.enable_dhcp
356         if self.dns_nameservers and len(self.dns_nameservers) > 0:
357             out['dns_nameservers'] = self.dns_nameservers
358         if self.host_routes and len(self.host_routes) > 0:
359             out['host_routes'] = self.host_routes
360         if self.destination:
361             out['destination'] = self.destination
362         if self.nexthop:
363             out['nexthop'] = self.nexthop
364         if self.ipv6_ra_mode:
365             out['ipv6_ra_mode'] = self.ipv6_ra_mode
366         if self.ipv6_address_mode:
367             out['ipv6_address_mode'] = self.ipv6_address_mode
368         return out
369
370
371 class PortSettings:
372     """
373     Class representing a port configuration
374     """
375
376     def __init__(self, **kwargs):
377         """
378         Constructor - all parameters are optional
379         :param name: A symbolic name for the port.
380         :param network_name: The name of the network on which to create the
381                              port.
382         :param admin_state_up: A boolean value denoting the administrative
383                                status of the port. True = up / False = down
384         :param project_name: The name of the project who owns the network.
385                              Only administrative users can specify a project ID
386                              other than their own. You cannot change this value
387                              through authorization policies.
388         :param mac_address: The MAC address. If you specify an address that is
389                             not valid, a Bad Request (400) status code is
390                             returned. If you do not specify a MAC address,
391                             OpenStack Networking tries to allocate one. If a
392                             failure occurs, a Service Unavailable (503) status
393                             code is returned.
394         :param ip_addrs: A list of dict objects where each contains two keys
395                          'subnet_name' and 'ip' values which will get mapped to
396                          self.fixed_ips. These values will be directly
397                          translated into the fixed_ips dict
398         :param fixed_ips: A dict where the key is the subnet IDs and value is
399                           the IP address to assign to the port
400         :param security_groups: One or more security group IDs.
401         :param allowed_address_pairs: A dictionary containing a set of zero or
402                                       more allowed address pairs. An address
403                                       pair contains an IP address and MAC
404                                       address.
405         :param opt_value: The extra DHCP option value.
406         :param opt_name: The extra DHCP option name.
407         :param device_owner: The ID of the entity that uses this port.
408                              For example, a DHCP agent.
409         :param device_id: The ID of the device that uses this port.
410                           For example, a virtual server.
411         :return:
412         """
413         if 'port' in kwargs:
414             kwargs = kwargs['port']
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.id})
458                 else:
459                     raise Exception(
460                         'Invalid port configuration, subnet does not exist '
461                         'with name - ' + ip_addr_dict['subnet_name'])
462
463     def dict_for_neutron(self, neutron, os_creds):
464         """
465         Returns a dictionary object representing this object.
466         This is meant to be converted into JSON designed for use by the Neutron
467         API
468
469         TODO - expand automated testing to exercise all parameters
470         :param neutron: the Neutron client
471         :param os_creds: the OpenStack credentials
472         :return: the dictionary object
473         """
474         self.__set_fixed_ips(neutron)
475
476         out = dict()
477
478         project_id = None
479         if self.project_name:
480             keystone = keystone_utils.keystone_client(os_creds)
481             project = keystone_utils.get_project(keystone, self.project_name)
482             if project:
483                 project_id = project.id
484
485         if not self.network:
486             self.network = neutron_utils.get_network(neutron,
487                                                      self.network_name,
488                                                      project_id)
489         if not self.network:
490             raise Exception(
491                 'Cannot locate network with name - ' + self.network_name)
492
493         out['network_id'] = self.network.id
494
495         if self.admin_state_up is not None:
496             out['admin_state_up'] = self.admin_state_up
497         if self.name:
498             out['name'] = self.name
499         if self.project_name:
500             if project_id:
501                 out['project_id'] = project_id
502             else:
503                 raise Exception(
504                     'Could not find project ID for project named - ' +
505                     self.project_name)
506         if self.mac_address:
507             out['mac_address'] = self.mac_address
508         if self.fixed_ips and len(self.fixed_ips) > 0:
509             out['fixed_ips'] = self.fixed_ips
510         if self.security_groups:
511             out['security_groups'] = self.security_groups
512         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
513             out['allowed_address_pairs'] = self.allowed_address_pairs
514         if self.opt_value:
515             out['opt_value'] = self.opt_value
516         if self.opt_name:
517             out['opt_name'] = self.opt_name
518         if self.device_owner:
519             out['device_owner'] = self.device_owner
520         if self.device_id:
521             out['device_id'] = self.device_id
522         return {'port': out}
523
524     def __eq__(self, other):
525         return (self.name == other.name and
526                 self.network_name == other.network_name and
527                 self.admin_state_up == other.admin_state_up and
528                 self.project_name == other.project_name and
529                 self.mac_address == other.mac_address and
530                 self.ip_addrs == other.ip_addrs and
531                 self.fixed_ips == other.fixed_ips and
532                 self.security_groups == other.security_groups and
533                 self.allowed_address_pairs == other.allowed_address_pairs and
534                 self.opt_value == other.opt_value and
535                 self.opt_name == other.opt_name and
536                 self.device_owner == other.device_owner and
537                 self.device_id == other.device_id)