lvi-api-client 0.2.2__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1 @@
1
+ 0.2.4
@@ -3863,18 +3863,40 @@ class LogicVeinAPIClient:
3863
3863
  return False
3864
3864
 
3865
3865
  def associate_monitor_set(self, set_id: int, host_queries: List[str]) -> bool:
3866
- """Associate a monitor set with device(s).
3866
+ """Deploy a monitor set to the devices that match every one of the given queries.
3867
3867
 
3868
- Calls Collection.associateMonitorSet. Returns null on success.
3868
+ Calls Collection.associateMonitorSet. The server answers null whether or not any device
3869
+ matched, so check :meth:`get_monitors_for_host` to know what was deployed.
3870
+
3871
+ The server combines the queries with AND, so one entry for each device matches none of
3872
+ them. Name several devices in one entry, each with its network:
3873
+ ``['ipCsv=192.0.2.1@Default,192.0.2.2@Default']``. An address with no network of its own is
3874
+ not looked up in the network named after a later one. Another query can narrow it:
3875
+ ``['ipCsv=192.0.2.1@Default,192.0.2.2@Default', 'ipAddress=192.0.2.1']``.
3876
+
3877
+ With no queries the server deploys to every device, so that is refused here rather than
3878
+ passed on from an empty list.
3869
3879
 
3870
3880
  Args:
3871
3881
  set_id (int): The ID of the monitor set to associate.
3872
- host_queries (List[str]): List of host query strings in format 'ipCsv=IP@NetworkName'.
3873
- Example: ['ipCsv=192.168.1.6@Default']
3882
+ host_queries (List[str]): The queries, at least one, none of them blank, and at most
3883
+ one of them an ``ipCsv`` query.
3874
3884
 
3875
3885
  Returns:
3876
- bool: True if successful (result is null), otherwise False.
3877
- """
3886
+ bool: True if the server answered without an error, otherwise False. This does not
3887
+ mean any device matched.
3888
+
3889
+ Raises:
3890
+ ValueError: If there is no query, a query is blank, or more than one is ``ipCsv``.
3891
+ Nothing is sent.
3892
+ """
3893
+ if not host_queries or any(not query.strip() for query in host_queries):
3894
+ raise ValueError("host_queries needs at least one query and no blank ones: none would match every device")
3895
+ if sum(query.strip().startswith("ipCsv=") for query in host_queries) > 1:
3896
+ raise ValueError(
3897
+ "Queries combine with AND, so several ipCsv queries cannot name several devices. "
3898
+ "List them in one: ipCsv=IP@Network,IP@Network"
3899
+ )
3878
3900
  response = self.call("Collection.associateMonitorSet", setId=set_id, hostQueries=host_queries)
3879
3901
 
3880
3902
  if response and isinstance(response, dict) and "result" in response and response["result"] in (None, "None"):
@@ -3891,14 +3913,18 @@ class LogicVeinAPIClient:
3891
3913
  set, or with the ID of an existing monitor, and leaves the old one as it was. Use
3892
3914
  :meth:`save_monitor` to change one.
3893
3915
 
3916
+ The server answers success but stores neither ``disabled`` nor ``hostSpecific`` as true,
3917
+ here or in :meth:`save_monitor`. To stop a monitor on a device, use
3918
+ :meth:`disable_monitor_for_host`.
3919
+
3894
3920
  Each monitor object should have:
