Merge "Auto Generated INFO.yaml file"
[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 os_creds: the OpenStack credentials
453         :return: the dictionary object
454         """
455         out = dict()
456         session = keystone_utils.keystone_session(os_creds)
457         keystone = keystone_utils.keystone_client(os_creds, session)
458
459         project_name = os_creds.project_name
460         if self.project_name:
461             project_name = project_name
462         try:
463             network = neutron_utils.get_network(
464                 neutron, keystone, network_name=self.network_name)
465             if network and not (network.shared or network.external):
466                 network = neutron_utils.get_network(
467                     neutron, keystone, network_name=self.network_name,
468                     project_name=project_name)
469         finally:
470             if session:
471                 keystone_utils.close_session(session)
472
473         if not network:
474             raise PortConfigError(
475                 'Cannot locate network with name - ' + self.network_name
476                 + ' in project - ' + str(project_name))
477
478         out['network_id'] = network.id
479
480         if self.admin_state_up is not None:
481             out['admin_state_up'] = self.admin_state_up
482         if self.name:
483             out['name'] = self.name
484         if self.project_name:
485             project = keystone_utils.get_project(
486                 keystone=keystone, project_name=self.project_name)
487             project_id = None
488             if project:
489                 project_id = project.id
490             if project_id:
491                 out['tenant_id'] = project_id
492             else:
493                 raise PortConfigError(
494                     'Could not find project ID for project named - ' +
495                     self.project_name)
496         if self.mac_address:
497             out['mac_address'] = self.mac_address
498
499         fixed_ips = self.__get_fixed_ips(neutron, network)
500         if fixed_ips and len(fixed_ips) > 0:
501             out['fixed_ips'] = fixed_ips
502
503         if self.security_groups:
504             sec_grp_ids = list()
505             for sec_grp_name in self.security_groups:
506                 sec_grp = neutron_utils.get_security_group(
507                     neutron, keystone, sec_grp_name=sec_grp_name,
508                     project_name=self.project_name)
509                 if sec_grp:
510                     sec_grp_ids.append(sec_grp.id)
511             out['security_groups'] = sec_grp_ids
512         if self.port_security_enabled is not None:
513             out['port_security_enabled'] = self.port_security_enabled
514         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
515             out['allowed_address_pairs'] = self.allowed_address_pairs
516         if self.opt_value:
517             out['opt_value'] = self.opt_value
518         if self.opt_name:
519             out['opt_name'] = self.opt_name
520         if self.device_owner:
521             out['device_owner'] = self.device_owner
522         if self.device_id:
523             out['device_id'] = self.device_id
524         if self.extra_dhcp_opts:
525             out['extra_dhcp_opts'] = self.extra_dhcp_opts
526         return {'port': out}
527
528     def __eq__(self, other):
529         return (self.name == other.name and
530                 self.network_name == other.network_name and
531                 self.admin_state_up == other.admin_state_up and
532                 self.project_name == other.project_name and
533                 self.mac_address == other.mac_address and
534                 self.ip_addrs == other.ip_addrs and
535                 # self.fixed_ips == other.fixed_ips and
536                 self.security_groups == other.security_groups and
537                 self.allowed_address_pairs == other.allowed_address_pairs and
538                 self.opt_value == other.opt_value and
539                 self.opt_name == other.opt_name and
540                 self.device_owner == other.device_owner and
541                 self.device_id == other.device_id)
542
543
544 class PortConfigError(Exception):
545     """
546     Exception to be thrown when port settings attributes are incorrect
547     """