Add base directories for tets. Add tests with simple verification of status code for next cases:

* List services by admin in keystone
* List users by admin
* List instances
* List volumes
* List snapshots
* list flavors
* list rate limits
* List networks
* List ports

Change-Id: I1d030ffc2b3ed18c7a194693f1cd2a382b961c1c
This commit is contained in:
Tatyana Leontovich 2013-06-17 13:23:44 +03:00
parent 5dd517d172
commit 499d63fe96
79 changed files with 5003 additions and 0 deletions

318
etc/test.conf Normal file
View File

@ -0,0 +1,318 @@
[identity]
# This section contains configuration options that a variety of
# test clients use when authenticating with different user/tenant
# combinations
# The type of endpoint for a Identity service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
# this value as "identity"
catalog_type = identity
# Ignore SSL certificate validation failures? Use when in testing
# environments that have self-signed SSL certs.
disable_ssl_certificate_validation = False
# URL for where to find the OpenStack Identity API endpoint (Keystone)
uri = http://172.18.194.41:5000/v2.0/
# URL for where to find the OpenStack V3 Identity API endpoint (Keystone)
#uri_v3 = http://127.0.0.1:5000/v3/
# Should typically be left as keystone unless you have a non-Keystone
# authentication API service
strategy = keystone
# The identity region
region = RegionOne
# This should be the username of a user WITHOUT administrative privileges
username = demo
# The above non-administrative user's password
password = nova
# The above non-administrative user's tenant name
tenant_name = demo
# This should be the username of an alternate user WITHOUT
# administrative privileges
alt_username = alt_demo
# The above non-administrative user's password
alt_password = nova
# The above non-administrative user's tenant name
alt_tenant_name = alt_demo
# This should be the username of a user WITH administrative privileges
admin_username = admin
# The above administrative user's password
admin_password = nova
# The above administrative user's tenant name
admin_tenant_name = admin
[compute]
# This section contains configuration options used when executing tests
# against the OpenStack Compute API.
# Allows test cases to create/destroy tenants and users. This option
# enables isolated test cases and better parallel execution,
# but also requires that OpenStack Identity API admin credentials
# are known.
allow_tenant_isolation = True
# Allows test cases to create/destroy tenants and users. This option
# enables isolated test cases and better parallel execution,
# but also requires that OpenStack Identity API admin credentials
# are known.
allow_tenant_reuse = true
# Reference data for tests. The ref and ref_alt should be
# distinct images/flavors.
#image_ref = 0ee318a0-3a30-44b8-8b73-21f7ac00a6b5
#image_ref_alt = 0ee318a0-3a30-44b8-8b73-21f7ac00a6b5
#flavor_ref = 42
#flavor_ref_alt = 84
# User names used to authenticate to an instance for a given image.
image_ssh_user = cirros
image_alt_ssh_user = cirros
# Number of seconds to wait while looping to check the status of an
# instance that is building.
build_interval = 3
# Number of seconds to time out on waiting for an instance
# to build or reach an expected status
build_timeout = 400
# Run additional tests that use SSH for instance validation?
# This requires the instances be routable from the host
# executing the tests
run_ssh = false
# Name of a user used to authenticated to an instance
ssh_user = cirros
# Visible fixed network name
fixed_network_name = private
# Network id used for SSH (public, private, etc)
network_for_ssh = private
# IP version of the address used for SSH
ip_version_for_ssh = 4
# Number of seconds to wait to authenticate to an instance
ssh_timeout = 400
# Number of seconds to wait for output from ssh channel
ssh_channel_timeout = 60
# The type of endpoint for a Compute API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
# this value as "compute"
catalog_type = compute
# Does the Compute API support creation of images?
create_image_enabled = true
# For resize to work with libvirt/kvm, one of the following must be true:
# Single node: allow_resize_to_same_host=True must be set in nova.conf
# Cluster: the 'nova' user must have scp access between cluster nodes
resize_available = true
# Does the compute API support changing the admin password?
change_password_available=False
# Run live migration tests (requires 2 hosts)
live_migration_available = False
# Use block live migration (Otherwise, non-block migration will be
# performed, which requires XenServer pools in case of using XS)
use_block_migration_for_live_migration = False
# Supports iSCSI block migration - depends on a XAPI supporting
# relax-xsm-sr-check
block_migrate_supports_cinder_iscsi = false
# By default, rely on the status of the diskConfig extension to
# decide if to execute disk config tests. When set to false, tests
# are forced to skip, regardless of the extension status
disk_config_enabled_override = true
[compute-admin]
# This should be the username of a user WITH administrative privileges
# If not defined the admin user from the identity section will be used
username =
# The above administrative user's password
password =nova
# The above administrative user's tenant name
tenant_name =
[image]
# This section contains configuration options used when executing tests
# against the OpenStack Images API
# The type of endpoint for an Image API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
# this value as "image"
catalog_type = image
# The version of the OpenStack Images API to use
api_version = 1
# HTTP image to use for glance http image testing
http_image = http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
[network]
# This section contains configuration options used when executing tests
# against the OpenStack Network API.
# Version of the Quantum API
api_version = 2.0
# Catalog type of the Quantum Service
catalog_type = network
# A large private cidr block from which to allocate smaller blocks for
# tenant networks.
tenant_network_cidr = 10.100.0.0/16
# The mask bits used to partition the tenant block.
tenant_network_mask_bits = 28
# If tenant networks are reachable, connectivity checks will be
# performed directly against addresses on those networks.
tenant_networks_reachable = false
# Id of the public network that provides external connectivity.
public_network_id =
# Id of a shared public router that provides external connectivity.
# A shared public router would commonly be used where IP namespaces
# were disabled. If namespaces are enabled, it would be preferable
# for each tenant to have their own router.
public_router_id =
# Whether or not quantum is expected to be available
quantum_available = false
[volume]
# This section contains the configuration options used when executing tests
# against the OpenStack Block Storage API service
# The type of endpoint for a Cinder or Block Storage API service.
# Unless you have a custom Keystone service catalog implementation, you
# probably want to leave this value as "volume"
catalog_type = volume
# Number of seconds to wait while looping to check the status of a
# volume that is being made available
build_interval = 3
# Number of seconds to time out on waiting for a volume
# to be available or reach an expected status
build_timeout = 400
# Runs Cinder multi-backend tests (requires 2 backends declared in cinder.conf)
# They must have different volume_backend_name (backend1_name and backend2_name
# have to be different)
multi_backend_enabled = false
backend1_name = BACKEND_1
backend2_name = BACKEND_2
[object-storage]
# This section contains configuration options used when executing tests
# against the OpenStack Object Storage API.
# You can configure the credentials in the compute section
# The type of endpoint for an Object Storage API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
# this value as "object-store"
catalog_type = object-store
# Number of seconds to time on waiting for a container to container
# synchronization complete
container_sync_timeout = 120
# Number of seconds to wait while looping to check the status of a
# container to container synchronization
container_sync_interval = 5
[smoke]
# This section contains configuration options used when executing tests
# against the OpenStack Compute API.
# Allows test cases to create/destroy tenants and users. This option
# enables isolated test cases and better parallel execution,
# but also requires that OpenStack Identity API admin credentials
# are known.
allow_tenant_isolation = True
# Allows test cases to create/destroy tenants and users. This option
# enables isolated test cases and better parallel execution,
# but also requires that OpenStack Identity API admin credentials
# are known.
allow_tenant_reuse = true
# Reference data for tests. The ref and ref_alt should be
# distinct images/flavors.
#image_ref = cef3a728-63ad-498c-886c-f76a77c5defe
#image_ref_alt = cef3a728-63ad-498c-886c-f76a77c5defe
#flavor_ref = 42
#flavor_ref_alt = 84
# User names used to authenticate to an instance for a given image.
image_ssh_user = cirros
image_alt_ssh_user = cirros
# Number of seconds to wait while looping to check the status of an
# instance that is building.
build_interval = 3
# Number of seconds to time out on waiting for an instance
# to build or reach an expected status
build_timeout = 400
# Run additional tests that use SSH for instance validation?
# This requires the instances be routable from the host
# executing the tests
run_ssh = false
# Name of a user used to authenticated to an instance
ssh_user = cirros
# Visible fixed network name
fixed_network_name = private
# Network id used for SSH (public, private, etc)
network_for_ssh = private
# IP version of the address used for SSH
ip_version_for_ssh = 4
# Number of seconds to wait to authenticate to an instance
ssh_timeout = 400
# Number of seconds to wait for output from ssh channel
ssh_channel_timeout = 60
# The type of endpoint for a Compute API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
# this value as "compute"
catalog_type = compute
# Does the Compute API support creation of images?
create_image_enabled = true
# For resize to work with libvirt/kvm, one of the following must be true:
# Single node: allow_resize_to_same_host=True must be set in nova.conf
# Cluster: the 'nova' user must have scp access between cluster nodes
resize_available = true
# Does the compute API support changing the admin password?
change_password_available=False
# Run live migration tests (requires 2 hosts)
live_migration_available = False
# Use block live migration (Otherwise, non-block migration will be
# performed, which requires XenServer pools in case of using XS)
use_block_migration_for_live_migration = False
# Supports iSCSI block migration - depends on a XAPI supporting
# relax-xsm-sr-check
block_migrate_supports_cinder_iscsi = false
# By default, rely on the status of the diskConfig extension to
# decide if to execute disk config tests. When set to false, tests
# are forced to skip, regardless of the extension status
disk_config_enabled_override = true

0
fuel/__init__.py Normal file
View File

255
fuel/clients.py Normal file
View File

@ -0,0 +1,255 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
from fuel.common import log as logging
from fuel import config
from fuel import exceptions
from fuel.services.compute.json.fixed_ips_client import FixedIPsClientJSON
from fuel.services.compute.json.flavors_client import FlavorsClientJSON
from fuel.services.compute.json.floating_ips_client import \
FloatingIPsClientJSON
from fuel.services.compute.json.hosts_client import HostsClientJSON
from fuel.services.compute.json.hypervisor_client import \
HypervisorClientJSON
from fuel.services.compute.json.images_client import ImagesClientJSON
from fuel.services.compute.json.interfaces_client import \
InterfacesClientJSON
from fuel.services.compute.json.keypairs_client import KeyPairsClientJSON
from fuel.services.compute.json.limits_client import LimitsClientJSON
from fuel.services.compute.json.quotas_client import QuotasClientJSON
from fuel.services.compute.json.security_groups_client import \
SecurityGroupsClientJSON
from fuel.services.compute.json.servers_client import ServersClientJSON
from fuel.services.compute.json.services_client import ServicesClientJSON
from fuel.services.compute.json.tenant_usages_client import \
TenantUsagesClientJSON
from fuel.services.identity.json.identity_client import IdentityClientJSON
from fuel.services.identity.json.identity_client import TokenClientJSON
from fuel.services.network.json.network_client import NetworkClient
from fuel.services.volume.json.admin.volume_types_client import \
VolumeTypesClientJSON
from fuel.services.volume.json.snapshots_client import SnapshotsClientJSON
from fuel.services.volume.json.volumes_client import VolumesClientJSON
LOG = logging.getLogger(__name__)
IMAGES_CLIENTS = {
"json": ImagesClientJSON,
}
KEYPAIRS_CLIENTS = {
"json": KeyPairsClientJSON,
}
QUOTAS_CLIENTS = {
"json": QuotasClientJSON,
}
SERVERS_CLIENTS = {
"json": ServersClientJSON,
}
LIMITS_CLIENTS = {
"json": LimitsClientJSON,
}
FLAVORS_CLIENTS = {
"json": FlavorsClientJSON,
}
FLOAT_CLIENTS = {
"json": FloatingIPsClientJSON,
}
SNAPSHOTS_CLIENTS = {
"json": SnapshotsClientJSON,
}
VOLUMES_CLIENTS = {
"json": VolumesClientJSON,
}
VOLUME_TYPES_CLIENTS = {
"json": VolumeTypesClientJSON,
}
IDENTITY_CLIENT = {
"json": IdentityClientJSON,
}
TOKEN_CLIENT = {
"json": TokenClientJSON,
}
SECURITY_GROUPS_CLIENT = {
"json": SecurityGroupsClientJSON,
}
INTERFACES_CLIENT = {
"json": InterfacesClientJSON,
}
FIXED_IPS_CLIENT = {
"json": FixedIPsClientJSON,
}
SERVICES_CLIENT = {
"json": ServicesClientJSON,
}
TENANT_USAGES_CLIENT = {
"json": TenantUsagesClientJSON,
}
HYPERVISOR_CLIENT = {
"json": HypervisorClientJSON,
}
class Manager(object):
"""
Top level manager for OpenStack Compute clients
"""
def __init__(self, username=None, password=None, tenant_name=None,
interface='json'):
"""
We allow overriding of the credentials used within the various
client classes managed by the Manager object. Left as None, the
standard username/password/tenant_name is used.
:param username: Override of the username
:param password: Override of the password
:param tenant_name: Override of the tenant name
"""
self.config = config.FuelConfig()
# If no creds are provided, we fall back on the defaults
# in the config file for the Compute API.
self.username = username or self.config.identity.username
self.password = password or self.config.identity.password
self.tenant_name = tenant_name or self.config.identity.tenant_name
if None in (self.username, self.password, self.tenant_name):
msg = ("Missing required credentials. "
"username: %(username)s, password: %(password)s, "
"tenant_name: %(tenant_name)s") % locals()
raise exceptions.InvalidConfiguration(msg)
self.auth_url = self.config.identity.uri
self.auth_url_v3 = self.config.identity.uri_v3
if self.config.identity.strategy == 'keystone':
client_args = (self.config, self.username, self.password,
self.auth_url, self.tenant_name)
if self.auth_url_v3:
auth_version = 'v3'
client_args_v3_auth = (self.config, self.username,
self.password, self.auth_url_v3,
self.tenant_name, auth_version)
else:
client_args_v3_auth = None
else:
client_args = (self.config, self.username, self.password,
self.auth_url)
client_args_v3_auth = None
try:
self.servers_client = SERVERS_CLIENTS[interface](*client_args)
self.limits_client = LIMITS_CLIENTS[interface](*client_args)
self.images_client = IMAGES_CLIENTS[interface](*client_args)
self.keypairs_client = KEYPAIRS_CLIENTS[interface](*client_args)
self.quotas_client = QUOTAS_CLIENTS[interface](*client_args)
self.flavors_client = FLAVORS_CLIENTS[interface](*client_args)
self.floating_ips_client = FLOAT_CLIENTS[interface](*client_args)
self.snapshots_client = SNAPSHOTS_CLIENTS[interface](*client_args)
self.volumes_client = VOLUMES_CLIENTS[interface](*client_args)
self.volume_types_client = \
VOLUME_TYPES_CLIENTS[interface](*client_args)
self.identity_client = IDENTITY_CLIENT[interface](*client_args)
self.token_client = TOKEN_CLIENT[interface](self.config)
self.security_groups_client = \
SECURITY_GROUPS_CLIENT[interface](*client_args)
self.interfaces_client = INTERFACES_CLIENT[interface](*client_args)
self.fixed_ips_client = FIXED_IPS_CLIENT[interface](*client_args)
self.services_client = SERVICES_CLIENT[interface](*client_args)
self.tenant_usages_client = \
TENANT_USAGES_CLIENT[interface](*client_args)
self.hypervisor_client = HYPERVISOR_CLIENT[interface](*client_args)
if client_args_v3_auth:
self.servers_client_v3_auth = SERVERS_CLIENTS[interface](
*client_args_v3_auth)
else:
self.servers_client_v3_auth = None
except KeyError:
msg = "Unsupported interface type `%s'" % interface
raise exceptions.InvalidConfiguration(msg)
self.network_client = NetworkClient(*client_args)
self.hosts_client = HostsClientJSON(*client_args)
class AltManager(Manager):
"""
Manager object that uses the alt_XXX credentials for its
managed client objects
"""
def __init__(self):
conf = config.FuelConfig()
super(AltManager, self).__init__(conf.identity.alt_username,
conf.identity.alt_password,
conf.identity.alt_tenant_name)
class AdminManager(Manager):
"""
Manager object that uses the admin credentials for its
managed client objects
"""
def __init__(self, interface='json'):
conf = config.FuelConfig()
super(AdminManager, self).__init__(conf.identity.admin_username,
conf.identity.admin_password,
conf.identity.admin_tenant_name,
interface=interface)
class ComputeAdminManager(Manager):
"""
Manager object that uses the compute_admin credentials for its
managed client objects
"""
def __init__(self, interface='json'):
conf = config.FuelConfig()
base = super(ComputeAdminManager, self)
base.__init__(conf.compute_admin.username,
conf.compute_admin.password,
conf.compute_admin.tenant_name,
interface=interface)

