Merge "Remove ironic_tempest_plugin/ directory"

This commit is contained in:
Zuul 2018-01-11 00:46:43 +00:00 committed by Gerrit Code Review
commit 7ac39e4029
44 changed files with 15 additions and 5571 deletions

View File

@ -100,8 +100,7 @@ pygments_style = 'sphinx'
# A list of glob-style patterns that should be excluded when looking for # A list of glob-style patterns that should be excluded when looking for
# source files. They are matched against the source file names relative to the # source files. They are matched against the source file names relative to the
# source directory, using slashes as directory separators on all platforms. # source directory, using slashes as directory separators on all platforms.
exclude_patterns = ['api/ironic_tempest_plugin.*', exclude_patterns = ['api/ironic.drivers.modules.ansible.playbooks.*',
'api/ironic.drivers.modules.ansible.playbooks.*',
'api/ironic.tests.*'] 'api/ironic.tests.*']
# Ignore the following warning: WARNING: while setting up extension # Ignore the following warning: WARNING: while setting up extension

View File

@ -0,0 +1,5 @@
The ironic tempest plugin code has been moved to a new dedicated repository:
openstack/ironic-tempest-plugin/
Please update any dependencies to use this new repository for doing ironic tempest tests.

View File

@ -1,15 +0,0 @@
=====================
Ironic tempest plugin
=====================
This directory contains Tempest tests to cover the Ironic project,
as well as a plugin to automatically load these tests into tempest.
See the tempest plugin documentation for information about creating
a plugin, stable API interface, TempestPlugin class interface, plugin
structure, and how to use plugins:
https://docs.openstack.org/tempest/latest/plugin.html
See the Ironic documentation for information about how to run the
tempest tests:
https://docs.openstack.org/ironic/latest/contributor/dev-quickstart.html#running-tempest-tests

View File

@ -1,53 +0,0 @@
# 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.
from tempest import clients
from tempest.common import credentials_factory as common_creds
from tempest import config
from ironic_tempest_plugin.services.baremetal.v1.json.baremetal_client import \
BaremetalClient
CONF = config.CONF
ADMIN_CREDS = None
class Manager(clients.Manager):
def __init__(self,
credentials=None):
"""Initialization of Manager class.
Setup service client and make it available for test cases.
:param credentials: type Credentials or TestResources
"""
if credentials is None:
global ADMIN_CREDS
if ADMIN_CREDS is None:
ADMIN_CREDS = common_creds.get_configured_admin_credentials()
credentials = ADMIN_CREDS
super(Manager, self).__init__(credentials)
default_params_with_timeout_values = {
'build_interval': CONF.compute.build_interval,
'build_timeout': CONF.compute.build_timeout
}
default_params_with_timeout_values.update(self.default_params)
self.baremetal_client = BaremetalClient(
self.auth_provider,
CONF.baremetal.catalog_type,
CONF.identity.region,
endpoint_type=CONF.baremetal.endpoint_type,
**default_params_with_timeout_values)

View File

@ -1,33 +0,0 @@
# 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.
def get_node(client, node_id=None, instance_uuid=None):
"""Get a node by its identifier or instance UUID.
If both node_id and instance_uuid specified, node_id will be used.
:param client: an instance of tempest plugin BaremetalClient.
:param node_id: identifier (UUID or name) of the node.
:param instance_uuid: UUID of the instance.
:returns: the requested node.
:raises: AssertionError, if neither node_id nor instance_uuid was provided
"""
assert node_id or instance_uuid, ('Either node or instance identifier '
'has to be provided.')
if node_id:
_, body = client.show_node(node_id)
return body
elif instance_uuid:
_, body = client.show_node_by_instance_uuid(instance_uuid)
if body['nodes']:
return body['nodes'][0]

View File

@ -1,112 +0,0 @@
# 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 six
from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.common import utils
CONF = config.CONF
def _determine_and_check_timeout_interval(timeout, default_timeout,
interval, default_interval):
if timeout is None:
timeout = default_timeout
if interval is None:
interval = default_interval
if (not isinstance(timeout, six.integer_types) or
not isinstance(interval, six.integer_types) or
timeout < 0 or interval < 0):
raise AssertionError(
'timeout and interval should be >= 0 or None, current values are: '
'%(timeout)s, %(interval)s respectively. If timeout and/or '
'interval are None, the default_timeout and default_interval are '
'used, and they should be integers >= 0, current values are: '
'%(default_timeout)s, %(default_interval)s respectively.' % dict(
timeout=timeout, interval=interval,
default_timeout=default_timeout,
default_interval=default_interval)
)
return timeout, interval
def wait_for_bm_node_status(client, node_id, attr, status, timeout=None,
interval=None):
"""Waits for a baremetal node attribute to reach given status.
:param client: an instance of tempest plugin BaremetalClient.
:param node_id: identifier of the node.
:param attr: node's API-visible attribute to check status of.
:param status: desired status. Can be a list of statuses.
:param timeout: the timeout after which the check is considered as failed.
Defaults to client.build_timeout.
:param interval: an interval between show_node calls for status check.
Defaults to client.build_interval.
The client should have a show_node(node_id) method to get the node.
"""
timeout, interval = _determine_and_check_timeout_interval(
timeout, client.build_timeout, interval, client.build_interval)
if not isinstance(status, list):
status = [status]
def is_attr_in_status():
node = utils.get_node(client, node_id=node_id)
if node[attr] in status:
return True
return False
if not test_utils.call_until_true(is_attr_in_status, timeout,
interval):
message = ('Node %(node_id)s failed to reach %(attr)s=%(status)s '
'within the required time (%(timeout)s s).' %
{'node_id': node_id,
'attr': attr,
'status': status,
'timeout': timeout})
caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
raise lib_exc.TimeoutException(message)
def wait_node_instance_association(client, instance_uuid, timeout=None,
interval=None):
"""Waits for a node to be associated with instance_id.
:param client: an instance of tempest plugin BaremetalClient.
:param instance_uuid: UUID of the instance.
:param timeout: the timeout after which the check is considered as failed.
Defaults to CONF.baremetal.association_timeout.
:param interval: an interval between show_node calls for status check.
Defaults to client.build_interval.
"""
timeout, interval = _determine_and_check_timeout_interval(
timeout, CONF.baremetal.association_timeout,
interval, client.build_interval)
def is_some_node_associated():
node = utils.get_node(client, instance_uuid=instance_uuid)
return node is not None
if not test_utils.call_until_true(is_some_node_associated, timeout,
interval):
msg = ('Timed out waiting to get Ironic node by instance UUID '
'%(instance_uuid)s within the required time (%(timeout)s s).'
% {'instance_uuid': instance_uuid, 'timeout': timeout})
raise lib_exc.TimeoutException(msg)

View File

@ -1,116 +0,0 @@
# Copyright 2015 NEC Corporation
# 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.
from oslo_config import cfg
from tempest import config # noqa
service_option = cfg.BoolOpt('ironic',
default=False,
help='Whether or not Ironic is expected to be '
'available')
baremetal_group = cfg.OptGroup(name='baremetal',
title='Baremetal provisioning service options',
help='When enabling baremetal tests, Nova '
'must be configured to use the Ironic '
'driver. The following parameters for the '
'[compute] section must be disabled: '
'console_output, interface_attach, '
'live_migration, pause, rescue, resize, '
'shelve, snapshot, and suspend')
baremetal_features_group = cfg.OptGroup(
name='baremetal_feature_enabled',
title="Enabled Baremetal Service Features")
BaremetalGroup = [
cfg.StrOpt('catalog_type',
default='baremetal',
help="Catalog type of the baremetal provisioning service"),
cfg.StrOpt('driver',
default='fake',
help="Driver name which Ironic uses"),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the baremetal provisioning"
" service"),
cfg.IntOpt('deploywait_timeout',
default=15,
help="Timeout for Ironic node to reach the "
"wait-callback state after powering on."),
cfg.IntOpt('active_timeout',
default=300,
help="Timeout for Ironic node to completely provision"),
cfg.IntOpt('association_timeout',
default=30,
help="Timeout for association of Nova instance and Ironic "
"node"),
cfg.IntOpt('power_timeout',
default=60,
help="Timeout for Ironic power transitions."),
cfg.IntOpt('unprovision_timeout',
default=300,
help="Timeout for unprovisioning an Ironic node. "
"Takes longer since Kilo as Ironic performs an extra "
"step in Node cleaning."),
cfg.StrOpt('min_microversion',
help="Lower version of the test target microversion range. "
"The format is 'X.Y', where 'X' and 'Y' are int values. "
"Tempest selects tests based on the range between "
"min_microversion and max_microversion. "
"If both values are None, Tempest avoids tests which "
"require a microversion."),
cfg.StrOpt('max_microversion',
default='latest',
help="Upper version of the test target microversion range. "
"The format is 'X.Y', where 'X' and 'Y' are int values. "
"Tempest selects tests based on the range between "
"min_microversion and max_microversion. "
"If both values are None, Tempest avoids tests which "
"require a microversion."),
cfg.BoolOpt('use_provision_network',
default=False,
help="Whether the Ironic/Neutron tenant isolation is enabled"),
cfg.StrOpt('whole_disk_image_ref',
help="UUID of the wholedisk image to use in the tests."),
cfg.StrOpt('whole_disk_image_url',
help="An http link to the wholedisk image to use in the "
"tests."),
cfg.StrOpt('whole_disk_image_checksum',
help="An MD5 checksum of the image."),
cfg.StrOpt('partition_image_ref',
help="UUID of the partitioned image to use in the tests."),
cfg.ListOpt('enabled_drivers',
default=['fake', 'pxe_ipmitool', 'agent_ipmitool'],
help="List of Ironic enabled drivers."),
cfg.ListOpt('enabled_hardware_types',
default=['ipmi'],
help="List of Ironic enabled hardware types."),
cfg.IntOpt('adjusted_root_disk_size_gb',
min=0,
help="Ironic adjusted disk size to use in the standalone tests "
"as instance_info/root_gb value."),
]
BaremetalFeaturesGroup = [
cfg.BoolOpt('ipxe_enabled',
default=True,
help="Defines if IPXE is enabled"),
]

View File

@ -1,559 +0,0 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2013 IBM Corp.
# 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.
# NOTE(soliosg) Do not edit this file. It will only stay temporarily
# in ironic, while QA refactors the tempest.scenario interface. This
# file was copied from openstack/tempest/tempest/scenario/manager.py,
# openstack/tempest commit: 82a278e88c9e9f9ba49f81c1f8dba0bca7943daf
import subprocess
from oslo_log import log
from oslo_utils import netutils
from tempest.common import compute
from tempest.common.utils.linux import remote_client
from tempest.common.utils import net_utils
from tempest.common import waiters
from tempest import config
from tempest import exceptions
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions as lib_exc
import tempest.test
CONF = config.CONF
LOG = log.getLogger(__name__)
class ScenarioTest(tempest.test.BaseTestCase):
"""Base class for scenario tests. Uses tempest own clients. """
credentials = ['primary']
@classmethod
def setup_clients(cls):
super(ScenarioTest, cls).setup_clients()
# Clients (in alphabetical order)
cls.flavors_client = cls.os_primary.flavors_client
cls.compute_floating_ips_client = (
cls.os_primary.compute_floating_ips_client)
if CONF.service_available.glance:
# Check if glance v1 is available to determine which client to use.
if CONF.image_feature_enabled.api_v1:
cls.image_client = cls.os_primary.image_client
elif CONF.image_feature_enabled.api_v2:
cls.image_client = cls.os_primary.image_client_v2
else:
raise lib_exc.InvalidConfiguration(
'Either api_v1 or api_v2 must be True in '
'[image-feature-enabled].')
# Compute image client
cls.compute_images_client = cls.os_primary.compute_images_client
cls.keypairs_client = cls.os_primary.keypairs_client
# Nova security groups client
cls.compute_security_groups_client = (
cls.os_primary.compute_security_groups_client)
cls.compute_security_group_rules_client = (
cls.os_primary.compute_security_group_rules_client)
cls.servers_client = cls.os_primary.servers_client
cls.interface_client = cls.os_primary.interfaces_client
# Neutron network client
cls.networks_client = cls.os_primary.networks_client
cls.ports_client = cls.os_primary.ports_client
cls.routers_client = cls.os_primary.routers_client
cls.subnets_client = cls.os_primary.subnets_client
cls.floating_ips_client = cls.os_primary.floating_ips_client
cls.security_groups_client = cls.os_primary.security_groups_client
cls.security_group_rules_client = (
cls.os_primary.security_group_rules_client)
if CONF.volume_feature_enabled.api_v2:
cls.volumes_client = cls.os_primary.volumes_v2_client
cls.snapshots_client = cls.os_primary.snapshots_v2_client
else:
cls.volumes_client = cls.os_primary.volumes_client
cls.snapshots_client = cls.os_primary.snapshots_client
# ## Test functions library
#
# The create_[resource] functions only return body and discard the
# resp part which is not used in scenario tests
def _create_port(self, network_id, client=None, namestart='port-quotatest',
**kwargs):
if not client:
client = self.ports_client
name = data_utils.rand_name(namestart)
result = client.create_port(
name=name,
network_id=network_id,
**kwargs)
self.assertIsNotNone(result, 'Unable to allocate port')
port = result['port']
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
client.delete_port, port['id'])
return port
def create_keypair(self, client=None):
if not client:
client = self.keypairs_client
name = data_utils.rand_name(self.__class__.__name__)
# We don't need to create a keypair by pubkey in scenario
body = client.create_keypair(name=name)
self.addCleanup(client.delete_keypair, name)
return body['keypair']
def create_server(self, name=None, image_id=None, flavor=None,
validatable=False, wait_until='ACTIVE',
clients=None, **kwargs):
"""Wrapper utility that returns a test server.
This wrapper utility calls the common create test server and
returns a test server. The purpose of this wrapper is to minimize
the impact on the code of the tests already using this
function.
"""
# NOTE(jlanoux): As a first step, ssh checks in the scenario
# tests need to be run regardless of the run_validation and
# validatable parameters and thus until the ssh validation job
# becomes voting in CI. The test resources management and IP
# association are taken care of in the scenario tests.
# Therefore, the validatable parameter is set to false in all
# those tests. In this way create_server just return a standard
# server and the scenario tests always perform ssh checks.
# Needed for the cross_tenant_traffic test:
if clients is None:
clients = self.os_primary
if name is None:
name = data_utils.rand_name(self.__class__.__name__ + "-server")
vnic_type = CONF.network.port_vnic_type
# If vnic_type is configured create port for
# every network
if vnic_type:
ports = []
create_port_body = {'binding:vnic_type': vnic_type,
'namestart': 'port-smoke'}
if kwargs:
# Convert security group names to security group ids
# to pass to create_port
if 'security_groups' in kwargs:
security_groups = \
clients.security_groups_client.list_security_groups(
).get('security_groups')
sec_dict = dict([(s['name'], s['id'])
for s in security_groups])
sec_groups_names = [s['name'] for s in kwargs.pop(
'security_groups')]
security_groups_ids = [sec_dict[s]
for s in sec_groups_names]
if security_groups_ids:
create_port_body[
'security_groups'] = security_groups_ids
networks = kwargs.pop('networks', [])
else:
networks = []
# If there are no networks passed to us we look up
# for the project's private networks and create a port.
# The same behaviour as we would expect when passing
# the call to the clients with no networks
if not networks:
networks = clients.networks_client.list_networks(
**{'router:external': False, 'fields': 'id'})['networks']
# It's net['uuid'] if networks come from kwargs
# and net['id'] if they come from
# clients.networks_client.list_networks
for net in networks:
net_id = net.get('uuid', net.get('id'))
if 'port' not in net:
port = self._create_port(network_id=net_id,
client=clients.ports_client,
**create_port_body)
ports.append({'port': port['id']})
else:
ports.append({'port': net['port']})
if ports:
kwargs['networks'] = ports
self.ports = ports
tenant_network = self.get_tenant_network()
body, servers = compute.create_test_server(
clients,
tenant_network=tenant_network,
wait_until=wait_until,
name=name, flavor=flavor,
image_id=image_id, **kwargs)
self.addCleanup(waiters.wait_for_server_termination,
clients.servers_client, body['id'])
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
clients.servers_client.delete_server, body['id'])
server = clients.servers_client.show_server(body['id'])['server']
return server
def get_remote_client(self, ip_address, username=None, private_key=None):
"""Get a SSH client to a remote server
@param ip_address the server floating or fixed IP address to use
for ssh validation
@param username name of the Linux account on the remote server
@param private_key the SSH private key to use
@return a RemoteClient object
"""
if username is None:
username = CONF.validation.image_ssh_user
# Set this with 'keypair' or others to log in with keypair or
# username/password.
if CONF.validation.auth_method == 'keypair':
password = None
if private_key is None:
private_key = self.keypair['private_key']
else:
password = CONF.validation.image_ssh_password
private_key = None
linux_client = remote_client.RemoteClient(ip_address, username,
pkey=private_key,
password=password)
try:
linux_client.validate_authentication()
except Exception as e:
message = ('Initializing SSH connection to %(ip)s failed. '
'Error: %(error)s' % {'ip': ip_address,
'error': e})
caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
LOG.exception(message)
self._log_console_output()
raise
return linux_client
def _log_console_output(self, servers=None):
if not CONF.compute_feature_enabled.console_output:
LOG.debug('Console output not supported, cannot log')
return
if not servers:
servers = self.servers_client.list_servers()
servers = servers['servers']
for server in servers:
try:
console_output = self.servers_client.get_console_output(
server['id'])['output']
LOG.debug('Console output for %s\nbody=\n%s',
server['id'], console_output)
except lib_exc.NotFound:
LOG.debug("Server %s disappeared(deleted) while looking "
"for the console log", server['id'])
def rebuild_server(self, server_id, image=None,
preserve_ephemeral=False, wait=True,
rebuild_kwargs=None):
if image is None:
image = CONF.compute.image_ref
rebuild_kwargs = rebuild_kwargs or {}
LOG.debug("Rebuilding server (id: %s, image: %s, preserve eph: %s)",
server_id, image, preserve_ephemeral)
self.servers_client.rebuild_server(
server_id=server_id, image_ref=image,
preserve_ephemeral=preserve_ephemeral,
**rebuild_kwargs)
if wait:
waiters.wait_for_server_status(self.servers_client,
server_id, 'ACTIVE')
def ping_ip_address(self, ip_address, should_succeed=True,
ping_timeout=None, mtu=None):
timeout = ping_timeout or CONF.validation.ping_timeout
cmd = ['ping', '-c1', '-w1']
if mtu:
cmd += [
# don't fragment
'-M', 'do',
# ping receives just the size of ICMP payload
'-s', str(net_utils.get_ping_payload_size(mtu, 4))
]
cmd.append(ip_address)
def ping():
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.communicate()
return (proc.returncode == 0) == should_succeed
caller = test_utils.find_test_caller()
LOG.debug('%(caller)s begins to ping %(ip)s in %(timeout)s sec and the'
' expected result is %(should_succeed)s', {
'caller': caller, 'ip': ip_address, 'timeout': timeout,
'should_succeed':
'reachable' if should_succeed else 'unreachable'
})
result = test_utils.call_until_true(ping, timeout, 1)
LOG.debug('%(caller)s finishes ping %(ip)s in %(timeout)s sec and the '
'ping result is %(result)s', {
'caller': caller, 'ip': ip_address, 'timeout': timeout,
'result': 'expected' if result else 'unexpected'
})
return result
def check_vm_connectivity(self, ip_address,
username=None,
private_key=None,
should_connect=True,
mtu=None):
"""Check server connectivity
:param ip_address: server to test against
:param username: server's ssh username
:param private_key: server's ssh private key to be used
:param should_connect: True/False indicates positive/negative test
positive - attempt ping and ssh
negative - attempt ping and fail if succeed
:param mtu: network MTU to use for connectivity validation
:raises: AssertError if the result of the connectivity check does
not match the value of the should_connect param
"""
if should_connect:
msg = "Timed out waiting for %s to become reachable" % ip_address
else:
msg = "ip address %s is reachable" % ip_address
self.assertTrue(self.ping_ip_address(ip_address,
should_succeed=should_connect,
mtu=mtu),
msg=msg)
if should_connect:
# no need to check ssh for negative connectivity
self.get_remote_client(ip_address, username, private_key)
def create_floating_ip(self, thing, pool_name=None):
"""Create a floating IP and associates to a server on Nova"""
if not pool_name:
pool_name = CONF.network.floating_network_name
floating_ip = (self.compute_floating_ips_client.
create_floating_ip(pool=pool_name)['floating_ip'])
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.compute_floating_ips_client.delete_floating_ip,
floating_ip['id'])
self.compute_floating_ips_client.associate_floating_ip_to_server(
floating_ip['ip'], thing['id'])
return floating_ip
def create_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
private_key=None):
ssh_client = self.get_remote_client(ip_address,
private_key=private_key)
if dev_name is not None:
ssh_client.make_fs(dev_name)
ssh_client.exec_command('sudo mount /dev/%s %s' % (dev_name,
mount_path))
cmd_timestamp = 'sudo sh -c "date > %s/timestamp; sync"' % mount_path
ssh_client.exec_command(cmd_timestamp)
timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
% mount_path)
if dev_name is not None:
ssh_client.exec_command('sudo umount %s' % mount_path)
return timestamp
def get_server_ip(self, server):
"""Get the server fixed or floating IP.
Based on the configuration we're in, return a correct ip
address for validating that a guest is up.
"""
if CONF.validation.connect_method == 'floating':
# The tests calling this method don't have a floating IP
# and can't make use of the validation resources. So the
# method is creating the floating IP there.
return self.create_floating_ip(server)['ip']
elif CONF.validation.connect_method == 'fixed':
# Determine the network name to look for based on config or creds
# provider network resources.
if CONF.validation.network_for_ssh:
addresses = server['addresses'][
CONF.validation.network_for_ssh]
else:
creds_provider = self._get_credentials_provider()
net_creds = creds_provider.get_primary_creds()
network = getattr(net_creds, 'network', None)
addresses = (server['addresses'][network['name']]
if network else [])
for address in addresses:
if (address['version'] == CONF.validation.ip_version_for_ssh
and address['OS-EXT-IPS:type'] == 'fixed'):
return address['addr']
raise exceptions.ServerUnreachable(server_id=server['id'])
else:
raise lib_exc.InvalidConfiguration()
def _get_router(self, client=None, tenant_id=None):
"""Retrieve a router for the given tenant id.
If a public router has been configured, it will be returned.
If a public router has not been configured, but a public
network has, a tenant router will be created and returned that
routes traffic to the public network.
"""
if not client:
client = self.routers_client
if not tenant_id:
tenant_id = client.tenant_id
router_id = CONF.network.public_router_id
network_id = CONF.network.public_network_id
if router_id:
body = client.show_router(router_id)
return body['router']
elif network_id:
router = self._create_router(client, tenant_id)
kwargs = {'external_gateway_info': dict(network_id=network_id)}
router = client.update_router(router['id'], **kwargs)['router']
return router
else:
raise Exception("Neither of 'public_router_id' or "
"'public_network_id' has been defined.")
def _create_router(self, client=None, tenant_id=None,
namestart='router-smoke'):
if not client:
client = self.routers_client
if not tenant_id:
tenant_id = client.tenant_id
name = data_utils.rand_name(namestart)
result = client.create_router(name=name,
admin_state_up=True,
tenant_id=tenant_id)
router = result['router']
self.assertEqual(router['name'], name)
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
client.delete_router,
router['id'])
return router
class NetworkScenarioTest(ScenarioTest):
"""Base class for network scenario tests.
This class provide helpers for network scenario tests, using the neutron
API. Helpers from ancestor which use the nova network API are overridden
with the neutron API.
This Class also enforces using Neutron instead of novanetwork.
Subclassed tests will be skipped if Neutron is not enabled
"""
credentials = ['primary', 'admin']
@classmethod
def skip_checks(cls):
super(NetworkScenarioTest, cls).skip_checks()
if not CONF.service_available.neutron:
raise cls.skipException('Neutron not available')
def _create_network(self, networks_client=None,
tenant_id=None,
namestart='network-smoke-',
port_security_enabled=True):
if not networks_client:
networks_client = self.networks_client
if not tenant_id:
tenant_id = networks_client.tenant_id
name = data_utils.rand_name(namestart)
network_kwargs = dict(name=name, tenant_id=tenant_id)
# Neutron disables port security by default so we have to check the
# config before trying to create the network with port_security_enabled
if CONF.network_feature_enabled.port_security:
network_kwargs['port_security_enabled'] = port_security_enabled
result = networks_client.create_network(**network_kwargs)
network = result['network']
self.assertEqual(network['name'], name)
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
networks_client.delete_network,
network['id'])
return network
def _get_server_port_id_and_ip4(self, server, ip_addr=None):
ports = self.os_admin.ports_client.list_ports(
device_id=server['id'], fixed_ip=ip_addr)['ports']
# A port can have more than one IP address in some cases.
# If the network is dual-stack (IPv4 + IPv6), this port is associated
# with 2 subnets
p_status = ['ACTIVE']
# NOTE(vsaienko) With Ironic, instances live on separate hardware
# servers. Neutron does not bind ports for Ironic instances, as a
# result the port remains in the DOWN state.
# TODO(vsaienko) remove once bug: #1599836 is resolved.
if getattr(CONF.service_available, 'ironic', False):
p_status.append('DOWN')
port_map = [(p["id"], fxip["ip_address"])
for p in ports
for fxip in p["fixed_ips"]
if netutils.is_valid_ipv4(fxip["ip_address"])
and p['status'] in p_status]
inactive = [p for p in ports if p['status'] != 'ACTIVE']
if inactive:
LOG.warning("Instance has ports that are not ACTIVE: %s", inactive)
self.assertNotEqual(0, len(port_map),
"No IPv4 addresses found in: %s" % ports)
self.assertEqual(len(port_map), 1,
"Found multiple IPv4 addresses: %s. "
"Unable to determine which port to target."
% port_map)
return port_map[0]
def create_floating_ip(self, thing, external_network_id=None,
port_id=None, client=None):
"""Create a floating IP and associates to a resource/port on Neutron"""
if not external_network_id:
external_network_id = CONF.network.public_network_id
if not client:
client = self.floating_ips_client
if not port_id:
port_id, ip4 = self._get_server_port_id_and_ip4(thing)
else:
ip4 = None
result = client.create_floatingip(
floating_network_id=external_network_id,
port_id=port_id,
tenant_id=thing['tenant_id'],
fixed_ip_address=ip4
)
floating_ip = result['floatingip']
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
client.delete_floatingip,
floating_ip['id'])
return floating_ip

