f48cd276561edc69d0c9e663ef2a93fe6bc60e5f
[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 '8.8.8.8')
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             if self.ip_version == 4:
225                 self.dns_nameservers = ['8.8.8.8']
226             else:
227                 self.dns_nameservers = list()
228
229         self.host_routes = kwargs.get('host_routes')
230         self.destination = kwargs.get('destination')
231         self.nexthop = kwargs.get('nexthop')
232         self.ipv6_ra_mode = map_mode(kwargs.get('ipv6_ra_mode'))
233         self.ipv6_address_mode = map_mode(kwargs.get('ipv6_address_mode'))
234
235         if not self.name or not self.cidr:
236             raise SubnetConfigError('Name and cidr required for subnets')
237
238     def dict_for_neutron(self, os_creds, network=None):
239         """
240         Returns a dictionary object representing this object.
241         This is meant to be converted into JSON designed for use by the Neutron
242         API
243         :param os_creds: the OpenStack credentials
244         :param network: The network object on which the subnet will be created
245                         (optional)
246         :return: the dictionary object
247         """
248         out = {
249             'cidr': self.cidr,
250             'ip_version': self.ip_version,
251         }
252
253         if network:
254             out['network_id'] = network.id
255         if self.name:
256             out['name'] = self.name
257         if self.project_name:
258             keystone = keystone_utils.keystone_client(os_creds)
259             project = keystone_utils.get_project(
260                 keystone=keystone, project_name=self.project_name)
261             project_id = None
262             if project:
263                 project_id = project.id
264             if project_id:
265                 out['tenant_id'] = project_id
266             else:
267                 raise SubnetConfigError(
268                     'Could not find project ID for project named - ' +
269                     self.project_name)
270         if self.start and self.end:
271             out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
272         if self.gateway_ip:
273             out['gateway_ip'] = self.gateway_ip
274         if self.enable_dhcp is not None:
275             out['enable_dhcp'] = self.enable_dhcp
276         if self.dns_nameservers and len(self.dns_nameservers) > 0:
277             out['dns_nameservers'] = self.dns_nameservers
278         if self.host_routes and len(self.host_routes) > 0:
279             out['host_routes'] = self.host_routes
280         if self.destination:
281             out['destination'] = self.destination
282         if self.nexthop:
283             out['nexthop'] = self.nexthop
284         if self.ipv6_ra_mode:
285             out['ipv6_ra_mode'] = self.ipv6_ra_mode.value
286         if self.ipv6_address_mode:
287             out['ipv6_address_mode'] = self.ipv6_address_mode.value
288         return out
289
290
291 def map_mode(mode):
292     """
293     Takes a the direction value maps it to the Direction enum. When None return
294     None
295     :param mode: the mode value
296     :return: the IPv6Mode enum object
297     :raise: SubnetConfigError if value is invalid
298     """
299     if not mode:
300         return None
301     if isinstance(mode, IPv6Mode):
302         return mode
303     elif isinstance(mode, str):
304         mode_str = str(mode)
305         if mode_str == 'slaac':
306             return IPv6Mode.slaac
307         elif mode_str == 'dhcpv6-stateful':
308             return IPv6Mode.stateful
309         elif mode_str == 'stateful':
310             return IPv6Mode.stateful
311         elif mode_str == 'dhcpv6-stateless':
312             return IPv6Mode.stateless
313         elif mode_str == 'stateless':
314             return IPv6Mode.stateless
315         else:
316             raise SubnetConfigError('Invalid mode - ' + mode_str)
317     else:
318         return map_mode(mode.value)
319
320
321 class SubnetConfigError(Exception):
322     """
323     Exception to be thrown when subnet settings attributes are incorrect
324     """
325
326
327 class PortConfig(object):
328     """
329     Class representing a port configuration
330     """
331
332     def __init__(self, **kwargs):
333         """
334         Constructor
335         :param name: A symbolic name for the port (optional).
336         :param network_name: The name of the network on which to create the
337                              port (required).
338         :param admin_state_up: A boolean value denoting the administrative
339                                status of the port (default = True)
340         :param project_name: The name of the project who owns the network.
341                              Only administrative users can specify a project ID
342                              other than their own. You cannot change this value
343                              through authorization policies (optional)
344         :param mac_address: The MAC address. If you specify an address that is
345                             not valid, a Bad Request (400) status code is
346                             returned. If you do not specify a MAC address,
347                             OpenStack Networking tries to allocate one. If a
348                             failure occurs, a Service Unavailable (503) status
349                             code is returned (optional)
350         :param ip_addrs: A list of dict objects where each contains two keys
351                          'subnet_name' and 'ip' values which will get mapped to
352                          self.fixed_ips. These values will be directly
353                          translated into the fixed_ips dict (optional)
354         :param security_groups: One or more security group IDs.
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         :return:
366         """
367         if 'port' in kwargs:
368             kwargs = kwargs['port']
369
370         self.name = kwargs.get('name')
371         self.network_name = kwargs.get('network_name')
372
373         if kwargs.get('admin_state_up') is not None:
374             self.admin_state_up = str2bool(str(kwargs['admin_state_up']))
375         else:
376             self.admin_state_up = True
377
378         self.project_name = kwargs.get('project_name')
379         self.mac_address = kwargs.get('mac_address')
380         self.ip_addrs = kwargs.get('ip_addrs')
381         self.security_groups = kwargs.get('security_groups')
382         self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
383         self.opt_value = kwargs.get('opt_value')
384         self.opt_name = kwargs.get('opt_name')
385         self.device_owner = kwargs.get('device_owner')
386         self.device_id = kwargs.get('device_id')
387
388         if not self.network_name:
389             raise PortConfigError(
390                 'The attribute network_name is required')
391
392     def __get_fixed_ips(self, neutron):
393         """
394         Sets the self.fixed_ips value
395         :param neutron: the Neutron client
396         :return: None
397         """
398
399         fixed_ips = list()
400         if self.ip_addrs:
401
402             for ip_addr_dict in self.ip_addrs:
403                 subnet = neutron_utils.get_subnet(
404                     neutron, subnet_name=ip_addr_dict['subnet_name'])
405                 if subnet and 'ip' in ip_addr_dict:
406                     fixed_ips.append({'ip_address': ip_addr_dict['ip'],
407                                       'subnet_id': subnet.id})
408                 else:
409                     raise PortConfigError(
410                         'Invalid port configuration, subnet does not exist '
411                         'with name - ' + ip_addr_dict['subnet_name'])
412
413         return fixed_ips
414
415     def dict_for_neutron(self, neutron, os_creds):
416         """
417         Returns a dictionary object representing this object.
418         This is meant to be converted into JSON designed for use by the Neutron
419         API
420
421         TODO - expand automated testing to exercise all parameters
422         :param neutron: the Neutron client
423         :param os_creds: the OpenStack credentials
424         :return: the dictionary object
425         """
426
427         out = dict()
428
429         project_id = None
430         if self.project_name:
431             keystone = keystone_utils.keystone_client(os_creds)
432             project = keystone_utils.get_project(
433                 keystone=keystone, project_name=self.project_name)
434             if project:
435                 project_id = project.id
436
437         network = neutron_utils.get_network(
438             neutron, network_name=self.network_name, project_id=project_id)
439         if not network:
440             raise PortConfigError(
441                 'Cannot locate network with name - ' + self.network_name)
442
443         out['network_id'] = network.id
444
445         if self.admin_state_up is not None:
446             out['admin_state_up'] = self.admin_state_up
447         if self.name:
448             out['name'] = self.name
449         if self.project_name:
450             if project_id:
451                 out['tenant_id'] = project_id
452             else:
453                 raise PortConfigError(
454                     'Could not find project ID for project named - ' +
455                     self.project_name)
456         if self.mac_address:
457             out['mac_address'] = self.mac_address
458
459         fixed_ips = self.__get_fixed_ips(neutron)
460         if fixed_ips and len(fixed_ips) > 0:
461             out['fixed_ips'] = fixed_ips
462
463         if self.security_groups:
464             out['security_groups'] = self.security_groups
465         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
466             out['allowed_address_pairs'] = self.allowed_address_pairs
467         if self.opt_value:
468             out['opt_value'] = self.opt_value
469         if self.opt_name:
470             out['opt_name'] = self.opt_name
471         if self.device_owner:
472             out['device_owner'] = self.device_owner
473         if self.device_id:
474             out['device_id'] = self.device_id
475         return {'port': out}
476
477     def __eq__(self, other):
478         return (self.name == other.name and
479                 self.network_name == other.network_name and
480                 self.admin_state_up == other.admin_state_up and
481                 self.project_name == other.project_name and
482                 self.mac_address == other.mac_address and
483                 self.ip_addrs == other.ip_addrs and
484                 # self.fixed_ips == other.fixed_ips and
485                 self.security_groups == other.security_groups and
486                 self.allowed_address_pairs == other.allowed_address_pairs and
487                 self.opt_value == other.opt_value and
488                 self.opt_name == other.opt_name and
489                 self.device_owner == other.device_owner and
490                 self.device_id == other.device_id)
491
492
493 class PortConfigError(Exception):
494     """
495     Exception to be thrown when port settings attributes are incorrect
496     """