Split cases for nova and neutron networks.

Added simlest network tests.

Change-Id: Ifc3f951a29d85fad61869376bcbe602ae209f6c6
This commit is contained in:
alexey-mr 2015-11-05 19:17:16 +03:00
parent 3ecaef77b7
commit 93aabba2ff
21 changed files with 505 additions and 262 deletions

View File

@ -60,9 +60,9 @@ now, 8-byte hashes are generated and returned for any ID to report.
Solution: Nova GCE API just uses first key. Solution: Nova GCE API just uses first key.
* Default Openstack flavors are available as machine types. GCE doesn't allow symbol '.' in machine type names, * Default Openstack flavors are available as machine types. GCE doesn't allow symbol '.' in machine type names,
that's why GCE API plugin converts symbols '.' into '-' in 'get' requests (e.g. request of machine types converts that's why GCE API plugin converts symbols '.' into '-' in 'get' requests (e.g. request of machine types converts
the name 'm1.tiny' into m1-tiny) and vise versa in 'put/post/delete' requests (e.g. instance creation converts the name 'm1.tiny' into m1-tiny) and vise versa in 'put/post/delete' requests (e.g. instance creation converts
the name 'n1-standard-1' to 'n1.standard.1'). the name 'n1-standard-1' to 'n1.standard.1').
Authentication specifics Authentication specifics
======================== ========================

View File

@ -207,6 +207,14 @@ class API(object):
self._delete_db_item(context, item) self._delete_db_item(context, item)
return only_os_items return only_os_items
@staticmethod
def _from_gce(name):
return name.replace("-", ".")
@staticmethod
def _to_gce(name):
return name.replace(".", "-")
class _CallbackReasons(object): class _CallbackReasons(object):
check_delete = 1 check_delete = 1

View File

@ -119,7 +119,7 @@ def keystone(context):
# Ver2 doesn't create session and performs # Ver2 doesn't create session and performs
# authentication automatically, but Ver3 does create session # authentication automatically, but Ver3 does create session
# if it's not provided and doesn't perform authentication. # if it's not provided and doesn't perform authentication.
# TODO(use sessions) # TODO(alexey-mr): use sessions
c.authenticate() c.authenticate()
return c return c

View File

@ -51,7 +51,7 @@ class Controller(object):
# Ver2 doesn't create session and performs # Ver2 doesn't create session and performs
# authentication automatically, but Ver3 does create session # authentication automatically, but Ver3 does create session
# if it's not provided and doesn't perform authentication. # if it's not provided and doesn't perform authentication.
# TODO(use sessions) # TODO(alexey-mr): use sessions
keystone.authenticate() keystone.authenticate()
catalog = keystone.service_catalog.get_data() catalog = keystone.service_catalog.get_data()
public_url = clients.get_url_from_catalog(catalog, "gceapi") public_url = clients.get_url_from_catalog(catalog, "gceapi")

View File

@ -60,9 +60,3 @@ class API(base_api.API):
def _prepare_item(self, item): def _prepare_item(self, item):
item["name"] = self._to_gce(item["name"]) item["name"] = self._to_gce(item["name"])
return item return item
def _from_gce(self, name):
return name.replace("-", ".")
def _to_gce(self, name):
return name.replace(".", "-")

View File

@ -154,7 +154,7 @@ class Controller(object):
# Ver2 doesn't create session and performs # Ver2 doesn't create session and performs
# authentication automatically, but Ver3 does create session # authentication automatically, but Ver3 does create session
# if it's not provided and doesn't perform authentication. # if it's not provided and doesn't perform authentication.
# TODO(use sessions) # TODO(alexy-mr): use sessions
keystone.authenticate() keystone.authenticate()
client.auth_token = keystone.auth_token client.auth_token = keystone.auth_token
s = keystone.auth_ref.issued s = keystone.auth_ref.issued
@ -223,7 +223,7 @@ class AuthProtocol(object):
# Ver2 doesn't create session and performs # Ver2 doesn't create session and performs
# authentication automatically, but Ver3 does create session # authentication automatically, but Ver3 does create session
# if it's not provided and doesn't perform authentication. # if it's not provided and doesn't perform authentication.
# TODO(use sessions) # TODO(alexey-mr): use sessions
keystone.authenticate() keystone.authenticate()
scoped_token = keystone.auth_token scoped_token = keystone.auth_token
env["HTTP_X_AUTH_TOKEN"] = scoped_token env["HTTP_X_AUTH_TOKEN"] = scoped_token

View File

@ -20,6 +20,19 @@ from gceapi import exception
CONF = cfg.CONF CONF = cfg.CONF
# OS usual region names are in PascalCase - e.g. RegionOne,
# GCE region name should matche the regexp [a-z](?:[-a-z0-9]{0,61}[a-z0-9])?
_OS_GCE_MAP = {
'RegionOne': 'region-one',
'RegionTwo': 'region-two',
'RegionThree': 'region-three',
'RegionFour': 'region-four',
}
def _map_region_name(name):
return _OS_GCE_MAP.get(name, name)
class API(base_api.API): class API(base_api.API):
"""GCE Regions API """GCE Regions API
@ -33,7 +46,7 @@ class API(base_api.API):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(API, self).__init__(*args, **kwargs) super(API, self).__init__(*args, **kwargs)
self._REGIONS = [CONF.get("region").strip()] self._REGIONS = [_map_region_name(CONF.get("region").strip())]
def _get_type(self): def _get_type(self):
return self.KIND return self.KIND

View File