View File

@ -1,46 +0,0 @@
# Copyright 2015 NEC Corporation
# 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 os
from tempest import config
from tempest.test_discover import plugins
from ironic_tempest_plugin import config as project_config
_opts = [
(project_config.baremetal_group, project_config.BaremetalGroup),
(project_config.baremetal_features_group,
project_config.BaremetalFeaturesGroup)
]
class IronicTempestPlugin(plugins.TempestPlugin):
def load_tests(self):
base_path = os.path.split(os.path.dirname(
os.path.abspath(__file__)))[0]
test_dir = "ironic_tempest_plugin/tests"
full_test_dir = os.path.join(base_path, test_dir)
return full_test_dir, base_path
def register_opts(self, conf):
conf.register_opt(project_config.service_option,
group='service_available')
for group, option in _opts:
config.register_opt_group(conf, group, option)
def get_opt_lists(self):
return [(group.name, option) for group, option in _opts]

View File

@ -1,264 +0,0 @@
# 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 functools
from oslo_serialization import jsonutils as json
from six.moves import http_client
from six.moves.urllib import parse as urllib
from tempest.lib.common import api_version_utils
from tempest.lib.common import rest_client
# NOTE(vsaienko): concurrent tests work because they are launched in
# separate processes so global variables are not shared among them.
BAREMETAL_MICROVERSION = None
def set_baremetal_api_microversion(baremetal_microversion):
global BAREMETAL_MICROVERSION
BAREMETAL_MICROVERSION = baremetal_microversion
def reset_baremetal_api_microversion():
global BAREMETAL_MICROVERSION
BAREMETAL_MICROVERSION = None
def handle_errors(f):
"""A decorator that allows to ignore certain types of errors."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
param_name = 'ignore_errors'
ignored_errors = kwargs.get(param_name, tuple())
if param_name in kwargs:
del kwargs[param_name]
try:
return f(*args, **kwargs)
except ignored_errors:
# Silently ignore errors
pass
return wrapper
class BaremetalClient(rest_client.RestClient):
"""Base Tempest REST client for Ironic API."""
api_microversion_header_name = 'X-OpenStack-Ironic-API-Version'
uri_prefix = ''
def get_headers(self):
headers = super(BaremetalClient, self).get_headers()
if BAREMETAL_MICROVERSION:
headers[self.api_microversion_header_name] = BAREMETAL_MICROVERSION
return headers
def request(self, *args, **kwargs):
resp, resp_body = super(BaremetalClient, self).request(*args, **kwargs)
if (BAREMETAL_MICROVERSION and
BAREMETAL_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
api_version_utils.assert_version_header_matches_request(
self.api_microversion_header_name,
BAREMETAL_MICROVERSION,
resp)
return resp, resp_body
def serialize(self, object_dict):
"""Serialize an Ironic object."""
return json.dumps(object_dict)
def deserialize(self, object_str):
"""Deserialize an Ironic object."""
return json.loads(object_str)
def _get_uri(self, resource_name, uuid=None, permanent=False):
"""Get URI for a specific resource or object.
:param resource_name: The name of the REST resource, e.g., 'nodes'.
:param uuid: The unique identifier of an object in UUID format.
:returns: Relative URI for the resource or object.
"""
prefix = self.uri_prefix if not permanent else ''
return '{pref}/{res}{uuid}'.format(pref=prefix,
res=resource_name,
uuid='/%s' % uuid if uuid else '')
def _make_patch(self, allowed_attributes, **kwargs):
"""Create a JSON patch according to RFC 6902.
:param allowed_attributes: An iterable object that contains a set of
allowed attributes for an object.
:param **kwargs: Attributes and new values for them.
:returns: A JSON path that sets values of the specified attributes to
the new ones.
"""
def get_change(kwargs, path='/'):
for name, value in kwargs.items():
if isinstance(value, dict):
for ch in get_change(value, path + '%s/' % name):
yield ch
else:
if value is None:
yield {'path': path + name,
'op': 'remove'}
else:
yield {'path': path + name,
'value': value,
'op': 'replace'}
patch = [ch for ch in get_change(kwargs)
if ch['path'].lstrip('/') in allowed_attributes]
return patch
def _list_request(self, resource, permanent=False, headers=None,
extra_headers=False, **kwargs):
"""Get the list of objects of the specified type.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param headers: List of headers to use in request.
:param extra_headers: Specify whether to use headers.
:param **kwargs: Parameters for the request.
:returns: A tuple with the server response and deserialized JSON list
of objects
"""
uri = self._get_uri(resource, permanent=permanent)
if kwargs:
uri += "?%s" % urllib.urlencode(kwargs)
resp, body = self.get(uri, headers=headers,
extra_headers=extra_headers)
self.expected_success(http_client.OK, resp.status)
return resp, self.deserialize(body)
def _show_request(self,
resource,
uuid=None,
permanent=False,
**kwargs):
"""Gets a specific object of the specified type.
:param uuid: Unique identifier of the object in UUID format.
:returns: Serialized object as a dictionary.
"""
if 'uri' in kwargs:
uri = kwargs['uri']
else:
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
resp, body = self.get(uri)
self.expected_success(http_client.OK, resp.status)
return resp, self.deserialize(body)
def _create_request(self, resource, object_dict):
"""Create an object of the specified type.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param object_dict: A Python dict that represents an object of the
specified type.
:returns: A tuple with the server response and the deserialized created
object.
"""
body = self.serialize(object_dict)
uri = self._get_uri(resource)
resp, body = self.post(uri, body=body)
self.expected_success(http_client.CREATED, resp.status)
return resp, self.deserialize(body)
def _create_request_no_response_body(self, resource, object_dict):
"""Create an object of the specified type.
Do not expect any body in the response.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param object_dict: A Python dict that represents an object of the
specified type.
:returns: The server response.
"""
body = self.serialize(object_dict)
uri = self._get_uri(resource)
resp, body = self.post(uri, body=body)
self.expected_success(http_client.NO_CONTENT, resp.status)
return resp
def _delete_request(self, resource, uuid):
"""Delete specified object.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param uuid: The unique identifier of an object in UUID format.
:returns: A tuple with the server response and the response body.
"""
uri = self._get_uri(resource, uuid)
resp, body = self.delete(uri)
self.expected_success(http_client.NO_CONTENT, resp.status)
return resp, body
def _patch_request(self, resource, uuid, patch_object):
"""Update specified object with JSON-patch.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param uuid: The unique identifier of an object in UUID format.
:returns: A tuple with the server response and the serialized patched
object.
"""
uri = self._get_uri(resource, uuid)
patch_body = json.dumps(patch_object)
resp, body = self.patch(uri, body=patch_body)
self.expected_success(http_client.OK, resp.status)
return resp, self.deserialize(body)
@handle_errors
def get_api_description(self):
"""Retrieves all versions of the Ironic API."""
return self._list_request('', permanent=True)
@handle_errors
def get_version_description(self, version='v1'):
"""Retrieves the description of the API.
:param version: The version of the API. Default: 'v1'.
:returns: Serialized description of API resources.
"""
return self._list_request(version, permanent=True)
def _put_request(self, resource, put_object):
"""Update specified object with JSON-patch."""
uri = self._get_uri(resource)
put_body = json.dumps(put_object)
resp, body = self.put(uri, body=put_body)
self.expected_success([http_client.ACCEPTED, http_client.NO_CONTENT],
resp.status)
return resp, body

View File

@ -1,641 +0,0 @@
# 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 six.moves import http_client
from ironic_tempest_plugin.services.baremetal import base
class BaremetalClient(base.BaremetalClient):
"""Base Tempest REST client for Ironic API v1."""
version = '1'
uri_prefix = 'v1'
@base.handle_errors
def list_nodes(self, **kwargs):
"""List all existing nodes."""
return self._list_request('nodes', **kwargs)
@base.handle_errors
def list_nodes_detail(self, **kwargs):
"""Detailed list of all existing nodes."""
return self._list_request('/nodes/detail', **kwargs)
@base.handle_errors
def list_chassis(self):
"""List all existing chassis."""
return self._list_request('chassis')
@base.handle_errors
def list_chassis_nodes(self, chassis_uuid):
"""List all nodes associated with a chassis."""
return self._list_request('/chassis/%s/nodes' % chassis_uuid)
@base.handle_errors
def list_ports(self, **kwargs):
"""List all existing ports."""
return self._list_request('ports', **kwargs)
@base.handle_errors
def list_portgroups(self, **kwargs):
"""List all existing port groups."""
return self._list_request('portgroups', **kwargs)
@base.handle_errors
def list_volume_connectors(self, **kwargs):
"""List all existing volume connectors."""
return self._list_request('volume/connectors', **kwargs)
@base.handle_errors
def list_volume_targets(self, **kwargs):
"""List all existing volume targets."""
return self._list_request('volume/targets', **kwargs)
@base.handle_errors
def list_node_ports(self, uuid):
"""List all ports associated with the node."""
return self._list_request('/nodes/%s/ports' % uuid)
@base.handle_errors
def list_nodestates(self, uuid):
"""List all existing states."""
return self._list_request('/nodes/%s/states' % uuid)
@base.handle_errors
def list_ports_detail(self, **kwargs):
"""Details list all existing ports."""
return self._list_request('/ports/detail', **kwargs)
@base.handle_errors
def list_drivers(self):
"""List all existing drivers."""
return self._list_request('drivers')
@base.handle_errors
def show_node(self, uuid):
"""Gets a specific node.
:param uuid: Unique identifier of the node in UUID format.
:return: Serialized node as a dictionary.
"""
return self._show_request('nodes', uuid)
@base.handle_errors
def show_node_by_instance_uuid(self, instance_uuid):
"""Gets a node associated with given instance uuid.
:param instance_uuid: Unique identifier of the instance in UUID format.
:return: Serialized node as a dictionary.
"""
uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
return self._show_request('nodes',
uuid=None,
uri=uri)
@base.handle_errors
def show_chassis(self, uuid):
"""Gets a specific chassis.
:param uuid: Unique identifier of the chassis in UUID format.
:return: Serialized chassis as a dictionary.
"""
return self._show_request('chassis', uuid)
@base.handle_errors
def show_port(self, uuid):
"""Gets a specific port.
:param uuid: Unique identifier of the port in UUID format.
:return: Serialized port as a dictionary.
"""
return self._show_request('ports', uuid)
@base.handle_errors
def show_portgroup(self, portgroup_ident):
"""Gets a specific port group.
:param portgroup_ident: Name or UUID of the port group.
:return: Serialized port group as a dictionary.
"""
return self._show_request('portgroups', portgroup_ident)
@base.handle_errors
def show_volume_connector(self, volume_connector_ident):
"""Gets a specific volume connector.
:param volume_connector_ident: UUID of the volume connector.
:return: Serialized volume connector as a dictionary.
"""
return self._show_request('volume/connectors', volume_connector_ident)
@base.handle_errors
def show_volume_target(self, volume_target_ident):
"""Gets a specific volume target.
:param volume_target_ident: UUID of the volume target.
:return: Serialized volume target as a dictionary.
"""
return self._show_request('volume/targets', volume_target_ident)
@base.handle_errors
def show_port_by_address(self, address):
"""Gets a specific port by address.
:param address: MAC address of the port.
:return: Serialized port as a dictionary.
"""
uri = '/ports/detail?address=%s' % address
return self._show_request('ports', uuid=None, uri=uri)
def show_driver(self, driver_name):
"""Gets a specific driver.
:param driver_name: Name of driver.
:return: Serialized driver as a dictionary.
"""
return self._show_request('drivers', driver_name)
@base.handle_errors
def create_node(self, chassis_id=None, **kwargs):
"""Create a baremetal node with the specified parameters.
:param chassis_id: The unique identifier of the chassis.
:param cpu_arch: CPU architecture of the node. Default: x86_64.
:param cpus: Number of CPUs. Default: 8.
:param local_gb: Disk size. Default: 1024.
:param memory_mb: Available RAM. Default: 4096.
:param driver: Driver name. Default: "fake"
:return: A tuple with the server response and the created node.
"""
node = {}
if kwargs.get('resource_class'):
node['resource_class'] = kwargs['resource_class']
node.update(
{'chassis_uuid': chassis_id,
'properties': {'cpu_arch': kwargs.get('cpu_arch', 'x86_64'),
'cpus': kwargs.get('cpus', 8),
'local_gb': kwargs.get('local_gb', 1024),
'memory_mb': kwargs.get('memory_mb', 4096)},
'driver': kwargs.get('driver', 'fake')}
)
return self._create_request('nodes', node)
@base.handle_errors
def create_chassis(self, **kwargs):
"""Create a chassis with the specified parameters.
:param description: The description of the chassis.
Default: test-chassis
:return: A tuple with the server response and the created chassis.
"""
chassis = {'description': kwargs.get('description', 'test-chassis')}
if 'uuid' in kwargs:
chassis.update({'uuid': kwargs.get('uuid')})
return self._create_request('chassis', chassis)
@base.handle_errors
def create_port(self, node_id, **kwargs):
"""Create a port with the specified parameters.
:param node_id: The ID of the node which owns the port.
:param address: MAC address of the port.
:param extra: Meta data of the port. Default: {'foo': 'bar'}.
:param uuid: UUID of the port.
:param portgroup_uuid: The UUID of a portgroup of which this port is a
member.
:param physical_network: The physical network to which the port is
attached.
:return: A tuple with the server response and the created port.
"""
port = {'extra': kwargs.get('extra', {'foo': 'bar'}),
'uuid': kwargs['uuid']}
if node_id is not None:
port['node_uuid'] = node_id
for key in ('address', 'physical_network', 'portgroup_uuid'):
if kwargs.get(key) is not None:
port[key] = kwargs[key]
return self._create_request('ports', port)
@base.handle_errors
def create_portgroup(self, node_uuid, **kwargs):
"""Create a port group with the specified parameters.
:param node_uuid: The UUID of the node which owns the port group.
:param kwargs:
address: MAC address of the port group. Optional.
extra: Meta data of the port group. Default: {'foo': 'bar'}.
name: Name of the port group. Optional.
uuid: UUID of the port group. Optional.
:return: A tuple with the server response and the created port group.
"""
portgroup = {'extra': kwargs.get('extra', {'foo': 'bar'})}
portgroup['node_uuid'] = node_uuid
if kwargs.get('address'):
portgroup['address'] = kwargs['address']
if kwargs.get('name'):
portgroup['name'] = kwargs['name']
return self._create_request('portgroups', portgroup)
@base.handle_errors
def create_volume_connector(self, node_uuid, **kwargs):
"""Create a volume connector with the specified parameters.
:param node_uuid: The UUID of the node which owns the volume connector.
:param kwargs:
type: type of the volume connector.
connector_id: connector_id of the volume connector.
uuid: UUID of the volume connector. Optional.
extra: meta data of the volume connector; a dictionary. Optional.
:return: A tuple with the server response and the created volume
connector.
"""
volume_connector = {'node_uuid': node_uuid}
for arg in ('type', 'connector_id', 'uuid', 'extra'):
if arg in kwargs:
volume_connector[arg] = kwargs[arg]
return self._create_request('volume/connectors', volume_connector)
@base.handle_errors
def create_volume_target(self, node_uuid, **kwargs):
"""Create a volume target with the specified parameters.
:param node_uuid: The UUID of the node which owns the volume target.
:param kwargs:
volume_type: type of the volume target.
volume_id: volume_id of the volume target.
boot_index: boot index of the volume target.
uuid: UUID of the volume target. Optional.
extra: meta data of the volume target; a dictionary. Optional.
properties: properties related to the type of the volume target;
a dictionary. Optional.
:return: A tuple with the server response and the created volume
target.
"""
volume_target = {'node_uuid': node_uuid}
for arg in ('volume_type', 'volume_id', 'boot_index', 'uuid', 'extra',
'properties'):
if arg in kwargs:
volume_target[arg] = kwargs[arg]
return self._create_request('volume/targets', volume_target)
@base.handle_errors
def delete_node(self, uuid):
"""Deletes a node having the specified UUID.
:param uuid: The unique identifier of the node.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('nodes', uuid)
@base.handle_errors
def delete_chassis(self, uuid):
"""Deletes a chassis having the specified UUID.
:param uuid: The unique identifier of the chassis.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('chassis', uuid)
@base.handle_errors
def delete_port(self, uuid):
"""Deletes a port having the specified UUID.
:param uuid: The unique identifier of the port.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('ports', uuid)
@base.handle_errors
def delete_portgroup(self, portgroup_ident):
"""Deletes a port group having the specified UUID or name.
:param portgroup_ident: Name or UUID of the port group.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('portgroups', portgroup_ident)
@base.handle_errors
def delete_volume_connector(self, volume_connector_ident):
"""Deletes a volume connector having the specified UUID.
:param volume_connector_ident: UUID of the volume connector.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('volume/connectors',
volume_connector_ident)
@base.handle_errors
def delete_volume_target(self, volume_target_ident):
"""Deletes a volume target having the specified UUID.
:param volume_target_ident: UUID of the volume target.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('volume/targets', volume_target_ident)
@base.handle_errors
def update_node(self, uuid, patch=None, **kwargs):
"""Update the specified node.
:param uuid: The unique identifier of the node.
:param patch: A JSON path that sets values of the specified attributes
to the new ones.
:param **kwargs: Attributes and new values for them, used only when
patch param is not set.
:return: A tuple with the server response and the updated node.
"""
node_attributes = ('properties/cpu_arch',
'properties/cpus',
'properties/local_gb',
'properties/memory_mb',
'driver',
'instance_uuid',
'resource_class')
if not patch:
patch = self._make_patch(node_attributes, **kwargs)
return self._patch_request('nodes', uuid, patch)
@base.handle_errors
def update_chassis(self, uuid, **kwargs):
"""Update the specified chassis.
:param uuid: The unique identifier of the chassis.
:return: A tuple with the server response and the updated chassis.
"""
chassis_attributes = ('description',)
patch = self._make_patch(chassis_attributes, **kwargs)
return self._patch_request('chassis', uuid, patch)
@base.handle_errors
def update_port(self, uuid, patch):
"""Update the specified port.
:param uuid: The unique identifier of the port.
:param patch: List of dicts representing json patches.
:return: A tuple with the server response and the updated port.
"""
return self._patch_request('ports', uuid, patch)
@base.handle_errors
def update_volume_connector(self, uuid, patch):
"""Update the specified volume connector.
:param uuid: The unique identifier of the volume connector.
:param patch: List of dicts representing json patches. Each dict
has keys 'path', 'op' and 'value'; to update a field.
:return: A tuple with the server response and the updated volume
connector.
"""
return self._patch_request('volume/connectors', uuid, patch)
@base.handle_errors
def update_volume_target(self, uuid, patch):
"""Update the specified volume target.
:param uuid: The unique identifier of the volume target.
:param patch: List of dicts representing json patches. Each dict
has keys 'path', 'op' and 'value'; to update a field.
:return: A tuple with the server response and the updated volume
target.
"""
return self._patch_request('volume/targets', uuid, patch)
@base.handle_errors
def set_node_power_state(self, node_uuid, state):
"""Set power state of the specified node.
:param node_uuid: The unique identifier of the node.
:param state: desired state to set (on/off/reboot).
"""
target = {'target': state}
return self._put_request('nodes/%s/states/power' % node_uuid,
target)
@base.handle_errors
def set_node_provision_state(self, node_uuid, state, configdrive=None,
clean_steps=None):
"""Set provision state of the specified node.
:param node_uuid: The unique identifier of the node.
:param state: desired state to set
(active/rebuild/deleted/inspect/manage/provide).
:param configdrive: A gzipped, base64-encoded
configuration drive string.
:param clean_steps: A list with clean steps to execute.
"""
data = {'target': state}
# NOTE (vsaienk0): Add both here if specified, do not check anything.
# API will return an error in case of invalid parameters.
if configdrive is not None:
data['configdrive'] = configdrive
if clean_steps is not None:
data['clean_steps'] = clean_steps
return self._put_request('nodes/%s/states/provision' % node_uuid,
data)
@base.handle_errors
def set_node_raid_config(self, node_uuid, target_raid_config):
"""Set raid config of the specified node.
:param node_uuid: The unique identifier of the node.
:param target_raid_config: desired RAID configuration of the node.
"""
return self._put_request('nodes/%s/states/raid' % node_uuid,
target_raid_config)
@base.handle_errors
def validate_driver_interface(self, node_uuid):
"""Get all driver interfaces of a specific node.
:param node_uuid: Unique identifier of the node in UUID format.
"""
uri = '{pref}/{res}/{uuid}/{postf}'.format(pref=self.uri_prefix,
res='nodes',
uuid=node_uuid,
postf='validate')
return self._show_request('nodes', node_uuid, uri=uri)
@base.handle_errors
def set_node_boot_device(self, node_uuid, boot_device, persistent=False):
"""Set the boot device of the specified node.
:param node_uuid: The unique identifier of the node.
:param boot_device: The boot device name.
:param persistent: Boolean value. True if the boot device will
persist to all future boots, False if not.
Default: False.
"""
request = {'boot_device': boot_device, 'persistent': persistent}
resp, body = self._put_request('nodes/%s/management/boot_device' %
node_uuid, request)
self.expected_success(http_client.NO_CONTENT, resp.status)
return body
@base.handle_errors
def get_node_boot_device(self, node_uuid):
"""Get the current boot device of the specified node.
:param node_uuid: The unique identifier of the node.
"""
path = 'nodes/%s/management/boot_device' % node_uuid
resp, body = self._list_request(path)
self.expected_success(http_client.OK, resp.status)
return body
@base.handle_errors
def get_node_supported_boot_devices(self, node_uuid):
"""Get the supported boot devices of the specified node.
:param node_uuid: The unique identifier of the node.
"""
path = 'nodes/%s/management/boot_device/supported' % node_uuid
resp, body = self._list_request(path)
self.expected_success(http_client.OK, resp.status)
return body
@base.handle_errors
def get_console(self, node_uuid):
"""Get connection information about the console.
:param node_uuid: Unique identifier of the node in UUID format.
"""
resp, body = self._show_request('nodes/states/console', node_uuid)
self.expected_success(http_client.OK, resp.status)
return resp, body
@base.handle_errors
def set_console_mode(self, node_uuid, enabled):
"""Start and stop the node console.
:param node_uuid: Unique identifier of the node in UUID format.
:param enabled: Boolean value; whether to enable or disable the
console.
"""
enabled = {'enabled': enabled}
resp, body = self._put_request('nodes/%s/states/console' % node_uuid,
enabled)
self.expected_success(http_client.ACCEPTED, resp.status)
return resp, body
@base.handle_errors
def vif_list(self, node_uuid, api_version=None):
"""Get list of attached VIFs.
:param node_uuid: Unique identifier of the node in UUID format.
:param api_version: Ironic API version to use.
"""
extra_headers = False
headers = None
if api_version is not None:
extra_headers = True
headers = {'x-openstack-ironic-api-version': api_version}
return self._list_request('nodes/%s/vifs' % node_uuid,
headers=headers,
extra_headers=extra_headers)
@base.handle_errors
def vif_attach(self, node_uuid, vif_id):
"""Attach a VIF to a node
:param node_uuid: Unique identifier of the node in UUID format.
:param vif_id: An ID representing the VIF
"""
vif = {'id': vif_id}
resp = self._create_request_no_response_body(
'nodes/%s/vifs' % node_uuid, vif)
return resp
@base.handle_errors
def vif_detach(self, node_uuid, vif_id):
"""Detach a VIF from a node
:param node_uuid: Unique identifier of the node in UUID format.
:param vif_id: An ID representing the VIF
"""
resp, body = self._delete_request('nodes/%s/vifs' % node_uuid, vif_id)
self.expected_success(http_client.NO_CONTENT, resp.status)
return resp, body
@base.handle_errors
def get_driver_properties(self, driver_name):
"""Get properties information about driver.
:param driver_name: Name of driver.
:return: tuple of response and serialized properties as a dictionary.
"""
uri = 'drivers/%s/properties' % driver_name
resp, body = self.get(uri)
self.expected_success(200, resp.status)
return resp, self.deserialize(body)
@base.handle_errors
def get_driver_logical_disk_properties(self, driver_name):
"""Get driver logical disk properties.
:param driver_name: Name of driver.
:return: tuple of response and serialized logical disk properties as
a dictionary.
"""
uri = 'drivers/%s/raid/logical_disk_properties' % driver_name
resp, body = self.get(uri)
self.expected_success(200, resp.status)
return resp, self.deserialize(body)

View File

@ -1,26 +0,0 @@
# 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 fixtures
from ironic_tempest_plugin.services.baremetal import base
class APIMicroversionFixture(fixtures.Fixture):
def __init__(self, baremetal_microversion):
self.baremetal_microversion = baremetal_microversion
def _setUp(self):
super(APIMicroversionFixture, self)._setUp()
base.set_baremetal_api_microversion(self.baremetal_microversion)
self.addCleanup(base.reset_baremetal_api_microversion)

View File

@ -1,351 +0,0 @@
# 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 functools
from tempest import config
from tempest.lib.common import api_version_utils
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions as lib_exc
from tempest import test
from ironic_tempest_plugin import clients
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
CONF = config.CONF
# NOTE(adam_g): The baremetal API tests exercise operations such as enroll
# node, power on, power off, etc. Testing against real drivers (ie, IPMI)
# will require passing driver-specific data to Tempest (addresses,
# credentials, etc). Until then, only support testing against the fake driver,
# which has no external dependencies.
SUPPORTED_DRIVERS = ['fake']
# NOTE(jroll): resources must be deleted in a specific order, this list
# defines the resource types to clean up, and the correct order.
RESOURCE_TYPES = ['port', 'portgroup', 'volume_connector', 'volume_target',
'node', 'chassis']
def creates(resource):
"""Decorator that adds resources to the appropriate cleanup list."""
def decorator(f):
@functools.wraps(f)
def wrapper(cls, *args, **kwargs):
resp, body = f(cls, *args, **kwargs)
if 'uuid' in body:
cls.created_objects[resource].add(body['uuid'])
return resp, body
return wrapper
return decorator
class BaseBaremetalTest(api_version_utils.BaseMicroversionTest,
test.BaseTestCase):
"""Base class for Baremetal API tests."""
credentials = ['admin']
@classmethod
def skip_checks(cls):
super(BaseBaremetalTest, cls).skip_checks()
if not CONF.service_available.ironic:
raise cls.skipException('Ironic is not enabled.')
if CONF.baremetal.driver not in SUPPORTED_DRIVERS:
skip_msg = ('%s skipped as Ironic driver %s is not supported for '
'testing.' %
(cls.__name__, CONF.baremetal.driver))
raise cls.skipException(skip_msg)
cfg_min_version = CONF.baremetal.min_microversion
cfg_max_version = CONF.baremetal.max_microversion
api_version_utils.check_skip_with_microversion(cls.min_microversion,
cls.max_microversion,
cfg_min_version,
cfg_max_version)
@classmethod
def setup_credentials(cls):
cls.request_microversion = (
api_version_utils.select_request_microversion(
cls.min_microversion,
CONF.baremetal.min_microversion))
cls.services_microversion = {
CONF.baremetal.catalog_type: cls.request_microversion}
super(BaseBaremetalTest, cls).setup_credentials()
@classmethod
def setup_clients(cls):
super(BaseBaremetalTest, cls).setup_clients()
cls.client = clients.Manager().baremetal_client
@classmethod
def resource_setup(cls):
super(BaseBaremetalTest, cls).resource_setup()
cls.request_microversion = (
api_version_utils.select_request_microversion(
cls.min_microversion,
CONF.baremetal.min_microversion))
cls.driver = CONF.baremetal.driver
cls.power_timeout = CONF.baremetal.power_timeout
cls.unprovision_timeout = CONF.baremetal.unprovision_timeout
cls.created_objects = {}
for resource in RESOURCE_TYPES:
cls.created_objects[resource] = set()
@classmethod
def resource_cleanup(cls):
"""Ensure that all created objects get destroyed."""
try:
for resource in RESOURCE_TYPES:
uuids = cls.created_objects[resource]
delete_method = getattr(cls.client, 'delete_%s' % resource)
for u in uuids:
delete_method(u, ignore_errors=lib_exc.NotFound)
finally:
super(BaseBaremetalTest, cls).resource_cleanup()
def _assertExpected(self, expected, actual):
"""Check if expected keys/values exist in actual response body.
Check if the expected keys and values are in the actual response body.
It will not check the keys 'created_at' and 'updated_at', since they
will always have different values. Asserts if any expected key (or
corresponding value) is not in the actual response.
Note: this method has an underscore even though it is used outside of
this class, in order to distinguish this method from the more standard
assertXYZ methods.
:param expected: dict of key-value pairs that are expected to be in
'actual' dict.
:param actual: dict of key-value pairs.
"""
for key, value in expected.items():
if key not in ('created_at', 'updated_at'):
self.assertIn(key, actual)
self.assertEqual(value, actual[key])
def setUp(self):
super(BaseBaremetalTest, self).setUp()
self.useFixture(api_microversion_fixture.APIMicroversionFixture(
self.request_microversion))
@classmethod
@creates('chassis')
def create_chassis(cls, description=None, **kwargs):
"""Wrapper utility for creating test chassis.
:param description: A description of the chassis. If not supplied,
a random value will be generated.
:return: A tuple with the server response and the created chassis.
"""
description = description or data_utils.rand_name('test-chassis')
resp, body = cls.client.create_chassis(description=description,
**kwargs)
return resp, body
@classmethod
@creates('node')
def create_node(cls, chassis_id, cpu_arch='x86', cpus=8, local_gb=10,
memory_mb=4096, resource_class=None):
"""Wrapper utility for creating test baremetal nodes.
:param chassis_id: The unique identifier of the chassis.
:param cpu_arch: CPU architecture of the node. Default: x86.
:param cpus: Number of CPUs. Default: 8.
:param local_gb: Disk size. Default: 10.
:param memory_mb: Available RAM. Default: 4096.
:param resource_class: Node resource class.
:return: A tuple with the server response and the created node.
"""
resp, body = cls.client.create_node(chassis_id, cpu_arch=cpu_arch,
cpus=cpus, local_gb=local_gb,
memory_mb=memory_mb,
driver=cls.driver,
resource_class=resource_class)
return resp, body
@classmethod
@creates('port')
def create_port(cls, node_id, address, extra=None, uuid=None,
portgroup_uuid=None, physical_network=None):
"""Wrapper utility for creating test ports.
:param node_id: The unique identifier of the node.
:param address: MAC address of the port.
:param extra: Meta data of the port. If not supplied, an empty
dictionary will be created.
:param uuid: UUID of the port.
:param portgroup_uuid: The UUID of a portgroup of which this port is a
member.
:param physical_network: The physical network to which the port is
attached.
:return: A tuple with the server response and the created port.
"""
extra = extra or {}
resp, body = cls.client.create_port(address=address, node_id=node_id,
extra=extra, uuid=uuid,
portgroup_uuid=portgroup_uuid,
physical_network=physical_network)
return resp, body
@classmethod
@creates('portgroup')
def create_portgroup(cls, node_uuid, **kwargs):
"""Wrapper utility for creating test port groups.
:param node_uuid: The unique identifier of the node.
:return: A tuple with the server response and the created port group.
"""
resp, body = cls.client.create_portgroup(node_uuid=node_uuid, **kwargs)
return resp, body
@classmethod
@creates('volume_connector')
def create_volume_connector(cls, node_uuid, **kwargs):
"""Wrapper utility for creating test volume connector.
:param node_uuid: The unique identifier of the node.
:return: A tuple with the server response and the created volume
connector.
"""
resp, body = cls.client.create_volume_connector(node_uuid=node_uuid,
**kwargs)
return resp, body
@classmethod
@creates('volume_target')
def create_volume_target(cls, node_uuid, **kwargs):
"""Wrapper utility for creating test volume target.
:param node_uuid: The unique identifier of the node.
:return: A tuple with the server response and the created volume
target.
"""
resp, body = cls.client.create_volume_target(node_uuid=node_uuid,
**kwargs)
return resp, body
@classmethod
def delete_chassis(cls, chassis_id):
"""Deletes a chassis having the specified UUID.
:param chassis_id: The unique identifier of the chassis.
:return: Server response.
"""
resp, body = cls.client.delete_chassis(chassis_id)
if chassis_id in cls.created_objects['chassis']:
cls.created_objects['chassis'].remove(chassis_id)
return resp
@classmethod
def delete_node(cls, node_id):
"""Deletes a node having the specified UUID.
:param node_id: The unique identifier of the node.
:return: Server response.
"""
resp, body = cls.client.delete_node(node_id)
if node_id in cls.created_objects['node']:
cls.created_objects['node'].remove(node_id)
return resp
@classmethod
def delete_port(cls, port_id):
"""Deletes a port having the specified UUID.
:param port_id: The unique identifier of the port.
:return: Server response.
"""
resp, body = cls.client.delete_port(port_id)
if port_id in cls.created_objects['port']:
cls.created_objects['port'].remove(port_id)
return resp
@classmethod
def delete_portgroup(cls, portgroup_ident):
"""Deletes a port group having the specified UUID or name.
:param portgroup_ident: The name or UUID of the port group.
:return: Server response.
"""
resp, body = cls.client.delete_portgroup(portgroup_ident)
if portgroup_ident in cls.created_objects['portgroup']:
cls.created_objects['portgroup'].remove(portgroup_ident)
return resp
@classmethod
def delete_volume_connector(cls, volume_connector_id):
"""Deletes a volume connector having the specified UUID.
:param volume_connector_id: The UUID of the volume connector.
:return: Server response.
"""
resp, body = cls.client.delete_volume_connector(volume_connector_id)
if volume_connector_id in cls.created_objects['volume_connector']:
cls.created_objects['volume_connector'].remove(
volume_connector_id)
return resp
@classmethod
def delete_volume_target(cls, volume_target_id):
"""Deletes a volume target having the specified UUID.
:param volume_target_id: The UUID of the volume target.
:return: Server response.
"""
resp, body = cls.client.delete_volume_target(volume_target_id)
if volume_target_id in cls.created_objects['volume_target']:
cls.created_objects['volume_target'].remove(volume_target_id)
return resp
def validate_self_link(self, resource, uuid, link):
"""Check whether the given self link formatted correctly."""
expected_link = "{base}/{pref}/{res}/{uuid}".format(
base=self.client.base_url.rstrip('/'),
pref=self.client.uri_prefix,
res=resource,
uuid=uuid)
self.assertEqual(expected_link, link)

View File

@ -1,43 +0,0 @@
# 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.lib import decorators
from ironic_tempest_plugin.tests.api.admin import base
class TestApiDiscovery(base.BaseBaremetalTest):
"""Tests for API discovery features."""
@decorators.idempotent_id('a3c27e94-f56c-42c4-8600-d6790650b9c5')
def test_api_versions(self):
_, descr = self.client.get_api_description()
expected_versions = ('v1',)
versions = [version['id'] for version in descr['versions']]
for v in expected_versions:
self.assertIn(v, versions)
@decorators.idempotent_id('896283a6-488e-4f31-af78-6614286cbe0d')
def test_default_version(self):
_, descr = self.client.get_api_description()
default_version = descr['default_version']
self.assertEqual('v1', default_version['id'])
@decorators.idempotent_id('abc0b34d-e684-4546-9728-ab7a9ad9f174')
def test_version_1_resources(self):
_, descr = self.client.get_version_description(version='v1')
expected_resources = ('nodes', 'chassis',
'ports', 'links', 'media_types')
for res in expected_resources:
self.assertIn(res, descr)

View File

@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
# 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.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import base
class TestChassis(base.BaseBaremetalTest):
"""Tests for chassis."""
@classmethod
def resource_setup(cls):
super(TestChassis, cls).resource_setup()
_, cls.chassis = cls.create_chassis()
@decorators.idempotent_id('7c5a2e09-699c-44be-89ed-2bc189992d42')
def test_create_chassis(self):
descr = data_utils.rand_name('test-chassis')
_, chassis = self.create_chassis(description=descr)
self.assertEqual(descr, chassis['description'])
@decorators.idempotent_id('cabe9c6f-dc16-41a7-b6b9-0a90c212edd5')
def test_create_chassis_unicode_description(self):
# Use a unicode string for testing:
# 'We ♡ OpenStack in Ukraine'
descr = u'В Україні ♡ OpenStack!'
_, chassis = self.create_chassis(description=descr)
self.assertEqual(descr, chassis['description'])
@decorators.idempotent_id('c84644df-31c4-49db-a307-8942881f41c0')
def test_show_chassis(self):
_, chassis = self.client.show_chassis(self.chassis['uuid'])
self._assertExpected(self.chassis, chassis)
@decorators.idempotent_id('29c9cd3f-19b5-417b-9864-99512c3b33b3')
def test_list_chassis(self):
_, body = self.client.list_chassis()
self.assertIn(self.chassis['uuid'],
[i['uuid'] for i in body['chassis']])
@decorators.idempotent_id('5ae649ad-22d1-4fe1-bbc6-97227d199fb3')
def test_delete_chassis(self):
_, body = self.create_chassis()
uuid = body['uuid']
self.delete_chassis(uuid)
self.assertRaises(lib_exc.NotFound, self.client.show_chassis, uuid)
@decorators.idempotent_id('cda8a41f-6be2-4cbf-840c-994b00a89b44')
def test_update_chassis(self):
_, body = self.create_chassis()
uuid = body['uuid']
new_description = data_utils.rand_name('new-description')
_, body = (self.client.update_chassis(uuid,
description=new_description))
_, chassis = self.client.show_chassis(uuid)
self.assertEqual(new_description, chassis['description'])
@decorators.idempotent_id('76305e22-a4e2-4ab3-855c-f4e2368b9335')
def test_chassis_node_list(self):
_, node = self.create_node(self.chassis['uuid'])
_, body = self.client.list_chassis_nodes(self.chassis['uuid'])
self.assertIn(node['uuid'], [n['uuid'] for n in body['nodes']])
@decorators.idempotent_id('dd52bd5d-610c-4f2c-8fa3-d5e59269325f')
def test_create_chassis_uuid(self):
uuid = data_utils.rand_uuid()
_, chassis = self.create_chassis(uuid=uuid)
self.assertEqual(uuid, chassis['uuid'])

View File

@ -1,54 +0,0 @@
# Copyright 2014 NEC Corporation. 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.
from tempest import config
from tempest.lib import decorators
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
CONF = config.CONF
class TestDrivers(base.BaseBaremetalTest):
"""Tests for drivers."""
@classmethod
def resource_setup(cls):
super(TestDrivers, cls).resource_setup()
cls.driver_name = CONF.baremetal.driver
@decorators.idempotent_id('5aed2790-7592-4655-9b16-99abcc2e6ec5')
def test_list_drivers(self):
_, drivers = self.client.list_drivers()
self.assertIn(self.driver_name,
[d['name'] for d in drivers['drivers']])
@decorators.idempotent_id('fb3287a3-c4d7-44bf-ae9d-1eef906d78ce')
def test_show_driver(self):
_, driver = self.client.show_driver(self.driver_name)
self.assertEqual(self.driver_name, driver['name'])
@decorators.idempotent_id('6efa976f-78a2-4859-b3aa-97d960d6e5e5')
def test_driver_properties(self):
_, properties = self.client.get_driver_properties(self.driver_name)
self.assertNotEmpty(properties)
@decorators.idempotent_id('fdf61f5a-f59d-4235-ad6c-cc718740e3e3')
def test_driver_logical_disk_properties(self):
self.useFixture(
api_microversion_fixture.APIMicroversionFixture('1.12'))
_, properties = self.client.get_driver_logical_disk_properties(
self.driver_name)
self.assertNotEmpty(properties)

View File

@ -1,403 +0,0 @@
# 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 six
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.common import waiters
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
CONF = config.CONF
class TestNodes(base.BaseBaremetalTest):
"""Tests for baremetal nodes."""
def setUp(self):
super(TestNodes, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
def _associate_node_with_instance(self):
self.client.set_node_power_state(self.node['uuid'], 'power off')
waiters.wait_for_bm_node_status(self.client, self.node['uuid'],
'power_state', 'power off')
instance_uuid = data_utils.rand_uuid()
self.client.update_node(self.node['uuid'],
instance_uuid=instance_uuid)
self.addCleanup(self.client.update_node,
uuid=self.node['uuid'], instance_uuid=None)
return instance_uuid
@decorators.idempotent_id('4e939eb2-8a69-4e84-8652-6fffcbc9db8f')
def test_create_node(self):
params = {'cpu_arch': 'x86_64',
'cpus': '12',
'local_gb': '10',
'memory_mb': '1024'}
_, body = self.create_node(self.chassis['uuid'], **params)
self._assertExpected(params, body['properties'])
@decorators.idempotent_id('9ade60a4-505e-4259-9ec4-71352cbbaf47')
def test_delete_node(self):
_, node = self.create_node(self.chassis['uuid'])
self.delete_node(node['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_node,
node['uuid'])
@decorators.idempotent_id('55451300-057c-4ecf-8255-ba42a83d3a03')
def test_show_node(self):
_, loaded_node = self.client.show_node(self.node['uuid'])
self._assertExpected(self.node, loaded_node)
@decorators.idempotent_id('4ca123c4-160d-4d8d-a3f7-15feda812263')
def test_list_nodes(self):
_, body = self.client.list_nodes()
self.assertIn(self.node['uuid'],
[i['uuid'] for i in body['nodes']])
@decorators.idempotent_id('85b1f6e0-57fd-424c-aeff-c3422920556f')
def test_list_nodes_association(self):
_, body = self.client.list_nodes(associated=True)
self.assertNotIn(self.node['uuid'],
[n['uuid'] for n in body['nodes']])
self._associate_node_with_instance()
_, body = self.client.list_nodes(associated=True)
self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
_, body = self.client.list_nodes(associated=False)
self.assertNotIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
@decorators.idempotent_id('18c4ebd8-f83a-4df7-9653-9fb33a329730')
def test_node_port_list(self):
_, port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
_, body = self.client.list_node_ports(self.node['uuid'])
self.assertIn(port['uuid'],
[p['uuid'] for p in body['ports']])
@decorators.idempotent_id('72591acb-f215-49db-8395-710d14eb86ab')
def test_node_port_list_no_ports(self):
_, node = self.create_node(self.chassis['uuid'])
_, body = self.client.list_node_ports(node['uuid'])
self.assertEmpty(body['ports'])
@decorators.idempotent_id('4fed270a-677a-4d19-be87-fd38ae490320')
def test_update_node(self):
props = {'cpu_arch': 'x86_64',
'cpus': '12',
'local_gb': '10',
'memory_mb': '128'}
_, node = self.create_node(self.chassis['uuid'], **props)
new_p = {'cpu_arch': 'x86',
'cpus': '1',
'local_gb': '10000',
'memory_mb': '12300'}
_, body = self.client.update_node(node['uuid'], properties=new_p)
_, node = self.client.show_node(node['uuid'])
self._assertExpected(new_p, node['properties'])
@decorators.idempotent_id('cbf1f515-5f4b-4e49-945c-86bcaccfeb1d')
def test_validate_driver_interface(self):
_, body = self.client.validate_driver_interface(self.node['uuid'])
core_interfaces = ['power', 'deploy']
for interface in core_interfaces:
self.assertIn(interface, body)
@decorators.idempotent_id('5519371c-26a2-46e9-aa1a-f74226e9d71f')
def test_set_node_boot_device(self):
self.client.set_node_boot_device(self.node['uuid'], 'pxe')
@decorators.idempotent_id('9ea73775-f578-40b9-bc34-efc639c4f21f')
def test_get_node_boot_device(self):
body = self.client.get_node_boot_device(self.node['uuid'])
self.assertIn('boot_device', body)
self.assertIn('persistent', body)
self.assertIsInstance(body['boot_device'], six.string_types)
self.assertIsInstance(body['persistent'], bool)
@decorators.idempotent_id('3622bc6f-3589-4bc2-89f3-50419c66b133')
def test_get_node_supported_boot_devices(self):
body = self.client.get_node_supported_boot_devices(self.node['uuid'])
self.assertIn('supported_boot_devices', body)
self.assertIsInstance(body['supported_boot_devices'], list)
@decorators.idempotent_id('f63b6288-1137-4426-8cfe-0d5b7eb87c06')
def test_get_console(self):
_, body = self.client.get_console(self.node['uuid'])
con_info = ['console_enabled', 'console_info']
for key in con_info:
self.assertIn(key, body)
@decorators.idempotent_id('80504575-9b21-4670-92d1-143b948f9437')
def test_set_console_mode(self):
self.client.set_console_mode(self.node['uuid'], True)
waiters.wait_for_bm_node_status(self.client, self.node['uuid'],
'console_enabled', True)
@decorators.idempotent_id('b02a4f38-5e8b-44b2-aed2-a69a36ecfd69')
def test_get_node_by_instance_uuid(self):
instance_uuid = self._associate_node_with_instance()
_, body = self.client.show_node_by_instance_uuid(instance_uuid)
self.assertEqual(1, len(body['nodes']))
self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
class TestNodesResourceClass(base.BaseBaremetalTest):
min_microversion = '1.21'
def setUp(self):
super(TestNodesResourceClass, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture(
TestNodesResourceClass.min_microversion)
)
_, self.chassis = self.create_chassis()
self.resource_class = data_utils.rand_name(name='Resource_Class')
_, self.node = self.create_node(
self.chassis['uuid'], resource_class=self.resource_class)
@decorators.idempotent_id('2a00340c-8152-4a61-9fc5-0b3cdefec258')
def test_create_node_resource_class_long(self):
"""Create new node with specified longest name of resource class."""
res_class_long_name = data_utils.arbitrary_string(80)
_, body = self.create_node(
self.chassis['uuid'],
resource_class=res_class_long_name)
self.assertEqual(res_class_long_name, body['resource_class'])
@decorators.idempotent_id('142db00d-ac0f-415b-8da8-9095fbb561f7')
def test_update_node_resource_class(self):
"""Update existing node with specified resource class."""
new_res_class_name = data_utils.rand_name(name='Resource_Class')
_, body = self.client.update_node(
self.node['uuid'], resource_class=new_res_class_name)
_, body = self.client.show_node(self.node['uuid'])
self.assertEqual(new_res_class_name, body['resource_class'])
@decorators.idempotent_id('73e6f7b5-3e51-49ea-af5b-146cd49f40ee')
def test_show_node_resource_class(self):
"""Show resource class field of specified node."""
_, body = self.client.show_node(self.node['uuid'])
self.assertEqual(self.resource_class, body['resource_class'])
@decorators.idempotent_id('f2bf4465-280c-4fdc-bbf7-fcf5188befa4')
def test_list_nodes_resource_class(self):
"""List nodes of specified resource class only."""
res_class = 'ResClass-{0}'.format(data_utils.rand_uuid())
for node in range(3):
_, body = self.create_node(
self.chassis['uuid'], resource_class=res_class)
_, body = self.client.list_nodes(resource_class=res_class)
self.assertEqual(3, len([i['uuid'] for i in body['nodes']]))
@decorators.idempotent_id('40733bad-bb79-445e-a094-530a44042995')
def test_list_nodes_detail_resource_class(self):
"""Get detailed nodes list of specified resource class only."""
res_class = 'ResClass-{0}'.format(data_utils.rand_uuid())
for node in range(3):
_, body = self.create_node(
self.chassis['uuid'], resource_class=res_class)
_, body = self.client.list_nodes_detail(resource_class=res_class)
self.assertEqual(3, len([i['uuid'] for i in body['nodes']]))
for node in body['nodes']:
self.assertEqual(res_class, node['resource_class'])
@decorators.attr(type='negative')
@decorators.idempotent_id('e75136d4-0690-48a5-aef3-75040aee73ad')
def test_create_node_resource_class_too_long(self):
"""Try to create a node with too long resource class name."""
resource_class = data_utils.arbitrary_string(81)
self.assertRaises(lib_exc.BadRequest, self.create_node,
self.chassis['uuid'], resource_class=resource_class)
@decorators.attr(type='negative')
@decorators.idempotent_id('f0aeece4-8671-44ea-a482-b4047fc4cf74')
def test_update_node_resource_class_too_long(self):
"""Try to update a node with too long resource class name."""
resource_class = data_utils.arbitrary_string(81)
self.assertRaises(lib_exc.BadRequest, self.client.update_node,
self.node['uuid'], resource_class=resource_class)
class TestNodesResourceClassOldApi(base.BaseBaremetalTest):
def setUp(self):
super(TestNodesResourceClassOldApi, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
@decorators.attr(type='negative')
@decorators.idempotent_id('2c364408-4746-4b3c-9821-20d47b57bdec')
def test_create_node_resource_class_old_api(self):
"""Try to create a node with resource class using older api version."""
resource_class = data_utils.arbitrary_string()
self.assertRaises(lib_exc.UnexpectedResponseCode, self.create_node,
self.chassis['uuid'], resource_class=resource_class)
@decorators.attr(type='negative')
@decorators.idempotent_id('666f3c1a-4922-4a3d-b6d9-dea7c74d30bc')
def test_update_node_resource_class_old_api(self):
"""Try to update a node with resource class using older api version."""
resource_class = data_utils.arbitrary_string()
self.assertRaises(lib_exc.UnexpectedResponseCode,
self.client.update_node,
self.node['uuid'], resource_class=resource_class)
@decorators.attr(type='negative')
@decorators.idempotent_id('95903480-f16d-4774-8775-6c7f87b27c59')
def test_list_nodes_by_resource_class_old_api(self):
"""Try to list nodes with resource class using older api version."""
resource_class = data_utils.arbitrary_string()
self.assertRaises(
lib_exc.UnexpectedResponseCode,
self.client.list_nodes, resource_class=resource_class)
self.assertRaises(
lib_exc.UnexpectedResponseCode,
self.client.list_nodes_detail, resource_class=resource_class)
class TestNodesVif(base.BaseBaremetalTest):
min_microversion = '1.28'
@classmethod
def skip_checks(cls):
super(TestNodesVif, cls).skip_checks()
if not CONF.service_available.neutron:
raise cls.skipException('Neutron is not enabled.')
def setUp(self):
super(TestNodesVif, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
if CONF.network.shared_physical_network:
self.net = self.os_admin.networks_client.list_networks(
name=CONF.compute.fixed_network_name)['networks'][0]
else:
self.net = self.os_admin.networks_client.\
create_network()['network']
self.addCleanup(self.os_admin.networks_client.delete_network,
self.net['id'])
self.nport_id = self.os_admin.ports_client.create_port(
network_id=self.net['id'])['port']['id']
self.addCleanup(self.os_admin.ports_client.delete_port,
self.nport_id)
@decorators.idempotent_id('a3d319d0-cacb-4e55-a3dc-3fa8b74880f1')
def test_vif_on_port(self):
"""Test attachment and detachment of VIFs on the node with port.
Test steps:
1) Create chassis and node in setUp.
2) Create port for the node.
3) Attach VIF to the node.
4) Check VIF info in VIFs list and port internal_info.
5) Detach VIF from the node.
6) Check that no more VIF info in VIFs list and port internal_info.
"""
self.useFixture(
api_microversion_fixture.APIMicroversionFixture('1.28'))
_, self.port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
self.client.vif_attach(self.node['uuid'], self.nport_id)
_, body = self.client.vif_list(self.node['uuid'])
self.assertEqual({'vifs': [{'id': self.nport_id}]}, body)
_, port = self.client.show_port(self.port['uuid'])
self.assertEqual(self.nport_id,
port['internal_info']['tenant_vif_port_id'])
self.client.vif_detach(self.node['uuid'], self.nport_id)
_, body = self.client.vif_list(self.node['uuid'])
self.assertEqual({'vifs': []}, body)
_, port = self.client.show_port(self.port['uuid'])
self.assertNotIn('tenant_vif_port_id', port['internal_info'])
@decorators.idempotent_id('95279515-7d0a-4f5f-987f-93e36aae5585')
def test_vif_on_portgroup(self):
"""Test attachment and detachment of VIFs on the node with port group.
Test steps:
1) Create chassis and node in setUp.
2) Create port for the node.
3) Create port group for the node.
4) Plug port into port group.
5) Attach VIF to the node.
6) Check VIF info in VIFs list and port group internal_info, but
not in port internal_info.
7) Detach VIF from the node.
8) Check that no VIF info in VIFs list and port group internal_info.
"""
self.useFixture(
api_microversion_fixture.APIMicroversionFixture('1.28'))
_, self.port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
_, self.portgroup = self.create_portgroup(
self.node['uuid'], address=data_utils.rand_mac_address())
patch = [{'path': '/portgroup_uuid',
'op': 'add',
'value': self.portgroup['uuid']}]
self.client.update_port(self.port['uuid'], patch)
self.client.vif_attach(self.node['uuid'], self.nport_id)
_, body = self.client.vif_list(self.node['uuid'])
self.assertEqual({'vifs': [{'id': self.nport_id}]}, body)
_, port = self.client.show_port(self.port['uuid'])
self.assertNotIn('tenant_vif_port_id', port['internal_info'])
_, portgroup = self.client.show_portgroup(self.portgroup['uuid'])
self.assertEqual(self.nport_id,
portgroup['internal_info']['tenant_vif_port_id'])
self.client.vif_detach(self.node['uuid'], self.nport_id)
_, body = self.client.vif_list(self.node['uuid'])
self.assertEqual({'vifs': []}, body)
_, portgroup = self.client.show_portgroup(self.portgroup['uuid'])
self.assertNotIn('tenant_vif_port_id', portgroup['internal_info'])
@decorators.idempotent_id('a3d319d0-cacb-4e55-a3dc-3fa8b74880f2')
def test_vif_already_set_on_extra(self):
self.useFixture(
api_microversion_fixture.APIMicroversionFixture('1.28'))
_, self.port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
patch = [{'path': '/extra/vif_port_id',
'op': 'add',
'value': self.nport_id}]
self.client.update_port(self.port['uuid'], patch)
_, body = self.client.vif_list(self.node['uuid'])
self.assertEqual({'vifs': [{'id': self.nport_id}]}, body)
self.assertRaises(lib_exc.Conflict, self.client.vif_attach,
self.node['uuid'], self.nport_id)
self.client.vif_detach(self.node['uuid'], self.nport_id)

View File

@ -1,193 +0,0 @@
# Copyright 2014 NEC Corporation. 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.
from oslo_utils import timeutils
from tempest.lib import decorators
from tempest.lib import exceptions
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
class TestNodeStatesMixin(object):
"""Mixin for for baremetal node states tests."""
@classmethod
def resource_setup(cls):
super(TestNodeStatesMixin, cls).resource_setup()
_, cls.chassis = cls.create_chassis()
def _validate_power_state(self, node_uuid, power_state):
# Validate that power state is set within timeout
if power_state == 'rebooting':
power_state = 'power on'
start = timeutils.utcnow()
while timeutils.delta_seconds(
start, timeutils.utcnow()) < self.power_timeout:
_, node = self.client.show_node(node_uuid)
if node['power_state'] == power_state:
return
message = ('Failed to set power state within '
'the required time: %s sec.' % self.power_timeout)
raise exceptions.TimeoutException(message)
def _validate_provision_state(self, node_uuid, target_state):
# Validate that provision state is set within timeout
start = timeutils.utcnow()
while timeutils.delta_seconds(
start, timeutils.utcnow()) < self.unprovision_timeout:
_, node = self.client.show_node(node_uuid)
if node['provision_state'] == target_state:
return
message = ('Failed to set provision state %(state)s within '
'the required time: %(timeout)s sec.',
{'state': target_state,
'timeout': self.unprovision_timeout})
raise exceptions.TimeoutException(message)
@decorators.idempotent_id('cd8afa5e-3f57-4e43-8185-beb83d3c9015')
def test_list_nodestates(self):
_, node = self.create_node(self.chassis['uuid'])
_, nodestates = self.client.list_nodestates(node['uuid'])
for key in nodestates:
self.assertEqual(nodestates[key], node[key])
@decorators.idempotent_id('fc5b9320-0c98-4e5a-8848-877fe5a0322c')
def test_set_node_power_state(self):
_, node = self.create_node(self.chassis['uuid'])
states = ["power on", "rebooting", "power off"]
for state in states:
# Set power state
self.client.set_node_power_state(node['uuid'], state)
# Check power state after state is set
self._validate_power_state(node['uuid'], state)
class TestNodeStatesV1_1(TestNodeStatesMixin, base.BaseBaremetalTest):
@decorators.idempotent_id('ccb8fca9-2ba0-480c-a037-34c3bd09dc74')
def test_set_node_provision_state(self):
_, node = self.create_node(self.chassis['uuid'])
# Nodes appear in NONE state by default until v1.1
self.assertIsNone(node['provision_state'])
provision_states_list = ['active', 'deleted']
target_states_list = ['active', None]
for (provision_state, target_state) in zip(provision_states_list,
target_states_list):
self.client.set_node_provision_state(node['uuid'], provision_state)
self._validate_provision_state(node['uuid'], target_state)
class TestNodeStatesV1_2(TestNodeStatesMixin, base.BaseBaremetalTest):
def setUp(self):
super(TestNodeStatesV1_2, self).setUp()
self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.2'))
@decorators.idempotent_id('9c414984-f3b6-4b3d-81da-93b60d4662fb')
def test_set_node_provision_state(self):
_, node = self.create_node(self.chassis['uuid'])
# Nodes appear in AVAILABLE state by default from v1.2 to v1.10
self.assertEqual('available', node['provision_state'])
provision_states_list = ['active', 'deleted']
target_states_list = ['active', 'available']
for (provision_state, target_state) in zip(provision_states_list,
target_states_list):
self.client.set_node_provision_state(node['uuid'], provision_state)
self._validate_provision_state(node['uuid'], target_state)
class TestNodeStatesV1_4(TestNodeStatesMixin, base.BaseBaremetalTest):
def setUp(self):
super(TestNodeStatesV1_4, self).setUp()
self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.4'))
@decorators.idempotent_id('3d606003-05ce-4b5a-964d-bdee382fafe9')
def test_set_node_provision_state(self):
_, node = self.create_node(self.chassis['uuid'])
# Nodes appear in AVAILABLE state by default from v1.2 to v1.10
self.assertEqual('available', node['provision_state'])
# MANAGEABLE state and PROVIDE transition have been added in v1.4
provision_states_list = [
'manage', 'provide', 'active', 'deleted']
target_states_list = [
'manageable', 'available', 'active', 'available']
for (provision_state, target_state) in zip(provision_states_list,
target_states_list):
self.client.set_node_provision_state(node['uuid'], provision_state)
self._validate_provision_state(node['uuid'], target_state)
class TestNodeStatesV1_6(TestNodeStatesMixin, base.BaseBaremetalTest):
def setUp(self):
super(TestNodeStatesV1_6, self).setUp()
self.useFixture(api_microversion_fixture.APIMicroversionFixture('1.6'))
@decorators.idempotent_id('6c9ce4a3-713b-4c76-91af-18c48d01f1bb')
def test_set_node_provision_state(self):
_, node = self.create_node(self.chassis['uuid'])
# Nodes appear in AVAILABLE state by default from v1.2 to v1.10
self.assertEqual('available', node['provision_state'])
# INSPECT* states have been added in v1.6
provision_states_list = [
'manage', 'inspect', 'provide', 'active', 'deleted']
target_states_list = [
'manageable', 'manageable', 'available', 'active', 'available']
for (provision_state, target_state) in zip(provision_states_list,
target_states_list):
self.client.set_node_provision_state(node['uuid'], provision_state)
self._validate_provision_state(node['uuid'], target_state)
class TestNodeStatesV1_11(TestNodeStatesMixin, base.BaseBaremetalTest):
def setUp(self):
super(TestNodeStatesV1_11, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture('1.11')
)
@decorators.idempotent_id('31f53828-b83d-40c7-98e5-843e28a1b6b9')
def test_set_node_provision_state(self):
_, node = self.create_node(self.chassis['uuid'])
# Nodes appear in ENROLL state by default from v1.11
self.assertEqual('enroll', node['provision_state'])
provision_states_list = [
'manage', 'inspect', 'provide', 'active', 'deleted']
target_states_list = [
'manageable', 'manageable', 'available', 'active', 'available']
for (provision_state, target_state) in zip(provision_states_list,
target_states_list):
self.client.set_node_provision_state(node['uuid'], provision_state)
self._validate_provision_state(node['uuid'], target_state)
class TestNodeStatesV1_12(TestNodeStatesMixin, base.BaseBaremetalTest):
def setUp(self):
super(TestNodeStatesV1_12, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture('1.12')
)
@decorators.idempotent_id('4427b1ca-8e79-4139-83d6-77dfac03e61e')
def test_set_node_raid_config(self):
_, node = self.create_node(self.chassis['uuid'])
target_raid_config = {'logical_disks': [{'size_gb': 100,
'raid_level': '1'}]}
self.client.set_node_raid_config(node['uuid'], target_raid_config)
_, ret = self.client.show_node(node['uuid'])
self.assertEqual(target_raid_config, ret['target_raid_config'])

View File

@ -1,74 +0,0 @@
# 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.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
class TestPortGroups(base.BaseBaremetalTest):
"""Basic positive test cases for port groups."""
min_microversion = '1.23'
def setUp(self):
super(TestPortGroups, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture(
self.min_microversion))
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
_, self.portgroup = self.create_portgroup(
self.node['uuid'], address=data_utils.rand_mac_address(),
name=data_utils.rand_name('portgroup'))
@decorators.idempotent_id('110cd302-256b-4ddc-be10-fc6c9ad8e649')
def test_create_portgroup_with_address(self):
"""Create a port group with specific MAC address."""
_, body = self.client.show_portgroup(self.portgroup['uuid'])
self.assertEqual(self.portgroup['address'], body['address'])
@decorators.idempotent_id('4336fa0f-da86-4cec-b788-89f59a7635a5')
def test_create_portgroup_no_address(self):
"""Create a port group without setting MAC address."""
_, portgroup = self.create_portgroup(self.node['uuid'])
_, body = self.client.show_portgroup(portgroup['uuid'])
self._assertExpected(portgroup, body)
self.assertIsNone(body['address'])
@decorators.idempotent_id('8378c69f-f806-454b-8ddd-6b7fd93ab12b')
def test_delete_portgroup(self):
"""Delete a port group."""
self.delete_portgroup(self.portgroup['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_portgroup,
self.portgroup['uuid'])
@decorators.idempotent_id('f6be5e70-3e3b-435c-b2fc-bbb2cc9b3185')
def test_show_portgroup(self):
"""Show a specified port group."""
_, portgroup = self.client.show_portgroup(self.portgroup['uuid'])
self._assertExpected(self.portgroup, portgroup)
@decorators.idempotent_id('cf2dfd95-5ea1-4109-8ad3-297cd76aa5d3')
def test_list_portgroups(self):
"""List port groups."""
_, body = self.client.list_portgroups()
self.assertIn(self.portgroup['uuid'],
[i['uuid'] for i in body['portgroups']])
self.assertIn(self.portgroup['address'],
[i['address'] for i in body['portgroups']])
self.assertIn(self.portgroup['name'],
[i['name'] for i in body['portgroups']])

View File

@ -1,376 +0,0 @@
# 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.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
class TestPorts(base.BaseBaremetalTest):
"""Tests for ports."""
def setUp(self):
super(TestPorts, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
_, self.port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
@decorators.idempotent_id('83975898-2e50-42ed-b5f0-e510e36a0b56')
def test_create_port(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
_, body = self.client.show_port(port['uuid'])
self._assertExpected(port, body)
@decorators.idempotent_id('d1f6b249-4cf6-4fe6-9ed6-a6e84b1bf67b')
def test_create_port_specifying_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
uuid = data_utils.rand_uuid()
_, port = self.create_port(node_id=node_id,
address=address, uuid=uuid)
_, body = self.client.show_port(uuid)
self._assertExpected(port, body)
@decorators.idempotent_id('4a02c4b0-6573-42a4-a513-2e36ad485b62')
def test_create_port_with_extra(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'str': 'value', 'int': 123, 'float': 0.123,
'bool': True, 'list': [1, 2, 3], 'dict': {'foo': 'bar'}}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
_, body = self.client.show_port(port['uuid'])
self._assertExpected(port, body)
@decorators.idempotent_id('1bf257a9-aea3-494e-89c0-63f657ab4fdd')
def test_delete_port(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
self.delete_port(port['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_port,
port['uuid'])
@decorators.idempotent_id('9fa77ab5-ce59-4f05-baac-148904ba1597')
def test_show_port(self):
_, port = self.client.show_port(self.port['uuid'])
self._assertExpected(self.port, port)
@decorators.idempotent_id('7c1114ff-fc3f-47bb-bc2f-68f61620ba8b')
def test_show_port_by_address(self):
_, port = self.client.show_port_by_address(self.port['address'])
self._assertExpected(self.port, port['ports'][0])
@decorators.idempotent_id('bd773405-aea5-465d-b576-0ab1780069e5')
def test_show_port_with_links(self):
_, port = self.client.show_port(self.port['uuid'])
self.assertIn('links', port.keys())
self.assertEqual(2, len(port['links']))
self.assertIn(port['uuid'], port['links'][0]['href'])
@decorators.idempotent_id('b5e91854-5cd7-4a8e-bb35-3e0a1314606d')
def test_list_ports(self):
_, body = self.client.list_ports()
self.assertIn(self.port['uuid'],
[i['uuid'] for i in body['ports']])
# Verify self links.
for port in body['ports']:
self.validate_self_link('ports', port['uuid'],
port['links'][0]['href'])
@decorators.idempotent_id('324a910e-2f80-4258-9087-062b5ae06240')
def test_list_with_limit(self):
_, body = self.client.list_ports(limit=3)
next_marker = body['ports'][-1]['uuid']
self.assertIn(next_marker, body['next'])
@decorators.idempotent_id('8a94b50f-9895-4a63-a574-7ecff86e5875')
def test_list_ports_details(self):
node_id = self.node['uuid']
uuids = [
self.create_port(node_id=node_id,
address=data_utils.rand_mac_address())
[1]['uuid'] for i in range(0, 5)]
_, body = self.client.list_ports_detail()
ports_dict = dict((port['uuid'], port) for port in body['ports']
if port['uuid'] in uuids)
for uuid in uuids:
self.assertIn(uuid, ports_dict)
port = ports_dict[uuid]
self.assertIn('extra', port)
self.assertIn('node_uuid', port)
# never expose the node_id
self.assertNotIn('node_id', port)
# Verify self link.
self.validate_self_link('ports', port['uuid'],
port['links'][0]['href'])
@decorators.idempotent_id('8a03f688-7d75-4ecd-8cbc-e06b8f346738')
def test_list_ports_details_with_address(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
for i in range(0, 5):
self.create_port(node_id=node_id,
address=data_utils.rand_mac_address())
_, body = self.client.list_ports_detail(address=address)
self.assertEqual(1, len(body['ports']))
self.assertEqual(address, body['ports'][0]['address'])
@decorators.idempotent_id('9c26298b-1bcb-47b7-9b9e-8bdd6e3c4aba')
def test_update_port_replace(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
new_address = data_utils.rand_mac_address()
new_extra = {'key1': 'new-value1', 'key2': 'new-value2',
'key3': 'new-value3'}
patch = [{'path': '/address',
'op': 'replace',
'value': new_address},
{'path': '/extra/key1',
'op': 'replace',
'value': new_extra['key1']},
{'path': '/extra/key2',
'op': 'replace',
'value': new_extra['key2']},
{'path': '/extra/key3',
'op': 'replace',
'value': new_extra['key3']}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(new_address, body['address'])
self.assertEqual(new_extra, body['extra'])
@decorators.idempotent_id('d7e7fece-6ed9-460a-9ebe-9267217e8580')
def test_update_port_remove(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
# Removing one item from the collection
self.client.update_port(port['uuid'],
[{'path': '/extra/key2',
'op': 'remove'}])
extra.pop('key2')
_, body = self.client.show_port(port['uuid'])
self.assertEqual(extra, body['extra'])
# Removing the collection
self.client.update_port(port['uuid'], [{'path': '/extra',
'op': 'remove'}])
_, body = self.client.show_port(port['uuid'])
self.assertEqual({}, body['extra'])
# Assert nothing else was changed
self.assertEqual(node_id, body['node_uuid'])
self.assertEqual(address, body['address'])
@decorators.idempotent_id('241288b3-e98a-400f-a4d7-d1f716146361')
def test_update_port_add(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
extra = {'key1': 'value1', 'key2': 'value2'}
patch = [{'path': '/extra/key1',
'op': 'add',
'value': extra['key1']},
{'path': '/extra/key2',
'op': 'add',
'value': extra['key2']}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(extra, body['extra'])
@decorators.idempotent_id('5309e897-0799-4649-a982-0179b04c3876')
def test_update_port_mixed_ops(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
new_address = data_utils.rand_mac_address()
new_extra = {'key1': 0.123, 'key3': {'cat': 'meow'}}
patch = [{'path': '/address',
'op': 'replace',
'value': new_address},
{'path': '/extra/key1',
'op': 'replace',
'value': new_extra['key1']},
{'path': '/extra/key2',
'op': 'remove'},
{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(new_address, body['address'])
self.assertEqual(new_extra, body['extra'])
class TestPortsWithPhysicalNetwork(base.BaseBaremetalTest):
"""Tests for ports with physical network information."""
min_microversion = '1.34'
def setUp(self):
super(TestPortsWithPhysicalNetwork, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture(
TestPortsWithPhysicalNetwork.min_microversion)
)
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
@decorators.idempotent_id('f1a5d279-c456-4311-ad31-fea09f61c22b')
def test_create_port_with_physical_network(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address,
physical_network='physnet1')
_, body = self.client.show_port(port['uuid'])
self._assertExpected(port, body)
self.assertEqual('physnet1', port['physical_network'])
@decorators.idempotent_id('9c26298b-1bcb-47b7-9b9e-8bdd6e3c4aba')
def test_update_port_replace_physical_network(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address,
physical_network='physnet1')
new_physnet = 'physnet2'
patch = [{'path': '/physical_network',
'op': 'replace',
'value': new_physnet}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(new_physnet, body['physical_network'])
@decorators.idempotent_id('6503309c-b2c7-4f59-b15a-0d92b5de9210')
def test_update_port_remove_physical_network(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address,
physical_network='physnet1')
patch = [{'path': '/physical_network',
'op': 'remove'}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertIsNone(body['physical_network'])
@decorators.idempotent_id('4155c24d-8474-4b53-a320-aee475f85a68')
def test_create_ports_in_portgroup_with_physical_network(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, portgroup = self.create_portgroup(node_id, address=address)
_, port1 = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
address = data_utils.rand_mac_address()
_, port2 = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
_, body = self.client.show_port(port1['uuid'])
self.assertEqual('physnet1', body['physical_network'])
self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
_, body = self.client.show_port(port2['uuid'])
self.assertEqual('physnet1', body['physical_network'])
self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
@decorators.idempotent_id('cf05a3ef-3bc4-4db7-bb4c-4eb871eb9f81')
def test_update_ports_in_portgroup_with_physical_network(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, portgroup = self.create_portgroup(node_id, address=address)
_, port1 = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
address = data_utils.rand_mac_address()
_, port2 = self.create_port(node_id=node_id, address=address,
physical_network='physnet1')
patch = [{'path': '/portgroup_uuid',
'op': 'replace',
'value': portgroup['uuid']}]
self.client.update_port(port2['uuid'], patch)
_, body = self.client.show_port(port1['uuid'])
self.assertEqual('physnet1', body['physical_network'])
self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])
_, body = self.client.show_port(port2['uuid'])
self.assertEqual('physnet1', body['physical_network'])
self.assertEqual(portgroup['uuid'], body['portgroup_uuid'])

View File

@ -1,463 +0,0 @@
# 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.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
class TestPortsNegative(base.BaseBaremetalTest):
"""Negative tests for ports."""
def setUp(self):
super(TestPortsNegative, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0a6ee1f7-d0d9-4069-8778-37f3aa07303a')
def test_create_port_malformed_mac(self):
node_id = self.node['uuid']
address = 'malformed:mac'
self.assertRaises(lib_exc.BadRequest,
self.create_port, node_id=node_id, address=address)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('30277ee8-0c60-4f1d-b125-0e51c2f43369')
def test_create_port_nonexsistent_node_id(self):
node_id = str(data_utils.rand_uuid())
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id=node_id, address=address)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('029190f6-43e1-40a3-b64a-65173ba653a3')
def test_show_port_malformed_uuid(self):
self.assertRaises(lib_exc.BadRequest, self.client.show_port,
'malformed:uuid')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0d00e13d-e2e0-45b1-bcbc-55a6d90ca793')
def test_show_port_nonexistent_uuid(self):
self.assertRaises(lib_exc.NotFound, self.client.show_port,
data_utils.rand_uuid())
@decorators.attr(type=['negative'])
@decorators.idempotent_id('4ad85266-31e9-4942-99ac-751897dc9e23')
def test_show_port_by_mac_not_allowed(self):
self.assertRaises(lib_exc.BadRequest, self.client.show_port,
data_utils.rand_mac_address())
@decorators.attr(type=['negative'])
@decorators.idempotent_id('89a34380-3c61-4c32-955c-2cd9ce94da21')
def test_create_port_duplicated_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
uuid = data_utils.rand_uuid()
self.create_port(node_id=node_id, address=address, uuid=uuid)
self.assertRaises(lib_exc.Conflict, self.create_port, node_id=node_id,
address=address, uuid=uuid)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('65e84917-733c-40ae-ae4b-96a4adff931c')
def test_create_port_no_mandatory_field_node_id(self):
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.create_port, node_id=None,
address=address)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('bcea3476-7033-4183-acfe-e56a30809b46')
def test_create_port_no_mandatory_field_mac(self):
node_id = self.node['uuid']
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id=node_id, address=None)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('2b51cd18-fb95-458b-9780-e6257787b649')
def test_create_port_malformed_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
uuid = 'malformed:uuid'
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id=node_id, address=address, uuid=uuid)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('583a6856-6a30-4ac4-889f-14e2adff8105')
def test_create_port_malformed_node_id(self):
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id='malformed:nodeid', address=address)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('e27f8b2e-42c6-4a43-a3cd-accff716bc5c')
def test_create_port_duplicated_mac(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
self.assertRaises(lib_exc.Conflict,
self.create_port, node_id=node_id,
address=address)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('8907082d-ac5e-4be3-b05f-d072ede82020')
def test_update_port_by_mac_not_allowed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key': 'value'}
self.create_port(node_id=node_id, address=address, extra=extra)
patch = [{'path': '/extra/key',
'op': 'replace',
'value': 'new-value'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, address,
patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('df1ac70c-db9f-41d9-90f1-78cd6b905718')
def test_update_port_nonexistent(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key': 'value'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
port_id = port['uuid']
_, body = self.client.delete_port(port_id)
patch = [{'path': '/extra/key',
'op': 'replace',
'value': 'new-value'}]
self.assertRaises(lib_exc.NotFound,
self.client.update_port, port_id, patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('c701e315-aa52-41ea-817c-65c5ca8ca2a8')
def test_update_port_malformed_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
new_address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.client.update_port,
uuid='malformed:uuid',
patch=[{'path': '/address', 'op': 'replace',
'value': new_address}])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('f8f15803-34d6-45dc-b06f-e5e04bf1b38b')
def test_update_port_add_nonexistent_property(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/nonexistent', ' op': 'add',
'value': 'value'}])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('898ec904-38b1-4fcb-9584-1187d4263a2a')
def test_update_port_replace_node_id_with_malformed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/node_uuid',
'op': 'replace',
'value': 'malformed:node_uuid'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('2949f30f-5f59-43fa-a6d9-4eac578afab4')
def test_update_port_replace_mac_with_duplicated(self):
node_id = self.node['uuid']
address1 = data_utils.rand_mac_address()
address2 = data_utils.rand_mac_address()
_, port1 = self.create_port(node_id=node_id, address=address1)
_, port2 = self.create_port(node_id=node_id, address=address2)
port_id = port2['uuid']
patch = [{'path': '/address',
'op': 'replace',
'value': address1}]
self.assertRaises(lib_exc.Conflict,
self.client.update_port, port_id, patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('97f6e048-6e4f-4eba-a09d-fbbc78b77a77')
def test_update_port_replace_node_id_with_nonexistent(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/node_uuid',
'op': 'replace',
'value': data_utils.rand_uuid()}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('375022c5-9e9e-4b11-9ca4-656729c0c9b2')
def test_update_port_replace_mac_with_malformed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/address',
'op': 'replace',
'value': 'malformed:mac'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('5722b853-03fc-4854-8308-2036a1b67d85')
def test_update_port_replace_nonexistent_property(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/nonexistent', ' op': 'replace', 'value': 'value'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('ae2696ca-930a-4a7f-918f-30ae97c60f56')
def test_update_port_remove_mandatory_field_mac(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/address', 'op': 'remove'}])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('5392c1f0-2071-4697-9064-ec2d63019018')
def test_update_port_remove_mandatory_field_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/uuid', 'op': 'remove'}])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('06b50d82-802a-47ef-b079-0a3311cf85a2')
def test_update_port_remove_nonexistent_property(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/nonexistent', 'op': 'remove'}])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('03d42391-2145-4a6c-95bf-63fe55eb64fd')
def test_delete_port_by_mac_not_allowed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
self.assertRaises(lib_exc.BadRequest, self.client.delete_port, address)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0629e002-818e-4763-b25b-ae5e07b1cb23')
def test_update_port_mixed_ops_integrity(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
port_id = port['uuid']
new_address = data_utils.rand_mac_address()
new_extra = {'key1': 'new-value1', 'key3': 'new-value3'}
patch = [{'path': '/address',
'op': 'replace',
'value': new_address},
{'path': '/extra/key1',
'op': 'replace',
'value': new_extra['key1']},
{'path': '/extra/key2',
'op': 'remove'},
{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']},
{'path': '/nonexistent',
'op': 'replace',
'value': 'value'}]
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
patch)
# patch should not be applied
_, body = self.client.show_port(port_id)
self.assertEqual(address, body['address'])
self.assertEqual(extra, body['extra'])
class TestPortsWithPhysicalNetworkOldAPI(base.BaseBaremetalTest):
"""Negative tests for ports with physical network information."""
def setUp(self):
super(TestPortsWithPhysicalNetworkOldAPI, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('307e57e9-082f-4830-9480-91affcbfda08')
def test_create_port_with_physical_network_old_api(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.assertRaises((lib_exc.BadRequest, lib_exc.UnexpectedResponseCode),
self.create_port,
node_id=node_id, address=address,
physical_network='physnet1')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0b278c0a-d334-424e-a5c5-b6d001c2a715')
def test_update_port_replace_physical_network_old_api(self):
_, port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
new_physnet = 'physnet1'
patch = [{'path': '/physical_network',
'op': 'replace',
'value': new_physnet}]
self.assertRaises((lib_exc.BadRequest, lib_exc.UnexpectedResponseCode),
self.client.update_port,
port['uuid'], patch)
class TestPortsNegativeWithPhysicalNetwork(base.BaseBaremetalTest):
"""Negative tests for ports with physical network information."""
min_microversion = '1.34'
def setUp(self):
super(TestPortsNegativeWithPhysicalNetwork, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture(
TestPortsNegativeWithPhysicalNetwork.min_microversion)
)
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('e20156fb-956b-4d5b-89a4-f379044a1d3c')
def test_create_ports_in_portgroup_with_inconsistent_physical_network(
self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, portgroup = self.create_portgroup(node_id, address=address)
_, _ = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.Conflict,
self.create_port,
node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet2')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('050e792c-22c9-4e4a-ae89-dfbfc52ad00d')
def test_update_ports_in_portgroup_with_inconsistent_physical_network(
self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, portgroup = self.create_portgroup(node_id, address=address)
_, _ = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
address = data_utils.rand_mac_address()
_, port2 = self.create_port(node_id=node_id, address=address,
physical_network='physnet2')
patch = [{'path': '/portgroup_uuid',
'op': 'replace',
'value': portgroup['uuid']}]
self.assertRaises(lib_exc.Conflict,
self.client.update_port,
port2['uuid'], patch)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('3cd1c8ec-57d1-40cb-922b-dd02431beea3')
def test_update_ports_in_portgroup_with_inconsistent_physical_network_2(
self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, portgroup = self.create_portgroup(node_id, address=address)
_, _ = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
address = data_utils.rand_mac_address()
_, port2 = self.create_port(node_id=node_id, address=address,
portgroup_uuid=portgroup['uuid'],
physical_network='physnet1')
patch = [{'path': '/physical_network',
'op': 'replace',
'value': 'physnet2'}]
self.assertRaises(lib_exc.Conflict,
self.client.update_port,
port2['uuid'], patch)

View File

@ -1,227 +0,0 @@
# Copyright 2017 FUJITSU LIMITED
#
# 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.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
class TestVolumeConnector(base.BaseBaremetalTest):
"""Basic test cases for volume connector."""
min_microversion = '1.32'
extra = {'key1': 'value1', 'key2': 'value2'}
def setUp(self):
super(TestVolumeConnector, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture(
self.min_microversion))
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
_, self.volume_connector = self.create_volume_connector(
self.node['uuid'], type='iqn',
connector_id=data_utils.rand_name('connector_id'),
extra=self.extra)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('3c3cbf45-488a-4386-a811-bf0aa2589c58')
def test_create_volume_connector_error(self):
"""Create a volume connector.
Fail when creating a volume connector with same connector_id
& type as an existing volume connector.
"""
regex_str = (r'.*A volume connector .*already exists')
self.assertRaisesRegex(
lib_exc.Conflict, regex_str,
self.create_volume_connector,
self.node['uuid'],
type=self.volume_connector['type'],
connector_id=self.volume_connector['connector_id'])
@decorators.idempotent_id('5795f816-0789-42e6-bb9c-91b4876ad13f')
def test_delete_volume_connector(self):
"""Delete a volume connector."""
# Powering off the Node before deleting a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power off')
self.delete_volume_connector(self.volume_connector['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_volume_connector,
self.volume_connector['uuid'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('ccbda5e6-52b7-400c-94d7-25eec1d590f0')
def test_delete_volume_connector_error(self):
"""Delete a volume connector
Fail when deleting a volume connector on node
with powered on state.
"""
# Powering on the Node before deleting a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power on')
regex_str = (r'.*The requested action \\\\"volume connector '
r'deletion\\\\" can not be performed on node*')
self.assertRaisesRegex(lib_exc.BadRequest,
regex_str,
self.delete_volume_connector,
self.volume_connector['uuid'])
@decorators.idempotent_id('6e4f50b7-0f4f-41c2-971e-d751abcac4e0')
def test_show_volume_connector(self):
"""Show a specified volume connector."""
_, volume_connector = self.client.show_volume_connector(
self.volume_connector['uuid'])
self._assertExpected(self.volume_connector, volume_connector)
@decorators.idempotent_id('a4725778-e164-4ee5-96a0-66119a35f783')
def test_list_volume_connectors(self):
"""List volume connectors."""
_, body = self.client.list_volume_connectors()
self.assertIn(self.volume_connector['uuid'],
[i['uuid'] for i in body['connectors']])
self.assertIn(self.volume_connector['type'],
[i['type'] for i in body['connectors']])
self.assertIn(self.volume_connector['connector_id'],
[i['connector_id'] for i in body['connectors']])
@decorators.idempotent_id('1d0459ad-01c0-46db-b930-7301bc2a3c98')
def test_list_with_limit(self):
"""List volume connectors with limit."""
_, body = self.client.list_volume_connectors(limit=3)
next_marker = body['connectors'][-1]['uuid']
self.assertIn(next_marker, body['next'])
@decorators.idempotent_id('3c6f8354-e9bd-4f21-aae2-6deb96b04be7')
def test_update_volume_connector_replace(self):
"""Update a volume connector with new connector id."""
new_connector_id = data_utils.rand_name('connector_id')
patch = [{'path': '/connector_id',
'op': 'replace',
'value': new_connector_id}]
# Powering off the Node before updating a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power off')
self.client.update_volume_connector(
self.volume_connector['uuid'], patch)
_, body = self.client.show_volume_connector(
self.volume_connector['uuid'])
self.assertEqual(new_connector_id, body['connector_id'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('5af8dc7a-9965-4787-8184-e60aeaf30957')
def test_update_volume_connector_replace_error(self):
"""Updating a volume connector.
Fail when updating a volume connector on node
with power on state.
"""
new_connector_id = data_utils.rand_name('connector_id')
patch = [{'path': '/connector_id',
'op': 'replace',
'value': new_connector_id}]
# Powering on the Node before updating a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power on')
regex_str = (r'.*The requested action \\\\"volume connector '
r'update\\\\" can not be performed on node*')
self.assertRaisesRegex(lib_exc.BadRequest,
regex_str,
self.client.update_volume_connector,
self.volume_connector['uuid'],
patch)
@decorators.idempotent_id('b95c75eb-4048-482e-99ff-fe1d32538383')
def test_update_volume_connector_remove_item(self):
"""Update a volume connector by removing one item from collection."""
new_extra = {'key1': 'value1'}
_, body = self.client.show_volume_connector(
self.volume_connector['uuid'])
connector_id = body['connector_id']
connector_type = body['type']
# Powering off the Node before updating a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power off')
# Removing one item from the collection
self.client.update_volume_connector(self.volume_connector['uuid'],
[{'path': '/extra/key2',
'op': 'remove'}])
_, body = self.client.show_volume_connector(
self.volume_connector['uuid'])
self.assertEqual(new_extra, body['extra'])
# Assert nothing else was changed
self.assertEqual(connector_id, body['connector_id'])
self.assertEqual(connector_type, body['type'])
@decorators.idempotent_id('8de03acd-532a-476f-8bc9-0e8b23bfe609')
def test_update_volume_connector_remove_collection(self):
"""Update a volume connector by removing collection."""
_, body = self.client.show_volume_connector(
self.volume_connector['uuid'])
connector_id = body['connector_id']
connector_type = body['type']
# Powering off the Node before updating a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power off')
# Removing the collection
self.client.update_volume_connector(self.volume_connector['uuid'],
[{'path': '/extra',
'op': 'remove'}])
_, body = self.client.show_volume_connector(
self.volume_connector['uuid'])
self.assertEqual({}, body['extra'])
# Assert nothing else was changed
self.assertEqual(connector_id, body['connector_id'])
self.assertEqual(connector_type, body['type'])
@decorators.idempotent_id('bfb0ca6b-086d-4663-9b25-e0eaf42da55b')
def test_update_volume_connector_add(self):
"""Update a volume connector by adding one item to collection."""
new_extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
patch = [{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']},
{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']}]
# Powering off the Node before updating a volume connector.
self.client.set_node_power_state(self.node['uuid'], 'power off')
self.client.update_volume_connector(
self.volume_connector['uuid'], patch)
_, body = self.client.show_volume_connector(
self.volume_connector['uuid'])
self.assertEqual(new_extra, body['extra'])

View File

@ -1,210 +0,0 @@
# Copyright 2017 FUJITSU LIMITED
#
# 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.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
from ironic_tempest_plugin.tests.api.admin import base
class TestVolumeTarget(base.BaseBaremetalTest):
"""Basic test cases for volume target."""
min_microversion = '1.32'
extra = {'key1': 'value1', 'key2': 'value2'}
def setUp(self):
super(TestVolumeTarget, self).setUp()
self.useFixture(
api_microversion_fixture.APIMicroversionFixture(
self.min_microversion))
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
_, self.volume_target = self.create_volume_target(
self.node['uuid'], volume_type=data_utils.rand_name('volume_type'),
volume_id=data_utils.rand_name('volume_id'),
boot_index=10,
extra=self.extra)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('da5c27d4-68cc-499f-b8ab-3048b87d3bca')
def test_create_volume_target_error(self):
"""Create a volume target.
Fail when creating a volume target with same boot index as the
existing volume target.
"""
regex_str = (r'.*A volume target .*already exists')
self.assertRaisesRegex(
lib_exc.Conflict, regex_str,
self.create_volume_target,
self.node['uuid'],
volume_type=data_utils.rand_name('volume_type'),
volume_id=data_utils.rand_name('volume_id'),
boot_index=self.volume_target['boot_index'])
@decorators.idempotent_id('ea3a9b2e-8971-4830-9274-abaf0239f1ce')
def test_delete_volume_target(self):
"""Delete a volume target."""
# Powering off the Node before deleting a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power off')
self.delete_volume_target(self.volume_target['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_volume_target,
self.volume_target['uuid'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('532a06bc-a9b2-44b0-828a-c53279c87cb2')
def test_delete_volume_target_error(self):
"""Fail when deleting a volume target on node with power on state."""
# Powering on the Node before deleting a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power on')
regex_str = (r'.*The requested action \\\\"volume target '
r'deletion\\\\" can not be performed on node*')
self.assertRaisesRegex(lib_exc.BadRequest,
regex_str,
self.delete_volume_target,
self.volume_target['uuid'])
@decorators.idempotent_id('a2598388-8f61-4b7e-944f-f37e4f60e1e2')
def test_show_volume_target(self):
"""Show a specified volume target."""
_, volume_target = self.client.show_volume_target(
self.volume_target['uuid'])
self._assertExpected(self.volume_target, volume_target)
@decorators.idempotent_id('ae99a986-d93c-4324-9cdc-41d89e3a659f')
def test_list_volume_targets(self):
"""List volume targets."""
_, body = self.client.list_volume_targets()
self.assertIn(self.volume_target['uuid'],
[i['uuid'] for i in body['targets']])
self.assertIn(self.volume_target['volume_type'],
[i['volume_type'] for i in body['targets']])
self.assertIn(self.volume_target['volume_id'],
[i['volume_id'] for i in body['targets']])
@decorators.idempotent_id('9da25447-0370-4b33-9c1f-d4503f5950ae')
def test_list_with_limit(self):
"""List volume targets with limit."""
_, body = self.client.list_volume_targets(limit=3)
next_marker = body['targets'][-1]['uuid']
self.assertIn(next_marker, body['next'])
@decorators.idempotent_id('8559cd08-feae-4f1a-a0ad-5bad8ea12b76')
def test_update_volume_target_replace(self):
"""Update a volume target by replacing volume id."""
new_volume_id = data_utils.rand_name('volume_id')
patch = [{'path': '/volume_id',
'op': 'replace',
'value': new_volume_id}]
# Powering off the Node before updating a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power off')
self.client.update_volume_target(self.volume_target['uuid'], patch)
_, body = self.client.show_volume_target(self.volume_target['uuid'])
self.assertEqual(new_volume_id, body['volume_id'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('fd5266d3-4f3c-4dce-9c87-bfdea2b756c7')
def test_update_volume_target_replace_error(self):
"""Fail when updating a volume target on node with power on state."""
new_volume_id = data_utils.rand_name('volume_id')
patch = [{'path': '/volume_id',
'op': 'replace',
'value': new_volume_id}]
# Powering on the Node before updating a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power on')
regex_str = (r'.*The requested action \\\\"volume target '
r'update\\\\" can not be performed on node*')
self.assertRaisesRegex(lib_exc.BadRequest,
regex_str,
self.client.update_volume_target,
self.volume_target['uuid'],
patch)
@decorators.idempotent_id('1c13a4ee-1a49-4739-8c19-77960fbd1af8')
def test_update_volume_target_remove_item(self):
"""Update a volume target by removing one item from the collection."""
new_extra = {'key1': 'value1'}
_, body = self.client.show_volume_target(self.volume_target['uuid'])
volume_id = body['volume_id']
volume_type = body['volume_type']
# Powering off the Node before updating a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power off')
# Removing one item from the collection
self.client.update_volume_target(self.volume_target['uuid'],
[{'path': '/extra/key2',
'op': 'remove'}])
_, body = self.client.show_volume_target(self.volume_target['uuid'])
self.assertEqual(new_extra, body['extra'])
# Assert nothing else was changed
self.assertEqual(volume_id, body['volume_id'])
self.assertEqual(volume_type, body['volume_type'])
@decorators.idempotent_id('6784ddb0-9144-41ea-b8a0-f888ad5c5b62')
def test_update_volume_target_remove_collection(self):
"""Update a volume target by removing the collection."""
_, body = self.client.show_volume_target(self.volume_target['uuid'])
volume_id = body['volume_id']
volume_type = body['volume_type']
# Powering off the Node before updating a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power off')
# Removing the collection
self.client.update_volume_target(self.volume_target['uuid'],
[{'path': '/extra',
'op': 'remove'}])
_, body = self.client.show_volume_target(self.volume_target['uuid'])
self.assertEqual({}, body['extra'])
# Assert nothing else was changed
self.assertEqual(volume_id, body['volume_id'])
self.assertEqual(volume_type, body['volume_type'])
@decorators.idempotent_id('9629715d-57ba-423b-b985-232674cc3a25')
def test_update_volume_target_add(self):
"""Update a volume target by adding to the collection."""
new_extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
patch = [{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']}]
# Powering off the Node before updating a volume target.
self.client.set_node_power_state(self.node['uuid'], 'power off')
self.client.update_volume_target(self.volume_target['uuid'], patch)
_, body = self.client.show_volume_target(self.volume_target['uuid'])
self.assertEqual(new_extra, body['extra'])

View File

@ -1,231 +0,0 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2013 IBM Corp.
# 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 time
from tempest.common import waiters
from tempest import config
from tempest.lib.common import api_version_utils
from tempest.lib import exceptions as lib_exc
from ironic_tempest_plugin import clients
from ironic_tempest_plugin.common import utils
from ironic_tempest_plugin.common import waiters as ironic_waiters
from ironic_tempest_plugin import manager
CONF = config.CONF
def retry_on_conflict(func):
def inner(*args, **kwargs):
# TODO(vsaienko): make number of retries and delay between
# them configurable in future.
e = None
for att in range(10):
try:
return func(*args, **kwargs)
except lib_exc.Conflict as e:
time.sleep(1)
raise lib_exc.Conflict(e)
return inner
# power/provision states as of icehouse
class BaremetalPowerStates(object):
"""Possible power states of an Ironic node."""
POWER_ON = 'power on'
POWER_OFF = 'power off'
REBOOT = 'rebooting'
SUSPEND = 'suspended'
class BaremetalProvisionStates(object):
"""Possible provision states of an Ironic node."""
ENROLL = 'enroll'
NOSTATE = None
AVAILABLE = 'available'
INIT = 'initializing'
ACTIVE = 'active'
BUILDING = 'building'
DEPLOYWAIT = 'wait call-back'
DEPLOYING = 'deploying'
DEPLOYFAIL = 'deploy failed'
DEPLOYDONE = 'deploy complete'
DELETING = 'deleting'
DELETED = 'deleted'
ERROR = 'error'
MANAGEABLE = 'manageable'
class BaremetalScenarioTest(manager.ScenarioTest):
credentials = ['primary', 'admin']
min_microversion = None
max_microversion = api_version_utils.LATEST_MICROVERSION
@classmethod
def skip_checks(cls):
super(BaremetalScenarioTest, cls).skip_checks()
if not CONF.service_available.ironic:
raise cls.skipException('Ironic is not enabled.')
cfg_min_version = CONF.baremetal.min_microversion
cfg_max_version = CONF.baremetal.max_microversion
api_version_utils.check_skip_with_microversion(cls.min_microversion,
cls.max_microversion,
cfg_min_version,
cfg_max_version)
@classmethod
def setup_clients(cls):
super(BaremetalScenarioTest, cls).setup_clients()
cls.baremetal_client = clients.Manager().baremetal_client
@classmethod
def resource_setup(cls):
super(BaremetalScenarioTest, cls).resource_setup()
# allow any issues obtaining the node list to raise early
cls.baremetal_client.list_nodes()
@classmethod
def wait_provisioning_state(cls, node_id, state, timeout=10, interval=1):
ironic_waiters.wait_for_bm_node_status(
cls.baremetal_client, node_id=node_id, attr='provision_state',
status=state, timeout=timeout, interval=interval)
@classmethod
def wait_power_state(cls, node_id, state):
ironic_waiters.wait_for_bm_node_status(
cls.baremetal_client, node_id=node_id, attr='power_state',
status=state, timeout=CONF.baremetal.power_timeout)
def wait_node(self, instance_id):
"""Waits for a node to be associated with instance_id."""
ironic_waiters.wait_node_instance_association(self.baremetal_client,
instance_id)
@classmethod
def get_node(cls, node_id=None, instance_id=None):
return utils.get_node(cls.baremetal_client, node_id, instance_id)
def get_ports(self, node_uuid):
ports = []
_, body = self.baremetal_client.list_node_ports(node_uuid)
for port in body['ports']:
_, p = self.baremetal_client.show_port(port['uuid'])
ports.append(p)
return ports
def get_node_vifs(self, node_uuid, api_version='1.28'):
_, body = self.baremetal_client.vif_list(node_uuid,
api_version=api_version)
return body['vifs']
def add_keypair(self):
self.keypair = self.create_keypair()
@classmethod
@retry_on_conflict
def update_node_driver(cls, node_id, driver):
_, body = cls.baremetal_client.update_node(
node_id, driver=driver)
return body
@classmethod
@retry_on_conflict
def update_node(cls, node_id, patch):
cls.baremetal_client.update_node(node_id, patch=patch)
@classmethod
@retry_on_conflict
def set_node_provision_state(cls, node_id, state, configdrive=None,
clean_steps=None):
cls.baremetal_client.set_node_provision_state(
node_id, state, configdrive=configdrive, clean_steps=clean_steps)
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 boot_instance(self, clients=None, keypair=None,
net_id=None, fixed_ip=None, **create_kwargs):
if clients is None:
servers_client = self.servers_client
else:
servers_client = clients.servers_client
if keypair is None:
keypair = self.keypair
if any([net_id, fixed_ip]):
network = {}
if net_id:
network['uuid'] = net_id
if fixed_ip:
network['fixed_ip'] = fixed_ip
instance = self.create_server(
key_name=keypair['name'],
networks=[network],
clients=clients,
**create_kwargs
)
else:
instance = self.create_server(
key_name=keypair['name'],
clients=clients,
**create_kwargs
)
self.wait_node(instance['id'])
node = self.get_node(instance_id=instance['id'])
self.wait_power_state(node['uuid'], BaremetalPowerStates.POWER_ON)
self.wait_provisioning_state(
node['uuid'],
[BaremetalProvisionStates.DEPLOYWAIT,
BaremetalProvisionStates.ACTIVE],
timeout=CONF.baremetal.deploywait_timeout)
self.wait_provisioning_state(node['uuid'],
BaremetalProvisionStates.ACTIVE,
timeout=CONF.baremetal.active_timeout,
interval=30)
waiters.wait_for_server_status(servers_client,
instance['id'], 'ACTIVE')
node = self.get_node(instance_id=instance['id'])
instance = servers_client.show_server(instance['id'])['server']
return instance, node
def terminate_instance(self, instance, servers_client=None):
if servers_client is None:
servers_client = self.servers_client
node = self.get_node(instance_id=instance['id'])
servers_client.delete_server(instance['id'])
self.wait_power_state(node['uuid'],
BaremetalPowerStates.POWER_OFF)
self.wait_provisioning_state(
node['uuid'],
[BaremetalProvisionStates.NOSTATE,
BaremetalProvisionStates.AVAILABLE],
timeout=CONF.baremetal.unprovision_timeout,
interval=30)

View File

@ -1,339 +0,0 @@
#
# Copyright 2017 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 random
from oslo_utils import uuidutils
from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions as lib_exc
from tempest.scenario import manager
from ironic_tempest_plugin.services.baremetal import base
from ironic_tempest_plugin.tests.scenario import baremetal_manager as bm
CONF = config.CONF
class BaremetalStandaloneManager(bm.BaremetalScenarioTest,
manager.NetworkScenarioTest):
credentials = ['primary', 'admin']
# NOTE(vsaienko): Standalone tests are using v1/node/<node_ident>/vifs to
# attach VIF to a node.
min_microversion = '1.28'
@classmethod
def skip_checks(cls):
"""Defines conditions to skip these tests."""
super(BaremetalStandaloneManager, cls).skip_checks()
if CONF.service_available.nova:
raise cls.skipException('Nova is enabled. Stand-alone tests will '
'be skipped.')
@classmethod
def create_networks(cls):
"""Create a network with a subnet connected to a router.
Return existed network specified in compute/fixed_network_name
config option.
TODO(vsaienko): Add network/subnet/router when we setup
ironic-standalone with multitenancy.
:returns: network, subnet, router
"""
network = None
subnet = None
router = None
if CONF.network.shared_physical_network:
if not CONF.compute.fixed_network_name:
m = ('Configuration option "[compute]/fixed_network_name" '
'must be set.')
raise lib_exc.InvalidConfiguration(m)
network = cls.os_admin.networks_client.list_networks(
name=CONF.compute.fixed_network_name)['networks'][0]
return network, subnet, router
@classmethod
def get_available_nodes(cls):
"""Get all ironic nodes that can be deployed.
We can deploy on nodes when the following conditions are met:
* provision_state is 'available'
* maintenance is False
* No instance_uuid is associated to node.
:returns: a list of Ironic nodes.
"""
fields = ['uuid', 'driver', 'instance_uuid', 'provision_state',
'name', 'maintenance']
_, body = cls.baremetal_client.list_nodes(provision_state='available',
associated=False,
maintenance=False,
fields=','.join(fields))
return body['nodes']
@classmethod
def get_random_available_node(cls):
"""Randomly pick an available node for deployment."""
nodes = cls.get_available_nodes()
if nodes:
return random.choice(nodes)
@classmethod
def create_neutron_port(cls, *args, **kwargs):
"""Creates a neutron port.
For a full list of available parameters, please refer to the official
API reference:
http://developer.openstack.org/api-ref/networking/v2/index.html#create-port
:returns: server response body.
"""
port = cls.ports_client.create_port(*args, **kwargs)['port']
return port
@classmethod
def _associate_instance_with_node(cls, node_id, instance_uuid):
"""Update instance_uuid for a given node.
:param node_id: Name or UUID of the node.
:param instance_uuid: UUID of the instance to associate.
:returns: server response body.
"""
_, body = cls.baremetal_client.update_node(
node_id, instance_uuid=instance_uuid)
return body
@classmethod
def get_node_vifs(cls, node_id):
"""Return a list of VIFs for a given node.
:param node_id: Name or UUID of the node.
:returns: A list of VIFs associated with the node.
"""
_, body = cls.baremetal_client.vif_list(node_id)
vifs = [v['id'] for v in body['vifs']]
return vifs
@classmethod
def add_floatingip_to_node(cls, node_id):
"""Add floating IP to node.
Create and associate floating IP with node VIF.
:param node_id: Name or UUID of the node.
:returns: IP address of associated floating IP.
"""
vif = cls.get_node_vifs(node_id)[0]
body = cls.floating_ips_client.create_floatingip(
floating_network_id=CONF.network.public_network_id)
floating_ip = body['floatingip']
cls.floating_ips_client.update_floatingip(floating_ip['id'],
port_id=vif)
return floating_ip['floating_ip_address']
@classmethod
def cleanup_floating_ip(cls, ip_address):
"""Removes floating IP."""
body = cls.os_admin.floating_ips_client.list_floatingips()
floating_ip_id = [f['id'] for f in body['floatingips'] if
f['floating_ip_address'] == ip_address][0]
cls.os_admin.floating_ips_client.delete_floatingip(floating_ip_id)
@classmethod
@bm.retry_on_conflict
def detach_all_vifs_from_node(cls, node_id):
"""Detach all VIFs from a given node.
:param node_id: Name or UUID of the node.
"""
vifs = cls.get_node_vifs(node_id)
for vif in vifs:
cls.baremetal_client.vif_detach(node_id, vif)
@classmethod
@bm.retry_on_conflict
def vif_attach(cls, node_id, vif_id):
"""Attach VIF to a give node.
:param node_id: Name or UUID of the node.
:param vif_id: Identifier of the VIF to attach.
"""
cls.baremetal_client.vif_attach(node_id, vif_id)
@classmethod
def get_and_reserve_node(cls, node=None):
"""Pick an available node for deployment and reserve it.
Only one instance_uuid may be associated, use this behaviour as
reservation node when tests are launched concurrently. If node is
not passed directly pick random available for deployment node.
:param node: Ironic node to associate instance_uuid with.
:returns: Ironic node.
"""
instance_uuid = uuidutils.generate_uuid()
nodes = []
def _try_to_associate_instance():
n = node or cls.get_random_available_node()
try:
cls._associate_instance_with_node(n['uuid'], instance_uuid)
nodes.append(n)
except lib_exc.Conflict:
return False
return True
if (not test_utils.call_until_true(_try_to_associate_instance,
duration=CONF.baremetal.association_timeout, sleep_for=1)):
msg = ('Timed out waiting to associate instance to ironic node '
'uuid %s' % instance_uuid)
raise lib_exc.TimeoutException(msg)
return nodes[0]
@classmethod
def boot_node(cls, driver, image_ref, image_checksum=None):
"""Boot ironic node.
The following actions are executed:
* Randomly pick an available node for deployment and reserve it.
* Update node driver.
* Create/Pick networks to boot node in.
* Create Neutron port and attach it to node.
* Update node image_source/root_gb.
* Deploy node.
* Wait until node is deployed.
:param driver: Node driver to use.
:param image_ref: Reference to user image to boot node with.
:param image_checksum: md5sum of image specified in image_ref.
Needed only when direct HTTP link is provided.
:returns: Ironic node.
"""
node = cls.get_and_reserve_node()
cls.update_node_driver(node['uuid'], driver)
network, subnet, router = cls.create_networks()
n_port = cls.create_neutron_port(network_id=network['id'])
cls.vif_attach(node_id=node['uuid'], vif_id=n_port['id'])
patch = [{'path': '/instance_info/image_source',
'op': 'add',
'value': image_ref}]
if image_checksum is not None:
patch.append({'path': '/instance_info/image_checksum',
'op': 'add',
'value': image_checksum})
patch.append({'path': '/instance_info/root_gb',
'op': 'add',
'value': CONF.baremetal.adjusted_root_disk_size_gb})
# TODO(vsaienko) add testing for custom configdrive
cls.update_node(node['uuid'], patch=patch)
cls.set_node_provision_state(node['uuid'], 'active')
cls.wait_power_state(node['uuid'], bm.BaremetalPowerStates.POWER_ON)
cls.wait_provisioning_state(node['uuid'],
bm.BaremetalProvisionStates.ACTIVE,
timeout=CONF.baremetal.active_timeout,
interval=30)
return node
@classmethod
def terminate_node(cls, node_id):
"""Terminate active ironic node.
The following actions are executed:
* Detach all VIFs from the given node.
* Unprovision node.
* Wait until node become available.
:param node_id: Name or UUID for the node.
"""
cls.detach_all_vifs_from_node(node_id)
cls.set_node_provision_state(node_id, 'deleted')
# NOTE(vsaienko) We expect here fast switching from deleted to
# available as automated cleaning is disabled so poll status each 1s.
cls.wait_provisioning_state(
node_id,
[bm.BaremetalProvisionStates.NOSTATE,
bm.BaremetalProvisionStates.AVAILABLE],
timeout=CONF.baremetal.unprovision_timeout,
interval=1)
class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
# API microversion to use among all calls
api_microversion = '1.28'
# The node driver to use in the test
driver = None
# User image ref to boot node with.
image_ref = None
# Boolean value specify if image is wholedisk or not.
wholedisk_image = None
# Image checksum, required when image is stored on HTTP server.
image_checksum = None
mandatory_attr = ['driver', 'image_ref', 'wholedisk_image']
node = None
node_ip = None
@classmethod
def skip_checks(cls):
super(BaremetalStandaloneScenarioTest, cls).skip_checks()
if (cls.driver not in CONF.baremetal.enabled_drivers +
CONF.baremetal.enabled_hardware_types):
raise cls.skipException(
'The driver: %(driver)s used in test is not in the list of '
'enabled_drivers %(enabled_drivers)s or '
'enabled_hardware_types %(enabled_hw_types)s '
'in the tempest config.' % {
'driver': cls.driver,
'enabled_drivers': CONF.baremetal.enabled_drivers,
'enabled_hw_types': CONF.baremetal.enabled_hardware_types})
if not cls.wholedisk_image and CONF.baremetal.use_provision_network:
raise cls.skipException(
'Partitioned images are not supported with multitenancy.')
@classmethod
def resource_setup(cls):
super(BaremetalStandaloneScenarioTest, cls).resource_setup()
base.set_baremetal_api_microversion(cls.api_microversion)
for v in cls.mandatory_attr:
if getattr(cls, v) is None:
raise lib_exc.InvalidConfiguration(
"Mandatory attribute %s not set." % v)
image_checksum = None
if not uuidutils.is_uuid_like(cls.image_ref):
image_checksum = cls.image_checksum
cls.node = cls.boot_node(cls.driver, cls.image_ref,
image_checksum=image_checksum)
cls.node_ip = cls.add_floatingip_to_node(cls.node['uuid'])
@classmethod
def resource_cleanup(cls):
cls.cleanup_floating_ip(cls.node_ip)
vifs = cls.get_node_vifs(cls.node['uuid'])
# Remove ports before deleting node, to catch regression for cases
# when user did this prior unprovision node.
for vif in vifs:
cls.ports_client.delete_port(vif)
cls.terminate_node(cls.node['uuid'])
base.reset_baremetal_api_microversion()
super(BaremetalStandaloneManager, cls).resource_cleanup()

View File

@ -1,145 +0,0 @@
#
# Copyright 2017 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.
from tempest.common import utils
from tempest import config
from tempest.lib import decorators
from ironic_tempest_plugin.tests.scenario import \
baremetal_standalone_manager as bsm
CONF = config.CONF
class BaremetalAgentIpmitoolWholedisk(bsm.BaremetalStandaloneScenarioTest):
driver = 'agent_ipmitool'
image_ref = CONF.baremetal.whole_disk_image_ref
wholedisk_image = True
@decorators.idempotent_id('defff515-a6ff-44f6-9d8d-2ded51196d98')
@utils.services('image', 'network', 'object_storage')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalAgentIpmitoolWholediskHttpLink(
bsm.BaremetalStandaloneScenarioTest):
driver = 'agent_ipmitool'
image_ref = CONF.baremetal.whole_disk_image_url
image_checksum = CONF.baremetal.whole_disk_image_checksum
wholedisk_image = True
@classmethod
def skip_checks(cls):
super(BaremetalAgentIpmitoolWholediskHttpLink, cls).skip_checks()
if not CONF.baremetal_feature_enabled.ipxe_enabled:
skip_msg = ("HTTP server is not available when ipxe is disabled.")
raise cls.skipException(skip_msg)
@decorators.idempotent_id('d926c683-1a32-44df-afd0-e60134346fd0')
@utils.services('network')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalAgentIpmitoolPartitioned(bsm.BaremetalStandaloneScenarioTest):
driver = 'agent_ipmitool'
image_ref = CONF.baremetal.partition_image_ref
wholedisk_image = False
@decorators.idempotent_id('27b86130-d8dc-419d-880a-fbbbe4ce3f8c')
@utils.services('image', 'network', 'object_storage')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalPxeIpmitoolWholedisk(bsm.BaremetalStandaloneScenarioTest):
driver = 'pxe_ipmitool'
image_ref = CONF.baremetal.whole_disk_image_ref
wholedisk_image = True
@decorators.idempotent_id('d8c5badd-45db-4d05-bbe8-35babbed6e86')
@utils.services('image', 'network')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalPxeIpmitoolWholediskHttpLink(
bsm.BaremetalStandaloneScenarioTest):
driver = 'pxe_ipmitool'
image_ref = CONF.baremetal.whole_disk_image_url
image_checksum = CONF.baremetal.whole_disk_image_checksum
wholedisk_image = True
@classmethod
def skip_checks(cls):
super(BaremetalPxeIpmitoolWholediskHttpLink, cls).skip_checks()
if not CONF.baremetal_feature_enabled.ipxe_enabled:
skip_msg = ("HTTP server is not available when ipxe is disabled.")
raise cls.skipException(skip_msg)
@decorators.idempotent_id('71ccf06f-6765-40fd-8252-1b1bfa423b9b')
@utils.services('network')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalPxeIpmitoolPartitioned(bsm.BaremetalStandaloneScenarioTest):
driver = 'pxe_ipmitool'
image_ref = CONF.baremetal.partition_image_ref
wholedisk_image = False
@decorators.idempotent_id('ea85e19c-6869-4577-b9bb-2eb150f77c90')
@utils.services('image', 'network')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalIpmiWholedisk(bsm.BaremetalStandaloneScenarioTest):
driver = 'ipmi'
image_ref = CONF.baremetal.whole_disk_image_ref
wholedisk_image = True
@decorators.idempotent_id('c2db24e7-07dc-4a20-8f93-d4efae2bfd4e')
@utils.services('image', 'network')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))
class BaremetalIpmiPartitioned(bsm.BaremetalStandaloneScenarioTest):
driver = 'ipmi'
image_ref = CONF.baremetal.partition_image_ref
wholedisk_image = False
@decorators.idempotent_id('7d0b205e-edbc-4e2d-9f6d-95cd74eefecb')
@utils.services('image', 'network')
def test_ip_access_to_server(self):
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=True))

View File

@ -1,146 +0,0 @@
#
# 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 oslo_log import log as logging
from tempest.common import utils
from tempest.common import waiters
from tempest import config
from tempest.lib.common import api_version_request
from tempest.lib import decorators
from ironic_tempest_plugin.tests.scenario import baremetal_manager
LOG = logging.getLogger(__name__)
CONF = config.CONF
class BaremetalBasicOps(baremetal_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 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
* Verifies instance rebuild with ephemeral partition preservation
* Deletes instance
* Monitors the associated Ironic node for power and
expected state transitions
"""
def rebuild_instance(self, preserve_ephemeral=False):
self.rebuild_server(server_id=self.instance['id'],
preserve_ephemeral=preserve_ephemeral,
wait=False)
node = self.get_node(instance_id=self.instance['id'])
# We should remain on the same node
self.assertEqual(self.node['uuid'], node['uuid'])
self.node = node
waiters.wait_for_server_status(
self.servers_client,
server_id=self.instance['id'],
status='REBUILD',
ready_wait=False)
waiters.wait_for_server_status(
self.servers_client,
server_id=self.instance['id'],
status='ACTIVE')
def verify_partition(self, client, label, mount, gib_size):
"""Verify a labeled partition's mount point and size."""
LOG.info("Looking for partition %s mounted on %s", label, mount)
# Validate we have a device with the given partition label
cmd = "/sbin/blkid | grep '%s' | cut -d':' -f1" % label
device = client.exec_command(cmd).rstrip('\n')
LOG.debug("Partition device is %s", device)
self.assertNotEqual('', device)
# Validate the mount point for the device
cmd = "mount | grep '%s' | cut -d' ' -f3" % device
actual_mount = client.exec_command(cmd).rstrip('\n')
LOG.debug("Partition mount point is %s", actual_mount)
self.assertEqual(actual_mount, mount)
# Validate the partition size matches what we expect
numbers = '0123456789'
devnum = device.replace('/dev/', '')
cmd = "cat /sys/block/%s/%s/size" % (devnum.rstrip(numbers), devnum)
num_bytes = client.exec_command(cmd).rstrip('\n')
num_bytes = int(num_bytes) * 512
actual_gib_size = num_bytes / (1024 * 1024 * 1024)
LOG.debug("Partition size is %d GiB", actual_gib_size)
self.assertEqual(actual_gib_size, gib_size)
def get_flavor_ephemeral_size(self):
"""Returns size of the ephemeral partition in GiB."""
f_id = self.instance['flavor']['id']
flavor = self.flavors_client.show_flavor(f_id)['flavor']
ephemeral = flavor.get('OS-FLV-EXT-DATA:ephemeral')
if not ephemeral or ephemeral == 'N/A':
return None
return int(ephemeral)
def validate_ports(self):
node_uuid = self.node['uuid']
vifs = []
# TODO(vsaienko) switch to get_node_vifs() when all stable releases
# supports Ironic API 1.28
if (api_version_request.APIVersionRequest(
CONF.baremetal.max_microversion) >=
api_version_request.APIVersionRequest('1.28')):
vifs = self.get_node_vifs(node_uuid)
else:
for port in self.get_ports(self.node['uuid']):
vif = port['extra'].get('vif_port_id')
if vif:
vifs.append({'id': vif})
ir_ports = self.get_ports(node_uuid)
ir_ports_addresses = [x['address'] for x in ir_ports]
for vif in vifs:
n_port_id = vif['id']
body = self.ports_client.show_port(n_port_id)
n_port = body['port']
self.assertEqual(n_port['device_id'], self.instance['id'])
self.assertIn(n_port['mac_address'], ir_ports_addresses)
@decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
@utils.services('compute', 'image', 'network')
def test_baremetal_server_ops(self):
self.add_keypair()
self.instance, self.node = self.boot_instance()
self.validate_ports()
ip_address = self.get_server_ip(self.instance)
self.get_remote_client(ip_address).validate_authentication()
vm_client = self.get_remote_client(ip_address)
# We expect the ephemeral partition to be mounted on /mnt and to have
# the same size as our flavor definition.
eph_size = self.get_flavor_ephemeral_size()
if eph_size:
self.verify_partition(vm_client, 'ephemeral0', '/mnt', eph_size)
# Create the test file
self.create_timestamp(
ip_address, private_key=self.keypair['private_key'])
self.terminate_instance(self.instance)

