Add OSTF tests for Ironic using ironicclient
Check Ironic services: - ironic-api on controller node; - ironic-conductor on ironic conductor node. Operate Ironic nodes via ironicclient api: - Create Ironic node with fake driver; - Update Ironic node properties; - Show and check updated node properties; - Delete Ironic node. List Ironic entities: - List chassis; - List drivers; - List nodes; - List ports. Implements: blueprint ironic-ostf-tests Change-Id: I90fafede030a243efac3c9eb8d840531b25d0d03 Co-Authored-by: Anton Arefiev <aarefiev@mirantis.com>
This commit is contained in:
parent
6bcb28b319
commit
5cf8c484a9
|
@ -156,6 +156,22 @@ def cleanup(cluster_deployment_info):
|
|||
except Exception:
|
||||
LOG.warning(traceback.format_exc())
|
||||
|
||||
if 'ironic' in cluster_deployment_info:
|
||||
try:
|
||||
ironic_client = manager._get_ironic_client()
|
||||
if ironic_client is not None:
|
||||
nodes = ironic_client.node.list()
|
||||
for n in nodes:
|
||||
if "NodeTest" in ironic_client.node.extra.items():
|
||||
try:
|
||||
LOG.info('Start nodes deletion.')
|
||||
ironic_client.node.delete(n.uuid)
|
||||
except Exception as exc:
|
||||
LOG.debug(exc)
|
||||
except Exception as exc:
|
||||
LOG.warning('Something wrong with ironic client. '
|
||||
'Exception: {0}'.format(exc))
|
||||
|
||||
instances_id = []
|
||||
servers = manager._get_compute_client().servers.list()
|
||||
floating_ips = manager._get_compute_client().floating_ips.list()
|
||||
|
|
|
@ -434,6 +434,22 @@ def register_heat_opts(conf):
|
|||
conf.register_opt(opt, group='heat')
|
||||
|
||||
|
||||
ironic_group = cfg.OptGroup(name='ironic',
|
||||
title='Bare Metal Service Options')
|
||||
|
||||
IronicConfig = [
|
||||
cfg.StrOpt('online_conductors',
|
||||
default=[],
|
||||
help="Ironic online conductors"),
|
||||
]
|
||||
|
||||
|
||||
def register_ironic_opts(conf):
|
||||
conf.register_group(ironic_group)
|
||||
for opt in IronicConfig:
|
||||
conf.register_opt(opt, group='ironic')
|
||||
|
||||
|
||||
def process_singleton(cls):
|
||||
"""Wrapper for classes... To be instantiated only one time per process."""
|
||||
instances = {}
|
||||
|
@ -498,6 +514,7 @@ class FileConfig(object):
|
|||
register_heat_opts(cfg.CONF)
|
||||
register_sahara_opts(cfg.CONF)
|
||||
register_fuel_opts(cfg.CONF)
|
||||
register_ironic_opts(cfg.CONF)
|
||||
self.compute = cfg.CONF.compute
|
||||
self.identity = cfg.CONF.identity
|
||||
self.network = cfg.CONF.network
|
||||
|
@ -507,6 +524,7 @@ class FileConfig(object):
|
|||
self.heat = cfg.CONF.heat
|
||||
self.sahara = cfg.CONF.sahara
|
||||
self.fuel = cfg.CONF.fuel
|
||||
self.ironic = cfg.CONF.ironic
|
||||
|
||||
|
||||
class ConfigGroup(object):
|
||||
|
@ -549,6 +567,7 @@ class NailgunConfig(object):
|
|||
sahara = ConfigGroup(SaharaConfig)
|
||||
heat = ConfigGroup(HeatConfig)
|
||||
fuel = ConfigGroup(FuelConf)
|
||||
ironic = ConfigGroup(IronicConfig)
|
||||
|
||||
def __init__(self, parse=True):
|
||||
LOG.info('INITIALIZING NAILGUN CONFIG')
|
||||
|
@ -706,6 +725,15 @@ class NailgunConfig(object):
|
|||
data)
|
||||
self.compute.ceph_nodes = ceph_nodes
|
||||
|
||||
online_ironic = filter(
|
||||
lambda node: 'ironic' in node['roles']
|
||||
and node['online'] is True, data)
|
||||
self.ironic.online_conductors = []
|
||||
for node in online_ironic:
|
||||
self.ironic.online_conductors.append(node['ip'])
|
||||
LOG.info('Online Ironic conductors\' ips are {0}'.format(
|
||||
self.ironic.online_conductors))
|
||||
|
||||
def _parse_meta(self):
|
||||
api_url = '/api/clusters/%s' % self.cluster_id
|
||||
data = self.req_session.get(self.nailgun_url + api_url).json()
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
# Copyright 2015 Mirantis, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from fuel_health.common.ssh import Client as SSHClient
|
||||
from fuel_health import exceptions
|
||||
import fuel_health.nmanager
|
||||
import fuel_health.test
|
||||
|
||||
from ironicclient.common import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IronicTest(fuel_health.nmanager.NovaNetworkScenarioTest):
|
||||
"""Provide access to the python-ironicclient for calling Ironic API."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup Ironic client and credentials."""
|
||||
super(IronicTest, cls).setUpClass()
|
||||
if cls.manager.clients_initialized:
|
||||
cls.usr = cls.config.compute.controller_node_ssh_user
|
||||
cls.pwd = cls.config.compute.controller_node_ssh_password
|
||||
cls.key = cls.config.compute.path_to_private_key
|
||||
cls.timeout = cls.config.compute.ssh_timeout
|
||||
if not cls.ironic_client:
|
||||
LOG.warning('Ironic client was not initialized')
|
||||
|
||||
def node_create(self, **kwargs):
|
||||
"""Create a new node."""
|
||||
node = self.ironic_client.node.create(**kwargs)
|
||||
self.addCleanup(self.node_delete, node)
|
||||
return node
|
||||
|
||||
def node_delete(self, node):
|
||||
"""Delete particular node."""
|
||||
try:
|
||||
self.ironic_client.node.delete(node.uuid)
|
||||
except exceptions.NotFound:
|
||||
LOG.debug(traceback.format_exc())
|
||||
|
||||
def node_update(self, node, prop, value_prop, row='properties'):
|
||||
"""Add property with value to node properties."""
|
||||
args = ['{0}/{1}={2}'.format(row, prop, value_prop)]
|
||||
patch = utils.args_array_to_patch('add', args)
|
||||
return self.ironic_client.node.update(node.uuid, patch)
|
||||
|
||||
def node_show(self, node):
|
||||
"""Show detailed information about a node."""
|
||||
if node.instance_uuid:
|
||||
n = self.ironic_client.node.get_by_instance_uuid(
|
||||
node.instance_uuid)
|
||||
else:
|
||||
n = self.ironic_client.node.get(node.uuid)
|
||||
return n
|
||||
|
||||
def _run_ssh_cmd_with_exit_code(self, host, cmd):
|
||||
"""Open SSH session with host and execute command.
|
||||
|
||||
Fail if exit code != 0
|
||||
"""
|
||||
try:
|
||||
sshclient = SSHClient(host, self.usr, self.pwd,
|
||||
key_filename=self.key, timeout=self.timeout)
|
||||
return sshclient.exec_command(cmd)
|
||||
except Exception:
|
||||
LOG.debug(traceback.format_exc())
|
||||
self.fail("{0} command failed.".format(cmd))
|
||||
|
||||
def check_service_availability(self, nodes, cmd, expected, timeout=30):
|
||||
"""Check running processes on nodes.
|
||||
|
||||
At least one controller should run ironic-api process.
|
||||
At least one Ironic node should run ironic-conductor process.
|
||||
"""
|
||||
def check_services():
|
||||
for node in nodes:
|
||||
output = self._run_ssh_cmd_with_exit_code(node, cmd)
|
||||
LOG.debug(output)
|
||||
if expected in output:
|
||||
return True
|
||||
return False
|
||||
|
||||
if not fuel_health.test.call_until_true(check_services, 30, timeout):
|
||||
self.fail('Failed to discover service {0} '
|
||||
'within specified timeout'.format(expected))
|
||||
return True
|
||||
|
||||
def list_nodes(self):
|
||||
"""Get list of nodes."""
|
||||
return self.ironic_client.node.list()
|
||||
|
||||
def list_ports(self):
|
||||
"""Get list of ports."""
|
||||
return self.ironic_client.port.list()
|
||||
|
||||
def list_drivers(self):
|
||||
"""Get list of drivers."""
|
||||
return self.ironic_client.driver.list()
|
||||
|
||||
def list_chassis(self):
|
||||
"""Get list of chassis."""
|
||||
return self.ironic_client.chassis.list()
|
|
@ -0,0 +1,134 @@
|
|||
# Copyright 2015 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from fuel_health.common.utils.data_utils import rand_name
|
||||
from fuel_health import ironicmanager
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IronicSmokeTests(ironicmanager.IronicTest):
|
||||
"""TestClass contains tests to check that Ironic nodes are operable
|
||||
|
||||
Special requirements:
|
||||
1. A controller's IP address should be specified.
|
||||
2. An ironic-conductor's IP address should be specified.
|
||||
3. SSH user credentials for the controller and the ironic-conductor
|
||||
should be specified in the controller_node_ssh_user parameter
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(IronicSmokeTests, cls).setUpClass()
|
||||
cls.controllers = cls.config.compute.online_controllers
|
||||
cls.conductors = cls.config.ironic.online_conductors
|
||||
if not cls.controllers:
|
||||
cls.skipTest('There are no Controller nodes.')
|
||||
if not cls.conductors:
|
||||
cls.skipTest('There are no Ironic Conductor nodes.')
|
||||
|
||||
def test_001_ironic_services(self):
|
||||
"""Check that Ironic services are running
|
||||
Target component: Ironic
|
||||
|
||||
Scenario:
|
||||
1. Check that ironic-api service is running on controller node.
|
||||
2. Check that ironic-conductor service is running on Ironic node.
|
||||
Duration: 60 s.
|
||||
Deployment tags: Ironic
|
||||
Available since release: 2015.1.0-8.0
|
||||
"""
|
||||
|
||||
# Step 1
|
||||
expected = u'/usr/bin/ironic-api'
|
||||
cmd = 'pgrep -la ironic-api'
|
||||
fail_msg = 'Ironic-api service is not running.'
|
||||
action = 'checking ironic-api service'
|
||||
self.verify(60, self.check_service_availability, 1, fail_msg, action,
|
||||
self.controllers, cmd, expected)
|
||||
# Step 2
|
||||
expected = u'/usr/bin/ironic-conductor'
|
||||
cmd = 'pgrep -la ironic'
|
||||
fail_msg = 'Ironic-conductor service is not running.'
|
||||
action = 'checking ironic-conductor service'
|
||||
self.verify(60, self.check_service_availability, 2, fail_msg, action,
|
||||
self.conductors, cmd, expected)
|
||||
|
||||
def test_002_ironic_node_actions(self):
|
||||
"""Check that Ironic can operate nodes
|
||||
Target component: Ironic
|
||||
|
||||
Scenario:
|
||||
1. Create Ironic node with fake driver.
|
||||
2. Update Ironic node properties.
|
||||
3. Show and check updated node properties.
|
||||
4. Delete Ironic node.
|
||||
Duration: 60 s.
|
||||
Deployment tags: Ironic
|
||||
Available since release: 2015.1.0-8.0
|
||||
"""
|
||||
# Step 1
|
||||
fail_msg = "Error creating node."
|
||||
self.node = self.verify(20, self.node_create, 1, fail_msg,
|
||||
'Node creation', driver='fake',
|
||||
extra={'NodeTest': ''})
|
||||
LOG.debug(self.node)
|
||||
# Step 2
|
||||
prop = rand_name("ostf-prop")
|
||||
value_prop = rand_name("prop-value")
|
||||
fail_msg = "Can't update node with properties."
|
||||
self.node = self.verify(20, self.node_update, 2, fail_msg,
|
||||
'Updating node', self.node, prop, value_prop)
|
||||
LOG.debug(self.node)
|
||||
# Step 3
|
||||
fail_msg = "Can't show node properties."
|
||||
self.node = self.verify(20, self.node_show, 3, fail_msg,
|
||||
'Showing node', self.node)
|
||||
LOG.debug(self.node)
|
||||
for p, v in self.node.properties.items():
|
||||
self.verify(5, self.assertTrue, 3, "Can't check node property.",
|
||||
'Checking node property', prop in p)
|
||||
self.verify(5, self.assertTrue, 3, "Can't check property value.",
|
||||
'Checking property value', value_prop in v)
|
||||
# Step 4
|
||||
fail_msg = "Can't delete node."
|
||||
self.verify(20, self.node_delete, 4, fail_msg, 'Deleting node',
|
||||
self.node)
|
||||
|
||||
def test_003_ironic_list_entities(self):
|
||||
"""List Ironic entities
|
||||
Target component: Ironic
|
||||
|
||||
Scenario:
|
||||
1. List chassis.
|
||||
2. List drivers.
|
||||
3. List nodes.
|
||||
4. List ports.
|
||||
Duration: 80 s.
|
||||
Deployment tags: Ironic
|
||||
Available since release: 2015.1.0-8.0
|
||||
"""
|
||||
fail_msg = "Can't list chassis."
|
||||
self.verify(20, self.list_chassis, 1, fail_msg, 'Chassis list')
|
||||
|
||||
fail_msg = "Can't list drivers."
|
||||
self.verify(20, self.list_drivers, 2, fail_msg, 'Drivers list')
|
||||
|
||||
fail_msg = "Can't list nodes."
|
||||
self.verify(20, self.list_nodes, 3, fail_msg, 'Nodes list')
|
||||
|
||||
fail_msg = "Can't list ports."
|
||||
self.verify(20, self.list_ports, 4, fail_msg, 'Ports list')
|
|
@ -179,7 +179,7 @@ def _get_cluster_attrs(cluster_id, token=None):
|
|||
|
||||
additional_depl_tags = set()
|
||||
|
||||
comp_names = ['murano', 'sahara', 'heat', 'ceilometer']
|
||||
comp_names = ['murano', 'sahara', 'heat', 'ceilometer', 'ironic']
|
||||
|
||||
def processor(comp):
|
||||
if comp in comp_names:
|
||||
|
|
Loading…
Reference in New Issue