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