diff --git a/kloudbuster/base_compute.py b/kloudbuster/base_compute.py index 84bdc3a..129fe58 100644 --- a/kloudbuster/base_compute.py +++ b/kloudbuster/base_compute.py @@ -312,15 +312,3 @@ class NovaQuota(object): def update_quota(self, **kwargs): self.novaclient.quotas.update(self.tenant_id, **kwargs) - -class CinderQuota(object): - - def __init__(self, cinderclient, tenant_id): - self.cinderclient = cinderclient - self.tenant_id = tenant_id - - def get(self): - return vars(self.cinderclient.quotas.get(self.tenant_id)) - - def update_quota(self, **kwargs): - self.cinderclient.quotas.update(self.tenant_id, **kwargs) diff --git a/kloudbuster/cfg.scale.yaml b/kloudbuster/cfg.scale.yaml index c68dd21..ed191c3 100644 --- a/kloudbuster/cfg.scale.yaml +++ b/kloudbuster/cfg.scale.yaml @@ -10,14 +10,26 @@ # COMMON CONFIG OPTIONS FOR BOTH SERVER AND CLIENT SIDE # ===================================================== +# The openrc file - can be overridden at the cli with the --rc argument +openrc_file: + # Name of the image to use for all test VMs (client, server and proxy) +# without the qcow2 extension # The image name must exist in OpenStack and built with appropriate packages. # The default test VM image is named "kloudbuster_v" where -# is the KloudBuster test VM image version (e.g. "kloudbuster_v3") +# is the KloudBuster test VM image version (e.g. "kloudbuster_v6") # Leave empty to use the default test VM image (recommended). # If non empty use quotes if there are space characters in the name (e.g. 'my image') image_name: +# KloudBuster can automatically upload a VM image if the image named by +# image_name is missing, for that you need to specify a file location where +# the image can be retrieved +# +# To upload the image as a file, download it to preferred location +# file:// +vm_image_file: + # Keystone admin role name (default should work in most deployments) keystone_admin_role: "admin" diff --git a/kloudbuster/credentials.py b/kloudbuster/credentials.py index 8c8cccd..b49fdfb 100644 --- a/kloudbuster/credentials.py +++ b/kloudbuster/credentials.py @@ -49,6 +49,28 @@ class Credentials(object): auth = v2.Password(**dct) return session.Session(auth=auth, verify=self.rc_cacert) + def get_user_session(self, username, password, tenant_name): + dct = { + 'username': username, + 'password': password, + 'auth_url': self.rc_auth_url + } + auth = None + + if self.rc_identity_api_version == 3: + dct.update({ + 'project_name': tenant_name, + 'project_domain_name': self.rc_project_domain_name, + 'user_domain_name': self.rc_user_domain_name + }) + auth = v3.Password(**dct) + else: + dct.update({ + 'tenant_name': tenant_name + }) + auth = v2.Password(**dct) + return session.Session(auth=auth, verify=self.rc_cacert) + def __parse_openrc(self, file): export_re = re.compile('export OS_([A-Z_]*)="?(.*)') for line in file: diff --git a/kloudbuster/kb_config.py b/kloudbuster/kb_config.py index f8858bf..8759f52 100644 --- a/kloudbuster/kb_config.py +++ b/kloudbuster/kb_config.py @@ -157,8 +157,11 @@ class KBConfig(object): def init_with_cli(self): self.storage_mode = CONF.storage self.multicast_mode = CONF.multicast - self.get_credentials() self.get_configs() + # check if an openrc file was passed from config file + if not CONF.tested_rc and self.config_scale['openrc_file']: + CONF.tested_rc = self.config_scale['openrc_file'] + self.get_credentials() self.get_topo_cfg() self.get_tenants_list() self.update_configs() diff --git a/kloudbuster/kb_runner_base.py b/kloudbuster/kb_runner_base.py index 1d3758d..0f5e234 100644 --- a/kloudbuster/kb_runner_base.py +++ b/kloudbuster/kb_runner_base.py @@ -19,6 +19,7 @@ from distutils.version import LooseVersion import json import log as logging import redis +import sys import threading import time @@ -83,6 +84,9 @@ class KBRunner(object): self.redis_obj.get("test") success = True except (redis.exceptions.ConnectionError): + # clear active exception to avoid the exception summary + # appended to LOG.info by oslo log + sys.exc_clear() LOG.info("Connecting to redis server... Retry #%d/%d", retry, retry_count) time.sleep(self.config.polling_interval) continue @@ -133,6 +137,8 @@ class KBRunner(object): msg = self.message_queue.popleft() except IndexError: # No new message, commands are in executing + # clear active exc to prevent LOG pollution + sys.exc_clear() break payload = eval(msg['data']) diff --git a/kloudbuster/kloudbuster.py b/kloudbuster/kloudbuster.py index bc172cf..ffee654 100755 --- a/kloudbuster/kloudbuster.py +++ b/kloudbuster/kloudbuster.py @@ -25,7 +25,8 @@ import webbrowser import base_compute import base_network -import glanceclient.exc as glance_exception +from cinderclient import client as cinderclient +from glanceclient import exc as glance_exception from glanceclient.v2 import client as glanceclient from kb_config import KBConfig from kb_res_logger import KBResLogger @@ -36,9 +37,11 @@ from kb_runner_storage import KBRunner_Storage from kb_scheduler import KBScheduler import kb_vm_agent +import keystoneauth1 from keystoneclient.v2_0 import client as keystoneclient import log as logging +from neutronclient.neutron import client as neutronclient from novaclient import client as novaclient from oslo_config import cfg import pbr.version @@ -64,6 +67,7 @@ class Kloud(object): self.reusing_tenants = reusing_tenants self.storage_mode = storage_mode self.multicast_mode = multicast_mode + self.credentials = cred self.osclient_session = cred.get_session() self.flavor_to_use = None self.vm_up_count = 0 @@ -78,7 +82,15 @@ class Kloud(object): self.placement_az = scale_cfg['availability_zone'] \ if scale_cfg['availability_zone'] else None self.exc_info = None + # these client handles use the kloudbuster credentials (usually admin) + # to do tenant creation, tenant nova+cinder quota allocation and the like self.keystone = keystoneclient.Client(session=self.osclient_session) + self.neutron_client = neutronclient.Client('2.0', endpoint_type='publicURL', + session=self.osclient_session) + self.nova_client = novaclient.Client('2', endpoint_type='publicURL', + session=self.osclient_session) + self.cinder_client = cinderclient.Client('2', endpoint_type='publicURL', + session=self.osclient_session) LOG.info("Creating kloud: " + self.prefix) if self.placement_az: LOG.info('%s Availability Zone: %s' % (self.name, self.placement_az)) @@ -306,46 +318,70 @@ class KloudBuster(object): def check_and_upload_images(self, retry_count=150): retry = 0 - creds_list = [self.server_cred.get_session(), - self.client_cred.get_session()] - creds_dict = dict(zip(['Server kloud', 'Client kloud'], creds_list)) - img_name_dict = dict(zip(['Server kloud', 'Client kloud'], - [self.server_cfg.image_name, self.client_cfg.image_name])) - - for kloud, sess in creds_dict.items(): + image_location = None + image_name = self.client_cfg.image_name + image_url = self.client_cfg.vm_image_file + kloud_name_list = ['Server kloud', 'Client kloud'] + session_list = [self.server_cred.get_session(), + self.client_cred.get_session()] + for kloud, sess in zip(kloud_name_list, session_list): glance_client = glanceclient.Client('2', session=sess) try: # Search for the image - img = glance_client.images.list(filters={'name': img_name_dict[kloud]}).next() + img = glance_client.images.list(filters={'name': image_name}).next() continue except StopIteration: - pass + sys.exc_clear() # Trying to upload images - kb_image_name = kb_vm_agent.get_image_name() + '.qcow2' - image_url = 'http://storage.apps.openstack.org/images/%s' % kb_image_name - LOG.info("Image is not found in %s, uploading from OpenStack App Store..." % kloud) + LOG.info("KloudBuster VM Image is not found in %s, trying to upload it..." % kloud) + + if not image_location: + if not image_url: + LOG.error('Configuration file is missing a VM image URL (vm_image_name)') + return False + file_prefix = 'file://' + if not image_url.startswith(file_prefix): + LOG.error('vm_image_name (%s) must start with "%s", aborting' % + (image_url, file_prefix)) + return False + image_location = image_url.split(file_prefix)[1] + retry = 0 try: - img = glance_client.images.create(name=img_name_dict[kloud], - disk_format="qcow2", - container_format="bare", - is_public=True, - copy_from=image_url) + LOG.info("Uploading VM Image from %s..." % image_location) + with open(image_location) as f_image: + img = glance_client.images.create(name=image_name, + disk_format="qcow2", + container_format="bare", + visibility="public") + glance_client.images.upload(img.id, image_data=f_image) + # Check for the image in glance while img.status in ['queued', 'saving'] and retry < retry_count: - img = glance_client.images.find(name=img.name) - retry = retry + 1 + img = glance_client.images.get(img.id) + retry += 1 + LOG.debug("Image not yet active, retrying %s of %s...", retry, retry_count) time.sleep(2) if img.status != 'active': - raise Exception + LOG.error("Image uploaded but too long to get to active state") + raise Exception("Image update active state timeout") 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.") + 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 IOError as exc: + # 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: %s", + image_url, str(exc)) + return False + except keystoneauth1.exceptions.http.NotFound as exc: + LOG.error("Authentication error while uploading the image: " + str(exc)) return False except Exception: - LOG.error("Failed while uploading the image, please make sure the cloud " - "under test has the access to URL: %s." % image_url) + LOG.error(traceback.format_exc()) + LOG.error("Failed while uploading the image: %s", str(exc)) return False - + return True return True def print_provision_info(self): @@ -611,7 +647,8 @@ class KloudBuster(object): cleanup_flag = False try: - cleanup_flag = self.testing_kloud.delete_resources() + if self.testing_kloud: + cleanup_flag = self.testing_kloud.delete_resources() except Exception: traceback.print_exc() if not cleanup_flag: diff --git a/kloudbuster/tenant.py b/kloudbuster/tenant.py index d418d75..b5a0546 100644 --- a/kloudbuster/tenant.py +++ b/kloudbuster/tenant.py @@ -12,12 +12,23 @@ # License for the specific language governing permissions and limitations # under the License. +import base_compute +import base_network +import base_storage + from keystoneclient import exceptions as keystone_exception import log as logging +import sys import users LOG = logging.getLogger(__name__) +class KBFlavorCheckException(Exception): + pass + +class KBQuotaCheckException(Exception): + pass + class Tenant(object): """ Holds the tenant resources @@ -61,7 +72,7 @@ class Tenant(object): LOG.info("Creating tenant: " + self.tenant_name) tenant_object = \ self.tenant_api.create(self.tenant_name, - domain="default", + # domain="default", description="KloudBuster tenant", enabled=True) return tenant_object @@ -76,11 +87,76 @@ class Tenant(object): # Should never come here raise Exception() + def update_quota(self): + nova_quota = base_compute.NovaQuota(self.kloud.nova_client, self.tenant_id) + nova_quota.update_quota(**self.tenant_quota['nova']) + + if self.kloud.storage_mode: + cinder_quota = base_storage.CinderQuota(self.kloud.cinder_client, self.tenant_id) + cinder_quota.update_quota(**self.tenant_quota['cinder']) + + neutron_quota = base_network.NeutronQuota(self.kloud.neutron_client, self.tenant_id) + neutron_quota.update_quota(self.tenant_quota['neutron']) + + def check_quota(self): + # Flavor check + flavor_manager = base_compute.Flavor(self.kloud.nova_client) + find_flag = False + fcand = {'vcpus': sys.maxint, 'ram': sys.maxint, 'disk': sys.maxint} + for flavor in flavor_manager.list(): + flavor = vars(flavor) + if flavor['vcpus'] < 1 or flavor['ram'] < 1024 or flavor['disk'] < 10: + continue + if flavor['vcpus'] < fcand['vcpus']: + fcand = flavor + if flavor['vcpus'] == fcand['vcpus'] and flavor['ram'] < fcand['ram']: + fcand = flavor + if flavor['vcpus'] == fcand['vcpus'] and flavor['ram'] == fcand['ram'] and\ + flavor['disk'] < fcand['disk']: + fcand = flavor + find_flag = True + + if find_flag: + LOG.info('Automatically selects flavor %s to instantiate VMs.' % fcand['name']) + self.kloud.flavor_to_use = fcand['name'] + else: + LOG.error('Cannot find a flavor which meets the minimum ' + 'requirements to instantiate VMs.') + raise KBFlavorCheckException() + + # Nova/Cinder/Neutron quota check + tenant_id = self.tenant_id + meet_quota = True + for quota_type in ['nova', 'cinder', 'neutron']: + if quota_type == 'nova': + quota_manager = base_compute.NovaQuota(self.kloud.nova_client, tenant_id) + elif quota_type == 'cinder': + quota_manager = base_storage.CinderQuota(self.kloud.cinder_client, tenant_id) + else: + quota_manager = base_network.NeutronQuota(self.kloud.neutron_client, tenant_id) + + meet_quota = True + quota = quota_manager.get() + for key, value in self.tenant_quota[quota_type].iteritems(): + if quota[key] < value: + meet_quota = False + break + + if not meet_quota: + LOG.error('%s quota is too small. Minimum requirement: %s.' % + (quota_type, self.tenant_quota[quota_type])) + raise KBQuotaCheckException() + def create_resources(self): """ Creates all the entities associated with a user offloads tasks to user class """ + if self.kloud.reusing_tenants: + self.check_quota() + else: + self.update_quota() + if self.reusing_users: user_name = self.reusing_users['username'] password = self.reusing_users['password'] diff --git a/kloudbuster/users.py b/kloudbuster/users.py index 4af9eb6..d69dd48 100644 --- a/kloudbuster/users.py +++ b/kloudbuster/users.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import sys - import base_compute import base_network from cinderclient import client as cinderclient @@ -24,12 +22,6 @@ from novaclient import client as novaclient LOG = logging.getLogger(__name__) -class KBFlavorCheckException(Exception): - pass - -class KBQuotaCheckException(Exception): - pass - class User(object): """ User class that stores router list @@ -49,10 +41,17 @@ class User(object): self.tenant = tenant self.res_logger = tenant.res_logger self.router_list = [] - # Store the nova, neutron and cinder client - self.nova_client = None - self.neutron_client = None - self.cinder_client = None + # nova, neutron and cinder client for this user + session = self.tenant.kloud.credentials.get_user_session(user_name, password, + tenant.tenant_name) + + # Create nova/neutron/cinder clients to be used for all operations + self.neutron_client = neutronclient.Client('2.0', endpoint_type='publicURL', + session=session) + self.nova_client = novaclient.Client('2', endpoint_type='publicURL', + http_log_debug=True, session=session) + self.cinder_client = cinderclient.Client('2', endpoint_type='publicURL', + session=session) # Each user is associated to 1 key pair at most self.key_pair = None self.key_name = None @@ -127,86 +126,12 @@ class User(object): return flag - def update_tenant_quota(self, tenant_quota): - nova_quota = base_compute.NovaQuota(self.nova_client, self.tenant.tenant_id) - nova_quota.update_quota(**tenant_quota['nova']) - - if self.tenant.kloud.storage_mode: - cinder_quota = base_compute.CinderQuota(self.cinder_client, self.tenant.tenant_id) - cinder_quota.update_quota(**tenant_quota['cinder']) - - neutron_quota = base_network.NeutronQuota(self.neutron_client, self.tenant.tenant_id) - neutron_quota.update_quota(tenant_quota['neutron']) - - def check_resources_quota(self): - # Flavor check - flavor_manager = base_compute.Flavor(self.nova_client) - find_flag = False - fcand = {'vcpus': sys.maxint, 'ram': sys.maxint, 'disk': sys.maxint} - for flavor in flavor_manager.list(): - flavor = vars(flavor) - if flavor['vcpus'] < 1 or flavor['ram'] < 1024 or flavor['disk'] < 10: - continue - if flavor['vcpus'] < fcand['vcpus']: - fcand = flavor - if flavor['vcpus'] == fcand['vcpus'] and flavor['ram'] < fcand['ram']: - fcand = flavor - if flavor['vcpus'] == fcand['vcpus'] and flavor['ram'] == fcand['ram'] and\ - flavor['disk'] < fcand['disk']: - fcand = flavor - find_flag = True - - if find_flag: - LOG.info('Automatically selects flavor %s to instantiate VMs.' % fcand['name']) - self.tenant.kloud.flavor_to_use = fcand['name'] - else: - LOG.error('Cannot find a flavor which meets the minimum ' - 'requirements to instantiate VMs.') - raise KBFlavorCheckException() - - # Nova/Cinder/Neutron quota check - tenant_id = self.tenant.tenant_id - meet_quota = True - for quota_type in ['nova', 'cinder', 'neutron']: - if quota_type == 'nova': - quota_manager = base_compute.NovaQuota(self.nova_client, tenant_id) - elif quota_type == 'cinder': - quota_manager = base_compute.CinderQuota(self.cinder_client, tenant_id) - else: - quota_manager = base_network.NeutronQuota(self.neutron_client, tenant_id) - - meet_quota = True - quota = quota_manager.get() - for key, value in self.tenant.tenant_quota[quota_type].iteritems(): - if quota[key] < value: - meet_quota = False - break - - if not meet_quota: - LOG.error('%s quota is too small. Minimum requirement: %s.' % - (quota_type, self.tenant.tenant_quota[quota_type])) - raise KBQuotaCheckException() - def create_resources(self): """ Creates all the User elements associated with a User 1. Creates the routers 2. Creates the neutron and nova client objects """ - session = self.tenant.kloud.osclient_session - - # Create nova/neutron/cinder clients to be used for all operations - self.neutron_client = neutronclient.Client('2.0', endpoint_type='publicURL', - session=session) - self.nova_client = novaclient.Client('2', endpoint_type='publicURL', - http_log_debug=True, session=session) - self.cinder_client = cinderclient.Client('2', endpoint_type='publicURL', - session=session) - - if self.tenant.kloud.reusing_tenants: - self.check_resources_quota() - else: - self.update_tenant_quota(self.tenant.tenant_quota) config_scale = self.tenant.kloud.scale_cfg