@ -58,11 +58,13 @@ if [[ ! -f $TEST_CONFIG_DIR/$TEST_CONFIG ]]; then
[[ "$?" -eq 0 ]] || { echo "Failed to prepare flavor"; exit 1; } [[ "$?" -eq 0 ]] || { echo "Failed to prepare flavor"; exit 1; }
fi fi
# create network # create default network
if [[ -n $(openstack service list | grep neutron) ]]; then if [[ -n $(openstack service list | grep neutron) ]]; then
net_id=$(neutron net-create --tenant-id $project_id "private" | grep ' id ' | awk '{print $4}') # neutron networking
networking="neutron"
net_id=$(neutron net-create --tenant-id $project_id "default" | grep ' id ' | awk '{print $4}')
[[ -n "$net_id" ]] || { echo "net-create failed"; exit 1; } [[ -n "$net_id" ]] || { echo "net-create failed"; exit 1; }
subnet_id=$(neutron subnet-create --tenant-id $project_id --ip_version 4 --gateway 10.0.0.1 --name "private_subnet" $net_id 10.0.0.0/24 | grep ' id ' | awk '{print $4}') subnet_id=$(neutron subnet-create --tenant-id $project_id --ip_version 4 --gateway 10.240.0.1 --name "private_subnet" $net_id 10.240.0.0/24 | grep ' id ' | awk '{print $4}')
[[ -n "$subnet_id" ]] || { echo "subnet-create failed"; exit 1; } [[ -n "$subnet_id" ]] || { echo "subnet-create failed"; exit 1; }
router_id=$(neutron router-create --tenant-id $project_id "private_router" | grep ' id ' | awk '{print $4}') router_id=$(neutron router-create --tenant-id $project_id "private_router" | grep ' id ' | awk '{print $4}')
[[ -n "$router_id" ]] || { echo "router-create failed"; exit 1; } [[ -n "$router_id" ]] || { echo "router-create failed"; exit 1; }
@ -72,6 +74,10 @@ if [[ ! -f $TEST_CONFIG_DIR/$TEST_CONFIG ]]; then
[[ -n "$public_net_id" ]] || { echo "can't find public network"; exit 1; } [[ -n "$public_net_id" ]] || { echo "can't find public network"; exit 1; }
neutron router-gateway-set $router_id $public_net_id neutron router-gateway-set $router_id $public_net_id
[[ "$?" -eq 0 ]] || { echo "router-gateway-set failed"; exit 1; } [[ "$?" -eq 0 ]] || { echo "router-gateway-set failed"; exit 1; }
else
# nova networking
networking="nova-network"
nova network-create "default" --fixed-range-v4 10.240.0.0/24 --gateway 10.240.0.1
fi fi
#create image in raw format #create image in raw format
@ -117,7 +123,8 @@ discovery_url=${GCE_DISCOVERY_URL:-'/discovery/v1/apis/{api}/{apiVersion}/rest'}
# GCE resource IDs for testing # GCE resource IDs for testing
project_id=${OS_PROJECT_NAME} project_id=${OS_PROJECT_NAME}
zone=${ZONE:-'nova'} zone=${ZONE:-'nova'}
region=${REGION:-'RegionOne'} networking=${networking}
region=${REGION:-'region-one'}
# convert flavor name: becase GCE dowsn't allows '.' and converts '-' into '.' # convert flavor name: becase GCE dowsn't allows '.' and converts '-' into '.'
machine_type=${flavor_name//\./-} machine_type=${flavor_name//\./-}
image=${os_image_name} image=${os_image_name}

View File

@ -0,0 +1,129 @@
# Copyright 2015 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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 gceapi.tests.functional import test_base
CREATE_ADDRESS_TEMPLATE = {
"name": "${name}",
}
def _prepare_address_create_parameters(**kwargs):
return test_base.insert_json_parameters(CREATE_ADDRESS_TEMPLATE, **kwargs)
class TestAddressesBase(test_base.GCETestCase):
@property
def addresses(self):
res = self.api.compute.addresses()
self.assertIsNotNone(
res,
'Null addresses object, api is not built properly')
return res
def _create_address(self, options):
self._add_cleanup(self._delete_address, options['name'])
cfg = self.cfg
project_id = cfg.project_id
region = cfg.region
config = _prepare_address_create_parameters(**options)
self.trace('Crete address with options {}'.format(config))
request = self.addresses.insert(
project=project_id,
region=region,
body=config)
self._execute_async_request(request, project_id, region=region)
def _delete_address(self, name):
cfg = self.cfg
project_id = cfg.project_id
region = cfg.region
self.trace('Delete address: project_id={} region={} name={}'.
format(project_id, region, name))
request = self.addresses.delete(
project=project_id,
region=region,
address=name)
self._remove_cleanup(self._delete_address, name)
self._execute_async_request(request, project_id, region=region)
def _list_addresses(self):
cfg = self.cfg
project_id = cfg.project_id
region = cfg.region
self.trace('List addresses: project_id={} region={}'.
format(project_id, region))
request = self.addresses.list(
project=project_id,
region=region)
result = request.execute()
self.trace('Addresses: {}'.format(result))
self.api.validate_schema(value=result, schema_name='AddressList')
return result
def _get_address(self, name):
cfg = self.cfg
project_id = cfg.project_id
region = cfg.region
self.trace('Get address: project_id={} region={} name={}'.
format(project_id, region, name))
request = self.addresses.get(
project=project_id,
region=region,
address=name)
result = request.execute()
self.trace('Addresses: {}'.format(result))
self.api.validate_schema(value=result, schema_name='Address')
return result
class TestAddressesCRUD(TestAddressesBase):
@property
def addresses(self):
res = self.api.compute.addresses()
self.assertIsNotNone(
res,
'Null addresses object, api is not built properly')
return res
def setUp(self):
super(TestAddressesCRUD, self).setUp()
self._address_name = self._rand_name('testaddr')
def _create(self):
options = {
'name': self._address_name
}
self._create_address(options)
def _read(self):
result = self._get_address(self._address_name)
self.assertEqual(self._address_name, result['name'])
result = self._list_addresses()
self.assertFind(self._address_name, result)
def _update(self):
pass
def _delete(self):
self._delete_address(self._address_name)
def test_crud(self):
self._create()
self._read()
self._update()
self._delete()

View File

@ -15,17 +15,11 @@
# under the License. # under the License.
from string import Template
from json import dumps
from json import loads
from gceapi.tests.functional import test_base from gceapi.tests.functional import test_base
BASE_COMPUTE_URL = '{address}/compute/v1'
CREATE_INSTANCE_TEMPLATE = { CREATE_INSTANCE_TEMPLATE = {
"name": "${instance}", "name": "${name}",
"description": "Testing instance", "description": "Testing instance",
"machineType": "zones/${zone}/machineTypes/${machine_type}", "machineType": "zones/${zone}/machineTypes/${machine_type}",
"disks": [ "disks": [
@ -66,30 +60,13 @@ CREATE_INSTANCE_TEMPLATE = {
} }
] ]
} }
CREATE_NETWORK_TEMPLATE = {
"name": "${name}",
"IPv4Range": "10.240.0.0/16",
"description": "testing network ${name}",
"gatewayIPv4": "10.240.0.1"
}
def _insert_json_parameters(obj, **kwargs): def _prepare_instance_insert_parameters(**kwargs):
s = dumps(obj) return test_base.insert_json_parameters(CREATE_INSTANCE_TEMPLATE, **kwargs)
t = Template(s)
s = t.substitute(**kwargs)
return loads(s)
def _prepare_instace_insert_parameters(**kwargs): class TestInstancesBase(test_base.GCETestCase):
return _insert_json_parameters(CREATE_INSTANCE_TEMPLATE, **kwargs)
def _prepare_network_create_parameters(**kwargs):
return _insert_json_parameters(CREATE_NETWORK_TEMPLATE, **kwargs)
class TestIntancesBase(test_base.GCETestCase):
@property @property
def instances(self): def instances(self):
res = self.api.compute.instances() res = self.api.compute.instances()
@ -98,132 +75,87 @@ class TestIntancesBase(test_base.GCETestCase):
'Null instances object, api is not built properly') 'Null instances object, api is not built properly')
return res return res
@property def _create_instance(self, options):
def networks(self): self._add_cleanup(self._delete_instance, options['name'])
res = self.api.compute.networks()
self.assertIsNotNone(
res,
'Null networks object, api is not built properly')
return res
def setUp(self):
super(TestIntancesBase, self).setUp()
self._instance_name = self.getUniqueString('testinst')
self._network_name = self.getUniqueString('testnet')
def _create_network(self):
cfg = self.cfg
project_id = cfg.project_id
network = self._network_name
kw = {
'name': network,
}
config = _prepare_network_create_parameters(**kw)
self.trace('Crete network with options {}'.format(config))
request = self.networks.insert(
project=project_id,
body=config)
result = self._execute_async_request(request, project_id)
self.api.validate_schema(value=result, schema_name='Operation')
return result
def _delete_network(self):
cfg = self.cfg
project_id = cfg.project_id
network = self._network_name
self.trace(
'Delete network: project_id={} network={}'.
format(project_id, network))
request = self.networks.delete(
project=project_id,
network=network)
result = self._execute_async_request(request, project_id)
self.api.validate_schema(value=result, schema_name='Operation')
return result
def _create_instance(self):
cfg = self.cfg cfg = self.cfg
project_id = cfg.project_id project_id = cfg.project_id
zone = cfg.zone zone = cfg.zone
kw = { config = _prepare_instance_insert_parameters(**options)
'zone': zone,
'instance': self._instance_name,
'machine_type': cfg.machine_type,
'image': cfg.image,
'network': self._network_name,
}
config = _prepare_instace_insert_parameters(**kw)
self.trace('Crete instance with options {}'.format(config)) self.trace('Crete instance with options {}'.format(config))
request = self.instances.insert( request = self.instances.insert(
project=project_id, project=project_id,
zone=zone, zone=zone,
body=config) body=config)
result = self._execute_async_request(request, project_id, zone=zone) self._execute_async_request(request, project_id, zone=zone)
self.api.validate_schema(value=result, schema_name='Operation')
return result
def _delete_instance(self): def _delete_instance(self, name):
cfg = self.cfg cfg = self.cfg
project_id = cfg.project_id project_id = cfg.project_id
zone = cfg.zone zone = cfg.zone
instance = self._instance_name self.trace('Delete instance: project_id={} zone={} instance {}'.
self.trace( format(project_id, zone, name))
'Delete instance: project_id={} zone={} instance {}'.
format(project_id, zone, instance))
request = self.instances.delete( request = self.instances.delete(
project=project_id, project=project_id,
zone=zone, zone=zone,
instance=instance) instance=name)
result = self._execute_async_request(request, project_id, zone=zone) self._remove_cleanup(self._delete_instance, name)
self.api.validate_schema(value=result, schema_name='Operation') self._execute_async_request(request, project_id, zone=zone)
return result
def _list(self): def _list_instances(self):
project_id = self.cfg.project_id project_id = self.cfg.project_id
zone = self.cfg.zone zone = self.cfg.zone
self.trace( self.trace('List instances: project_id={} zone={}'.
'List instances: project_id={} zone={}'.format(project_id, zone)) format(project_id, zone))
request = self.instances.list(project=project_id, zone=zone) request = self.instances.list(project=project_id, zone=zone)
self._trace_request(request) self.trace_request(request)
result = request.execute() result = request.execute()
self.trace('Instances: {}'.format(result)) self.trace('Instances: {}'.format(result))
self.api.validate_schema(value=result, schema_name='InstanceList') self.api.validate_schema(value=result, schema_name='InstanceList')
self.assertFind(self._instance_name, result)
return result return result
def _get(self): def _get_instance(self, name):
project_id = self.cfg.project_id project_id = self.cfg.project_id
zone = self.cfg.zone zone = self.cfg.zone
instance = self._instance_name self.trace('Get instance: project_id={} zone={} instance={}'.
self.trace( format(project_id, zone, name))
'Get instance: project_id={} zone={} instance={}'.
format(project_id, zone, instance))
request = self.instances.get( request = self.instances.get(
project=project_id, project=project_id,
zone=zone, zone=zone,
instance=instance) instance=name)
result = request.execute() result = request.execute()
self.trace('Instance: {}'.format(result)) self.trace('Instance: {}'.format(result))
self.api.validate_schema(value=result, schema_name='Instance') self.api.validate_schema(value=result, schema_name='Instance')
return result return result
class TestIntancesCRUD(TestIntancesBase): class TestInstancesCRUD(TestInstancesBase):
def setUp(self):
super(TestInstancesCRUD, self).setUp()
self._instance_name = self._rand_name('testinst')
def _create(self): def _create(self):
self._create_network() cfg = self.cfg
self._create_instance() options = {
'zone': cfg.zone,
'name': self._instance_name,
'machine_type': cfg.machine_type,
'image': cfg.image,
'network': 'default',
}
self._create_instance(options)
def _read(self): def _read(self):
self._get() result = self._get_instance(self._instance_name)
self._list() self.assertEqual(self._instance_name, result['name'])
result = self._list_instances()
self.assertFind(self._instance_name, result)
def _update(self): def _update(self):
#TODO(to impl simple update cases) # TODO(alexey-mr): to impl simple update cases
pass pass
def _delete(self): def _delete(self):
self._delete_instance() self._delete_instance(self._instance_name)
self._delete_network()
def test_crud(self): def test_crud(self):
self._create() self._create()

