lvi-api-client 0.2.2__tar.gz → 0.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/PKG-INFO +1 -1
- lvi_api_client-0.2.6/VERSION +1 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/logicvein/lvi_api_client.py +127 -27
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/PKG-INFO +1 -1
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/tests/test_lvi_api_client.py +60 -28
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/tests/test_server_request_shapes.py +77 -0
- lvi_api_client-0.2.2/VERSION +0 -1
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/LICENSE +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/README.md +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/pyproject.toml +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/setup.cfg +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/logicvein/__init__.py +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/SOURCES.txt +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/dependency_links.txt +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/requires.txt +0 -0
- {lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.6
|
|
@@ -3003,6 +3003,7 @@ class LogicVeinAPIClient:
|
|
|
3003
3003
|
commands (str): CLI commands to run, one per line.
|
|
3004
3004
|
job_name (str): Display name for the job.
|
|
3005
3005
|
backup_on_completion (bool): If True, LVI backs up the device after the job runs.
|
|
3006
|
+
description (str): The job's description. Empty uses ``job_name``.
|
|
3006
3007
|
|
|
3007
3008
|
Returns:
|
|
3008
3009
|
Dict[str, Any]: The saved job record (contains ``jobId``), or None on failure.
|
|
@@ -3011,7 +3012,7 @@ class LogicVeinAPIClient:
|
|
|
3011
3012
|
{
|
|
3012
3013
|
"jobId": 0,
|
|
3013
3014
|
"jobName": job_name,
|
|
3014
|
-
"description": description or
|
|
3015
|
+
"description": description or job_name,
|
|
3015
3016
|
"managedNetworks": [network],
|
|
3016
3017
|
"jobType": "Script Tool Job",
|
|
3017
3018
|
"subType": "org.ziptie.tools.scripts.commandRunner",
|
|
@@ -3052,6 +3053,8 @@ class LogicVeinAPIClient:
|
|
|
3052
3053
|
network (str): LVI managed network the device belongs to.
|
|
3053
3054
|
commands (str): CLI commands to run, one per line.
|
|
3054
3055
|
job_name (str): Display name for the job in the LVI scheduler.
|
|
3056
|
+
backup_on_completion (bool): If True, LVI backs up the device after the job runs.
|
|
3057
|
+
description (str): The job's description. Empty uses ``job_name``.
|
|
3055
3058
|
|
|
3056
3059
|
Returns:
|
|
3057
3060
|
Dict[str, Any]: The job execution result dict from LVI (contains ``id``
|
|
@@ -3071,7 +3074,7 @@ class LogicVeinAPIClient:
|
|
|
3071
3074
|
"tool": "org.ziptie.tools.scripts.commandRunner",
|
|
3072
3075
|
"backupOnCompletion": "true" if backup_on_completion else "false",
|
|
3073
3076
|
},
|
|
3074
|
-
description=description or
|
|
3077
|
+
description=description or job_name,
|
|
3075
3078
|
)
|
|
3076
3079
|
|
|
3077
3080
|
# =====================================================
|
|
@@ -3863,18 +3866,40 @@ class LogicVeinAPIClient:
|
|
|
3863
3866
|
return False
|
|
3864
3867
|
|
|
3865
3868
|
def associate_monitor_set(self, set_id: int, host_queries: List[str]) -> bool:
|
|
3866
|
-
"""
|
|
3869
|
+
"""Deploy a monitor set to the devices that match every one of the given queries.
|
|
3870
|
+
|
|
3871
|
+
Calls Collection.associateMonitorSet. The server answers null whether or not any device
|
|
3872
|
+
matched, so check :meth:`get_monitors_for_host` to know what was deployed.
|
|
3867
3873
|
|
|
3868
|
-
|
|
3874
|
+
The server combines the queries with AND, so one entry for each device matches none of
|
|
3875
|
+
them. Name several devices in one entry, each with its network:
|
|
3876
|
+
``['ipCsv=192.0.2.1@Default,192.0.2.2@Default']``. An address with no network of its own is
|
|
3877
|
+
not looked up in the network named after a later one. Another query can narrow it:
|
|
3878
|
+
``['ipCsv=192.0.2.1@Default,192.0.2.2@Default', 'ipAddress=192.0.2.1']``.
|
|
3879
|
+
|
|
3880
|
+
With no queries the server deploys to every device, so that is refused here rather than
|
|
3881
|
+
passed on from an empty list.
|
|
3869
3882
|
|
|
3870
3883
|
Args:
|
|
3871
3884
|
set_id (int): The ID of the monitor set to associate.
|
|
3872
|
-
host_queries (List[str]):
|
|
3873
|
-
|
|
3885
|
+
host_queries (List[str]): The queries, at least one, none of them blank, and at most
|
|
3886
|
+
one of them an ``ipCsv`` query.
|
|
3874
3887
|
|
|
3875
3888
|
Returns:
|
|
3876
|
-
bool: True if
|
|
3877
|
-
|
|
3889
|
+
bool: True if the server answered without an error, otherwise False. This does not
|
|
3890
|
+
mean any device matched.
|
|
3891
|
+
|
|
3892
|
+
Raises:
|
|
3893
|
+
ValueError: If there is no query, a query is blank, or more than one is ``ipCsv``.
|
|
3894
|
+
Nothing is sent.
|
|
3895
|
+
"""
|
|
3896
|
+
if not host_queries or any(not query.strip() for query in host_queries):
|
|
3897
|
+
raise ValueError("host_queries needs at least one query and no blank ones: none would match every device")
|
|
3898
|
+
if sum(query.strip().startswith("ipCsv=") for query in host_queries) > 1:
|
|
3899
|
+
raise ValueError(
|
|
3900
|
+
"Queries combine with AND, so several ipCsv queries cannot name several devices. "
|
|
3901
|
+
"List them in one: ipCsv=IP@Network,IP@Network"
|
|
3902
|
+
)
|
|
3878
3903
|
response = self.call("Collection.associateMonitorSet", setId=set_id, hostQueries=host_queries)
|
|
3879
3904
|
|
|
3880
3905
|
if response and isinstance(response, dict) and "result" in response and response["result"] in (None, "None"):
|
|
@@ -3891,14 +3916,18 @@ class LogicVeinAPIClient:
|
|
|
3891
3916
|
set, or with the ID of an existing monitor, and leaves the old one as it was. Use
|
|
3892
3917
|
:meth:`save_monitor` to change one.
|
|
3893
3918
|
|
|
3919
|
+
The server answers success but stores neither ``disabled`` nor ``hostSpecific`` as true,
|
|
3920
|
+
here or in :meth:`save_monitor`. To stop a monitor on a device, use
|
|
3921
|
+
:meth:`disable_monitor_for_host`.
|
|
3922
|
+
|
|
3894
3923
|
Each monitor object should have:
|
|
3895
3924
|
{
|
|
3896
3925
|
"monitorId": int - Use -1 for new monitors; assigned ID returned in response,
|
|
3897
3926
|
"monitorName": str - Monitor name,
|
|
3898
3927
|
"setId": int - Must match set_id,
|
|
3899
3928
|
"monitorType": str - Type (e.g. "snmp", "tcp", "icmp", "telegraf", "web", "catchalltrap"),
|
|
3900
|
-
"disabled": bool -
|
|
3901
|
-
"hostSpecific": bool -
|
|
3929
|
+
"disabled": bool - Stored as false whatever is sent,
|
|
3930
|
+
"hostSpecific": bool - Stored as false whatever is sent,
|
|
3902
3931
|
"managedNetworks": list[str] - Networks the monitor belongs to,
|
|
3903
3932
|
"retentionPolicy": str - Data retention (e.g. "THREE_MONTHS"),
|
|
3904
3933
|
"json": dict - Monitor-type-specific config (OIDs, pollingPeriod, table, triggers, etc.),
|
|
@@ -3951,7 +3980,8 @@ class LogicVeinAPIClient:
|
|
|
3951
3980
|
Args:
|
|
3952
3981
|
set_name (str): The set's name. Use :meth:`is_monitor_set_name_unique` to check it first.
|
|
3953
3982
|
managed_networks (List[str]): The networks the set belongs to, at least one.
|
|
3954
|
-
smart_set (bool): Whether this is a smart set
|
|
3983
|
+
smart_set (bool): Whether this is a smart set, one applied automatically to newly
|
|
3984
|
+
discovered devices.
|
|
3955
3985
|
|
|
3956
3986
|
Returns:
|
|
3957
3987
|
Dict[str, Any]: The saved set (``setId``, ``setName``, ``smartSet``,
|
|
@@ -4054,7 +4084,9 @@ class LogicVeinAPIClient:
|
|
|
4054
4084
|
def save_monitor(self, monitor: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
4055
4085
|
"""Update a monitor in place.
|
|
4056
4086
|
|
|
4057
|
-
The monitor keeps its ID, and a device it is deployed to gets the change.
|
|
4087
|
+
The monitor keeps its ID, and a device it is deployed to gets the change. A change to
|
|
4088
|
+
``disabled`` or ``isHostSpecific`` is answered with success and not stored, and a changed
|
|
4089
|
+
``monitorType`` is answered with HTTP 500, which raises.
|
|
4058
4090
|
|
|
4059
4091
|
Args:
|
|
4060
4092
|
monitor (Dict[str, Any]): The whole record as :meth:`get_monitors_for_set` returns
|
|
@@ -4076,8 +4108,9 @@ class LogicVeinAPIClient:
|
|
|
4076
4108
|
network (str): Network the device belongs to.
|
|
4077
4109
|
|
|
4078
4110
|
Returns:
|
|
4079
|
-
List[Dict[str, Any]]: The monitors, each with ``monitorId``, ``monitorName``
|
|
4080
|
-
``
|
|
4111
|
+
List[Dict[str, Any]]: The monitors, each with ``monitorId``, ``monitorName``, ``setId``
|
|
4112
|
+
and ``disabled``, which is the state on this device. The list is empty only when the
|
|
4113
|
+
server says the device has none,
|
|
4081
4114
|
which it also says for a device that does not exist.
|
|
4082
4115
|
|
|
4083
4116
|
Raises:
|
|
@@ -4090,6 +4123,68 @@ class LogicVeinAPIClient:
|
|
|
4090
4123
|
raise ValueError(f"Collection.getMonitorsForHost did not return a list for device {device_uuid}")
|
|
4091
4124
|
return monitors
|
|
4092
4125
|
|
|
4126
|
+
def disable_monitor_for_host(self, monitor_id: int, device_uuid: str, network: str) -> bool:
|
|
4127
|
+
"""Stop a deployed monitor on one device, leaving it deployed and other devices as they are.
|
|
4128
|
+
|
|
4129
|
+
Deploying the set again keeps the monitor disabled, and repeating this does no harm.
|
|
4130
|
+
|
|
4131
|
+
Args:
|
|
4132
|
+
monitor_id (int): ID of the monitor.
|
|
4133
|
+
device_uuid (str): UUID of the device.
|
|
4134
|
+
network (str): Network the device belongs to.
|
|
4135
|
+
|
|
4136
|
+
Returns:
|
|
4137
|
+
bool: True if the device now shows the monitor disabled. False if it does not, as for
|
|
4138
|
+
a monitor not deployed to that device.
|
|
4139
|
+
|
|
4140
|
+
Raises:
|
|
4141
|
+
ValueError: If the device's monitors cannot be read afterwards.
|
|
4142
|
+
"""
|
|
4143
|
+
return self._set_monitor_disabled_for_host("disableMonitorForHost", monitor_id, device_uuid, network, True)
|
|
4144
|
+
|
|
4145
|
+
def enable_monitor_for_host(self, monitor_id: int, device_uuid: str, network: str) -> bool:
|
|
4146
|
+
"""Start a monitor again on a device where it was disabled.
|
|
4147
|
+
|
|
4148
|
+
Repeating this does no harm.
|
|
4149
|
+
|
|
4150
|
+
Args:
|
|
4151
|
+
monitor_id (int): ID of the monitor.
|
|
4152
|
+
device_uuid (str): UUID of the device.
|
|
4153
|
+
network (str): Network the device belongs to.
|
|
4154
|
+
|
|
4155
|
+
Returns:
|
|
4156
|
+
bool: True if the device now shows the monitor enabled. False if it does not, as for
|
|
4157
|
+
a monitor not deployed to that device.
|
|
4158
|
+
|
|
4159
|
+
Raises:
|
|
4160
|
+
ValueError: If the device's monitors cannot be read afterwards.
|
|
4161
|
+
"""
|
|
4162
|
+
return self._set_monitor_disabled_for_host("enableMonitorForHost", monitor_id, device_uuid, network, False)
|
|
4163
|
+
|
|
4164
|
+
def _set_monitor_disabled_for_host(
|
|
4165
|
+
self, method: str, monitor_id: int, device_uuid: str, network: str, disabled: bool
|
|
4166
|
+
) -> bool:
|
|
4167
|
+
"""Call a per-device enable or disable, and read back whether it took effect.
|
|
4168
|
+
|
|
4169
|
+
The server answers null to these whatever happens, even for an unknown monitor or
|
|
4170
|
+
device or the wrong network, so only the device's own monitors can show the outcome.
|
|
4171
|
+
|
|
4172
|
+
Args:
|
|
4173
|
+
method (str): The method's name under Collection.
|
|
4174
|
+
monitor_id (int): ID of the monitor.
|
|
4175
|
+
device_uuid (str): UUID of the device.
|
|
4176
|
+
network (str): Network the device belongs to.
|
|
4177
|
+
disabled (bool): The state the call should leave.
|
|
4178
|
+
|
|
4179
|
+
Returns:
|
|
4180
|
+
bool: Whether the device shows the monitor in that state.
|
|
4181
|
+
"""
|
|
4182
|
+
self.call(f"Collection.{method}", monitorId=monitor_id, uuid=device_uuid, network=network)
|
|
4183
|
+
return any(
|
|
4184
|
+
monitor.get("monitorId") == monitor_id and monitor.get("disabled") is disabled
|
|
4185
|
+
for monitor in self.get_monitors_for_host(device_uuid, network)
|
|
4186
|
+
)
|
|
4187
|
+
|
|
4093
4188
|
def validate_monitor_sets_against_device(
|
|
4094
4189
|
self, monitor_set_ids: List[int], host_queries: List[str]
|
|
4095
4190
|
) -> Dict[str, Any]:
|
|
@@ -4528,27 +4623,32 @@ class LogicVeinAPIClient:
|
|
|
4528
4623
|
where ``recordId`` is the ``id`` field from a ``getExecutionDetails``
|
|
4529
4624
|
record.
|
|
4530
4625
|
|
|
4626
|
+
The server finds the output by ``recordId`` alone: a record of another
|
|
4627
|
+
execution comes back as long as the execution ID exists.
|
|
4628
|
+
|
|
4531
4629
|
Args:
|
|
4532
4630
|
execution_id: The execution ID (``executionId`` from the job result).
|
|
4533
4631
|
record_id: The per-device record ID (``id`` from ``getExecutionDetails``).
|
|
4534
4632
|
|
|
4535
4633
|
Returns:
|
|
4536
|
-
The raw output text
|
|
4634
|
+
The raw output text.
|
|
4635
|
+
|
|
4636
|
+
Raises:
|
|
4637
|
+
requests.HTTPError: If the server refuses the request, for example
|
|
4638
|
+
401 for a bad token, 404 for an unknown execution or 500 for a
|
|
4639
|
+
malformed ID.
|
|
4640
|
+
ValueError: If the server has no record with that ID. It answers
|
|
4641
|
+
200 with an empty body and no content type then, which is not
|
|
4642
|
+
an empty output: a Command Runner output holds at least the
|
|
4643
|
+
command and the prompt.
|
|
4537
4644
|
"""
|
|
4538
4645
|
url = f"https://{self.host}/servlet/pluginDetail"
|
|
4539
4646
|
params = {"executionId": execution_id, "recordId": record_id}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
logger.warning(
|
|
4546
|
-
"get_execution_output failed for executionId=%s recordId=%s: %s",
|
|
4547
|
-
execution_id,
|
|
4548
|
-
record_id,
|
|
4549
|
-
exc,
|
|
4550
|
-
)
|
|
4551
|
-
return ""
|
|
4647
|
+
response = self.lvi_client.get(url, params=params, timeout=self.timeout)
|
|
4648
|
+
response.raise_for_status()
|
|
4649
|
+
if not response.content and not response.headers.get("Content-Type"):
|
|
4650
|
+
raise ValueError(f"No output record {record_id} in execution {execution_id}")
|
|
4651
|
+
return response.text
|
|
4552
4652
|
|
|
4553
4653
|
# =====================================================
|
|
4554
4654
|
# ZeroTouch
|
|
@@ -5048,7 +5048,7 @@ class TestCreateCommandRunnerJob:
|
|
|
5048
5048
|
|
|
5049
5049
|
|
|
5050
5050
|
class TestGetExecutionOutput:
|
|
5051
|
-
"""Tests for get_execution_output
|
|
5051
|
+
"""Tests for get_execution_output: the text on success, and an error for every failure."""
|
|
5052
5052
|
|
|
5053
5053
|
@pytest.fixture
|
|
5054
5054
|
def client(self, monkeypatch):
|
|
@@ -5057,39 +5057,50 @@ class TestGetExecutionOutput:
|
|
|
5057
5057
|
with patch("src.logicvein.lvi_api_client.requests.Session"):
|
|
5058
5058
|
return LogicVeinAPIClient()
|
|
5059
5059
|
|
|
5060
|
+
@staticmethod
|
|
5061
|
+
def answer(status, body=b"", content_type=None):
|
|
5062
|
+
"""A real requests.Response, shaped like the lab server's answers (issue #6)."""
|
|
5063
|
+
response = requests.Response()
|
|
5064
|
+
response.status_code = status
|
|
5065
|
+
response._content = body
|
|
5066
|
+
response.encoding = "utf-8"
|
|
5067
|
+
if content_type:
|
|
5068
|
+
response.headers["Content-Type"] = content_type
|
|
5069
|
+
response.url = "https://test.example.com/servlet/pluginDetail"
|
|
5070
|
+
return response
|
|
5071
|
+
|
|
5060
5072
|
def test_get_execution_output_success(self, client):
|
|
5061
|
-
"""get_execution_output returns
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
mock_get_response.text = "show version output\nCisco IOS Version 15.1"
|
|
5065
|
-
client.lvi_client.get = Mock(return_value=mock_get_response)
|
|
5073
|
+
"""get_execution_output returns the text, and asks for the execution and record."""
|
|
5074
|
+
output = b"show clock\r\n16:57:34.919 UTC Sat Nov 7 2026\r\nc1800#"
|
|
5075
|
+
client.lvi_client.get = Mock(return_value=self.answer(200, output, "text/plain;charset=utf-8"))
|
|
5066
5076
|
|
|
5067
5077
|
result = client.get_execution_output(execution_id=42, record_id=7)
|
|
5068
5078
|
|
|
5069
|
-
assert result ==
|
|
5070
|
-
client.lvi_client.get.
|
|
5071
|
-
call_args = client.lvi_client.get.call_args
|
|
5072
|
-
assert "executionId" in call_args.kwargs["params"]
|
|
5073
|
-
assert call_args.kwargs["params"]["executionId"] == 42
|
|
5074
|
-
assert call_args.kwargs["params"]["recordId"] == 7
|
|
5079
|
+
assert result == output.decode()
|
|
5080
|
+
assert client.lvi_client.get.call_args.kwargs["params"] == {"executionId": 42, "recordId": 7}
|
|
5075
5081
|
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
client.lvi_client.get = Mock(return_value=
|
|
5082
|
+
@pytest.mark.parametrize("status", [401, 404, 500])
|
|
5083
|
+
def test_get_execution_output_http_error_raises(self, client, status):
|
|
5084
|
+
"""A refused read raises instead of reading as an empty output (issue #6)."""
|
|
5085
|
+
page = b"<!doctype html><html></html>"
|
|
5086
|
+
client.lvi_client.get = Mock(return_value=self.answer(status, page, "text/html;charset=utf-8"))
|
|
5081
5087
|
|
|
5082
|
-
|
|
5088
|
+
with pytest.raises(requests.HTTPError):
|
|
5089
|
+
client.get_execution_output(execution_id=42, record_id=7)
|
|
5083
5090
|
|
|
5084
|
-
|
|
5091
|
+
def test_get_execution_output_unknown_record_raises(self, client):
|
|
5092
|
+
"""The server answers an unknown record with 200, no body and no content type (issue #6)."""
|
|
5093
|
+
client.lvi_client.get = Mock(return_value=self.answer(200))
|
|
5085
5094
|
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
client.lvi_client.get = Mock(side_effect=requests.exceptions.ConnectionError("refused"))
|
|
5095
|
+
with pytest.raises(ValueError, match="No output record 7 in execution 42"):
|
|
5096
|
+
client.get_execution_output(execution_id=42, record_id=7)
|
|
5089
5097
|
|
|
5090
|
-
|
|
5098
|
+
def test_get_execution_output_connection_error_raises(self, client):
|
|
5099
|
+
"""A transport failure propagates instead of reading as an empty output (issue #6)."""
|
|
5100
|
+
client.lvi_client.get = Mock(side_effect=requests.exceptions.ConnectionError("refused"))
|
|
5091
5101
|
|
|
5092
|
-
|
|
5102
|
+
with pytest.raises(requests.exceptions.ConnectionError):
|
|
5103
|
+
client.get_execution_output(execution_id=99, record_id=1)
|
|
5093
5104
|
|
|
5094
5105
|
|
|
5095
5106
|
class TestRunCommandRunnerJob:
|
|
@@ -5121,6 +5132,27 @@ class TestRunCommandRunnerJob:
|
|
|
5121
5132
|
assert result["executionId"] == "exec-77"
|
|
5122
5133
|
client.lvi_client.post.assert_called_once()
|
|
5123
5134
|
|
|
5135
|
+
def test_run_command_runner_job_describes_the_job_by_its_name(self, client, mock_response):
|
|
5136
|
+
"""An empty description uses the job name, not "Job scheduled by ServiceNow" (issue #5)."""
|
|
5137
|
+
mock_response.json.return_value = {"jsonrpc": "2.0", "id": "t", "result": {"executionId": 1}}
|
|
5138
|
+
client.lvi_client.post = Mock(return_value=mock_response)
|
|
5139
|
+
|
|
5140
|
+
client.run_command_runner_job("10.1.1.1", "Default", "show version", job_name="NTP check")
|
|
5141
|
+
|
|
5142
|
+
body = json.loads(client.lvi_client.post.call_args.kwargs["data"])
|
|
5143
|
+
assert body["method"] == "Scheduler.runNow"
|
|
5144
|
+
assert body["params"]["jobData"]["description"] == "NTP check"
|
|
5145
|
+
|
|
5146
|
+
def test_run_command_runner_job_keeps_a_given_description(self, client, mock_response):
|
|
5147
|
+
"""A description the caller gives is sent as it is."""
|
|
5148
|
+
mock_response.json.return_value = {"jsonrpc": "2.0", "id": "t", "result": {"executionId": 1}}
|
|
5149
|
+
client.lvi_client.post = Mock(return_value=mock_response)
|
|
5150
|
+
|
|
5151
|
+
client.run_command_runner_job("10.1.1.1", "Default", "show version", description="Change 42")
|
|
5152
|
+
|
|
5153
|
+
body = json.loads(client.lvi_client.post.call_args.kwargs["data"])
|
|
5154
|
+
assert body["params"]["jobData"]["description"] == "Change 42"
|
|
5155
|
+
|
|
5124
5156
|
def test_run_command_runner_job_with_backup(self, client, mock_response):
|
|
5125
5157
|
"""run_command_runner_job passes backup_on_completion=True in job parameters."""
|
|
5126
5158
|
mock_response.json.return_value = {
|
|
@@ -5201,15 +5233,15 @@ class TestCreateCommandRunnerJobBackupAndDescription:
|
|
|
5201
5233
|
body = json.loads(client.lvi_client.post.call_args.kwargs["data"])
|
|
5202
5234
|
assert body["params"]["jobData"]["description"] == "My custom description"
|
|
5203
5235
|
|
|
5204
|
-
def
|
|
5205
|
-
"""create_command_runner_job
|
|
5236
|
+
def test_empty_description_defaults_to_the_job_name(self, client, mock_response):
|
|
5237
|
+
"""create_command_runner_job describes the job by its name, not by any one caller (issue #5)."""
|
|
5206
5238
|
mock_response.json.return_value = {"jsonrpc": "2.0", "id": "t", "result": {"jobId": 12}}
|
|
5207
5239
|
client.lvi_client.post = Mock(return_value=mock_response)
|
|
5208
5240
|
|
|
5209
|
-
client.create_command_runner_job("10.1.1.1", "Default", "show version", description="")
|
|
5241
|
+
client.create_command_runner_job("10.1.1.1", "Default", "show version", job_name="NTP check", description="")
|
|
5210
5242
|
|
|
5211
5243
|
body = json.loads(client.lvi_client.post.call_args.kwargs["data"])
|
|
5212
|
-
assert body["params"]["jobData"]["description"] == "
|
|
5244
|
+
assert body["params"]["jobData"]["description"] == "NTP check"
|
|
5213
5245
|
|
|
5214
5246
|
|
|
5215
5247
|
class TestRunJobNowSubTypeBranch:
|
|
@@ -543,3 +543,80 @@ class TestCreateRuleSet:
|
|
|
543
543
|
with pytest.raises(ValueError):
|
|
544
544
|
client.create_rule_set(name, adapter, document, **arguments)
|
|
545
545
|
assert server.requests == []
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class TestMonitorStateOnADevice:
|
|
549
|
+
"""On a real server these answer null whatever happens, so the device's monitors decide."""
|
|
550
|
+
|
|
551
|
+
@pytest.mark.parametrize(
|
|
552
|
+
"call, method, disabled",
|
|
553
|
+
[
|
|
554
|
+
("disable_monitor_for_host", "Collection.disableMonitorForHost", True),
|
|
555
|
+
("enable_monitor_for_host", "Collection.enableMonitorForHost", False),
|
|
556
|
+
],
|
|
557
|
+
)
|
|
558
|
+
def test_the_request_and_a_read_back_of_the_device(self, connect, call, method, disabled):
|
|
559
|
+
client, server = connect(
|
|
560
|
+
{method: None, "Collection.getMonitorsForHost": [{"monitorId": 7, "disabled": disabled}]}
|
|
561
|
+
)
|
|
562
|
+
assert getattr(client, call)(7, "u1", "n1") is True
|
|
563
|
+
assert server.requests == [
|
|
564
|
+
(method, {"monitorId": 7, "uuid": "u1", "network": "n1"}),
|
|
565
|
+
("Collection.getMonitorsForHost", {"uuid": "u1", "network": "n1"}),
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
@pytest.mark.parametrize(
|
|
569
|
+
"call, rows",
|
|
570
|
+
[
|
|
571
|
+
pytest.param("disable_monitor_for_host", [{"monitorId": 7, "disabled": False}], id="disable did not take"),
|
|
572
|
+
pytest.param("enable_monitor_for_host", [{"monitorId": 7, "disabled": True}], id="enable did not take"),
|
|
573
|
+
pytest.param("disable_monitor_for_host", [], id="not on the device"),
|
|
574
|
+
pytest.param("disable_monitor_for_host", [{"monitorId": 8, "disabled": True}], id="another monitor"),
|
|
575
|
+
pytest.param(
|
|
576
|
+
"enable_monitor_for_host", [{"monitorId": 8, "disabled": False}], id="another monitor, enable"
|
|
577
|
+
),
|
|
578
|
+
],
|
|
579
|
+
)
|
|
580
|
+
def test_a_null_answer_is_not_success(self, connect, call, rows):
|
|
581
|
+
client, _ = connect({"Collection.getMonitorsForHost": rows})
|
|
582
|
+
assert getattr(client, call)(7, "u1", "n1") is False
|
|
583
|
+
|
|
584
|
+
def test_a_failed_read_raises(self, connect):
|
|
585
|
+
client, _ = connect({"Collection.getMonitorsForHost": None})
|
|
586
|
+
with pytest.raises(ValueError):
|
|
587
|
+
client.disable_monitor_for_host(7, "u1", "n1")
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class TestDeployingASet:
|
|
591
|
+
"""On a real server several ipCsv queries match nothing, and no query matches every device."""
|
|
592
|
+
|
|
593
|
+
@pytest.mark.parametrize(
|
|
594
|
+
"queries",
|
|
595
|
+
[
|
|
596
|
+
pytest.param(["ipCsv=192.0.2.1@n1"], id="one device"),
|
|
597
|
+
pytest.param(["ipCsv=192.0.2.1@n1,192.0.2.2@n1"], id="several devices in one entry"),
|
|
598
|
+
pytest.param(["ipCsv=192.0.2.1@n1,192.0.2.2@n1", "ipAddress=192.0.2.1"], id="narrowed by another query"),
|
|
599
|
+
pytest.param(["sysObjectID=1.3.6.1.4.1.9.1.324", "sysDescr=*C2950*"], id="other queries together"),
|
|
600
|
+
],
|
|
601
|
+
)
|
|
602
|
+
def test_queries_the_server_can_act_on_are_sent_as_given(self, connect, queries):
|
|
603
|
+
client, server = connect({"Collection.associateMonitorSet": None})
|
|
604
|
+
assert client.associate_monitor_set(2, queries) is True
|
|
605
|
+
assert server.sent("Collection.associateMonitorSet") == {"setId": 2, "hostQueries": queries}
|
|
606
|
+
|
|
607
|
+
@pytest.mark.parametrize(
|
|
608
|
+
"queries",
|
|
609
|
+
[
|
|
610
|
+
pytest.param([], id="no queries"),
|
|
611
|
+
pytest.param([""], id="a blank query"),
|
|
612
|
+
pytest.param([" "], id="a query of spaces"),
|
|
613
|
+
pytest.param(["ipCsv=192.0.2.1@n1", ""], id="a blank among others"),
|
|
614
|
+
pytest.param(["ipCsv=192.0.2.1@n1", "ipCsv=192.0.2.2@n1"], id="one entry per device"),
|
|
615
|
+
pytest.param(["ipCsv=192.0.2.1@n1", " ipCsv=192.0.2.2@n1"], id="one entry per device, spaced"),
|
|
616
|
+
],
|
|
617
|
+
)
|
|
618
|
+
def test_queries_that_would_match_everything_or_nothing_are_never_sent(self, connect, queries):
|
|
619
|
+
client, server = connect({"Collection.associateMonitorSet": None})
|
|
620
|
+
with pytest.raises(ValueError):
|
|
621
|
+
client.associate_monitor_set(2, queries)
|
|
622
|
+
assert server.requests == []
|
lvi_api_client-0.2.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lvi_api_client-0.2.2 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|