lvi-api-client 0.2.4__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.4 → lvi_api_client-0.2.6}/PKG-INFO +1 -1
- lvi_api_client-0.2.6/VERSION +1 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/logicvein/lvi_api_client.py +23 -15
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/PKG-INFO +1 -1
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/tests/test_lvi_api_client.py +60 -28
- lvi_api_client-0.2.4/VERSION +0 -1
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/LICENSE +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/README.md +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/pyproject.toml +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/setup.cfg +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/logicvein/__init__.py +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/SOURCES.txt +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/dependency_links.txt +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/requires.txt +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/src/lvi_api_client.egg-info/top_level.txt +0 -0
- {lvi_api_client-0.2.4 → lvi_api_client-0.2.6}/tests/test_server_request_shapes.py +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
|
# =====================================================
|
|
@@ -4620,27 +4623,32 @@ class LogicVeinAPIClient:
|
|
|
4620
4623
|
where ``recordId`` is the ``id`` field from a ``getExecutionDetails``
|
|
4621
4624
|
record.
|
|
4622
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
|
+
|
|
4623
4629
|
Args:
|
|
4624
4630
|
execution_id: The execution ID (``executionId`` from the job result).
|
|
4625
4631
|
record_id: The per-device record ID (``id`` from ``getExecutionDetails``).
|
|
4626
4632
|
|
|
4627
4633
|
Returns:
|
|
4628
|
-
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.
|
|
4629
4644
|
"""
|
|
4630
4645
|
url = f"https://{self.host}/servlet/pluginDetail"
|
|
4631
4646
|
params = {"executionId": execution_id, "recordId": record_id}
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
logger.warning(
|
|
4638
|
-
"get_execution_output failed for executionId=%s recordId=%s: %s",
|
|
4639
|
-
execution_id,
|
|
4640
|
-
record_id,
|
|
4641
|
-
exc,
|
|
4642
|
-
)
|
|
4643
|
-
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
|
|
4644
4652
|
|
|
4645
4653
|
# =====================================================
|
|
4646
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:
|
lvi_api_client-0.2.4/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lvi_api_client-0.2.4 → 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
|
|
File without changes
|