From c9639b1b078e0e8817c52aad7d65e20d15dfd603 Mon Sep 17 00:00:00 2001 From: ozgen Date: Thu, 11 Sep 2025 14:38:09 +0200 Subject: [PATCH 1/5] Change: update modify_agents and add modify_agent_controller_scan_config --- gvm/protocols/gmp/_gmpnext.py | 82 ++++++++-- gvm/protocols/gmp/requests/next/_agents.py | 153 ++++++++++++++++-- ...est_modify_agent_controller_scan_config.py | 136 ++++++++++++++++ .../entities/agents/test_modify_agents.py | 112 ++++++++++++- .../protocols/gmpnext/entities/test_agents.py | 9 ++ 5 files changed, 453 insertions(+), 39 deletions(-) create mode 100644 tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index b09dc5c4..ecfcb213 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Mapping, Optional, Sequence +from typing import Any, Mapping, Optional, Sequence from gvm.protocols.gmp.requests import EntityID @@ -114,28 +114,42 @@ def modify_agents( agent_ids: list[EntityID], *, authorized: Optional[bool] = None, - min_interval: Optional[int] = None, - heartbeat_interval: Optional[int] = None, - schedule: Optional[str] = None, + config: Optional[Mapping[str, Any]] = None, comment: Optional[str] = None, ) -> T: - """Modify multiple agents + """ + Modify multiple agents. Args: - agent_ids: List of agent UUIDs to modify - authorized: Whether the agent is authorized - min_interval: Minimum scan interval - heartbeat_interval: Interval for sending heartbeats - schedule: Cron-style schedule for agent - comment: Comment for the agents + agent_ids: List of agent UUIDs to modify. + authorized: Whether the agent is authorized (writes 1/0). + config: Nested config matching the new schema, e.g.: + { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], # str or list[str] + }, + "heartbeat": { + "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + } + comment: Optional comment for the change. """ return self._send_request_and_transform_response( Agents.modify_agents( agent_ids=agent_ids, authorized=authorized, - min_interval=min_interval, - heartbeat_interval=heartbeat_interval, - schedule=schedule, + config=config, comment=comment, ) ) @@ -150,6 +164,46 @@ def delete_agents(self, agent_ids: list[EntityID]) -> T: Agents.delete_agents(agent_ids=agent_ids) ) + def modify_agent_control_scan_config( + self, + agent_control_id: EntityID, + config: Mapping[str, Any], + ) -> T: + """ + Modify agent control scan config. + + Args: + agent_control_id: The agent control UUID. + config: Nested config, e.g.: + { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], # str or list[str] + }, + "heartbeat": { + "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + } + Returns: + Request: Prepared XML command. + """ + return self._send_request_and_transform_response( + Agents.modify_agent_control_scan_config( + agent_control_id=agent_control_id, + config=config, + ) + ) + def get_agent_groups( self, *, diff --git a/gvm/protocols/gmp/requests/next/_agents.py b/gvm/protocols/gmp/requests/next/_agents.py index 47b7db96..c0e96033 100644 --- a/gvm/protocols/gmp/requests/next/_agents.py +++ b/gvm/protocols/gmp/requests/next/_agents.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Optional +from typing import Any, Mapping, Optional from gvm.errors import RequiredArgument from gvm.protocols.core import Request @@ -12,6 +12,57 @@ class Agents: + + @staticmethod + def _add_el(parent, name: str, value) -> None: + if value is not None: + parent.add_element(name, str(value)) + + @classmethod + def _append_agent_config(cls, parent, config: Mapping[str, Any]) -> None: + xml_config = parent.add_element("config") + + # agent_control.retry + ac = config["agent_control"] + retry = ac["retry"] + xml_ac = xml_config.add_element("agent_control") + xml_retry = xml_ac.add_element("retry") + cls._add_el(xml_retry, "attempts", retry.get("attempts")) + cls._add_el( + xml_retry, "delay_in_seconds", retry.get("delay_in_seconds") + ) + cls._add_el( + xml_retry, + "max_jitter_in_seconds", + retry.get("max_jitter_in_seconds"), + ) + + # agent_script_executor + se = config["agent_script_executor"] + xml_se = xml_config.add_element("agent_script_executor") + cls._add_el(xml_se, "bulk_size", se.get("bulk_size")) + cls._add_el( + xml_se, + "bulk_throttle_time_in_ms", + se.get("bulk_throttle_time_in_ms"), + ) + cls._add_el(xml_se, "indexer_dir_depth", se.get("indexer_dir_depth")) + sched = se.get("scheduler_cron_time") + if sched: + xml_sched = xml_se.add_element("scheduler_cron_time") + for item in sched: + xml_sched.add_element("item", str(item)) + + # heartbeat + hb = config["heartbeat"] + xml_hb = xml_config.add_element("heartbeat") + cls._add_el( + xml_hb, "interval_in_seconds", hb.get("interval_in_seconds") + ) + cls._add_el( + xml_hb, "miss_until_inactive", hb.get("miss_until_inactive") + ) + @classmethod def get_agents( cls, @@ -41,20 +92,36 @@ def modify_agents( agent_ids: list[EntityID], *, authorized: Optional[bool] = None, - min_interval: Optional[int] = None, - heartbeat_interval: Optional[int] = None, - schedule: Optional[str] = None, + config: Optional[Mapping[str, Any]] = None, comment: Optional[str] = None, ) -> Request: - """Modify multiple agents + """ + Modify multiple agents. Args: - agent_ids: List of agent UUIDs to modify - authorized: Whether the agent is authorized - min_interval: Minimum scan interval - heartbeat_interval: Interval for sending heartbeats - schedule: Cron-style schedule for agent - comment: Comment for the agents + agent_ids: List of agent UUIDs to modify. + authorized: Whether the agent is authorized. + config: Nested config matching the new schema, e.g.: + { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], # list[str] + }, + "heartbeat": { + "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + } + comment: Optional comment for the change. """ if not agent_ids: raise RequiredArgument( @@ -69,12 +136,10 @@ def modify_agents( if authorized is not None: cmd.add_element("authorized", to_bool(authorized)) - if min_interval is not None: - cmd.add_element("min_interval", str(min_interval)) - if heartbeat_interval is not None: - cmd.add_element("heartbeat_interval", str(heartbeat_interval)) - if schedule: - cmd.add_element("schedule", schedule) + + if config is not None: + cls._append_agent_config(cmd, config) + if comment: cmd.add_element("comment", comment) @@ -99,3 +164,57 @@ def delete_agents(cls, agent_ids: list[EntityID]) -> Request: xml_agents.add_element("agent", attrs={"id": agent_id}) return cmd + + @classmethod + def modify_agent_control_scan_config( + cls, + agent_control_id: EntityID, + config: Mapping[str, Any], + ) -> Request: + """ + Modify agent control scan config. + + Args: + agent_control_id: The agent control UUID. + config: Nested config, e.g.: + { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], # str or list[str] + }, + "heartbeat": { + "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + } + Returns: + Request: Prepared XML command. + """ + if not agent_control_id: + raise RequiredArgument( + function=cls.modify_agent_control_scan_config.__name__, + argument="agent_control_id", + ) + if not config: + raise RequiredArgument( + function=cls.modify_agent_control_scan_config.__name__, + argument="config", + ) + + cmd = XmlCommand( + "modify_agent_control_scan_config", + ) + cmd.set_attribute("agent_control_id", str(agent_control_id)) + + cls._append_agent_config(cmd, config) + + return cmd diff --git a/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py b/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py new file mode 100644 index 00000000..9e535b78 --- /dev/null +++ b/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gvm.errors import RequiredArgument + + +class GmpModifyAgentControllerScanConfigTestMixin: + def test_modify_agent_control_scan_config_full(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", + config=cfg, + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"" + b"" + b"6" + b"60" + b"10" + b"" + b"" + b"" + b"2" + b"300" + b"100" + b"" + b"0 */12 * * *" + b"" + b"" + b"" + b"300" + b"1" + b"" + b"" + b"" + ) + + def test_modify_agent_control_scan_config_with_missing_element(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + # max_jitter_in_seconds is missing + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", + config=cfg, + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"" + b"" + b"6" + b"60" + b"" + b"" + b"" + b"2" + b"300" + b"100" + b"" + b"0 */12 * * *" + b"" + b"" + b"" + b"300" + b"1" + b"" + b"" + b"" + ) + + def test_modify_agent_control_scan_config_missing_id_raises(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "", # missing id + config={ + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": { + "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + }, + ) + + def test_modify_agent_control_scan_config_missing_config_raises(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", + config=None, # missing config + ) diff --git a/tests/protocols/gmpnext/entities/agents/test_modify_agents.py b/tests/protocols/gmpnext/entities/agents/test_modify_agents.py index 50777d93..e7e54613 100644 --- a/tests/protocols/gmpnext/entities/agents/test_modify_agents.py +++ b/tests/protocols/gmpnext/entities/agents/test_modify_agents.py @@ -15,13 +15,94 @@ def test_modify_agents_basic(self): b"" ) - def test_modify_agents_with_all_fields(self): + def test_modify_agents_with_authorized_only(self): + self.gmp.modify_agents( + agent_ids=["agent-123", "agent-456"], authorized=True + ) + + self.connection.send.has_been_called_with( + b"" + b'' + b"1" + b"" + ) + + def test_modify_agents_with_full_config_and_comment(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + + self.gmp.modify_agents( + agent_ids=["agent-123", "agent-456"], + authorized=True, + config=cfg, + comment="Updated agents", + ) + + self.connection.send.has_been_called_with( + b"" + b'' + b"1" + b"" + b"" + b"" + b"6" + b"60" + b"10" + b"" + b"" + b"" + b"2" + b"300" + b"100" + b"" + b"0 */12 * * *" + b"" + b"" + b"" + b"300" + b"1" + b"" + b"" + b"Updated agents" + b"" + ) + + def test_modify_agents_with_full_config_with_missing_element(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + # scheduler_cron_time is missing + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + self.gmp.modify_agents( agent_ids=["agent-123", "agent-456"], authorized=True, - min_interval=300, - heartbeat_interval=600, - schedule="@every 6h", + config=cfg, comment="Updated agents", ) @@ -29,13 +110,28 @@ def test_modify_agents_with_all_fields(self): b"" b'' b"1" - b"300" - b"600" - b"@every 6h" + b"" + b"" + b"" + b"6" + b"60" + b"10" + b"" + b"" + b"" + b"2" + b"300" + b"100" + b"" + b"" + b"300" + b"1" + b"" + b"" b"Updated agents" b"" ) - def test_modify_agents_without_ids(self): + def test_modify_agents_without_ids_raises(self): with self.assertRaises(RequiredArgument): self.gmp.modify_agents(agent_ids=[]) diff --git a/tests/protocols/gmpnext/entities/test_agents.py b/tests/protocols/gmpnext/entities/test_agents.py index accf9e5f..6c5aaacc 100644 --- a/tests/protocols/gmpnext/entities/test_agents.py +++ b/tests/protocols/gmpnext/entities/test_agents.py @@ -10,6 +10,9 @@ from .agents.test_get_agents import ( GmpGetAgentsTestMixin, ) +from .agents.test_modify_agent_controller_scan_config import ( + GmpModifyAgentControllerScanConfigTestMixin, +) from .agents.test_modify_agents import ( GmpModifyAgentsTestMixin, ) @@ -25,3 +28,9 @@ class GMPModifyAgentsTestCase(GmpModifyAgentsTestMixin, GMPTestCase): class GMPDeleteAgentsTestCase(GmpDeleteAgentsTestMixin, GMPTestCase): pass + + +class GMPModifyAgentControllerScanConfigTestCase( + GmpModifyAgentControllerScanConfigTestMixin, GMPTestCase +): + pass From 5f6e87ead4b7907821c4a42aa7273139196f8907 Mon Sep 17 00:00:00 2001 From: ozgen Date: Thu, 11 Sep 2025 15:04:16 +0200 Subject: [PATCH 2/5] Fix documentation error in workflow --- gvm/protocols/gmp/_gmpnext.py | 46 ++------------------- gvm/protocols/gmp/requests/next/_agents.py | 48 +++++++++++++++++++--- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index ecfcb213..fae99d99 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -122,27 +122,8 @@ def modify_agents( Args: agent_ids: List of agent UUIDs to modify. - authorized: Whether the agent is authorized (writes 1/0). - config: Nested config matching the new schema, e.g.: - { - "agent_control": { - "retry": { - "attempts": 6, - "delay_in_seconds": 60, - "max_jitter_in_seconds": 10, - } - }, - "agent_script_executor": { - "bulk_size": 2, - "bulk_throttle_time_in_ms": 300, - "indexer_dir_depth": 100, - "scheduler_cron_time": ["0 */12 * * *"], # str or list[str] - }, - "heartbeat": { - "interval_in_seconds": 300, - "miss_until_inactive": 1, - }, - } + authorized: Whether the agent is authorized. + config: Nested config for Agent Controller. comment: Optional comment for the change. """ return self._send_request_and_transform_response( @@ -174,28 +155,7 @@ def modify_agent_control_scan_config( Args: agent_control_id: The agent control UUID. - config: Nested config, e.g.: - { - "agent_control": { - "retry": { - "attempts": 6, - "delay_in_seconds": 60, - "max_jitter_in_seconds": 10, - } - }, - "agent_script_executor": { - "bulk_size": 2, - "bulk_throttle_time_in_ms": 300, - "indexer_dir_depth": 100, - "scheduler_cron_time": ["0 */12 * * *"], # str or list[str] - }, - "heartbeat": { - "interval_in_seconds": 300, - "miss_until_inactive": 1, - }, - } - Returns: - Request: Prepared XML command. + config: Nested config for Agent Controller. """ return self._send_request_and_transform_response( Agents.modify_agent_control_scan_config( diff --git a/gvm/protocols/gmp/requests/next/_agents.py b/gvm/protocols/gmp/requests/next/_agents.py index c0e96033..f0d68fef 100644 --- a/gvm/protocols/gmp/requests/next/_agents.py +++ b/gvm/protocols/gmp/requests/next/_agents.py @@ -15,11 +15,51 @@ class Agents: @staticmethod def _add_el(parent, name: str, value) -> None: + """ + Helper to add a sub-element with a value if the value is not None. + + Args: + parent: The XML parent element to which the new element is added. + name: Name of the sub-element to create. + value: Value to set as the text of the sub-element. If None, the + element will not be created. + """ if value is not None: parent.add_element(name, str(value)) @classmethod def _append_agent_config(cls, parent, config: Mapping[str, Any]) -> None: + """ + Append an agent configuration block to the given XML parent element. + + Expected config structure:: + + { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10 + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"] + }, + "heartbeat": { + "interval_in_seconds": 300, + "miss_until_inactive": 1 + } + } + + Args: + parent: The XML parent element to which the `` element + should be appended. + config: Mapping containing the agent configuration fields to + serialize. + """ xml_config = parent.add_element("config") # agent_control.retry @@ -101,10 +141,10 @@ def modify_agents( Args: agent_ids: List of agent UUIDs to modify. authorized: Whether the agent is authorized. - config: Nested config matching the new schema, e.g.: + config: Nested config, e.g.: { "agent_control": { - "retry": { + "retry": { "attempts": 6, "delay_in_seconds": 60, "max_jitter_in_seconds": 10, @@ -114,7 +154,7 @@ def modify_agents( "bulk_size": 2, "bulk_throttle_time_in_ms": 300, "indexer_dir_depth": 100, - "scheduler_cron_time": ["0 */12 * * *"], # list[str] + "scheduler_cron_time": ["0 */12 * * *"], # str or list[str] }, "heartbeat": { "interval_in_seconds": 300, @@ -196,8 +236,6 @@ def modify_agent_control_scan_config( "miss_until_inactive": 1, }, } - Returns: - Request: Prepared XML command. """ if not agent_control_id: raise RequiredArgument( From d9c207d6fc6e8b217bd120bca8a44a5cd722bdbf Mon Sep 17 00:00:00 2001 From: ozgen Date: Fri, 12 Sep 2025 08:32:22 +0200 Subject: [PATCH 3/5] Add validation for agent config and raise descriptive errors --- gvm/protocols/gmp/requests/next/_agents.py | 91 +++++++++++++++---- ...est_modify_agent_controller_scan_config.py | 36 ++------ .../entities/agents/test_modify_agents.py | 39 ++------ 3 files changed, 88 insertions(+), 78 deletions(-) diff --git a/gvm/protocols/gmp/requests/next/_agents.py b/gvm/protocols/gmp/requests/next/_agents.py index f0d68fef..3eaaee52 100644 --- a/gvm/protocols/gmp/requests/next/_agents.py +++ b/gvm/protocols/gmp/requests/next/_agents.py @@ -7,25 +7,76 @@ from gvm.errors import RequiredArgument from gvm.protocols.core import Request from gvm.protocols.gmp.requests._entity_id import EntityID -from gvm.utils import to_bool +from gvm.utils import SupportsStr, to_bool from gvm.xml import XmlCommand class Agents: @staticmethod - def _add_el(parent, name: str, value) -> None: + def _add_element(element, name: str, value: Optional[SupportsStr]) -> None: """ Helper to add a sub-element with a value if the value is not None. Args: - parent: The XML parent element to which the new element is added. + element: The XML parent element to which the new element is added. name: Name of the sub-element to create. value: Value to set as the text of the sub-element. If None, the element will not be created. """ if value is not None: - parent.add_element(name, str(value)) + element.add_element(name, str(value)) + + @classmethod + def _validate_agent_config( + cls, config: Mapping[str, Any], *, caller: str + ) -> None: + """Ensure all required fields exist and are non-empty.""" + + def valid(d: Mapping[str, Any], key: str, path: str): + if ( + not isinstance(d, Mapping) + or d.get(key) is None + or d.get(key) == "" + ): + raise RequiredArgument( + function=caller, argument=f"config.{path}{key}" + ) + + # agent_control.retry + ac = config.get("agent_control") + valid(config, "agent_control", "") + retry = ac.get("retry") if isinstance(ac, Mapping) else None + valid(ac, "retry", "agent_control.") + valid(retry, "attempts", "agent_control.retry.") + valid(retry, "delay_in_seconds", "agent_control.retry.") + valid(retry, "max_jitter_in_seconds", "agent_control.retry.") + + # agent_script_executor + se = config.get("agent_script_executor") + valid(config, "agent_script_executor", "") + valid(se, "bulk_size", "agent_script_executor.") + valid(se, "bulk_throttle_time_in_ms", "agent_script_executor.") + valid(se, "indexer_dir_depth", "agent_script_executor.") + + sched = ( + se.get("scheduler_cron_time") if isinstance(se, Mapping) else None + ) + if isinstance(sched, (list, tuple, set)): + items = list(sched) + else: + items = [] + if not items or any(str(x).strip() == "" for x in items): + raise RequiredArgument( + function=caller, + argument="config.agent_script_executor.scheduler_cron_time", + ) + + # heartbeat + hb = config.get("heartbeat") + valid(config, "heartbeat", "") + valid(hb, "interval_in_seconds", "heartbeat.") + valid(hb, "miss_until_inactive", "heartbeat.") @classmethod def _append_agent_config(cls, parent, config: Mapping[str, Any]) -> None: @@ -67,11 +118,11 @@ def _append_agent_config(cls, parent, config: Mapping[str, Any]) -> None: retry = ac["retry"] xml_ac = xml_config.add_element("agent_control") xml_retry = xml_ac.add_element("retry") - cls._add_el(xml_retry, "attempts", retry.get("attempts")) - cls._add_el( + cls._add_element(xml_retry, "attempts", retry.get("attempts")) + cls._add_element( xml_retry, "delay_in_seconds", retry.get("delay_in_seconds") ) - cls._add_el( + cls._add_element( xml_retry, "max_jitter_in_seconds", retry.get("max_jitter_in_seconds"), @@ -80,26 +131,27 @@ def _append_agent_config(cls, parent, config: Mapping[str, Any]) -> None: # agent_script_executor se = config["agent_script_executor"] xml_se = xml_config.add_element("agent_script_executor") - cls._add_el(xml_se, "bulk_size", se.get("bulk_size")) - cls._add_el( + cls._add_element(xml_se, "bulk_size", se.get("bulk_size")) + cls._add_element( xml_se, "bulk_throttle_time_in_ms", se.get("bulk_throttle_time_in_ms"), ) - cls._add_el(xml_se, "indexer_dir_depth", se.get("indexer_dir_depth")) + cls._add_element( + xml_se, "indexer_dir_depth", se.get("indexer_dir_depth") + ) sched = se.get("scheduler_cron_time") - if sched: - xml_sched = xml_se.add_element("scheduler_cron_time") - for item in sched: - xml_sched.add_element("item", str(item)) + xml_sched = xml_se.add_element("scheduler_cron_time") + for item in sched: + xml_sched.add_element("item", str(item)) # heartbeat hb = config["heartbeat"] xml_hb = xml_config.add_element("heartbeat") - cls._add_el( + cls._add_element( xml_hb, "interval_in_seconds", hb.get("interval_in_seconds") ) - cls._add_el( + cls._add_element( xml_hb, "miss_until_inactive", hb.get("miss_until_inactive") ) @@ -178,6 +230,9 @@ def modify_agents( cmd.add_element("authorized", to_bool(authorized)) if config is not None: + cls._validate_agent_config( + config, caller=cls.modify_agents.__name__ + ) cls._append_agent_config(cmd, config) if comment: @@ -248,6 +303,10 @@ def modify_agent_control_scan_config( argument="config", ) + cls._validate_agent_config( + config, caller=cls.modify_agent_control_scan_config.__name__ + ) + cmd = XmlCommand( "modify_agent_control_scan_config", ) diff --git a/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py b/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py index 9e535b78..3dba9dbb 100644 --- a/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py +++ b/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py @@ -55,7 +55,7 @@ def test_modify_agent_control_scan_config_full(self): b"" ) - def test_modify_agent_control_scan_config_with_missing_element(self): + def test_modify_agent_control_scan_config_with_missing_element_raises(self): cfg = { "agent_control": { "retry": { @@ -73,35 +73,11 @@ def test_modify_agent_control_scan_config_with_missing_element(self): "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, } - self.gmp.modify_agent_control_scan_config( - "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", - config=cfg, - ) - - self.connection.send.has_been_called_with( - b'' - b"" - b"" - b"" - b"6" - b"60" - b"" - b"" - b"" - b"2" - b"300" - b"100" - b"" - b"0 */12 * * *" - b"" - b"" - b"" - b"300" - b"1" - b"" - b"" - b"" - ) + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", + config=cfg, + ) def test_modify_agent_control_scan_config_missing_id_raises(self): with self.assertRaises(RequiredArgument): diff --git a/tests/protocols/gmpnext/entities/agents/test_modify_agents.py b/tests/protocols/gmpnext/entities/agents/test_modify_agents.py index e7e54613..83737b53 100644 --- a/tests/protocols/gmpnext/entities/agents/test_modify_agents.py +++ b/tests/protocols/gmpnext/entities/agents/test_modify_agents.py @@ -99,38 +99,13 @@ def test_modify_agents_with_full_config_with_missing_element(self): "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, } - self.gmp.modify_agents( - agent_ids=["agent-123", "agent-456"], - authorized=True, - config=cfg, - comment="Updated agents", - ) - - self.connection.send.has_been_called_with( - b"" - b'' - b"1" - b"" - b"" - b"" - b"6" - b"60" - b"10" - b"" - b"" - b"" - b"2" - b"300" - b"100" - b"" - b"" - b"300" - b"1" - b"" - b"" - b"Updated agents" - b"" - ) + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents( + agent_ids=["agent-123", "agent-456"], + authorized=True, + config=cfg, + comment="Updated agents", + ) def test_modify_agents_without_ids_raises(self): with self.assertRaises(RequiredArgument): From bd3cf984f1f0ec13c175de59b030a6fbb838fdbc Mon Sep 17 00:00:00 2001 From: ozgen Date: Fri, 12 Sep 2025 08:36:41 +0200 Subject: [PATCH 4/5] Fix linting issue for agent config --- gvm/protocols/gmp/requests/next/_agents.py | 72 ++++++++++++---------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/gvm/protocols/gmp/requests/next/_agents.py b/gvm/protocols/gmp/requests/next/_agents.py index 3eaaee52..9cba99a9 100644 --- a/gvm/protocols/gmp/requests/next/_agents.py +++ b/gvm/protocols/gmp/requests/next/_agents.py @@ -2,19 +2,19 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Any, Mapping, Optional +from typing import Any, Mapping, Optional, Sequence from gvm.errors import RequiredArgument from gvm.protocols.core import Request from gvm.protocols.gmp.requests._entity_id import EntityID -from gvm.utils import SupportsStr, to_bool +from gvm.utils import to_bool from gvm.xml import XmlCommand class Agents: @staticmethod - def _add_element(element, name: str, value: Optional[SupportsStr]) -> None: + def _add_element(element, name: str, value: Any) -> None: """ Helper to add a sub-element with a value if the value is not None. @@ -31,52 +31,56 @@ def _add_element(element, name: str, value: Optional[SupportsStr]) -> None: def _validate_agent_config( cls, config: Mapping[str, Any], *, caller: str ) -> None: - """Ensure all required fields exist and are non-empty.""" - - def valid(d: Mapping[str, Any], key: str, path: str): - if ( - not isinstance(d, Mapping) - or d.get(key) is None - or d.get(key) == "" - ): + """Ensure all required fields exist, are well-shaped, and non-empty.""" + + def valid_map(d: Any, key: str, path: str) -> Mapping[str, Any]: + if not isinstance(d, Mapping): + raise RequiredArgument( + function=caller, argument=f"config.{path.rstrip('.')}" + ) + v = d.get(key) + if not isinstance(v, Mapping): + raise RequiredArgument( + function=caller, argument=f"config.{path}{key}" + ) + return v + + def valid_value(d: Mapping[str, Any], key: str, path: str) -> Any: + v = d.get(key) + if v is None or (isinstance(v, str) and v.strip() == ""): raise RequiredArgument( function=caller, argument=f"config.{path}{key}" ) + return v # agent_control.retry - ac = config.get("agent_control") - valid(config, "agent_control", "") - retry = ac.get("retry") if isinstance(ac, Mapping) else None - valid(ac, "retry", "agent_control.") - valid(retry, "attempts", "agent_control.retry.") - valid(retry, "delay_in_seconds", "agent_control.retry.") - valid(retry, "max_jitter_in_seconds", "agent_control.retry.") + ac = valid_map(config, "agent_control", "") + retry = valid_map(ac, "retry", "agent_control.") + valid_value(retry, "attempts", "agent_control.retry.") + valid_value(retry, "delay_in_seconds", "agent_control.retry.") + valid_value(retry, "max_jitter_in_seconds", "agent_control.retry.") # agent_script_executor - se = config.get("agent_script_executor") - valid(config, "agent_script_executor", "") - valid(se, "bulk_size", "agent_script_executor.") - valid(se, "bulk_throttle_time_in_ms", "agent_script_executor.") - valid(se, "indexer_dir_depth", "agent_script_executor.") - - sched = ( - se.get("scheduler_cron_time") if isinstance(se, Mapping) else None - ) - if isinstance(sched, (list, tuple, set)): - items = list(sched) + se = valid_map(config, "agent_script_executor", "") + valid_value(se, "bulk_size", "agent_script_executor.") + valid_value(se, "bulk_throttle_time_in_ms", "agent_script_executor.") + valid_value(se, "indexer_dir_depth", "agent_script_executor.") + + sched = se.get("scheduler_cron_time") + if isinstance(sched, Sequence) and not isinstance(sched, (str, bytes)): + items = [str(x) for x in sched] else: items = [] - if not items or any(str(x).strip() == "" for x in items): + if not items or any(not str(x).strip() for x in items): raise RequiredArgument( function=caller, argument="config.agent_script_executor.scheduler_cron_time", ) # heartbeat - hb = config.get("heartbeat") - valid(config, "heartbeat", "") - valid(hb, "interval_in_seconds", "heartbeat.") - valid(hb, "miss_until_inactive", "heartbeat.") + hb = valid_map(config, "heartbeat", "") + valid_value(hb, "interval_in_seconds", "heartbeat.") + valid_value(hb, "miss_until_inactive", "heartbeat.") @classmethod def _append_agent_config(cls, parent, config: Mapping[str, Any]) -> None: From caae97c1379e0fffd582a62d9ed2a85631eb3b9b Mon Sep 17 00:00:00 2001 From: ozgen Date: Fri, 12 Sep 2025 09:18:16 +0200 Subject: [PATCH 5/5] Add missing unit tests --- ...est_modify_agent_controller_scan_config.py | 343 ++++++++++++++++++ .../entities/agents/test_modify_agents.py | 291 +++++++++++++++ 2 files changed, 634 insertions(+) diff --git a/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py b/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py index 3dba9dbb..22bc54c8 100644 --- a/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py +++ b/tests/protocols/gmpnext/entities/agents/test_modify_agent_controller_scan_config.py @@ -110,3 +110,346 @@ def test_modify_agent_control_scan_config_missing_config_raises(self): "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=None, # missing config ) + + def test_modify_agent_control_scan_config_config_not_mapping_raises(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=123 + ) + + def test_modify_agent_control_scan_config_agent_control_not_mapping_raises( + self, + ): + cfg = { + "agent_control": "oops-not-a-mapping", + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_scheduler_empty_list_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": [], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_scheduler_with_empty_item_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["", " "], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_agent_control_raises( + self, + ): + cfg = { + # "agent_control": missing + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_retry_block_raises(self): + cfg = { + "agent_control": { + # "retry": {} + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_retry_attempts_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + # "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_retry_delay_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + # "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_retry_max_jitter_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + # "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_agent_script_executor_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + # "agent_script_executor": missing + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_bulk_size_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + # "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_bulk_throttle_time_in_ms_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + # "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_indexer_dir_depth_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + # "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_heartbeat_block_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + # "heartbeat": missing + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_heartbeat_interval_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": { + # "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) + + def test_modify_agent_control_scan_config_missing_heartbeat_miss_until_inactive_raises( + self, + ): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": { + "interval_in_seconds": 300, + # "miss_until_inactive": 1, + }, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agent_control_scan_config( + "fb6451bf-ec5a-45a8-8bab-5cf4b862e51b", config=cfg + ) diff --git a/tests/protocols/gmpnext/entities/agents/test_modify_agents.py b/tests/protocols/gmpnext/entities/agents/test_modify_agents.py index 83737b53..673a1241 100644 --- a/tests/protocols/gmpnext/entities/agents/test_modify_agents.py +++ b/tests/protocols/gmpnext/entities/agents/test_modify_agents.py @@ -110,3 +110,294 @@ def test_modify_agents_with_full_config_with_missing_element(self): def test_modify_agents_without_ids_raises(self): with self.assertRaises(RequiredArgument): self.gmp.modify_agents(agent_ids=[]) + + def test_modify_agents_scheduler_empty_list_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": [], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_scheduler_with_empty_item_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["", " "], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_agent_control_raises(self): + cfg = { + # "agent_control": missing + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_retry_block_raises(self): + cfg = { + "agent_control": { # retry missing + # "retry": {} + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_retry_attempts_raises(self): + cfg = { + "agent_control": { + "retry": { + # "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_retry_delay_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + # "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_retry_max_jitter_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + # "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_agent_script_executor_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + # "agent_script_executor": missing + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_bulk_size_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + # "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_bulk_throttle_time_in_ms_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + # "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_indexer_dir_depth_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + # "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_heartbeat_block_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + # "heartbeat": missing + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_heartbeat_interval_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": { + # "interval_in_seconds": 300, + "miss_until_inactive": 1, + }, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_missing_heartbeat_miss_until_inactive_raises(self): + cfg = { + "agent_control": { + "retry": { + "attempts": 6, + "delay_in_seconds": 60, + "max_jitter_in_seconds": 10, + } + }, + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": { + "interval_in_seconds": 300, + # "miss_until_inactive": 1, + }, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg) + + def test_modify_agents_config_not_mapping_raises(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents( + agent_ids=["agent-123"], config="not-a-mapping" + ) + + def test_modify_agents_agent_control_not_mapping_raises(self): + cfg = { + "agent_control": "oops-not-a-mapping", + "agent_script_executor": { + "bulk_size": 2, + "bulk_throttle_time_in_ms": 300, + "indexer_dir_depth": 100, + "scheduler_cron_time": ["0 */12 * * *"], + }, + "heartbeat": {"interval_in_seconds": 300, "miss_until_inactive": 1}, + } + with self.assertRaises(RequiredArgument): + self.gmp.modify_agents(agent_ids=["agent-123"], config=cfg)