1
fuel/common/__init__.py Normal file
View File

@ -0,0 +1 @@
__author__ = 'tleontovich'

116
fuel/common/log.py Normal file
View File

@ -0,0 +1,116 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 NEC Corporation.
# 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.
import ConfigParser
import inspect
import logging
import logging.config
import os
import re
from oslo.config import cfg
_DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
_loggers = {}
def getLogger(name='unknown'):
if len(_loggers) == 0:
loaded = _load_log_config()
getLogger.adapter = TestsAdapter if loaded else None
if name not in _loggers:
logger = logging.getLogger(name)
if getLogger.adapter:
_loggers[name] = getLogger.adapter(logger, name)
else:
_loggers[name] = logger
return _loggers[name]
def _load_log_config():
conf_dir = os.environ.get('FUEL_LOG_CONFIG_DIR', None)
conf_file = os.environ.get('FUEL_LOG_CONFIG', None)
if not conf_dir or not conf_file:
return False
log_config = os.path.join(conf_dir, conf_file)
try:
logging.config.fileConfig(log_config)
except ConfigParser.Error, exc:
raise cfg.ConfigFileParseError(log_config, str(exc))
return True
class TestsAdapter(logging.LoggerAdapter):
def __init__(self, logger, project_name):
self.logger = logger
self.project = project_name
self.regexp = re.compile(r"test_\w+\.py")
def __getattr__(self, key):
return getattr(self.logger, key)
def _get_test_name(self):
frames = inspect.stack()
for frame in frames:
binary_name = frame[1]
if self.regexp.search(binary_name) and 'self' in frame[0].f_locals:
return frame[0].f_locals.get('self').id()
elif frame[3] == '_run_cleanups':
#NOTE(myamazaki): method calling addCleanup
return frame[0].f_locals.get('self').case.id()
elif frame[3] in ['setUpClass', 'tearDownClass']:
#NOTE(myamazaki): setUpClass or tearDownClass
return "%s.%s.%s" % (frame[0].f_locals['cls'].__module__,
frame[0].f_locals['cls'].__name__,
frame[3])
return None
def process(self, msg, kwargs):
if 'extra' not in kwargs:
kwargs['extra'] = {}
extra = kwargs['extra']
test_name = self._get_test_name()
if test_name:
extra.update({'testname': test_name})
extra['extra'] = extra.copy()
return msg, kwargs
class TestsFormatter(logging.Formatter):
def __init__(self, fmt=None, datefmt=None):
super(TestsFormatter, self).__init__()
self.default_format = _DEFAULT_LOG_FORMAT
self.testname_format =\
"%(asctime)s %(levelname)8s [%(testname)s] %(message)s"
self.datefmt = _DEFAULT_LOG_DATE_FORMAT
def format(self, record):
extra = record.__dict__.get('extra', None)
if extra and 'testname' in extra:
self._fmt = self.testname_format
else:
self._fmt = self.default_format
return logging.Formatter.format(self, record)

511
fuel/common/rest_client.py Normal file
View File

@ -0,0 +1,511 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# Copyright 2013 IBM Corp.
# 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.
import collections
import hashlib
import httplib2
import json
from lxml import etree
import re
import time
from fuel.common import log as logging
from fuel import exceptions
# redrive rate limited calls at most twice
MAX_RECURSION_DEPTH = 2
TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
class RestClient(object):
TYPE = "json"
LOG = logging.getLogger(__name__)
def __init__(self, config, user, password, auth_url, tenant_name=None,
auth_version='v2'):
self.config = config
self.user = user
self.password = password
self.auth_url = auth_url
self.tenant_name = tenant_name
self.auth_version = auth_version
self.service = None
self.token = None
self.base_url = None
self.region = {'compute': self.config.identity.region}
self.endpoint_url = 'publicURL'
self.strategy = self.config.identity.strategy
self.headers = {'Content-Type': 'application/%s' % self.TYPE,
'Accept': 'application/%s' % self.TYPE}
self.build_interval = config.compute.build_interval
self.build_timeout = config.compute.build_timeout
self.general_header_lc = set(('cache-control', 'connection',
'date', 'pragma', 'trailer',
'transfer-encoding', 'via',
'warning'))
self.response_header_lc = set(('accept-ranges', 'age', 'etag',
'location', 'proxy-authenticate',
'retry-after', 'server',
'vary', 'www-authenticate'))
dscv = self.config.identity.disable_ssl_certificate_validation
self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
def _set_auth(self):
"""
Sets the token and base_url used in requests based on the strategy type
"""
if self.strategy == 'keystone':
if self.auth_version == 'v3':
auth_func = self.identity_auth_v3
else:
auth_func = self.keystone_auth
self.token, self.base_url = (
auth_func(self.user, self.password, self.auth_url,
self.service, self.tenant_name))
else:
self.token, self.base_url = self.basic_auth(self.user,
self.password,
self.auth_url)
def clear_auth(self):
"""
Can be called to clear the token and base_url so that the next request
will fetch a new token and base_url.
"""
self.token = None
self.base_url = None
def get_auth(self):
"""Returns the token of the current request or sets the token if
none.
"""
if not self.token:
self._set_auth()
return self.token
def basic_auth(self, user, password, auth_url):
"""
Provides authentication for the target API.
"""
params = {}
params['headers'] = {'User-Agent': 'Test-Client', 'X-Auth-User': user,
'X-Auth-Key': password}
resp, body = self.http_obj.request(auth_url, 'GET', **params)
try:
return resp['x-auth-token'], resp['x-server-management-url']
except Exception:
raise
def keystone_auth(self, user, password, auth_url, service, tenant_name):
"""
Provides authentication via Keystone using v2 identity API.
"""
# Normalize URI to ensure /tokens is in it.
if 'tokens' not in auth_url:
auth_url = auth_url.rstrip('/') + '/tokens'
creds = {
'auth': {
'passwordCredentials': {
'username': user,
'password': password,
},
'tenantName': tenant_name,
}
}
headers = {'Content-Type': 'application/json'}
body = json.dumps(creds)
self._log_request('POST', auth_url, headers, body)
resp, resp_body = self.http_obj.request(auth_url, 'POST',
headers=headers, body=body)
self._log_response(resp, resp_body)
if resp.status == 200:
try:
auth_data = json.loads(resp_body)['access']
token = auth_data['token']['id']
except Exception, e:
print "Failed to obtain token for user: %s" % e
raise
mgmt_url = None
for ep in auth_data['serviceCatalog']:
if ep["type"] == service:
for _ep in ep['endpoints']:
if service in self.region and \
_ep['region'] == self.region[service]:
mgmt_url = _ep[self.endpoint_url]
if not mgmt_url:
mgmt_url = ep['endpoints'][0][self.endpoint_url]
break
if mgmt_url is None:
raise exceptions.EndpointNotFound(service)
return token, mgmt_url
elif resp.status == 401:
raise exceptions.AuthenticationFailure(user=user,
password=password)
raise exceptions.IdentityError('Unexpected status code {0}'.format(
resp.status))
def identity_auth_v3(self, user, password, auth_url, service,
project_name, domain_id='default'):
"""Provides authentication using Identity API v3."""
req_url = auth_url.rstrip('/') + '/auth/tokens'
creds = {
"auth": {
"identity": {
"methods": ["password"],
"password": {
"user": {
"name": user, "password": password,
"domain": {"id": domain_id}
}
}
},
"scope": {
"project": {
"domain": {"id": domain_id},
"name": project_name
}
}
}
}
headers = {'Content-Type': 'application/json'}
body = json.dumps(creds)
resp, body = self.http_obj.request(req_url, 'POST',
headers=headers, body=body)
if resp.status == 201:
try:
token = resp['x-subject-token']
except Exception:
self.LOG.exception("Failed to obtain token using V3"
" authentication (auth URL is '%s')" %
req_url)
raise
catalog = json.loads(body)['token']['catalog']
mgmt_url = None
for service_info in catalog:
if service_info['type'] != service:
continue # this isn't the entry for us.
endpoints = service_info['endpoints']
# Look for an endpoint in the region if configured.
if service in self.region:
region = self.region[service]
for ep in endpoints:
if ep['region'] != region:
continue
mgmt_url = ep['url']
# FIXME(blk-u): this isn't handling endpoint type
# (public, internal, admin).
break
if not mgmt_url:
# Didn't find endpoint for region, use the first.
ep = endpoints[0]
mgmt_url = ep['url']
# FIXME(blk-u): this isn't handling endpoint type
# (public, internal, admin).
break
return token, mgmt_url
elif resp.status == 401:
raise exceptions.AuthenticationFailure(user=user,
password=password)
else:
self.LOG.error("Failed to obtain token using V3 authentication"
" (auth URL is '%s'), the response status is %s" %
(req_url, resp.status))
raise exceptions.AuthenticationFailure(user=user,
password=password)
def post(self, url, body, headers):
return self.request('POST', url, headers, body)
def get(self, url, headers=None):
return self.request('GET', url, headers)
def delete(self, url, headers=None):
return self.request('DELETE', url, headers)
def patch(self, url, body, headers):
return self.request('PATCH', url, headers, body)
def put(self, url, body, headers):
return self.request('PUT', url, headers, body)
def head(self, url, headers=None):
return self.request('HEAD', url, headers)
def copy(self, url, headers=None):
return self.request('COPY', url, headers)
def get_versions(self):
resp, body = self.get('')
body = self._parse_resp(body)
body = body['versions']
versions = map(lambda x: x['id'], body)
return resp, versions
def _log_request(self, method, req_url, headers, body):
self.LOG.info('Request: ' + method + ' ' + req_url)
if headers:
print_headers = headers
if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
token = headers['X-Auth-Token']
if len(token) > 64 and TOKEN_CHARS_RE.match(token):
print_headers = headers.copy()
print_headers['X-Auth-Token'] = "<Token omitted>"
self.LOG.debug('Request Headers: ' + str(print_headers))
if body:
str_body = str(body)
length = len(str_body)
self.LOG.debug('Request Body: ' + str_body[:2048])
if length >= 2048:
self.LOG.debug("Large body (%d) md5 summary: %s", length,
hashlib.md5(str_body).hexdigest())
def _log_response(self, resp, resp_body):
status = resp['status']
self.LOG.info("Response Status: " + status)
headers = resp.copy()
del headers['status']
if len(headers):
self.LOG.debug('Response Headers: ' + str(headers))
if resp_body:
str_body = str(resp_body)
length = len(str_body)
self.LOG.debug('Response Body: ' + str_body[:2048])
if length >= 2048:
self.LOG.debug("Large body (%d) md5 summary: %s", length,
hashlib.md5(str_body).hexdigest())
def _parse_resp(self, body):
return json.loads(body)
def response_checker(self, method, url, headers, body, resp, resp_body):
if (resp.status in set((204, 205, 304)) or resp.status < 200 or
method.upper() == 'HEAD') and resp_body:
raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
#NOTE(afazekas):
# If the HTTP Status Code is 205
# 'The response MUST NOT include an entity.'
# A HTTP entity has an entity-body and an 'entity-header'.
# In the HTTP response specification (Section 6) the 'entity-header'
# 'generic-header' and 'response-header' are in OR relation.
# All headers not in the above two group are considered as entity
# header in every interpretation.
if (resp.status == 205 and
0 != len(set(resp.keys()) - set(('status',)) -
self.response_header_lc - self.general_header_lc)):
raise exceptions.ResponseWithEntity()
#NOTE(afazekas)
# Now the swift sometimes (delete not empty container)
# returns with non json error response, we can create new rest class
# for swift.
# Usually RFC2616 says error responses SHOULD contain an explanation.
# The warning is normal for SHOULD/SHOULD NOT case
# Likely it will cause an error
if not resp_body and resp.status >= 400:
self.LOG.warning("status >= 400 response with empty body")
def _request(self, method, url,
headers=None, body=None):
"""A simple HTTP request interface."""
req_url = "%s/%s" % (self.base_url, url)
self._log_request(method, req_url, headers, body)
resp, resp_body = self.http_obj.request(req_url, method,
headers=headers, body=body)
self._log_response(resp, resp_body)
self.response_checker(method, url, headers, body, resp, resp_body)
return resp, resp_body
def request(self, method, url,
headers=None, body=None):
retry = 0
if (self.token is None) or (self.base_url is None):
self._set_auth()
if headers is None:
headers = {}
headers['X-Auth-Token'] = self.token
resp, resp_body = self._request(method, url,
headers=headers, body=body)
while (resp.status == 413 and
'retry-after' in resp and
not self.is_absolute_limit(
resp, self._parse_resp(resp_body)) and
retry < MAX_RECURSION_DEPTH):
retry += 1
delay = int(resp['retry-after'])
time.sleep(delay)
resp, resp_body = self._request(method, url,
headers=headers, body=body)
self._error_checker(method, url, headers, body,
resp, resp_body)
return resp, resp_body
def _error_checker(self, method, url,
headers, body, resp, resp_body):
# NOTE(mtreinish): Check for httplib response from glance_http. The
# object can't be used here because importing httplib breaks httplib2.
# If another object from a class not imported were passed here as
# resp this could possibly fail
if str(type(resp)) == "<type 'instance'>":
ctype = resp.getheader('content-type')
else:
try:
ctype = resp['content-type']
# NOTE(mtreinish): Keystone delete user responses doesn't have a
# content-type header. (They don't have a body) So just pretend it
# is set.
except KeyError:
ctype = 'application/json'
# It is not an error response
if resp.status < 400:
return
JSON_ENC = ['application/json; charset=UTF-8', 'application/json',
'application/json; charset=utf-8']
# NOTE(mtreinish): This is for compatibility with Glance and swift
# APIs. These are the return content types that Glance api v1
# (and occasionally swift) are using.
TXT_ENC = ['text/plain; charset=UTF-8', 'text/html; charset=UTF-8',
'text/plain; charset=utf-8']
XML_ENC = ['application/xml', 'application/xml; charset=UTF-8']
if ctype in JSON_ENC or ctype in XML_ENC:
parse_resp = True
elif ctype in TXT_ENC:
parse_resp = False
else:
raise exceptions.RestClientException(str(resp.status))
if resp.status == 401 or resp.status == 403:
raise exceptions.Unauthorized()
if resp.status == 404:
raise exceptions.NotFound(resp_body)
if resp.status == 400:
if parse_resp:
resp_body = self._parse_resp(resp_body)
raise exceptions.BadRequest(resp_body)
if resp.status == 409:
if parse_resp:
resp_body = self._parse_resp(resp_body)
raise exceptions.Duplicate(resp_body)
if resp.status == 413:
if parse_resp:
resp_body = self._parse_resp(resp_body)
if self.is_absolute_limit(resp, resp_body):
raise exceptions.OverLimit(resp_body)
else:
raise exceptions.RateLimitExceeded(resp_body)
if resp.status == 422:
if parse_resp:
resp_body = self._parse_resp(resp_body)
raise exceptions.UnprocessableEntity(resp_body)
if resp.status in (500, 501):
message = resp_body
if parse_resp:
resp_body = self._parse_resp(resp_body)
#I'm seeing both computeFault and cloudServersFault come back.
#Will file a bug to fix, but leave as is for now.
if 'cloudServersFault' in resp_body:
message = resp_body['cloudServersFault']['message']
elif 'computeFault' in resp_body:
message = resp_body['computeFault']['message']
elif 'error' in resp_body: # Keystone errors
message = resp_body['error']['message']
raise exceptions.IdentityError(message)
elif 'message' in resp_body:
message = resp_body['message']
raise exceptions.ComputeFault(message)
if resp.status >= 400:
if parse_resp:
resp_body = self._parse_resp(resp_body)
raise exceptions.RestClientException(str(resp.status))
def is_absolute_limit(self, resp, resp_body):
if (not isinstance(resp_body, collections.Mapping) or
'retry-after' not in resp):
return True
over_limit = resp_body.get('overLimit', None)
if not over_limit:
return True
return 'exceed' in over_limit.get('message', 'blabla')
def wait_for_resource_deletion(self, id):
"""Waits for a resource to be deleted."""
start_time = int(time.time())
while True:
if self.is_resource_deleted(id):
return
if int(time.time()) - start_time >= self.build_timeout:
raise exceptions.TimeoutException
time.sleep(self.build_interval)
def is_resource_deleted(self, id):
"""
Subclasses override with specific deletion detection.
"""
message = ('"%s" does not implement is_resource_deleted'
% self.__class__.__name__)
raise NotImplementedError(message)

