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:
Kyrylo Romanenko 2015-10-09 17:13:26 +03:00
parent 6bcb28b319
commit 5cf8c484a9
5 changed files with 297 additions and 1 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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')

View File

@ -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: