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