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