Add openstackcli support for novacli

* Added Client, Config, Behaviors, response models and extensions
* Added unittests for client calls

Change-Id: I149f8ec3f17c5af1766027d1e7375852dabe2a35
This commit is contained in:
Jose Idar 2014-01-28 13:49:42 -06:00
parent 9955831638
commit 59fb3c1d96
10 changed files with 682 additions and 0 deletions

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
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.
"""

View File

@ -0,0 +1,142 @@
from time import time
from cafe.engine.behaviors import behavior
from cloudcafe.common.tools.datagen import random_string
from cloudcafe.openstackcli.novacli.client import NovaCLI
from cloudcafe.openstackcli.novacli.config import NovaCLI_Config
from cloudcafe.openstackcli.common.behaviors import \
OpenstackCLI_BaseBehavior, OpenstackCLI_BehaviorError
from cloudcafe.compute.servers_api.config import ServersConfig
from cloudcafe.compute.images_api.config import ImagesConfig
from cloudcafe.compute.flavors_api.config import FlavorsConfig
from cloudcafe.compute.common.types import \
NovaServerStatusTypes as ServerStates
from cloudcafe.compute.common.exceptions import \
TimeoutException, BuildErrorException, RequiredResourceException
class NovaCLIBehaviorError(OpenstackCLI_BehaviorError):
pass
class NovaCLI_Behaviors(OpenstackCLI_BaseBehavior):
_default_error = NovaCLIBehaviorError
def __init__(
self, nova_cli_client=None, nova_cli_config=None,
servers_api_config=None, images_api_config=None,
flavors_api_config=None):
super(NovaCLI_Behaviors, self).__init__()
self.nova_cli_client = nova_cli_client
self.nova_cli_config = nova_cli_config or NovaCLI_Config()
self.servers_api_config = servers_api_config or ServersConfig()
self.images_api_config = images_api_config or ImagesConfig()
self.flavors_api_config = flavors_api_config or FlavorsConfig()
@behavior(NovaCLI)
def create_available_server(
self, name, flavor=None, image=None, no_service_net=None,
no_public=None, disk_config=None, image_with=None,
boot_volume=None, snapshot=None, num_instances=None, meta=None,
file_=None, key_name=None, user_data=None, availability_zone=None,
security_groups=None, block_device_mapping=None, block_device=None,
swap=None, ephemeral=None, hint=None, nic=None, config_drive=None):
"""
Expected input for non-string parameters
disk_config: 'auto' or 'manual'
image-with: {key: value}
meta: {key: value, [key2=value2, ...] }
file_: {dst-path: src-path}
block_device_mapping: {dev-name: mapping}
block_device: {key=value, [key2=value2, ...] }
ephemeral: {'size': size, ['format': format]}
hint: {key: value}
nic: {'net-id'=net-uuid,
'port-id'=port-uuid,
['v4-fixed-ip'=ip-addr]}
"""
name = name or random_string('NovaCLI')
image = image or self.images_api_config.primary_image
flavor = flavor or self.flavors_api_config.primary_flavor
failures = []
attempts = self.servers_api_config.resource_build_attempts
for attempt in range(attempts):
self._log.debug(
'Attempt {attempt} of {attempts} to create server with the '
'NovaCLI.'.format(attempt=attempt + 1, attempts=attempts))
resp = self.nova_cli_client.create_server(
name=name, flavor=flavor, image=image,
no_service_net=no_service_net, no_public=no_public,
disk_config=disk_config, image_with=image_with,
boot_volume=boot_volume, snapshot=snapshot,
num_instances=num_instances, meta=meta, file_=file_,
key_name=key_name, user_data=user_data,
availability_zone=availability_zone,
security_groups=security_groups,
block_device_mapping=block_device_mapping,
block_device=block_device,
swap=swap, ephemeral=ephemeral, hint=hint, nic=nic,
config_drive=config_drive)
server = resp.entity
if server is None:
raise self._default_error("Unable to parse nova boot response")
try:
resp = self.wait_for_server_status(
server.id_, ServerStates.ACTIVE)
# Add the password from the create request
# into the final response
resp.entity.admin_pass = server.admin_pass
return resp
except (TimeoutException, BuildErrorException) as ex:
msg = 'Failed to build server {server_id}'.format(
server_id=server.id_)
self._log.exception(msg)
failures.append(ex.message)
self.nova_cli_client.delete_server(server.id_)
raise RequiredResourceException(
'Failed to successfully build a server after '
'{attempts} attempts: {failures}'.format(
attempts=attempts, failures=failures))
def wait_for_server_status(
self, server_id, desired_status, interval_time=None, timeout=None):
interval_time = int(
interval_time or self.servers_api_config.server_status_interval)
timeout = int(timeout or self.servers_api_config.server_build_timeout)
end_time = time.time() + timeout
time.sleep(interval_time)
while time.time() < end_time:
resp = self.nova_cli_client.show_server(server_id)
server = resp.entity
if server.status.lower() == ServerStates.ERROR.lower():
raise BuildErrorException(
'Build failed. Server with uuid "{0} entered ERROR status.'
.format(server.id))
if server.status == desired_status:
break
time.sleep(interval_time)
else:
raise TimeoutException(
"wait_for_server_status ran for {0} seconds and did not "
"observe server {1} reach the {2} status.".format(
timeout, server_id, desired_status))
return resp

View File

@ -0,0 +1,121 @@
from cloudcafe.openstackcli.common.client import BaseOpenstackPythonCLI_Client
from cloudcafe.openstackcli.novacli.models import responses
class NovaCLI(BaseOpenstackPythonCLI_Client):
_KWMAP = {
'os_cache': 'os-cache',
'timings': 'timings',
'timeout': 'timeout',
'os_tenant_id': 'os-tenant-id',
'os_auth_system': 'os-auth-system',
'service_type': 'service-type',
'service_name': 'service-name',
'volume_service_name': 'volume-service-name',
'os_compute_api_version': 'os-compute-api-version',
'bypass_url': 'bypass-url',
'os_auth_url': 'os-auth-url',
'endpoint_type': 'endpoint-type',
'insecure': 'insecure'}
# Make sure to include all openstack common cli paramaters in addition to
# the nova specific ones
_KWMAP.update(BaseOpenstackPythonCLI_Client._KWMAP)
#The client command the must precede any call to the cli
_CMD = 'nova'
def __init__(
self, os_cache=None, timings=None, timeout=None, os_tenant_id=None,
os_auth_system=None, service_type=None, service_name=None,
volume_service_name=None, os_compute_api_version=None,
insecure=False, bypass_url=None, os_auth_url=None,
endpoint_type=None, **kwargs):
super(NovaCLI, self).__init__(**kwargs)
self.os_cache = os_cache
self.timings = timings
self.timeout = timeout
self.os_auth_system = os_auth_system
self.service_type = service_type
self.service_name = service_name
self.volume_service_name = volume_service_name
self.os_compute_api_version = os_compute_api_version
self.insecure = insecure
self.bypass_url = bypass_url
self.os_tenant_id = os_tenant_id
self.os_auth_url = os_auth_url
self.endpoint_type = endpoint_type
def create_server(
self, name, no_service_net=None, no_public=None, disk_config=None,
flavor=None, image=None, image_with=None, boot_volume=None,
snapshot=None, num_instances=None, meta=None, file_=None,
key_name=None, user_data=None, availability_zone=None,
security_groups=None, block_device_mapping=None, block_device=None,
swap=None, ephemeral=None, hint=None, nic=None, config_drive=None):
"""
Expected input for parameters
disk_config: 'auto' or 'manual'
image-with: {key: value}
meta: {key: value, [key2=value2, ...] }
file_: {dst-path: src-path}
block_device_mapping: {dev-name: mapping}
block_device: {key=value, [key2=value2, ...] }
ephemeral: {'size': size, ['format': format]}
hint: {key: value}
nic: {'net-id'=net-uuid,
'port-id'=port-uuid,
['v4-fixed-ip'=ip-addr]}
"""
_cmd = 'boot'
_kwmap = {
'no_service_net': 'no-service-net',
'no_public': 'no-public',
'disk_config': 'disk-config',
'flavor': 'flavor',
'image': 'image',
'image_with': 'image-with',
'boot_volume': 'boot-volume',
'snapshot': 'snapshot',
'num_instances': 'num-instances',
'meta': 'meta',
'file_': 'file',
'key_name': 'key-name',
'user_data': 'user-data',
'availability_zone': 'availability-zone',
'security_groups': 'security-groups',
'block_device_mapping': 'block-device-mapping',
'block_device': 'block-device',
'swap': 'swap',
'ephemeral': 'ephemeral',
'hint': 'hint',
'nic': 'nic',
'config_drive': 'config-drive'}
no_service_net = True if no_service_net else False
no_public = True if no_public else False
meta = self._multiplicable_flag_data_to_string('meta', meta)
image_with = self._dict_to_string(image_with)
file_ = self._dict_to_string(file_)
block_device = self._dict_to_string(block_device)
ephemeral = self._dict_to_string(ephemeral)
hint = self._dict_to_string(hint)
nic = self._dict_to_string(nic)
block_device_mapping = self._dict_to_string(block_device_mapping)
_response_type = responses.ServerResponse
return self._process_command()
def show_server(self, server_id):
_cmd = 'show'
_response_type = responses.ServerResponse
return self._process_command()
def list_servers(self):
_cmd = 'list'
_response_type = responses.ServerListResponse
return self._process_command()

View File

@ -0,0 +1,29 @@
"""
Copyright 2013 Rackspace
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 cloudcafe.common.models.configuration import ConfigSectionInterface
class NovaCLI_Config(ConfigSectionInterface):
SECTION_NAME = 'nova_cli'
@property
def insecure(self):
return self.get_boolean('insecure', True)
@property
def os_auth_system(self):
return self.get('os_auth_system')

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
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.
"""

View File

@ -0,0 +1,51 @@
from cloudcafe.openstackcli.common.models.extensions import \
ResponseExtensionType, SingleAttributeResponseExtension
# Extensions defined here are registered in this list
extensions = []
class OS_DCF_show(SingleAttributeResponseExtension):
__extends__ = 'ServerResponse'
key_name = 'OS-DCF:diskConfig'
attr_name = 'disk_config'
class ConfigDrive(SingleAttributeResponseExtension):
__extends__ = 'ServerResponse'
key_name = 'config_drive'
attr_name = 'config_drive'
class OS_EXT_STS_show(object):
__metaclass__ = ResponseExtensionType
__extends__ = 'ServerResponse'
_prefix = 'OS-EXT-STS'
_sub_attr_map = {
'OS-EXT-STS:power_state': 'power_state',
'OS-EXT-STS:task_state': 'task_state',
'OS-EXT-STS:vm_state': 'vm_state'}
def extend(cls, obj, **kwargs):
if obj.__class__.__name__ not in cls.__extends__:
return obj
for kw_name, attr_name in cls._sub_attr_map.items():
setattr(obj, attr_name, kwargs.get(kw_name, None))
return obj
class OS_EXT_STS_list(object):
__metaclass__ = ResponseExtensionType
__extends__ = '_ServerListItem'
_sub_attr_map = {
'Power State': 'power_state',
'Task State': 'task_state'}
def extend(cls, obj, **kwargs):
if obj.__class__.__name__ not in cls.__extends__:
return obj
for kw_name, attr_name in cls._sub_attr_map.items():
setattr(obj, attr_name, kwargs.get(kw_name, None))
return obj

View File

@ -0,0 +1,80 @@
# Used by models that inherit from an *Extensible* model
from cloudcafe.openstackcli.novacli.models.extensions import extensions
from cloudcafe.openstackcli.common.models.responses import (
BaseExtensibleModel, BasePrettyTableResponseModel,
BasePrettyTableResponseListModel)
class ServerResponse(BasePrettyTableResponseModel):
def __init__(
self, status=None, updated=None, key_name=None, image=None,
host_id=None, flavor=None, id_=None, user_id=None, name=None,
admin_pass=None, tenant_id=None, created_at=None, access_ipv4=None,
access_ipv6=None, progress=None, metadata=None,
private_network=None, public_network=None, **kwargs):
super(ServerResponse, self).__init__(**kwargs)
self.status = status
self.updated = updated
self.key_name = key_name
self.image = image
self.host_id = host_id
self.flavor = flavor
self.id_ = id_
self.user_id = user_id
self.name = name
self.admin_pass = admin_pass
self.tenant_id = tenant_id
self.created_at = created_at
self.access_ipv4 = access_ipv4
self.access_ipv6 = access_ipv6
self.progress = progress
self.metadata = metadata
self.private_network = private_network
self.public_network = public_network
@classmethod
def _prettytable_str_to_obj(cls, prettytable_string):
kwdict = cls._property_value_table_to_dict(prettytable_string)
kwmap = {
'id_': 'id',
'private_network': 'private network',
'public_network': 'public network',
'host_id': 'hostId',
'access_ipv4': 'accessIPv4',
'access_ipv6': 'accessIPv6',
'admin_pass': 'adminPass'}
kwdict = cls._apply_kwmap(kwmap, kwdict)
return ServerResponse(**kwdict)
class _ServerListItem(BaseExtensibleModel):
def __init__(
self, id_=None, name=None, status=None, networks=None, **kwargs):
super(_ServerListItem, self).__init__(**kwargs)
self.id_ = id_
self.name = name
self.status = status
self.networks = networks
class ServerListResponse(BasePrettyTableResponseListModel):
@classmethod
def _prettytable_str_to_obj(cls, prettytable_string):
server_list_response = ServerListResponse()
datatuple = cls._load_prettytable_string(prettytable_string)
for datadict in datatuple:
kwmap = {
'id_': 'ID',
'name': 'Name',
'status': 'Status',
'networks': 'Networks'}
kwdict = cls._apply_kwmap(kwmap, datadict)
server_list_response.append(_ServerListItem(**kwdict))
return server_list_response

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
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.
"""

