lvi-api-client 0.2.7__tar.gz → 0.2.9__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.7 → lvi_api_client-0.2.9}/PKG-INFO +1 -1
- lvi_api_client-0.2.9/VERSION +1 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/logicvein/lvi_api_client.py +52 -21
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/PKG-INFO +1 -1
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/tests/test_check_version_bump.py +1 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/tests/test_lvi_api_client.py +10 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/tests/test_server_request_shapes.py +52 -6
- lvi_api_client-0.2.7/VERSION +0 -1
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/LICENSE +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/README.md +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/pyproject.toml +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/setup.cfg +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/logicvein/__init__.py +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/SOURCES.txt +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/dependency_links.txt +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/requires.txt +0 -0
- {lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.9
|
|
@@ -3,7 +3,7 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
-
from typing import Any, Dict, List,
|
|
6
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
7
7
|
from xml.etree import ElementTree
|
|
8
8
|
from xml.sax.saxutils import escape
|
|
9
9
|
|
|
@@ -211,7 +211,10 @@ class LogicVeinAPIClient:
|
|
|
211
211
|
f"LogicVeinAPIClient initialized: host={self.host}, username={self.username}, using_token={bool(self.api_token)}, ignore_env={ignore_env}"
|
|
212
212
|
)
|
|
213
213
|
self.base_url = f"https://{self.host}/rest"
|
|
214
|
-
|
|
214
|
+
# The session holds this same mapping, so its headers are exactly these (requests' defaults are not sent).
|
|
215
|
+
self.headers: requests.structures.CaseInsensitiveDict[str] = requests.structures.CaseInsensitiveDict(
|
|
216
|
+
{"Content-Type": "application/json"}
|
|
217
|
+
)
|
|
215
218
|
if self.api_token:
|
|
216
219
|
self.headers["Authorization"] = f"Bearer {self.api_token}"
|
|
217
220
|
self.lvi_client = requests.Session()
|
|
@@ -4394,41 +4397,69 @@ class LogicVeinAPIClient:
|
|
|
4394
4397
|
return None
|
|
4395
4398
|
|
|
4396
4399
|
def delete_alert_policy(self, policy_id: int, reassign_policy_id: int = -1) -> bool:
|
|
4397
|
-
"""Delete an alert policy, with its actions.
|
|
4400
|
+
"""Delete an alert policy, with its actions, refusing one still in use.
|
|
4398
4401
|
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4402
|
+
A policy that alert triggers still use (:meth:`is_alert_policy_referenced`) may not be
|
|
4403
|
+
deleted on its own: as in the web UI, which asks for an alternate policy to take those
|
|
4404
|
+
triggers, ``reassign_policy_id`` must name another existing policy. Otherwise this
|
|
4405
|
+
raises before deleting anything.
|
|
4403
4406
|
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
+
Calls Collection.deleteAlertPolicy with ``policyId`` and ``reassignPolicyId``, as the UI
|
|
4408
|
+
does; the server refuses ``policyId`` alone. It answers true when it deletes the policy
|
|
4409
|
+
and false for an unknown ID. The policies are then read back, and the result is whether
|
|
4410
|
+
this one is gone: an answer that cannot show it raises rather than reading as deleted.
|
|
4411
|
+
Errors from the calls themselves (``LogicVeinAPIError``, ``requests`` errors) propagate.
|
|
4407
4412
|
|
|
4408
4413
|
Args:
|
|
4409
4414
|
policy_id (int): The ID of the policy to delete.
|
|
4410
|
-
reassign_policy_id (int): The policy to move its
|
|
4415
|
+
reassign_policy_id (int): The policy to move its triggers to, required when the
|
|
4416
|
+
policy is in use; -1 for none.
|
|
4411
4417
|
|
|
4412
4418
|
Returns:
|
|
4413
4419
|
bool: True if the policy is gone afterwards.
|
|
4414
4420
|
|
|
4415
4421
|
Raises:
|
|
4416
|
-
ValueError: If the
|
|
4417
|
-
|
|
4422
|
+
ValueError: If the policy is in use and no other existing policy is given to take
|
|
4423
|
+
its triggers, if the policies cannot be read (an answer with no list of policies,
|
|
4424
|
+
or a policy without an ID), or if the reference check does not answer true or false.
|
|
4425
|
+
"""
|
|
4426
|
+
if reassign_policy_id == policy_id:
|
|
4427
|
+
raise ValueError(f"Alert policy {policy_id} cannot take its own triggers; name another policy")
|
|
4428
|
+
in_use = self.is_alert_policy_referenced(policy_id)
|
|
4429
|
+
if reassign_policy_id != -1:
|
|
4430
|
+
if reassign_policy_id not in self._alert_policy_ids():
|
|
4431
|
+
raise ValueError(f"Alert policy {reassign_policy_id}, to take the triggers, does not exist")
|
|
4432
|
+
elif in_use:
|
|
4433
|
+
raise ValueError(
|
|
4434
|
+
f"Alert policy {policy_id} is used by alert triggers, so it may not be deleted until another "
|
|
4435
|
+
"policy takes them: pass reassign_policy_id"
|
|
4436
|
+
)
|
|
4418
4437
|
self.call("Collection.deleteAlertPolicy", policyId=policy_id, reassignPolicyId=reassign_policy_id)
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4438
|
+
return policy_id not in self._alert_policy_ids()
|
|
4439
|
+
|
|
4440
|
+
def _alert_policy_ids(self) -> List[int]:
|
|
4441
|
+
"""Read the IDs of every alert policy, failing on an answer that cannot list them.
|
|
4442
|
+
|
|
4443
|
+
Returns:
|
|
4444
|
+
List[int]: The IDs.
|
|
4445
|
+
|
|
4446
|
+
Raises:
|
|
4447
|
+
ValueError: If the answer has no list of policies, or a policy without an ID.
|
|
4448
|
+
"""
|
|
4449
|
+
policies = self._search_alert_policies([], [], "policyName", False, 500, strict=True)
|
|
4450
|
+
ids = [policy.get("policyId") if isinstance(policy, dict) else None for policy in policies]
|
|
4451
|
+
known = [found for found in ids if isinstance(found, int)]
|
|
4452
|
+
if len(known) != len(ids):
|
|
4422
4453
|
raise ValueError("Collection.searchAlertPolicies answered with a policy that has no ID")
|
|
4423
|
-
return
|
|
4454
|
+
return known
|
|
4424
4455
|
|
|
4425
4456
|
def is_alert_policy_referenced(self, policy_id: int) -> bool:
|
|
4426
4457
|
"""Check whether anything refers to an alert policy.
|
|
4427
4458
|
|
|
4428
|
-
Calls Collection.isAlertPolicyReferenced, which the web UI calls before deleting one
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
unknown ID answers false.
|
|
4459
|
+
Calls Collection.isAlertPolicyReferenced, which the web UI calls before deleting one: a
|
|
4460
|
+
policy in use may only be deleted with another policy to take its alert triggers (see
|
|
4461
|
+
:meth:`delete_alert_policy`). On the lab, a policy no monitor trigger names also
|
|
4462
|
+
answered true, so more than monitor triggers count. An unknown ID answers false.
|
|
4432
4463
|
|
|
4433
4464
|
Args:
|
|
4434
4465
|
policy_id (int): The ID of the policy.
|
|
@@ -7,6 +7,7 @@ import pytest
|
|
|
7
7
|
|
|
8
8
|
_PATH = Path(__file__).resolve().parent.parent / "scripts" / "ci" / "check_version_bump.py"
|
|
9
9
|
_SPEC = importlib.util.spec_from_file_location("check_version_bump", _PATH)
|
|
10
|
+
assert _SPEC is not None and _SPEC.loader is not None, _PATH
|
|
10
11
|
check = importlib.util.module_from_spec(_SPEC)
|
|
11
12
|
_SPEC.loader.exec_module(check)
|
|
12
13
|
|
|
@@ -48,6 +48,16 @@ class TestLogicVeinAPIClientUnit:
|
|
|
48
48
|
assert "Authorization" in client.headers
|
|
49
49
|
assert client.headers["Authorization"] == "Bearer test-token-123"
|
|
50
50
|
|
|
51
|
+
def test_the_session_sends_only_the_clients_headers(self, mock_env):
|
|
52
|
+
"""The session holds the client's own header mapping, with no requests defaults added."""
|
|
53
|
+
client = LogicVeinAPIClient()
|
|
54
|
+
assert client.lvi_client.headers is client.headers
|
|
55
|
+
assert dict(client.lvi_client.headers) == {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Authorization": "Bearer test-token-123",
|
|
58
|
+
}
|
|
59
|
+
assert client.lvi_client.headers["content-type"] == "application/json"
|
|
60
|
+
|
|
51
61
|
def test_initialization_with_kwargs(self, monkeypatch):
|
|
52
62
|
"""Test client initialization with kwargs instead of env vars."""
|
|
53
63
|
# Clear env vars so kwargs take precedence
|
|
@@ -625,31 +625,77 @@ class TestDeployingASet:
|
|
|
625
625
|
class TestAlertPolicies:
|
|
626
626
|
"""Deleting alert policies and the checks the web UI makes around saving and deleting them."""
|
|
627
627
|
|
|
628
|
-
|
|
628
|
+
@staticmethod
|
|
629
|
+
def listing(*ids):
|
|
630
|
+
return {"policies": [{"policyId": i, "policyName": f"p{i}"} for i in ids]}
|
|
631
|
+
|
|
632
|
+
def test_a_delete_of_a_policy_not_in_use_sends_what_the_ui_sends_and_reads_the_policies_back(self, connect):
|
|
629
633
|
client, server = connect(
|
|
630
634
|
{
|
|
631
|
-
"Collection.
|
|
632
|
-
"Collection.
|
|
635
|
+
"Collection.isAlertPolicyReferenced": False,
|
|
636
|
+
"Collection.deleteAlertPolicy": True,
|
|
637
|
+
"Collection.searchAlertPolicies": self.listing(1),
|
|
633
638
|
}
|
|
634
639
|
)
|
|
635
640
|
assert client.delete_alert_policy(65) is True
|
|
641
|
+
assert server.sent("Collection.isAlertPolicyReferenced") == {"policyId": 65}
|
|
636
642
|
assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": -1}
|
|
637
643
|
assert server.sent("Collection.searchAlertPolicies")["managedNetworks"] == []
|
|
638
644
|
|
|
639
645
|
def test_a_policy_still_listed_after_a_delete_is_not_deleted(self, connect):
|
|
640
646
|
client, _ = connect(
|
|
641
647
|
{
|
|
648
|
+
"Collection.isAlertPolicyReferenced": False,
|
|
649
|
+
"Collection.deleteAlertPolicy": False,
|
|
650
|
+
"Collection.searchAlertPolicies": self.listing(65),
|
|
651
|
+
}
|
|
652
|
+
)
|
|
653
|
+
assert client.delete_alert_policy(65) is False
|
|
654
|
+
|
|
655
|
+
def test_a_policy_in_use_is_not_deleted_without_a_policy_to_take_its_triggers(self, connect):
|
|
656
|
+
client, server = connect({"Collection.isAlertPolicyReferenced": True})
|
|
657
|
+
with pytest.raises(ValueError, match="used by alert triggers"):
|
|
658
|
+
client.delete_alert_policy(65)
|
|
659
|
+
assert [method for method, _ in server.requests] == ["Collection.isAlertPolicyReferenced"]
|
|
660
|
+
|
|
661
|
+
def test_a_policy_in_use_is_deleted_with_another_to_take_its_triggers(self, connect):
|
|
662
|
+
client, server = connect(
|
|
663
|
+
{
|
|
664
|
+
"Collection.isAlertPolicyReferenced": True,
|
|
642
665
|
"Collection.deleteAlertPolicy": True,
|
|
643
|
-
"Collection.searchAlertPolicies":
|
|
666
|
+
"Collection.searchAlertPolicies": self.listing(1, 65),
|
|
644
667
|
}
|
|
645
668
|
)
|
|
646
|
-
|
|
669
|
+
# The read-back still lists 65 because this fake does not delete; what matters is the request.
|
|
670
|
+
client.delete_alert_policy(65, reassign_policy_id=1)
|
|
671
|
+
assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": 1}
|
|
672
|
+
|
|
673
|
+
@pytest.mark.parametrize("reassign", [65, 2])
|
|
674
|
+
def test_a_policy_to_take_the_triggers_must_be_another_that_exists(self, connect, reassign):
|
|
675
|
+
client, server = connect(
|
|
676
|
+
{"Collection.isAlertPolicyReferenced": True, "Collection.searchAlertPolicies": self.listing(1, 65)}
|
|
677
|
+
)
|
|
678
|
+
with pytest.raises(ValueError):
|
|
679
|
+
client.delete_alert_policy(65, reassign_policy_id=reassign)
|
|
680
|
+
assert "Collection.deleteAlertPolicy" not in [method for method, _ in server.requests]
|
|
681
|
+
|
|
682
|
+
def test_a_reference_check_with_no_answer_deletes_nothing(self, connect):
|
|
683
|
+
client, server = connect({"Collection.isAlertPolicyReferenced": None})
|
|
684
|
+
with pytest.raises(ValueError):
|
|
685
|
+
client.delete_alert_policy(65)
|
|
686
|
+
assert "Collection.deleteAlertPolicy" not in [method for method, _ in server.requests]
|
|
647
687
|
|
|
648
688
|
@pytest.mark.parametrize(
|
|
649
689
|
"answer", [None, {}, {"policies": None}, {"policies": [{}]}, {"policies": [{"policyId": None}, "x"]}]
|
|
650
690
|
)
|
|
651
691
|
def test_a_failed_read_back_raises_rather_than_reading_as_deleted(self, connect, answer):
|
|
652
|
-
client, _ = connect(
|
|
692
|
+
client, _ = connect(
|
|
693
|
+
{
|
|
694
|
+
"Collection.isAlertPolicyReferenced": False,
|
|
695
|
+
"Collection.deleteAlertPolicy": True,
|
|
696
|
+
"Collection.searchAlertPolicies": answer,
|
|
697
|
+
}
|
|
698
|
+
)
|
|
653
699
|
with pytest.raises(ValueError):
|
|
654
700
|
client.delete_alert_policy(65)
|
|
655
701
|
|
lvi_api_client-0.2.7/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.7
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lvi_api_client-0.2.7 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|