Add configurable delays to the fake drivers

Simulating workloads with the fake driver currently misses the reality
that some operations take time to complete, rather than occuring
instantly. This makes it difficult to mock real workloads for
performance and functional testing of ironic itself.

This change adds configurable random wait times for fake drivers in a
new ironic.conf [fake] section. Each supported driver having one
configuration option controlling the delay. These delays are applied
to operations which typically block in other drivers.

The default value of zero continues the existing behaviour of no
delay. A single integer value will result in a constant delay in
seconds. Two values separated by a comma will result in a triangular
distribution weighted by the first value, specifically in python[1]:

    random.triangular(a, b, a)

Change-Id: I7cb1b50d035939e6c4538b3373002a309bfedea4
[1] https://docs.python.org/3/library/random.html#random.triangular
This commit is contained in:
Steve Baker 2022-10-13 10:33:14 +13:00
parent cbaa871b25
commit 393b20204b
5 changed files with 187 additions and 0 deletions

View File

@ -29,6 +29,7 @@ from ironic.conf import deploy
from ironic.conf import dhcp from ironic.conf import dhcp
from ironic.conf import dnsmasq from ironic.conf import dnsmasq
from ironic.conf import drac from ironic.conf import drac
from ironic.conf import fake
from ironic.conf import glance from ironic.conf import glance
from ironic.conf import healthcheck from ironic.conf import healthcheck
from ironic.conf import ibmc from ironic.conf import ibmc
@ -64,6 +65,7 @@ deploy.register_opts(CONF)
drac.register_opts(CONF) drac.register_opts(CONF)
dhcp.register_opts(CONF) dhcp.register_opts(CONF)
dnsmasq.register_opts(CONF) dnsmasq.register_opts(CONF)
fake.register_opts(CONF)
glance.register_opts(CONF) glance.register_opts(CONF)
healthcheck.register_opts(CONF) healthcheck.register_opts(CONF)
ibmc.register_opts(CONF) ibmc.register_opts(CONF)

85
ironic/conf/fake.py Normal file
View File