View File

@ -0,0 +1,199 @@
import unittest
from cloudcafe.openstackcli.novacli.client import NovaCLI
class NovaCLI_InitializeClientWithAllArguments(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.novacli = NovaCLI(
os_cache=True, timings=True, timeout=30,
os_username='fake_username', os_password='fake_password',
os_tenant_name='FakeTenantName', os_tenant_id='1234567',
os_auth_url='os-auth-url', os_region_name='region_name',
os_auth_system='auth_system', service_type='service-type',
volume_service_name='vol serv name', endpoint_type='endpoint_type',
os_compute_api_version='v111', os_cacert='cert_here',
insecure=True, bypass_url='bypass_url')
cls.base_cmd = cls.novacli.base_cmd()
def test_os_cache(self):
self.assertIn('--os-cache', self.base_cmd)
def test_timings(self):
self.assertIn('--timings', self.base_cmd)
def test_timeout(self):
self.assertIn('--timeout 30', self.base_cmd)
def test_os_username(self):
self.assertIn('--os-username fake_username', self.base_cmd)
def test_os_password(self):
self.assertIn('--os-password fake_password', self.base_cmd)
def test_os_tenant_name(self):
self.assertIn('--os-tenant-name FakeTenantName', self.base_cmd)
def test_os_tenant_id(self):
self.assertIn('--os-tenant-id 1234567', self.base_cmd)
def test_os_auth_url(self):
self.assertIn('--os-auth-url os-auth-url', self.base_cmd)
def test_os_region_name(self):
self.assertIn('--os-region-name region_name', self.base_cmd)
def test_os_auth_system(self):
self.assertIn('--os-auth-system auth_system', self.base_cmd)
def test_service_type(self):
self.assertIn('--service-type service-type', self.base_cmd)
def test_volume_service_name(self):
self.assertIn('--volume-service-name vol serv name', self.base_cmd)
def test_endpoint_type(self):
self.assertIn('--endpoint-type endpoint_type', self.base_cmd)
def test_os_compute_api_version(self):
self.assertIn('--os-compute-api-version v111', self.base_cmd)
def test_os_cacert(self):
self.assertIn('--os-cacert cert_here', self.base_cmd)
def test_insecure(self):
self.assertIn('--insecure', self.base_cmd)
def test_bypass_url(self):
self.assertIn('--bypass-url bypass_url', self.base_cmd)
def test_no_arguments_positive(self):
novacli = NovaCLI()
self.assertEquals(novacli.base_cmd().strip(), 'nova')
class NovaCLI_CommandSerializationTests_CreateServer(unittest.TestCase):
@classmethod
def setUpClass(cls):
class FakeResponse(object):
def __init__(self, cmd):
self.command = cmd
self.standard_out = "fake standard out"
cls.novacli = NovaCLI()
cls.novacli.run_command = lambda x: FakeResponse(x)
cls.command = cls.novacli.create_server(
name='fake name',
no_service_net=True,
no_public=True,
disk_config='auto',
flavor='fake flavor',
image='fake image',
boot_volume='fake boot volume',
snapshot='fake snapshot',
num_instances='55',
key_name='SomeKeyName',
user_data='SomeUserData',
availability_zone='SomeAvailabilityZone',
security_groups='SomeSecurityGroups',
swap=100, # Just so that there's an int in this test.
config_drive='/dev/sda',
image_with={'fake_image_with_key': 'fake_image_with_value'},
meta={
'fake_meta_key1': 'fake_meta_value1',
'fake_meta_key2': 'fake_meta_value2'},
file_={'dst-path': 'src-path'},
block_device_mapping={'dev-name': 'mapping'},
block_device={'bdkey': 'bdvalue'},
ephemeral={'size': 'SomeSize', 'format': 'SomeFormat'},
hint={'HintKey': 'HintValue'},
nic={
'net-id': 'Some-net-uuid',
'port-id': 'Some-port-uuid',
'v4-fixed-ip': 'Some-ip-addr'}).command
def test_no_arguments(self):
r = self.novacli.create_server("")
self.assertEqual(r.command.strip(), "nova boot")
def test_name(self):
self.assertIn("fake name", self.command)
def test_no_service_net(self):
self.assertIn("--no-service-net", self.command)
def test_no_public(self):
self.assertIn("--no-public", self.command)
def test_disk_config(self):
self.assertIn("--disk-config auto", self.command)
def test_flavor(self):
self.assertIn("--flavor fake flavor", self.command)
def test_image(self):
self.assertIn("--image fake image", self.command)
def test_boot_volume(self):
self.assertIn("--boot-volume fake boot volume", self.command)
def test_snapshot(self):
self.assertIn("--snapshot fake snapshot", self.command)
def test_num_instances(self):
self.assertIn("--num-instances 55", self.command)
def test_key_name(self):
self.assertIn("--key-name SomeKeyName", self.command)
def test_user_data(self):
self.assertIn("--user-data SomeUserData", self.command)
def test_availability_zone(self):
self.assertIn("--availability-zone SomeAvailabilityZone", self.command)
def test_security_groups(self):
self.assertIn("--security-groups SomeSecurityGroups", self.command)
def test_swap(self):
self.assertIn("--swap 100", self.command)
def test_config_drive(self):
self.assertIn("--config-drive /dev/sda", self.command)
def test_image_with(self):
self.assertIn(
"--image-with 'fake_image_with_key'='fake_image_with_value'",
self.command)
def test_meta(self):
self.assertIn(
"--meta 'fake_meta_key1'='fake_meta_value1'", self.command)
self.assertIn(
"--meta 'fake_meta_key2'='fake_meta_value2'", self.command)
def test_file(self):
self.assertIn("--file 'dst-path'='src-path'", self.command)
def test_block_device_mapping(self):
self.assertIn(
"--block-device-mapping 'dev-name'='mapping'",
self.command)
def test_block_device(self):
self.assertIn("--block-device 'bdkey'='bdvalue'", self.command)
def test_ephemeral(self):
self.assertIn(
"--ephemeral 'format'='SomeFormat' 'size'='SomeSize' ",
self.command)
def test_hint(self):
self.assertIn("--hint 'HintKey'='HintValue'", self.command)
def test_nic(self):
self.assertIn(
"--nic 'port-id'='Some-port-uuid' 'net-id'='Some-net-uuid' "
"'v4-fixed-ip'='Some-ip-addr'", self.command)

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
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.
"""