View File

@ -0,0 +1,132 @@
# Copyright 2015 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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 gceapi.tests.functional import test_base
CREATE_NETWORK_TEMPLATE = {
"name": "${name}",
"IPv4Range": "${ip_range}",
"description": "testing network ${name}",
"gatewayIPv4": "${gateway}"
}
def _prepare_network_create_parameters(**kwargs):
return test_base.insert_json_parameters(CREATE_NETWORK_TEMPLATE, **kwargs)
class TestNetworksBase(test_base.GCETestCase):
@property
def networks(self):
res = self.api.compute.networks()
self.assertIsNotNone(
res,
'Null networks object, api is not built properly')
return res
def _create_network(self, options):
self._add_cleanup(self._delete_network, options['name'])
project_id = self.cfg.project_id
config = _prepare_network_create_parameters(**options)
self.trace('Crete network with options {}'.format(config))
request = self.networks.insert(
project=project_id,
body=config)
self._execute_async_request(request, project_id)
def _delete_network(self, name):
cfg = self.cfg
project_id = cfg.project_id
self.trace('Delete network: project_id={} network={}'.
format(project_id, name))
request = self.networks.delete(
project=project_id,
network=name)
self._remove_cleanup(self._delete_network, name)
self._execute_async_request(request, project_id)
def _list_networks(self):
project_id = self.cfg.project_id
self.trace('List networks: project_id={}'.format(project_id))
request = self.networks.list(project=project_id)
self.trace_request(request)
result = request.execute()
self.trace('Networks: {}'.format(result))
self.api.validate_schema(value=result, schema_name='NetworkList')
return result
def _get_network(self, name):
project_id = self.cfg.project_id
self.trace('Get network: project_id={} network={}'.
format(project_id, name))
request = self.networks.get(
project=project_id,
network=name)
result = request.execute()
self.trace('Network: {}'.format(result))
self.api.validate_schema(value=result, schema_name='Network')
return result
class TestReadDefaultNetwork(TestNetworksBase):
def setUp(self):
super(TestReadDefaultNetwork, self).setUp()
self._network_name = 'default'
def test_get(self):
self._get_network(self._network_name)
def test_list(self):
result = self._list_networks()
self.assertFind(self._network_name, result)
class TestNetworksCRUD(TestNetworksBase):
def setUp(self):
if self.cfg.networking == 'nova-network':
self.skipTest('Skip network because of nova-network')
return
super(TestNetworksCRUD, self).setUp()
self._network_name = self._rand_name('network')
def _create(self):
options = {
'name': self._network_name,
'ip_range': '10.240.0.0/16',
'gateway': '10.240.0.1'
}
# TODO(alexey-mr): gateway is optional, so add case with absent one
self._create_network(options)
def _read(self):
result = self._get_network(self._network_name)
self.assertEqual(self._network_name, result['name'])
result = self._list_networks()
self.assertFind(self._network_name, result)
def _update(self):
# TODO(alexey-mr): to be implemented
pass
def _delete(self):
self._delete_network(self._network_name)
def test_crud(self):
self._create()
self._read()
self._update()
self._delete()

