Skip to content

Commit 6924a16

Browse files
committed
ADCM-5640 Reworked adcm_change_maintenance_mode plugin
1 parent 12e0ed1 commit 6924a16

File tree

9 files changed

+247
-286
lines changed

9 files changed

+247
-286
lines changed

python/ansible/plugins/action/adcm_change_maintenance_mode.py

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,13 @@
4545

4646
import sys
4747

48-
from ansible.errors import AnsibleActionFail
49-
from ansible.plugins.action import ActionBase
50-
5148
sys.path.append("/adcm/python")
5249

5350
import adcm.init_django # noqa: F401, isort:skip
5451

55-
from ansible_plugin.maintenance_mode import get_object, validate_args, validate_obj
56-
from cm.models import MaintenanceMode
57-
from cm.services.maintenance_mode import set_maintenance_mode
58-
59-
60-
class ActionModule(ActionBase):
61-
TRANSFERS_FILES = False
62-
_VALID_ARGS = frozenset(["type", "value"])
63-
64-
def run(self, tmp=None, task_vars=None):
65-
super().run(tmp, task_vars)
66-
67-
error = validate_args(task_args=self._task.args)
68-
if error is not None:
69-
raise error
70-
71-
obj, error = get_object(task_vars=task_vars, obj_type=self._task.args["type"])
72-
if error is not None:
73-
raise error
74-
75-
error = validate_obj(obj=obj)
76-
if error is not None:
77-
raise error
52+
from ansible_plugin.base import ADCMAnsiblePlugin
53+
from ansible_plugin.executors.change_maintenance_mode import ADCMChangeMMExecutor
7854

79-
value = MaintenanceMode.ON if self._task.args["value"] else MaintenanceMode.OFF
80-
try:
81-
set_maintenance_mode(obj=obj, value=value)
82-
except Exception as e: # noqa: BLE001
83-
raise AnsibleActionFail("Unexpected error occurred while changing object's maintenance mode") from e
8455

85-
return {"failed": False, "changed": True}
56+
class ActionModule(ADCMAnsiblePlugin):
57+
executor_class = ADCMChangeMMExecutor

python/ansible_plugin/base.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,21 @@ class RuntimeEnvironment(BaseModel):
9595
# Target
9696

9797

98-
class CoreObjectTargetDescription(BaseModel):
98+
class ObjectWithType(BaseModel):
9999
type: TargetTypeLiteral
100100

101-
service_name: str | None = None
102-
component_name: str | None = None
103-
host_id: int | str | None = None
104-
105101
@field_validator("type", mode="before")
106102
@classmethod
107103
def convert_type_to_string(cls, v: Any) -> str:
108104
# requited to pre-process Ansible Strings
109105
return str(v)
110106

