lvi-api-client 0.2.6__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.6 → lvi_api_client-0.2.9}/PKG-INFO +1 -1
- lvi_api_client-0.2.9/VERSION +1 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/src/logicvein/lvi_api_client.py +167 -7
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/PKG-INFO +1 -1
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/SOURCES.txt +1 -0
- lvi_api_client-0.2.9/tests/test_check_version_bump.py +62 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/tests/test_lvi_api_client.py +10 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/tests/test_server_request_shapes.py +113 -0
- lvi_api_client-0.2.6/VERSION +0 -1
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/LICENSE +0 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/README.md +0 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/pyproject.toml +0 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/setup.cfg +0 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/src/logicvein/__init__.py +0 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/dependency_links.txt +0 -0
- {lvi_api_client-0.2.6 → lvi_api_client-0.2.9}/src/lvi_api_client.egg-info/requires.txt +0 -0
- {lvi_api_client-0.2.6 → 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()
|
|
@@ -1840,7 +1843,8 @@ class LogicVeinAPIClient:
|
|
|
1840
1843
|
"summary": str - Incident summary,
|
|
1841
1844
|
"severity": str - "INFO", "WARNING", "ERROR", or "CRITICAL",
|
|
1842
1845
|
"priority": str - "LOW", "MEDIUM", "HIGH", or "CRITICAL",
|
|
1843
|
-
"status": str - "OPEN", "
|
|
1846
|
+
"status": str - "OPEN", "RESOLVED" or "DEFERRED" seen on a server; whether
|
|
1847
|
+
"WORKING" or "CLOSED" exist has not been measured,
|
|
1844
1848
|
"resolution": str - "PENDING" or "RESOLVED",
|
|
1845
1849
|
"assignee": str - Assigned username (empty string if unassigned),
|
|
1846
1850
|
"labels": list[str] - Label tags,
|
|
@@ -1875,14 +1879,15 @@ class LogicVeinAPIClient:
|
|
|
1875
1879
|
# Incidents
|
|
1876
1880
|
# =====================================================
|
|
1877
1881
|
|
|
1878
|
-
def get_incident_by_id(self, incident_id: int) -> Dict[str, Any]:
|
|
1882
|
+
def get_incident_by_id(self, incident_id: int) -> Optional[Dict[str, Any]]:
|
|
1879
1883
|
"""Get an incident by its ID from the LogicVein server.
|
|
1880
1884
|
|
|
1881
1885
|
Args:
|
|
1882
1886
|
incident_id (int): The ID of the incident.
|
|
1883
1887
|
|
|
1884
1888
|
Returns:
|
|
1885
|
-
Dict[str, Any]: The incident
|
|
1889
|
+
Dict[str, Any]: The incident, or None for an unknown ID, which the server answers with
|
|
1890
|
+
a null result. An empty dictionary only when the server gives no result at all.
|
|
1886
1891
|
"""
|
|
1887
1892
|
response = self.call("Incidents.getIncidentById", incidentId=incident_id)
|
|
1888
1893
|
|
|
@@ -2549,6 +2554,8 @@ class LogicVeinAPIClient:
|
|
|
2549
2554
|
def delete_managed_network(self, network_name: str) -> bool:
|
|
2550
2555
|
"""Delete a managed network from the LogicVein server.
|
|
2551
2556
|
|
|
2557
|
+
An alert policy whose only network this is goes with it.
|
|
2558
|
+
|
|
2552
2559
|
Args:
|
|
2553
2560
|
network_name (str): The name of the managed network to delete.
|
|
2554
2561
|
|
|
@@ -3648,6 +3655,8 @@ class LogicVeinAPIClient:
|
|
|
3648
3655
|
"""Get actions configured for an alert policy.
|
|
3649
3656
|
|
|
3650
3657
|
Calls Collection.getActionsForPolicy. Returns the result list directly (not paginated).
|
|
3658
|
+
Settings come back as stored, secrets included: an exec action's ``password`` and a
|
|
3659
|
+
trap action's ``community`` are not masked, and the web UI sends them back unchanged.
|
|
3651
3660
|
Each action has:
|
|
3652
3661
|
{
|
|
3653
3662
|
"actionId": int - Action ID,
|
|
@@ -3677,6 +3686,10 @@ class LogicVeinAPIClient:
|
|
|
3677
3686
|
Returns the saved actions with assigned actionIds (new actions use -1 as input id,
|
|
3678
3687
|
which is replaced with the assigned ID in the response).
|
|
3679
3688
|
|
|
3689
|
+
The saved actions **replace** all of the policy's actions; they are not added to them.
|
|
3690
|
+
An empty list does nothing, so it cannot clear a policy's actions. ``username`` is
|
|
3691
|
+
filled in with the caller's when left out.
|
|
3692
|
+
|
|
3680
3693
|
Each action object should have:
|
|
3681
3694
|
{
|
|
3682
3695
|
"actionId": int - Use -1 for new actions,
|
|
@@ -4288,6 +4301,33 @@ class LogicVeinAPIClient:
|
|
|
4288
4301
|
Returns:
|
|
4289
4302
|
List[Dict[str, Any]]: All matching alert policies.
|
|
4290
4303
|
"""
|
|
4304
|
+
return self._search_alert_policies(managed_networks, queries, sort_column, descending, page_size, strict=False)
|
|
4305
|
+
|
|
4306
|
+
def _search_alert_policies(
|
|
4307
|
+
self,
|
|
4308
|
+
managed_networks: List[str],
|
|
4309
|
+
queries: List[str],
|
|
4310
|
+
sort_column: str,
|
|
4311
|
+
descending: bool,
|
|
4312
|
+
page_size: int,
|
|
4313
|
+
strict: bool,
|
|
4314
|
+
) -> List[Dict[str, Any]]:
|
|
4315
|
+
"""Read every page of alert policies.
|
|
4316
|
+
|
|
4317
|
+
Args:
|
|
4318
|
+
managed_networks (List[str]): Networks to search. Empty for all.
|
|
4319
|
+
queries (List[str]): Query filters.
|
|
4320
|
+
sort_column (str): Column to sort by.
|
|
4321
|
+
descending (bool): Sort descending if True.
|
|
4322
|
+
page_size (int): Records per API call.
|
|
4323
|
+
strict (bool): Raise on an answer with no policies list, rather than stopping there.
|
|
4324
|
+
|
|
4325
|
+
Returns:
|
|
4326
|
+
List[Dict[str, Any]]: All matching alert policies.
|
|
4327
|
+
|
|
4328
|
+
Raises:
|
|
4329
|
+
ValueError: If ``strict`` and an answer has no policies list.
|
|
4330
|
+
"""
|
|
4291
4331
|
all_policies: List[Dict[str, Any]] = []
|
|
4292
4332
|
offset = 0
|
|
4293
4333
|
|
|
@@ -4306,8 +4346,11 @@ class LogicVeinAPIClient:
|
|
|
4306
4346
|
}
|
|
4307
4347
|
response = self.call("Collection.searchAlertPolicies", **params)
|
|
4308
4348
|
|
|
4309
|
-
|
|
4310
|
-
|
|
4349
|
+
result = response.get("result") if isinstance(response, dict) else None
|
|
4350
|
+
if isinstance(result, dict) and isinstance(result.get("policies"), list):
|
|
4351
|
+
batch = result["policies"]
|
|
4352
|
+
elif strict:
|
|
4353
|
+
raise ValueError("Collection.searchAlertPolicies did not answer with a list of policies")
|
|
4311
4354
|
else:
|
|
4312
4355
|
break
|
|
4313
4356
|
|
|
@@ -4325,6 +4368,12 @@ class LogicVeinAPIClient:
|
|
|
4325
4368
|
|
|
4326
4369
|
Calls Collection.saveAlertPolicy. The response mirrors the input policy object.
|
|
4327
4370
|
Use policyId=-1 (or omit) to create a new policy; provide an existing policyId to update.
|
|
4371
|
+
The result carries the new policy's ID.
|
|
4372
|
+
|
|
4373
|
+
Saving an existing policy **removes all its actions**, although ``actionTypes`` still
|
|
4374
|
+
lists them: save them again with :meth:`save_alert_actions`, as the web UI does. Saving
|
|
4375
|
+
a name that is already in use makes a second policy of that name; check with
|
|
4376
|
+
:meth:`is_alert_policy_name_unique` first.
|
|
4328
4377
|
|
|
4329
4378
|
The policy object has:
|
|
4330
4379
|
{
|
|
@@ -4347,6 +4396,117 @@ class LogicVeinAPIClient:
|
|
|
4347
4396
|
return response["result"]
|
|
4348
4397
|
return None
|
|
4349
4398
|
|
|
4399
|
+
def delete_alert_policy(self, policy_id: int, reassign_policy_id: int = -1) -> bool:
|
|
4400
|
+
"""Delete an alert policy, with its actions, refusing one still in use.
|
|
4401
|
+
|
|
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.
|
|
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.
|
|
4412
|
+
|
|
4413
|
+
Args:
|
|
4414
|
+
policy_id (int): The ID of the policy to delete.
|
|
4415
|
+
reassign_policy_id (int): The policy to move its triggers to, required when the
|
|
4416
|
+
policy is in use; -1 for none.
|
|
4417
|
+
|
|
4418
|
+
Returns:
|
|
4419
|
+
bool: True if the policy is gone afterwards.
|
|
4420
|
+
|
|
4421
|
+
Raises:
|
|
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
|
+
)
|
|
4437
|
+
self.call("Collection.deleteAlertPolicy", policyId=policy_id, reassignPolicyId=reassign_policy_id)
|
|
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):
|
|
4453
|
+
raise ValueError("Collection.searchAlertPolicies answered with a policy that has no ID")
|
|
4454
|
+
return known
|
|
4455
|
+
|
|
4456
|
+
def is_alert_policy_referenced(self, policy_id: int) -> bool:
|
|
4457
|
+
"""Check whether anything refers to an alert policy.
|
|
4458
|
+
|
|
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.
|
|
4463
|
+
|
|
4464
|
+
Args:
|
|
4465
|
+
policy_id (int): The ID of the policy.
|
|
4466
|
+
|
|
4467
|
+
Returns:
|
|
4468
|
+
bool: True if the policy is referenced.
|
|
4469
|
+
|
|
4470
|
+
Raises:
|
|
4471
|
+
ValueError: If the server's answer is not true or false.
|
|
4472
|
+
"""
|
|
4473
|
+
response = self.call("Collection.isAlertPolicyReferenced", policyId=policy_id)
|
|
4474
|
+
referenced = response.get("result") if isinstance(response, dict) else None
|
|
4475
|
+
if not isinstance(referenced, bool):
|
|
4476
|
+
raise ValueError(f"Collection.isAlertPolicyReferenced did not answer true or false for {policy_id}")
|
|
4477
|
+
return referenced
|
|
4478
|
+
|
|
4479
|
+
def is_alert_policy_name_unique(
|
|
4480
|
+
self, policy_name: str, managed_networks: List[str], ignore_policy_id: int = -1
|
|
4481
|
+
) -> bool:
|
|
4482
|
+
"""Check whether an alert policy name is free in some networks.
|
|
4483
|
+
|
|
4484
|
+
Calls Collection.isAlertPolicyNameUnique, with the parameters the web UI sends as a
|
|
4485
|
+
name is typed. :meth:`save_alert_policy` does not check: it saves a second policy of
|
|
4486
|
+
a name in use.
|
|
4487
|
+
|
|
4488
|
+
Args:
|
|
4489
|
+
policy_name (str): The name to check.
|
|
4490
|
+
managed_networks (List[str]): The networks to check it in.
|
|
4491
|
+
ignore_policy_id (int): A policy to leave out, such as the one being renamed. -1 for none.
|
|
4492
|
+
|
|
4493
|
+
Returns:
|
|
4494
|
+
bool: True if the name is free.
|
|
4495
|
+
|
|
4496
|
+
Raises:
|
|
4497
|
+
ValueError: If the server's answer is not true or false.
|
|
4498
|
+
"""
|
|
4499
|
+
response = self.call(
|
|
4500
|
+
"Collection.isAlertPolicyNameUnique",
|
|
4501
|
+
policyName=policy_name,
|
|
4502
|
+
managedNetworks=managed_networks,
|
|
4503
|
+
ignorePolicyId=ignore_policy_id,
|
|
4504
|
+
)
|
|
4505
|
+
unique = response.get("result") if isinstance(response, dict) else None
|
|
4506
|
+
if not isinstance(unique, bool):
|
|
4507
|
+
raise ValueError(f"Collection.isAlertPolicyNameUnique did not answer true or false for {policy_name}")
|
|
4508
|
+
return unique
|
|
4509
|
+
|
|
4350
4510
|
# =====================================================
|
|
4351
4511
|
# Maps / Topology
|
|
4352
4512
|
# =====================================================
|
|
@@ -9,5 +9,6 @@ src/lvi_api_client.egg-info/SOURCES.txt
|
|
|
9
9
|
src/lvi_api_client.egg-info/dependency_links.txt
|
|
10
10
|
src/lvi_api_client.egg-info/requires.txt
|
|
11
11
|
src/lvi_api_client.egg-info/top_level.txt
|
|
12
|
+
tests/test_check_version_bump.py
|
|
12
13
|
tests/test_lvi_api_client.py
|
|
13
14
|
tests/test_server_request_shapes.py
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tests for scripts/ci/check_version_bump.py."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
_PATH = Path(__file__).resolve().parent.parent / "scripts" / "ci" / "check_version_bump.py"
|
|
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
|
|
11
|
+
check = importlib.util.module_from_spec(_SPEC)
|
|
12
|
+
_SPEC.loader.exec_module(check)
|
|
13
|
+
|
|
14
|
+
LOG = "# Changelog\n\n## [0.2.7] - 2026-11-08\n\n### Fixed\n- Something.\n\n## [0.2.6] - 2026-11-07\n"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_a_heading_for_the_version_passes():
|
|
18
|
+
assert check.changelog_problem(LOG, "0.2.7") == ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize(
|
|
22
|
+
"changelog",
|
|
23
|
+
[
|
|
24
|
+
"## [Unreleased]\n\n### Fixed\n- Something.\n\n## [0.2.6] - 2026-11-07\n",
|
|
25
|
+
"Fixed in [0.2.7], see below.\n## [0.2.6]\n",
|
|
26
|
+
"## [0.2.70] - 2026-11-08\n",
|
|
27
|
+
],
|
|
28
|
+
ids=["only unreleased", "not a heading", "another version"],
|
|
29
|
+
)
|
|
30
|
+
def test_no_heading_for_the_version_fails(changelog):
|
|
31
|
+
assert "no '## [0.2.7]' heading" in check.changelog_problem(changelog, "0.2.7")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run(monkeypatch, tmp_path, changed, version, changelog, base_version="0.2.6"):
|
|
35
|
+
(tmp_path / "VERSION").write_text(version + "\n")
|
|
36
|
+
(tmp_path / "CHANGELOG.md").write_text(changelog)
|
|
37
|
+
monkeypatch.setattr(check, "REPO_ROOT", tmp_path)
|
|
38
|
+
|
|
39
|
+
def git(*args):
|
|
40
|
+
return "\n".join(changed) if args[0] == "diff" else base_version + "\n"
|
|
41
|
+
|
|
42
|
+
monkeypatch.setattr(check, "_git", git)
|
|
43
|
+
monkeypatch.setattr("sys.argv", ["check_version_bump.py", "--base", "abc123"])
|
|
44
|
+
return check.main()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_a_bump_with_entries_left_under_unreleased_fails(monkeypatch, tmp_path, capsys):
|
|
48
|
+
changelog = "## [Unreleased]\n\n### Fixed\n- Something.\n"
|
|
49
|
+
assert _run(monkeypatch, tmp_path, ["src/x.py", "VERSION", "CHANGELOG.md"], "0.2.7", changelog) == 1
|
|
50
|
+
assert "no '## [0.2.7]' heading" in capsys.readouterr().out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_a_bump_without_a_src_change_still_needs_the_heading(monkeypatch, tmp_path):
|
|
54
|
+
assert _run(monkeypatch, tmp_path, ["VERSION"], "0.2.7", "## [0.2.6]\n") == 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_a_bump_with_its_heading_passes(monkeypatch, tmp_path):
|
|
58
|
+
assert _run(monkeypatch, tmp_path, ["src/x.py", "VERSION", "CHANGELOG.md"], "0.2.7", LOG) == 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_a_docs_change_needs_nothing(monkeypatch, tmp_path):
|
|
62
|
+
assert _run(monkeypatch, tmp_path, ["CHANGELOG.md", "README.md"], "0.2.6", "## [Unreleased]\n") == 0
|
|
@@ -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
|
|
@@ -620,3 +620,116 @@ class TestDeployingASet:
|
|
|
620
620
|
with pytest.raises(ValueError):
|
|
621
621
|
client.associate_monitor_set(2, queries)
|
|
622
622
|
assert server.requests == []
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class TestAlertPolicies:
|
|
626
|
+
"""Deleting alert policies and the checks the web UI makes around saving and deleting them."""
|
|
627
|
+
|
|
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):
|
|
633
|
+
client, server = connect(
|
|
634
|
+
{
|
|
635
|
+
"Collection.isAlertPolicyReferenced": False,
|
|
636
|
+
"Collection.deleteAlertPolicy": True,
|
|
637
|
+
"Collection.searchAlertPolicies": self.listing(1),
|
|
638
|
+
}
|
|
639
|
+
)
|
|
640
|
+
assert client.delete_alert_policy(65) is True
|
|
641
|
+
assert server.sent("Collection.isAlertPolicyReferenced") == {"policyId": 65}
|
|
642
|
+
assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": -1}
|
|
643
|
+
assert server.sent("Collection.searchAlertPolicies")["managedNetworks"] == []
|
|
644
|
+
|
|
645
|
+
def test_a_policy_still_listed_after_a_delete_is_not_deleted(self, connect):
|
|
646
|
+
client, _ = connect(
|
|
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,
|
|
665
|
+
"Collection.deleteAlertPolicy": True,
|
|
666
|
+
"Collection.searchAlertPolicies": self.listing(1, 65),
|
|
667
|
+
}
|
|
668
|
+
)
|
|
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]
|
|
687
|
+
|
|
688
|
+
@pytest.mark.parametrize(
|
|
689
|
+
"answer", [None, {}, {"policies": None}, {"policies": [{}]}, {"policies": [{"policyId": None}, "x"]}]
|
|
690
|
+
)
|
|
691
|
+
def test_a_failed_read_back_raises_rather_than_reading_as_deleted(self, connect, answer):
|
|
692
|
+
client, _ = connect(
|
|
693
|
+
{
|
|
694
|
+
"Collection.isAlertPolicyReferenced": False,
|
|
695
|
+
"Collection.deleteAlertPolicy": True,
|
|
696
|
+
"Collection.searchAlertPolicies": answer,
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
with pytest.raises(ValueError):
|
|
700
|
+
client.delete_alert_policy(65)
|
|
701
|
+
|
|
702
|
+
def test_a_failed_search_still_reads_as_no_policies(self, connect):
|
|
703
|
+
client, _ = connect({"Collection.searchAlertPolicies": None})
|
|
704
|
+
assert client.search_alert_policies() == []
|
|
705
|
+
|
|
706
|
+
@pytest.mark.parametrize("answer", [True, False])
|
|
707
|
+
def test_a_reference_check_sends_the_id_and_returns_the_answer(self, connect, answer):
|
|
708
|
+
client, server = connect({"Collection.isAlertPolicyReferenced": answer})
|
|
709
|
+
assert client.is_alert_policy_referenced(27) is answer
|
|
710
|
+
assert server.sent("Collection.isAlertPolicyReferenced") == {"policyId": 27}
|
|
711
|
+
|
|
712
|
+
def test_a_reference_check_with_no_answer_raises_rather_than_reading_as_unreferenced(self, connect):
|
|
713
|
+
client, _ = connect({"Collection.isAlertPolicyReferenced": None})
|
|
714
|
+
with pytest.raises(ValueError):
|
|
715
|
+
client.is_alert_policy_referenced(27)
|
|
716
|
+
|
|
717
|
+
@pytest.mark.parametrize("answer", [True, False])
|
|
718
|
+
def test_a_name_check_sends_what_the_ui_sends_and_returns_the_answer(self, connect, answer):
|
|
719
|
+
client, server = connect({"Collection.isAlertPolicyNameUnique": answer})
|
|
720
|
+
assert client.is_alert_policy_name_unique("p1", ["Default"], ignore_policy_id=1) is answer
|
|
721
|
+
assert server.sent("Collection.isAlertPolicyNameUnique") == {
|
|
722
|
+
"policyName": "p1",
|
|
723
|
+
"managedNetworks": ["Default"],
|
|
724
|
+
"ignorePolicyId": 1,
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
def test_a_name_check_ignores_no_policy_by_default(self, connect):
|
|
728
|
+
client, server = connect({"Collection.isAlertPolicyNameUnique": True})
|
|
729
|
+
client.is_alert_policy_name_unique("p1", ["Default"])
|
|
730
|
+
assert server.sent("Collection.isAlertPolicyNameUnique")["ignorePolicyId"] == -1
|
|
731
|
+
|
|
732
|
+
def test_a_name_check_with_no_answer_raises_rather_than_reading_as_taken(self, connect):
|
|
733
|
+
client, _ = connect({"Collection.isAlertPolicyNameUnique": None})
|
|
734
|
+
with pytest.raises(ValueError):
|
|
735
|
+
client.is_alert_policy_name_unique("p1", ["Default"])
|
lvi_api_client-0.2.6/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.6
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lvi_api_client-0.2.6 → 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
|