Merge "Remove ironic_tempest_plugin/ directory"
This commit is contained in:
commit
7ac39e4029
@ -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
|
||||||
|
5
ironic_tempest_plugin/PLUGIN-MOVED
Normal file
5
ironic_tempest_plugin/PLUGIN-MOVED
Normal 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.
|
@ -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
|
|
@ -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)
|
|
@ -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]
|
|
@ -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)
|
|
@ -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"),
|
|
||||||
]
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'])
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'])
|
|
@ -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']])
|
|
@ -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'])
|
|
@ -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)
|
|
@ -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'])
|
|
@ -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'])
|
|
@ -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)
|
|
@ -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()
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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>`_.
|
@ -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
|
||||||
|
|
||||||
|
@ -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' ' ')
|
||||||
|
@ -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
|
|
Loading…
Reference in New Issue
Block a user