vmtp/vmtp/compute.py

514 lines
21 KiB
Python

# Copyright 2014 Cisco Systems, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
'''Module for Openstack compute operations'''
import os
import time
import glanceclient.exc as glance_exception
import keystoneauth1
from .log import LOG
from neutronclient.common.exceptions import Conflict
import novaclient
import novaclient.exceptions as exceptions
try:
from glanceclient.openstack.common.apiclient.exceptions import NotFound as GlanceImageNotFound
except ImportError:
from glanceclient.v1.apiclient.exceptions import NotFound as GlanceImageNotFound
class Compute(object):
def __init__(self, nova_client, neutron, config):
self.novaclient = nova_client
self.neutron = neutron
self.config = config
def find_image(self, glance_client, image_name):
try:
return next(glance_client.images.list(filters={'name': image_name}), None)
except (novaclient.exceptions.NotFound, keystoneauth1.exceptions.http.NotFound,
GlanceImageNotFound):
pass
return None
def upload_image_via_url(self, glance_client, final_image_name,
image_url, retry_count=60):
'''
Directly uploads image to Nova via URL if image is not present
'''
retry = 0
file_prefix = "file://"
if not image_url.startswith(file_prefix):
LOG.error("File format %s is not supported. It must start with %s", image_url,
file_prefix)
return False
try:
image_location = image_url.split(file_prefix)[1]
f_image = open(image_location, 'rb')
img = glance_client.images.create(name=str(final_image_name))
glance_client.images.update(img.id, disk_format="qcow2",
container_format="bare")
glance_client.images.upload(img.id, f_image)
# Check for the image in glance
while img.status in ['queued', 'saving'] and retry < retry_count:
img = self.find_image(glance_client, image_name=img.name)
retry = retry + 1
LOG.debug("Image not yet active, retrying %s of %s...", retry, retry_count)
time.sleep(2)
if img.status != 'active':
raise Exception
except glance_exception.HTTPForbidden:
LOG.error("Cannot upload image without admin access. Please make "
"sure the image is uploaded and is either public or owned by you.")
return False
except (Exception, IOError):
# catch the exception for file based errors.
LOG.error("Failed while uploading the image. Please make sure the "
"image at the specified location %s is correct.", image_url)
return False
return True
def delete_image(self, glance_client, img_name):
try:
LOG.log("Deleting image %s...", img_name)
img = self.find_image(glance_client, img_name)
glance_client.images.delete(img.id)
except Exception:
LOG.error("Failed to delete the image %s.", img_name)
return False
return True
# Remove keypair name from openstack if exists
def remove_public_key(self, name):
keypair_list = self.novaclient.keypairs.list()
for key in keypair_list:
if key.name == name:
self.novaclient.keypairs.delete(name)
LOG.info('Removed public key %s', name)
break
# Test if keypair file is present if not create it
def create_keypair(self, name, private_key_pair_file):
self.remove_public_key(name)
keypair = self.novaclient.keypairs.create(name)
# Now write the keypair to the file if requested
if private_key_pair_file:
kpf = os.open(private_key_pair_file,
os.O_WRONLY | os.O_CREAT, 0o600)
with os.fdopen(kpf, 'w') as kpf:
kpf.write(keypair.private_key)
return keypair
# Add an existing public key to openstack
def add_public_key(self, name, public_key_file):
self.remove_public_key(name)
# extract the public key from the file
public_key = None
try:
with open(os.path.expanduser(public_key_file)) as pkf:
public_key = pkf.read()
except IOError as exc:
LOG.error('Cannot open public key file %s: %s', public_key_file, exc)
return None
keypair = self.novaclient.keypairs.create(name, public_key)
return keypair
def init_key_pair(self, kp_name, ssh_access):
'''Initialize the key pair for all test VMs
if a key pair is specified in access, use that key pair else
create a temporary key pair
'''
if ssh_access.public_key_file:
return self.add_public_key(kp_name, ssh_access.public_key_file)
else:
keypair = self.create_keypair(kp_name, None)
ssh_access.private_key = keypair.private_key
return keypair
def find_network(self, label):
net = self.novaclient.networks.find(label=label)
return net
# Create a server instance with name vmname
# and check that it gets into the ACTIVE state
def create_server(self, vmname, image, flavor, key_name,
nic, sec_group, avail_zone=None, user_data=None,
config_drive=None, files=None, retry_count=10, volume=None):
if sec_group:
security_groups = [sec_group["name"]]
else:
security_groups = None
# Also attach the created security group for the test
vol_map = None
if volume:
vol_map = [{"source_type": "volume", "boot_index": "0",
"uuid": volume.id, "destination_type": "volume"}]
image = None
instance = self.novaclient.servers.create(name=vmname,
image=image,
block_device_mapping_v2=vol_map,
flavor=flavor,
key_name=key_name,
nics=nic,
availability_zone=avail_zone,
userdata=user_data,
config_drive=config_drive,
files=files,
security_groups=security_groups)
if not instance:
return None
# Verify that the instance gets into the ACTIVE state
for retry_attempt in range(retry_count):
instance = self.novaclient.servers.get(instance.id)
if instance.status == 'ACTIVE':
return instance
if instance.status == 'ERROR':
LOG.error('Instance creation error: %s', instance.fault['message'])
break
LOG.debug("[%s] VM status=%s, retrying %s of %s...",
vmname, instance.status, (retry_attempt + 1), retry_count)
time.sleep(2)
# instance not in ACTIVE state
LOG.error('Instance failed status=%s', instance.status)
self.delete_server(instance)
return None
def get_server_list(self):
servers_list = self.novaclient.servers.list()
return servers_list
def find_floating_ips(self):
floating_ip = self.novaclient.floating_ips.list()
return floating_ip
# Return the server network for a server
def find_server_network(self, vmname):
servers_list = self.get_server_list()
for server in servers_list:
if server.name == vmname and server.status == "ACTIVE":
return server.networks
return None
# Returns True if server is present false if not.
# Retry for a few seconds since after VM creation sometimes
# it takes a while to show up
def find_server(self, vmname, retry_count):
for retry_attempt in range(retry_count):
servers_list = self.get_server_list()
for server in servers_list:
if server.name == vmname and server.status == "ACTIVE":
return True
# Sleep between retries
LOG.debug("[%s] VM not yet found, retrying %s of %s...",
vmname, (retry_attempt + 1), retry_count)
time.sleep(2)
LOG.error("[%s] VM not found, after %s attempts", vmname, retry_count)
return False
# Returns True if server is found and deleted/False if not,
# retry the delete if there is a delay
def delete_server_by_name(self, vmname):
servers_list = self.get_server_list()
for server in servers_list:
if server.name == vmname:
LOG.info('Deleting server %s', server)
self.novaclient.servers.delete(server)
return True
return False
def delete_server(self, server):
for _ in range(1, 5):
try:
self.novaclient.servers.delete(server)
except novaclient.exceptions.NotFound:
break
time.sleep(2)
def find_flavor(self, flavor_type):
try:
flavor = self.novaclient.flavors.find(name=flavor_type)
return flavor
except exceptions.NotFound:
return None
def create_flavor(self, flavor_type, vcpus, ram, disk, ephemeral=0, extra_specs=None):
flavor = self.novaclient.flavors.create(name=flavor_type, vcpus=vcpus, ram=ram, disk=disk,
ephemeral=ephemeral)
if extra_specs:
flavor.set_keys(extra_specs)
LOG.info('Flavor %s created.' % flavor_type)
return flavor
def delete_flavor(self, flavor):
try:
flavor.delete()
LOG.info('Flavor %s deleted.' % flavor.name)
return True
except novaclient.exceptions:
LOG.error('Failed deleting flavor %s.' % flavor.name)
return False
def normalize_az_host(self, az, host):
if not az:
az = self.config.availability_zone
return az + ':' + host
def auto_fill_az(self, host_list, host):
'''
no az provided, if there is a host list we can auto-fill the az
else we use the configured az if available
else we return an error
'''
if host_list:
for hyp in host_list:
if hyp.host == host:
return self.normalize_az_host(hyp.zone, host)
# no match on host
LOG.error('Passed host name does not exist: ' + host)
return None
if self.config.availability_zone:
return self.normalize_az_host(None, host)
LOG.error('--hypervisor passed without an az and no az configured')
return None
def sanitize_az_host(self, host_list, az_host):
'''
host_list: list of hosts as retrieved from openstack (can be empty)
az_host: either a host or a az:host string
if a host, will check host is in the list, find the corresponding az and
return az:host
if az:host is passed will check the host is in the list and az matches
if host_list is empty, will return the configured az if there is no
az passed
'''
if ':' in az_host:
# no host_list, return as is (no check)
if not host_list:
return az_host
# if there is a host_list, extract and verify the az and host
az_host_list = az_host.split(':')
zone = az_host_list[0]
host = az_host_list[1]
for hyp in host_list:
if hyp.host == host:
if hyp.zone == zone:
# matches
return az_host
# else continue - another zone with same host name?
# no match
LOG.error('No match for availability zone and host ' + az_host)
return None
else:
return self.auto_fill_az(host_list, az_host)
#
# Return a list of 0, 1 or 2 az:host
#
# The list is computed as follows:
# The list of all hosts is retrieved first from openstack
# if this fails, checks and az auto-fill are disabled
#
# If the user provides a list of hypervisors (--hypervisor)
# that list is checked and returned
#
# If the user provides a configured az name (config.availability_zone)
# up to the first 2 hosts from the list that match the az are returned
#
# If the user did not configure an az name
# up to the first 2 hosts from the list are returned
# Possible return values:
# [ az ]
# [ az:hyp ]
# [ az1:hyp1, az2:hyp2 ]
# [] if an error occurred (error message printed to console)
#
def get_az_host_list(self):
avail_list = []
host_list = []
try:
host_list = self.novaclient.services.list()
except novaclient.exceptions.Forbidden:
LOG.warning('Operation Forbidden: could not retrieve list of hosts'
' (likely no permission)')
# the user has specified a list of 1 or 2 hypervisors to use
if self.config.hypervisors:
for hyp in self.config.hypervisors:
hyp = self.sanitize_az_host(host_list, hyp)
if hyp:
avail_list.append(hyp)
else:
return []
# if the user did not specify an az, insert the configured az
if ':' not in hyp:
if self.config.availability_zone:
hyp = self.normalize_az_host(None, hyp)
else:
return []
# pick first 2 matches at most
if len(avail_list) == 2:
break
LOG.info('Using hypervisors ' + ', '.join(avail_list))
if len(avail_list) < 2:
for host in host_list:
# this host must be a compute node
if host.binary != 'nova-compute' or host.state != 'up' or host.status != 'enabled':
continue
candidate = None
if self.config.availability_zone:
if host.zone == self.config.availability_zone:
candidate = self.normalize_az_host(None, host.host)
else:
candidate = self.normalize_az_host(host.zone, host.host)
if candidate and candidate not in avail_list:
avail_list.append(candidate)
# pick first 2 matches at most
if len(avail_list) == 2:
break
# if empty we insert the configured az
if not avail_list:
if not self.config.availability_zone:
LOG.error('Availability_zone must be configured')
elif host_list:
LOG.error('No host matching the selection for availability zone: ' +
self.config.availability_zone)
avail_list = []
else:
avail_list = [self.config.availability_zone]
return avail_list
# Given 2 VMs test if they are running on same Host or not
def check_vm_placement(self, vm_instance1, vm_instance2):
try:
server_instance_1 = self.novaclient.servers.get(vm_instance1)
server_instance_2 = self.novaclient.servers.get(vm_instance2)
if server_instance_1.hostId == server_instance_2.hostId:
return True
else:
return False
except novaclient.exceptions:
LOG.warning("Exception in retrieving the hostId of servers")
# Create a new security group with appropriate rules
def security_group_create(self):
# check first the security group exists
for security_group in self.neutron.list_security_groups()["security_groups"]:
if security_group["name"] == self.config.security_group_name:
return security_group
group = self.neutron.create_security_group({
"security_group": {
"name": self.config.security_group_name,
"description": "PNS Security groups"}})
self.security_group_add_rules(group["security_group"])
return group["security_group"]
# Delete a security group
def security_group_delete(self, group):
if group:
LOG.info("Deleting security group")
for _ in range(1, 5):
try:
self.neutron.delete_security_group(group["id"])
break
except Conflict:
time.sleep(1)
# Add rules to the security group
def security_group_add_rules(self, group):
# Allow ping traffic
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="icmp"))
if self.config.ipv6_mode:
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="icmpv6",
ethertype="IPv6"))
# Allow SSH traffic
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="tcp",
port_range_min=22,
port_range_max=22))
# Allow TCP/UDP traffic for perf tools like iperf/nuttcp
# 5001: Data traffic (standard iperf data port)
# 5002: Control traffic (non standard)
# note that 5000/tcp is already picked by openstack keystone
if not self.config.ipv6_mode:
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="tcp",
port_range_min=5001,
port_range_max=5002))
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="udp",
port_range_min=5001,
port_range_max=5002))
else:
# IPV6 rules addition
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="tcp",
ethertype="IPv6",
port_range_min=22,
port_range_max=22))
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="tcp",
ethertype="IPv6",
port_range_min=5001,
port_range_max=5002))
self.neutron.create_security_group_rule(
self.generate_security_group_rule_dict(group_id=group["id"],
protocol="udp",
ethertype="IPv6",
port_range_min=5001,
port_range_max=5002))
# Generates dict for security group rule
def generate_security_group_rule_dict(self, group_id, protocol, ethertype='IPv4',
port_range_min=None,
port_range_max=None):
remote_ip_prefix = '0.0.0.0/0'
security_group_rule = {
'direction': 'ingress',
'security_group_id': group_id,
'ethertype': ethertype,
'port_range_min': port_range_min,
'port_range_max': port_range_max,
'protocol': protocol,
'remote_group_id': None}
if ethertype == 'IPv6':
remote_ip_prefix = '::/0'
security_group_rule.update({'remote_ip_prefix': remote_ip_prefix})
return {
'security_group_rule': security_group_rule}