Adds Ironic test_baremetal_basic_ops scenario test

Adds an Ironic scenario test that validates a full instance
boot using Ironic.  In addition to verifying the Nova instance
boots and has connectivity, it monitors power and state transitions
on the Ironic side.  It currently validates orchestration of the pxe_ssh
driver but the goal would be to support other drivers, and test them
conditionally based on the driver associated with the configured Ironic
node.

Change-Id: I7a98ab9c771fe17387dfb591df5a40d27194a5c8
This commit is contained in:
Adam Gandelman 2014-03-20 18:23:18 -07:00
parent eb667156a0
commit 4a48a603f4
7 changed files with 313 additions and 6 deletions

View File

@ -101,14 +101,32 @@
# Options defined in tempest.config
#
# Catalog type of the baremetal provisioning service. (string
# Catalog type of the baremetal provisioning service (string
# value)
#catalog_type=baremetal
# Whether the Ironic nova-compute driver is enabled (boolean
# value)
#driver_enabled=false
# The endpoint type to use for the baremetal provisioning
# service. (string value)
# service (string value)
#endpoint_type=publicURL
# Timeout for Ironic node to completely provision (integer
# value)
#active_timeout=300
# Timeout for association of Nova instance and Ironic node
# (integer value)
#association_timeout=10
# Timeout for Ironic power transitions. (integer value)
#power_timeout=20
# Timeout for unprovisioning an Ironic node. (integer value)
#unprovision_timeout=20
[boto]

View File

@ -13,6 +13,7 @@ python-novaclient>=2.17.0
python-neutronclient>=2.3.4,<3
python-cinderclient>=1.0.6
python-heatclient>=0.2.3
python-ironicclient
python-saharaclient>=0.6.0
python-swiftclient>=1.6
testresources>=0.2.4

View File

@ -17,6 +17,7 @@
import cinderclient.client
import glanceclient
import heatclient.client
import ironicclient.client
import keystoneclient.exceptions
import keystoneclient.v2_0.client
import neutronclient.v2_0.client
@ -456,6 +457,7 @@ class OfficialClientManager(manager.Manager):
NOVACLIENT_VERSION = '2'
CINDERCLIENT_VERSION = '1'
HEATCLIENT_VERSION = '1'
IRONICCLIENT_VERSION = '1'
def __init__(self, username, password, tenant_name):
# FIXME(andreaf) Auth provider for client_type 'official' is
@ -465,6 +467,7 @@ class OfficialClientManager(manager.Manager):
# super cares for credentials validation
super(OfficialClientManager, self).__init__(
username=username, password=password, tenant_name=tenant_name)
self.baremetal_client = self._get_baremetal_client()
self.compute_client = self._get_compute_client(username,
password,
tenant_name)
@ -485,6 +488,22 @@ class OfficialClientManager(manager.Manager):
password,
tenant_name)
def _get_roles(self):
keystone_admin = self._get_identity_client(
CONF.identity.admin_username,
CONF.identity.admin_password,
CONF.identity.admin_tenant_name)
username = self.credentials['username']
tenant_name = self.credentials['tenant_name']
user_id = keystone_admin.users.find(name=username).id
tenant_id = keystone_admin.tenants.find(name=tenant_name).id
roles = keystone_admin.roles.roles_for_user(
user=user_id, tenant=tenant_id)
return [r.name for r in roles]
def _get_compute_client(self, username, password, tenant_name):
# Novaclient will not execute operations for anyone but the
# identified user, so a new client needs to be created for
@ -606,6 +625,34 @@ class OfficialClientManager(manager.Manager):
auth_url=auth_url,
insecure=dscv)
def _get_baremetal_client(self):
# ironic client is currently intended to by used by admin users
roles = self._get_roles()
if CONF.identity.admin_role not in roles:
return None
auth_url = CONF.identity.uri
api_version = self.IRONICCLIENT_VERSION
insecure = CONF.identity.disable_ssl_certificate_validation
service_type = CONF.baremetal.catalog_type
endpoint_type = CONF.baremetal.endpoint_type
creds = {
'os_username': self.credentials['username'],
'os_password': self.credentials['password'],
'os_tenant_name': self.credentials['tenant_name']
}
try:
return ironicclient.client.get_client(
api_version=api_version,
os_auth_url=auth_url,
insecure=insecure,
os_service_type=service_type,
os_endpoint_type=endpoint_type,
**creds)
except keystoneclient.exceptions.EndpointNotFound:
return None
def _get_network_client(self):
# The intended configuration is for the network client to have
# admin privileges and indicate for whom resources are being

View File