View File

@ -0,0 +1,4 @@
LAST_REBOOT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
PING_IPV4_COMMAND = 'ping -c 3 '
PING_IPV6_COMMAND = 'ping6 -c 3 '
PING_PACKET_LOSS_REGEX = '(\d{1,3})\.?\d*\% packet loss'

Binary file not shown.

View File

@ -0,0 +1,77 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import itertools
import random
import re
import urllib
from fuel import exceptions
def rand_name(name='test'):
return name + str(random.randint(1, 0x7fffffff))
def rand_int_id(start=0, end=0x7fffffff):
return random.randint(start, end)
def build_url(host, port, api_version=None, path=None,
params=None, use_ssl=False):
"""Build the request URL from given host, port, path and parameters."""
pattern = 'v\d\.\d'
if re.match(pattern, path):
message = 'Version should not be included in path.'
raise exceptions.InvalidConfiguration(message=message)
if use_ssl:
url = "https://" + host
else:
url = "http://" + host
if port is not None:
url += ":" + port
url += "/"
if api_version is not None:
url += api_version + "/"
if path is not None:
url += path
if params is not None:
url += "?"
url += urllib.urlencode(params)
return url
def parse_image_id(image_ref):
"""Return the image id from a given image ref."""
return image_ref.rsplit('/')[-1]
def arbitrary_string(size=4, base_text=None):
"""
Return size characters from base_text, repeating the base_text infinitely
if needed.
"""
if not base_text:
base_text = 'test'
return ''.join(itertools.islice(itertools.cycle(base_text), size))

Binary file not shown.

27
fuel/common/utils/misc.py Normal file
View File

@ -0,0 +1,27 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
def singleton(cls):
"""Simple wrapper for classes that should only have a single instance."""
instances = {}
def getinstance():
if cls not in instances:
instances[cls] = cls()
return instances[cls]
return getinstance

BIN
fuel/common/utils/misc.pyc Normal file

Binary file not shown.

521
fuel/config.py Normal file
View File

@ -0,0 +1,521 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import os
import sys
from oslo.config import cfg
from fuel.common import log as logging
from fuel.common.utils.misc import singleton
LOG = logging.getLogger(__name__)
identity_group = cfg.OptGroup(name='identity',
title="Keystone Configuration Options")
IdentityGroup = [
cfg.StrOpt('catalog_type',
default='identity',
help="Catalog type of the Identity service."),
cfg.BoolOpt('disable_ssl_certificate_validation',
default=False,
help="Set to True if using self-signed SSL certificates."),
cfg.StrOpt('uri',
default=None,
help="Full URI of the OpenStack Identity API (Keystone), v2"),
cfg.StrOpt('uri_v3',
help='Full URI of the OpenStack Identity API (Keystone), v3'),
cfg.StrOpt('strategy',
default='keystone',
help="Which auth method does the environment use? "
"(basic|keystone)"),
cfg.StrOpt('region',
default='RegionOne',
help="The identity region name to use."),
cfg.StrOpt('username',
default='demo',
help="Username to use for Nova API requests."),
cfg.StrOpt('tenant_name',
default='demo',
help="Tenant name to use for Nova API requests."),
cfg.StrOpt('password',
default='pass',
help="API key to use when authenticating.",
secret=True),
cfg.StrOpt('alt_username',
default=None,
help="Username of alternate user to use for Nova API "
"requests."),
cfg.StrOpt('alt_tenant_name',
default=None,
help="Alternate user's Tenant name to use for Nova API "
"requests."),
cfg.StrOpt('alt_password',
default=None,
help="API key to use when authenticating as alternate user.",
secret=True),
cfg.StrOpt('admin_username',
default='admin',
help="Administrative Username to use for"
"Keystone API requests."),
cfg.StrOpt('admin_tenant_name',
default='admin',
help="Administrative Tenant name to use for Keystone API "
"requests."),
cfg.StrOpt('admin_password',
default='pass',
help="API key to use when authenticating as admin.",
secret=True),
]
def register_identity_opts(conf):
conf.register_group(identity_group)
for opt in IdentityGroup:
conf.register_opt(opt, group='identity')
compute_group = cfg.OptGroup(name='compute',
title='Compute Service Options')
ComputeGroup = [
cfg.BoolOpt('allow_tenant_isolation',
default=False,
help="Allows test cases to create/destroy tenants and "
"users. This option enables isolated test cases and "
"better parallel execution, but also requires that "
"OpenStack Identity API admin credentials are known."),
cfg.BoolOpt('allow_tenant_reuse',
default=True,
help="If allow_tenant_isolation is True and a tenant that "
"would be created for a given test already exists (such "
"as from a previously-failed run), re-use that tenant "
"instead of failing because of the conflict. Note that "
"this would result in the tenant being deleted at the "
"end of a subsequent successful run."),
cfg.StrOpt('image_ssh_user',
default="root",
help="User name used to authenticate to an instance."),
cfg.StrOpt('image_alt_ssh_user',
default="root",
help="User name used to authenticate to an instance using "
"the alternate image."),
cfg.BoolOpt('resize_available',
default=False,
help="Does the test environment support resizing?"),
cfg.BoolOpt('live_migration_available',
default=False,
help="Does the test environment support live migration "
"available?"),
cfg.BoolOpt('use_block_migration_for_live_migration',
default=False,
help="Does the test environment use block devices for live "
"migration"),
cfg.BoolOpt('block_migrate_supports_cinder_iscsi',
default=False,
help="Does the test environment block migration support "
"cinder iSCSI volumes"),
cfg.BoolOpt('change_password_available',
default=False,
help="Does the test environment support changing the admin "
"password?"),
cfg.BoolOpt('create_image_enabled',
default=False,
help="Does the test environment support snapshots?"),
cfg.IntOpt('build_interval',
default=10,
help="Time in seconds between build status checks."),
cfg.IntOpt('build_timeout',
default=300,
help="Timeout in seconds to wait for an instance to build."),
cfg.BoolOpt('run_ssh',
default=False,
help="Does the test environment support snapshots?"),
cfg.StrOpt('ssh_user',
default='root',
help="User name used to authenticate to an instance."),
cfg.IntOpt('ssh_timeout',
default=300,
help="Timeout in seconds to wait for authentication to "
"succeed."),
cfg.IntOpt('ssh_channel_timeout',
default=60,
help="Timeout in seconds to wait for output from ssh "
"channel."),
cfg.StrOpt('fixed_network_name',
default='private',
help="Visible fixed network name "),
cfg.StrOpt('network_for_ssh',
default='public',
help="Network used for SSH connections."),
cfg.IntOpt('ip_version_for_ssh',
default=4,
help="IP version used for SSH connections."),
cfg.StrOpt('catalog_type',
default='compute',
help="Catalog type of the Compute service."),
cfg.StrOpt('path_to_private_key',
default=None,
help="Path to a private key file for SSH access to remote "
"hosts"),
cfg.BoolOpt('disk_config_enabled_override',
default=True,
help="If false, skip config tests regardless of the "
"extension status"),
]
def register_compute_opts(conf):
conf.register_group(compute_group)
for opt in ComputeGroup:
conf.register_opt(opt, group='compute')
compute_admin_group = cfg.OptGroup(name='compute-admin',
title="Compute Admin Options")
ComputeAdminGroup = [
cfg.StrOpt('username',
default='admin',
help="Administrative Username to use for Nova API requests."),
cfg.StrOpt('tenant_name',
default='admin',
help="Administrative Tenant name to use for Nova API "
"requests."),
cfg.StrOpt('password',
default='pass',
help="API key to use when authenticating as admin.",
secret=True),
]
def register_compute_admin_opts(conf):
conf.register_group(compute_admin_group)
for opt in ComputeAdminGroup:
conf.register_opt(opt, group='compute-admin')
image_group = cfg.OptGroup(name='image',
title="Image Service Options")
ImageGroup = [
cfg.StrOpt('api_version',
default='1',
help="Version of the API"),
cfg.StrOpt('catalog_type',
default='image',
help='Catalog type of the Image service.'),
cfg.StrOpt('http_image',
default='http://download.cirros-cloud.net/0.3.1/'
'cirros-0.3.1-x86_64-uec.tar.gz',
help='http accessable image')
]
def register_image_opts(conf):
conf.register_group(image_group)
for opt in ImageGroup:
conf.register_opt(opt, group='image')
network_group = cfg.OptGroup(name='network',
title='Network Service Options')
NetworkGroup = [
cfg.StrOpt('catalog_type',
default='network',
help='Catalog type of the Quantum service.'),
cfg.StrOpt('tenant_network_cidr',
default="10.100.0.0/16",
help="The cidr block to allocate tenant networks from"),
cfg.IntOpt('tenant_network_mask_bits',
default=29,
help="The mask bits for tenant networks"),
cfg.BoolOpt('tenant_networks_reachable',
default=False,
help="Whether tenant network connectivity should be "
"evaluated directly"),
cfg.StrOpt('public_network_id',
default="",
help="Id of the public network that provides external "
"connectivity"),
cfg.StrOpt('public_router_id',
default="",
help="Id of the public router that provides external "
"connectivity"),
cfg.BoolOpt('quantum_available',
default=False,
help="Whether or not quantum is expected to be available"),
]
def register_network_opts(conf):
conf.register_group(network_group)
for opt in NetworkGroup:
conf.register_opt(opt, group='network')
volume_group = cfg.OptGroup(name='volume',
title='Block Storage Options')
VolumeGroup = [
cfg.IntOpt('build_interval',
default=10,
help='Time in seconds between volume availability checks.'),
cfg.IntOpt('build_timeout',
default=300,
help='Timeout in seconds to wait for a volume to become'
'available.'),
cfg.StrOpt('catalog_type',
default='Volume',
help="Catalog type of the Volume Service"),
cfg.BoolOpt('multi_backend_enabled',
default=False,
help="Runs Cinder multi-backend test (requires 2 backends)"),
cfg.StrOpt('backend1_name',
default='BACKEND_1',
help="Name of the backend1 (must be declared in cinder.conf)"),
cfg.StrOpt('backend2_name',
default='BACKEND_2',
help="Name of the backend2 (must be declared in cinder.conf)"),
]
def register_volume_opts(conf):
conf.register_group(volume_group)
for opt in VolumeGroup:
conf.register_opt(opt, group='volume')
object_storage_group = cfg.OptGroup(name='object-storage',
title='Object Storage Service Options')
ObjectStoreConfig = [
cfg.StrOpt('catalog_type',
default='object-store',
help="Catalog type of the Object-Storage service."),
cfg.StrOpt('container_sync_timeout',
default=120,
help="Number of seconds to time on waiting for a container"
"to container synchronization complete."),
cfg.StrOpt('container_sync_interval',
default=5,
help="Number of seconds to wait while looping to check the"
"status of a container to container synchronization"),
]
def register_object_storage_opts(conf):
conf.register_group(object_storage_group)
for opt in ObjectStoreConfig:
conf.register_opt(opt, group='object-storage')
orchestration_group = cfg.OptGroup(name='orchestration',
title='Orchestration Service Options')
OrchestrationGroup = [
cfg.StrOpt('catalog_type',
default='orchestration',
help="Catalog type of the Orchestration service."),
cfg.BoolOpt('allow_tenant_isolation',
default=False,
help="Allows test cases to create/destroy tenants and "
"users. This option enables isolated test cases and "
"better parallel execution, but also requires that "
"OpenStack Identity API admin credentials are known."),
cfg.IntOpt('build_interval',
default=1,
help="Time in seconds between build status checks."),
cfg.IntOpt('build_timeout',
default=300,
help="Timeout in seconds to wait for a stack to build."),
cfg.BoolOpt('heat_available',
default=False,
help="Whether or not Heat is expected to be available"),
cfg.StrOpt('instance_type',
default='m1.micro',
help="Instance type for tests. Needs to be big enough for a "
"full OS plus the test workload"),
cfg.StrOpt('image_ref',
default=None,
help="Name of heat-cfntools enabled image to use when "
"launching test instances."),
cfg.StrOpt('keypair_name',
default=None,
help="Name of existing keypair to launch servers with."),
]
smoke_group = cfg.OptGroup(name='smoke',
title='Smoke Tests Options')
SmokeGroup = [
cfg.BoolOpt('allow_tenant_isolation',
default=False,
help="Allows test cases to create/destroy tenants and "
"users. This option enables isolated test cases and "
"better parallel execution, but also requires that "
"OpenStack Identity API admin credentials are known."),
cfg.BoolOpt('allow_tenant_reuse',
default=True,
help="If allow_tenant_isolation is True and a tenant that "
"would be created for a given test already exists (such "
"as from a previously-failed run), re-use that tenant "
"instead of failing because of the conflict. Note that "
"this would result in the tenant being deleted at the "
"end of a subsequent successful run."),
cfg.StrOpt('image_ref',
default="{$IMAGE_ID}",
help="Valid secondary image reference to be used in tests."),
cfg.StrOpt('image_ref_alt',
default="{$IMAGE_ID_ALT}",
help="Valid secondary image reference to be used in tests."),
cfg.IntOpt('flavor_ref',
default=1,
help="Valid primary flavor to use in tests."),
cfg.IntOpt('flavor_ref_alt',
default=2,
help='Valid secondary flavor to be used in tests.'),
cfg.StrOpt('image_ssh_user',
default="root",
help="User name used to authenticate to an instance."),
cfg.StrOpt('image_alt_ssh_user',
default="root",
help="User name used to authenticate to an instance using "
"the alternate image."),
cfg.BoolOpt('resize_available',
default=False,
help="Does the test environment support resizing?"),
cfg.BoolOpt('live_migration_available',
default=False,
help="Does the test environment support live migration "
"available?"),
cfg.BoolOpt('use_block_migration_for_live_migration',
default=False,
help="Does the test environment use block devices for live "
"migration"),
cfg.BoolOpt('block_migrate_supports_cinder_iscsi',
default=False,
help="Does the test environment block migration support "
"cinder iSCSI volumes"),
cfg.BoolOpt('change_password_available',
default=False,
help="Does the test environment support changing the admin "
"password?"),
cfg.BoolOpt('create_image_enabled',
default=False,
help="Does the test environment support snapshots?"),
cfg.IntOpt('build_interval',
default=10,
help="Time in seconds between build status checks."),
cfg.IntOpt('build_timeout',
default=300,
help="Timeout in seconds to wait for an instance to build."),
cfg.BoolOpt('run_ssh',
default=False,
help="Does the test environment support snapshots?"),
cfg.StrOpt('ssh_user',
default='root',
help="User name used to authenticate to an instance."),
cfg.IntOpt('ssh_timeout',
default=300,
help="Timeout in seconds to wait for authentication to "
"succeed."),
cfg.IntOpt('ssh_channel_timeout',
default=60,
help="Timeout in seconds to wait for output from ssh "
"channel."),
cfg.StrOpt('fixed_network_name',
default='private',
help="Visible fixed network name "),
cfg.StrOpt('network_for_ssh',
default='public',
help="Network used for SSH connections."),
cfg.IntOpt('ip_version_for_ssh',
default=4,
help="IP version used for SSH connections."),
cfg.StrOpt('catalog_type',
default='compute',
help="Catalog type of the Compute service."),
cfg.StrOpt('path_to_private_key',
default=None,
help="Path to a private key file for SSH access to remote "
"hosts"),
cfg.BoolOpt('disk_config_enabled_override',
default=True,
help="If false, skip config tests regardless of the "
"extension status"),
]
def register_smoke_opts(conf):
conf.register_group(smoke_group)
for opt in SmokeGroup:
conf.register_opt(opt, group='smoke')
@singleton
class FuelConfig:
"""Provides OpenStack configuration information."""
DEFAULT_CONFIG_DIR = os.path.join(
os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
"etc")
DEFAULT_CONFIG_FILE = "test.conf"
def __init__(self):
"""Initialize a configuration from a conf directory and conf file."""
config_files = []
failsafe_path = "/etc/fuel/" + self.DEFAULT_CONFIG_FILE
# Environment variables override defaults...
conf_dir = os.environ.get('FUEL_CONFIG_DIR',
self.DEFAULT_CONFIG_DIR)
conf_file = os.environ.get('FUEL_CONFIG', self.DEFAULT_CONFIG_FILE)
path = os.path.join(conf_dir, conf_file)
if not (os.path.isfile(path) or
'FUEL_CONFIG_DIR' in os.environ or
'FUEL_CONFIG' in os.environ):
path = failsafe_path
LOG.info("Using fuel config file %s" % path)
if not os.path.exists(path):
msg = "Config file %(path)s not found" % locals()
print >> sys.stderr, RuntimeError(msg)
else:
config_files.append(path)
cfg.CONF([], project='fuel', default_config_files=config_files)
register_compute_opts(cfg.CONF)
register_identity_opts(cfg.CONF)
register_network_opts(cfg.CONF)
register_volume_opts(cfg.CONF)
register_compute_admin_opts(cfg.CONF)
self.compute = cfg.CONF.compute
self.identity = cfg.CONF.identity
self.network = cfg.CONF.network
self.volume = cfg.CONF.volume
self.compute_admin = cfg.CONF['compute-admin']
if not self.compute_admin.username:
self.compute_admin.username = self.identity.admin_username
self.compute_admin.password = self.identity.admin_password
self.compute_admin.tenant_name = self.identity.admin_tenant_name