View File

@ -14,8 +14,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import unittest
from gceapi.tests.functional import test_base from gceapi.tests.functional import test_base
@ -34,8 +32,6 @@ class TestRegions(test_base.GCETestCase):
'Null regions object, api is not built properly') 'Null regions object, api is not built properly')
return res return res
# TODO(alexey-mr): Google allows [a-z](?:[-a-z0-9]{0,61}[a-z0-9])?
@unittest.skip("Skip test for now: google dosnt't allow name RegionOne")
def test_describe(self): def test_describe(self):
project_id = self.cfg.project_id project_id = self.cfg.project_id
region = self.cfg.region region = self.cfg.region

View File

@ -31,13 +31,14 @@ import tempest.test
CONF = config.CONF CONF = config.CONF
LOG = logging.getLogger("tempest.thirdparty.gce") LOG = logging.getLogger("tempest.thirdparty.gce")
REGION_NAME = 'region-one'
class GCEConnection(rest_client.RestClient): class GCEConnection(rest_client.RestClient):
def __init__(self, auth_provider): def __init__(self, auth_provider):
super(GCEConnection, self).__init__(auth_provider, super(GCEConnection, self).__init__(auth_provider,
"gceapi", "RegionOne") "gceapi", REGION_NAME)
self.service = CONF.gceapi.catalog_type self.service = CONF.gceapi.catalog_type
def set_zone(self, zone): def set_zone(self, zone):

View File