@ -0,0 +1,85 @@
#
# Copyright 2022 Red Hat, 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 oslo_config import cfg
from ironic.common.i18n import _
opts = [
cfg.StrOpt('power_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'power driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('boot_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'boot driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('deploy_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'deploy driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('vendor_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'vendor driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('management_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'management driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('inspect_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'inspect driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('raid_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'raid driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('bios_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'bios driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('storage_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'storage driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
cfg.StrOpt('rescue_delay',
default='0',
help=_('Delay in seconds for operations with the fake '
'rescue driver. Two comma-delimited values will '
'result in a delay with a triangular random '
'distribution, weighted on the first value.')),
]
def register_opts(conf):
conf.register_opts(opts, group='fake')

View File

@ -24,6 +24,9 @@ functionality between a power interface and a deploy interface, when both rely
on separate vendor_passthru methods. on separate vendor_passthru methods.
""" """
import random
import time
from oslo_log import log from oslo_log import log
from ironic.common import boot_devices from ironic.common import boot_devices
@ -32,6 +35,7 @@ from ironic.common import exception
from ironic.common.i18n import _ from ironic.common.i18n import _
from ironic.common import indicator_states from ironic.common import indicator_states
from ironic.common import states from ironic.common import states
from ironic.conf import CONF
from ironic.drivers import base from ironic.drivers import base
from ironic import objects from ironic import objects
@ -39,6 +43,34 @@ from ironic import objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
def parse_sleep_range(sleep_range):
if not sleep_range:
return 0, 0
sleep_split = sleep_range.split(',')
if len(sleep_split) == 1:
a = sleep_split[0]
b = sleep_split[0]
else:
a = sleep_split[0]
b = sleep_split[1]
return int(a), int(b)
def sleep(sleep_range):
earliest, latest = parse_sleep_range(sleep_range)
if earliest == 0 and latest == 0:
# no sleep
return
if earliest == latest:
# constant sleep
sleep = earliest
else:
# triangular random sleep, weighted towards the earliest
sleep = random.triangular(earliest, latest, earliest)
time.sleep(sleep)
class FakePower(base.PowerInterface): class FakePower(base.PowerInterface):
"""Example implementation of a simple power interface.""" """Example implementation of a simple power interface."""
@ -49,12 +81,15 @@ class FakePower(base.PowerInterface):
pass pass
def get_power_state(self, task): def get_power_state(self, task):
sleep(CONF.fake.power_delay)
return task.node.power_state return task.node.power_state
def reboot(self, task, timeout=None): def reboot(self, task, timeout=None):
sleep(CONF.fake.power_delay)
pass pass
def set_power_state(self, task, power_state, timeout=None): def set_power_state(self, task, power_state, timeout=None):
sleep(CONF.fake.power_delay)
if power_state not in [states.POWER_ON, states.POWER_OFF, if power_state not in [states.POWER_ON, states.POWER_OFF,
states.SOFT_REBOOT, states.SOFT_POWER_OFF]: states.SOFT_REBOOT, states.SOFT_POWER_OFF]:
raise exception.InvalidParameterValue( raise exception.InvalidParameterValue(
@ -81,15 +116,19 @@ class FakeBoot(base.BootInterface):
pass pass
def prepare_ramdisk(self, task, ramdisk_params, mode='deploy'): def prepare_ramdisk(self, task, ramdisk_params, mode='deploy'):
sleep(CONF.fake.boot_delay)
pass pass
def clean_up_ramdisk(self, task, mode='deploy'): def clean_up_ramdisk(self, task, mode='deploy'):
sleep(CONF.fake.boot_delay)
pass pass
def prepare_instance(self, task): def prepare_instance(self, task):
sleep(CONF.fake.boot_delay)
pass pass
def clean_up_instance(self, task): def clean_up_instance(self, task):
sleep(CONF.fake.boot_delay)
pass pass
@ -108,18 +147,23 @@ class FakeDeploy(base.DeployInterface):
@base.deploy_step(priority=100) @base.deploy_step(priority=100)
def deploy(self, task): def deploy(self, task):
sleep(CONF.fake.deploy_delay)
return None return None
def tear_down(self, task): def tear_down(self, task):
sleep(CONF.fake.deploy_delay)
return states.DELETED return states.DELETED
def prepare(self, task): def prepare(self, task):
sleep(CONF.fake.deploy_delay)
pass pass
def clean_up(self, task): def clean_up(self, task):
sleep(CONF.fake.deploy_delay)
pass pass
def take_over(self, task): def take_over(self, task):
sleep(CONF.fake.deploy_delay)
pass pass
@ -140,6 +184,7 @@ class FakeVendorA(base.VendorInterface):
@base.passthru(['POST'], @base.passthru(['POST'],
description=_("Test if the value of bar is baz")) description=_("Test if the value of bar is baz"))
def first_method(self, task, http_method, bar): def first_method(self, task, http_method, bar):
sleep(CONF.fake.vendor_delay)
return True if bar == 'baz' else False return True if bar == 'baz' else False
@ -161,16 +206,19 @@ class FakeVendorB(base.VendorInterface):
@base.passthru(['POST'], @base.passthru(['POST'],
description=_("Test if the value of bar is kazoo")) description=_("Test if the value of bar is kazoo"))
def second_method(self, task, http_method, bar): def second_method(self, task, http_method, bar):
sleep(CONF.fake.vendor_delay)
return True if bar == 'kazoo' else False return True if bar == 'kazoo' else False
@base.passthru(['POST'], async_call=False, @base.passthru(['POST'], async_call=False,
description=_("Test if the value of bar is meow")) description=_("Test if the value of bar is meow"))
def third_method_sync(self, task, http_method, bar): def third_method_sync(self, task, http_method, bar):
sleep(CONF.fake.vendor_delay)
return True if bar == 'meow' else False return True if bar == 'meow' else False
@base.passthru(['POST'], require_exclusive_lock=False, @base.passthru(['POST'], require_exclusive_lock=False,
description=_("Test if the value of bar is woof")) description=_("Test if the value of bar is woof"))
def fourth_method_shared_lock(self, task, http_method, bar): def fourth_method_shared_lock(self, task, http_method, bar):
sleep(CONF.fake.vendor_delay)
return True if bar == 'woof' else False return True if bar == 'woof' else False
@ -211,17 +259,21 @@ class FakeManagement(base.ManagementInterface):
return [boot_devices.PXE] return [boot_devices.PXE]
def set_boot_device(self, task, device, persistent=False): def set_boot_device(self, task, device, persistent=False):
sleep(CONF.fake.management_delay)
if device not in self.get_supported_boot_devices(task): if device not in self.get_supported_boot_devices(task):
raise exception.InvalidParameterValue(_( raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % device) "Invalid boot device %s specified.") % device)
def get_boot_device(self, task): def get_boot_device(self, task):
sleep(CONF.fake.management_delay)
return {'boot_device': boot_devices.PXE, 'persistent': False} return {'boot_device': boot_devices.PXE, 'persistent': False}
def get_sensors_data(self, task): def get_sensors_data(self, task):
sleep(CONF.fake.management_delay)
return {} return {}
def get_supported_indicators(self, task, component=None): def get_supported_indicators(self, task, component=None):
sleep(CONF.fake.management_delay)
indicators = { indicators = {
components.CHASSIS: { components.CHASSIS: {
'led-0': { 'led-0': {
@ -248,6 +300,7 @@ class FakeManagement(base.ManagementInterface):
if not component or component == c} if not component or component == c}
def get_indicator_state(self, task, component, indicator): def get_indicator_state(self, task, component, indicator):
sleep(CONF.fake.management_delay)
indicators = self.get_supported_indicators(task) indicators = self.get_supported_indicators(task)
if component not in indicators: if component not in indicators:
raise exception.InvalidParameterValue(_( raise exception.InvalidParameterValue(_(
@ -271,6 +324,7 @@ class FakeInspect(base.InspectInterface):
pass pass
def inspect_hardware(self, task): def inspect_hardware(self, task):
sleep(CONF.fake.inspect_delay)
return states.MANAGEABLE return states.MANAGEABLE
@ -282,9 +336,11 @@ class FakeRAID(base.RAIDInterface):
def create_configuration(self, task, create_root_volume=True, def create_configuration(self, task, create_root_volume=True,
create_nonroot_volumes=True): create_nonroot_volumes=True):
sleep(CONF.fake.raid_delay)
pass pass
def delete_configuration(self, task): def delete_configuration(self, task):
sleep(CONF.fake.raid_delay)
pass pass
@ -302,6 +358,7 @@ class FakeBIOS(base.BIOSInterface):
'to contain a dictionary with name/value pairs'), 'to contain a dictionary with name/value pairs'),
'required': True}}) 'required': True}})
def apply_configuration(self, task, settings): def apply_configuration(self, task, settings):
sleep(CONF.fake.bios_delay)
# Note: the implementation of apply_configuration in fake interface # Note: the implementation of apply_configuration in fake interface
# is just for testing purpose, for real driver implementation, please # is just for testing purpose, for real driver implementation, please
# refer to develop doc at https://docs.openstack.org/ironic/latest/ # refer to develop doc at https://docs.openstack.org/ironic/latest/
@ -328,6 +385,7 @@ class FakeBIOS(base.BIOSInterface):
@base.clean_step(priority=0) @base.clean_step(priority=0)
def factory_reset(self, task): def factory_reset(self, task):
sleep(CONF.fake.bios_delay)
# Note: the implementation of factory_reset in fake interface is # Note: the implementation of factory_reset in fake interface is
# just for testing purpose, for real driver implementation, please # just for testing purpose, for real driver implementation, please
# refer to develop doc at https://docs.openstack.org/ironic/latest/ # refer to develop doc at https://docs.openstack.org/ironic/latest/
@ -340,6 +398,7 @@ class FakeBIOS(base.BIOSInterface):
@base.clean_step(priority=0) @base.clean_step(priority=0)
def cache_bios_settings(self, task): def cache_bios_settings(self, task):
sleep(CONF.fake.bios_delay)
# Note: the implementation of cache_bios_settings in fake interface # Note: the implementation of cache_bios_settings in fake interface
# is just for testing purpose, for real driver implementation, please # is just for testing purpose, for real driver implementation, please
# refer to develop doc at https://docs.openstack.org/ironic/latest/ # refer to develop doc at https://docs.openstack.org/ironic/latest/
@ -357,9 +416,11 @@ class FakeStorage(base.StorageInterface):
return {} return {}
def attach_volumes(self, task): def attach_volumes(self, task):
sleep(CONF.fake.storage_delay)
pass pass
def detach_volumes(self, task): def detach_volumes(self, task):
sleep(CONF.fake.storage_delay)
pass pass
def should_write_image(self, task): def should_write_image(self, task):
@ -376,7 +437,9 @@ class FakeRescue(base.RescueInterface):
pass pass
def rescue(self, task): def rescue(self, task):
sleep(CONF.fake.rescue_delay)
return states.RESCUE return states.RESCUE
def unrescue(self, task): def unrescue(self, task):
sleep(CONF.fake.rescue_delay)
return states.ACTIVE return states.ACTIVE

View File

@ -17,6 +17,8 @@
"""Test class for Fake driver.""" """Test class for Fake driver."""
import time
from unittest import mock
from ironic.common import boot_devices from ironic.common import boot_devices
from ironic.common import boot_modes from ironic.common import boot_modes
@ -26,6 +28,7 @@ from ironic.common import indicator_states
from ironic.common import states from ironic.common import states
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.drivers import base as driver_base from ironic.drivers import base as driver_base
from ironic.drivers.modules import fake
from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.db import utils as db_utils
@ -164,3 +167,29 @@ class FakeHardwareTestCase(db_base.DbTestCase):
self.assertEqual({}, self.driver.inspect.get_properties()) self.assertEqual({}, self.driver.inspect.get_properties())
self.driver.inspect.validate(self.task) self.driver.inspect.validate(self.task)
self.driver.inspect.inspect_hardware(self.task) self.driver.inspect.inspect_hardware(self.task)
def test_parse_sleep_range(self):
self.assertEqual((0, 0), fake.parse_sleep_range('0'))
self.assertEqual((0, 0), fake.parse_sleep_range(''))
self.assertEqual((1, 1), fake.parse_sleep_range('1'))
self.assertEqual((1, 10), fake.parse_sleep_range('1,10'))
self.assertEqual((10, 20), fake.parse_sleep_range('10, 20'))
@mock.patch.object(time, 'sleep', autospec=True)
def test_sleep_zero(self, mock_sleep):
fake.sleep("0")
mock_sleep.assert_not_called()
@mock.patch.object(time, 'sleep', autospec=True)
def test_sleep_one(self, mock_sleep):
fake.sleep("1")
mock_sleep.assert_called_once_with(1)
@mock.patch.object(time, 'sleep', autospec=True)
def test_sleep_range(self, mock_sleep):
for i in range(100):
fake.sleep("1,10")
for call in mock_sleep.call_args_list:
v = call[0][0]
self.assertGreaterEqual(v, 1)
self.assertLessEqual(v, 10)

View File

@ -0,0 +1,8 @@
---
features:
- |
There are now configurable random wait times for fake drivers in a new
ironic.conf [fake] section. Each supported driver having one configuration
option controlling the delay. These delays are applied to operations which
typically block in other drivers. This allows more realistic scenarios to
be arranged for performance and functional testing of ironic itself.