1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
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
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
34 __author__ = 'spisarski'
36 logger = logging.getLogger('nova_utils')
39 Utilities for basic OpenStack Nova API calls
43 def nova_client(os_creds):
45 Instantiates and returns a client for communications with OpenStack's Nova
47 :param os_creds: The connection credentials to the OpenStack API
48 :return: the client object
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)
56 def create_server(nova, neutron, glance, instance_config, image_config,
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
71 for port_setting in instance_config.port_settings:
72 ports.append(neutron_utils.get_port(
73 neutron, port_settings=port_setting))
77 kv['port-id'] = port.id
80 logger.info('Creating VM with name - ' + instance_config.name)
83 keypair_name = keypair_config.name
85 flavor = get_flavor_by_name(nova, instance_config.flavor)
88 'Flavor not found with name - %s', instance_config.flavor)
90 image = glance_utils.get_image(glance, image_settings=image_config)
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):
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,
108 'key_name': keypair_name,
110 instance_config.security_group_names,
111 'userdata': userdata}
113 if instance_config.availability_zone:
114 args['availability_zone'] = instance_config.availability_zone
116 server = nova.servers.create(**args)
118 return __map_os_server_obj_to_vm_inst(server)
121 'Cannot create instance, image cannot be located with name %s',
125 def get_server(nova, vm_inst_settings=None, server_name=None):
127 Returns a VmInst object for the first server instance found.
128 :param nova: the Nova client
129 :param vm_inst_settings: the VmInstanceConfig object from which to build
130 the query if not None
131 :param server_name: the server with this name to return if vm_inst_settings
133 :return: a snaps.domain.VmInst object or None if not found
137 search_opts['name'] = vm_inst_settings.name
139 search_opts['name'] = server_name
141 servers = nova.servers.list(search_opts=search_opts)
142 for server in servers:
143 return __map_os_server_obj_to_vm_inst(server)
146 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
148 Returns a VmInst object for the first server instance found.
149 :param nova: the Nova client
150 :param vm_inst_settings: the VmInstanceConfig object from which to build
151 the query if not None
152 :param server_name: the server with this name to return if vm_inst_settings
154 :return: a snaps.domain.VmInst object or None if not found
158 search_opts['name'] = vm_inst_settings.name
160 search_opts['name'] = server_name
162 servers = nova.servers.list(search_opts=search_opts)
163 for server in servers:
164 return server.links[0]
167 def __map_os_server_obj_to_vm_inst(os_server):
169 Returns a VmInst object for an OpenStack Server object
170 :param os_server: the OpenStack server object
171 :return: an equivalent SNAPS-OO VmInst domain object
173 sec_grp_names = list()
174 # VM must be active for 'security_groups' attr to be initialized
175 if hasattr(os_server, 'security_groups'):
176 for sec_group in os_server.security_groups:
177 if sec_group.get('name'):
178 sec_grp_names.append(sec_group.get('name'))
181 if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
182 volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
185 name=os_server.name, inst_id=os_server.id,
186 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
187 networks=os_server.networks, keypair_name=os_server.key_name,
188 sec_grp_names=sec_grp_names, volume_ids=volumes)
191 def __get_latest_server_os_object(nova, server):
193 Returns a server with a given id
194 :param nova: the Nova client
195 :param server: the domain VmInst object
196 :return: the list of servers or None if not found
198 return __get_latest_server_os_object_by_id(nova, server.id)
201 def __get_latest_server_os_object_by_id(nova, server_id):
203 Returns a server with a given id
204 :param nova: the Nova client
205 :param server_id: the server's ID
206 :return: the list of servers or None if not found
208 return nova.servers.get(server_id)
211 def get_server_status(nova, server):
213 Returns the a VM instance's status from OpenStack
214 :param nova: the Nova client
215 :param server: the domain VmInst object
216 :return: the VM's string status or None if not founc
218 server = __get_latest_server_os_object(nova, server)
224 def get_server_console_output(nova, server):
226 Returns the console object for parsing VM activity
227 :param nova: the Nova client
228 :param server: the domain VmInst object
229 :return: the console output object or None if server object is not found
231 server = __get_latest_server_os_object(nova, server)
233 return server.get_console_output()
237 def get_latest_server_object(nova, server):
239 Returns a server with a given id
240 :param nova: the Nova client
241 :param server: the old server object
242 :return: the list of servers or None if not found
244 server = __get_latest_server_os_object(nova, server)
245 return __map_os_server_obj_to_vm_inst(server)
248 def get_server_object_by_id(nova, server_id):
250 Returns a server with a given id
251 :param nova: the Nova client
252 :param server_id: the server's id
253 :return: an SNAPS-OO VmInst object or None if not found
255 server = __get_latest_server_os_object_by_id(nova, server_id)
256 return __map_os_server_obj_to_vm_inst(server)
259 def get_server_security_group_names(nova, server):
261 Returns a server with a given id
262 :param nova: the Nova client
263 :param server: the old server object
264 :return: the list of security groups associated with a VM
267 os_vm_inst = __get_latest_server_os_object(nova, server)
268 for sec_grp_dict in os_vm_inst.security_groups:
269 out.append(sec_grp_dict['name'])
273 def get_server_info(nova, server):
275 Returns a dictionary of a VMs info as returned by OpenStack
276 :param nova: the Nova client
277 :param server: the old server object
278 :return: a dict of the info if VM exists else None
280 vm = __get_latest_server_os_object(nova, server)
285 def reboot_server(nova, server, reboot_type=None):
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 :param reboot_type: Acceptable values 'SOFT', 'HARD'
291 (api uses SOFT as the default)
292 :return: a dict of the info if VM exists else None
294 vm = __get_latest_server_os_object(nova, server)
296 vm.reboot(reboot_type=reboot_type.value)
298 raise ServerNotFoundError('Cannot locate server')
301 def create_keys(key_size=2048):
303 Generates public and private keys
304 :param key_size: the number of bytes for the key size
305 :return: the cryptography keys
307 return rsa.generate_private_key(backend=default_backend(),
308 public_exponent=65537,
312 def public_key_openssh(keys):
314 Returns the public key for OpenSSH
315 :param keys: the keys generated by create_keys() from cryptography
316 :return: the OpenSSH public key
318 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
319 serialization.PublicFormat.OpenSSH)
322 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
324 Saves the generated RSA generated keys to the filesystem
325 :param keys: the keys to save generated by cryptography
326 :param pub_file_path: the path to the public keys
327 :param priv_file_path: the path to the private keys
332 pub_expand_file = os.path.expanduser(pub_file_path)
333 pub_dir = os.path.dirname(pub_expand_file)
335 if not os.path.isdir(pub_dir):
340 public_handle = open(pub_expand_file, 'wb')
341 public_bytes = keys.public_key().public_bytes(
342 serialization.Encoding.OpenSSH,
343 serialization.PublicFormat.OpenSSH)
344 public_handle.write(public_bytes)
347 public_handle.close()
349 os.chmod(pub_expand_file, 0o600)
350 logger.info("Saved public key to - " + pub_expand_file)
353 priv_expand_file = os.path.expanduser(priv_file_path)
354 priv_dir = os.path.dirname(priv_expand_file)
355 if not os.path.isdir(priv_dir):
358 private_handle = None
360 private_handle = open(priv_expand_file, 'wb')
361 private_handle.write(
363 encoding=serialization.Encoding.PEM,
364 format=serialization.PrivateFormat.TraditionalOpenSSL,
365 encryption_algorithm=serialization.NoEncryption()))
368 private_handle.close()
370 os.chmod(priv_expand_file, 0o600)
371 logger.info("Saved private key to - " + priv_expand_file)
374 def upload_keypair_file(nova, name, file_path):
376 Uploads a public key from a file
377 :param nova: the Nova client
378 :param name: the keypair name
379 :param file_path: the path to the public key file
380 :return: the keypair object
384 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
385 logger.info('Saving keypair to - ' + file_path)
386 return upload_keypair(nova, name, fpubkey.read())
392 def upload_keypair(nova, name, key):
394 Uploads a public key from a file
395 :param nova: the Nova client
396 :param name: the keypair name
397 :param key: the public key object
398 :return: the keypair object
400 logger.info('Creating keypair with name - ' + name)
401 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
402 return Keypair(name=os_kp.name, kp_id=os_kp.id,
403 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
406 def keypair_exists(nova, keypair_obj):
408 Returns a copy of the keypair object if found
409 :param nova: the Nova client
410 :param keypair_obj: the keypair object
411 :return: the keypair object or None if not found
414 os_kp = nova.keypairs.get(keypair_obj)
415 return Keypair(name=os_kp.name, kp_id=os_kp.id,
416 public_key=os_kp.public_key)
421 def get_keypair_by_name(nova, name):
423 Returns a list of all available keypairs
424 :param nova: the Nova client
425 :param name: the name of the keypair to lookup
426 :return: the keypair object or None if not found
428 keypairs = nova.keypairs.list()
430 for keypair in keypairs:
431 if keypair.name == name:
432 return Keypair(name=keypair.name, kp_id=keypair.id,
433 public_key=keypair.public_key)
438 def get_keypair_by_id(nova, kp_id):
440 Returns a list of all available keypairs
441 :param nova: the Nova client
442 :param kp_id: the ID of the keypair to return
443 :return: the keypair object
445 keypair = nova.keypairs.get(kp_id)
446 return Keypair(name=keypair.name, kp_id=keypair.id,
447 public_key=keypair.public_key)
450 def delete_keypair(nova, key):
452 Deletes a keypair object from OpenStack
453 :param nova: the Nova client
454 :param key: the SNAPS-OO keypair domain object to delete
456 logger.debug('Deleting keypair - ' + key.name)
457 nova.keypairs.delete(key.id)
460 def get_availability_zone_hosts(nova, zone_name='nova'):
462 Returns the names of all nova active compute servers
463 :param nova: the Nova client
464 :param zone_name: the Nova client
465 :return: a list of compute server names
468 zones = nova.availability_zones.list()
470 if zone.zoneName == zone_name and zone.hosts:
471 for key, host in zone.hosts.items():
472 if host['nova-compute']['available']:
473 out.append(zone.zoneName + ':' + key)
478 def get_hypervisor_hosts(nova):
480 Returns the host names of all nova nodes with active hypervisors
481 :param nova: the Nova client
482 :return: a list of hypervisor host names
485 hypervisors = nova.hypervisors.list()
486 for hypervisor in hypervisors:
487 if hypervisor.state == "up":
488 out.append(hypervisor.hypervisor_hostname)
493 def delete_vm_instance(nova, vm_inst):
495 Deletes a VM instance
496 :param nova: the nova client
497 :param vm_inst: the snaps.domain.VmInst object
499 nova.servers.delete(vm_inst.id)
502 def __get_os_flavor(nova, flavor_id):
504 Returns to OpenStack flavor object by name
505 :param nova: the Nova client
506 :param flavor_id: the flavor's ID value
507 :return: the OpenStack Flavor object
510 return nova.flavors.get(flavor_id)
515 def get_flavor(nova, flavor):
517 Returns to OpenStack flavor object by name
518 :param nova: the Nova client
519 :param flavor: the SNAPS flavor domain object
520 :return: the SNAPS Flavor domain object
522 os_flavor = __get_os_flavor(nova, flavor.id)
525 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
526 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
527 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
528 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
530 return nova.flavors.get(flavor.id)
535 def get_flavor_by_id(nova, flavor_id):
537 Returns to OpenStack flavor object by name
538 :param nova: the Nova client
539 :param flavor_id: the flavor ID value
540 :return: the SNAPS Flavor domain object
542 os_flavor = __get_os_flavor(nova, flavor_id)
545 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
546 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
547 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
548 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
551 def __get_os_flavor_by_name(nova, name):
553 Returns to OpenStack flavor object by name
554 :param nova: the Nova client
555 :param name: the name of the flavor to query
556 :return: OpenStack flavor object
559 return nova.flavors.find(name=name)
564 def get_flavor_by_name(nova, name):
566 Returns a flavor by name
567 :param nova: the Nova client
568 :param name: the flavor name to return
569 :return: the SNAPS flavor domain object or None if not exists
571 os_flavor = __get_os_flavor_by_name(nova, name)
574 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
575 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
576 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
577 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
580 def create_flavor(nova, flavor_settings):
582 Creates and returns and OpenStack flavor object
583 :param nova: the Nova client
584 :param flavor_settings: the flavor settings
585 :return: the SNAPS flavor domain object
587 os_flavor = nova.flavors.create(
588 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
589 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
590 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
591 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
592 is_public=flavor_settings.is_public)
594 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
595 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
596 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
597 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
600 def delete_flavor(nova, flavor):
603 :param nova: the Nova client
604 :param flavor: the SNAPS flavor domain object
606 nova.flavors.delete(flavor.id)
609 def set_flavor_keys(nova, flavor, metadata):
611 Sets metadata on the flavor
612 :param nova: the Nova client
613 :param flavor: the SNAPS flavor domain object
614 :param metadata: the metadata to set
616 os_flavor = __get_os_flavor(nova, flavor.id)
618 os_flavor.set_keys(metadata)
621 def get_flavor_keys(nova, flavor):
623 Sets metadata on the flavor
624 :param nova: the Nova client
625 :param flavor: the SNAPS flavor domain object
627 os_flavor = __get_os_flavor(nova, flavor.id)
629 return os_flavor.get_keys()
632 def add_security_group(nova, vm, security_group_name):
634 Adds a security group to an existing VM
635 :param nova: the nova client
636 :param vm: the OpenStack server object (VM) to alter
637 :param security_group_name: the name of the security group to add
640 nova.servers.add_security_group(str(vm.id), security_group_name)
641 except ClientException as e:
642 sec_grp_names = get_server_security_group_names(nova, vm)
643 if security_group_name in sec_grp_names:
644 logger.warn('Security group [%s] already added to VM [%s]',
645 security_group_name, vm.name)
648 logger.error('Unexpected error while adding security group [%s] - %s',
649 security_group_name, e)
653 def remove_security_group(nova, vm, security_group):
655 Removes a security group from an existing VM
656 :param nova: the nova client
657 :param vm: the OpenStack server object (VM) to alter
658 :param security_group: the SNAPS SecurityGroup domain object to add
660 nova.servers.remove_security_group(str(vm.id), security_group.name)
663 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
665 Adds a floating IP to a server instance
666 :param nova: the nova client
667 :param vm: VmInst domain object
668 :param floating_ip: FloatingIp domain object
669 :param ip_addr: the IP to which to bind the floating IP to
671 vm = __get_latest_server_os_object(nova, vm)
672 vm.add_floating_ip(floating_ip.ip, ip_addr)
675 def get_compute_quotas(nova, project_id):
677 Returns a list of all available keypairs
678 :param nova: the Nova client
679 :param project_id: the project's ID of the quotas to lookup
680 :return: an object of type ComputeQuotas or None if not found
682 quotas = nova.quotas.get(tenant_id=project_id)
684 return ComputeQuotas(quotas)
687 def update_quotas(nova, project_id, compute_quotas):
689 Updates the compute quotas for a given project
690 :param nova: the Nova client
691 :param project_id: the project's ID that requires quota updates
692 :param compute_quotas: an object of type ComputeQuotas containing the
696 update_values = dict()
697 update_values['metadata_items'] = compute_quotas.metadata_items
698 update_values['cores'] = compute_quotas.cores
699 update_values['instances'] = compute_quotas.instances
700 update_values['injected_files'] = compute_quotas.injected_files
701 update_values['injected_file_content_bytes'] = (
702 compute_quotas.injected_file_content_bytes)
703 update_values['ram'] = compute_quotas.ram
704 update_values['fixed_ips'] = compute_quotas.fixed_ips
705 update_values['key_pairs'] = compute_quotas.key_pairs
707 return nova.quotas.update(project_id, **update_values)
710 def attach_volume(nova, server, volume, timeout=None):
712 Attaches a volume to a server
713 :param nova: the nova client
714 :param server: the VMInst domain object
715 :param volume: the Volume domain object
716 :param timeout: denotes the amount of time to block to determine if the
717 has been properly attached. When None, do not wait.
718 :return: the value from the nova call
720 nova.volumes.create_server_volume(server.id, volume.id)
723 start_time = time.time()
724 while time.time() < start_time + timeout:
725 vm = get_server_object_by_id(nova, server.id)
726 for vol_dict in vm.volume_ids:
727 if volume.id == vol_dict['id']:
732 return get_server_object_by_id(nova, server.id)
735 def detach_volume(nova, server, volume, timeout=None):
737 Attaches a volume to a server
738 :param nova: the nova client
739 :param server: the VMInst domain object
740 :param volume: the Volume domain object
741 :param timeout: denotes the amount of time to block to determine if the
742 has been properly detached. When None, do not wait.
743 :return: the value from the nova call
745 nova.volumes.delete_server_volume(server.id, volume.id)
748 start_time = time.time()
749 while time.time() < start_time + timeout:
750 vm = get_server_object_by_id(nova, server.id)
752 for vol_dict in vm.volume_ids:
753 if volume.id == vol_dict['id']:
761 return get_server_object_by_id(nova, server.id)
764 class RebootType(enum.Enum):
772 class NovaException(Exception):
774 Exception when calls to the Keystone client cannot be served properly
778 class ServerNotFoundError(Exception):
780 Exception when operations to a VM/Server is requested and the OpenStack
781 Server instance cannot be located