View File

@ -1,152 +0,0 @@
# Copyright 2017 FUJITSU LIMITED
#
# 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.common import utils
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from ironic_tempest_plugin.tests.scenario import baremetal_manager
CONF = config.CONF
class BaremetalBFV(baremetal_manager.BaremetalScenarioTest):
"""Check baremetal instance that can boot from Cinder volume:
* Create a volume from an image
* Create a keypair
* Boot an instance from the volume using the keypair
* Verify instance IP address
* Delete instance
"""
credentials = ['primary', 'admin']
min_microversion = '1.32'
@classmethod
def skip_checks(cls):
super(BaremetalBFV, cls).skip_checks()
if CONF.baremetal.use_provision_network:
msg = 'Ironic boot-from-volume requires a flat network.'
raise cls.skipException(msg)
def create_volume(self, size=None, name=None, snapshot_id=None,
image_id=None, volume_type=None):
if size is None:
size = CONF.volume.volume_size
if image_id is None:
image = self.compute_images_client.show_image(image_id)['image']
min_disk = image.get('minDisk')
size = max(size, min_disk)
if name is None:
name = data_utils.rand_name(self.__class__.__name__ + "-volume")
kwargs = {'display_name': name,
'snapshot_id': snapshot_id,
'imageRef': image_id,
'volume_type': volume_type,
'size': size}
volume = self.volumes_client.create_volume(**kwargs)['volume']
self.addCleanup(self.volumes_client.wait_for_resource_deletion,
volume['id'])
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.volumes_client.delete_volume, volume['id'])
self.assertEqual(name, volume['name'])
waiters.wait_for_volume_resource_status(self.volumes_client,
volume['id'], 'available')
# The volume retrieved on creation has a non-up-to-date status.
# Retrieval after it becomes active ensures correct details.
volume = self.volumes_client.show_volume(volume['id'])['volume']
return volume
def _create_volume_from_image(self):
"""Create a cinder volume from the default image."""
image_id = CONF.compute.image_ref
vol_name = data_utils.rand_name(
self.__class__.__name__ + '-volume-origin')
return self.create_volume(name=vol_name, image_id=image_id)
def _get_bdm(self, source_id, source_type, delete_on_termination=False):
"""Create block device mapping config options dict.
:param source_id: id of the source device.
:param source_type: type of the source device.
:param delete_on_termination: what to do with the volume when
the instance is terminated.
:return: a dictionary of configuration options for block
device mapping.
"""
bd_map_v2 = [{
'uuid': source_id,
'source_type': source_type,
'destination_type': 'volume',
'boot_index': 0,
'delete_on_termination': delete_on_termination}]
return {'block_device_mapping_v2': bd_map_v2}
def _boot_instance_from_resource(self, source_id,
source_type,
keypair=None,
delete_on_termination=False):
"""Boot instance from the specified resource."""
# NOTE(tiendc): Boot to the volume, use image_id=''.
# We don't pass image_id=None as that will cause the default image
# to be used.
create_kwargs = {'image_id': ''}
create_kwargs.update(self._get_bdm(
source_id,
source_type,
delete_on_termination=delete_on_termination))
return self.boot_instance(
clients=self.os_primary,
keypair=keypair,
**create_kwargs
)
@decorators.idempotent_id('d6e05e61-8221-44ac-b785-57545f8e0fcf')
@utils.services('compute', 'image', 'network', 'volume')
def test_baremetal_boot_from_volume(self):
"""Test baremetal node can boot from a cinder volume.
This test function first creates a cinder volume from an image.
Then it executes "server create" action with appropriate block
device mapping config options, the baremetal node will boot
from the newly created volume. This requires a volume connector
is created for the node, and the node capabilities flag
iscsi_boot is set to true.
"""
# Create volume from image
volume_origin = self._create_volume_from_image()
# NOTE: node properties/capabilities for flag iscsi_boot=true,
# and volume connector should be added by devstack already.
# Boot instance
self.keypair = self.create_keypair()
self.instance, self.node = self._boot_instance_from_resource(
source_id=volume_origin['id'],
source_type='volume',
keypair=self.keypair
)
# Get server ip and validate authentication
ip_address = self.get_server_ip(self.instance)
self.get_remote_client(ip_address).validate_authentication()
self.terminate_instance(instance=self.instance)

