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