3895
3921
  {
3896
3922
  "monitorId": int - Use -1 for new monitors; assigned ID returned in response,
3897
3923
  "monitorName": str - Monitor name,
3898
3924
  "setId": int - Must match set_id,
3899
3925
  "monitorType": str - Type (e.g. "snmp", "tcp", "icmp", "telegraf", "web", "catchalltrap"),
3900
- "disabled": bool - Whether the monitor is disabled,
3901
- "hostSpecific": bool - Whether the monitor is host-specific,
3926
+ "disabled": bool - Stored as false whatever is sent,
3927
+ "hostSpecific": bool - Stored as false whatever is sent,
3902
3928
  "managedNetworks": list[str] - Networks the monitor belongs to,
3903
3929
  "retentionPolicy": str - Data retention (e.g. "THREE_MONTHS"),
3904
3930
  "json": dict - Monitor-type-specific config (OIDs, pollingPeriod, table, triggers, etc.),
@@ -3951,7 +3977,8 @@ class LogicVeinAPIClient:
3951
3977
  Args:
3952
3978
  set_name (str): The set's name. Use :meth:`is_monitor_set_name_unique` to check it first.
3953
3979
  managed_networks (List[str]): The networks the set belongs to, at least one.
3954
- smart_set (bool): Whether this is a smart set.
3980
+ smart_set (bool): Whether this is a smart set, one applied automatically to newly
3981
+ discovered devices.
3955
3982
 
3956
3983
  Returns:
3957
3984
  Dict[str, Any]: The saved set (``setId``, ``setName``, ``smartSet``,
@@ -4054,7 +4081,9 @@ class LogicVeinAPIClient:
4054
4081
  def save_monitor(self, monitor: Dict[str, Any]) -> Optional[Dict[str, Any]]:
4055
4082
  """Update a monitor in place.
4056
4083
 
4057
- The monitor keeps its ID, and a device it is deployed to gets the change.
4084
+ The monitor keeps its ID, and a device it is deployed to gets the change. A change to
4085
+ ``disabled`` or ``isHostSpecific`` is answered with success and not stored, and a changed
4086
+ ``monitorType`` is answered with HTTP 500, which raises.
4058
4087
 
4059
4088
  Args:
4060
4089
  monitor (Dict[str, Any]): The whole record as :meth:`get_monitors_for_set` returns
@@ -4076,8 +4105,9 @@ class LogicVeinAPIClient:
4076
4105
  network (str): Network the device belongs to.
4077
4106
 
4078
4107
  Returns:
4079
- List[Dict[str, Any]]: The monitors, each with ``monitorId``, ``monitorName`` and
4080
- ``setId``. The list is empty only when the server says the device has none,
4108
+ List[Dict[str, Any]]: The monitors, each with ``monitorId``, ``monitorName``, ``setId``
4109
+ and ``disabled``, which is the state on this device. The list is empty only when the
4110
+ server says the device has none,
4081
4111
  which it also says for a device that does not exist.
4082
4112
 
4083
4113
  Raises:
@@ -4090,6 +4120,68 @@ class LogicVeinAPIClient:
4090
4120
  raise ValueError(f"Collection.getMonitorsForHost did not return a list for device {device_uuid}")
4091
4121
  return monitors
4092
4122
 
4123
+ def disable_monitor_for_host(self, monitor_id: int, device_uuid: str, network: str) -> bool:
4124
+ """Stop a deployed monitor on one device, leaving it deployed and other devices as they are.
4125
+
4126
+ Deploying the set again keeps the monitor disabled, and repeating this does no harm.
4127
+
4128
+ Args:
4129
+ monitor_id (int): ID of the monitor.
4130
+ device_uuid (str): UUID of the device.
4131
+ network (str): Network the device belongs to.
4132
+
4133
+ Returns:
4134
+ bool: True if the device now shows the monitor disabled. False if it does not, as for
4135
+ a monitor not deployed to that device.
4136
+
4137
+ Raises:
4138
+ ValueError: If the device's monitors cannot be read afterwards.
4139
+ """
4140
+ return self._set_monitor_disabled_for_host("disableMonitorForHost", monitor_id, device_uuid, network, True)
4141
+
4142
+ def enable_monitor_for_host(self, monitor_id: int, device_uuid: str, network: str) -> bool:
4143
+ """Start a monitor again on a device where it was disabled.
4144
+
4145
+ Repeating this does no harm.
4146
+
4147
+ Args:
4148
+ monitor_id (int): ID of the monitor.
4149
+ device_uuid (str): UUID of the device.
4150
+ network (str): Network the device belongs to.
4151
+
4152
+ Returns:
4153
+ bool: True if the device now shows the monitor enabled. False if it does not, as for
4154
+ a monitor not deployed to that device.
4155
+
4156
+ Raises:
4157
+ ValueError: If the device's monitors cannot be read afterwards.
4158
+ """
4159
+ return self._set_monitor_disabled_for_host("enableMonitorForHost", monitor_id, device_uuid, network, False)
4160
+
4161
+ def _set_monitor_disabled_for_host(
4162
+ self, method: str, monitor_id: int, device_uuid: str, network: str, disabled: bool
4163
+ ) -> bool:
4164
+ """Call a per-device enable or disable, and read back whether it took effect.
4165
+
4166
+ The server answers null to these whatever happens, even for an unknown monitor or
4167
+ device or the wrong network, so only the device's own monitors can show the outcome.
4168
+
4169
+ Args:
4170
+ method (str): The method's name under Collection.
4171
+ monitor_id (int): ID of the monitor.
4172
+ device_uuid (str): UUID of the device.
4173
+ network (str): Network the device belongs to.
4174
+ disabled (bool): The state the call should leave.
4175
+
4176
+ Returns:
4177
+ bool: Whether the device shows the monitor in that state.
4178
+ """
4179
+ self.call(f"Collection.{method}", monitorId=monitor_id, uuid=device_uuid, network=network)
4180
+ return any(
4181
+ monitor.get("monitorId") == monitor_id and monitor.get("disabled") is disabled
4182
+ for monitor in self.get_monitors_for_host(device_uuid, network)
4183
+ )
4184
+
4093
4185
  def validate_monitor_sets_against_device(
4094
4186
  self, monitor_set_ids: List[int], host_queries: List[str]
4095
4187
  ) -> Dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -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 == []
@@ -1 +0,0 @@
1
- 0.2.2
File without changes
File without changes
File without changes