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