e7b002a2e279039bf4f5908e5a0238fdd7b6db5c
[snaps.git] / snaps / openstack / utils / neutron_utils.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 logging
16
17 from neutronclient.common.exceptions import NotFound
18 from neutronclient.neutron.client import Client
19
20 from snaps.domain.network import (
21     Port, SecurityGroup, SecurityGroupRule, Router, InterfaceRouter, Subnet,
22     Network)
23 from snaps.domain.vm_inst import FloatingIp
24 from snaps.openstack.utils import keystone_utils
25
26 __author__ = 'spisarski'
27
28 logger = logging.getLogger('neutron_utils')
29
30 """
31 Utilities for basic neutron API calls
32 """
33
34
35 def neutron_client(os_creds):
36     """
37     Instantiates and returns a client for communications with OpenStack's
38     Neutron server
39     :param os_creds: the credentials for connecting to the OpenStack remote API
40     :return: the client object
41     """
42     return Client(api_version=os_creds.network_api_version,
43                   session=keystone_utils.keystone_session(os_creds),
44                   region_name=os_creds.region_name)
45
46
47 def create_network(neutron, os_creds, network_settings):
48     """
49     Creates a network for OpenStack
50     :param neutron: the client
51     :param os_creds: the OpenStack credentials
52     :param network_settings: A dictionary containing the network configuration
53                              and is responsible for creating the network
54                             request JSON body
55     :return: a SNAPS-OO Network domain object
56     """
57     if neutron and network_settings:
58         logger.info('Creating network with name ' + network_settings.name)
59         json_body = network_settings.dict_for_neutron(os_creds)
60         os_network = neutron.create_network(body=json_body)
61         return Network(**os_network['network'])
62     else:
63         raise NeutronException('Failded to create network')
64
65
66 def delete_network(neutron, network):
67     """
68     Deletes a network for OpenStack
69     :param neutron: the client
70     :param network: a SNAPS-OO Network domain object
71     """
72     if neutron and network:
73         logger.info('Deleting network with name ' + network.name)
74         neutron.delete_network(network.id)
75
76
77 def get_network(neutron, network_settings=None, network_name=None,
78                 project_id=None):
79     """
80     Returns Network SNAPS-OO domain object the first network found with
81     either the given attributes from the network_settings object if not None,
82     else the query will use just the name from the network_name parameter.
83     When the project_id is included, that will be added to the query filter.
84     :param neutron: the client
85     :param network_settings: the NetworkSettings object used to create filter
86     :param network_name: the name of the network to retrieve
87     :param project_id: the id of the network's project
88     :return: a SNAPS-OO Network domain object
89     """
90     net_filter = dict()
91     if network_settings:
92         net_filter['name'] = network_settings.name
93     elif network_name:
94         net_filter['name'] = network_name
95
96     if project_id:
97         net_filter['project_id'] = project_id
98
99     networks = neutron.list_networks(**net_filter)
100     for network, netInsts in networks.items():
101         for inst in netInsts:
102             return Network(**inst)
103
104
105 def get_network_by_id(neutron, network_id):
106     """
107     Returns the network object (dictionary) with the given ID else None
108     :param neutron: the client
109     :param network_id: the id of the network to retrieve
110     :return: a SNAPS-OO Network domain object
111     """
112     networks = neutron.list_networks(**{'id': network_id})
113     for network in networks['networks']:
114         if network['id'] == network_id:
115             return Network(**network)
116
117
118 def create_subnet(neutron, subnet_settings, os_creds, network=None):
119     """
120     Creates a network subnet for OpenStack
121     :param neutron: the client
122     :param network: the network object
123     :param subnet_settings: A dictionary containing the subnet configuration
124                             and is responsible for creating the subnet request
125                             JSON body
126     :param os_creds: the OpenStack credentials
127     :return: a SNAPS-OO Subnet domain object
128     """
129     if neutron and network and subnet_settings:
130         json_body = {'subnets': [subnet_settings.dict_for_neutron(
131             os_creds, network=network)]}
132         logger.info('Creating subnet with name ' + subnet_settings.name)
133         subnets = neutron.create_subnet(body=json_body)
134         return Subnet(**subnets['subnets'][0])
135     else:
136         raise NeutronException('Failed to create subnet')
137
138
139 def delete_subnet(neutron, subnet):
140     """
141     Deletes a network subnet for OpenStack
142     :param neutron: the client
143     :param subnet: a SNAPS-OO Subnet domain object
144     """
145     if neutron and subnet:
146         logger.info('Deleting subnet with name ' + subnet.name)
147         neutron.delete_subnet(subnet.id)
148
149
150 def get_subnet(neutron, subnet_settings=None, subnet_name=None):
151     """
152     Returns the first subnet object that fits the query else None including
153     if subnet_settings or subnet_name parameters are None.
154     :param neutron: the client
155     :param subnet_settings: the subnet settings of the object to retrieve
156     :param subnet_name: the name of the subnet to retrieve
157     :return: a SNAPS-OO Subnet domain object or None
158     """
159     sub_filter = dict()
160     if subnet_settings:
161         sub_filter['name'] = subnet_settings.name
162         sub_filter['cidr'] = subnet_settings.cidr
163         if subnet_settings.gateway_ip:
164             sub_filter['gateway_ip'] = subnet_settings.gateway_ip
165
166         if subnet_settings.enable_dhcp is not None:
167             sub_filter['enable_dhcp'] = subnet_settings.enable_dhcp
168
169         if subnet_settings.destination:
170             sub_filter['destination'] = subnet_settings.destination
171
172         if subnet_settings.nexthop:
173             sub_filter['nexthop'] = subnet_settings.nexthop
174
175         if subnet_settings.ipv6_ra_mode:
176             sub_filter['ipv6_ra_mode'] = subnet_settings.ipv6_ra_mode
177
178         if subnet_settings.ipv6_address_mode:
179             sub_filter['ipv6_address_mode'] = subnet_settings.ipv6_address_mode
180     elif subnet_name:
181         sub_filter['name'] = subnet_name
182     else:
183         return None
184
185     subnets = neutron.list_subnets(**sub_filter)
186     for subnet in subnets['subnets']:
187         return Subnet(**subnet)
188
189
190 def create_router(neutron, os_creds, router_settings):
191     """
192     Creates a router for OpenStack
193     :param neutron: the client
194     :param os_creds: the OpenStack credentials
195     :param router_settings: A dictionary containing the router configuration
196                             and is responsible for creating the subnet request
197                             JSON body
198     :return: a SNAPS-OO Router domain object
199     """
200     if neutron:
201         json_body = router_settings.dict_for_neutron(neutron, os_creds)
202         logger.info('Creating router with name - ' + router_settings.name)
203         os_router = neutron.create_router(json_body)
204         return Router(**os_router['router'])
205     else:
206         logger.error("Failed to create router.")
207         raise NeutronException('Failed to create router')
208
209
210 def delete_router(neutron, router):
211     """
212     Deletes a router for OpenStack
213     :param neutron: the client
214     :param router: a SNAPS-OO Router domain object
215     """
216     if neutron and router:
217         logger.info('Deleting router with name - ' + router.name)
218         neutron.delete_router(router=router.id)
219
220
221 def get_router(neutron, router_settings=None, router_name=None):
222     """
223     Returns the first router object (dictionary) found the given the settings
224     values if not None, else finds the first with the value of the router_name
225     parameter, else None
226     :param neutron: the client
227     :param router_settings: the RouterSettings object
228     :param router_name: the name of the network to retrieve
229     :return: a SNAPS-OO Router domain object
230     """
231     router_filter = dict()
232     if router_settings:
233         router_filter['name'] = router_settings.name
234         if router_settings.admin_state_up is not None:
235             router_filter['admin_state_up'] = router_settings.admin_state_up
236     elif router_name:
237         router_filter['name'] = router_name
238     else:
239         return None
240
241     routers = neutron.list_routers(**router_filter)
242     for routerInst in routers['routers']:
243         return Router(**routerInst)
244     return None
245
246
247 def add_interface_router(neutron, router, subnet=None, port=None):
248     """
249     Adds an interface router for OpenStack for either a subnet or port.
250     Exception will be raised if requesting for both.
251     :param neutron: the client
252     :param router: the router object
253     :param subnet: the subnet object
254     :param port: the port object
255     :return: the interface router object
256     """
257     if subnet and port:
258         raise NeutronException(
259             'Cannot add interface to the router. Both subnet and '
260             'port were sent in. Either or please.')
261
262     if neutron and router and (router or subnet):
263         logger.info('Adding interface to router with name ' + router.name)
264         os_intf_router = neutron.add_interface_router(
265             router=router.id, body=__create_port_json_body(subnet, port))
266         return InterfaceRouter(**os_intf_router)
267     else:
268         raise NeutronException(
269             'Unable to create interface router as neutron client,'
270             ' router or subnet were not created')
271
272
273 def remove_interface_router(neutron, router, subnet=None, port=None):
274     """
275     Removes an interface router for OpenStack
276     :param neutron: the client
277     :param router: the SNAPS-OO Router domain object
278     :param subnet: the subnet object (either subnet or port, not both)
279     :param port: the port object
280     """
281     if router:
282         try:
283             logger.info('Removing router interface from router named ' +
284                         router.name)
285             neutron.remove_interface_router(
286                 router=router.id,
287                 body=__create_port_json_body(subnet, port))
288         except NotFound as e:
289             logger.warning('Could not remove router interface. NotFound - %s',
290                            e)
291             pass
292     else:
293         logger.warning('Could not remove router interface, No router object')
294
295
296 def __create_port_json_body(subnet=None, port=None):
297     """
298     Returns the dictionary required for creating and deleting router
299     interfaces. Will only work on a subnet or port object. Will throw and
300     exception if parameters contain both or neither
301     :param subnet: the subnet object
302     :param port: the port object
303     :return: the dict
304     """
305     if subnet and port:
306         raise NeutronException(
307             'Cannot create JSON body with both subnet and port')
308     if not subnet and not port:
309         raise NeutronException(
310             'Cannot create JSON body without subnet or port')
311
312     if subnet:
313         return {"subnet_id": subnet.id}
314     else:
315         return {"port_id": port.id}
316
317
318 def create_port(neutron, os_creds, port_settings):
319     """
320     Creates a port for OpenStack
321     :param neutron: the client
322     :param os_creds: the OpenStack credentials
323     :param port_settings: the settings object for port configuration
324     :return: the SNAPS-OO Port domain object
325     """
326     json_body = port_settings.dict_for_neutron(neutron, os_creds)
327     logger.info('Creating port for network with name - %s',
328                 port_settings.network_name)
329     os_port = neutron.create_port(body=json_body)['port']
330     return Port(name=os_port['name'], id=os_port['id'],
331                 ips=os_port['fixed_ips'],
332                 mac_address=os_port['mac_address'],
333                 allowed_address_pairs=os_port['allowed_address_pairs'])
334
335
336 def delete_port(neutron, port):
337     """
338     Removes an OpenStack port
339     :param neutron: the client
340     :param port: the SNAPS-OO Port domain object
341     """
342     logger.info('Deleting port with name ' + port.name)
343     neutron.delete_port(port.id)
344
345
346 def get_port(neutron, port_settings=None, port_name=None):
347     """
348     Returns the first port object (dictionary) found for the given query
349     :param neutron: the client
350     :param port_settings: the PortSettings object used for generating the query
351     :param port_name: if port_settings is None, this name is the value to place
352                       into the query
353     :return: a SNAPS-OO Port domain object
354     """
355     port_filter = dict()
356
357     if port_settings:
358         port_filter['name'] = port_settings.name
359         if port_settings.admin_state_up:
360             port_filter['admin_state_up'] = port_settings.admin_state_up
361         if port_settings.device_id:
362             port_filter['device_id'] = port_settings.device_id
363         if port_settings.mac_address:
364             port_filter['mac_address'] = port_settings.mac_address
365     elif port_name:
366         port_filter['name'] = port_name
367
368     ports = neutron.list_ports(**port_filter)
369     for port in ports['ports']:
370         return Port(name=port['name'], id=port['id'],
371                     ips=port['fixed_ips'], mac_address=port['mac_address'])
372     return None
373
374
375 def create_security_group(neutron, keystone, sec_grp_settings):
376     """
377     Creates a security group object in OpenStack
378     :param neutron: the Neutron client
379     :param keystone: the Keystone client
380     :param sec_grp_settings: the security group settings
381     :return: a SNAPS-OO SecurityGroup domain object
382     """
383     logger.info('Creating security group with name - %s',
384                 sec_grp_settings.name)
385     os_group = neutron.create_security_group(
386         sec_grp_settings.dict_for_neutron(keystone))
387     return SecurityGroup(**os_group['security_group'])
388
389
390 def delete_security_group(neutron, sec_grp):
391     """
392     Deletes a security group object from OpenStack
393     :param neutron: the client
394     :param sec_grp: the SNAPS SecurityGroup object to delete
395     """
396     logger.info('Deleting security group with name - %s', sec_grp.name)
397     neutron.delete_security_group(sec_grp.id)
398
399
400 def get_security_group(neutron, sec_grp_settings=None, sec_grp_name=None,
401                        project_id=None):
402     """
403     Returns the first security group for a given query. The query gets built
404     from the sec_grp_settings parameter if not None, else only the name of
405     the security group will be used, else if the query parameters are None then
406     None will be returned
407     :param neutron: the client
408     :param sec_grp_settings: an instance of SecurityGroupSettings config object
409     :param sec_grp_name: the name of security group object to retrieve
410     :param project_id: the ID of the project/tentant object that owns the
411                        secuity group to retrieve
412     :return: a SNAPS-OO SecurityGroup domain object or None if not found
413     """
414
415     sec_grp_filter = dict()
416     if project_id:
417         sec_grp_filter['tenant_id'] = project_id
418
419     if sec_grp_settings:
420         sec_grp_filter['name'] = sec_grp_settings.name
421
422         if sec_grp_settings.description:
423             sec_grp_filter['description'] = sec_grp_settings.description
424     elif sec_grp_name:
425         sec_grp_filter['name'] = sec_grp_name
426     else:
427         return None
428
429     groups = neutron.list_security_groups(**sec_grp_filter)
430     for group in groups['security_groups']:
431         return SecurityGroup(**group)
432
433
434 def get_security_group_by_id(neutron, sec_grp_id):
435     """
436     Returns the first security group object of the given name else None
437     :param neutron: the client
438     :param sec_grp_id: the id of the security group to retrieve
439     :return: a SNAPS-OO SecurityGroup domain object or None if not found
440     """
441     logger.info('Retrieving security group with ID - ' + sec_grp_id)
442
443     groups = neutron.list_security_groups(**{'id': sec_grp_id})
444     for group in groups['security_groups']:
445         if group['id'] == sec_grp_id:
446             return SecurityGroup(**group)
447     return None
448
449
450 def create_security_group_rule(neutron, sec_grp_rule_settings):
451     """
452     Creates a security group object in OpenStack
453     :param neutron: the client
454     :param sec_grp_rule_settings: the security group rule settings
455     :return: a SNAPS-OO SecurityGroupRule domain object
456     """
457     logger.info('Creating security group to security group - %s',
458                 sec_grp_rule_settings.sec_grp_name)
459     os_rule = neutron.create_security_group_rule(
460         sec_grp_rule_settings.dict_for_neutron(neutron))
461     return SecurityGroupRule(**os_rule['security_group_rule'])
462
463
464 def delete_security_group_rule(neutron, sec_grp_rule):
465     """
466     Deletes a security group object from OpenStack
467     :param neutron: the client
468     :param sec_grp_rule: the SNAPS SecurityGroupRule object to delete
469     """
470     logger.info('Deleting security group rule with ID - %s',
471                 sec_grp_rule.id)
472     neutron.delete_security_group_rule(sec_grp_rule.id)
473
474
475 def get_rules_by_security_group(neutron, sec_grp):
476     """
477     Retrieves all of the rules for a given security group
478     :param neutron: the client
479     :param sec_grp: a list of SNAPS SecurityGroupRule domain objects
480     """
481     logger.info('Retrieving security group rules associate with the '
482                 'security group - %s', sec_grp.name)
483     out = list()
484     rules = neutron.list_security_group_rules(
485         **{'security_group_id': sec_grp.id})
486     for rule in rules['security_group_rules']:
487         if rule['security_group_id'] == sec_grp.id:
488             out.append(SecurityGroupRule(**rule))
489     return out
490
491
492 def get_rule_by_id(neutron, sec_grp, rule_id):
493     """
494     Deletes a security group object from OpenStack
495     :param neutron: the client
496     :param sec_grp: the SNAPS SecurityGroup domain object
497     :param rule_id: the rule's ID
498     :param sec_grp: a SNAPS SecurityGroupRule domain object
499     """
500     rules = neutron.list_security_group_rules(
501         **{'security_group_id': sec_grp.id})
502     for rule in rules['security_group_rules']:
503         if rule['id'] == rule_id:
504             return SecurityGroupRule(**rule)
505     return None
506
507
508 def get_external_networks(neutron):
509     """
510     Returns a list of external OpenStack network object/dict for all external
511     networks
512     :param neutron: the client
513     :return: a list of external networks of Type SNAPS-OO domain class Network
514     """
515     out = list()
516     for network in neutron.list_networks(
517             **{'router:external': True})['networks']:
518         out.append(Network(**network))
519     return out
520
521
522 def get_floating_ips(neutron, ports=None):
523     """
524     Returns all of the floating IPs
525     When ports is not None, FIPs returned must be associated with one of the
526     ports in the list and a tuple 2 where the first element being the port's
527     name and the second being the FloatingIp SNAPS-OO domain object.
528     When ports is None, all known FloatingIp SNAPS-OO domain objects will be
529     returned in a list
530     :param neutron: the Neutron client
531     :param ports: a list of SNAPS-OO Port objects to join
532     :return: a list of tuple 2 (port_name, SNAPS FloatingIp) objects when ports
533              is not None else a list of Port objects
534     """
535     out = list()
536     fips = neutron.list_floatingips()
537     for fip in fips['floatingips']:
538         if ports:
539             for port_name, port in ports:
540                 if fip['port_id'] == port.id:
541                     out.append((port.name, FloatingIp(
542                         inst_id=fip['id'], ip=fip['floating_ip_address'])))
543                     break
544         else:
545             out.append(FloatingIp(inst_id=fip['id'],
546                                   ip=fip['floating_ip_address']))
547
548     return out
549
550
551 def create_floating_ip(neutron, ext_net_name):
552     """
553     Returns the floating IP object that was created with this call
554     :param neutron: the Neutron client
555     :param ext_net_name: the name of the external network on which to apply the
556                          floating IP address
557     :return: the SNAPS FloatingIp object
558     """
559     logger.info('Creating floating ip to external network - ' + ext_net_name)
560     ext_net = get_network(neutron, network_name=ext_net_name)
561     if ext_net:
562         fip = neutron.create_floatingip(
563             body={'floatingip':
564                   {'floating_network_id': ext_net.id}})
565
566         return FloatingIp(inst_id=fip['floatingip']['id'],
567                           ip=fip['floatingip']['floating_ip_address'])
568     else:
569         raise NeutronException(
570             'Cannot create floating IP, external network not found')
571
572
573 def get_floating_ip(neutron, floating_ip):
574     """
575     Returns a floating IP object that should be identical to the floating_ip
576     parameter
577     :param neutron: the Neutron client
578     :param floating_ip: the SNAPS FloatingIp object
579     :return: hopefully the same floating IP object input
580     """
581     logger.debug('Attempting to retrieve existing floating ip with IP - %s',
582                  floating_ip.ip)
583     os_fip = __get_os_floating_ip(neutron, floating_ip)
584     if os_fip:
585         return FloatingIp(
586             inst_id=os_fip['id'], ip=os_fip['floating_ip_address'])
587
588
589 def __get_os_floating_ip(neutron, floating_ip):
590     """
591     Returns an OpenStack floating IP object
592     parameter
593     :param neutron: the Neutron client
594     :param floating_ip: the SNAPS FloatingIp object
595     :return: hopefully the same floating IP object input
596     """
597     logger.debug('Attempting to retrieve existing floating ip with IP - %s',
598                  floating_ip.ip)
599     fips = neutron.list_floatingips(ip=floating_ip.id)
600
601     for fip in fips['floatingips']:
602         if fip['id'] == floating_ip.id:
603             return fip
604
605
606 def delete_floating_ip(neutron, floating_ip):
607     """
608     Responsible for deleting a floating IP
609     :param neutron: the Neutron client
610     :param floating_ip: the SNAPS FloatingIp object
611     :return:
612     """
613     logger.debug('Attempting to delete existing floating ip with IP - %s',
614                  floating_ip.ip)
615     return neutron.delete_floatingip(floating_ip.id)
616
617
618 class NeutronException(Exception):
619     """
620     Exception when calls to the Keystone client cannot be served properly
621     """