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.
19 from cryptography.hazmat.backends import default_backend
20 from cryptography.hazmat.primitives import serialization
21 from cryptography.hazmat.primitives.asymmetric import rsa
22 from novaclient.client import Client
23 from novaclient.exceptions import NotFound
25 from snaps import file_utils
26 from snaps.domain.flavor import Flavor
27 from snaps.domain.keypair import Keypair
28 from snaps.domain.project import ComputeQuotas
29 from snaps.domain.vm_inst import VmInst
30 from snaps.openstack.utils import keystone_utils, glance_utils, neutron_utils
32 __author__ = 'spisarski'
34 logger = logging.getLogger('nova_utils')
37 Utilities for basic OpenStack Nova API calls
41 def nova_client(os_creds):
43 Instantiates and returns a client for communications with OpenStack's Nova
45 :param os_creds: The connection credentials to the OpenStack API
46 :return: the client object
48 logger.debug('Retrieving Nova Client')
49 return Client(os_creds.compute_api_version,
50 session=keystone_utils.keystone_session(os_creds),
51 region_name=os_creds.region_name)
54 def create_server(nova, neutron, glance, instance_settings, image_settings,
55 keypair_settings=None):
58 :param nova: the nova client (required)
59 :param neutron: the neutron client for retrieving ports (required)
60 :param glance: the glance client (required)
61 :param instance_settings: the VM instance settings object (required)
62 :param image_settings: the VM's image settings object (required)
63 :param keypair_settings: the VM's keypair settings object (optional)
64 :return: a snaps.domain.VmInst object
69 for port_setting in instance_settings.port_settings:
70 ports.append(neutron_utils.get_port(
71 neutron, port_settings=port_setting))
75 kv['port-id'] = port.id
78 logger.info('Creating VM with name - ' + instance_settings.name)
81 keypair_name = keypair_settings.name
83 flavor = get_flavor_by_name(nova, instance_settings.flavor)
86 'Flavor not found with name - %s', instance_settings.flavor)
88 image = glance_utils.get_image(glance, image_settings=image_settings)
91 if instance_settings.userdata:
92 if isinstance(instance_settings.userdata, str):
93 userdata = instance_settings.userdata + '\n'
94 elif (isinstance(instance_settings.userdata, dict) and
95 'script_file' in instance_settings.userdata):
97 userdata = file_utils.read_file(
98 instance_settings.userdata['script_file'])
99 except Exception as e:
100 logger.warn('error reading userdata file %s - %s',
101 instance_settings.userdata, e)
102 args = {'name': instance_settings.name,
106 'key_name': keypair_name,
108 instance_settings.security_group_names,
109 'userdata': userdata}
111 if instance_settings.availability_zone:
112 args['availability_zone'] = instance_settings.availability_zone
114 server = nova.servers.create(**args)
116 return __map_os_server_obj_to_vm_inst(server)
119 'Cannot create instance, image cannot be located with name %s',
123 def get_server(nova, vm_inst_settings=None, server_name=None):
125 Returns a VmInst object for the first server instance found.
126 :param nova: the Nova client
127 :param vm_inst_settings: the VmInstanceSettings object from which to build
128 the query if not None
129 :param server_name: the server with this name to return if vm_inst_settings
131 :return: a snaps.domain.VmInst object or None if not found
135 search_opts['name'] = vm_inst_settings.name
137 search_opts['name'] = server_name
139 servers = nova.servers.list(search_opts=search_opts)
140 for server in servers:
141 return __map_os_server_obj_to_vm_inst(server)
144 def __map_os_server_obj_to_vm_inst(os_server):
146 Returns a VmInst object for an OpenStack Server object
147 :param os_server: the OpenStack server object
148 :return: an equivalent SNAPS-OO VmInst domain object
150 sec_grp_names = list()
151 # VM must be active for 'security_groups' attr to be initialized
152 if hasattr(os_server, 'security_groups'):
153 for sec_group in os_server.security_groups:
154 if sec_group.get('name'):
155 sec_grp_names.append(sec_group.get('name'))
158 name=os_server.name, inst_id=os_server.id,
159 image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
160 networks=os_server.networks, keypair_name=os_server.key_name,
161 sec_grp_names=sec_grp_names)
164 def __get_latest_server_os_object(nova, server):
166 Returns a server with a given id
167 :param nova: the Nova client
168 :param server: the domain VmInst object
169 :return: the list of servers or None if not found
171 return __get_latest_server_os_object_by_id(nova, server.id)
174 def __get_latest_server_os_object_by_id(nova, server_id):
176 Returns a server with a given id
177 :param nova: the Nova client
178 :param server_id: the server's ID
179 :return: the list of servers or None if not found
181 return nova.servers.get(server_id)
184 def get_server_status(nova, server):
186 Returns the a VM instance's status from OpenStack
187 :param nova: the Nova client
188 :param server: the domain VmInst object
189 :return: the VM's string status or None if not founc
191 server = __get_latest_server_os_object(nova, server)
197 def get_server_console_output(nova, server):
199 Returns the console object for parsing VM activity
200 :param nova: the Nova client
201 :param server: the domain VmInst object
202 :return: the console output object or None if server object is not found
204 server = __get_latest_server_os_object(nova, server)
206 return server.get_console_output()
210 def get_latest_server_object(nova, server):
212 Returns a server with a given id
213 :param nova: the Nova client
214 :param server: the old server object
215 :return: the list of servers or None if not found
217 server = __get_latest_server_os_object(nova, server)
218 return __map_os_server_obj_to_vm_inst(server)
221 def get_server_object_by_id(nova, server_id):
223 Returns a server with a given id
224 :param nova: the Nova client
225 :param server_id: the server's id
226 :return: an SNAPS-OO VmInst object or None if not found
228 server = __get_latest_server_os_object_by_id(nova, server_id)
229 return __map_os_server_obj_to_vm_inst(server)
232 def get_server_security_group_names(nova, server):
234 Returns a server with a given id
235 :param nova: the Nova client
236 :param server: the old server object
237 :return: the list of security groups associated with a VM
240 os_vm_inst = __get_latest_server_os_object(nova, server)
241 for sec_grp_dict in os_vm_inst.security_groups:
242 out.append(sec_grp_dict['name'])
246 def get_server_info(nova, server):
248 Returns a dictionary of a VMs info as returned by OpenStack
249 :param nova: the Nova client
250 :param server: the old server object
251 :return: a dict of the info if VM exists else None
253 vm = __get_latest_server_os_object(nova, server)
259 def create_keys(key_size=2048):
261 Generates public and private keys
262 :param key_size: the number of bytes for the key size
263 :return: the cryptography keys
265 return rsa.generate_private_key(backend=default_backend(),
266 public_exponent=65537,
270 def public_key_openssh(keys):
272 Returns the public key for OpenSSH
273 :param keys: the keys generated by create_keys() from cryptography
274 :return: the OpenSSH public key
276 return keys.public_key().public_bytes(serialization.Encoding.OpenSSH,
277 serialization.PublicFormat.OpenSSH)
280 def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
282 Saves the generated RSA generated keys to the filesystem
283 :param keys: the keys to save generated by cryptography
284 :param pub_file_path: the path to the public keys
285 :param priv_file_path: the path to the private keys
290 pub_expand_file = os.path.expanduser(pub_file_path)
291 pub_dir = os.path.dirname(pub_expand_file)
293 if not os.path.isdir(pub_dir):
298 public_handle = open(pub_expand_file, 'wb')
299 public_bytes = keys.public_key().public_bytes(
300 serialization.Encoding.OpenSSH,
301 serialization.PublicFormat.OpenSSH)
302 public_handle.write(public_bytes)
305 public_handle.close()
307 os.chmod(pub_expand_file, 0o600)
308 logger.info("Saved public key to - " + pub_expand_file)
311 priv_expand_file = os.path.expanduser(priv_file_path)
312 priv_dir = os.path.dirname(priv_expand_file)
313 if not os.path.isdir(priv_dir):
316 private_handle = None
318 private_handle = open(priv_expand_file, 'wb')
319 private_handle.write(
321 encoding=serialization.Encoding.PEM,
322 format=serialization.PrivateFormat.TraditionalOpenSSL,
323 encryption_algorithm=serialization.NoEncryption()))
326 private_handle.close()
328 os.chmod(priv_expand_file, 0o600)
329 logger.info("Saved private key to - " + priv_expand_file)
332 def upload_keypair_file(nova, name, file_path):
334 Uploads a public key from a file
335 :param nova: the Nova client
336 :param name: the keypair name
337 :param file_path: the path to the public key file
338 :return: the keypair object
342 with open(os.path.expanduser(file_path), 'rb') as fpubkey:
343 logger.info('Saving keypair to - ' + file_path)
344 return upload_keypair(nova, name, fpubkey.read())
350 def upload_keypair(nova, name, key):
352 Uploads a public key from a file
353 :param nova: the Nova client
354 :param name: the keypair name
355 :param key: the public key object
356 :return: the keypair object
358 logger.info('Creating keypair with name - ' + name)
359 os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
360 return Keypair(name=os_kp.name, kp_id=os_kp.id,
361 public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
364 def keypair_exists(nova, keypair_obj):
366 Returns a copy of the keypair object if found
367 :param nova: the Nova client
368 :param keypair_obj: the keypair object
369 :return: the keypair object or None if not found
372 os_kp = nova.keypairs.get(keypair_obj)
373 return Keypair(name=os_kp.name, kp_id=os_kp.id,
374 public_key=os_kp.public_key)
379 def get_keypair_by_name(nova, name):
381 Returns a list of all available keypairs
382 :param nova: the Nova client
383 :param name: the name of the keypair to lookup
384 :return: the keypair object or None if not found
386 keypairs = nova.keypairs.list()
388 for keypair in keypairs:
389 if keypair.name == name:
390 return Keypair(name=keypair.name, kp_id=keypair.id,
391 public_key=keypair.public_key)
396 def delete_keypair(nova, key):
398 Deletes a keypair object from OpenStack
399 :param nova: the Nova client
400 :param key: the SNAPS-OO keypair domain object to delete
402 logger.debug('Deleting keypair - ' + key.name)
403 nova.keypairs.delete(key.id)
406 def get_availability_zone_hosts(nova, zone_name='nova'):
408 Returns the names of all nova active compute servers
409 :param nova: the Nova client
410 :param zone_name: the Nova client
411 :return: a list of compute server names
414 zones = nova.availability_zones.list()
416 if zone.zoneName == zone_name and zone.hosts:
417 for key, host in zone.hosts.items():
418 if host['nova-compute']['available']:
419 out.append(zone.zoneName + ':' + key)
424 def delete_vm_instance(nova, vm_inst):
426 Deletes a VM instance
427 :param nova: the nova client
428 :param vm_inst: the snaps.domain.VmInst object
430 nova.servers.delete(vm_inst.id)
433 def __get_os_flavor(nova, flavor_id):
435 Returns to OpenStack flavor object by name
436 :param nova: the Nova client
437 :param flavor_id: the flavor's ID value
438 :return: the OpenStack Flavor object
441 return nova.flavors.get(flavor_id)
446 def get_flavor(nova, flavor):
448 Returns to OpenStack flavor object by name
449 :param nova: the Nova client
450 :param flavor: the SNAPS flavor domain object
451 :return: the SNAPS Flavor domain object
453 os_flavor = __get_os_flavor(nova, flavor.id)
456 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
457 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
458 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
459 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
461 return nova.flavors.get(flavor.id)
466 def get_flavor_by_id(nova, flavor_id):
468 Returns to OpenStack flavor object by name
469 :param nova: the Nova client
470 :param flavor_id: the flavor ID value
471 :return: the SNAPS Flavor domain object
473 os_flavor = __get_os_flavor(nova, flavor_id)
476 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
477 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
478 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
479 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
482 def __get_os_flavor_by_name(nova, name):
484 Returns to OpenStack flavor object by name
485 :param nova: the Nova client
486 :param name: the name of the flavor to query
487 :return: OpenStack flavor object
490 return nova.flavors.find(name=name)
495 def get_flavor_by_name(nova, name):
497 Returns a flavor by name
498 :param nova: the Nova client
499 :param name: the flavor name to return
500 :return: the SNAPS flavor domain object or None if not exists
502 os_flavor = __get_os_flavor_by_name(nova, name)
505 name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
506 disk=os_flavor.disk, vcpus=os_flavor.vcpus,
507 ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
508 rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
511 def create_flavor(nova, flavor_settings):
513 Creates and returns and OpenStack flavor object
514 :param nova: the Nova client
515 :param flavor_settings: the flavor settings
516 :return: the SNAPS flavor domain object
518 os_flavor = nova.flavors.create(
519 name=flavor_settings.name, flavorid=flavor_settings.flavor_id,
520 ram=flavor_settings.ram, vcpus=flavor_settings.vcpus,
521 disk=flavor_settings.disk, ephemeral=flavor_settings.ephemeral,
522 swap=flavor_settings.swap, rxtx_factor=flavor_settings.rxtx_factor,
523 is_public=flavor_settings.is_public)
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)
531 def delete_flavor(nova, flavor):
534 :param nova: the Nova client
535 :param flavor: the SNAPS flavor domain object
537 nova.flavors.delete(flavor.id)
540 def set_flavor_keys(nova, flavor, metadata):
542 Sets metadata on the flavor
543 :param nova: the Nova client
544 :param flavor: the SNAPS flavor domain object
545 :param metadata: the metadata to set
547 os_flavor = __get_os_flavor(nova, flavor.id)
549 os_flavor.set_keys(metadata)
552 def get_flavor_keys(nova, flavor):
554 Sets metadata on the flavor
555 :param nova: the Nova client
556 :param flavor: the SNAPS flavor domain object
558 os_flavor = __get_os_flavor(nova, flavor.id)
560 return os_flavor.get_keys()
563 def add_security_group(nova, vm, security_group_name):
565 Adds a security group to an existing VM
566 :param nova: the nova client
567 :param vm: the OpenStack server object (VM) to alter
568 :param security_group_name: the name of the security group to add
570 nova.servers.add_security_group(str(vm.id), security_group_name)
573 def remove_security_group(nova, vm, security_group):
575 Removes a security group from an existing VM
576 :param nova: the nova client
577 :param vm: the OpenStack server object (VM) to alter
578 :param security_group: the SNAPS SecurityGroup domain object to add
580 nova.servers.remove_security_group(str(vm.id), security_group.name)
583 def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
585 Adds a floating IP to a server instance
586 :param nova: the nova client
587 :param vm: VmInst domain object
588 :param floating_ip: FloatingIp domain object
589 :param ip_addr: the IP to which to bind the floating IP to
591 vm = __get_latest_server_os_object(nova, vm)
592 vm.add_floating_ip(floating_ip.ip, ip_addr)
595 def get_compute_quotas(nova, project_id):
597 Returns a list of all available keypairs
598 :param nova: the Nova client
599 :param project_id: the project's ID of the quotas to lookup
600 :return: an object of type ComputeQuotas or None if not found
602 quotas = nova.quotas.get(tenant_id=project_id)
604 return ComputeQuotas(quotas)
607 def update_quotas(nova, project_id, compute_quotas):
609 Updates the compute quotas for a given project
610 :param nova: the Nova client
611 :param project_id: the project's ID that requires quota updates
612 :param compute_quotas: an object of type ComputeQuotas containing the
616 update_values = dict()
617 update_values['metadata_items'] = compute_quotas.metadata_items
618 update_values['cores'] = compute_quotas.cores
619 update_values['instances'] = compute_quotas.instances
620 update_values['injected_files'] = compute_quotas.injected_files
621 update_values['injected_file_content_bytes'] = compute_quotas.injected_file_content_bytes
622 update_values['ram'] = compute_quotas.ram
623 update_values['fixed_ips'] = compute_quotas.fixed_ips
624 update_values['key_pairs'] = compute_quotas.key_pairs
626 return nova.quotas.update(project_id, **update_values)
629 class NovaException(Exception):
631 Exception when calls to the Keystone client cannot be served properly