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.
20 from cryptography.hazmat.backends import default_backend
21 from cryptography.hazmat.primitives import serialization
22 from cryptography.hazmat.primitives.asymmetric import rsa
23 from novaclient.client import Client
24 from novaclient.exceptions import NotFound
26 from snaps import file_utils
27 from snaps.domain.flavor import Flavor
28 from snaps.domain.keypair import Keypair
29 from snaps.domain.project import ComputeQuotas
30 from snaps.domain.vm_inst import VmInst
31 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
33 __author__ = 'spisarski'
35 logger = logging.getLogger('nova_utils')
38 Utilities for basic OpenStack Nova API calls
42 def nova_client(os_creds):
44 Instantiates and returns a client for communications with OpenStack's Nova
46 :param os_creds: The connection credentials to the OpenStack API
47 :return: the client object
49 logger.debug('Retrieving Nova Client')
50 return Client(os_creds.compute_api_version,
51 session=keystone_utils.keystone_session(os_creds),
52 region_name=os_creds.region_name)
55 def create_server(nova, neutron, glance, instance_config, image_config,
59 :param nova: the nova client (required)
60 :param neutron: the neutron client for retrieving ports (required)
61 :param glance: the glance client (required)
62 :param instance_config: the VMInstConfig object (required)
63 :param image_config: the VM's ImageConfig object (required)
64 :param keypair_config: the VM's KeypairConfig object (optional)
65 :return: a snaps.domain.VmInst object
70 for port_setting in instance_config.port_settings:
71 ports.append(neutron_utils.get_port(
72 neutron, port_settings=port_setting))
76 kv['port-id'] = port.id
79 logger.info('Creating VM with name - ' + instance_config.name)
82 keypair_name = keypair_config.name
84 flavor = get_flavor_by_name(nova, instance_config.flavor)
87 'Flavor not found with name - %s', instance_config.flavor)
89 image = glance_utils.get_image(glance, image_settings=image_config)
92 if instance_config.userdata:
93 if isinstance(instance_config.userdata, str):
94 userdata = instance_config.userdata + '\n'
95 elif (isinstance(instance_config.userdata, dict) and
96 'script_file' in instance_config.userdata):
98 userdata = file_utils.read_file(
99 instance_config.userdata['script_file'])
100 except Exception as e:
101 logger.warn('error reading userdata file %s - %s',
102 instance_config.userdata, e)
103 args = {'name': instance_config.name,
107 'key_name': keypair_name,
109 instance_config.security_group_names,
110 'userdata': userdata}
112 if instance_config.availability_zone:
113 args['availability_zone'] = instance_config.availability_zone
115 server = nova.servers.create(**args)
117 return __map_os_server_obj_to_vm_inst(server)
120 'Cannot create instance, image cannot be located with name %s',
124 def get_server(nova, vm_inst_settings=None, server_name=None):
126 Returns a VmInst object for the first server instance found.
127 :param nova: the Nova client
128 :param vm_inst_settings: the VmInstanceSettings object from which to build
129 the query if not None
130 :param server_name: the server with this name to return if vm_inst_settings
132 :return: a snaps.domain.VmInst object or None if not found
136 search_opts['name'] = vm_inst_settings.name
138 search_opts['name'] = server_name
140 servers = nova.servers.list(search_opts=search_opts)
141 for server in servers:
142 return __map_os_server_obj_to_vm_inst(server)
145 def get_server_connection(nova, vm_inst_settings=None, server_name=None):
147 Returns a VmInst object for the first server instance found.
148 :param nova: the Nova client
149 :param vm_inst_settings: the VmInstanceSettings object from which to build
150 the query if not None
151 :param server_name: the server with this name to return if vm_inst_settings
153 :return: a snaps.domain.VmInst object or None if not found
157 search_opts['name'] = vm_inst_settings.name
159 search_opts['name'] = server_name
161 servers = nova.servers.list(search_opts=search_opts)
162 for server in servers:
163 return server.links[0]
166 def __map_os_server_obj_to_vm_inst(os_server):
168 Returns a VmInst object for an OpenStack Server object
169 :param os_server: the OpenStack server object
170 :return: an equivalent SNAPS-OO VmInst domain object
172 sec_grp_names = list()
173 # VM must be active for 'security_groups' attr to be initialized
174 if hasattr(os_server, 'security_groups'):
175 for sec_group in os_server.security_groups:
176 if sec_group.get('name'):
177 sec_grp_names.append(sec_group.get('name'))
180 if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
181 volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
184 name=os_server.name, inst_id=os_server.id,
185 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
186 networks=os_server.networks, keypair_name=os_server.key_name,
187 sec_grp_names=sec_grp_names, volume_ids=volumes)
190 def __get_latest_server_os_object(nova, server):
192 Returns a server with a given id
193 :param nova: the Nova client
194 :param server: the domain VmInst object
195 :return: the list of servers or None if not found
197 return __get_latest_server_os_object_by_id(nova, server.id)
200 def __get_latest_server_os_object_by_id(nova, server_id):
202 Returns a server with a given id
203 :param nova: the Nova client
204 :param server_id: the server's ID
205 :return: the list of servers or None if not found
207 return nova.servers.get(server_id)
210 def get_server_status(nova, server):
212 Returns the a VM instance's status from OpenStack
213 :param nova: the Nova client
214 :param server: the domain VmInst object
215 :return: the VM's string status or None if not founc
217 server = __get_latest_server_os_object(nova, server)
223 def get_server_console_output(nova, server):
225 Returns the console object for parsing VM activity
226 :param nova: the Nova client
227 :param server: the domain VmInst object
228 :return: the console output object or None if server object is not found
230 server = __get_latest_server_os_object(nova, server)
232 return server.get_console_output()
236 def get_latest_server_object(nova, server):
238 Returns a server with a given id
239 :param nova: the Nova client
240 :param server: the old server object
241 :return: the list of servers or None if not found
243 server = __get_latest_server_os_object(nova, server)
244 return __map_os_server_obj_to_vm_inst(server)
247 def get_server_object_by_id(nova, server_id):
249 Returns a server with a given id
250 :param nova: the Nova client
251 :param server_id: the server's id
252 :return: an SNAPS-OO VmInst object or None if not found
254 server = __get_latest_server_os_object_by_id(nova, server_id)
255 return __map_os_server_obj_to_vm_inst(server)
258 def get_server_security_group_names(nova, server):
260 Returns a server with a given id
261 :param nova: the Nova client
262 :param server: the old server object
263 :return: the list of security groups associated with a VM
266 os_vm_inst = __get_latest_server_os_object(nova, server)
267 for sec_grp_dict in os_vm_inst.security_groups:
268 out.append(sec_grp_dict['name'])
272 def get_server_info(nova, server):
274 Returns a dictionary of a VMs info as returned by OpenStack
275 :param nova: the Nova client
276 :param server: the old server object
277 :return: a dict of the info if VM exists else None
279 vm = __get_latest_server_os_object(nova, server)
285 def create_keys(key_size=2048):
287 Generates public and private keys
288 :param key_size: the number of bytes for the key size
289 :return: the cryptography keys
291 return rsa.generate_private_key(backend=default_backend(),
292 public_exponent=65537,
296 def public_key_openssh(keys):
298 Returns the public key for OpenSSH
299 :param keys: the keys generated by create_keys() from cryptography
300 :return: the OpenSSH public key
302 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
303 serialization.PublicFormat.OpenSSH)
306 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
308 Saves the generated RSA generated keys to the filesystem
309 :param keys: the keys to save generated by cryptography
310 :param pub_file_path: the path to the public keys
311 :param priv_file_path: the path to the private keys
316 pub_expand_file = os.path.expanduser(pub_file_path)
317 pub_dir = os.path.dirname(pub_expand_file)
319 if not os.path.isdir(pub_dir):
324 public_handle = open(pub_expand_file, 'wb')
325 public_bytes = keys.public_key().public_bytes(
326 serialization.Encoding.OpenSSH,
327 serialization.PublicFormat.OpenSSH)
328 public_handle.write(public_bytes)
331 public_handle.close()
333 os.chmod(pub_expand_file, 0o600)
334 logger.info("Saved public key to - " + pub_expand_file)
337 priv_expand_file = os.path.expanduser(priv_file_path)
338 priv_dir = os.path.dirname(priv_expand_file)
339 if not os.path.isdir(priv_dir):
342 private_handle = None
344 private_handle = open(priv_expand_file, 'wb')
345 private_handle.write(
347 encoding=serialization.Encoding.PEM,
348 format=serialization.PrivateFormat.TraditionalOpenSSL,
349 encryption_algorithm=serialization.NoEncryption()))
352 private_handle.close()
354 os.chmod(priv_expand_file, 0o600)
355 logger.info("Saved private key to - " + priv_expand_file)
358 def upload_keypair_file(nova, name, file_path):
360 Uploads a public key from a file
361 :param nova: the Nova client
362 :param name: the keypair name
363 :param file_path: the path to the public key file
364 :return: the keypair object
368 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
369 logger.info('Saving keypair to - ' + file_path)
370 return upload_keypair(nova, name, fpubkey.read())
376 def upload_keypair(nova, name, key):
378 Uploads a public key from a file
379 :param nova: the Nova client
380 :param name: the keypair name
381 :param key: the public key object
382 :return: the keypair object
384 logger.info('Creating keypair with name - ' + name)
385 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
386 return Keypair(name=os_kp.name, kp_id=os_kp.id,
387 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
390 def keypair_exists(nova, keypair_obj):
392 Returns a copy of the keypair object if found
393 :param nova: the Nova client
394 :param keypair_obj: the keypair object
395 :return: the keypair object or None if not found
398 os_kp = nova.keypairs.get(keypair_obj)
399 return Keypair(name=os_kp.name, kp_id=os_kp.id,
400 public_key=os_kp.public_key)
405 def get_keypair_by_name(nova, name):
407 Returns a list of all available keypairs
408 :param nova: the Nova client
409 :param name: the name of the keypair to lookup
410 :return: the keypair object or None if not found
412 keypairs = nova.keypairs.list()
414 for keypair in keypairs:
415 if keypair.name == name:
416 return Keypair(name=keypair.name, kp_id=keypair.id,
417 public_key=keypair.public_key)
422 def get_keypair_by_id(nova, kp_id):
424 Returns a list of all available keypairs
425 :param nova: the Nova client
426 :param kp_id: the ID of the keypair to return
427 :return: the keypair object
429 keypair = nova.keypairs.get(kp_id)
430 return Keypair(name=keypair.name, kp_id=keypair.id,
431 public_key=keypair.public_key)
434 def delete_keypair(nova, key):
436 Deletes a keypair object from OpenStack
437 :param nova: the Nova client
438 :param key: the SNAPS-OO keypair domain object to delete
440 logger.debug('Deleting keypair - ' + key.name)
441 nova.keypairs.delete(key.id)
444 def get_availability_zone_hosts(nova, zone_name='nova'):
446 Returns the names of all nova active compute servers
447 :param nova: the Nova client
448 :param zone_name: the Nova client
449 :return: a list of compute server names
452 zones = nova.availability_zones.list()
454 if zone.zoneName == zone_name and zone.hosts:
455 for key, host in zone.hosts.items():
456 if host['nova-compute']['available']:
457 out.append(zone.zoneName + ':' + key)
462 def delete_vm_instance(nova, vm_inst):
464 Deletes a VM instance
465 :param nova: the nova client
466 :param vm_inst: the snaps.domain.VmInst object
468 nova.servers.delete(vm_inst.id)
471 def __get_os_flavor(nova, flavor_id):
473 Returns to OpenStack flavor object by name
474 :param nova: the Nova client
475 :param flavor_id: the flavor's ID value
476 :return: the OpenStack Flavor object
479 return nova.flavors.get(flavor_id)
484 def get_flavor(nova, flavor):
486 Returns to OpenStack flavor object by name
487 :param nova: the Nova client
488 :param flavor: the SNAPS flavor domain object
489 :return: the SNAPS Flavor domain object
491 os_flavor = __get_os_flavor(nova, flavor.id)
494 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
495 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
496 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
497 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
499 return nova.flavors.get(flavor.id)
504 def get_flavor_by_id(nova, flavor_id):
506 Returns to OpenStack flavor object by name
507 :param nova: the Nova client
508 :param flavor_id: the flavor ID value
509 :return: the SNAPS Flavor domain object
511 os_flavor = __get_os_flavor(nova, flavor_id)
514 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
515 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
516 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
517 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
520 def __get_os_flavor_by_name(nova, name):
522 Returns to OpenStack flavor object by name
523 :param nova: the Nova client
524 :param name: the name of the flavor to query
525 :return: OpenStack flavor object
528 return nova.flavors.find(name=name)
533 def get_flavor_by_name(nova, name):
535 Returns a flavor by name
536 :param nova: the Nova client
537 :param name: the flavor name to return
538 :return: the SNAPS flavor domain object or None if not exists
540 os_flavor = __get_os_flavor_by_name(nova, name)
543 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
544 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
545 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
546 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
549 def create_flavor(nova, flavor_settings):
551 Creates and returns and OpenStack flavor object
552 :param nova: the Nova client
553 :param flavor_settings: the flavor settings
554 :return: the SNAPS flavor domain object
556 os_flavor = nova.flavors.create(
557 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
558 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
559 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
560 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
561 is_public=flavor_settings.is_public)
563 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
564 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
565 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
566 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
569 def delete_flavor(nova, flavor):
572 :param nova: the Nova client
573 :param flavor: the SNAPS flavor domain object
575 nova.flavors.delete(flavor.id)
578 def set_flavor_keys(nova, flavor, metadata):
580 Sets metadata on the flavor
581 :param nova: the Nova client
582 :param flavor: the SNAPS flavor domain object
583 :param metadata: the metadata to set
585 os_flavor = __get_os_flavor(nova, flavor.id)
587 os_flavor.set_keys(metadata)
590 def get_flavor_keys(nova, flavor):
592 Sets metadata on the flavor
593 :param nova: the Nova client
594 :param flavor: the SNAPS flavor domain object
596 os_flavor = __get_os_flavor(nova, flavor.id)
598 return os_flavor.get_keys()
601 def add_security_group(nova, vm, security_group_name):
603 Adds a security group to an existing VM
604 :param nova: the nova client
605 :param vm: the OpenStack server object (VM) to alter
606 :param security_group_name: the name of the security group to add
608 nova.servers.add_security_group(str(vm.id), security_group_name)
611 def remove_security_group(nova, vm, security_group):
613 Removes a security group from an existing VM
614 :param nova: the nova client
615 :param vm: the OpenStack server object (VM) to alter
616 :param security_group: the SNAPS SecurityGroup domain object to add
618 nova.servers.remove_security_group(str(vm.id), security_group.name)
621 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
623 Adds a floating IP to a server instance
624 :param nova: the nova client
625 :param vm: VmInst domain object
626 :param floating_ip: FloatingIp domain object
627 :param ip_addr: the IP to which to bind the floating IP to
629 vm = __get_latest_server_os_object(nova, vm)
630 vm.add_floating_ip(floating_ip.ip, ip_addr)
633 def get_compute_quotas(nova, project_id):
635 Returns a list of all available keypairs
636 :param nova: the Nova client
637 :param project_id: the project's ID of the quotas to lookup
638 :return: an object of type ComputeQuotas or None if not found
640 quotas = nova.quotas.get(tenant_id=project_id)
642 return ComputeQuotas(quotas)
645 def update_quotas(nova, project_id, compute_quotas):
647 Updates the compute quotas for a given project
648 :param nova: the Nova client
649 :param project_id: the project's ID that requires quota updates
650 :param compute_quotas: an object of type ComputeQuotas containing the
654 update_values = dict()
655 update_values['metadata_items'] = compute_quotas.metadata_items
656 update_values['cores'] = compute_quotas.cores
657 update_values['instances'] = compute_quotas.instances
658 update_values['injected_files'] = compute_quotas.injected_files
659 update_values['injected_file_content_bytes'] = (
660 compute_quotas.injected_file_content_bytes)
661 update_values['ram'] = compute_quotas.ram
662 update_values['fixed_ips'] = compute_quotas.fixed_ips
663 update_values['key_pairs'] = compute_quotas.key_pairs
665 return nova.quotas.update(project_id, **update_values)
668 def attach_volume(nova, server, volume, timeout=None):
670 Attaches a volume to a server
671 :param nova: the nova client
672 :param server: the VMInst domain object
673 :param volume: the Volume domain object
674 :param timeout: denotes the amount of time to block to determine if the
675 has been properly attached. When None, do not wait.
676 :return: the value from the nova call
678 nova.volumes.create_server_volume(server.id, volume.id)
681 start_time = time.time()
682 while time.time() < start_time + timeout:
683 vm = get_server_object_by_id(nova, server.id)
684 for vol_dict in vm.volume_ids:
685 if volume.id == vol_dict['id']:
690 return get_server_object_by_id(nova, server.id)
693 def detach_volume(nova, server, volume, timeout=None):
695 Attaches a volume to a server
696 :param nova: the nova client
697 :param server: the VMInst domain object
698 :param volume: the Volume domain object
699 :param timeout: denotes the amount of time to block to determine if the
700 has been properly detached. When None, do not wait.
701 :return: the value from the nova call
703 nova.volumes.delete_server_volume(server.id, volume.id)
706 start_time = time.time()
707 while time.time() < start_time + timeout:
708 vm = get_server_object_by_id(nova, server.id)
710 for vol_dict in vm.volume_ids:
711 if volume.id == vol_dict['id']:
719 return get_server_object_by_id(nova, server.id)
722 class NovaException(Exception):
724 Exception when calls to the Keystone client cannot be served properly