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:
parent
5dd517d172
commit
499d63fe96
|
@ -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,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)
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'tleontovich'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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.
|
@ -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.
|
@ -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
|
Binary file not shown.
|
@ -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
|
|
@ -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")
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
@ -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)
|
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
Binary file not shown.
Binary file not shown.
|
@ -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.
Binary file not shown.
Binary file not shown.
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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.
|
@ -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.
|
@ -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
|
Loading…
Reference in New Issue