blob: 54695791a34ab6908edb41b1e94c803f1e235848 [file] [log] [blame]
Yuiko Takadab6527002015-12-07 11:49:12 +09001# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
Takashi Kajinami6bddaab2022-05-10 00:58:56 +090013import functools
14
Julia Kregercda96d52023-01-20 08:05:41 -080015from oslo_log import log as logging
Yuiko Takadab6527002015-12-07 11:49:12 +090016from tempest import config
Yuiko Takadaff785002015-12-17 15:56:42 +090017from tempest.lib.common import api_version_utils
Lenny Verkhovsky88625042016-03-08 17:44:00 +020018from tempest.lib.common.utils import data_utils
19from tempest.lib import exceptions as lib_exc
Yuiko Takadab6527002015-12-07 11:49:12 +090020from tempest import test
Yuiko Takadab6527002015-12-07 11:49:12 +090021
Dmitry Tantsur0325dbd2018-10-24 15:13:46 +020022from ironic_tempest_plugin.common import waiters
Mark Goddard6f2e72c2019-02-15 12:23:34 +000023from ironic_tempest_plugin.services.baremetal import base
Yuiko Takadaff785002015-12-17 15:56:42 +090024from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
Yuiko Takadab6527002015-12-07 11:49:12 +090025
Julia Kregercda96d52023-01-20 08:05:41 -080026
27LOG = logging.getLogger(__name__)
Yuiko Takadab6527002015-12-07 11:49:12 +090028CONF = config.CONF
29
30
31# NOTE(adam_g): The baremetal API tests exercise operations such as enroll
32# node, power on, power off, etc. Testing against real drivers (ie, IPMI)
33# will require passing driver-specific data to Tempest (addresses,
34# credentials, etc). Until then, only support testing against the fake driver,
35# which has no external dependencies.
Dmitry Tantsur893b1a92018-04-26 16:12:45 +020036SUPPORTED_DRIVERS = ['fake', 'fake-hardware']
Yuiko Takadab6527002015-12-07 11:49:12 +090037
38# NOTE(jroll): resources must be deleted in a specific order, this list
39# defines the resource types to clean up, and the correct order.
Mark Goddard6f2e72c2019-02-15 12:23:34 +000040RESOURCE_TYPES = ['port', 'portgroup', 'node', 'volume_connector',
41 'volume_target', 'chassis', 'deploy_template']
Yuiko Takadab6527002015-12-07 11:49:12 +090042
43
44def creates(resource):
45 """Decorator that adds resources to the appropriate cleanup list."""
46
47 def decorator(f):
Takashi Kajinami6bddaab2022-05-10 00:58:56 +090048 @functools.wraps(f)
Yuiko Takadab6527002015-12-07 11:49:12 +090049 def wrapper(cls, *args, **kwargs):
50 resp, body = f(cls, *args, **kwargs)
51
52 if 'uuid' in body:
53 cls.created_objects[resource].add(body['uuid'])
54
55 return resp, body
56 return wrapper
57 return decorator
58
59
Yuiko Takadaff785002015-12-17 15:56:42 +090060class BaseBaremetalTest(api_version_utils.BaseMicroversionTest,
61 test.BaseTestCase):
Yuiko Takadab6527002015-12-07 11:49:12 +090062 """Base class for Baremetal API tests."""
63
Julia Kreger3a07c4d2021-06-22 10:27:56 -070064 credentials = ['admin', 'system_admin']
Yuiko Takadab6527002015-12-07 11:49:12 +090065
66 @classmethod
67 def skip_checks(cls):
68 super(BaseBaremetalTest, cls).skip_checks()
Jim Rollenhagen1c11bdf2016-11-29 16:57:30 -050069 if not CONF.service_available.ironic:
70 raise cls.skipException('Ironic is not enabled.')
Yuiko Takadab6527002015-12-07 11:49:12 +090071 if CONF.baremetal.driver not in SUPPORTED_DRIVERS:
72 skip_msg = ('%s skipped as Ironic driver %s is not supported for '
73 'testing.' %
74 (cls.__name__, CONF.baremetal.driver))
75 raise cls.skipException(skip_msg)
76
Yuiko Takadaff785002015-12-17 15:56:42 +090077 cfg_min_version = CONF.baremetal.min_microversion
78 cfg_max_version = CONF.baremetal.max_microversion
79 api_version_utils.check_skip_with_microversion(cls.min_microversion,
80 cls.max_microversion,
81 cfg_min_version,
82 cfg_max_version)
83
84 @classmethod
85 def setup_credentials(cls):
86 cls.request_microversion = (
87 api_version_utils.select_request_microversion(
88 cls.min_microversion,
89 CONF.baremetal.min_microversion))
90 cls.services_microversion = {
91 CONF.baremetal.catalog_type: cls.request_microversion}
Julia Kreger3a07c4d2021-06-22 10:27:56 -070092
Yuiko Takadaff785002015-12-17 15:56:42 +090093 super(BaseBaremetalTest, cls).setup_credentials()
94
Yuiko Takadab6527002015-12-07 11:49:12 +090095 @classmethod
96 def setup_clients(cls):
97 super(BaseBaremetalTest, cls).setup_clients()
Julia Kreger3a07c4d2021-06-22 10:27:56 -070098 if CONF.enforce_scope.ironic:
99 cls.client = cls.os_system_admin.baremetal.BaremetalClient()
100 else:
101 cls.client = cls.os_admin.baremetal.BaremetalClient()
Yuiko Takadab6527002015-12-07 11:49:12 +0900102
103 @classmethod
104 def resource_setup(cls):
105 super(BaseBaremetalTest, cls).resource_setup()
Yuiko Takadaff785002015-12-17 15:56:42 +0900106 cls.request_microversion = (
107 api_version_utils.select_request_microversion(
108 cls.min_microversion,
109 CONF.baremetal.min_microversion))
Yuiko Takadab6527002015-12-07 11:49:12 +0900110 cls.driver = CONF.baremetal.driver
111 cls.power_timeout = CONF.baremetal.power_timeout
Yuiko Takadaff785002015-12-17 15:56:42 +0900112 cls.unprovision_timeout = CONF.baremetal.unprovision_timeout
Yuiko Takadab6527002015-12-07 11:49:12 +0900113 cls.created_objects = {}
Dmitry Tantsuraac618b2019-04-18 12:40:15 +0200114 for resource in RESOURCE_TYPES + ['allocation']:
Yuiko Takadab6527002015-12-07 11:49:12 +0900115 cls.created_objects[resource] = set()
Dmitry Tantsur0325dbd2018-10-24 15:13:46 +0200116 cls.deployed_nodes = set()
Yuiko Takadab6527002015-12-07 11:49:12 +0900117
118 @classmethod
119 def resource_cleanup(cls):
120 """Ensure that all created objects get destroyed."""
Mark Goddard6f2e72c2019-02-15 12:23:34 +0000121 # Use the requested microversion for cleanup to ensure we can delete
122 # resources.
123 base.set_baremetal_api_microversion(cls.request_microversion)
Yuiko Takadab6527002015-12-07 11:49:12 +0900124 try:
Dmitry Tantsur0325dbd2018-10-24 15:13:46 +0200125 for node in cls.deployed_nodes:
126 try:
127 cls.set_node_provision_state(node, 'deleted',
128 ['available', None])
129 except lib_exc.BadRequest:
Julia Kregercda96d52023-01-20 08:05:41 -0800130 LOG.warning('Cleanup: Failed to unprovision node: %s',
131 node)
Dmitry Tantsur0325dbd2018-10-24 15:13:46 +0200132
Dmitry Tantsuraac618b2019-04-18 12:40:15 +0200133 # Delete allocations explicitly after unprovisioning instances, but
134 # before deleting nodes.
135 for allocation in cls.created_objects['allocation']:
136 try:
137 cls.client.delete_allocation(allocation)
138 except lib_exc.NotFound:
Julia Kregercda96d52023-01-20 08:05:41 -0800139 LOG.warning('Cleanup: Failed to delete allocation: %s',
140 allocation)
Dmitry Tantsuraac618b2019-04-18 12:40:15 +0200141
Dmitry Tantsur47ff4892019-02-08 17:24:46 +0100142 for node in cls.created_objects['node']:
143 try:
144 cls.client.update_node(node, instance_uuid=None)
145 except lib_exc.TempestException:
Julia Kregercda96d52023-01-20 08:05:41 -0800146 LOG.warning('Cleanup: Failed to delete node: %s',
147 node)
Dmitry Tantsur47ff4892019-02-08 17:24:46 +0100148
Yuiko Takadab6527002015-12-07 11:49:12 +0900149 for resource in RESOURCE_TYPES:
150 uuids = cls.created_objects[resource]
151 delete_method = getattr(cls.client, 'delete_%s' % resource)
152 for u in uuids:
153 delete_method(u, ignore_errors=lib_exc.NotFound)
154 finally:
Mark Goddard6f2e72c2019-02-15 12:23:34 +0000155 base.reset_baremetal_api_microversion()
Yuiko Takadab6527002015-12-07 11:49:12 +0900156 super(BaseBaremetalTest, cls).resource_cleanup()
157
Kyrylo Romanenko62c0c092017-02-20 18:22:27 +0200158 def _assertExpected(self, expected, actual):
Ruby Loo6abadd42017-02-27 15:45:03 +0000159 """Check if expected keys/values exist in actual response body.
160
161 Check if the expected keys and values are in the actual response body.
162 It will not check the keys 'created_at' and 'updated_at', since they
163 will always have different values. Asserts if any expected key (or
164 corresponding value) is not in the actual response.
165
166 Note: this method has an underscore even though it is used outside of
167 this class, in order to distinguish this method from the more standard
168 assertXYZ methods.
169
170 :param expected: dict of key-value pairs that are expected to be in
171 'actual' dict.
172 :param actual: dict of key-value pairs.
173
174 """
Kyrylo Romanenko62c0c092017-02-20 18:22:27 +0200175 for key, value in expected.items():
176 if key not in ('created_at', 'updated_at'):
177 self.assertIn(key, actual)
178 self.assertEqual(value, actual[key])
179
Yuiko Takadaff785002015-12-17 15:56:42 +0900180 def setUp(self):
181 super(BaseBaremetalTest, self).setUp()
182 self.useFixture(api_microversion_fixture.APIMicroversionFixture(
183 self.request_microversion))
184
Yuiko Takadab6527002015-12-07 11:49:12 +0900185 @classmethod
186 @creates('chassis')
SofiiaAndriichenko3c833962016-12-09 10:12:18 -0500187 def create_chassis(cls, description=None, **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900188 """Wrapper utility for creating test chassis.
189
Kyrylo Romanenkodbda6492016-09-14 14:58:54 +0300190 :param description: A description of the chassis. If not supplied,
Yuiko Takadab6527002015-12-07 11:49:12 +0900191 a random value will be generated.
Kyrylo Romanenko8e1be642017-03-20 16:01:33 +0000192 :return: A tuple with the server response and the created chassis.
Yuiko Takadab6527002015-12-07 11:49:12 +0900193
194 """
195 description = description or data_utils.rand_name('test-chassis')
SofiiaAndriichenko3c833962016-12-09 10:12:18 -0500196 resp, body = cls.client.create_chassis(description=description,
197 **kwargs)
Yuiko Takadab6527002015-12-07 11:49:12 +0900198 return resp, body
199
200 @classmethod
201 @creates('node')
Sam Betts6f083ce2018-07-03 14:41:39 +0100202 def create_node(cls, chassis_id, cpu_arch='x86_64', cpus=8, local_gb=10,
Dmitry Tantsur47ff4892019-02-08 17:24:46 +0100203 memory_mb=4096, **kwargs):
Yuiko Takadab6527002015-12-07 11:49:12 +0900204 """Wrapper utility for creating test baremetal nodes.
205
Kyrylo Romanenkodbda6492016-09-14 14:58:54 +0300206 :param chassis_id: The unique identifier of the chassis.
Sam Betts6f083ce2018-07-03 14:41:39 +0100207 :param cpu_arch: CPU architecture of the node. Default: x86_64.
Yuiko Takadab6527002015-12-07 11:49:12 +0900208 :param cpus: Number of CPUs. Default: 8.
209 :param local_gb: Disk size. Default: 10.
210 :param memory_mb: Available RAM. Default: 4096.
Dmitry Tantsur47ff4892019-02-08 17:24:46 +0100211 :param kwargs: Other optional node fields.
Kyrylo Romanenko8e1be642017-03-20 16:01:33 +0000212 :return: A tuple with the server response and the created node.
Yuiko Takadab6527002015-12-07 11:49:12 +0900213
214 """
215 resp, body = cls.client.create_node(chassis_id, cpu_arch=cpu_arch,
216 cpus=cpus, local_gb=local_gb,
217 memory_mb=memory_mb,
Kyrylo Romanenko96876912017-02-24 18:14:46 +0200218 driver=cls.driver,
Dmitry Tantsur47ff4892019-02-08 17:24:46 +0100219 **kwargs)
Yuiko Takadab6527002015-12-07 11:49:12 +0900220
221 return resp, body
222
223 @classmethod
Dmitry Tantsur0325dbd2018-10-24 15:13:46 +0200224 def set_node_provision_state(cls, node_id, target, expected, timeout=None,
225 interval=None):
226 """Sets the node's provision state.
227
228 :param node_id: The unique identifier of the node.
229 :param target: Target provision state.
230 :param expected: Expected final provision state or list of states.
231 :param timeout: The timeout for reaching the expected state.
232 Defaults to client.build_timeout.
233 :param interval: An interval between show_node calls for status check.
234 Defaults to client.build_interval.
235 """
236 cls.client.set_node_provision_state(node_id, target)
237 waiters.wait_for_bm_node_status(cls.client, node_id,
238 'provision_state', expected,
239 timeout=timeout, interval=interval)
240
241 @classmethod
242 def provide_node(cls, node_id, cleaning_timeout=None):
243 """Make the node available.
244
245 :param node_id: The unique identifier of the node.
246 :param cleaning_timeout: The timeout to wait for cleaning.
247 Defaults to client.build_timeout.
248 """
249 _, body = cls.client.show_node(node_id)
250 current_state = body['provision_state']
251 if current_state == 'enroll':
252 cls.set_node_provision_state(node_id, 'manage', 'manageable',
253 timeout=60, interval=1)
254 current_state = 'manageable'
255 if current_state == 'manageable':
256 cls.set_node_provision_state(node_id, 'provide',
257 ['available', None],
258 timeout=cleaning_timeout)
259 current_state = 'available'
260 if current_state not in ('available', None):
261 raise RuntimeError("Cannot reach state 'available': node %(node)s "
262 "is in unexpected state %(state)s" %
263 {'node': node_id, 'state': current_state})
264
265 @classmethod
266 def deploy_node(cls, node_id, cleaning_timeout=None, deploy_timeout=None):
267 """Deploy the node.
268
269 :param node_id: The unique identifier of the node.
270 :param cleaning_timeout: The timeout to wait for cleaning.
271 Defaults to client.build_timeout.
272 :param deploy_timeout: The timeout to wait for deploy.
273 Defaults to client.build_timeout.
274 """
275 cls.provide_node(node_id, cleaning_timeout=cleaning_timeout)
276 cls.set_node_provision_state(node_id, 'active', 'active',
277 timeout=deploy_timeout)
278 cls.deployed_nodes.add(node_id)
279
280 @classmethod
Yuiko Takadab6527002015-12-07 11:49:12 +0900281 @creates('port')
Mark Goddard53bb68c2017-06-05 11:35:28 +0100282 def create_port(cls, node_id, address, extra=None, uuid=None,
283 portgroup_uuid=None, physical_network=None):
Yuiko Takadab6527002015-12-07 11:49:12 +0900284 """Wrapper utility for creating test ports.
285
Kyrylo Romanenkodbda6492016-09-14 14:58:54 +0300286 :param node_id: The unique identifier of the node.
Yuiko Takadab6527002015-12-07 11:49:12 +0900287 :param address: MAC address of the port.
288 :param extra: Meta data of the port. If not supplied, an empty
289 dictionary will be created.
290 :param uuid: UUID of the port.
Mark Goddard53bb68c2017-06-05 11:35:28 +0100291 :param portgroup_uuid: The UUID of a portgroup of which this port is a
292 member.
293 :param physical_network: The physical network to which the port is
294 attached.
Kyrylo Romanenko8e1be642017-03-20 16:01:33 +0000295 :return: A tuple with the server response and the created port.
Yuiko Takadab6527002015-12-07 11:49:12 +0900296
297 """
298 extra = extra or {}
299 resp, body = cls.client.create_port(address=address, node_id=node_id,
Mark Goddard53bb68c2017-06-05 11:35:28 +0100300 extra=extra, uuid=uuid,
301 portgroup_uuid=portgroup_uuid,
302 physical_network=physical_network)
Yuiko Takadab6527002015-12-07 11:49:12 +0900303
304 return resp, body
305
306 @classmethod
Kyrylo Romanenko2193e542017-02-21 18:47:53 +0200307 @creates('portgroup')
308 def create_portgroup(cls, node_uuid, **kwargs):
309 """Wrapper utility for creating test port groups.
310
311 :param node_uuid: The unique identifier of the node.
Kyrylo Romanenko8e1be642017-03-20 16:01:33 +0000312 :return: A tuple with the server response and the created port group.
Kyrylo Romanenko2193e542017-02-21 18:47:53 +0200313 """
314 resp, body = cls.client.create_portgroup(node_uuid=node_uuid, **kwargs)
315
316 return resp, body
317
318 @classmethod
Nguyen Hung Phuongca69ffe2017-06-13 14:22:10 +0700319 @creates('volume_connector')
320 def create_volume_connector(cls, node_uuid, **kwargs):
321 """Wrapper utility for creating test volume connector.
322
323 :param node_uuid: The unique identifier of the node.
324 :return: A tuple with the server response and the created volume
325 connector.
326 """
327 resp, body = cls.client.create_volume_connector(node_uuid=node_uuid,
328 **kwargs)
329
330 return resp, body
331
332 @classmethod
333 @creates('volume_target')
334 def create_volume_target(cls, node_uuid, **kwargs):
335 """Wrapper utility for creating test volume target.
336
337 :param node_uuid: The unique identifier of the node.
338 :return: A tuple with the server response and the created volume
339 target.
340 """
341 resp, body = cls.client.create_volume_target(node_uuid=node_uuid,
342 **kwargs)
343
344 return resp, body
345
346 @classmethod
Mark Goddard6f2e72c2019-02-15 12:23:34 +0000347 @creates('deploy_template')
348 def create_deploy_template(cls, name, **kwargs):
349 """Wrapper utility for creating test deploy template.
350
351 :param name: The name of the deploy template.
352 :return: A tuple with the server response and the created deploy
353 template.
354 """
355 resp, body = cls.client.create_deploy_template(name=name, **kwargs)
356
357 return resp, body
358
359 @classmethod
Yuiko Takadab6527002015-12-07 11:49:12 +0900360 def delete_chassis(cls, chassis_id):
361 """Deletes a chassis having the specified UUID.
362
Kyrylo Romanenkodbda6492016-09-14 14:58:54 +0300363 :param chassis_id: The unique identifier of the chassis.
Yuiko Takadab6527002015-12-07 11:49:12 +0900364 :return: Server response.
365
366 """
367
368 resp, body = cls.client.delete_chassis(chassis_id)
369
370 if chassis_id in cls.created_objects['chassis']:
371 cls.created_objects['chassis'].remove(chassis_id)
372
373 return resp
374
375 @classmethod
376 def delete_node(cls, node_id):
377 """Deletes a node having the specified UUID.
378
Kyrylo Romanenkodbda6492016-09-14 14:58:54 +0300379 :param node_id: The unique identifier of the node.
Yuiko Takadab6527002015-12-07 11:49:12 +0900380 :return: Server response.
381
382 """
383
384 resp, body = cls.client.delete_node(node_id)
385
386 if node_id in cls.created_objects['node']:
387 cls.created_objects['node'].remove(node_id)
388
389 return resp
390
391 @classmethod
392 def delete_port(cls, port_id):
393 """Deletes a port having the specified UUID.
394
Kyrylo Romanenkodbda6492016-09-14 14:58:54 +0300395 :param port_id: The unique identifier of the port.
Yuiko Takadab6527002015-12-07 11:49:12 +0900396 :return: Server response.
397
398 """
399
400 resp, body = cls.client.delete_port(port_id)
401
402 if port_id in cls.created_objects['port']:
403 cls.created_objects['port'].remove(port_id)
404
405 return resp
406
Kyrylo Romanenko2193e542017-02-21 18:47:53 +0200407 @classmethod
408 def delete_portgroup(cls, portgroup_ident):
409 """Deletes a port group having the specified UUID or name.
410
411 :param portgroup_ident: The name or UUID of the port group.
412 :return: Server response.
413 """
414 resp, body = cls.client.delete_portgroup(portgroup_ident)
415
416 if portgroup_ident in cls.created_objects['portgroup']:
417 cls.created_objects['portgroup'].remove(portgroup_ident)
418
419 return resp
420
Nguyen Hung Phuongca69ffe2017-06-13 14:22:10 +0700421 @classmethod
422 def delete_volume_connector(cls, volume_connector_id):
423 """Deletes a volume connector having the specified UUID.
424
425 :param volume_connector_id: The UUID of the volume connector.
426 :return: Server response.
427 """
428 resp, body = cls.client.delete_volume_connector(volume_connector_id)
429
430 if volume_connector_id in cls.created_objects['volume_connector']:
431 cls.created_objects['volume_connector'].remove(
432 volume_connector_id)
433
434 return resp
435
436 @classmethod
437 def delete_volume_target(cls, volume_target_id):
438 """Deletes a volume target having the specified UUID.
439
440 :param volume_target_id: The UUID of the volume target.
441 :return: Server response.
442 """
443 resp, body = cls.client.delete_volume_target(volume_target_id)
444
445 if volume_target_id in cls.created_objects['volume_target']:
446 cls.created_objects['volume_target'].remove(volume_target_id)
447
448 return resp
449
Mark Goddard6f2e72c2019-02-15 12:23:34 +0000450 @classmethod
451 def delete_deploy_template(cls, deploy_template_ident):
452 """Deletes a deploy template having the specified name or UUID.
453
454 :param deploy_template_ident: Name or UUID of the deploy template.
455 :return: Server response.
456 """
457 resp, body = cls.client.delete_deploy_template(deploy_template_ident)
458
459 if deploy_template_ident in cls.created_objects['deploy_template']:
460 cls.created_objects['deploy_template'].remove(
461 deploy_template_ident)
462
463 return resp
464
Yuiko Takadab6527002015-12-07 11:49:12 +0900465 def validate_self_link(self, resource, uuid, link):
466 """Check whether the given self link formatted correctly."""
467 expected_link = "{base}/{pref}/{res}/{uuid}".format(
Andrey Shestakov7cdfac32017-02-06 14:44:23 +0200468 base=self.client.base_url.rstrip('/'),
Yuiko Takadab6527002015-12-07 11:49:12 +0900469 pref=self.client.uri_prefix,
470 res=resource,
471 uuid=uuid)
472 self.assertEqual(expected_link, link)
Dmitry Tantsuraac618b2019-04-18 12:40:15 +0200473
474 @classmethod
475 @creates('allocation')
476 def create_allocation(cls, resource_class, **kwargs):
477 """Wrapper utility for creating test allocations.
478
479 :param resource_class: Resource class to request.
480 :param kwargs: Other fields to pass.
481 :return: A tuple with the server response and the created allocation.
482 """
483 resp, body = cls.client.create_allocation(resource_class, **kwargs)
484 return resp, body