174
fuel/exceptions.py Normal file
View File

@ -0,0 +1,174 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import testtools
class FuelException(Exception):
"""
Base Tempest Exception
To correctly use this class, inherit from it and define
a 'message' property. That message will get printf'd
with the keyword arguments provided to the constructor.
"""
message = "An unknown exception occurred"
def __init__(self, *args, **kwargs):
super(FuelException, self).__init__()
try:
self._error_string = self.message % kwargs
except Exception:
# at least get the core message out if something happened
self._error_string = self.message
if len(args) > 0:
# If there is a non-kwarg parameter, assume it's the error
# message or reason description and tack it on to the end
# of the exception message
# Convert all arguments into their string representations...
args = ["%s" % arg for arg in args]
self._error_string = (self._error_string +
"\nDetails: %s" % '\n'.join(args))
def __str__(self):
return self._error_string
class InvalidConfiguration(FuelException):
message = "Invalid Configuration"
class RestClientException(FuelException,
testtools.TestCase.failureException):
pass
class NotFound(RestClientException):
message = "Object not found"
class Unauthorized(RestClientException):
message = 'Unauthorized'
class TimeoutException(FuelException):
message = "Request timed out"
class BuildErrorException(FuelException):
message = "Server %(server_id)s failed to build and is in ERROR status"
class AddImageException(FuelException):
message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
class EC2RegisterImageException(FuelException):
message = ("Image %(image_id)s failed to become 'available' "
"in the allotted time")
class VolumeBuildErrorException(FuelException):
message = "Volume %(volume_id)s failed to build and is in ERROR status"
class SnapshotBuildErrorException(FuelException):
message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
class StackBuildErrorException(FuelException):
message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
"due to '%(stack_status_reason)s'")
class BadRequest(RestClientException):
message = "Bad request"
class UnprocessableEntity(RestClientException):
message = "Unprocessable entity"
class AuthenticationFailure(RestClientException):
message = ("Authentication with user %(user)s and password "
"%(password)s failed")
class EndpointNotFound(FuelException):
message = "Endpoint not found"
class RateLimitExceeded(FuelException):
message = ("Rate limit exceeded.\nMessage: %(message)s\n"
"Details: %(details)s")
class OverLimit(FuelException):
message = "Quota exceeded"
class ComputeFault(FuelException):
message = "Got compute fault"
class ImageFault(FuelException):
message = "Got image fault"
class IdentityError(FuelException):
message = "Got identity error"
class Duplicate(RestClientException):
message = "An object with that identifier already exists"
class SSHTimeout(FuelException):
message = ("Connection to the %(host)s via SSH timed out.\n"
"User: %(user)s, Password: %(password)s")
class SSHExecCommandFailed(FuelException):
"""Raised when remotely executed command returns nonzero status."""
message = ("Command '%(command)s', exit status: %(exit_status)d, "
"Error:\n%(strerror)s")
class ServerUnreachable(FuelException):
message = "The server is not reachable via the configured network"
class SQLException(FuelException):
message = "SQL error: %(message)s"
class TearDownException(FuelException):
message = "%(num)d cleanUp operation failed"
class RFCViolation(RestClientException):
message = "RFC Violation"
class ResponseWithNonEmptyBody(RFCViolation):
message = ("RFC Violation! Response with %(status)d HTTP Status Code "
"MUST NOT have a body")
class ResponseWithEntity(RFCViolation):
message = ("RFC Violation! Response with 205 HTTP Status Code "
"MUST NOT have an entity")

161
fuel/manager.py Normal file
View File

@ -0,0 +1,161 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
from fuel.common import log as logging
import fuel.config
from fuel import exceptions
# REST Fuzz testing client libs
from fuel.services.compute.json import flavors_client
from fuel.services.compute.json import floating_ips_client
from fuel.services.compute.json import hypervisor_client
from fuel.services.compute.json import images_client
from fuel.services.compute.json import keypairs_client
from fuel.services.compute.json import limits_client
from fuel.services.compute.json import quotas_client
from fuel.services.compute.json import security_groups_client
from fuel.services.compute.json import servers_client
from fuel.services.network.json import network_client
from fuel.services.volume.json import snapshots_client
from fuel.services.volume.json import volumes_client
NetworkClient = network_client.NetworkClient
ImagesClient = images_client.ImagesClientJSON
FlavorsClient = flavors_client.FlavorsClientJSON
ServersClient = servers_client.ServersClientJSON
LimitsClient = limits_client.LimitsClientJSON
FloatingIPsClient = floating_ips_client.FloatingIPsClientJSON
SecurityGroupsClient = security_groups_client.SecurityGroupsClientJSON
KeyPairsClient = keypairs_client.KeyPairsClientJSON
VolumesClient = volumes_client.VolumesClientJSON
SnapshotsClient = snapshots_client.SnapshotsClientJSON
QuotasClient = quotas_client.QuotasClientJSON
HypervisorClient = hypervisor_client.HypervisorClientJSON
LOG = logging.getLogger(__name__)
class Manager(object):
"""
Base manager class
Manager objects are responsible for providing a configuration object
and a client object for a test case to use in performing actions.
"""
def __init__(self):
self.config = fuel.config.TempestConfig()
self.client_attr_names = []
class FuzzClientManager(Manager):
"""
Manager class that indicates the client provided by the manager
is a fuzz-testing client that Tempest contains. These fuzz-testing
clients are used to be able to throw random or invalid data at
an endpoint and check for appropriate error messages returned
from the endpoint.
"""
pass
class ComputeFuzzClientManager(FuzzClientManager):
"""
Manager that uses the Tempest REST client that can send
random or invalid data at the OpenStack Compute API
"""
def __init__(self, username=None, password=None, tenant_name=None):
"""
We allow overriding of the credentials used within the various
client classes managed by the Manager object. Left as None, the
standard username/password/tenant_name is used.
:param username: Override of the username
:param password: Override of the password
:param tenant_name: Override of the tenant name
"""
super(ComputeFuzzClientManager, self).__init__()
# If no creds are provided, we fall back on the defaults
# in the config file for the Compute API.
username = username or self.config.identity.username
password = password or self.config.identity.password
tenant_name = tenant_name or self.config.identity.tenant_name
if None in (username, password, tenant_name):
msg = ("Missing required credentials. "
"username: %(username)s, password: %(password)s, "
"tenant_name: %(tenant_name)s") % locals()
raise exceptions.InvalidConfiguration(msg)
auth_url = self.config.identity.uri
# Ensure /tokens is in the URL for Keystone...
if 'tokens' not in auth_url:
auth_url = auth_url.rstrip('/') + '/tokens'
if self.config.identity.strategy == 'keystone':
client_args = (self.config, username, password, auth_url,
tenant_name)
else:
client_args = (self.config, username, password, auth_url)
self.servers_client = ServersClient(*client_args)
self.flavors_client = FlavorsClient(*client_args)
self.images_client = ImagesClient(*client_args)
self.limits_client = LimitsClient(*client_args)
self.keypairs_client = KeyPairsClient(*client_args)
self.security_groups_client = SecurityGroupsClient(*client_args)
self.floating_ips_client = FloatingIPsClient(*client_args)
self.volumes_client = VolumesClient(*client_args)
self.snapshots_client = SnapshotsClient(*client_args)
self.quotas_client = QuotasClient(*client_args)
self.network_client = NetworkClient(*client_args)
self.hypervisor_client = HypervisorClient(*client_args)
class ComputeFuzzClientAltManager(Manager):
"""
Manager object that uses the alt_XXX credentials for its
managed client objects
"""
def __init__(self):
conf = fuel.config.TempestConfig()
super(ComputeFuzzClientAltManager, self).__init__(
conf.identity.alt_username,
conf.identity.alt_password,
conf.identity.alt_tenant_name)
class ComputeFuzzClientAdminManager(Manager):
"""
Manager object that uses the alt_XXX credentials for its
managed client objects
"""
def __init__(self):
conf = fuel.config.TempestConfig()
super(ComputeFuzzClientAdminManager, self).__init__(
conf.compute_admin.username,
conf.compute_admin.password,
conf.compute_admin.tenant_name)

58
fuel/sanity/__init__.py Normal file
View File

@ -0,0 +1,58 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
from fuel import clients
from fuel.common import log as logging
from fuel import config
from fuel.exceptions import InvalidConfiguration
LOG = logging.getLogger(__name__)
CONFIG = config.FuelConfig()
CREATE_IMAGE_ENABLED = CONFIG.compute.create_image_enabled
RESIZE_AVAILABLE = CONFIG.compute.resize_available
CHANGE_PASSWORD_AVAILABLE = CONFIG.compute.change_password_available
DISK_CONFIG_ENABLED = True
DISK_CONFIG_ENABLED_OVERRIDE = CONFIG.compute.disk_config_enabled_override
FLAVOR_EXTRA_DATA_ENABLED = True
MULTI_USER = True
# All compute tests -- single setup function
def generic_setup_package():
LOG.debug("Entering fuel.setup_package")
global MULTI_USER, DISK_CONFIG_ENABLED, FLAVOR_EXTRA_DATA_ENABLED
os = clients.Manager()
# Determine if there are two regular users that can be
# used in testing. If the test cases are allowed to create
# users (config.compute.allow_tenant_isolation is true,
# then we allow multi-user.
if not CONFIG.compute.allow_tenant_isolation:
user1 = CONFIG.identity.username
user2 = CONFIG.identity.alt_username
if not user2 or user1 == user2:
MULTI_USER = False
else:
user2_password = CONFIG.identity.alt_password
user2_tenant_name = CONFIG.identity.alt_tenant_name
if not user2_password or not user2_tenant_name:
msg = ("Alternate user specified but not alternate "
"tenant or password: alt_tenant_name=%s alt_password=%s"
% (user2_tenant_name, user2_password))
raise InvalidConfiguration(msg)

348
fuel/sanity/base.py Normal file
View File

