Ensure project for routers are handled properly.
[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). When not
179                            configured, the IP address will be automatically
180                            assigned; when 'none', no gateway address will be
181                            assigned, else the value must be valid
182         :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
183                             disabled (optional)
184         :param dns_nameservers: A list of DNS name servers for the subnet.
185                                 Specify each name server as an IP address
186                                 and separate multiple entries with a space.
187                                 For example [8.8.8.7 8.8.8.8]
188                                 (default [])
189         :param host_routes: A list of host route dictionaries for the subnet.
190                             For example:
191                                 "host_routes":[
192                                     {
193                                         "destination":"0.0.0.0/0",
194                                         "nexthop":"123.456.78.9"
195                                     },
196                                     {
197                                         "destination":"192.168.0.0/24",
198                                         "nexthop":"192.168.0.1"
199                                     }
200                                 ]
201         :param destination: The destination for static route (optional)
202         :param nexthop: The next hop for the destination (optional)
203         :param ipv6_ra_mode: an instance of the IPv6Mode enum
204                              (optional when enable_dhcp is True)
205         :param ipv6_address_mode: an instance of the IPv6Mode enum
206                                   (optional when enable_dhcp is True)
207         :raise: SubnetConfigError when config does not have or cidr values
208                 are None
209         """
210         self.cidr = kwargs.get('cidr')
211         if kwargs.get('ip_version'):
212             self.ip_version = kwargs['ip_version']
213         else:
214             self.ip_version = 4
215
216         # Optional attributes that can be set after instantiation
217         self.name = kwargs.get('name')
218         self.project_name = kwargs.get('project_name')
219         self.start = kwargs.get('start')
220         self.end = kwargs.get('end')
221         self.gateway_ip = kwargs.get('gateway_ip')
222         self.enable_dhcp = kwargs.get('enable_dhcp')
223
224         if 'dns_nameservers' in kwargs:
225             self.dns_nameservers = kwargs.get('dns_nameservers')
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             if self.gateway_ip == 'none':
274                 out['gateway_ip'] = None
275             else:
276                 out['gateway_ip'] = self.gateway_ip
277         if self.enable_dhcp is not None:
278             out['enable_dhcp'] = self.enable_dhcp
279         if self.dns_nameservers and len(self.dns_nameservers) > 0:
280             out['dns_nameservers'] = self.dns_nameservers
281         if self.host_routes and len(self.host_routes) > 0:
282             out['host_routes'] = self.host_routes
283         if self.destination:
284             out['destination'] = self.destination
285         if self.nexthop:
286             out['nexthop'] = self.nexthop
287         if self.ipv6_ra_mode:
288             out['ipv6_ra_mode'] = self.ipv6_ra_mode.value
289         if self.ipv6_address_mode:
290             out['ipv6_address_mode'] = self.ipv6_address_mode.value
291         return out
292
293
294 def map_mode(mode):
295     """
296     Takes a the direction value maps it to the Direction enum. When None return
297     None
298     :param mode: the mode value
299     :return: the IPv6Mode enum object
300     :raise: SubnetConfigError if value is invalid
301     """
302     if not mode:
303         return None
304     if isinstance(mode, IPv6Mode):
305         return mode
306     elif isinstance(mode, str):
307         mode_str = str(mode)
308         if mode_str == 'slaac':
309             return IPv6Mode.slaac
310         elif mode_str == 'dhcpv6-stateful':
311             return IPv6Mode.stateful
312         elif mode_str == 'stateful':
313             return IPv6Mode.stateful
314         elif mode_str == 'dhcpv6-stateless':
315             return IPv6Mode.stateless
316         elif mode_str == 'stateless':
317             return IPv6Mode.stateless
318         else:
319             raise SubnetConfigError('Invalid mode - ' + mode_str)
320     else:
321         return map_mode(mode.value)
322
323
324 class SubnetConfigError(Exception):
325     """
326     Exception to be thrown when subnet settings attributes are incorrect
327     """
328
329
330 class PortConfig(object):
331     """
332     Class representing a port configuration
333     """
334
335     def __init__(self, **kwargs):
336         """
337         Constructor
338         :param name: A symbolic name for the port (optional).
339         :param network_name: The name of the network on which to create the
340                              port (required).
341         :param admin_state_up: A boolean value denoting the administrative
342                                status of the port (default = True)
343         :param project_name: The name of the project who owns the network.
344                              Only administrative users can specify a project ID
345                              other than their own. You cannot change this value
346                              through authorization policies (optional)
347         :param mac_address: The MAC address. If you specify an address that is
348                             not valid, a Bad Request (400) status code is
349                             returned. If you do not specify a MAC address,
350                             OpenStack Networking tries to allocate one. If a
351                             failure occurs, a Service Unavailable (503) status
352                             code is returned (optional)
353         :param ip_addrs: A list of dict objects where each contains two keys
354                          'subnet_name' and 'ip' values which will get mapped to
355                          self.fixed_ips. These values will be directly
356                          translated into the fixed_ips dict (optional)
357         :param security_groups: One or more security group IDs.
358         :param port_security_enabled: When True, security groups will be
359                                       applied to the port else not
360                                       (default - True)
361         :param allowed_address_pairs: A dictionary containing a set of zero or
362                                       more allowed address pairs. An address
363                                       pair contains an IP address and MAC
364                                       address (optional)
365         :param opt_value: The extra DHCP option value (optional)
366         :param opt_name: The extra DHCP option name (optional)
367         :param device_owner: The ID of the entity that uses this port.
368                              For example, a DHCP agent (optional)
369         :param device_id: The ID of the device that uses this port.
370                           For example, a virtual server (optional)
371         :param extra_dhcp_opts: k/v of options to use with your DHCP (optional)
372         :return:
373         """
374         if 'port' in kwargs:
375             kwargs = kwargs['port']
376
377         self.name = kwargs.get('name')
378         self.network_name = kwargs.get('network_name')
379
380         if kwargs.get('admin_state_up') is not None:
381             self.admin_state_up = str2bool(str(kwargs['admin_state_up']))
382         else:
383             self.admin_state_up = True
384
385         self.project_name = kwargs.get('project_name')
386         self.mac_address = kwargs.get('mac_address')
387         self.ip_addrs = kwargs.get('ip_addrs')
388         self.security_groups = kwargs.get('security_groups')
389
390         if kwargs.get('port_security_enabled') is not None:
391             self.port_security_enabled = str2bool(
392                 str(kwargs['port_security_enabled']))
393         else:
394             self.port_security_enabled = None
395
396         self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
397         self.opt_value = kwargs.get('opt_value')
398         self.opt_name = kwargs.get('opt_name')
399         self.device_owner = kwargs.get('device_owner')
400         self.device_id = kwargs.get('device_id')
401         self.extra_dhcp_opts = kwargs.get('extra_dhcp_opts')
402
403         if not self.network_name:
404             raise PortConfigError(
405                 'The attribute network_name is required')
406
407     def __get_fixed_ips(self, neutron):
408         """
409         Sets the self.fixed_ips value
410         :param neutron: the Neutron client
411         :return: None
412         """
413
414         fixed_ips = list()
415         if self.ip_addrs:
416
417             for ip_addr_dict in self.ip_addrs:
418                 subnet = neutron_utils.get_subnet(
419                     neutron, subnet_name=ip_addr_dict['subnet_name'])
420                 if subnet and 'ip' in ip_addr_dict:
421                     fixed_ips.append({'ip_address': ip_addr_dict['ip'],
422                                       'subnet_id': subnet.id})
423                 else:
424                     raise PortConfigError(
425                         'Invalid port configuration, subnet does not exist '
426                         'with name - ' + ip_addr_dict['subnet_name'])
427
428         return fixed_ips
429
430     def dict_for_neutron(self, neutron, os_creds):
431         """
432         Returns a dictionary object representing this object.
433         This is meant to be converted into JSON designed for use by the Neutron
434         API
435
436         TODO - expand automated testing to exercise all parameters
437         :param neutron: the Neutron client
438         :param os_creds: the OpenStack credentials
439         :return: the dictionary object
440         """
441
442         out = dict()
443
444         keystone = keystone_utils.keystone_client(os_creds)
445         network = neutron_utils.get_network(
446             neutron, keystone, network_name=self.network_name,
447             project_name=self.project_name)
448         if not network:
449             raise PortConfigError(
450                 'Cannot locate network with name - ' + self.network_name
451                 + ' in project - ' + str(self.project_name))
452
453         out['network_id'] = network.id
454
455         if self.admin_state_up is not None:
456             out['admin_state_up'] = self.admin_state_up
457         if self.name:
458             out['name'] = self.name
459         if self.project_name:
460             project = keystone_utils.get_project(
461                 keystone=keystone, project_name=self.project_name)
462             project_id = None
463             if project:
464                 project_id = project.id
465             if project_id:
466                 out['tenant_id'] = project_id
467             else:
468                 raise PortConfigError(
469                     'Could not find project ID for project named - ' +
470                     self.project_name)
471         if self.mac_address:
472             out['mac_address'] = self.mac_address
473
474         fixed_ips = self.__get_fixed_ips(neutron)
475         if fixed_ips and len(fixed_ips) > 0:
476             out['fixed_ips'] = fixed_ips
477
478         if self.security_groups:
479             sec_grp_ids = list()
480             for sec_grp_name in self.security_groups:
481                 sec_grp = neutron_utils.get_security_group(
482                     neutron, keystone, sec_grp_name=sec_grp_name,
483                     project_name=self.project_name)
484                 if sec_grp:
485                     sec_grp_ids.append(sec_grp.id)
486             out['security_groups'] = sec_grp_ids
487         if self.port_security_enabled is not None:
488             out['port_security_enabled'] = self.port_security_enabled
489         if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
490             out['allowed_address_pairs'] = self.allowed_address_pairs
491         if self.opt_value:
492             out['opt_value'] = self.opt_value
493         if self.opt_name:
494             out['opt_name'] = self.opt_name
495         if self.device_owner:
496             out['device_owner'] = self.device_owner
497         if self.device_id:
498             out['device_id'] = self.device_id
499         if self.extra_dhcp_opts:
500             out['extra_dhcp_opts'] = self.extra_dhcp_opts
501         return {'port': out}
502
503     def __eq__(self, other):
504         return (self.name == other.name and
505                 self.network_name == other.network_name and
506                 self.admin_state_up == other.admin_state_up and
507                 self.project_name == other.project_name and
508                 self.mac_address == other.mac_address and
509                 self.ip_addrs == other.ip_addrs and
510                 # self.fixed_ips == other.fixed_ips and
511                 self.security_groups == other.security_groups and
512                 self.allowed_address_pairs == other.allowed_address_pairs and
513                 self.opt_value == other.opt_value and
514                 self.opt_name == other.opt_name and
515                 self.device_owner == other.device_owner and
516                 self.device_id == other.device_id)
517
518
519 class PortConfigError(Exception):
520     """
521     Exception to be thrown when port settings attributes are incorrect
522     """