View File

@ -1,153 +0,0 @@
#
# Copyright (c) 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.
from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from ironic_tempest_plugin import manager
from ironic_tempest_plugin.tests.scenario import baremetal_manager
CONF = config.CONF
class BaremetalMultitenancy(baremetal_manager.BaremetalScenarioTest,
manager.NetworkScenarioTest):
"""Check L2 isolation of baremetal instances in different tenants:
* Create a keypair, network, subnet and router for the primary tenant
* Boot 2 instances in the different tenant's network using the keypair
* Associate floating ips to both instance
* Verify there is no L3 connectivity between instances of different tenants
* Verify connectivity between instances floating IP's
* Delete both instances
"""
credentials = ['primary', 'alt', 'admin']
@classmethod
def skip_checks(cls):
super(BaremetalMultitenancy, cls).skip_checks()
if not CONF.baremetal.use_provision_network:
msg = 'Ironic/Neutron tenant isolation is not configured.'
raise cls.skipException(msg)
def create_tenant_network(self, clients, tenant_cidr):
network = self._create_network(
networks_client=clients.networks_client,
tenant_id=clients.credentials.tenant_id)
router = self._get_router(
client=clients.routers_client,
tenant_id=clients.credentials.tenant_id)
result = clients.subnets_client.create_subnet(
name=data_utils.rand_name('subnet'),
network_id=network['id'],
tenant_id=clients.credentials.tenant_id,
ip_version=4,
cidr=tenant_cidr)
subnet = result['subnet']
clients.routers_client.add_router_interface(router['id'],
subnet_id=subnet['id'])
self.addCleanup(clients.subnets_client.delete_subnet, subnet['id'])
self.addCleanup(clients.routers_client.remove_router_interface,
router['id'], subnet_id=subnet['id'])
return network, subnet, router
def verify_l3_connectivity(self, source_ip, private_key,
destination_ip, conn_expected=True):
remote = self.get_remote_client(source_ip, private_key=private_key)
remote.validate_authentication()
cmd = 'ping %s -c4 -w4 || exit 0' % destination_ip
success_substring = "64 bytes from %s" % destination_ip
output = remote.exec_command(cmd)
if conn_expected:
self.assertIn(success_substring, output)
else:
self.assertNotIn(success_substring, output)
@decorators.idempotent_id('26e2f145-2a8e-4dc7-8457-7f2eb2c6749d')
@utils.services('compute', 'image', 'network')
def test_baremetal_multitenancy(self):
tenant_cidr = '10.0.100.0/24'
fixed_ip1 = '10.0.100.3'
fixed_ip2 = '10.0.100.5'
keypair = self.create_keypair()
network, subnet, router = self.create_tenant_network(
self.os_primary, tenant_cidr)
# Boot 2 instances in the primary tenant network
# and check L2 connectivity between them
instance1, node1 = self.boot_instance(
clients=self.os_primary,
keypair=keypair,
net_id=network['id'],
fixed_ip=fixed_ip1
)
floating_ip1 = self.create_floating_ip(
instance1,
)['floating_ip_address']
self.check_vm_connectivity(ip_address=floating_ip1,
private_key=keypair['private_key'])
# Boot instance in the alt tenant network and ensure there is no
# L2 connectivity between instances of the different tenants
alt_keypair = self.create_keypair(self.os_alt.keypairs_client)
alt_network, alt_subnet, alt_router = self.create_tenant_network(
self.os_alt, tenant_cidr)
alt_instance, alt_node = self.boot_instance(
keypair=alt_keypair,
clients=self.os_alt,
net_id=alt_network['id'],
fixed_ip=fixed_ip2
)
alt_floating_ip = self.create_floating_ip(
alt_instance,
client=self.os_alt.floating_ips_client
)['floating_ip_address']
self.check_vm_connectivity(ip_address=alt_floating_ip,
private_key=alt_keypair['private_key'])
self.verify_l3_connectivity(
alt_floating_ip,
alt_keypair['private_key'],
fixed_ip1,
conn_expected=False
)
self.verify_l3_connectivity(
floating_ip1,
keypair['private_key'],
fixed_ip2,
conn_expected=False
)
self.verify_l3_connectivity(
floating_ip1,
keypair['private_key'],
alt_floating_ip,
conn_expected=True
)
self.terminate_instance(
instance=alt_instance,
servers_client=self.os_alt.servers_client)
self.terminate_instance(instance=instance1)