@ -0,0 +1,348 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import netaddr
import time
from fuel import clients
from fuel.common.utils.data_utils import rand_name
import fuel.test
from fuel import sanity
from fuel.common import log as logging
from fuel import exceptions
LOG = logging.getLogger(__name__)
class BaseComputeTest(fuel.test.BaseTestCase):
"""Base test case class for all Compute API tests."""
conclusion = sanity.generic_setup_package()
@classmethod
def setUpClass(cls):
cls.isolated_creds = []
if cls.config.compute.allow_tenant_isolation:
creds = cls._get_isolated_creds()
username, tenant_name, password = creds
os = clients.Manager(username=username,
password=password,
tenant_name=tenant_name,
interface=cls._interface)
else:
os = clients.Manager(interface=cls._interface)
cls.os = os
cls.servers_client = os.servers_client
cls.flavors_client = os.flavors_client
cls.images_client = os.images_client
cls.floating_ips_client = os.floating_ips_client
cls.keypairs_client = os.keypairs_client
cls.security_groups_client = os.security_groups_client
cls.quotas_client = os.quotas_client
cls.limits_client = os.limits_client
cls.volumes_client = os.volumes_client
cls.snapshots_client = os.snapshots_client
cls.interfaces_client = os.interfaces_client
cls.fixed_ips_client = os.fixed_ips_client
cls.services_client = os.services_client
cls.hypervisor_client = os.hypervisor_client
cls.build_interval = cls.config.compute.build_interval
cls.build_timeout = cls.config.compute.build_timeout
cls.ssh_user = cls.config.compute.ssh_user
cls.servers = []
cls.servers_client_v3_auth = os.servers_client_v3_auth
@classmethod
def _get_identity_admin_client(cls):
"""
Returns an instance of the Identity Admin API client
"""
os = clients.AdminManager(interface=cls._interface)
admin_client = os.identity_client
return admin_client
@classmethod
def _get_client_args(cls):
return (
cls.config,
cls.config.identity.admin_username,
cls.config.identity.admin_password,
cls.config.identity.uri
)
@classmethod
def _get_isolated_creds(cls):
"""
Creates a new set of user/tenant/password credentials for a
**regular** user of the Compute API so that a test case can
operate in an isolated tenant container.
"""
admin_client = cls._get_identity_admin_client()
password = "pass"
while True:
try:
rand_name_root = rand_name(cls.__name__)
if cls.isolated_creds:
# Main user already created. Create the alt one...
rand_name_root += '-alt'
tenant_name = rand_name_root + "-tenant"
tenant_desc = tenant_name + "-desc"
resp, tenant = admin_client.create_tenant(
name=tenant_name, description=tenant_desc)
break
except exceptions.Duplicate:
if cls.config.compute.allow_tenant_reuse:
tenant = admin_client.get_tenant_by_name(tenant_name)
LOG.info('Re-using existing tenant %s', tenant)
break
while True:
try:
rand_name_root = rand_name(cls.__name__)
if cls.isolated_creds:
# Main user already created. Create the alt one...
rand_name_root += '-alt'
username = rand_name_root + "-user"
email = rand_name_root + "@example.com"
resp, user = admin_client.create_user(username,
password,
tenant['id'],
email)
break
except exceptions.Duplicate:
if cls.config.compute.allow_tenant_reuse:
user = admin_client.get_user_by_username(tenant['id'],
username)
LOG.info('Re-using existing user %s', user)
break
# Store the complete creds (including UUID ids...) for later
# but return just the username, tenant_name, password tuple
# that the various clients will use.
cls.isolated_creds.append((user, tenant))
return username, tenant_name, password
@classmethod
def clear_isolated_creds(cls):
if not cls.isolated_creds:
return
admin_client = cls._get_identity_admin_client()
for user, tenant in cls.isolated_creds:
admin_client.delete_user(user['id'])
admin_client.delete_tenant(tenant['id'])
@classmethod
def clear_servers(cls):
for server in cls.servers:
try:
cls.servers_client.delete_server(server['id'])
except Exception:
pass
for server in cls.servers:
try:
cls.servers_client.wait_for_server_termination(server['id'])
except Exception:
pass
@classmethod
def tearDownClass(cls):
cls.clear_servers()
cls.clear_isolated_creds()
def wait_for(self, condition):
"""Repeatedly calls condition() until a timeout."""
start_time = int(time.time())
while True:
try:
condition()
except Exception:
pass
else:
return
if int(time.time()) - start_time >= self.build_timeout:
condition()
return
time.sleep(self.build_interval)
class BaseComputeAdminTest(BaseComputeTest):
"""Base test case class for all Compute Admin API tests."""
@classmethod
def setUpClass(cls):
super(BaseComputeAdminTest, cls).setUpClass()
admin_username = cls.config.compute_admin.username
admin_password = cls.config.compute_admin.password
admin_tenant = cls.config.compute_admin.tenant_name
if not (admin_username and admin_password and admin_tenant):
msg = ("Missing Compute Admin API credentials "
"in configuration.")
raise cls.skipException(msg)
cls.os_adm = clients.ComputeAdminManager(interface=cls._interface)
class BaseIdentityAdminTest(fuel.test.BaseTestCase):
@classmethod
def setUpClass(cls):
os = clients.AdminManager(interface=cls._interface)
cls.client = os.identity_client
cls.token_client = os.token_client
cls.service_client = os.services_client
if not cls.client.has_admin_extensions():
raise cls.skipException("Admin extensions disabled")
cls.data = DataGenerator(cls.client)
os = clients.Manager(interface=cls._interface)
cls.non_admin_client = os.identity_client
@classmethod
def tearDownClass(cls):
cls.data.teardown_all()
def disable_user(self, user_name):
user = self.get_user_by_name(user_name)
self.client.enable_disable_user(user['id'], False)
def disable_tenant(self, tenant_name):
tenant = self.get_tenant_by_name(tenant_name)
self.client.update_tenant(tenant['id'], enabled=False)
def get_user_by_name(self, name):
_, users = self.client.get_users()
user = [u for u in users if u['name'] == name]
if len(user) > 0:
return user[0]
def get_tenant_by_name(self, name):
_, tenants = self.client.list_tenants()
tenant = [t for t in tenants if t['name'] == name]
if len(tenant) > 0:
return tenant[0]
def get_role_by_name(self, name):
_, roles = self.client.list_roles()
role = [r for r in roles if r['name'] == name]
if len(role) > 0:
return role[0]
class DataGenerator(object):
def __init__(self, client):
self.client = client
self.users = []
self.tenants = []
self.roles = []
self.role_name = None
def setup_test_user(self):
"""Set up a test user."""
self.setup_test_tenant()
self.test_user = rand_name('test_user_')
self.test_password = rand_name('pass_')
self.test_email = self.test_user + '@testmail.tm'
resp, self.user = self.client.create_user(self.test_user,
self.test_password,
self.tenant['id'],
self.test_email)
self.users.append(self.user)
def setup_test_tenant(self):
"""Set up a test tenant."""
self.test_tenant = rand_name('test_tenant_')
self.test_description = rand_name('desc_')
resp, self.tenant = self.client.create_tenant(
name=self.test_tenant,
description=self.test_description)
self.tenants.append(self.tenant)
def setup_test_role(self):
"""Set up a test role."""
self.test_role = rand_name('role')
resp, self.role = self.client.create_role(self.test_role)
self.roles.append(self.role)
def teardown_all(self):
for user in self.users:
self.client.delete_user(user['id'])
for tenant in self.tenants:
self.client.delete_tenant(tenant['id'])
for role in self.roles:
self.client.delete_role(role['id'])
class BaseNetworkTest(fuel.test.BaseTestCase):
@classmethod
def setUpClass(cls):
os = clients.Manager()
cls.network_cfg = os.config.network
if not cls.network_cfg.quantum_available:
raise cls.skipException("Quantum support is required")
cls.client = os.network_client
cls.networks = []
cls.subnets = []
@classmethod
def tearDownClass(cls):
for subnet in cls.subnets:
cls.client.delete_subnet(subnet['id'])
for network in cls.networks:
cls.client.delete_network(network['id'])
@classmethod
def create_network(cls, network_name=None):
"""Wrapper utility that returns a test network."""
network_name = network_name or rand_name('test-network-')
resp, body = cls.client.create_network(network_name)
network = body['network']
cls.networks.append(network)
return network
@classmethod
def create_subnet(cls, network):
"""Wrapper utility that returns a test subnet."""
cidr = netaddr.IPNetwork(cls.network_cfg.tenant_network_cidr)
mask_bits = cls.network_cfg.tenant_network_mask_bits
# Find a cidr that is not in use yet and create a subnet with it
for subnet_cidr in cidr.subnet(mask_bits):
try:
resp, body = cls.client.create_subnet(network['id'],
str(subnet_cidr))
break
except exceptions.BadRequest as e:
is_overlapping_cidr = 'overlaps with another subnet' in str(e)
if not is_overlapping_cidr:
raise
subnet = body['subnet']
cls.subnets.append(subnet)
return subnet

View File

@ -0,0 +1,38 @@
from fuel.sanity import base
from fuel.test import attr
class SanityComputeTest(base.BaseComputeTest):
_interface = 'json'
@attr(type='sanity')
def test_list_instances(self):
resp, body = self.servers_client.list_servers()
self.assertEqual(200, resp.status)
self.assertTrue(u'servers' in body)
@attr(type='sanity')
def test_list_images(self):
resp, body = self.images_client.list_images()
self.assertEqual(200, resp.status)
@attr(type='sanity')
def test_list_volumes(self):
resp, body = self.volumes_client.list_volumes()
self.assertEqual(200, resp.status)
@attr(type='sanity')
def test_list_snapshots(self):
resp, body = self.snapshots_client.list_snapshots()
self.assertEqual(200, resp.status)
@attr(type='sanity')
def test_list_flavors(self):
resp, body = self.flavors_client.list_flavors()
self.assertEqual(200, resp.status)
@attr(type='sanity')
def test_list_rate_limits(self):
resp, body = self.limits_client.get_absolute_limits()
self.assertEqual(200, resp.status)

View File

@ -0,0 +1,20 @@
from fuel.sanity import base
from fuel.test import attr
class ServicesTestJSON(base.BaseIdentityAdminTest):
_interface = 'json'
@attr(type='sanity')
def test_list_services(self):
# List and Verify Services
resp, body = self.client.list_services()
self.assertEqual(200, resp.status)
@attr(type='sanity')
def test_list_users(self):
# List users
resp, body = self.client.get_users()
self.assertEqual(200, resp.status)

View File

@ -0,0 +1,15 @@
from fuel.sanity import base
from fuel.test import attr
class NetworksTest(base.BaseNetworkTest):
@attr(type='sanity')
def test_list_networks(self):
resp, body = self.client.list_networks()
self.assertEqual(200, resp.status)
@attr(type='sanity')
def test_list_ports(self):
resp, body = self.client.list_ports()
self.assertEqual(200, resp.status)

39
fuel/services/__init__.py Normal file
View File

@ -0,0 +1,39 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
"""
Base Service class, which acts as a descriptor for an OpenStack service
in the test environment
"""
class Service(object):
def __init__(self, config):
"""
Initializes the service.
:param config: `tempest.config.Config` object
"""
self.config = config
def get_client(self):
"""
Returns a client object that may be used to query
the service API.
"""
raise NotImplementedError

BIN
fuel/services/__init__.pyc Normal file

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@ -0,0 +1,40 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp
# 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.
import json
from fuel.common.rest_client import RestClient
class FixedIPsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(FixedIPsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def get_fixed_ip_details(self, fixed_ip):
url = "os-fixed-ips/%s" % (fixed_ip)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['fixed_ip']
def reserve_fixed_ip(self, ip, body):
"""This reserves and unreserves fixed ips."""
url = "os-fixed-ips/%s/action" % (ip)
resp, body = self.post(url, json.dumps(body), self.headers)
return resp, body

Binary file not shown.

View File

@ -0,0 +1,134 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
import urllib
from fuel.common.rest_client import RestClient
class FlavorsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(FlavorsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_flavors(self, params=None):
url = 'flavors'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['flavors']
def list_flavors_with_detail(self, params=None):
url = 'flavors/detail'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['flavors']
def get_flavor_details(self, flavor_id):
resp, body = self.get("flavors/%s" % str(flavor_id))
body = json.loads(body)
return resp, body['flavor']
def create_flavor(self, name, ram, vcpus, disk, flavor_id, **kwargs):
"""Creates a new flavor or instance type."""
post_body = {
'name': name,
'ram': ram,
'vcpus': vcpus,
'disk': disk,
'id': flavor_id,
}
if kwargs.get('ephemeral'):
post_body['OS-FLV-EXT-DATA:ephemeral'] = kwargs.get('ephemeral')
if kwargs.get('swap'):
post_body['swap'] = kwargs.get('swap')
if kwargs.get('rxtx'):
post_body['rxtx_factor'] = kwargs.get('rxtx')
if kwargs.get('is_public'):
post_body['os-flavor-access:is_public'] = kwargs.get('is_public')
post_body = json.dumps({'flavor': post_body})
resp, body = self.post('flavors', post_body, self.headers)
body = json.loads(body)
return resp, body['flavor']
def delete_flavor(self, flavor_id):
"""Deletes the given flavor."""
return self.delete("flavors/%s" % str(flavor_id))
def is_resource_deleted(self, id):
#Did not use get_flavor_details(id) for verification as it gives
#200 ok even for deleted id. LP #981263
#we can remove the loop here and use get by ID when bug gets sortedout
resp, flavors = self.list_flavors_with_detail()
for flavor in flavors:
if flavor['id'] == id:
return False
return True
def set_flavor_extra_spec(self, flavor_id, specs):
"""Sets extra Specs to the mentioned flavor."""
post_body = json.dumps({'extra_specs': specs})
resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id,
post_body, self.headers)
body = json.loads(body)
return resp, body['extra_specs']
def get_flavor_extra_spec(self, flavor_id):
"""Gets extra Specs details of the mentioned flavor."""
resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id)
body = json.loads(body)
return resp, body['extra_specs']
def unset_flavor_extra_spec(self, flavor_id, key):
"""Unsets extra Specs from the mentioned flavor."""
return self.delete('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
key))
def add_flavor_access(self, flavor_id, tenant_id):
"""Add flavor access for the specified tenant."""
post_body = {
'addTenantAccess': {
'tenant': tenant_id
}
}
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id,
post_body, self.headers)
body = json.loads(body)
return resp, body['flavor_access']
def remove_flavor_access(self, flavor_id, tenant_id):
"""Remove flavor access from the specified tenant."""
post_body = {
'removeTenantAccess': {
'tenant': tenant_id
}
}
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id,
post_body, self.headers)
body = json.loads(body)
return resp, body['flavor_access']

Binary file not shown.

View File

