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