@ -818,13 +818,29 @@ baremetal_group = cfg.OptGroup(name='baremetal',
BaremetalGroup = [
cfg.StrOpt('catalog_type',
default='baremetal',
help="Catalog type of the baremetal provisioning service."),
help="Catalog type of the baremetal provisioning service"),
cfg.BoolOpt('driver_enabled',
default=False,
help="Whether the Ironic nova-compute driver is enabled"),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the baremetal provisioning "
"service."),
"service"),
cfg.IntOpt('active_timeout',
default=300,
help="Timeout for Ironic node to completely provision"),
cfg.IntOpt('association_timeout',
default=10,
help="Timeout for association of Nova instance and Ironic "
"node"),
cfg.IntOpt('power_timeout',
default=20,
help="Timeout for Ironic power transitions."),
cfg.IntOpt('unprovision_timeout',
default=20,
help="Timeout for unprovisioning an Ironic node.")
]
cli_group = cfg.OptGroup(name='cli', title="cli Configuration Options")

View File

@ -19,6 +19,7 @@ import os
import six
import subprocess
from ironicclient import exc as ironic_exceptions
import netaddr
from neutronclient.common import exceptions as exc
from novaclient import exceptions as nova_exceptions
@ -71,6 +72,7 @@ class OfficialClientTest(tempest.test.BaseTestCase):
username, password, tenant_name)
cls.compute_client = cls.manager.compute_client
cls.image_client = cls.manager.image_client
cls.baremetal_client = cls.manager.baremetal_client
cls.identity_client = cls.manager.identity_client
cls.network_client = cls.manager.network_client
cls.volume_client = cls.manager.volume_client
@ -283,7 +285,7 @@ class OfficialClientTest(tempest.test.BaseTestCase):
return rules
def create_server(self, client=None, name=None, image=None, flavor=None,
create_kwargs={}):
wait=True, create_kwargs={}):
if client is None:
client = self.compute_client
if name is None:
@ -318,7 +320,8 @@ class OfficialClientTest(tempest.test.BaseTestCase):
server = client.servers.create(name, image, flavor, **create_kwargs)
self.assertEqual(server.name, name)
self.set_resource(name, server)
self.status_timeout(client.servers, server.id, 'ACTIVE')
if wait:
self.status_timeout(client.servers, server.id, 'ACTIVE')
# The instance retrieved on creation is missing network
# details, necessitating retrieval after it becomes active to
# ensure correct details.
@ -439,6 +442,80 @@ class OfficialClientTest(tempest.test.BaseTestCase):
LOG.debug("image:%s" % self.image)
class BaremetalScenarioTest(OfficialClientTest):
@classmethod
def setUpClass(cls):
super(BaremetalScenarioTest, cls).setUpClass()
if (not CONF.service_available.ironic or
not CONF.baremetal.driver_enabled):
msg = 'Ironic not available or Ironic compute driver not enabled'
raise cls.skipException(msg)
# use an admin client manager for baremetal client
username, password, tenant = cls.admin_credentials()
manager = clients.OfficialClientManager(username, password, tenant)
cls.baremetal_client = manager.baremetal_client
# allow any issues obtaining the node list to raise early
cls.baremetal_client.node.list()
def _node_state_timeout(self, node_id, state_attr,
target_states, timeout=10, interval=1):
if not isinstance(target_states, list):
target_states = [target_states]
def check_state():
node = self.get_node(node_id=node_id)
if getattr(node, state_attr) in target_states:
return True
return False
if not tempest.test.call_until_true(
check_state, timeout, interval):
msg = ("Timed out waiting for node %s to reach %s state(s) %s" %
(node_id, state_attr, target_states))
raise exceptions.TimeoutException(msg)
def wait_provisioning_state(self, node_id, state, timeout):
self._node_state_timeout(
node_id=node_id, state_attr='provision_state',
target_states=state, timeout=timeout)
def wait_power_state(self, node_id, state):
self._node_state_timeout(
node_id=node_id, state_attr='power_state',
target_states=state, timeout=CONF.baremetal.power_timeout)
def wait_node(self, instance_id):
"""Waits for a node to be associated with instance_id."""
def _get_node():
node = None
try:
node = self.get_node(instance_id=instance_id)
except ironic_exceptions.HTTPNotFound:
pass
return node is not None
if not tempest.test.call_until_true(
_get_node, CONF.baremetal.association_timeout, 1):
msg = ('Timed out waiting to get Ironic node by instance id %s'
% instance_id)
raise exceptions.TimeoutException(msg)
def get_node(self, node_id=None, instance_id=None):
if node_id:
return self.baremetal_client.node.get(node_id)
elif instance_id:
return self.baremetal_client.node.get_by_instance_uuid(instance_id)
def get_ports(self, node_id):
ports = []
for port in self.baremetal_client.node.list_ports(node_id):
ports.append(self.baremetal_client.port.get(port.uuid))
return ports
class NetworkScenarioTest(OfficialClientTest):
"""
Base class for network scenario tests

View File

@ -0,0 +1,147 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 tempest import config
from tempest.openstack.common import log as logging
from tempest.scenario import manager
from tempest import test
CONF = config.CONF
LOG = logging.getLogger(__name__)
# power/provision states as of icehouse
class PowerStates(object):
"""Possible power states of an Ironic node."""
POWER_ON = 'power on'
POWER_OFF = 'power off'
REBOOT = 'rebooting'
SUSPEND = 'suspended'
class ProvisionStates(object):
"""Possible provision states of an Ironic node."""
NOSTATE = None
INIT = 'initializing'
ACTIVE = 'active'
BUILDING = 'building'
DEPLOYWAIT = 'wait call-back'
DEPLOYING = 'deploying'
DEPLOYFAIL = 'deploy failed'
DEPLOYDONE = 'deploy complete'
DELETING = 'deleting'
DELETED = 'deleted'
ERROR = 'error'
class BaremetalBasicOptsPXESSH(manager.BaremetalScenarioTest):
"""
This smoke test tests the pxe_ssh Ironic driver. It follows this basic
set of operations:
* Creates a keypair
* Boots an instance using the keypair
* Monitors the associated Ironic node for power and
expected state transitions
* Validates Ironic node's driver_info has been properly
updated
* Validates Ironic node's port data has been properly updated
* Verifies SSH connectivity using created keypair via fixed IP
* Associates a floating ip
* Verifies SSH connectivity using created keypair via floating IP
* Deletes instance
* Monitors the associated Ironic node for power and
expected state transitions
"""
def add_keypair(self):
self.keypair = self.create_keypair()
def add_floating_ip(self):
floating_ip = self.compute_client.floating_ips.create()
self.instance.add_floating_ip(floating_ip)
return floating_ip.ip
def verify_connectivity(self, ip=None):
if ip:
dest = self.get_remote_client(ip)
else:
dest = self.get_remote_client(self.instance)
dest.validate_authentication()
def validate_driver_info(self):
f_id = self.instance.flavor['id']
flavor_extra = self.compute_client.flavors.get(f_id).get_keys()
driver_info = self.node.driver_info
self.assertEqual(driver_info['pxe_deploy_kernel'],
flavor_extra['baremetal:deploy_kernel_id'])
self.assertEqual(driver_info['pxe_deploy_ramdisk'],
flavor_extra['baremetal:deploy_ramdisk_id'])
self.assertEqual(driver_info['pxe_image_source'],
self.instance.image['id'])
def validate_ports(self):
for port in self.get_ports(self.node.uuid):
n_port_id = port.extra['vif_port_id']
n_port = self.network_client.show_port(n_port_id)['port']
self.assertEqual(n_port['device_id'], self.instance.id)
self.assertEqual(n_port['mac_address'], port.address)
def boot_instance(self):
create_kwargs = {
'key_name': self.keypair.id
}
self.instance = self.create_server(
wait=False, create_kwargs=create_kwargs)
self.set_resource('instance', self.instance)
self.wait_node(self.instance.id)
self.node = self.get_node(instance_id=self.instance.id)
self.wait_power_state(self.node.uuid, PowerStates.POWER_ON)
self.wait_provisioning_state(
self.node.uuid,
[ProvisionStates.DEPLOYWAIT, ProvisionStates.ACTIVE],
timeout=15)
self.wait_provisioning_state(self.node.uuid, ProvisionStates.ACTIVE,
timeout=CONF.baremetal.active_timeout)
self.status_timeout(
self.compute_client.servers, self.instance.id, 'ACTIVE')
self.node = self.get_node(instance_id=self.instance.id)
self.instance = self.compute_client.servers.get(self.instance.id)
def terminate_instance(self):
self.instance.delete()
self.remove_resource('instance')
self.wait_power_state(self.node.uuid, PowerStates.POWER_OFF)
self.wait_provisioning_state(
self.node.uuid,
ProvisionStates.NOSTATE,
timeout=CONF.baremetal.unprovision_timeout)
@test.services('baremetal', 'compute', 'image', 'network')
def test_baremetal_server_ops(self):
self.add_keypair()
self.boot_instance()
self.validate_driver_info()
self.validate_ports()
self.verify_connectivity()
floating_ip = self.add_floating_ip()
self.verify_connectivity(ip=floating_ip)
self.terminate_instance()

View File

@ -93,6 +93,7 @@ def services(*args, **kwargs):
service_list = {
'compute': CONF.service_available.nova,
'image': CONF.service_available.glance,
'baremetal': CONF.service_available.ironic,
'volume': CONF.service_available.cinder,
'orchestration': CONF.service_available.heat,
# NOTE(mtreinish) nova-network will provide networking functionality