@ -0,0 +1,96 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
import urllib
from fuel.common.rest_client import RestClient
from fuel import exceptions
class FloatingIPsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(FloatingIPsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_floating_ips(self, params=None):
"""Returns a list of all floating IPs filtered by any parameters."""
url = 'os-floating-ips'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['floating_ips']
def get_floating_ip_details(self, floating_ip_id):
"""Get the details of a floating IP."""
url = "os-floating-ips/%s" % str(floating_ip_id)
resp, body = self.get(url)
body = json.loads(body)
if resp.status == 404:
raise exceptions.NotFound(body)
return resp, body['floating_ip']
def create_floating_ip(self, pool_name=None):
"""Allocate a floating IP to the project."""
url = 'os-floating-ips'
post_body = {'pool': pool_name}
post_body = json.dumps(post_body)
resp, body = self.post(url, post_body, self.headers)
body = json.loads(body)
return resp, body['floating_ip']
def delete_floating_ip(self, floating_ip_id):
"""Deletes the provided floating IP from the project."""
url = "os-floating-ips/%s" % str(floating_ip_id)
resp, body = self.delete(url)
return resp, body
def associate_floating_ip_to_server(self, floating_ip, server_id):
"""Associate the provided floating IP to a specific server."""
url = "servers/%s/action" % str(server_id)
post_body = {
'addFloatingIp': {
'address': floating_ip,
}
}
post_body = json.dumps(post_body)
resp, body = self.post(url, post_body, self.headers)
return resp, body
def disassociate_floating_ip_from_server(self, floating_ip, server_id):
"""Disassociate the provided floating IP from a specific server."""
url = "servers/%s/action" % str(server_id)
post_body = {
'removeFloatingIp': {
'address': floating_ip,
}
}
post_body = json.dumps(post_body)
resp, body = self.post(url, post_body, self.headers)
return resp, body
def is_resource_deleted(self, id):
try:
self.get_floating_ip_details(id)
except exceptions.NotFound:
return True
return False

Binary file not shown.

View File

@ -0,0 +1,19 @@
import json
from fuel.common.rest_client import RestClient
class HostsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(HostsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_hosts(self):
"""Lists all hosts."""
url = 'os-hosts'
resp, body = self.get(url)
body = json.loads(body)
return resp, body['hosts']

Binary file not shown.

View File

@ -0,0 +1,65 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corporation.
# 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.
import json
from fuel.common.rest_client import RestClient
class HypervisorClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(HypervisorClientJSON, self).__init__(config, username,
password, auth_url,
tenant_name)
self.service = self.config.compute.catalog_type
def get_hypervisor_list(self):
"""List hypervisors information."""
resp, body = self.get('os-hypervisors')
body = json.loads(body)
return resp, body['hypervisors']
def get_hypervisor_list_details(self):
"""Show detailed hypervisors information."""
resp, body = self.get('os-hypervisors/detail')
body = json.loads(body)
return resp, body['hypervisors']
def get_hypervisor_show_details(self, hyper_id):
"""Display the details of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s' % hyper_id)
body = json.loads(body)
return resp, body['hypervisor']
def get_hypervisor_servers(self, hyper_name):
"""List instances belonging to the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/servers' % hyper_name)
body = json.loads(body)
return resp, body['hypervisors']
def get_hypervisor_stats(self):
"""Get hypervisor statistics over all compute nodes."""
resp, body = self.get('os-hypervisors/statistics')
body = json.loads(body)
return resp, body['hypervisor_statistics']
def get_hypervisor_uptime(self, hyper_id):
"""Display the uptime of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/uptime' % hyper_id)
body = json.loads(body)
return resp, body['hypervisor']

Binary file not shown.

View File

@ -0,0 +1,159 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
import time
import urllib
from fuel.common.rest_client import RestClient
from fuel import exceptions
class ImagesClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(ImagesClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
self.build_interval = self.config.compute.build_interval
self.build_timeout = self.config.compute.build_timeout
def create_image(self, server_id, name, meta=None):
"""Creates an image of the original server."""
post_body = {
'createImage': {
'name': name,
}
}
if meta is not None:
post_body['createImage']['metadata'] = meta
post_body = json.dumps(post_body)
resp, body = self.post('servers/%s/action' % str(server_id),
post_body, self.headers)
return resp, body
def list_images(self, params=None):
"""Returns a list of all images filtered by any parameters."""
url = 'images'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['images']
def list_images_with_detail(self, params=None):
"""Returns a detailed list of images filtered by any parameters."""
url = 'images/detail'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['images']
def get_image(self, image_id):
"""Returns the details of a single image."""
resp, body = self.get("images/%s" % str(image_id))
body = json.loads(body)
return resp, body['image']
def delete_image(self, image_id):
"""Deletes the provided image."""
return self.delete("images/%s" % str(image_id))
def wait_for_image_resp_code(self, image_id, code):
"""
Waits until the HTTP response code for the request matches the
expected value
"""
resp, body = self.get("images/%s" % str(image_id))
start = int(time.time())
while resp.status != code:
time.sleep(self.build_interval)
resp, body = self.get("images/%s" % str(image_id))
if int(time.time()) - start >= self.build_timeout:
raise exceptions.TimeoutException
def wait_for_image_status(self, image_id, status):
"""Waits for an image to reach a given status."""
resp, image = self.get_image(image_id)
start = int(time.time())
while image['status'] != status:
time.sleep(self.build_interval)
resp, image = self.get_image(image_id)
if image['status'] == 'ERROR':
raise exceptions.AddImageException(image_id=image_id)
if int(time.time()) - start >= self.build_timeout:
raise exceptions.TimeoutException
def list_image_metadata(self, image_id):
"""Lists all metadata items for an image."""
resp, body = self.get("images/%s/metadata" % str(image_id))
body = json.loads(body)
return resp, body['metadata']
def set_image_metadata(self, image_id, meta):
"""Sets the metadata for an image."""
post_body = json.dumps({'metadata': meta})
resp, body = self.put('images/%s/metadata' % str(image_id),
post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def update_image_metadata(self, image_id, meta):
"""Updates the metadata for an image."""
post_body = json.dumps({'metadata': meta})
resp, body = self.post('images/%s/metadata' % str(image_id),
post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def get_image_metadata_item(self, image_id, key):
"""Returns the value for a specific image metadata key."""
resp, body = self.get("images/%s/metadata/%s" % (str(image_id), key))
body = json.loads(body)
return resp, body['meta']
def set_image_metadata_item(self, image_id, key, meta):
"""Sets the value for a specific image metadata key."""
post_body = json.dumps({'meta': meta})
resp, body = self.put('images/%s/metadata/%s' % (str(image_id), key),
post_body, self.headers)
body = json.loads(body)
return resp, body['meta']
def delete_image_metadata_item(self, image_id, key):
"""Deletes a single image metadata key/value pair."""
resp, body = self.delete("images/%s/metadata/%s" %
(str(image_id), key))
return resp, body
def is_resource_deleted(self, id):
try:
self.get_image(id)
except exceptions.NotFound:
return True
return False

Binary file not shown.

View File

@ -0,0 +1,80 @@
# Copyright 2013 IBM Corp.
# 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.
import json
import time
from fuel.common.rest_client import RestClient
from fuel import exceptions
class InterfacesClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(InterfacesClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_interfaces(self, server):
resp, body = self.get('servers/%s/os-interface' % server)
body = json.loads(body)
return resp, body['interfaceAttachments']
def create_interface(self, server, port_id=None, network_id=None,
fixed_ip=None):
post_body = dict(interfaceAttachment=dict())
if port_id:
post_body['port_id'] = port_id
if network_id:
post_body['net_id'] = network_id
if fixed_ip:
post_body['fixed_ips'] = [dict(ip_address=fixed_ip)]
post_body = json.dumps(post_body)
resp, body = self.post('servers/%s/os-interface' % server,
headers=self.headers,
body=post_body)
body = json.loads(body)
return resp, body['interfaceAttachment']
def show_interface(self, server, port_id):
resp, body = self.get('servers/%s/os-interface/%s' % (server, port_id))
body = json.loads(body)
return resp, body['interfaceAttachment']
def delete_interface(self, server, port_id):
resp, body = self.delete('servers/%s/os-interface/%s' % (server,
port_id))
return resp, body
def wait_for_interface_status(self, server, port_id, status):
"""Waits for a interface to reach a given status."""
resp, body = self.show_interface(server, port_id)
interface_status = body['port_state']
start = int(time.time())
while(interface_status != status):
time.sleep(self.build_interval)
resp, body = self.show_interface(server, port_id)
interface_status = body['port_state']
timed_out = int(time.time()) - start >= self.build_timeout
if interface_status != status and timed_out:
message = ('Interface %s failed to reach %s status within '
'the required time (%s s).' %
(port_id, status, self.build_timeout))
raise exceptions.TimeoutException(message)
return resp, body

Binary file not shown.

View File

@ -0,0 +1,56 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
from fuel.common.rest_client import RestClient
class KeyPairsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(KeyPairsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_keypairs(self):
resp, body = self.get("os-keypairs")
body = json.loads(body)
#Each returned keypair is embedded within an unnecessary 'keypair'
#element which is a deviation from other resources like floating-ips,
#servers, etc. A bug?
#For now we shall adhere to the spec, but the spec for keypairs
#is yet to be found
return resp, body['keypairs']
def get_keypair(self, key_name):
resp, body = self.get("os-keypairs/%s" % str(key_name))
body = json.loads(body)
return resp, body['keypair']
def create_keypair(self, name, pub_key=None):
post_body = {'keypair': {'name': name}}
if pub_key:
post_body['keypair']['public_key'] = pub_key
post_body = json.dumps(post_body)
resp, body = self.post("os-keypairs",
headers=self.headers, body=post_body)
body = json.loads(body)
return resp, body['keypair']
def delete_keypair(self, key_name):
return self.delete("os-keypairs/%s" % str(key_name))

Binary file not shown.

View File

@ -0,0 +1,40 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
from fuel.common.rest_client import RestClient
class LimitsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(LimitsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def get_absolute_limits(self):
resp, body = self.get("limits")
body = json.loads(body)
return resp, body['limits']['absolute']
def get_specific_absolute_limit(self, absolute_limit):
resp, body = self.get("limits")
body = json.loads(body)
if absolute_limit not in body['limits']['absolute']:
return None
else:
return body['limits']['absolute'][absolute_limit]

Binary file not shown.

View File

@ -0,0 +1,103 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012 NTT Data
# 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.
import json
from fuel.common.rest_client import RestClient
class QuotasClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(QuotasClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def get_quota_set(self, tenant_id):
"""List the quota set for a tenant."""
url = 'os-quota-sets/%s' % str(tenant_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['quota_set']
def get_default_quota_set(self, tenant_id):
"""List the default quota set for a tenant."""
url = 'os-quota-sets/%s/defaults' % str(tenant_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['quota_set']
def update_quota_set(self, tenant_id, force=None,
injected_file_content_bytes=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, injected_files=None,
cores=None, injected_file_path_bytes=None,
security_groups=None):
"""
Updates the tenant's quota limits for one or more resources
"""
post_body = {}
if force is not None:
post_body['force'] = force
if injected_file_content_bytes is not None:
post_body['injected_file_content_bytes'] = \
injected_file_content_bytes
if metadata_items is not None:
post_body['metadata_items'] = metadata_items
if ram is not None:
post_body['ram'] = ram
if floating_ips is not None:
post_body['floating_ips'] = floating_ips
if fixed_ips is not None:
post_body['fixed_ips'] = fixed_ips
if key_pairs is not None:
post_body['key_pairs'] = key_pairs
if instances is not None:
post_body['instances'] = instances
if security_group_rules is not None:
post_body['security_group_rules'] = security_group_rules
if injected_files is not None:
post_body['injected_files'] = injected_files
if cores is not None:
post_body['cores'] = cores
if injected_file_path_bytes is not None:
post_body['injected_file_path_bytes'] = injected_file_path_bytes
if security_groups is not None:
post_body['security_groups'] = security_groups
post_body = json.dumps({'quota_set': post_body})
resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body,
self.headers)
body = json.loads(body)
return resp, body['quota_set']

Binary file not shown.

View File

@ -0,0 +1,107 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
import urllib
from fuel.common.rest_client import RestClient
from fuel import exceptions
class SecurityGroupsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(SecurityGroupsClientJSON, self).__init__(config, username,
password, auth_url,
tenant_name)
self.service = self.config.compute.catalog_type
def list_security_groups(self, params=None):
"""List all security groups for a user."""
url = 'os-security-groups'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['security_groups']
def get_security_group(self, security_group_id):
"""Get the details of a Security Group."""
url = "os-security-groups/%s" % str(security_group_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['security_group']
def create_security_group(self, name, description):
"""
Creates a new security group.
name (Required): Name of security group.
description (Required): Description of security group.
"""
post_body = {
'name': name,
'description': description,
}
post_body = json.dumps({'security_group': post_body})
resp, body = self.post('os-security-groups', post_body, self.headers)
body = json.loads(body)
return resp, body['security_group']
def delete_security_group(self, security_group_id):
"""Deletes the provided Security Group."""
return self.delete('os-security-groups/%s' % str(security_group_id))
def create_security_group_rule(self, parent_group_id, ip_proto, from_port,
to_port, **kwargs):
"""
Creating a new security group rules.
parent_group_id :ID of Security group
ip_protocol : ip_proto (icmp, tcp, udp).
from_port: Port at start of range.
to_port : Port at end of range.
Following optional keyword arguments are accepted:
cidr : CIDR for address range.
group_id : ID of the Source group
"""
post_body = {
'parent_group_id': parent_group_id,
'ip_protocol': ip_proto,
'from_port': from_port,
'to_port': to_port,
'cidr': kwargs.get('cidr'),
'group_id': kwargs.get('group_id'),
}
post_body = json.dumps({'security_group_rule': post_body})
url = 'os-security-group-rules'
resp, body = self.post(url, post_body, self.headers)
body = json.loads(body)
return resp, body['security_group_rule']
def delete_security_group_rule(self, group_rule_id):
"""Deletes the provided Security Group rule."""
return self.delete('os-security-group-rules/%s' % str(group_rule_id))
def list_security_group_rules(self, security_group_id):
"""List all rules for a security group."""
resp, body = self.get('os-security-groups')
body = json.loads(body)
for sg in body['security_groups']:
if sg['id'] == security_group_id:
return resp, sg['rules']
raise exceptions.NotFound('No such Security Group')

Binary file not shown.

View File

@ -0,0 +1,408 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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.
import json
import time
import urllib
from fuel.common.rest_client import RestClient
from fuel import exceptions
class ServersClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None,
auth_version='v2'):
super(ServersClientJSON, self).__init__(config, username, password,
auth_url, tenant_name,
auth_version=auth_version)
self.service = self.config.compute.catalog_type
def create_server(self, name, image_ref, flavor_ref, **kwargs):
"""
Creates an instance of a server.
name (Required): The name of the server.
image_ref (Required): Reference to the image used to build the server.
flavor_ref (Required): The flavor used to build the server.
Following optional keyword arguments are accepted:
adminPass: Sets the initial root password.
key_name: Key name of keypair that was created earlier.
meta: A dictionary of values to be used as metadata.
personality: A list of dictionaries for files to be injected into
the server.
security_groups: A list of security group dicts.
networks: A list of network dicts with UUID and fixed_ip.
user_data: User data for instance.
availability_zone: Availability zone in which to launch instance.
accessIPv4: The IPv4 access address for the server.
accessIPv6: The IPv6 access address for the server.
min_count: Count of minimum number of instances to launch.
max_count: Count of maximum number of instances to launch.
disk_config: Determines if user or admin controls disk configuration.
return_reservation_id: Enable/Disable the return of reservation id
"""
post_body = {
'name': name,
'imageRef': image_ref,
'flavorRef': flavor_ref
}
for option in ['personality', 'adminPass', 'key_name',
'security_groups', 'networks', 'user_data',
'availability_zone', 'accessIPv4', 'accessIPv6',
'min_count', 'max_count', ('metadata', 'meta'),
('OS-DCF:diskConfig', 'disk_config'),
'return_reservation_id']:
if isinstance(option, tuple):
post_param = option[0]
key = option[1]
else:
post_param = option
key = option
value = kwargs.get(key)
if value is not None:
post_body[post_param] = value
post_body = json.dumps({'server': post_body})
resp, body = self.post('servers', post_body, self.headers)
body = json.loads(body)
# NOTE(maurosr): this deals with the case of multiple server create
# with return reservation id set True
if 'reservation_id' in body:
return resp, body
return resp, body['server']
def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
accessIPv6=None):
"""
Updates the properties of an existing server.
server_id: The id of an existing server.
name: The name of the server.
personality: A list of files to be injected into the server.
accessIPv4: The IPv4 access address for the server.
accessIPv6: The IPv6 access address for the server.
"""
post_body = {}
if meta is not None:
post_body['metadata'] = meta
if name is not None:
post_body['name'] = name
if accessIPv4 is not None:
post_body['accessIPv4'] = accessIPv4
if accessIPv6 is not None:
post_body['accessIPv6'] = accessIPv6
post_body = json.dumps({'server': post_body})
resp, body = self.put("servers/%s" % str(server_id),
post_body, self.headers)
body = json.loads(body)
return resp, body['server']
def get_server(self, server_id):
"""Returns the details of an existing server."""
resp, body = self.get("servers/%s" % str(server_id))
body = json.loads(body)
return resp, body['server']
def delete_server(self, server_id):
"""Deletes the given server."""
return self.delete("servers/%s" % str(server_id))
def list_servers(self, params=None):
"""Lists all servers for a user."""
url = 'servers'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body
def list_servers_with_detail(self, params=None):
"""Lists all servers in detail for a user."""
url = 'servers/detail'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body
def wait_for_server_status(self, server_id, status):
"""Waits for a server to reach a given status."""
resp, body = self.get_server(server_id)
server_status = body['status']
start = int(time.time())
while(server_status != status):
time.sleep(self.build_interval)
resp, body = self.get_server(server_id)
server_status = body['status']
if server_status == 'ERROR':
raise exceptions.BuildErrorException(server_id=server_id)
timed_out = int(time.time()) - start >= self.build_timeout
if server_status != status and timed_out:
message = ('Server %s failed to reach %s status within the '
'required time (%s s).' %
(server_id, status, self.build_timeout))
message += ' Current status: %s.' % server_status
raise exceptions.TimeoutException(message)
def wait_for_server_termination(self, server_id, ignore_error=False):
"""Waits for server to reach termination."""
start_time = int(time.time())
while True:
try:
resp, body = self.get_server(server_id)
except exceptions.NotFound:
return
server_status = body['status']
if server_status == 'ERROR' and not ignore_error:
raise exceptions.BuildErrorException(server_id=server_id)
if int(time.time()) - start_time >= self.build_timeout:
raise exceptions.TimeoutException
time.sleep(self.build_interval)
def list_addresses(self, server_id):
"""Lists all addresses for a server."""
resp, body = self.get("servers/%s/ips" % str(server_id))
body = json.loads(body)
return resp, body['addresses']
def list_addresses_by_network(self, server_id, network_id):
"""Lists all addresses of a specific network type for a server."""
resp, body = self.get("servers/%s/ips/%s" %
(str(server_id), network_id))
body = json.loads(body)
return resp, body
def action(self, server_id, action_name, response_key, **kwargs):
post_body = json.dumps({action_name: kwargs})
resp, body = self.post('servers/%s/action' % str(server_id),
post_body, self.headers)
if response_key is not None:
body = json.loads(body)[response_key]
return resp, body
def change_password(self, server_id, adminPass):
"""Changes the root password for the server."""
return self.action(server_id, 'changePassword', None,
adminPass=adminPass)
def reboot(self, server_id, reboot_type):
"""Reboots a server."""
return self.action(server_id, 'reboot', None, type=reboot_type)
def rebuild(self, server_id, image_ref, **kwargs):
"""Rebuilds a server with a new image."""
kwargs['imageRef'] = image_ref
if 'disk_config' in kwargs:
kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
del kwargs['disk_config']
return self.action(server_id, 'rebuild', 'server', **kwargs)
def resize(self, server_id, flavor_ref, **kwargs):
"""Changes the flavor of a server."""
kwargs['flavorRef'] = flavor_ref
if 'disk_config' in kwargs:
kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
del kwargs['disk_config']
return self.action(server_id, 'resize', None, **kwargs)
def confirm_resize(self, server_id, **kwargs):
"""Confirms the flavor change for a server."""
return self.action(server_id, 'confirmResize', None, **kwargs)
def revert_resize(self, server_id, **kwargs):
"""Reverts a server back to its original flavor."""
return self.action(server_id, 'revertResize', None, **kwargs)
def create_image(self, server_id, name):
"""Creates an image of the given server."""
return self.action(server_id, 'createImage', None, name=name)
def list_server_metadata(self, server_id):
resp, body = self.get("servers/%s/metadata" % str(server_id))
body = json.loads(body)
return resp, body['metadata']
def set_server_metadata(self, server_id, meta):
post_body = json.dumps({'metadata': meta})
resp, body = self.put('servers/%s/metadata' % str(server_id),
post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def update_server_metadata(self, server_id, meta):
post_body = json.dumps({'metadata': meta})
resp, body = self.post('servers/%s/metadata' % str(server_id),
post_body, self.headers)
body = json.loads(body)
return resp, body['metadata']
def get_server_metadata_item(self, server_id, key):
resp, body = self.get("servers/%s/metadata/%s" % (str(server_id), key))
body = json.loads(body)
return resp, body['meta']
def set_server_metadata_item(self, server_id, key, meta):
post_body = json.dumps({'meta': meta})
resp, body = self.put('servers/%s/metadata/%s' % (str(server_id), key),
post_body, self.headers)
body = json.loads(body)
return resp, body['meta']
def delete_server_metadata_item(self, server_id, key):
resp, body = self.delete("servers/%s/metadata/%s" %
(str(server_id), key))
return resp, body
def stop(self, server_id, **kwargs):
return self.action(server_id, 'os-stop', None, **kwargs)
def start(self, server_id, **kwargs):
return self.action(server_id, 'os-start', None, **kwargs)
def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
"""Attaches a volume to a server instance."""
post_body = json.dumps({
'volumeAttachment': {
'volumeId': volume_id,
'device': device,
}
})
resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
post_body, self.headers)
return resp, body
def detach_volume(self, server_id, volume_id):
"""Detaches a volume from a server instance."""
resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
(server_id, volume_id))
return resp, body
def add_security_group(self, server_id, name):
"""Adds a security group to the server."""
return self.action(server_id, 'addSecurityGroup', None, name=name)
def remove_security_group(self, server_id, name):
"""Removes a security group from the server."""
return self.action(server_id, 'removeSecurityGroup', None, name=name)
def live_migrate_server(self, server_id, dest_host, use_block_migration):
"""This should be called with administrator privileges ."""
migrate_params = {
"disk_over_commit": False,
"block_migration": use_block_migration,
"host": dest_host
}
req_body = json.dumps({'os-migrateLive': migrate_params})
resp, body = self.post("servers/%s/action" % str(server_id),
req_body, self.headers)
return resp, body
def list_servers_for_all_tenants(self):
url = self.base_url + '/servers?all_tenants=1'
resp = self.requests.get(url)
resp, body = self.get('servers', self.headers)
body = json.loads(body)
return resp, body['servers']
def migrate_server(self, server_id, **kwargs):
"""Migrates a server to a new host."""
return self.action(server_id, 'migrate', None, **kwargs)
def lock_server(self, server_id, **kwargs):
"""Locks the given server."""
return self.action(server_id, 'lock', None, **kwargs)
def unlock_server(self, server_id, **kwargs):
"""UNlocks the given server."""
return self.action(server_id, 'unlock', None, **kwargs)
def suspend_server(self, server_id, **kwargs):
"""Suspends the provded server."""
return self.action(server_id, 'suspend', None, **kwargs)
def resume_server(self, server_id, **kwargs):
"""Un-suspends the provded server."""
return self.action(server_id, 'resume', None, **kwargs)
def pause_server(self, server_id, **kwargs):
"""Pauses the provded server."""
return self.action(server_id, 'pause', None, **kwargs)
def unpause_server(self, server_id, **kwargs):
"""Un-pauses the provded server."""
return self.action(server_id, 'unpause', None, **kwargs)
def reset_state(self, server_id, state='error'):
"""Resets the state of a server to active/error."""
return self.action(server_id, 'os-resetState', None, state=state)
def get_console_output(self, server_id, length):
return self.action(server_id, 'os-getConsoleOutput', 'output',
length=length)
def list_virtual_interfaces(self, server_id):
"""
List the virtual interfaces used in an instance.
"""
resp, body = self.get('/'.join(['servers', server_id,
'os-virtual-interfaces']))
return resp, json.loads(body)
def rescue_server(self, server_id, adminPass=None):
"""Rescue the provided server."""
return self.action(server_id, 'rescue', None, adminPass=adminPass)
def unrescue_server(self, server_id):
"""Unrescue the provided server."""
return self.action(server_id, 'unrescue', None)
def list_instance_actions(self, server_id):
"""List the provided server action."""
resp, body = self.get("servers/%s/os-instance-actions" %
str(server_id))
body = json.loads(body)
return resp, body['instanceActions']
def get_instance_action(self, server_id, request_id):
"""Returns the action details of the provided server."""
resp, body = self.get("servers/%s/os-instance-actions/%s" %
(str(server_id), str(request_id)))
body = json.loads(body)
return resp, body['instanceAction']

Binary file not shown.

View File

@ -0,0 +1,33 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 NEC Corporation
# 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.
import json
from fuel.common.rest_client import RestClient
class ServicesClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(ServicesClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_services(self):
resp, body = self.get("os-services")
body = json.loads(body)
return resp, body['services']

Binary file not shown.

View File

@ -0,0 +1,47 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 NEC Corporation
# 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.
import json
import urllib
from fuel.common.rest_client import RestClient
class TenantUsagesClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(TenantUsagesClientJSON, self).__init__(
config, username, password, auth_url, tenant_name)
self.service = self.config.compute.catalog_type
def list_tenant_usages(self, params=None):
url = 'os-simple-tenant-usage'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['tenant_usages'][0]
def get_tenant_usage(self, tenant_id, params=None):
url = 'os-simple-tenant-usage/%s' % tenant_id
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['tenant_usage']

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@ -0,0 +1,270 @@
import httplib2
import json
from fuel.common.rest_client import RestClient
from fuel import exceptions
class IdentityClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(IdentityClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.identity.catalog_type
self.endpoint_url = 'adminURL'
def has_admin_extensions(self):
"""
Returns True if the KSADM Admin Extensions are supported
False otherwise
"""
if hasattr(self, '_has_admin_extensions'):
return self._has_admin_extensions
resp, body = self.list_roles()
self._has_admin_extensions = ('status' in resp and resp.status != 503)
return self._has_admin_extensions
def create_role(self, name):
"""Create a role."""
post_body = {
'name': name,
}
post_body = json.dumps({'role': post_body})
resp, body = self.post('OS-KSADM/roles', post_body, self.headers)
body = json.loads(body)
return resp, body['role']
def create_tenant(self, name, **kwargs):
"""
Create a tenant
name (required): New tenant name
description: Description of new tenant (default is none)
enabled <true|false>: Initial tenant status (default is true)
"""
post_body = {
'name': name,
'description': kwargs.get('description', ''),
'enabled': kwargs.get('enabled', True),
}
post_body = json.dumps({'tenant': post_body})
resp, body = self.post('tenants', post_body, self.headers)
body = json.loads(body)
return resp, body['tenant']
def delete_role(self, role_id):
"""Delete a role."""
resp, body = self.delete('OS-KSADM/roles/%s' % str(role_id))
return resp, body
def list_user_roles(self, tenant_id, user_id):
"""Returns a list of roles assigned to a user for a tenant."""
url = '/tenants/%s/users/%s/roles' % (tenant_id, user_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['roles']
def assign_user_role(self, tenant_id, user_id, role_id):
"""Add roles to a user on a tenant."""
post_body = json.dumps({})
resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
(tenant_id, user_id, role_id), post_body,
self.headers)
body = json.loads(body)
return resp, body['role']
def remove_user_role(self, tenant_id, user_id, role_id):
"""Removes a role assignment for a user on a tenant."""
return self.delete('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
(tenant_id, user_id, role_id))
def delete_tenant(self, tenant_id):
"""Delete a tenant."""
resp, body = self.delete('tenants/%s' % str(tenant_id))
return resp, body
def get_tenant(self, tenant_id):
"""Get tenant details."""
resp, body = self.get('tenants/%s' % str(tenant_id))
body = json.loads(body)
return resp, body['tenant']
def list_roles(self):
"""Returns roles."""
resp, body = self.get('OS-KSADM/roles')
body = json.loads(body)
return resp, body['roles']
def list_tenants(self):
"""Returns tenants."""
resp, body = self.get('tenants')
body = json.loads(body)
return resp, body['tenants']
def get_tenant_by_name(self, tenant_name):
resp, tenants = self.list_tenants()
for tenant in tenants:
if tenant['name'] == tenant_name:
return tenant
raise exceptions.NotFound('No such tenant')
def update_tenant(self, tenant_id, **kwargs):
"""Updates a tenant."""
resp, body = self.get_tenant(tenant_id)
name = kwargs.get('name', body['name'])
desc = kwargs.get('description', body['description'])
en = kwargs.get('enabled', body['enabled'])
post_body = {
'id': tenant_id,
'name': name,
'description': desc,
'enabled': en,
}
post_body = json.dumps({'tenant': post_body})
resp, body = self.post('tenants/%s' % tenant_id, post_body,
self.headers)
body = json.loads(body)
return resp, body['tenant']
def create_user(self, name, password, tenant_id, email):
"""Create a user."""
post_body = {
'name': name,
'password': password,
'tenantId': tenant_id,
'email': email
}
post_body = json.dumps({'user': post_body})
resp, body = self.post('users', post_body, self.headers)
body = json.loads(body)
return resp, body['user']
def get_user(self, user_id):
"""GET a user."""
resp, body = self.get("users/%s" % user_id)
body = json.loads(body)
return resp, body['user']
def delete_user(self, user_id):
"""Delete a user."""
resp, body = self.delete("users/%s" % user_id)
return resp, body
def get_users(self):
"""Get the list of users."""
resp, body = self.get("users")
body = json.loads(body)
return resp, body['users']
def enable_disable_user(self, user_id, enabled):
"""Enables or disables a user."""
put_body = {
'enabled': enabled
}
put_body = json.dumps({'user': put_body})
resp, body = self.put('users/%s/enabled' % user_id,
put_body, self.headers)
body = json.loads(body)
return resp, body
def delete_token(self, token_id):
"""Delete a token."""
resp, body = self.delete("tokens/%s" % token_id)
return resp, body
def list_users_for_tenant(self, tenant_id):
"""List users for a Tenant."""
resp, body = self.get('/tenants/%s/users' % tenant_id)
body = json.loads(body)
return resp, body['users']
def get_user_by_username(self, tenant_id, username):
resp, users = self.list_users_for_tenant(tenant_id)
for user in users:
if user['name'] == username:
return user
raise exceptions.NotFound('No such user')
def create_service(self, name, type, **kwargs):
"""Create a service."""
post_body = {
'name': name,
'type': type,
'description': kwargs.get('description')
}
post_body = json.dumps({'OS-KSADM:service': post_body})
resp, body = self.post('/OS-KSADM/services', post_body, self.headers)
body = json.loads(body)
return resp, body['OS-KSADM:service']
def get_service(self, service_id):
"""Get Service."""
url = '/OS-KSADM/services/%s' % service_id
resp, body = self.get(url)
body = json.loads(body)
return resp, body['OS-KSADM:service']
def list_services(self):
"""List Service - Returns Services."""
resp, body = self.get('/OS-KSADM/services/')
body = json.loads(body)
return resp, body['OS-KSADM:services']
def delete_service(self, service_id):
"""Delete Service."""
url = '/OS-KSADM/services/%s' % service_id
return self.delete(url)
class TokenClientJSON(RestClient):
def __init__(self, config):
auth_url = config.identity.uri
# TODO(jaypipes) Why is this all repeated code in here?
# Normalize URI to ensure /tokens is in it.
if 'tokens' not in auth_url:
auth_url = auth_url.rstrip('/') + '/tokens'
self.auth_url = auth_url
self.config = config
def auth(self, user, password, tenant):
creds = {
'auth': {
'passwordCredentials': {
'username': user,
'password': password,
},
'tenantName': tenant,
}
}
headers = {'Content-Type': 'application/json'}
body = json.dumps(creds)
resp, body = self.post(self.auth_url, headers=headers, body=body)
return resp, body
def request(self, method, url, headers=None, body=None):
"""A simple HTTP request interface."""
dscv = self.config.identity.disable_ssl_certificate_validation
self.http_obj = httplib2.Http(disable_ssl_certificate_validation=dscv)
if headers is None:
headers = {}
self._log_request(method, url, headers, body)
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_response(resp, resp_body)
if resp.status in (401, 403):
resp_body = json.loads(resp_body)
raise exceptions.Unauthorized(resp_body['error']['message'])
return resp, resp_body
def get_token(self, user, password, tenant):
resp, body = self.auth(user, password, tenant)
if resp['status'] != '202':
body = json.loads(body)
access = body['access']
token = access['token']
return token['id']

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@ -0,0 +1,115 @@
import json
from fuel.common.rest_client import RestClient
class NetworkClient(RestClient):
"""
REST client for Quantum. Uses v2 of the Quantum API, since the
V1 API has been removed from the code base.
Implements the following operations for each one of the basic Quantum
abstractions (networks, sub-networks and ports):
create
delete
list
show
"""
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(NetworkClient, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.network.catalog_type
self.version = '2.0'
self.uri_prefix = "v%s" % (self.version)
def list_networks(self):
uri = '%s/networks' % (self.uri_prefix)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body
def create_network(self, name):
post_body = {
'network': {
'name': name,
}
}
body = json.dumps(post_body)
uri = '%s/networks' % (self.uri_prefix)
resp, body = self.post(uri, headers=self.headers, body=body)
body = json.loads(body)
return resp, body
def show_network(self, uuid):
uri = '%s/networks/%s' % (self.uri_prefix, uuid)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body
def delete_network(self, uuid):
uri = '%s/networks/%s' % (self.uri_prefix, uuid)
resp, body = self.delete(uri, self.headers)
return resp, body
def create_subnet(self, net_uuid, cidr):
post_body = dict(
subnet=dict(
ip_version=4,
network_id=net_uuid,
cidr=cidr),)
body = json.dumps(post_body)
uri = '%s/subnets' % (self.uri_prefix)
resp, body = self.post(uri, headers=self.headers, body=body)
body = json.loads(body)
return resp, body
def delete_subnet(self, uuid):
uri = '%s/subnets/%s' % (self.uri_prefix, uuid)
resp, body = self.delete(uri, self.headers)
return resp, body
def list_subnets(self):
uri = '%s/subnets' % (self.uri_prefix)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body
def show_subnet(self, uuid):
uri = '%s/subnets/%s' % (self.uri_prefix, uuid)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body
def create_port(self, network_id, state=None):
if not state:
state = True
post_body = {
'port': {
'network_id': network_id,
'admin_state_up': state,
}
}
body = json.dumps(post_body)
uri = '%s/ports' % (self.uri_prefix)
resp, body = self.post(uri, headers=self.headers, body=body)
body = json.loads(body)
return resp, body
def delete_port(self, port_id):
uri = '%s/ports/%s' % (self.uri_prefix, port_id)
resp, body = self.delete(uri, self.headers)
return resp, body
def list_ports(self):
uri = '%s/ports' % (self.uri_prefix)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body
def show_port(self, port_id):
uri = '%s/ports/%s' % (self.uri_prefix, port_id)
resp, body = self.get(uri, self.headers)
body = json.loads(body)
return resp, body

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,124 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
import urllib
from fuel.common.rest_client import RestClient
class VolumeTypesClientJSON(RestClient):
"""
Client class to send CRUD Volume Types API requests to a Cinder endpoint
"""
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(VolumeTypesClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.volume.catalog_type
self.build_interval = self.config.volume.build_interval
self.build_timeout = self.config.volume.build_timeout
def list_volume_types(self, params=None):
"""List all the volume_types created."""
url = 'types'
if params is not None:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volume_types']
def get_volume_type(self, volume_id):
"""Returns the details of a single volume_type."""
url = "types/%s" % str(volume_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volume_type']
def create_volume_type(self, name, **kwargs):
"""
Creates a new Volume_type.
name(Required): Name of volume_type.
Following optional keyword arguments are accepted:
extra_specs: A dictionary of values to be used as extra_specs.
"""
post_body = {
'name': name,
'extra_specs': kwargs.get('extra_specs'),
}
post_body = json.dumps({'volume_type': post_body})
resp, body = self.post('types', post_body, self.headers)
body = json.loads(body)
return resp, body['volume_type']
def delete_volume_type(self, volume_id):
"""Deletes the Specified Volume_type."""
return self.delete("types/%s" % str(volume_id))
def list_volume_types_extra_specs(self, vol_type_id, params=None):
"""List all the volume_types extra specs created."""
url = 'types/%s/extra_specs' % str(vol_type_id)
if params is not None:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['extra_specs']
def get_volume_type_extra_specs(self, vol_type_id, extra_spec_name):
"""Returns the details of a single volume_type extra spec."""
url = "types/%s/extra_specs/%s" % (str(vol_type_id),
str(extra_spec_name))
resp, body = self.get(url)
body = json.loads(body)
return resp, body
def create_volume_type_extra_specs(self, vol_type_id, extra_spec):
"""
Creates a new Volume_type extra spec.
vol_type_id: Id of volume_type.
extra_specs: A dictionary of values to be used as extra_specs.
"""
url = "types/%s/extra_specs" % str(vol_type_id)
post_body = json.dumps({'extra_specs': extra_spec})
resp, body = self.post(url, post_body, self.headers)
body = json.loads(body)
return resp, body['extra_specs']
def delete_volume_type_extra_specs(self, vol_id, extra_spec_name):
"""Deletes the Specified Volume_type extra spec."""
return self.delete("types/%s/extra_specs/%s" % ((str(vol_id)),
str(extra_spec_name)))
def update_volume_type_extra_specs(self, vol_type_id, extra_spec_name,
extra_spec):
"""
Update a volume_type extra spec.
vol_type_id: Id of volume_type.
extra_spec_name: Name of the extra spec to be updated.
extra_spec: A dictionary of with key as extra_spec_name and the
updated value.
"""
url = "types/%s/extra_specs/%s" % (str(vol_type_id),
str(extra_spec_name))
put_body = json.dumps(extra_spec)
resp, body = self.put(url, put_body, self.headers)
body = json.loads(body)
return resp, body

View File

@ -0,0 +1,125 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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.
import json
import time
import urllib
from fuel.common import log as logging
from fuel.common.rest_client import RestClient
from fuel import exceptions
LOG = logging.getLogger(__name__)
class SnapshotsClientJSON(RestClient):
"""Client class to send CRUD Volume API requests."""
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(SnapshotsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.volume.catalog_type
self.build_interval = self.config.volume.build_interval
self.build_timeout = self.config.volume.build_timeout
def list_snapshots(self, params=None):
"""List all the snapshot."""
url = 'snapshots'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['snapshots']
def list_snapshot_with_detail(self, params=None):
"""List the details of all snapshots."""
url = 'snapshots/detail'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['snapshots']
def get_snapshot(self, snapshot_id):
"""Returns the details of a single snapshot."""
url = "snapshots/%s" % str(snapshot_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['snapshot']
def create_snapshot(self, volume_id, **kwargs):
"""
Creates a new snapshot.
volume_id(Required): id of the volume.
force: Create a snapshot even if the volume attached (Default=False)
display_name: Optional snapshot Name.
display_description: User friendly snapshot description.
"""
post_body = {'volume_id': volume_id}
post_body.update(kwargs)
post_body = json.dumps({'snapshot': post_body})
resp, body = self.post('snapshots', post_body, self.headers)
body = json.loads(body)
return resp, body['snapshot']
#NOTE(afazekas): just for the wait function
def _get_snapshot_status(self, snapshot_id):
resp, body = self.get_snapshot(snapshot_id)
status = body['status']
#NOTE(afazekas): snapshot can reach an "error"
# state in a "normal" lifecycle
if (status == 'error'):
raise exceptions.SnapshotBuildErrorException(
snapshot_id=snapshot_id)
return status
#NOTE(afazkas): Wait reinvented again. It is not in the correct layer
def wait_for_snapshot_status(self, snapshot_id, status):
"""Waits for a Snapshot to reach a given status."""
start_time = time.time()
old_value = value = self._get_snapshot_status(snapshot_id)
while True:
dtime = time.time() - start_time
time.sleep(self.build_interval)
if value != old_value:
LOG.info('Value transition from "%s" to "%s"'
'in %d second(s).', old_value,
value, dtime)
if (value == status):
return value
if dtime > self.build_timeout:
message = ('Time Limit Exceeded! (%ds)'
'while waiting for %s, '
'but we got %s.' %
(self.build_timeout, status, value))
raise exceptions.TimeoutException(message)
time.sleep(self.build_interval)
old_value = value
value = self._get_snapshot_status(snapshot_id)
def delete_snapshot(self, snapshot_id):
"""Delete Snapshot."""
return self.delete("snapshots/%s" % str(snapshot_id))
def is_resource_deleted(self, id):
try:
self.get_snapshot(id)
except exceptions.NotFound:
return True
return False

Binary file not shown.

View File

@ -0,0 +1,132 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import json
import time
import urllib
from fuel.common.rest_client import RestClient
from fuel import exceptions
class VolumesClientJSON(RestClient):
"""
Client class to send CRUD Volume API requests to a Cinder endpoint
"""
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(VolumesClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.volume.catalog_type
self.build_interval = self.config.volume.build_interval
self.build_timeout = self.config.volume.build_timeout
def list_volumes(self, params=None):
"""List all the volumes created."""
url = 'volumes'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volumes']
def list_volumes_with_detail(self, params=None):
"""List the details of all volumes."""
url = 'volumes/detail'
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volumes']
def get_volume(self, volume_id):
"""Returns the details of a single volume."""
url = "volumes/%s" % str(volume_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volume']
def create_volume(self, size, **kwargs):
"""
Creates a new Volume.
size(Required): Size of volume in GB.
Following optional keyword arguments are accepted:
display_name: Optional Volume Name.
metadata: A dictionary of values to be used as metadata.
volume_type: Optional Name of volume_type for the volume
snapshot_id: When specified the volume is created from this snapshot
imageRef: When specified the volume is created from this image
"""
post_body = {'size': size}
post_body.update(kwargs)
post_body = json.dumps({'volume': post_body})
resp, body = self.post('volumes', post_body, self.headers)
body = json.loads(body)
return resp, body['volume']
def delete_volume(self, volume_id):
"""Deletes the Specified Volume."""
return self.delete("volumes/%s" % str(volume_id))
def attach_volume(self, volume_id, instance_uuid, mountpoint):
"""Attaches a volume to a given instance on a given mountpoint."""
post_body = {
'instance_uuid': instance_uuid,
'mountpoint': mountpoint,
}
post_body = json.dumps({'os-attach': post_body})
url = 'volumes/%s/action' % (volume_id)
resp, body = self.post(url, post_body, self.headers)
return resp, body
def detach_volume(self, volume_id):
"""Detaches a volume from an instance."""
post_body = {}
post_body = json.dumps({'os-detach': post_body})
url = 'volumes/%s/action' % (volume_id)
resp, body = self.post(url, post_body, self.headers)
return resp, body
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status."""
resp, body = self.get_volume(volume_id)
volume_name = body['display_name']
volume_status = body['status']
start = int(time.time())
while volume_status != status:
time.sleep(self.build_interval)
resp, body = self.get_volume(volume_id)
volume_status = body['status']
if volume_status == 'error':
raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
if int(time.time()) - start >= self.build_timeout:
message = ('Volume %s failed to reach %s status within '
'the required time (%s s).' %
(volume_name, status, self.build_timeout))
raise exceptions.TimeoutException(message)
def is_resource_deleted(self, id):
try:
self.get_volume(id)
except exceptions.NotFound:
return True
return False

Binary file not shown.

167
fuel/test.py Normal file
View File

@ -0,0 +1,167 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack, LLC
# 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.
import time
import nose.plugins.attrib
import testresources
import testtools
from fuel.common import log as logging
from fuel import config
from fuel import manager
LOG = logging.getLogger(__name__)
def attr(*args, **kwargs):
"""A decorator which applies the nose and testtools attr decorator
This decorator applies the nose attr decorator as well as the
the testtools.testcase.attr if it is in the list of attributes
to testtools we want to apply.
"""
def decorator(f):
if 'type' in kwargs and isinstance(kwargs['type'], str):
f = testtools.testcase.attr(kwargs['type'])(f)
if kwargs['type'] == 'smoke':
f = testtools.testcase.attr('smoke')(f)
elif 'type' in kwargs and isinstance(kwargs['type'], str):
f = testtools.testcase.attr(kwargs['type'])(f)
if kwargs['type'] == 'sanity':
f = testtools.testcase.attr('sanity')(f)
elif 'type' in kwargs and isinstance(kwargs['type'], list):
for attr in kwargs['type']:
f = testtools.testcase.attr(attr)(f)
if attr == 'sanity':
f = testtools.testcase.attr('sanity')(f)
elif attr == 'smoke':
f = testtools.testcase.attr('smoke')(f)
return nose.plugins.attrib.attr(*args, **kwargs)(f)
return decorator
class BaseTestCase(testtools.TestCase,
testtools.testcase.WithAttributes,
testresources.ResourcedTestCase):
config = config.FuelConfig()
@classmethod
def setUpClass(cls):
if hasattr(super(BaseTestCase, cls), 'setUpClass'):
super(BaseTestCase, cls).setUpClass()
def call_until_true(func, duration, sleep_for):
"""
Call the given function until it returns True (and return True) or
until the specified duration (in seconds) elapses (and return
False).
:param func: A zero argument callable that returns True on success.
:param duration: The number of seconds for which to attempt a
successful call of the function.
:param sleep_for: The number of seconds to sleep after an unsuccessful
invocation of the function.
"""
now = time.time()
timeout = now + duration
while now < timeout:
if func():
return True
LOG.debug("Sleeping for %d seconds", sleep_for)
time.sleep(sleep_for)
now = time.time()
return False
class TestCase(BaseTestCase):
"""Base test case class for all Tempest tests
Contains basic setup and convenience methods
"""
manager_class = None
@classmethod
def setUpClass(cls):
cls.manager = cls.manager_class()
for attr_name in cls.manager.client_attr_names:
# Ensure that pre-existing class attributes won't be
# accidentally overriden.
assert not hasattr(cls, attr_name)
client = getattr(cls.manager, attr_name)
setattr(cls, attr_name, client)
cls.resource_keys = {}
cls.os_resources = []
def set_resource(self, key, thing):
LOG.debug("Adding %r to shared resources of %s" %
(thing, self.__class__.__name__))
self.resource_keys[key] = thing
self.os_resources.append(thing)
def get_resource(self, key):
return self.resource_keys[key]
def remove_resource(self, key):
thing = self.resource_keys[key]
self.os_resources.remove(thing)
del self.resource_keys[key]
def status_timeout(self, things, thing_id, expected_status):
"""
Given a thing and an expected status, do a loop, sleeping
for a configurable amount of time, checking for the
expected status to show. At any time, if the returned
status of the thing is ERROR, fail out.
"""
def check_status():
# python-novaclient has resources available to its client
# that all implement a get() method taking an identifier
# for the singular resource to retrieve.
thing = things.get(thing_id)
new_status = thing.status
if new_status == 'ERROR':
self.fail("%s failed to get to expected status."
"In ERROR state."
% thing)
elif new_status == expected_status:
return True # All good.
LOG.debug("Waiting for %s to get to %s status. "
"Currently in %s status",
thing, expected_status, new_status)
conf = config.TempestConfig()
if not call_until_true(check_status,
conf.compute.build_timeout,
conf.compute.build_interval):
self.fail("Timed out waiting for thing %s to become %s"
% (thing_id, expected_status))
class ComputeFuzzClientTest(TestCase):
"""
Base test case class for OpenStack Compute API (Nova)
that uses the Tempest REST fuzz client libs for calling the API.
"""
manager_class = manager.ComputeFuzzClientManager