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