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