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