@ -79,8 +79,11 @@ OPTIONS = [
default='nova', default='nova',
help='GCE Zone for testing'), help='GCE Zone for testing'),
cfg.StrOpt('region', cfg.StrOpt('region',
default='RegionOne', default='us-central1',
help='GCE Region for testing'), help='GCE Region for testing'),
cfg.StrOpt('networking',
default='neutron',
help='Types of OS networking: neutron or nova-network'),
cfg.StrOpt('machine_type', cfg.StrOpt('machine_type',
default='n1-standard-1', default='n1-standard-1',

View File

@ -15,39 +15,34 @@
# under the License. # under the License.
from keystoneclient.client import Client as KeystoneClient from keystoneclient import client as keystone_client
from oauth2client.client import AccessTokenCredentials from oauth2client import client as oauth_client
from oauth2client.client import GoogleCredentials
class CredentialsProvider(object): class CredentialsProvider(object):
def __init__(self, supp): def __init__(self, cfg):
self._supp = supp self.cfg = cfg
def _trace(self, msg): @staticmethod
self._supp.trace(msg) def _get_app_credentials():
return oauth_client.GoogleCredentials.get_application_default()
def _get_app_credentials(self): def _get_token_credentials(self):
self._trace('Create GoogleCredentials from default app file')
return GoogleCredentials.get_application_default()
def _get_token_crenetials(self):
client = self._create_keystone_client() client = self._create_keystone_client()
token = client.auth_token token = client.auth_token
self._trace('Created token {}'.format(token)) return oauth_client.AccessTokenCredentials(
return AccessTokenCredentials(access_token=token, access_token=token,
user_agent='GCE test') user_agent='GCE test')
def _create_keystone_client(self): def _create_keystone_client(self):
cfg = self._supp.cfg cfg = self.cfg
auth_data = { auth_data = {
'username': cfg.username, 'username': cfg.username,
'password': cfg.password, 'password': cfg.password,
'tenant_name': cfg.project_id, 'tenant_name': cfg.project_id,
'auth_url': cfg.auth_url 'auth_url': cfg.auth_url
} }
self._trace('Create keystone client, auth_data={}'.format(auth_data)) client = keystone_client.Client(**auth_data)
client = KeystoneClient(**auth_data)
if not client.authenticate(): if not client.authenticate():
raise Exception('Failed to authenticate user') raise Exception('Failed to authenticate user')
return client return client
@ -58,9 +53,9 @@ class CredentialsProvider(object):
@property @property
def credentials(self): def credentials(self):
cred_type = self._supp.cfg.cred_type cred_type = self.cfg.cred_type
if cred_type == 'os_token': if cred_type == 'os_token':
return self._get_token_crenetials() return self._get_token_credentials()
elif cred_type == 'gcloud_auth': elif cred_type == 'gcloud_auth':
return self._get_app_credentials() return self._get_app_credentials()
else: else:

View File

@ -14,33 +14,43 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import string
import time import time
import traceback
from googleapiclient.discovery import build from googleapiclient import discovery
from googleapiclient.schema import Schemas from googleapiclient import schema
from jsonschema import RefResolver import jsonschema
from jsonschema import validate from oslo_log import log as logging
from tempest_lib import base from tempest_lib import base
from tempest_lib.common.utils import data_utils
from gceapi.tests.functional import config from gceapi.tests.functional import config
from gceapi.tests.functional.credentials import CredentialsProvider from gceapi.tests.functional import credentials
class TestSupp(object): CONF = config.CONF.gce
def __init__(self, *args, **kwargs): LOG = logging.getLogger("gceapi")
self._cfg = config.CONF.gce
from oslo_log import log as logging
self._log = logging.getLogger("gceapi")
@property
def cfg(self):
return self._cfg
def trace(self, *args, **kwargs):
self._log.debug(*args, **kwargs)
class LocalRefResolver(RefResolver): def trace(msg):
LOG.debug(msg)
def safe_call(method):
def wrapper(self, *args, **kwargs):
try:
return method(self, *args, **kwargs)
except Exception as err:
trace('Exception {}'.format(err))
bt = traceback.format_exc()
trace('Exception back trace {}'.format(bt))
return None
return wrapper
class LocalRefResolver(jsonschema.RefResolver):
def __init__( def __init__(
self, self,
base_uri, base_uri,
@ -66,15 +76,14 @@ class LocalRefResolver(RefResolver):
class GCEApi(object): class GCEApi(object):
def __init__(self, supp, cred_provider): def __init__(self, cred_provider):
self._compute = None self._compute = None
self._cred_provider = cred_provider self._cred_provider = cred_provider
self._schema = None self._schema = None
self._scheme_ref_resolver = 0 self._scheme_ref_resolver = 0
self._supp = supp
def init(self): def init(self):
self._schema = Schemas(self._supp.cfg.schema) self._schema = schema.Schemas(CONF.schema)
self._scheme_ref_resolver = LocalRefResolver.from_schema( self._scheme_ref_resolver = LocalRefResolver.from_schema(
self._schema.schemas) self._schema.schemas)
self._build_api() self._build_api()
@ -82,9 +91,8 @@ class GCEApi(object):
def _build_api(self): def _build_api(self):
credentials = self._cred_provider.credentials credentials = self._cred_provider.credentials
url = self._discovery_url url = self._discovery_url
self._trace( trace('Build Google compute api with discovery url {}'.format(url))
'Build Google compute api with discovery url {}'.format(url)) self._compute = discovery.build(
self._compute = build(
'compute', 'v1', 'compute', 'v1',
credentials=credentials, credentials=credentials,
discoveryServiceUrl=url discoveryServiceUrl=url
@ -92,7 +100,7 @@ class GCEApi(object):
@property @property
def _discovery_url(self): def _discovery_url(self):
cfg = self._supp.cfg cfg = CONF
return '{}://{}:{}{}'.format( return '{}://{}:{}{}'.format(
cfg.protocol, cfg.protocol,
cfg.host, cfg.host,
@ -100,9 +108,6 @@ class GCEApi(object):
cfg.discovery_url cfg.discovery_url
) )
def _trace(self, msg):
self._supp.trace(msg)
@property @property
def compute(self): def compute(self):
assert(self._compute is not None) assert(self._compute is not None)
@ -110,7 +115,7 @@ class GCEApi(object):
@property @property
def base_url(self): def base_url(self):
cfg = self._supp.cfg cfg = CONF
return '{}://{}:{}'.format( return '{}://{}:{}'.format(
cfg.protocol, cfg.protocol,
cfg.host, cfg.host,
@ -119,27 +124,31 @@ class GCEApi(object):
def validate_schema(self, value, schema_name): def validate_schema(self, value, schema_name):
schema = self._schema.get(schema_name) schema = self._schema.get(schema_name)
validate(value, schema, resolver=self._scheme_ref_resolver) jsonschema.validate(value, schema, resolver=self._scheme_ref_resolver)
class GCETestCase(base.BaseTestCase): class GCETestCase(base.BaseTestCase):
@property
def cfg(self):
assert(self._supp.cfg is not None)
return self._supp.cfg
@property @property
def api(self): def api(self):
assert(self._api is not None) assert(self._api is not None)
return self._api return self._api
def trace(self, *args, **kwargs): @property
self._supp.trace(*args, **kwargs) def cfg(self):
return CONF
@staticmethod
def trace(msg):
trace(msg)
@staticmethod
def trace_request(request):
trace('Request: {}'.format(request.to_json()))
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls._supp = TestSupp() cp = credentials.CredentialsProvider(CONF)
cls._api = GCEApi(cls._supp, CredentialsProvider(cls._supp)) cls._api = GCEApi(cp)
cls._api.init() cls._api.init()
super(GCETestCase, cls).setUpClass() super(GCETestCase, cls).setUpClass()
@ -154,49 +163,60 @@ class GCETestCase(base.BaseTestCase):
self.fail( self.fail(
'There is no required item {} in the list {}'.format(item, items)) 'There is no required item {} in the list {}'.format(item, items))
def _trace_request(self, r): def _get_operations_request(self, name, project, zone, region):
self.trace('Request: {}'.format(r.to_json()))
def _get_operations_request(self, name, project, zone):
if zone is not None: if zone is not None:
return self.api.compute.zoneOperations().get( return self.api.compute.zoneOperations().get(
project=project, project=project,
zone=zone, zone=zone,
operation=name) operation=name)
if region is not None:
return self.api.compute.regionOperations().get(
project=project,
region=region,
operation=name)
return self.api.compute.globalOperations().get( return self.api.compute.globalOperations().get(
project=project, project=project,
operation=name) operation=name)
def _execute_async_request(self, request, project, zone=None): @staticmethod
self._trace_request(request) def _rand_name(prefix='n-'):
return data_utils.rand_name(prefix)
def _add_cleanup(self, method, *args, **kwargs):
self.addCleanup(method, *args, **kwargs)
@safe_call
def _remove_cleanup(self, method, *args, **kwargs):
v = (method, args, kwargs)
self._cleanups.remove(v)
def _execute_async_request(self, request, project, zone=None, region=None):
self.trace_request(request)
operation = request.execute() operation = request.execute()
name = operation['name'] name = operation['name']
self.trace('Waiting for operation {} to finish...'.format(name)) self.trace('Waiting for operation {} to finish...'.format(name))
begin = time.time() begin = time.time()
timeout = self._supp.cfg.build_timeout timeout = self.cfg.build_timeout
result = None
while time.time() - begin < timeout: while time.time() - begin < timeout:
result = self._get_operations_request( result = self._get_operations_request(
name, project, zone).execute() name, project, zone, region).execute()
self.api.validate_schema(value=result, schema_name='Operation')
if result['status'] == 'DONE': if result['status'] == 'DONE':
if 'error' in result: if 'error' in result:
self.fail('Request {} failed with error {}'. format( self.fail('Request {} failed with error {}'. format(
name, result['error'])) name, result['error']))
else: else:
self.trace("Request {} done successfully".format(name)) self.trace("Request {} done successfully".format(name))
return result return
time.sleep(1) time.sleep(1)
self.fail('Request {} failed with timeout {}'.format(name, timeout)) self.fail('Request {} failed with timeout {},'
' latest operation status {}'.format(name, timeout, result))
def safe_call(method): def insert_json_parameters(obj, **kwargs):
def wrapper(self, *args, **kwargs): s = json.dumps(obj)
try: t = string.Template(s)
return method(self, *args, **kwargs) s = t.substitute(**kwargs)
except Exception as err: return json.loads(s)
self.trace('Exception {}'.format(err))
import traceback
bt = traceback.format_exc()
self.trace('Exception back trace {}'.format(bt))
return None
return wrapper

View File

@ -58,13 +58,14 @@ COMMON_PENDING_OPERATION = {
} }
COMMON_PENDING_OPERATION.update(COMMON_OPERATION) COMMON_PENDING_OPERATION.update(COMMON_OPERATION)
REGION = fake_request.REGION
REGION_OPERATION_SPECIFIC = { REGION_OPERATION_SPECIFIC = {
u'id': u'6294142421306477203', u'id': u'5036531165588500177',
u'selfLink': u'http://localhost/compute/v1beta15/projects/' u'selfLink': u'http://localhost/compute/v1beta15/projects/'
'fake_project/regions/RegionOne/operations/' 'fake_project/regions/%s/operations/'
'operation-735d48a5-284e-4fb4-a10c-a465ac0b8888', 'operation-735d48a5-284e-4fb4-a10c-a465ac0b8888' % REGION,
u'region': u'http://localhost/compute/v1beta15/projects/' u'region': u'http://localhost/compute/v1beta15/projects/'
'fake_project/regions/RegionOne', 'fake_project/regions/%s' % REGION,
} }
COMMON_REGION_FINISHED_OPERATION = copy.copy(COMMON_FINISHED_OPERATION) COMMON_REGION_FINISHED_OPERATION = copy.copy(COMMON_FINISHED_OPERATION)

View File

@ -17,12 +17,12 @@ from gceapi import wsgi_ext as os_wsgi
PROJECT_ID = "4a5cc7d8893544a9babb3b890227d75e" PROJECT_ID = "4a5cc7d8893544a9babb3b890227d75e"
REGION = u'region-one'
FAKE_SERVICE_CATALOG = [{ FAKE_SERVICE_CATALOG = [{
u'endpoints': [{ u'endpoints': [{
u'adminURL': u'http://192.168.137.21:8774/v2/' + PROJECT_ID, u'adminURL': u'http://192.168.137.21:8774/v2/' + PROJECT_ID,
u'region': u'RegionOne', u'region': REGION,
u'id': u'81a8b36abc5f4945bbd1269be0423012', u'id': u'81a8b36abc5f4945bbd1269be0423012',
u'internalURL': u'http://192.168.137.21:8774/v2/' + PROJECT_ID, u'internalURL': u'http://192.168.137.21:8774/v2/' + PROJECT_ID,
u'publicURL': u'http://192.168.137.21:8774/v2/' + PROJECT_ID}], u'publicURL': u'http://192.168.137.21:8774/v2/' + PROJECT_ID}],
@ -32,7 +32,7 @@ FAKE_SERVICE_CATALOG = [{
}, { }, {
u'endpoints': [{ u'endpoints': [{
u'adminURL': u'http://192.168.137.21:9696/', u'adminURL': u'http://192.168.137.21:9696/',
u'region': u'RegionOne', u'region': REGION,
u'id': u'10a0fc598a5741c390f0d6560a89fced', u'id': u'10a0fc598a5741c390f0d6560a89fced',
u'internalURL': u'http://192.168.137.21:9696/', u'internalURL': u'http://192.168.137.21:9696/',
u'publicURL': u'http://192.168.137.21:9696/'}], u'publicURL': u'http://192.168.137.21:9696/'}],
@ -42,7 +42,7 @@ FAKE_SERVICE_CATALOG = [{
}, { }, {
u'endpoints': [{ u'endpoints': [{
u'adminURL': u'http://192.168.137.21:9292', u'adminURL': u'http://192.168.137.21:9292',
u'region': u'RegionOne', u'region': REGION,
u'id': u'39643060448c4c089535fce07f2d2aa4', u'id': u'39643060448c4c089535fce07f2d2aa4',
u'internalURL': u'http://192.168.137.21:9292', u'internalURL': u'http://192.168.137.21:9292',
u'publicURL': u'http://192.168.137.21:9292'}], u'publicURL': u'http://192.168.137.21:9292'}],
@ -52,7 +52,7 @@ FAKE_SERVICE_CATALOG = [{
}, { }, {
u'endpoints': [{ u'endpoints': [{
u'adminURL': u'http://192.168.137.21:8776/v1/' + PROJECT_ID, u'adminURL': u'http://192.168.137.21:8776/v1/' + PROJECT_ID,
u'region': u'RegionOne', u'region': REGION,
u'id': u'494bd5333aed467092316e03b1163139', u'id': u'494bd5333aed467092316e03b1163139',
u'internalURL': u'http://192.168.137.21:8776/v1/' + PROJECT_ID, u'internalURL': u'http://192.168.137.21:8776/v1/' + PROJECT_ID,
u'publicURL': u'http://192.168.137.21:8776/v1/' + PROJECT_ID}], u'publicURL': u'http://192.168.137.21:8776/v1/' + PROJECT_ID}],

View File

@ -13,22 +13,27 @@
# limitations under the License. # limitations under the License.
from gceapi.api import addresses from gceapi.api import addresses
from gceapi.tests.unit.api import common
from gceapi.tests.unit.api import common
from gceapi.tests.unit.api import fake_request
REGION = fake_request.REGION
EXPECTED_ADDRESSES = [{ EXPECTED_ADDRESSES = [{
"kind": "compute#address", "kind": "compute#address",
"id": "4065623605586261056", "id": "1870839154859306350",
"creationTimestamp": "", "creationTimestamp": "",
"status": "IN USE", "status": "IN USE",
"region": "http://localhost/compute/v1beta15/projects/" "region": "http://localhost/compute/v1beta15/projects/"
"fake_project/regions/RegionOne", "fake_project/regions/%s" % REGION,
"name": "address-172-24-4-227", "name": "address-172-24-4-227",
"description": "", "description": "",
"address": "172.24.4.227", "address": "172.24.4.227",
"selfLink": "http://localhost/compute/v1beta15/projects/" "selfLink": "http://localhost/compute/v1beta15/projects/"
"fake_project/regions/RegionOne/addresses/address-172-24-4-227", "fake_project/regions/%s/"
"addresses/address-172-24-4-227" % REGION,
"users": ["http://localhost/compute/v1beta15/projects/" "users": ["http://localhost/compute/v1beta15/projects/"
"fake_project/zones/nova/instances/i1"] "fake_project/zones/nova/instances/i1"]
}] }]
@ -40,37 +45,38 @@ class AddressesTest(common.GCEControllerTest):
def test_get_address_by_invalid_name(self): def test_get_address_by_invalid_name(self):
response = self.request_gce("/fake_project/regions/" response = self.request_gce("/fake_project/regions/"
"RegionOne/addresses/fake") "%s/addresses/fake" % REGION)
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
def test_get_address_by_name(self): def test_get_address_by_name(self):
response = self.request_gce("/fake_project/regions/" response = self.request_gce(
"RegionOne/addresses/address-172-24-4-227") "/fake_project/regions/%s/addresses/address-172-24-4-227" % REGION)
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)
self.assertEqual(response.json_body, EXPECTED_ADDRESSES[0]) self.assertEqual(response.json_body, EXPECTED_ADDRESSES[0])
def test_get_address_list_filtered(self): def test_get_address_list_filtered(self):
response = self.request_gce("/fake_project/regions/RegionOne/addresses" response = self.request_gce("/fake_project/regions/%s/addresses"
"?filter=name+eq+address-172-24-4-227") "?filter=name+eq+address-172-24-4-227" %
REGION)
expected = { expected = {
"kind": "compute#addressList", "kind": "compute#addressList",
"id": "projects/fake_project/regions/RegionOne/addresses", "id": "projects/fake_project/regions/%s/addresses" % REGION,
"selfLink": "http://localhost/compute/v1beta15/projects" "selfLink": "http://localhost/compute/v1beta15/projects"
"/fake_project/regions/RegionOne/addresses", "/fake_project/regions/%s/addresses" % REGION,
"items": [EXPECTED_ADDRESSES[0]] "items": [EXPECTED_ADDRESSES[0]]
} }
self.assertEqual(response.json_body, expected) self.assertEqual(response.json_body, expected)
def test_get_address_list(self): def test_get_address_list(self):
response = self.request_gce("/fake_project/regions/RegionOne" response = self.request_gce("/fake_project/regions/%s"
"/addresses") "/addresses" % REGION)
expected = { expected = {
"kind": "compute#addressList", "kind": "compute#addressList",
"id": "projects/fake_project/regions/RegionOne/addresses", "id": "projects/fake_project/regions/%s/addresses" % REGION,
"selfLink": "http://localhost/compute/v1beta15/projects" "selfLink": "http://localhost/compute/v1beta15/projects"
"/fake_project/regions/RegionOne/addresses", "/fake_project/regions/%s/addresses" % REGION,
"items": EXPECTED_ADDRESSES "items": EXPECTED_ADDRESSES
} }
@ -86,7 +92,7 @@ class AddressesTest(common.GCEControllerTest):
"selfLink": "http://localhost/compute/v1beta15/projects" "selfLink": "http://localhost/compute/v1beta15/projects"
"/fake_project/aggregated/addresses", "/fake_project/aggregated/addresses",
"items": { "items": {
"regions/RegionOne": { "regions/%s" % REGION: {
"addresses": [EXPECTED_ADDRESSES[0]] "addresses": [EXPECTED_ADDRESSES[0]]
}, },
} }
@ -103,7 +109,7 @@ class AddressesTest(common.GCEControllerTest):
"selfLink": "http://localhost/compute/v1beta15/projects" "selfLink": "http://localhost/compute/v1beta15/projects"
"/fake_project/aggregated/addresses", "/fake_project/aggregated/addresses",
"items": { "items": {
"regions/RegionOne": { "regions/%s" % REGION: {
"addresses": EXPECTED_ADDRESSES "addresses": EXPECTED_ADDRESSES
}, },
} }
@ -112,21 +118,21 @@ class AddressesTest(common.GCEControllerTest):
self.assertEqual(response.json_body, expected) self.assertEqual(response.json_body, expected)
def test_delete_address_with_invalid_name(self): def test_delete_address_with_invalid_name(self):
response = self.request_gce("/fake_project/regions/RegionOne" response = self.request_gce(
"/addresses/fake-address", method="DELETE") "/fake_project/regions/%s/addresses/fake-address" % REGION,
method="DELETE")
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
def test_delete_address(self): def test_delete_address(self):
response = self.request_gce( response = self.request_gce(
"/fake_project/regions/RegionOne/" "/fake_project/regions/%s/addresses/address-172-24-4-227" % REGION,
"addresses/address-172-24-4-227", method="DELETE")
method="DELETE")
expected = { expected = {
"operationType": "delete", "operationType": "delete",
"targetId": "4065623605586261056", "targetId": "1870839154859306350",
"targetLink": "http://localhost/compute/v1beta15/projects/" "targetLink": "http://localhost/compute/v1beta15/projects/"
"fake_project/regions/RegionOne/" "fake_project/regions/%s/"
"addresses/address-172-24-4-227", "addresses/address-172-24-4-227" % REGION,
} }
expected.update(common.COMMON_REGION_FINISHED_OPERATION) expected.update(common.COMMON_REGION_FINISHED_OPERATION)
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)
@ -136,16 +142,17 @@ class AddressesTest(common.GCEControllerTest):
request_body = { request_body = {
"name": "fake-address", "name": "fake-address",
} }
response = self.request_gce("/fake_project/regions/RegionOne/" response = self.request_gce("/fake_project/regions/%s/"
"addresses", "addresses" % REGION,
method="POST", method="POST",
body=request_body) body=request_body)
self.assertEqual(200, response.status_int) self.assertEqual(200, response.status_int)
expected = { expected = {
"operationType": "insert", "operationType": "insert",
"targetId": "4570437344333712421", "targetId": "8754519975833457287",
"targetLink": "http://localhost/compute/v1beta15/projects/" "targetLink": "http://localhost/compute/v1beta15/projects/"
"fake_project/regions/RegionOne/addresses/fake-address", "fake_project/regions/%s/addresses/fake-address" %
REGION,
} }
expected.update(common.COMMON_REGION_FINISHED_OPERATION) expected.update(common.COMMON_REGION_FINISHED_OPERATION)
self.assertDictEqual(expected, response.json_body) self.assertDictEqual(expected, response.json_body)

View File

@ -14,19 +14,22 @@
from gceapi.api import regions from gceapi.api import regions
from gceapi.tests.unit.api import common from gceapi.tests.unit.api import common
from gceapi.tests.unit.api import fake_request
REGION = fake_request.REGION
EXPECTED_REGIONS = [ EXPECTED_REGIONS = [
{ {
"id": "1905250285734383880", "id": "8220497844553564918",
"kind": "compute#region", "kind": "compute#region",
"selfLink": "http://localhost/compute/v1beta15/projects/fake_project" "selfLink": "http://localhost/compute/v1beta15/projects/fake_project"
"/regions/RegionOne", "/regions/%s" % REGION,
"name": "RegionOne", "name": REGION,
"status": "UP", "status": "UP",
"zones": [ "zones": [
"http://localhost/compute/v1beta15/projects/fake_project" "http://localhost/compute/v1beta15/projects/fake_project"
"/zones/nova"] "/zones/nova"
]
}, },
] ]
@ -46,14 +49,14 @@ class RegionsTest(common.GCEControllerTest):
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
def test_get_region(self): def test_get_region(self):
response = self.request_gce('/fake_project/regions/RegionOne') response = self.request_gce('/fake_project/regions/%s' % REGION)
expected = EXPECTED_REGIONS[0] expected = EXPECTED_REGIONS[0]
self.assertEqual(response.json_body, expected) self.assertEqual(response.json_body, expected)
def test_get_region_list_filtered(self): def test_get_region_list_filtered(self):
response = self.request_gce("/fake_project/regions" response = self.request_gce("/fake_project/regions"
"?filter=name+eq+RegionOne") "?filter=name+eq+%s" % REGION)
expected = { expected = {
"kind": "compute#regionList", "kind": "compute#regionList",
"id": "projects/fake_project/regions", "id": "projects/fake_project/regions",

View File

@ -14,16 +14,18 @@
from gceapi.api import zones from gceapi.api import zones
from gceapi.tests.unit.api import common from gceapi.tests.unit.api import common
from gceapi.tests.unit.api import fake_request
REGION = fake_request.REGION
EXPECTED_ZONES = [{ EXPECTED_ZONES = [{
"id": "3924463100986466035", "id": "3924463100986466035",
"kind": "compute#zone", "kind": "compute#zone",
"selfLink": "http://localhost/compute/v1beta15/projects/fake_project" "selfLink": "http://localhost/compute/v1beta15/projects/fake_project"
"/zones/nova", "/zones/nova",
"name": "nova", "name": "nova",
"status": "UP", "status": "UP",
"region": "RegionOne", "region": REGION,
}] }]