107+
108+
class CoreObjectTargetDescription(ObjectWithType):
109+
service_name: str | None = None
110+
component_name: str | None = None
111+
host_id: int | str | None = None
112+
111113
@model_validator(mode="after")
112114
def validate_args_allowed_for_type(self) -> Self:
113115
match self.type:
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain 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,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from contextlib import suppress
14+
from typing import Any, Collection
15+
16+
from cm.issue import update_hierarchy_issues
17+
from cm.models import Host, MaintenanceMode
18+
from cm.services.status.notify import reset_objects_in_mm
19+
from cm.status_api import send_object_update_event
20+
from core.types import ADCMCoreType, CoreObjectDescriptor
21+
from django.db.transaction import atomic
22+
from pydantic import field_validator
23+
24+
from ansible_plugin.base import (
25+
ADCMAnsiblePluginExecutor,
26+
ArgumentsConfig,
27+
CallResult,
28+
ObjectWithType,
29+
PluginExecutorConfig,
30+
RuntimeEnvironment,
31+
TargetConfig,
32+
VarsContextSection,
33+
retrieve_orm_object,
34+
)
35+
from ansible_plugin.errors import PluginIncorrectCallError
36+
37+
38+
class ChangeMaintenanceModeArguments(ObjectWithType):
39+
value: bool
40+
41+
@field_validator("type")
42+
@classmethod
43+
def check_type_is_allowed(cls, v: str) -> str:
44+
if v in ("service", "component", "host"):
45+
return v
46+
47+
message = f"`adcm_change_maintenance_mode` plugin can't be called to change {v}'s MM"
48+
raise ValueError(message)
49+
50+
51+
def from_context_based_on_type(
52+
context_owner: CoreObjectDescriptor, # noqa: ARG001
53+
context: VarsContextSection,
54+
parsed_arguments: Any,
55+
):
56+
if not isinstance(parsed_arguments, ChangeMaintenanceModeArguments):
57+
return ()
58+
59+
return (
60+
CoreObjectDescriptor(
61+
id=getattr(context, f"{parsed_arguments.type}_id"), type=ADCMCoreType(parsed_arguments.type)
62+
),
63+
)
64+
65+
66+
class ADCMChangeMMExecutor(ADCMAnsiblePluginExecutor[ChangeMaintenanceModeArguments, None]):
67+
_config = PluginExecutorConfig(
68+
arguments=ArgumentsConfig(represent_as=ChangeMaintenanceModeArguments),
69+
target=TargetConfig(detectors=(from_context_based_on_type,)),
70+
)
71+
72+
def __call__(
73+
self,
74+
targets: Collection[CoreObjectDescriptor],
75+
arguments: ChangeMaintenanceModeArguments,
76+
runtime: RuntimeEnvironment,
77+
) -> CallResult[None]:
78+
_ = runtime
79+
80+
target, *_ = targets
81+
target_object = retrieve_orm_object(object_=target)
82+
value = MaintenanceMode.ON if arguments.value else MaintenanceMode.OFF
83+
84+
if target_object.maintenance_mode != MaintenanceMode.CHANGING:
85+
return CallResult(
86+
value=None,
87+
changed=False,
88+
error=PluginIncorrectCallError(
89+
f'Only "{MaintenanceMode.CHANGING}" state of object maintenance mode can be changed'
90+
),
91+
)
92+
93+
with atomic():
94+
target_object.maintenance_mode = value
95+
target_object.save(
96+
update_fields=["maintenance_mode"] if isinstance(target_object, Host) else ["_maintenance_mode"]
97+
)
98+
99+
update_hierarchy_issues(target_object.cluster)
100+
101+
with suppress(Exception):
102+
send_object_update_event(object_=target_object, changes={"maintenanceMode": target_object.maintenance_mode})
103+
104+
with suppress(Exception):
105+
reset_objects_in_mm()
106+
107+
return CallResult(value=None, changed=True, error=None)

python/ansible_plugin/maintenance_mode.py

