Added exception handling around adding security groups.
[snaps.git] / snaps / openstack / utils / nova_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
16 import logging
17
18 import os
19 import time
20 from cryptography.hazmat.backends import default_backend
21 from cryptography.hazmat.primitives import serialization
22 from cryptography.hazmat.primitives.asymmetric import rsa
23 from novaclient.client import Client
24 from novaclient.exceptions import NotFound, ClientException
25
26 from snaps import file_utils
27 from snaps.domain.flavor import Flavor
28 from snaps.domain.keypair import Keypair
29 from snaps.domain.project import ComputeQuotas
30 from snaps.domain.vm_inst import VmInst
31 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
32
33 __author__ = 'spisarski'
34
35 logger = logging.getLogger('nova_utils')
36
37 """
38 Utilities for basic OpenStack Nova API calls
39 """
40
41
42 def nova_client(os_creds):
43     """
44     Instantiates and returns a client for communications with OpenStack's Nova
45     server
46     :param os_creds: The connection credentials to the OpenStack API
47     :return: the client object
48     """
49     logger.debug('Retrieving Nova Client')
50     return Client(os_creds.compute_api_version,
51                   session=keystone_utils.keystone_session(os_creds),
52                   region_name=os_creds.region_name)
53
54
55 def create_server(nova, neutron, glance, instance_config, image_config,
56                   keypair_config=None):
57     """
58     Creates a VM instance
59     :param nova: the nova client (required)
60     :param neutron: the neutron client for retrieving ports (required)
61     :param glance: the glance client (required)
62     :param instance_config: the VMInstConfig object (required)
63     :param image_config: the VM's ImageConfig object (required)
64     :param keypair_config: the VM's KeypairConfig object (optional)
65     :return: a snaps.domain.VmInst object
66     """
67
68     ports = list()
69
70     for port_setting in instance_config.port_settings:
71         ports.append(neutron_utils.get_port(
72             neutron, port_settings=port_setting))
73     nics = []
74     for port in ports:
75         kv = dict()
76         kv['port-id'] = port.id
77         nics.append(kv)
78
79     logger.info('Creating VM with name - ' + instance_config.name)
80     keypair_name = None
81     if keypair_config:
82         keypair_name = keypair_config.name
83
84     flavor = get_flavor_by_name(nova, instance_config.flavor)
85     if not flavor:
86         raise NovaException(
87             'Flavor not found with name - %s', instance_config.flavor)
88
89     image = glance_utils.get_image(glance, image_settings=image_config)
90     if image:
91         userdata = None
92         if instance_config.userdata:
93             if isinstance(instance_config.userdata, str):
94                 userdata = instance_config.userdata + '\n'
95             elif (isinstance(instance_config.userdata, dict) and
96                   'script_file' in instance_config.userdata):
97                 try:
98                     userdata = file_utils.read_file(
99                         instance_config.userdata['script_file'])
100                 except Exception as e:
101                     logger.warn('error reading userdata file %s - %s',
102                                 instance_config.userdata, e)
103         args = {'name': instance_config.name,
104                 'flavor': flavor,
105                 'image': image,
106                 'nics': nics,
107                 'key_name': keypair_name,
108                 'security_groups':
109                     instance_config.security_group_names,
110                 'userdata': userdata}
111
112         if instance_config.availability_zone:
113             args['availability_zone'] = instance_config.availability_zone
114
115         server = nova.servers.create(**args)
116
117         return __map_os_server_obj_to_vm_inst(server)
118     else:
119         raise NovaException(
120             'Cannot create instance, image cannot be located with name %s',
121             image_config.name)
122
123
124 def get_server(nova, vm_inst_settings=None, server_name=None):
125     """
126     Returns a VmInst object for the first server instance found.
127     :param nova: the Nova client
128     :param vm_inst_settings: the VmInstanceConfig object from which to build
129                              the query if not None
130     :param server_name: the server with this name to return if vm_inst_settings
131                         is not None
132     :return: a snaps.domain.VmInst object or None if not found
133     """
134     search_opts = dict()
135     if vm_inst_settings:
136         search_opts['name'] = vm_inst_settings.name
137     elif server_name:
138         search_opts['name'] = server_name
139
140     servers = nova.servers.list(search_opts=search_opts)
141     for server in servers:
142         return __map_os_server_obj_to_vm_inst(server)
143
144
145 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
146     """
147     Returns a VmInst object for the first server instance found.
148     :param nova: the Nova client
149     :param vm_inst_settings: the VmInstanceConfig object from which to build
150                              the query if not None
151     :param server_name: the server with this name to return if vm_inst_settings
152                         is not None
153     :return: a snaps.domain.VmInst object or None if not found
154     """
155     search_opts = dict()
156     if vm_inst_settings:
157         search_opts['name'] = vm_inst_settings.name
158     elif server_name:
159         search_opts['name'] = server_name
160
161     servers = nova.servers.list(search_opts=search_opts)
162     for server in servers:
163         return server.links[0]
164
165
166 def __map_os_server_obj_to_vm_inst(os_server):
167     """
168     Returns a VmInst object for an OpenStack Server object
169     :param os_server: the OpenStack server object
170     :return: an equivalent SNAPS-OO VmInst domain object
171     """
172     sec_grp_names = list()
173     # VM must be active for 'security_groups' attr to be initialized
174     if hasattr(os_server, 'security_groups'):
175         for sec_group in os_server.security_groups:
176             if sec_group.get('name'):
177                 sec_grp_names.append(sec_group.get('name'))
178
179     volumes = None
180     if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
181         volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
182
183     return VmInst(
184         name=os_server.name, inst_id=os_server.id,
185         image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
186         networks=os_server.networks, keypair_name=os_server.key_name,
187         sec_grp_names=sec_grp_names, volume_ids=volumes)
188
189
190 def __get_latest_server_os_object(nova, server):
191     """
192     Returns a server with a given id
193     :param nova: the Nova client
194     :param server: the domain VmInst object
195     :return: the list of servers or None if not found
196     """
197     return __get_latest_server_os_object_by_id(nova, server.id)
198
199
200 def __get_latest_server_os_object_by_id(nova, server_id):
201     """
202     Returns a server with a given id
203     :param nova: the Nova client
204     :param server_id: the server's ID
205     :return: the list of servers or None if not found
206     """
207     return nova.servers.get(server_id)
208
209
210 def get_server_status(nova, server):
211     """
212     Returns the a VM instance's status from OpenStack
213     :param nova: the Nova client
214     :param server: the domain VmInst object
215     :return: the VM's string status or None if not founc
216     """
217     server = __get_latest_server_os_object(nova, server)
218     if server:
219         return server.status
220     return None
221
222
223 def get_server_console_output(nova, server):
224     """
225     Returns the console object for parsing VM activity
226     :param nova: the Nova client
227     :param server: the domain VmInst object
228     :return: the console output object or None if server object is not found
229     """
230     server = __get_latest_server_os_object(nova, server)
231     if server:
232         return server.get_console_output()
233     return None
234
235
236 def get_latest_server_object(nova, server):
237     """
238     Returns a server with a given id
239     :param nova: the Nova client
240     :param server: the old server object
241     :return: the list of servers or None if not found
242     """
243     server = __get_latest_server_os_object(nova, server)
244     return __map_os_server_obj_to_vm_inst(server)
245
246
247 def get_server_object_by_id(nova, server_id):
248     """
249     Returns a server with a given id
250     :param nova: the Nova client
251     :param server_id: the server's id
252     :return: an SNAPS-OO VmInst object or None if not found
253     """
254     server = __get_latest_server_os_object_by_id(nova, server_id)
255     return __map_os_server_obj_to_vm_inst(server)
256
257
258 def get_server_security_group_names(nova, server):
259     """
260     Returns a server with a given id
261     :param nova: the Nova client
262     :param server: the old server object
263     :return: the list of security groups associated with a VM
264     """
265     out = list()
266     os_vm_inst = __get_latest_server_os_object(nova, server)
267     for sec_grp_dict in os_vm_inst.security_groups:
268         out.append(sec_grp_dict['name'])
269     return out
270
271
272 def get_server_info(nova, server):
273     """
274     Returns a dictionary of a VMs info as returned by OpenStack
275     :param nova: the Nova client
276     :param server: the old server object
277     :return: a dict of the info if VM exists else None
278     """
279     vm = __get_latest_server_os_object(nova, server)
280     if vm:
281         return vm._info
282     return None
283
284
285 def create_keys(key_size=2048):
286     """
287     Generates public and private keys
288     :param key_size: the number of bytes for the key size
289     :return: the cryptography keys
290     """
291     return rsa.generate_private_key(backend=default_backend(),
292                                     public_exponent=65537,
293                                     key_size=key_size)
294
295
296 def public_key_openssh(keys):
297     """
298     Returns the public key for OpenSSH
299     :param keys: the keys generated by create_keys() from cryptography
300     :return: the OpenSSH public key
301     """
302     return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
303                                           serialization.PublicFormat.OpenSSH)
304
305
306 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
307     """
308     Saves the generated RSA generated keys to the filesystem
309     :param keys: the keys to save generated by cryptography
310     :param pub_file_path: the path to the public keys
311     :param priv_file_path: the path to the private keys
312     """
313     if keys:
314         if pub_file_path:
315             # To support '~'
316             pub_expand_file = os.path.expanduser(pub_file_path)
317             pub_dir = os.path.dirname(pub_expand_file)
318
319             if not os.path.isdir(pub_dir):
320                 os.mkdir(pub_dir)
321
322             public_handle = None
323             try:
324                 public_handle = open(pub_expand_file, 'wb')
325                 public_bytes = keys.public_key().public_bytes(
326                     serialization.Encoding.OpenSSH,
327                     serialization.PublicFormat.OpenSSH)
328                 public_handle.write(public_bytes)
329             finally:
330                 if public_handle:
331                     public_handle.close()
332
333             os.chmod(pub_expand_file, 0o600)
334             logger.info("Saved public key to - " + pub_expand_file)
335         if priv_file_path:
336             # To support '~'
337             priv_expand_file = os.path.expanduser(priv_file_path)
338             priv_dir = os.path.dirname(priv_expand_file)
339             if not os.path.isdir(priv_dir):
340                 os.mkdir(priv_dir)
341
342             private_handle = None
343             try:
344                 private_handle = open(priv_expand_file, 'wb')
345                 private_handle.write(
346                     keys.private_bytes(
347                         encoding=serialization.Encoding.PEM,
348                         format=serialization.PrivateFormat.TraditionalOpenSSL,
349                         encryption_algorithm=serialization.NoEncryption()))
350             finally:
351                 if private_handle:
352                     private_handle.close()
353
354             os.chmod(priv_expand_file, 0o600)
355             logger.info("Saved private key to - " + priv_expand_file)
356
357
358 def upload_keypair_file(nova, name, file_path):
359     """
360     Uploads a public key from a file
361     :param nova: the Nova client
362     :param name: the keypair name
363     :param file_path: the path to the public key file
364     :return: the keypair object
365     """
366     fpubkey = None
367     try:
368         with open(os.path.expanduser(file_path), 'rb') as fpubkey:
369             logger.info('Saving keypair to - ' + file_path)
370             return upload_keypair(nova, name, fpubkey.read())
371     finally:
372         if fpubkey:
373             fpubkey.close()
374
375
376 def upload_keypair(nova, name, key):
377     """
378     Uploads a public key from a file
379     :param nova: the Nova client
380     :param name: the keypair name
381     :param key: the public key object
382     :return: the keypair object
383     """
384     logger.info('Creating keypair with name - ' + name)
385     os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
386     return Keypair(name=os_kp.name, kp_id=os_kp.id,
387                    public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
388
389
390 def keypair_exists(nova, keypair_obj):
391     """
392     Returns a copy of the keypair object if found
393     :param nova: the Nova client
394     :param keypair_obj: the keypair object
395     :return: the keypair object or None if not found
396     """
397     try:
398         os_kp = nova.keypairs.get(keypair_obj)
399         return Keypair(name=os_kp.name, kp_id=os_kp.id,
400                        public_key=os_kp.public_key)
401     except:
402         return None
403
404
405 def get_keypair_by_name(nova, name):
406     """
407     Returns a list of all available keypairs
408     :param nova: the Nova client
409     :param name: the name of the keypair to lookup
410     :return: the keypair object or None if not found
411     """
412     keypairs = nova.keypairs.list()
413
414     for keypair in keypairs:
415         if keypair.name == name:
416             return Keypair(name=keypair.name, kp_id=keypair.id,
417                            public_key=keypair.public_key)
418
419     return None
420
421
422 def get_keypair_by_id(nova, kp_id):
423     """
424     Returns a list of all available keypairs
425     :param nova: the Nova client
426     :param kp_id: the ID of the keypair to return
427     :return: the keypair object
428     """
429     keypair = nova.keypairs.get(kp_id)
430     return Keypair(name=keypair.name, kp_id=keypair.id,
431                    public_key=keypair.public_key)
432
433
434 def delete_keypair(nova, key):
435     """
436     Deletes a keypair object from OpenStack
437     :param nova: the Nova client
438     :param key: the SNAPS-OO keypair domain object to delete
439     """
440     logger.debug('Deleting keypair - ' + key.name)
441     nova.keypairs.delete(key.id)
442
443
444 def get_availability_zone_hosts(nova, zone_name='nova'):
445     """
446     Returns the names of all nova active compute servers
447     :param nova: the Nova client
448     :param zone_name: the Nova client
449     :return: a list of compute server names
450     """
451     out = list()
452     zones = nova.availability_zones.list()
453     for zone in zones:
454         if zone.zoneName == zone_name and zone.hosts:
455             for key, host in zone.hosts.items():
456                 if host['nova-compute']['available']:
457                     out.append(zone.zoneName + ':' + key)
458
459     return out
460
461
462 def delete_vm_instance(nova, vm_inst):
463     """
464     Deletes a VM instance
465     :param nova: the nova client
466     :param vm_inst: the snaps.domain.VmInst object
467     """
468     nova.servers.delete(vm_inst.id)
469
470
471 def __get_os_flavor(nova, flavor_id):
472     """
473     Returns to OpenStack flavor object by name
474     :param nova: the Nova client
475     :param flavor_id: the flavor's ID value
476     :return: the OpenStack Flavor object
477     """
478     try:
479         return nova.flavors.get(flavor_id)
480     except NotFound:
481         return None
482
483
484 def get_flavor(nova, flavor):
485     """
486     Returns to OpenStack flavor object by name
487     :param nova: the Nova client
488     :param flavor: the SNAPS flavor domain object
489     :return: the SNAPS Flavor domain object
490     """
491     os_flavor = __get_os_flavor(nova, flavor.id)
492     if os_flavor:
493         return Flavor(
494             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
495             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
496             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
497             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
498     try:
499         return nova.flavors.get(flavor.id)
500     except NotFound:
501         return None
502
503
504 def get_flavor_by_id(nova, flavor_id):
505     """
506     Returns to OpenStack flavor object by name
507     :param nova: the Nova client
508     :param flavor_id: the flavor ID value
509     :return: the SNAPS Flavor domain object
510     """
511     os_flavor = __get_os_flavor(nova, flavor_id)
512     if os_flavor:
513         return Flavor(
514             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
515             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
516             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
517             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
518
519
520 def __get_os_flavor_by_name(nova, name):
521     """
522     Returns to OpenStack flavor object by name
523     :param nova: the Nova client
524     :param name: the name of the flavor to query
525     :return: OpenStack flavor object
526     """
527     try:
528         return nova.flavors.find(name=name)
529     except NotFound:
530         return None
531
532
533 def get_flavor_by_name(nova, name):
534     """
535     Returns a flavor by name
536     :param nova: the Nova client
537     :param name: the flavor name to return
538     :return: the SNAPS flavor domain object or None if not exists
539     """
540     os_flavor = __get_os_flavor_by_name(nova, name)
541     if os_flavor:
542         return Flavor(
543             name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
544             disk=os_flavor.disk, vcpus=os_flavor.vcpus,
545             ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
546             rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
547
548
549 def create_flavor(nova, flavor_settings):
550     """
551     Creates and returns and OpenStack flavor object
552     :param nova: the Nova client
553     :param flavor_settings: the flavor settings
554     :return: the SNAPS flavor domain object
555     """
556     os_flavor = nova.flavors.create(
557         name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
558         ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
559         disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
560         swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
561         is_public=flavor_settings.is_public)
562     return Flavor(
563         name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
564         disk=os_flavor.disk, vcpus=os_flavor.vcpus,
565         ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
566         rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
567
568
569 def delete_flavor(nova, flavor):
570     """
571     Deletes a flavor
572     :param nova: the Nova client
573     :param flavor: the SNAPS flavor domain object
574     """
575     nova.flavors.delete(flavor.id)
576
577
578 def set_flavor_keys(nova, flavor, metadata):
579     """
580     Sets metadata on the flavor
581     :param nova: the Nova client
582     :param flavor: the SNAPS flavor domain object
583     :param metadata: the metadata to set
584     """
585     os_flavor = __get_os_flavor(nova, flavor.id)
586     if os_flavor:
587         os_flavor.set_keys(metadata)
588
589
590 def get_flavor_keys(nova, flavor):
591     """
592     Sets metadata on the flavor
593     :param nova: the Nova client
594     :param flavor: the SNAPS flavor domain object
595     """
596     os_flavor = __get_os_flavor(nova, flavor.id)
597     if os_flavor:
598         return os_flavor.get_keys()
599
600
601 def add_security_group(nova, vm, security_group_name):
602     """
603     Adds a security group to an existing VM
604     :param nova: the nova client
605     :param vm: the OpenStack server object (VM) to alter
606     :param security_group_name: the name of the security group to add
607     """
608     try:
609         nova.servers.add_security_group(str(vm.id), security_group_name)
610     except ClientException as e:
611         sec_grp_names = get_server_security_group_names(nova, vm)
612         if security_group_name in sec_grp_names:
613             logger.warn('Security group [%s] already added to VM [%s]',
614                         security_group_name, vm.name)
615             return
616
617         logger.error('Unexpected error while adding security group [%s] - %s',
618                      security_group_name, e)
619         raise
620
621
622 def remove_security_group(nova, vm, security_group):
623     """
624     Removes a security group from an existing VM
625     :param nova: the nova client
626     :param vm: the OpenStack server object (VM) to alter
627     :param security_group: the SNAPS SecurityGroup domain object to add
628     """
629     nova.servers.remove_security_group(str(vm.id), security_group.name)
630
631
632 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
633     """
634     Adds a floating IP to a server instance
635     :param nova: the nova client
636     :param vm: VmInst domain object
637     :param floating_ip: FloatingIp domain object
638     :param ip_addr: the IP to which to bind the floating IP to
639     """
640     vm = __get_latest_server_os_object(nova, vm)
641     vm.add_floating_ip(floating_ip.ip, ip_addr)
642
643
644 def get_compute_quotas(nova, project_id):
645     """
646     Returns a list of all available keypairs
647     :param nova: the Nova client
648     :param project_id: the project's ID of the quotas to lookup
649     :return: an object of type ComputeQuotas or None if not found
650     """
651     quotas = nova.quotas.get(tenant_id=project_id)
652     if quotas:
653         return ComputeQuotas(quotas)
654
655
656 def update_quotas(nova, project_id, compute_quotas):
657     """
658     Updates the compute quotas for a given project
659     :param nova: the Nova client
660     :param project_id: the project's ID that requires quota updates
661     :param compute_quotas: an object of type ComputeQuotas containing the
662                            values to update
663     :return:
664     """
665     update_values = dict()
666     update_values['metadata_items'] = compute_quotas.metadata_items
667     update_values['cores'] = compute_quotas.cores
668     update_values['instances'] = compute_quotas.instances
669     update_values['injected_files'] = compute_quotas.injected_files
670     update_values['injected_file_content_bytes'] = (
671         compute_quotas.injected_file_content_bytes)
672     update_values['ram'] = compute_quotas.ram
673     update_values['fixed_ips'] = compute_quotas.fixed_ips
674     update_values['key_pairs'] = compute_quotas.key_pairs
675
676     return nova.quotas.update(project_id, **update_values)
677
678
679 def attach_volume(nova, server, volume, timeout=None):
680     """
681     Attaches a volume to a server
682     :param nova: the nova client
683     :param server: the VMInst domain object
684     :param volume: the Volume domain object
685     :param timeout: denotes the amount of time to block to determine if the
686                     has been properly attached. When None, do not wait.
687     :return: the value from the nova call
688     """
689     nova.volumes.create_server_volume(server.id, volume.id)
690
691     if timeout:
692         start_time = time.time()
693         while time.time() < start_time + timeout:
694             vm = get_server_object_by_id(nova, server.id)
695             for vol_dict in vm.volume_ids:
696                 if volume.id == vol_dict['id']:
697                     return vm
698
699         return None
700     else:
701         return get_server_object_by_id(nova, server.id)
702
703
704 def detach_volume(nova, server, volume, timeout=None):
705     """
706     Attaches a volume to a server
707     :param nova: the nova client
708     :param server: the VMInst domain object
709     :param volume: the Volume domain object
710     :param timeout: denotes the amount of time to block to determine if the
711                     has been properly detached. When None, do not wait.
712     :return: the value from the nova call
713     """
714     nova.volumes.delete_server_volume(server.id, volume.id)
715
716     if timeout:
717         start_time = time.time()
718         while time.time() < start_time + timeout:
719             vm = get_server_object_by_id(nova, server.id)
720             found = False
721             for vol_dict in vm.volume_ids:
722                 if volume.id == vol_dict['id']:
723                     found = True
724
725             if not found:
726                 return vm
727
728         return None
729     else:
730         return get_server_object_by_id(nova, server.id)
731
732
733 class NovaException(Exception):
734     """
735     Exception when calls to the Keystone client cannot be served properly
736     """