import logging
+import enum
import os
+import time
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from novaclient.client import Client
-from novaclient.exceptions import NotFound
+from novaclient.exceptions import NotFound, ClientException
+from snaps import file_utils
from snaps.domain.flavor import Flavor
from snaps.domain.keypair import Keypair
from snaps.domain.project import ComputeQuotas
logger = logging.getLogger('nova_utils')
+POLL_INTERVAL = 3
+
"""
Utilities for basic OpenStack Nova API calls
"""
-def nova_client(os_creds):
+def nova_client(os_creds, session=None):
"""
Instantiates and returns a client for communications with OpenStack's Nova
server
:param os_creds: The connection credentials to the OpenStack API
+ :param session: the keystone session object (optional)
:return: the client object
"""
logger.debug('Retrieving Nova Client')
+ if not session:
+ session = keystone_utils.keystone_session(os_creds)
+
return Client(os_creds.compute_api_version,
- session=keystone_utils.keystone_session(os_creds),
+ session=session,
region_name=os_creds.region_name)
-def create_server(nova, neutron, glance, instance_settings, image_settings,
- keypair_settings=None):
+def create_server(nova, keystone, neutron, glance, instance_config,
+ image_config, project_name, keypair_config=None):
"""
Creates a VM instance
:param nova: the nova client (required)
+ :param keystone: the keystone client for retrieving projects (required)
:param neutron: the neutron client for retrieving ports (required)
:param glance: the glance client (required)
- :param instance_settings: the VM instance settings object (required)
- :param image_settings: the VM's image settings object (required)
- :param keypair_settings: the VM's keypair settings object (optional)
+ :param instance_config: the VMInstConfig object (required)
+ :param image_config: the VM's ImageConfig object (required)
+ :param project_name: the associated project name (required)
+ :param keypair_config: the VM's KeypairConfig object (optional)
:return: a snaps.domain.VmInst object
"""
ports = list()
- for port_setting in instance_settings.port_settings:
- ports.append(neutron_utils.get_port(
- neutron, port_settings=port_setting))
+ for port_setting in instance_config.port_settings:
+ port = neutron_utils.get_port(
+ neutron, keystone, port_settings=port_setting,
+ project_name=project_name)
+ if port:
+ ports.append(port)
+ else:
+ raise Exception('Cannot find port named - ' + port_setting.name)
nics = []
for port in ports:
kv = dict()
kv['port-id'] = port.id
nics.append(kv)
- logger.info('Creating VM with name - ' + instance_settings.name)
+ logger.info('Creating VM with name - ' + instance_config.name)
keypair_name = None
- if keypair_settings:
- keypair_name = keypair_settings.name
+ if keypair_config:
+ keypair_name = keypair_config.name
- flavor = get_flavor_by_name(nova, instance_settings.flavor)
+ flavor = get_flavor_by_name(nova, instance_config.flavor)
if not flavor:
raise NovaException(
- 'Flavor not found with name - %s', instance_settings.flavor)
+ 'Flavor not found with name - %s', instance_config.flavor)
- image = glance_utils.get_image(glance, image_settings=image_settings)
+ image = glance_utils.get_image(glance, image_settings=image_config)
if image:
- args = {'name': instance_settings.name,
+ userdata = None
+ if instance_config.userdata:
+ if isinstance(instance_config.userdata, str):
+ userdata = instance_config.userdata + '\n'
+ elif (isinstance(instance_config.userdata, dict) and
+ 'script_file' in instance_config.userdata):
+ try:
+ userdata = file_utils.read_file(
+ instance_config.userdata['script_file'])
+ except Exception as e:
+ logger.warn('error reading userdata file %s - %s',
+ instance_config.userdata, e)
+ args = {'name': instance_config.name,
'flavor': flavor,
'image': image,
'nics': nics,
'key_name': keypair_name,
'security_groups':
- instance_settings.security_group_names,
- 'userdata': instance_settings.userdata}
+ instance_config.security_group_names,
+ 'userdata': userdata}
- if instance_settings.availability_zone:
- args['availability_zone'] = instance_settings.availability_zone
+ if instance_config.availability_zone:
+ args['availability_zone'] = instance_config.availability_zone
server = nova.servers.create(**args)
- return VmInst(name=server.name, inst_id=server.id,
- networks=server.networks)
+
+ return __map_os_server_obj_to_vm_inst(
+ neutron, keystone, server, project_name)
else:
raise NovaException(
'Cannot create instance, image cannot be located with name %s',
- image_settings.name)
+ image_config.name)
-def get_server(nova, vm_inst_settings=None, server_name=None):
+def get_server(nova, neutron, keystone, vm_inst_settings=None,
+ server_name=None, project_id=None):
"""
Returns a VmInst object for the first server instance found.
:param nova: the Nova client
- :param vm_inst_settings: the VmInstanceSettings object from which to build
+ :param neutron: the Neutron client
+ :param keystone: the Keystone client
+ :param vm_inst_settings: the VmInstanceConfig object from which to build
the query if not None
:param server_name: the server with this name to return if vm_inst_settings
is not None
+ :param project_id: the assocaited project ID
:return: a snaps.domain.VmInst object or None if not found
"""
search_opts = dict()
servers = nova.servers.list(search_opts=search_opts)
for server in servers:
- return VmInst(name=server.name, inst_id=server.id,
- networks=server.networks)
+ return __map_os_server_obj_to_vm_inst(
+ neutron, keystone, server, project_id)
+
+
+def get_server_connection(nova, vm_inst_settings=None, server_name=None):
+ """
+ Returns a VmInst object for the first server instance found.
+ :param nova: the Nova client
+ :param vm_inst_settings: the VmInstanceConfig object from which to build
+ the query if not None
+ :param server_name: the server with this name to return if vm_inst_settings
+ is not None
+ :return: a snaps.domain.VmInst object or None if not found
+ """
+ search_opts = dict()
+ if vm_inst_settings:
+ search_opts['name'] = vm_inst_settings.name
+ elif server_name:
+ search_opts['name'] = server_name
+
+ servers = nova.servers.list(search_opts=search_opts)
+ for server in servers:
+ return server.links[0]
+
+
+def __map_os_server_obj_to_vm_inst(neutron, keystone, os_server,
+ project_name=None):
+ """
+ Returns a VmInst object for an OpenStack Server object
+ :param neutron: the Neutron client
+ :param keystone: the Keystone client
+ :param os_server: the OpenStack server object
+ :param project_name: the associated project name
+ :return: an equivalent SNAPS-OO VmInst domain object
+ """
+ sec_grp_names = list()
+ # VM must be active for 'security_groups' attr to be initialized
+ if hasattr(os_server, 'security_groups'):
+ for sec_group in os_server.security_groups:
+ if sec_group.get('name'):
+ sec_grp_names.append(sec_group.get('name'))
+
+ out_ports = list()
+ if len(os_server.networks) > 0:
+ for net_name, ips in os_server.networks.items():
+ network = neutron_utils.get_network(
+ neutron, keystone, network_name=net_name,
+ project_name=project_name)
+ if network:
+ ports = neutron_utils.get_ports(neutron, network, ips)
+ for port in ports:
+ out_ports.append(port)
+ else:
+ raise NovaException(
+ 'Unable to locate network in project {} with '
+ 'name {}'.format(project_name, net_name))
+
+ volumes = None
+ if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
+ volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
+
+ return VmInst(
+ name=os_server.name, inst_id=os_server.id,
+ image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
+ ports=out_ports, keypair_name=os_server.key_name,
+ sec_grp_names=sec_grp_names, volume_ids=volumes,
+ compute_host=os_server._info.get('OS-EXT-SRV-ATTR:host'),
+ availability_zone=os_server._info.get('OS-EXT-AZ:availability_zone'))
def __get_latest_server_os_object(nova, server):
:param server: the domain VmInst object
:return: the list of servers or None if not found
"""
- return nova.servers.get(server.id)
+ return __get_latest_server_os_object_by_id(nova, server.id)
+
+
+def __get_latest_server_os_object_by_id(nova, server_id):
+ """
+ Returns a server with a given id
+ :param nova: the Nova client
+ :param server_id: the server's ID
+ :return: the list of servers or None if not found
+ """
+ return nova.servers.get(server_id)
def get_server_status(nova, server):
return None
-def get_latest_server_object(nova, server):
+def get_latest_server_object(nova, neutron, keystone, server, project_name):
"""
Returns a server with a given id
:param nova: the Nova client
+ :param neutron: the Neutron client
+ :param keystone: the Keystone client
:param server: the old server object
+ :param project_name: the associated project name
:return: the list of servers or None if not found
"""
server = __get_latest_server_os_object(nova, server)
- return VmInst(name=server.name, inst_id=server.id,
- networks=server.networks)
+ return __map_os_server_obj_to_vm_inst(
+ neutron, keystone, server, project_name)
+
+
+def get_server_object_by_id(nova, neutron, keystone, server_id,
+ project_name=None):
+ """
+ Returns a server with a given id
+ :param nova: the Nova client
+ :param neutron: the Neutron client
+ :param keystone: the Keystone client
+ :param server_id: the server's id
+ :param project_name: the associated project name
+ :return: an SNAPS-OO VmInst object or None if not found
+ """
+ server = __get_latest_server_os_object_by_id(nova, server_id)
+ return __map_os_server_obj_to_vm_inst(
+ neutron, keystone, server, project_name)
def get_server_security_group_names(nova, server):
"""
out = list()
os_vm_inst = __get_latest_server_os_object(nova, server)
- for sec_grp_dict in os_vm_inst.security_groups:
- out.append(sec_grp_dict['name'])
+ if hasattr(os_vm_inst, 'security_groups'):
+ for sec_grp_dict in os_vm_inst.security_groups:
+ out.append(sec_grp_dict['name'])
return out
return None
+def reboot_server(nova, server, reboot_type=None):
+ """
+ Returns a dictionary of a VMs info as returned by OpenStack
+ :param nova: the Nova client
+ :param server: the old server object
+ :param reboot_type: Acceptable values 'SOFT', 'HARD'
+ (api uses SOFT as the default)
+ :return: a dict of the info if VM exists else None
+ """
+ vm = __get_latest_server_os_object(nova, server)
+ if vm:
+ vm.reboot(reboot_type=reboot_type.value)
+ else:
+ raise ServerNotFoundError('Cannot locate server')
+
+
def create_keys(key_size=2048):
"""
Generates public and private keys
if public_handle:
public_handle.close()
- os.chmod(pub_expand_file, 0o400)
+ os.chmod(pub_expand_file, 0o600)
logger.info("Saved public key to - " + pub_expand_file)
if priv_file_path:
# To support '~'
if private_handle:
private_handle.close()
- os.chmod(priv_expand_file, 0o400)
+ os.chmod(priv_expand_file, 0o600)
logger.info("Saved private key to - " + priv_expand_file)
"""
logger.info('Creating keypair with name - ' + name)
os_kp = nova.keypairs.create(name=name, public_key=key.decode('utf-8'))
- return Keypair(name=os_kp.name, id=os_kp.id, public_key=os_kp.public_key)
+ return Keypair(name=os_kp.name, kp_id=os_kp.id,
+ public_key=os_kp.public_key, fingerprint=os_kp.fingerprint)
def keypair_exists(nova, keypair_obj):
"""
try:
os_kp = nova.keypairs.get(keypair_obj)
- return Keypair(name=os_kp.name, id=os_kp.id,
+ return Keypair(name=os_kp.name, kp_id=os_kp.id,
public_key=os_kp.public_key)
except:
return None
for keypair in keypairs:
if keypair.name == name:
- return Keypair(name=keypair.name, id=keypair.id,
+ return Keypair(name=keypair.name, kp_id=keypair.id,
public_key=keypair.public_key)
return None
+def get_keypair_by_id(nova, kp_id):
+ """
+ Returns a list of all available keypairs
+ :param nova: the Nova client
+ :param kp_id: the ID of the keypair to return
+ :return: the keypair object
+ """
+ keypair = nova.keypairs.get(kp_id)
+ return Keypair(name=keypair.name, kp_id=keypair.id,
+ public_key=keypair.public_key)
+
+
def delete_keypair(nova, key):
"""
Deletes a keypair object from OpenStack
return out
+def get_hypervisor_hosts(nova):
+ """
+ Returns the host names of all nova nodes with active hypervisors
+ :param nova: the Nova client
+ :return: a list of hypervisor host names
+ """
+ out = list()
+ hypervisors = nova.hypervisors.list()
+ for hypervisor in hypervisors:
+ if hypervisor.state == "up":
+ out.append(hypervisor.hypervisor_hostname)
+
+ return out
+
+
def delete_vm_instance(nova, vm_inst):
"""
Deletes a VM instance
nova.servers.delete(vm_inst.id)
-def __get_os_flavor(nova, flavor):
+def __get_os_flavor(nova, flavor_id):
"""
Returns to OpenStack flavor object by name
:param nova: the Nova client
- :param flavor: the SNAPS flavor domain object
+ :param flavor_id: the flavor's ID value
:return: the OpenStack Flavor object
"""
try:
- return nova.flavors.get(flavor.id)
+ return nova.flavors.get(flavor_id)
except NotFound:
return None
:param flavor: the SNAPS flavor domain object
:return: the SNAPS Flavor domain object
"""
- os_flavor = __get_os_flavor(nova, flavor)
+ os_flavor = __get_os_flavor(nova, flavor.id)
if os_flavor:
return Flavor(
name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
return None
+def get_flavor_by_id(nova, flavor_id):
+ """
+ Returns to OpenStack flavor object by name
+ :param nova: the Nova client
+ :param flavor_id: the flavor ID value
+ :return: the SNAPS Flavor domain object
+ """
+ os_flavor = __get_os_flavor(nova, flavor_id)
+ if os_flavor:
+ return Flavor(
+ name=os_flavor.name, id=os_flavor.id, ram=os_flavor.ram,
+ disk=os_flavor.disk, vcpus=os_flavor.vcpus,
+ ephemeral=os_flavor.ephemeral, swap=os_flavor.swap,
+ rxtx_factor=os_flavor.rxtx_factor, is_public=os_flavor.is_public)
+
+
def __get_os_flavor_by_name(nova, name):
"""
Returns to OpenStack flavor object by name
:param flavor: the SNAPS flavor domain object
:param metadata: the metadata to set
"""
- os_flavor = __get_os_flavor(nova, flavor)
+ os_flavor = __get_os_flavor(nova, flavor.id)
if os_flavor:
os_flavor.set_keys(metadata)
:param nova: the Nova client
:param flavor: the SNAPS flavor domain object
"""
- os_flavor = __get_os_flavor(nova, flavor)
+ os_flavor = __get_os_flavor(nova, flavor.id)
if os_flavor:
return os_flavor.get_keys()
:param vm: the OpenStack server object (VM) to alter
:param security_group_name: the name of the security group to add
"""
- nova.servers.add_security_group(str(vm.id), security_group_name)
+ try:
+ nova.servers.add_security_group(str(vm.id), security_group_name)
+ except ClientException as e:
+ sec_grp_names = get_server_security_group_names(nova, vm)
+ if security_group_name in sec_grp_names:
+ logger.warn('Security group [%s] already added to VM [%s]',
+ security_group_name, vm.name)
+ return
+
+ logger.error('Unexpected error while adding security group [%s] - %s',
+ security_group_name, e)
+ raise
def remove_security_group(nova, vm, security_group):
nova.servers.remove_security_group(str(vm.id), security_group.name)
-def add_floating_ip_to_server(nova, vm, floating_ip, ip_addr):
- """
- Adds a floating IP to a server instance
- :param nova: the nova client
- :param vm: VmInst domain object
- :param floating_ip: FloatingIp domain object
- :param ip_addr: the IP to which to bind the floating IP to
- """
- vm = __get_latest_server_os_object(nova, vm)
- vm.add_floating_ip(floating_ip.ip, ip_addr)
-
-
def get_compute_quotas(nova, project_id):
"""
Returns a list of all available keypairs
update_values['cores'] = compute_quotas.cores
update_values['instances'] = compute_quotas.instances
update_values['injected_files'] = compute_quotas.injected_files
- update_values['injected_file_content_bytes'] = compute_quotas.injected_file_content_bytes
+ update_values['injected_file_content_bytes'] = (
+ compute_quotas.injected_file_content_bytes)
update_values['ram'] = compute_quotas.ram
update_values['fixed_ips'] = compute_quotas.fixed_ips
update_values['key_pairs'] = compute_quotas.key_pairs
return nova.quotas.update(project_id, **update_values)
+def attach_volume(nova, neutron, keystone, server, volume, project_name,
+ timeout=120):
+ """
+ Attaches a volume to a server. When the timeout parameter is used, a VmInst
+ object with the proper volume updates is returned unless it has not been
+ updated in the allotted amount of time then an Exception will be raised.
+ :param nova: the nova client
+ :param neutron: the neutron client
+ :param keystone: the neutron client
+ :param server: the VMInst domain object
+ :param volume: the Volume domain object
+ :param project_name: the associated project name
+ :param timeout: denotes the amount of time to block to determine if the
+ has been properly attached.
+ :return: updated VmInst object
+ """
+ nova.volumes.create_server_volume(server.id, volume.id)
+
+ start_time = time.time()
+ while time.time() < start_time + timeout:
+ vm = get_server_object_by_id(
+ nova, neutron, keystone, server.id, project_name)
+ for vol_dict in vm.volume_ids:
+ if volume.id == vol_dict['id']:
+ return vm
+ time.sleep(POLL_INTERVAL)
+
+ raise NovaException(
+ 'Attach failed on volume - {} and server - {}'.format(
+ volume.id, server.id))
+
+
+def detach_volume(nova, neutron, keystone, server, volume, project_name,
+ timeout=120):
+ """
+ Detaches a volume to a server. When the timeout parameter is used, a VmInst
+ object with the proper volume updates is returned unless it has not been
+ updated in the allotted amount of time then an Exception will be raised.
+ :param nova: the nova client
+ :param neutron: the neutron client
+ :param keystone: the keystone client
+ :param server: the VMInst domain object
+ :param volume: the Volume domain object
+ :param project_name: the associated project name
+ :param timeout: denotes the amount of time to block to determine if the
+ has been properly detached.
+ :return: updated VmInst object
+ """
+ nova.volumes.delete_server_volume(server.id, volume.id)
+
+ start_time = time.time()
+ while time.time() < start_time + timeout:
+ vm = get_server_object_by_id(
+ nova, neutron, keystone, server.id, project_name)
+ if len(vm.volume_ids) == 0:
+ return vm
+ else:
+ ids = list()
+ for vol_dict in vm.volume_ids:
+ ids.append(vol_dict['id'])
+ if volume.id not in ids:
+ return vm
+ time.sleep(POLL_INTERVAL)
+
+ raise NovaException(
+ 'Detach failed on volume - {} server - {}'.format(
+ volume.id, server.id))
+
+
+class RebootType(enum.Enum):
+ """
+ A rule's direction
+ """
+ soft = 'SOFT'
+ hard = 'HARD'
+
+
class NovaException(Exception):
"""
Exception when calls to the Keystone client cannot be served properly
"""
+
+
+class ServerNotFoundError(Exception):
+ """
+ Exception when operations to a VM/Server is requested and the OpenStack
+ Server instance cannot be located
+ """