Lines changed: 0 additions & 74 deletions
This file was deleted.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain 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,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from cm.models import MaintenanceMode, ServiceComponent
14+
from cm.services.job.run.repo import JobRepoImpl
15+
16+
from ansible_plugin.errors import (
17+
PluginValidationError,
18+
)
19+
from ansible_plugin.executors.change_maintenance_mode import ADCMChangeMMExecutor
20+
from ansible_plugin.tests.base import BaseTestEffectsOfADCMAnsiblePlugins
21+
22+
23+
class TestEffectsOfADCMAnsiblePlugins(BaseTestEffectsOfADCMAnsiblePlugins):
24+
def setUp(self) -> None:
25+
super().setUp()
26+
27+
self.service_1 = self.add_services_to_cluster(["service_1"], cluster=self.cluster).first()
28+
self.component_1 = ServiceComponent.objects.filter(service=self.service_1).first()
29+
30+
self.add_host_to_cluster(cluster=self.cluster, host=self.host_1)
31+
self.add_host_to_cluster(cluster=self.cluster, host=self.host_2)
32+
33+
def test_simple_call_success(self) -> None:
34+
for object_, arguments, expected_mm in (
35+
(self.service_1, {"type": "service", "value": False}, MaintenanceMode.OFF),
36+
(self.component_1, {"type": "component", "value": True}, MaintenanceMode.ON),
37+
(self.host_1, {"type": "host", "value": True}, MaintenanceMode.ON),
38+
):
39+
object_.maintenance_mode = MaintenanceMode.CHANGING
40+
object_.save()
41+
42+
with self.subTest(arguments["type"]):
43+
task = self.prepare_task(owner=object_, name="dummy")
44+
job, *_ = JobRepoImpl.get_task_jobs(task.id)
45+
46+
executor = self.prepare_executor(
47+
executor_type=ADCMChangeMMExecutor,
48+
call_arguments=arguments,
49+
call_context=job,
50+
)
51+
52+
result = executor.execute()
53+
self.assertIsNone(result.error)
54+
55+
object_.refresh_from_db()
56+
self.assertEqual(object_.maintenance_mode, expected_mm)
57+
58+
def test_call_from_another_context_success(self) -> None:
59+
self.service_1.maintenance_mode = MaintenanceMode.CHANGING
60+
self.service_1.save()
61+
self.component_1.maintenance_mode = MaintenanceMode.CHANGING
62+
self.component_1.save()
63+
self.host_1.maintenance_mode = MaintenanceMode.CHANGING
64+
self.host_1.save()
65+
66+
with self.subTest("component-from-host"):
67+
task = self.prepare_task(owner=self.component_1, name="on_host", host=self.host_1)
68+
job, *_ = JobRepoImpl.get_task_jobs(task.id)
69+
70+
executor = self.prepare_executor(
71+
executor_type=ADCMChangeMMExecutor,
72+
call_arguments="""
73+
type: component
74+
value: yes
75+
""",
76+
call_context=job,
77+
)
78+
79+
result = executor.execute()
80+
self.assertIsNone(result.error)
81+
82+
self.component_1.refresh_from_db()
83+
self.assertEqual(self.component_1.maintenance_mode, MaintenanceMode.ON)
84+
self.service_1.refresh_from_db()
85+
self.assertEqual(self.service_1.maintenance_mode, MaintenanceMode.CHANGING)
86+
self.host_1.refresh_from_db()
87+
self.assertEqual(self.host_1.maintenance_mode, MaintenanceMode.CHANGING)
88+
89+
with self.subTest("service-from-component"):
90+
task = self.prepare_task(owner=self.component_1, name="dummy")
91+
job, *_ = JobRepoImpl.get_task_jobs(task.id)
92+
93+
executor = self.prepare_executor(
94+
executor_type=ADCMChangeMMExecutor,
95+
call_arguments="""
96+
type: service
97+
value: no
98+
""",
99+
call_context=job,
100+
)
101+
102+
result = executor.execute()
103+
self.assertIsNone(result.error)
104+
105+
self.service_1.refresh_from_db()
106+
self.assertEqual(self.service_1.maintenance_mode, MaintenanceMode.OFF)
107+
self.component_1.refresh_from_db()
108+
self.assertEqual(self.component_1.maintenance_mode, MaintenanceMode.ON)
109+
self.host_1.refresh_from_db()
110+
self.assertEqual(self.host_1.maintenance_mode, MaintenanceMode.CHANGING)
111+
112+
def test_incorrect_type_fail(self) -> None:
113+
for type_ in ("cluster", "provider"):
114+
with self.subTest(type_):
115+
task = self.prepare_task(owner=self.component_1, name="dummy")
116+
job, *_ = JobRepoImpl.get_task_jobs(task.id)
117+
executor = self.prepare_executor(
118+
executor_type=ADCMChangeMMExecutor,
119+
call_arguments=f"""
120+
type: {type_}
121+
value: true
122+
""",
123+
call_context=job,
124+
)
125+
126+
result = executor.execute()
127+
128+
self.assertIsInstance(result.error, PluginValidationError)
129+
self.assertIn(f"plugin can't be called to change {type_}'s MM", result.error.message)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy