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