View File

@ -0,0 +1,9 @@
---
other:
- |
The tempest plugin code that was in ``ironic_tempest_plugin/`` has been
removed. Tempest plugin code has been migrated to the project
`openstack/ironic-tempest-plugin
<https://git.openstack.org/cgit/openstack/ironic-tempest-plugin>`_. This
was an OpenStack wide `goal for the Queens cycle
<https://governance.openstack.org/tc/goals/queens/split-tempest-plugins.html>`_.

View File

@ -23,7 +23,6 @@ data_files =
etc/ironic/rootwrap.d = etc/ironic/rootwrap.d/* etc/ironic/rootwrap.d = etc/ironic/rootwrap.d/*
packages = packages =
ironic ironic
ironic_tempest_plugin
[entry_points] [entry_points]
oslo.config.opts = oslo.config.opts =
@ -183,15 +182,11 @@ ironic.hardware.types =
ironic.database.migration_backend = ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration sqlalchemy = ironic.db.sqlalchemy.migration
tempest.test_plugins =
ironic_tests = ironic_tempest_plugin.plugin:IronicTempestPlugin
[pbr] [pbr]
autodoc_index_modules = True autodoc_index_modules = True
autodoc_exclude_modules = autodoc_exclude_modules =
ironic.db.sqlalchemy.alembic.env ironic.db.sqlalchemy.alembic.env
ironic.db.sqlalchemy.alembic.versions.* ironic.db.sqlalchemy.alembic.versions.*
ironic_tempest_plugin.*
ironic.drivers.modules.ansible.playbooks* ironic.drivers.modules.ansible.playbooks*
api_doc_dir = contributor/api api_doc_dir = contributor/api

View File

@ -9,24 +9,6 @@
# tox -epep8 -- -HEAD # tox -epep8 -- -HEAD
# #
TEMP_SHA_FILE="plugin.$$.SHA256SUM"
find ironic_tempest_plugin/ -type f | xargs sha256sum | sort > ${TEMP_SHA_FILE}
if ! diff -q ${TEMP_SHA_FILE} tools/ironic_tempest_plugin.SHA256SUM;
then
rm ${TEMP_SHA_FILE}
echo ""
echo "*******************************************************"
echo "ERROR: Detected changes made to the ironic_tempest_plugin/ directory"
echo "ERROR: Changes to the ironic_tempest_plugin/ are not allowed as"
echo "ERROR: we no longer use that content and it will be removed"
echo "ERROR: Please add changes to the tempest tests in the repository:"
echo "ERROR: openstack/ironic-tempest-plugin"
echo "*******************************************************"
echo ""
exit 1
fi
rm ${TEMP_SHA_FILE}
if test "x$1" = "x-HEAD" ; then if test "x$1" = "x-HEAD" ; then
shift shift
files=$(git diff --name-only HEAD~1 | tr '\n' ' ') files=$(git diff --name-only HEAD~1 | tr '\n' ' ')

View File

@ -1,38 +0,0 @@
0ab102bcc2208285e9390ba5582648eb90be9a78e46ca847832caa7364a4b71d ironic_tempest_plugin/tests/api/admin/test_ports.py
0e298a785d0d76cfc0bd35460e9cf20e68d06275f4af4e6cc534c86b0c77bfba ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
1217c75e72b8d2ed84f1f34ca9886032bbf0923d21a9930b4f8ea7520723b0ae ironic_tempest_plugin/tests/api/admin/test_portgroups.py
20c648134441fd39a72a24b011b012f0e58057560ff713eb261eebfa9a8f402c ironic_tempest_plugin/README.rst
3226a0449cae731d9706c6d036e1cc4d189d52ab854e0a5f7321122deba87c67 ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
41d9589c33d784c7d0b1c094f8052ce742a32fd5c6c1ecbfd45c23a42277fcf5 ironic_tempest_plugin/tests/api/admin/test_chassis.py
49446e3dfe214cdecec7121fe0d8a4f83a1db9c5b1dc6cd370427fff19bac072 ironic_tempest_plugin/tests/scenario/baremetal_manager.py
5ef9ed14ee2bdd1e0c94f35f2e7b9daf633ac4ca347f008fac224db245a20697 ironic_tempest_plugin/services/baremetal/base.py
727c722b1012c006226904456a96ac891e23be2ecbd9f301c4dfb7cf4b2412d6 ironic_tempest_plugin/clients.py
79fc7402060de6bdbb198148314bcdead6bae6350ccdee6407acb88b72990eff ironic_tempest_plugin/common/utils.py
891e10aac2728237718f9116d721ef61cb5c8ffa431fa3d2da55e125934e8c25 ironic_tempest_plugin/tests/scenario/test_baremetal_multitenancy.py
9836f4687f371942db76d987b3c9e6aa7cf72b98e040ac7f7844133f6ac371f1 ironic_tempest_plugin/tests/api/admin/api_microversion_fixture.py
9b82de2d28ca5cf76d03bcf2c6ff72898718c4516fa65040cdda63ee7e1482a1 ironic_tempest_plugin/tests/api/admin/test_nodes.py
9e69376e083d1abcb288aabe35e8d8903c1afa47796c65357fd193e681c30743 ironic_tempest_plugin/manager.py
ac50364f648c8b4d581c428aa65ef530f4382f07cb2c01a87589234a8373c5a3 ironic_tempest_plugin/tests/api/admin/test_volume_target.py
b5f2aed1f89b4c716d4b3915a388ccf3ed9c0eb72be20ec5b8857259e9db8664 ironic_tempest_plugin/common/waiters.py
bc899847544846a0413c25cc6656c7bdec7d1a772b4681f8bb09d3e71bdae61a ironic_tempest_plugin/config.py
bdcf5b98ee3c63d5d54bd824e650be19b5b991ba54ce60dc951a942c44f9fcd8 ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
c396d4d00f4d3078cbf5f89c06ac5db107d82f1fa5bae00da5bff5e3ddd56cb5 ironic_tempest_plugin/tests/api/admin/base.py
ccb280293b155925fec767a8e3c9add917f9e14a5fccd73dde2fe57ce62ce367 ironic_tempest_plugin/tests/scenario/test_baremetal_boot_from_volume.py
d7390c03610c91a71f2b9864a213c2751b9b269c2701982942054fd5f3b0a7cd ironic_tempest_plugin/tests/api/admin/test_api_discovery.py
d9543670e0de470f6a2ed2957cd712fcef1bf32006fc07dc4152f546bd5dd175 ironic_tempest_plugin/tests/api/admin/test_ports_negative.py
db55378fdc5a4a3c00b8901d25ba83e82cd02e8232ae0627ba31f46edc5f5f9c ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/common/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/services/baremetal/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/services/baremetal/v1/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/services/baremetal/v1/json/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/services/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/tests/api/admin/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/tests/api/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/tests/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/tests/scenario/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ironic_tempest_plugin/tests/scenario/ironic_standalone/__init__.py
eb4fddf42a72dce35bfc42a50367708e596ecdc158a14052d1c5f226e13332ed ironic_tempest_plugin/plugin.py
f60331afe7291c0068a42589b756ad51ec4941c8c1231b7b657a0a82ade6f958 ironic_tempest_plugin/tests/api/admin/test_drivers.py
f8ee75c2687ae8b99832201f459ffe5f7b6eac16b50a5713ee224039bd997110 ironic_tempest_plugin/tests/api/admin/test_volume_connector.py
fdf8369063f1fb2b357012b9d864421be98e3a2c4f52987b240b1980a66cbedc ironic_tempest_plugin/tests/api/admin/test_nodestates.py