Merge "Re-introduce kube upgrade health check for duplex systems"

This commit is contained in:
Zuul authored and Gerrit Code Review committed 2026-09-10 18:07:30 +00:00
commit 48ade5a616
12 files changed
+559 -27

No files matched your search

@@ -468,9 +468,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
admin_state, oper_state, avail_status, nfvi_data = host_state(
host_uuid,
@@ -641,9 +643,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
host_obj = nfvi.objects.v1.Host(
host_uuid,
@@ -3538,9 +3542,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
host_obj = nfvi.objects.v1.Host(
host_uuid,
@@ -3673,9 +3679,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
host_obj = nfvi.objects.v1.Host(
host_uuid,
@@ -3811,9 +3819,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
host_obj = nfvi.objects.v1.Host(
host_uuid,
@@ -3950,9 +3960,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
host_obj = nfvi.objects.v1.Host(
host_uuid,
@@ -4144,9 +4156,11 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
host_label_list = future.result.data["labels"]
openstack_compute, openstack_control, remote_storage = (
self._get_host_labels(host_label_list)
)
(
openstack_compute,
openstack_control,
remote_storage,
) = self._get_host_labels(host_label_list)
host_obj = nfvi.objects.v1.Host(
host_uuid,
@@ -4893,6 +4907,59 @@ class NFVIInfrastructureAPI(nfvi.api.v1.NFVIInfrastructureAPI):
callback.send(response)
callback.close()
def get_kube_upgrade_health(self, future, alarm_ignore_list, callback):
"""Get kube upgrade health status from the plugin."""
response = {}
response["completed"] = False
response["reason"] = ""
action_type = "get-kube-upgrade-health"
try:
future.set_timeouts(config.CONF.get("nfvi-timeouts", None))
if self._platform_token is None or self._platform_token.is_expired():
future.work(openstack.get_token, self._platform_directory)
future.result = yield
if not future.result.is_complete() or future.result.data is None:
DLOG.error("OpenStack get-token did not complete.")
return
self._platform_token = future.result.data
future.work(
sysinv.get_kube_upgrade_health, self._platform_token, alarm_ignore_list
)
future.result = yield
if not future.result.is_complete():
DLOG.error("SysInv get-kube-upgrade-health did not complete.")
return
response["result-data"] = future.result.data
response["completed"] = True
except exceptions.OpenStackRestAPIException as e:
if httplib.UNAUTHORIZED == e.http_status_code:
response["error-code"] = nfvi.NFVI_ERROR_CODE.TOKEN_EXPIRED
if self._platform_token is not None:
self._platform_token.set_expired()
else:
DLOG.exception(
"Caught API exception while trying %s. error=%s" % (action_type, e)
)
except Exception as e:
DLOG.exception(
"Caught exception while trying %s. error=%s" % (action_type, e)
)
finally:
callback.send(response)
callback.close()
def get_deployment_host(self, future, host_name, callback):
"""Get a host resource from the deployment namespace space."""
@@ -231,6 +231,23 @@ def get_kube_upgrade(token):
return response
def get_kube_upgrade_health(token, alarm_ignore_list=None):
"""Asks System Inventory for kube upgrade health status."""
url = token.get_service_url(PLATFORM_SERVICE.SYSINV)
if url is None:
raise ValueError("OpenStack SysInv URL is invalid")
api_cmd = url + "/health/kube-upgrade"
if alarm_ignore_list:
api_cmd += "?alarm_ignore_list=%s" % ",".join(alarm_ignore_list)
response = rest_api_request(
token, "GET", api_cmd, timeout_in_secs=REST_API_REQUEST_TIMEOUT
)
return response
def get_kube_version(token, kube_version):
"""Asks System Inventory for information a kube version."""
@@ -4972,12 +4972,12 @@ class TestSwUpgradeCombinedKubeStrategy(BaseSwUpgradeStrategy):
self.assertLess(sw_start_idx, kube_start_idx)
self.assertLess(sw_complete_idx, kube_start_idx)
# kube-wait-control-plane-pods-ready must appear
# kube-wait-upgrade-healthy must appear
# between sw-deploy and kube-upgrade
self.assertIn("kube-wait-control-plane-pods-ready", stage_names)
pods_ready_idx = stage_names.index("kube-wait-control-plane-pods-ready")
self.assertLess(sw_complete_idx, pods_ready_idx)
self.assertLess(pods_ready_idx, kube_start_idx)
self.assertIn("kube-wait-upgrade-healthy", stage_names)
upgrade_healthy_idx = stage_names.index("kube-wait-upgrade-healthy")
self.assertLess(sw_complete_idx, upgrade_healthy_idx)
self.assertLess(upgrade_healthy_idx, kube_start_idx)
# No sw-system-deploy-init (duplex does not use it)
self.assertNotIn("sw-system-deploy-init", stage_names)
@@ -6446,3 +6446,331 @@ class TestWaitKubeControlPlanePodsReadyStep(
self.mock_stage.step_complete.assert_called_once_with(
common_strategy.STRATEGY_STEP_RESULT.SUCCESS, ""
)
@mock.patch(
"nfv_vim.event_log._instance._event_issue", sw_update_testcase.fake_event_issue
)
@mock.patch("nfv_vim.objects._sw_update.SwUpdate.save", sw_update_testcase.fake_save)
@mock.patch(
"nfv_vim.objects._sw_update.timers.timers_create_timer",
sw_update_testcase.fake_timer,
)
@mock.patch(
"nfv_vim.nfvi.nfvi_compute_plugin_disabled",
sw_update_testcase.fake_nfvi_compute_plugin_disabled,
)
class TestWaitKubernetesUpgradeHealthy(sw_update_testcase.SwUpdateStrategyTestCase):
"""Unit tests for the WaitKubernetesUpgradeHealthy step."""
def setUp(self):
super().setUp()
from nfv_vim.strategy.steps.kube_upgrade_steps import (
WaitKubernetesUpgradeHealthy,
)
self.step = WaitKubernetesUpgradeHealthy(timeout_in_secs=180)
self.mock_stage = mock.MagicMock()
self.step.stage = self.mock_stage
def _send_callback(self, response):
"""Drive the coroutine callback with the given response."""
callback = self.step._query_health_callback()
try:
callback.send(response)
except StopIteration:
pass
def test_healthy_completes_step_successfully(self):
"""Step completes with SUCCESS when health check has no failures."""
self._send_callback(
{
"completed": True,
"result-data": "OK: all checks passed",
}
)
self.mock_stage.step_complete.assert_called_once_with(
common_strategy.STRATEGY_STEP_RESULT.SUCCESS, ""
)
def test_fail_in_health_does_not_complete_step(self):
"""Step keeps waiting when health check contains [Fail] entries."""
self._send_callback(
{
"completed": True,
"result-data": "[Fail] Kubernetes nodes not ready",
}
)
self.mock_stage.step_complete.assert_not_called()
def test_query_failure_does_not_fail_step(self):
"""Step retries when the query itself fails (does not fail step)."""
self._send_callback({"completed": False, "reason": "connection error"})
self.mock_stage.step_complete.assert_not_called()
def test_missing_result_data_fails_step(self):
"""Step fails when response is completed but result-data is missing."""
self._send_callback({"completed": True})
self.mock_stage.step_complete.assert_called_once_with(
common_strategy.STRATEGY_STEP_RESULT.FAILED,
"Kube upgrade health check missing result-data",
)
def test_apply_creates_timer_and_returns_wait(self):
"""Apply creates a polling timer and returns WAIT."""
from nfv_common import timers
with mock.patch.object(
timers, "timers_create_timer", return_value=999
) as mock_create:
result, reason = self.step.apply()
self.assertEqual(result, common_strategy.STRATEGY_STEP_RESULT.WAIT)
self.assertEqual(reason, "")
mock_create.assert_called_once()
self.assertEqual(self.step._poll_timer_id, 999)
def test_handle_event_returns_false(self):
"""handle_event is a no-op (polling is timer-driven)."""
from nfv_vim.strategy._strategy_defs import STRATEGY_EVENT
result = self.step.handle_event(STRATEGY_EVENT.HOST_AUDIT)
self.assertFalse(result)
def test_poll_action_triggers_query_with_alarm_ignore_list(self):
"""_poll_action calls the NFVI query with alarm_ignore_list."""
from nfv_vim.strategy.steps.kube_upgrade_steps import (
KUBE_UPGRADE_START_ALARM_IGNORE,
)
with mock.patch("nfv_vim.nfvi.nfvi_get_kube_upgrade_health") as mock_query:
self.step._poll_action()
mock_query.assert_called_once()
self.assertTrue(self.step._poll_in_progress)
# Verify alarm_ignore_list is passed as first argument
call_args = mock_query.call_args[0]
self.assertEqual(call_args[0], KUBE_UPGRADE_START_ALARM_IGNORE)
def test_poll_skipped_when_in_progress(self):
"""Timer callback does not trigger a new query while one is in progress."""
from nfv_common import timers
with mock.patch.object(timers, "timers_create_timer", return_value=999):
self.step.apply()
# Simulate first poll
with mock.patch("nfv_vim.nfvi.nfvi_get_kube_upgrade_health") as mock_query:
self.step._poll_action()
mock_query.assert_called_once()
# poll_in_progress is True, so _poll_timer_callback should not call again
self.assertTrue(self.step._poll_in_progress)
def test_from_dict_roundtrip(self):
"""Step can be serialized and deserialized without data loss."""
from nfv_vim.strategy.steps.kube_upgrade_steps import (
KUBE_UPGRADE_START_ALARM_IGNORE,
WaitKubernetesUpgradeHealthy,
)
data = self.step.as_dict()
self.assertEqual(data["alarm_ignore_list"], KUBE_UPGRADE_START_ALARM_IGNORE)
new_step = object.__new__(WaitKubernetesUpgradeHealthy)
new_step.from_dict(data)
self.assertIsNone(new_step._poll_timer_id)
self.assertFalse(new_step._poll_in_progress)
self.assertEqual(new_step._alarm_ignore_list, KUBE_UPGRADE_START_ALARM_IGNORE)
def test_from_dict_roundtrip_custom_alarm_ignore_list(self):
"""Step preserves a custom alarm_ignore_list through serialization."""
from nfv_vim.strategy.steps.kube_upgrade_steps import (
WaitKubernetesUpgradeHealthy,
)
custom_alarms = ["100.001", "200.002"]
step = WaitKubernetesUpgradeHealthy(
timeout_in_secs=180, alarm_ignore_list=custom_alarms
)
data = step.as_dict()
self.assertEqual(data["alarm_ignore_list"], custom_alarms)
new_step = object.__new__(WaitKubernetesUpgradeHealthy)
new_step.from_dict(data)
self.assertEqual(new_step._alarm_ignore_list, custom_alarms)
def test_from_dict_recreates_timer_on_resume(self):
"""from_dict recreates the polling timer when step was in WAIT state."""
from nfv_common import timers
from nfv_vim.strategy.steps.kube_upgrade_steps import (
WaitKubernetesUpgradeHealthy,
)
# Simulate a step that was in progress (WAIT state) before restart
data = self.step.as_dict()
data["result"] = "wait"
new_step = object.__new__(WaitKubernetesUpgradeHealthy)
with mock.patch.object(
timers, "timers_create_timer", return_value=888
) as mock_create:
new_step.from_dict(data)
mock_create.assert_called_once()
self.assertEqual(new_step._poll_timer_id, 888)
self.assertFalse(new_step._poll_in_progress)
def test_timeout_cleans_up_timer(self):
"""Timeout cleans up the timer and provides a meaningful error."""
from nfv_common import timers
with mock.patch.object(timers, "timers_create_timer", return_value=999):
self.step.apply()
with mock.patch.object(timers, "timers_delete_timer") as mock_delete:
result, reason = self.step.timeout()
self.assertEqual(result, common_strategy.STRATEGY_STEP_RESULT.TIMED_OUT)
self.assertIn("did not become healthy before timeout", reason)
mock_delete.assert_called_once_with(999)
self.assertIsNone(self.step._poll_timer_id)
def test_flow_healthy_after_poll(self):
"""Full flow: apply → timer polls → healthy → SUCCESS.
Simulates the flow where the step is applied, the timer fires,
issues a health query, and the callback reports the upgrade is
healthy.
"""
from nfv_common import timers
# Step is applied and enters the WAIT state
with mock.patch.object(timers, "timers_create_timer", return_value=999):
result, reason = self.step.apply()
self.assertEqual(result, common_strategy.STRATEGY_STEP_RESULT.WAIT)
# Timer fires → _poll_action triggers query
with mock.patch("nfv_vim.nfvi.nfvi_get_kube_upgrade_health") as mock_query:
self.step._poll_action()
mock_query.assert_called_once()
# Drive the callback coroutine
callback = mock_query.call_args[0][1]
with mock.patch.object(timers, "timers_delete_timer"):
try:
callback.send(
{
"completed": True,
"result-data": "OK: all checks passed",
}
)
except StopIteration:
pass
self.mock_stage.step_complete.assert_called_once_with(
common_strategy.STRATEGY_STEP_RESULT.SUCCESS, ""
)
def test_flow_unhealthy_leads_to_timeout(self):
"""Full flow: health check fails on every poll → eventually times out."""
from nfv_common import timers
# Step is applied and enters the WAIT state
with mock.patch.object(timers, "timers_create_timer", return_value=999):
result, reason = self.step.apply()
self.assertEqual(result, common_strategy.STRATEGY_STEP_RESULT.WAIT)
# Simulate multiple polls that all report unhealthy
for _ in range(3):
with mock.patch("nfv_vim.nfvi.nfvi_get_kube_upgrade_health") as mock_query:
self.step._poll_action()
mock_query.assert_called_once()
callback = mock_query.call_args[0][1]
try:
callback.send(
{
"completed": True,
"result-data": "[Fail] Kubernetes nodes not ready",
}
)
except StopIteration:
pass
# Step should NOT have completed - still waiting
self.mock_stage.step_complete.assert_not_called()
# The strategy framework calls timeout() when time exceeds timeout_in_secs
with mock.patch.object(timers, "timers_delete_timer"):
result, reason = self.step.timeout()
self.assertEqual(result, common_strategy.STRATEGY_STEP_RESULT.TIMED_OUT)
self.assertEqual(
reason, "Kubernetes upgrade did not become healthy before timeout"
)
def test_flow_api_exception_retries_then_succeeds(self):
"""Full flow: API exception causes retry, next poll succeeds."""
from nfv_common import timers
# Step is applied and enters the WAIT state
with mock.patch.object(timers, "timers_create_timer", return_value=999):
result, reason = self.step.apply()
self.assertEqual(result, common_strategy.STRATEGY_STEP_RESULT.WAIT)
# First poll - API exception occurs
with mock.patch("nfv_vim.nfvi.nfvi_get_kube_upgrade_health") as mock_query:
self.step._poll_action()
mock_query.assert_called_once()
callback = mock_query.call_args[0][1]
try:
callback.send({"completed": False, "reason": ""})
except StopIteration:
pass
# Step should NOT have failed - it retries
self.mock_stage.step_complete.assert_not_called()
self.assertFalse(self.step._poll_in_progress)
# Second poll - upgrade is healthy
with mock.patch("nfv_vim.nfvi.nfvi_get_kube_upgrade_health") as mock_query:
self.step._poll_action()
mock_query.assert_called_once()
callback = mock_query.call_args[0][1]
with mock.patch.object(timers, "timers_delete_timer"):
try:
callback.send(
{
"completed": True,
"result-data": "OK: all checks passed",
}
)
except StopIteration:
pass
self.mock_stage.step_complete.assert_called_once_with(
common_strategy.STRATEGY_STEP_RESULT.SUCCESS, ""
)
+3
View File
@@ -163,6 +163,9 @@ from nfv_vim.nfvi._nfvi_infrastructure_module import ( # noqa: F401
from nfv_vim.nfvi._nfvi_infrastructure_module import ( # noqa: F401
nfvi_get_kube_control_plane_pods_ready,
)
from nfv_vim.nfvi._nfvi_infrastructure_module import ( # noqa: F401
nfvi_get_kube_upgrade_health,
)
from nfv_vim.nfvi._nfvi_infrastructure_module import ( # noqa: F401
nfvi_host_device_image_update,
)
@@ -672,6 +672,17 @@ def nfvi_get_kube_control_plane_pods_ready(callback):
return cmd_id
def nfvi_get_kube_upgrade_health(alarm_ignore_list, callback):
"""Get kube upgrade health status."""
cmd_id = _infrastructure_plugin.invoke_plugin(
"get_kube_upgrade_health",
alarm_ignore_list,
callback=callback,
)
return cmd_id
def nfvi_register_host_add_callback(callback):
"""Register for host add notifications."""
+1
View File
@@ -42,6 +42,7 @@ from nfv_vim.strategy.steps.kube_upgrade_steps import ( # noqa: F401
QueryKubeUpgradeStep,
QueryKubeVersionsStep,
WaitKubeControlPlanePodsReadyStep,
WaitKubernetesUpgradeHealthy,
)
from nfv_vim.strategy._strategy_steps import DisableHostServicesStep # noqa: F401
from nfv_vim.strategy._strategy_steps import FwUpdateAbortHostsStep # noqa: F401
@@ -62,6 +62,7 @@ class StrategyStepNames(Constants, metaclass=Singleton):
QUERY_KUBE_ROOTCA_HOST_UPDATES = Constant("query-kube-rootca-host-updates")
# kube upgrade steps
WAIT_KUBE_CONTROL_PLANE_PODS_READY = Constant("wait-kube-control-plane-pods-ready")
KUBE_WAIT_UPGRADE_HEALTHY = Constant("kube-wait-upgrade-healthy")
APPLY_PATCHES = Constant("apply-patches")
QUERY_KUBE_HOST_UPGRADE = Constant("query-kube-host-upgrade")
QUERY_KUBE_UPGRADE = Constant("query-kube-upgrade")
+1 -1
View File
@@ -2135,7 +2135,7 @@ class SwUpgradeStrategy(
# Duplex: full kube upgrade runs sequentially after sw-deploy
if self._kube_upgrade_version and is_duplex:
self._add_wait_kube_control_plane_pods_ready_stage()
self._add_wait_kubernetes_upgrade_healthy_stage()
self._build_kube_upgrade_stages()
if self._state == strategy.STRATEGY_STATE.BUILD_FAILED:
return
@@ -58,6 +58,7 @@ class StrategyStageNames(Constants, metaclass=Singleton):
KUBE_ROOTCA_UPDATE_START = Constant("kube-rootca-update-start")
# kube upgrade stages
KUBE_WAIT_CONTROL_PLANE_PODS_READY = Constant("kube-wait-control-plane-pods-ready")
KUBE_WAIT_UPGRADE_HEALTHY = Constant("kube-wait-upgrade-healthy")
KUBE_HOST_CORDON = Constant("kube-host-cordon")
KUBE_HOST_UNCORDON = Constant("kube-host-uncordon")
KUBE_UPGRADE_QUERY = Constant("kube-upgrade-query")
@@ -4735,6 +4735,9 @@ def strategy_step_rebuild_from_dict(data):
STRATEGY_STEP_NAME.WAIT_KUBE_CONTROL_PLANE_PODS_READY: (
kube_upgrade_steps.WaitKubeControlPlanePodsReadyStep
),
STRATEGY_STEP_NAME.KUBE_WAIT_UPGRADE_HEALTHY: (
kube_upgrade_steps.WaitKubernetesUpgradeHealthy
),
#
# system config update steps
#
@@ -245,6 +245,17 @@ class KubeUpgradeStages: # pylint: disable=no-member
stage.add_step(strategy.WaitKubeControlPlanePodsReadyStep())
self.apply_phase.add_stage(stage)
def _add_wait_kubernetes_upgrade_healthy_stage(self):
"""Add wait for kubernetes upgrade healthy stage."""
from nfv_vim import strategy
stage = strategy.StrategyStage(
strategy.STRATEGY_STAGE_NAME.KUBE_WAIT_UPGRADE_HEALTHY
)
stage.add_step(strategy.WaitKubernetesUpgradeHealthy())
self.apply_phase.add_stage(stage)
def _add_kube_upgrade_start_stage(self):
"""Add upgrade start strategy stage
@@ -1359,3 +1359,92 @@ class WaitKubeControlPlanePodsReadyStep(TimerBasedPollingStep):
result, _ = super().timeout()
reason = "Kubernetes control-plane pods did not become ready before timeout"
return result, reason
class WaitKubernetesUpgradeHealthy(TimerBasedPollingStep):
"""Wait for Kubernetes Upgrade Healthy - Strategy Step.
Polls sysinv /health/kube-upgrade endpoint until no failures are
reported. Succeeds when the health response contains no '[Fail]'
entries.
Uses a self-scheduling timer to poll at a fixed interval, bypassing the
host audit mechanism which is gated by a 30-second delay.
"""
POLL_INTERVAL_IN_SECS = 5
FIRST_POLL_DELAY_IN_SECS = 15
def __init__(self, timeout_in_secs=180, alarm_ignore_list=None):
super().__init__(
STRATEGY_STEP_NAME.KUBE_WAIT_UPGRADE_HEALTHY,
timeout_in_secs=timeout_in_secs,
)
if alarm_ignore_list is None:
alarm_ignore_list = KUBE_UPGRADE_START_ALARM_IGNORE
self._alarm_ignore_list = alarm_ignore_list
def _poll_action(self):
"""Query kube upgrade health."""
from nfv_vim import nfvi
self._poll_in_progress = True
nfvi.nfvi_get_kube_upgrade_health(
self._alarm_ignore_list, self._query_health_callback()
)
@coroutine
def _query_health_callback(self):
"""Query Kube Upgrade Health Callback."""
response = yield
DLOG.debug("Query-Kube-Upgrade-Health callback response=%s." % response)
self._poll_in_progress = False
if response["completed"]:
result_data = response.get("result-data")
if result_data is None:
self._cleanup_timer()
result = strategy.STRATEGY_STEP_RESULT.FAILED
self.stage.step_complete(
result, "Kube upgrade health check missing result-data"
)
return
health_str = str(result_data)
if "[Fail]" in health_str:
DLOG.info("Kubernetes upgrade health check not passed: %s" % health_str)
# Keep waiting - timer will trigger the next query
else:
DLOG.info("Kubernetes upgrade health check passed: %s" % health_str)
self._cleanup_timer()
result = strategy.STRATEGY_STEP_RESULT.SUCCESS
self.stage.step_complete(result, "")
else:
# API call itself failed - don't fail the step, just retry
DLOG.warn("Query for kube upgrade health did not complete, will retry.")
def from_dict(self, data):
"""Returns the step object initialized using the given dictionary."""
super().from_dict(data)
self._alarm_ignore_list = data.get(
"alarm_ignore_list", KUBE_UPGRADE_START_ALARM_IGNORE
)
return self
def as_dict(self):
"""Represent the step as a dictionary."""
data = super().as_dict()
data["alarm_ignore_list"] = self._alarm_ignore_list
return data
def timeout(self):
"""Strategy Step Timeout Override."""
result, _ = super().timeout()
reason = "Kubernetes upgrade did not become healthy before timeout"
return result, reason