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