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:
parent
eb667156a0
commit
4a48a603f4
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
147
tempest/scenario/test_baremetal_basic_ops.py
Normal file
147
tempest/scenario/test_baremetal_basic_ops.py
Normal 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()
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user