besapi 4.2.0__tar.gz → 4.3.1__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.
- {besapi-4.2.0/src/besapi.egg-info → besapi-4.3.1}/PKG-INFO +3 -1
- {besapi-4.2.0 → besapi-4.3.1}/setup.cfg +15 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/besapi.py +226 -83
- besapi-4.3.1/src/besapi/plugin_utilities.py +593 -0
- {besapi-4.2.0 → besapi-4.3.1/src/besapi.egg-info}/PKG-INFO +3 -1
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi.egg-info/SOURCES.txt +8 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi.egg-info/requires.txt +3 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/bescli/bescli.py +5 -5
- besapi-4.3.1/tests/test_besapi_coverage.py +748 -0
- besapi-4.3.1/tests/test_besapi_sdk.py +776 -0
- besapi-4.3.1/tests/test_bescli_coverage.py +448 -0
- besapi-4.3.1/tests/test_example_mqtt_homeassistant.py +250 -0
- besapi-4.3.1/tests/test_live_sample_plugin.py +83 -0
- besapi-4.3.1/tests/test_plugin_utilities_coverage.py +193 -0
- besapi-4.3.1/tests/test_plugin_utilities_sdk.py +538 -0
- besapi-4.3.1/tests/test_sample_plugin.py +161 -0
- besapi-4.2.0/src/besapi/plugin_utilities.py +0 -349
- {besapi-4.2.0 → besapi-4.3.1}/LICENSE.txt +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/MANIFEST.in +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/README.md +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/pyproject.toml +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/setup.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/__init__.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/__main__.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/plugin_utilities_linux.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/plugin_utilities_win.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/schemas/BES.xsd +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/schemas/BESAPI.xsd +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi/schemas/BESActionSettings.xsd +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi.egg-info/dependency_links.txt +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/besapi.egg-info/top_level.txt +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/bescli/__init__.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/src/bescli/__main__.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/tests/test_besapi.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/tests/test_plugin_utilities.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/tests/test_plugin_utilities_linux.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/tests/test_plugin_utilities_logging.py +0 -0
- {besapi-4.2.0 → besapi-4.3.1}/tests/tests.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: besapi
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Library for working with the BigFix REST API
|
|
5
5
|
Home-page: https://github.com/jgstew/besapi
|
|
6
6
|
Author: Matt Hansen, James Stewart
|
|
@@ -26,6 +26,8 @@ Requires-Dist: requests
|
|
|
26
26
|
Requires-Dist: setuptools
|
|
27
27
|
Requires-Dist: pywin32; platform_system == "Windows"
|
|
28
28
|
Requires-Dist: urllib3>=2.2.3
|
|
29
|
+
Provides-Extra: plugins
|
|
30
|
+
Requires-Dist: ruamel.yaml; extra == "plugins"
|
|
29
31
|
Dynamic: license-file
|
|
30
32
|
|
|
31
33
|
# besapi
|
|
@@ -36,12 +36,27 @@ install_requires =
|
|
|
36
36
|
pywin32; platform_system == "Windows"
|
|
37
37
|
urllib3 >= 2.2.3
|
|
38
38
|
|
|
39
|
+
[options.extras_require]
|
|
40
|
+
plugins =
|
|
41
|
+
ruamel.yaml
|
|
42
|
+
|
|
39
43
|
[options.package_data]
|
|
40
44
|
besapi = schemas/*.xsd
|
|
41
45
|
|
|
42
46
|
[options.packages.find]
|
|
43
47
|
where = src
|
|
44
48
|
|
|
49
|
+
[coverage:run]
|
|
50
|
+
source =
|
|
51
|
+
src/besapi
|
|
52
|
+
src/bescli
|
|
53
|
+
branch = True
|
|
54
|
+
|
|
55
|
+
[coverage:report]
|
|
56
|
+
show_missing = True
|
|
57
|
+
skip_empty = True
|
|
58
|
+
fail_under = 75
|
|
59
|
+
|
|
45
60
|
[egg_info]
|
|
46
61
|
tag_build =
|
|
47
62
|
tag_date = 0
|
|
@@ -22,17 +22,23 @@ import random
|
|
|
22
22
|
import site
|
|
23
23
|
import string
|
|
24
24
|
import sys
|
|
25
|
+
import threading
|
|
25
26
|
import urllib.parse
|
|
27
|
+
from xml.sax.saxutils import escape as xml_escape
|
|
26
28
|
|
|
27
29
|
import lxml.etree
|
|
28
30
|
import lxml.objectify
|
|
29
31
|
import requests
|
|
30
32
|
import urllib3.poolmanager
|
|
31
33
|
|
|
32
|
-
__version__ = "4.
|
|
34
|
+
__version__ = "4.3.1"
|
|
33
35
|
|
|
34
36
|
besapi_logger = logging.getLogger("besapi")
|
|
35
37
|
|
|
38
|
+
# default requests timeout for each REST API request: (connect, read) seconds
|
|
39
|
+
# NOTE: long read timeout since some REST API calls, like exports, are slow
|
|
40
|
+
DEFAULT_TIMEOUT = (90, 600)
|
|
41
|
+
|
|
36
42
|
# pylint: disable=consider-using-f-string
|
|
37
43
|
|
|
38
44
|
|
|
@@ -65,6 +71,41 @@ def sanitize_txt(*args):
|
|
|
65
71
|
return tuple(sani_args)
|
|
66
72
|
|
|
67
73
|
|
|
74
|
+
def relevance_string_escape(value: str) -> str:
|
|
75
|
+
"""Escape text to embed inside a relevance "string literal".
|
|
76
|
+
|
|
77
|
+
Relevance string literals use `%XX` escapes, so `%` itself must be
|
|
78
|
+
escaped, as must `"` which would end the literal. Control characters
|
|
79
|
+
(tab, newline, ...) are escaped so the relevance stays on one line.
|
|
80
|
+
|
|
81
|
+
NOTE: `%XX` escapes are single bytes, so non-ASCII text is passed through
|
|
82
|
+
unchanged. Relevance only accepts characters in the FXF character set,
|
|
83
|
+
such as `é`; others, such as `✓`, are rejected by the evaluator however
|
|
84
|
+
they are written.
|
|
85
|
+
|
|
86
|
+
Verified with the BigFix client QnA, for example:
|
|
87
|
+
length of "100%25" = 4, "a%22b" = a"b
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
f'bes sites whose(name of it = "{relevance_string_escape(site_name)}")'
|
|
91
|
+
"""
|
|
92
|
+
escaped = []
|
|
93
|
+
for char in value:
|
|
94
|
+
if char in ("%", '"') or ord(char) < 0x20 or ord(char) == 0x7F:
|
|
95
|
+
escaped.append(f"%{ord(char):02X}")
|
|
96
|
+
else:
|
|
97
|
+
escaped.append(char)
|
|
98
|
+
return "".join(escaped)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cdata_escape(text: str) -> str:
|
|
102
|
+
"""Make text safe to place inside `<![CDATA[ ... ]]>`.
|
|
103
|
+
|
|
104
|
+
A literal `]]>` would end the CDATA section, so it is split across two.
|
|
105
|
+
"""
|
|
106
|
+
return str(text).replace("]]>", "]]]]><![CDATA[>")
|
|
107
|
+
|
|
108
|
+
|
|
68
109
|
def elem2dict(node):
|
|
69
110
|
"""
|
|
70
111
|
Convert an lxml.etree node tree into a dict.
|
|
@@ -182,7 +223,7 @@ def get_target_xml(targets=None):
|
|
|
182
223
|
# return "<AllComputers>false</AllComputers>"
|
|
183
224
|
return "<AllComputers>true</AllComputers>"
|
|
184
225
|
# treat as custom relevance:
|
|
185
|
-
return f"<CustomRelevance><![CDATA[{targets}]]></CustomRelevance>"
|
|
226
|
+
return f"<CustomRelevance><![CDATA[{cdata_escape(targets)}]]></CustomRelevance>"
|
|
186
227
|
|
|
187
228
|
# if targets is array:
|
|
188
229
|
if isinstance(targets, list):
|
|
@@ -198,7 +239,7 @@ def get_target_xml(targets=None):
|
|
|
198
239
|
# array of computer names
|
|
199
240
|
return (
|
|
200
241
|
"<ComputerName>"
|
|
201
|
-
+ "</ComputerName><ComputerName>".join(targets)
|
|
242
|
+
+ "</ComputerName><ComputerName>".join(map(xml_escape, targets))
|
|
202
243
|
+ "</ComputerName>"
|
|
203
244
|
)
|
|
204
245
|
|
|
@@ -208,30 +249,46 @@ def get_target_xml(targets=None):
|
|
|
208
249
|
return "<CustomRelevance>False</CustomRelevance>"
|
|
209
250
|
|
|
210
251
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
xmldoc = lxml.etree.fromstring(doc)
|
|
215
|
-
except BaseException: # pylint: disable=broad-except
|
|
216
|
-
return False
|
|
252
|
+
# compiled XSD schemas, cached per thread:
|
|
253
|
+
_xsd_schemas_cache = threading.local()
|
|
254
|
+
|
|
217
255
|
|
|
256
|
+
def _get_xsd_schemas():
|
|
257
|
+
"""Load and compile the BES XML schemas, once per thread.
|
|
258
|
+
|
|
259
|
+
NOTE: compiling them takes a few ms, and every REST API result is validated.
|
|
260
|
+
Cached per thread since sharing lxml schema objects across threads is not
|
|
261
|
+
guaranteed to be safe, and besapi is used from threads.
|
|
262
|
+
"""
|
|
263
|
+
cached = getattr(_xsd_schemas_cache, "schemas", None)
|
|
264
|
+
if cached is not None:
|
|
265
|
+
return cached
|
|
266
|
+
|
|
267
|
+
schemas = []
|
|
218
268
|
for xsd in ["BES.xsd", "BESAPI.xsd", "BESActionSettings.xsd"]:
|
|
219
269
|
schema_path = importlib.resources.files(__package__) / f"schemas/{xsd}"
|
|
220
270
|
with schema_path.open("r") as xsd_file:
|
|
221
271
|
xmlschema_doc = lxml.etree.parse(xsd_file)
|
|
222
|
-
|
|
223
|
-
# one schema may throw an error while another will validate
|
|
224
272
|
try:
|
|
225
|
-
|
|
273
|
+
schemas.append(lxml.etree.XMLSchema(xmlschema_doc))
|
|
226
274
|
except lxml.etree.XMLSchemaParseError as err:
|
|
227
275
|
# this should only error if the XSD itself is malformed
|
|
228
276
|
besapi_logger.error("ERROR with `%s`: %s", xsd, err)
|
|
229
277
|
raise err
|
|
230
278
|
|
|
231
|
-
|
|
232
|
-
|
|
279
|
+
_xsd_schemas_cache.schemas = tuple(schemas)
|
|
280
|
+
return _xsd_schemas_cache.schemas
|
|
233
281
|
|
|
234
|
-
|
|
282
|
+
|
|
283
|
+
def validate_xsd(doc):
|
|
284
|
+
"""Validate results using XML XSDs."""
|
|
285
|
+
try:
|
|
286
|
+
xmldoc = lxml.etree.fromstring(doc)
|
|
287
|
+
except BaseException: # pylint: disable=broad-except
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
# one schema may fail while another will validate
|
|
291
|
+
return any(xmlschema.validate(xmldoc) for xmlschema in _get_xsd_schemas())
|
|
235
292
|
|
|
236
293
|
|
|
237
294
|
def validate_xml_bes_file(file_path):
|
|
@@ -321,7 +378,7 @@ def action_xml_from_bes_file(file_path, targets="<AllComputers>"):
|
|
|
321
378
|
else:
|
|
322
379
|
custom_relevance = tree.xpath(f"//BES/{bes_type}/SuccessCriteria/text()")[0]
|
|
323
380
|
|
|
324
|
-
custom_relevance_xml = f"<![CDATA[{custom_relevance}]]>"
|
|
381
|
+
custom_relevance_xml = f"<![CDATA[{cdata_escape(custom_relevance)}]]>"
|
|
325
382
|
|
|
326
383
|
logging.debug("success_criteria: %s", success_criteria)
|
|
327
384
|
|
|
@@ -351,10 +408,10 @@ def action_xml_from_bes_file(file_path, targets="<AllComputers>"):
|
|
|
351
408
|
action_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
352
409
|
<BES xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="BES.xsd">
|
|
353
410
|
<SingleAction>
|
|
354
|
-
<Title>{title}</Title>
|
|
355
|
-
<Relevance><![CDATA[{relevance_clauses_combined}]]></Relevance>
|
|
411
|
+
<Title>{xml_escape(title)}</Title>
|
|
412
|
+
<Relevance><![CDATA[{cdata_escape(relevance_clauses_combined)}]]></Relevance>
|
|
356
413
|
<ActionScript MIMEType="application/x-Fixlet-Windows-Shell"><![CDATA[// Start:
|
|
357
|
-
{actionscript}
|
|
414
|
+
{cdata_escape(actionscript)}
|
|
358
415
|
// End]]></ActionScript>
|
|
359
416
|
<SuccessCriteria Option="{success_criteria}">{custom_relevance_xml}</SuccessCriteria>{settings_xml_string}
|
|
360
417
|
<Target>
|
|
@@ -545,11 +602,33 @@ class HTTPAdapterBlocksize(requests.adapters.HTTPAdapter):
|
|
|
545
602
|
class BESConnection:
|
|
546
603
|
"""BigFix RESTAPI connection abstraction class."""
|
|
547
604
|
|
|
548
|
-
def __init__(
|
|
605
|
+
def __init__(
|
|
606
|
+
self,
|
|
607
|
+
username,
|
|
608
|
+
password,
|
|
609
|
+
rootserver,
|
|
610
|
+
verify=False,
|
|
611
|
+
timeout=DEFAULT_TIMEOUT,
|
|
612
|
+
raise_for_status=False,
|
|
613
|
+
):
|
|
614
|
+
"""Create a connection to the BigFix REST API.
|
|
615
|
+
|
|
616
|
+
Arguments:
|
|
617
|
+
timeout: default `timeout` for each request, as used by
|
|
618
|
+
`requests`, such as 60 or (connect, read). A timeout passed to a
|
|
619
|
+
request wins. Defaults to DEFAULT_TIMEOUT, None means requests
|
|
620
|
+
wait indefinitely.
|
|
621
|
+
raise_for_status: if True, raise requests.HTTPError for 4xx / 5xx
|
|
622
|
+
responses instead of returning them. 403 always raises
|
|
623
|
+
PermissionError. NOTE: session relevance errors are returned
|
|
624
|
+
with HTTP 200, see session_relevance_array(raise_errors=True).
|
|
625
|
+
"""
|
|
549
626
|
if not verify:
|
|
550
627
|
# disable SSL warnings
|
|
551
628
|
requests.packages.urllib3.disable_warnings() # pylint: disable=no-member
|
|
552
629
|
self.verify = verify
|
|
630
|
+
self.timeout = timeout
|
|
631
|
+
self.raise_for_status = raise_for_status
|
|
553
632
|
self.last_connected = None
|
|
554
633
|
|
|
555
634
|
self.username = username
|
|
@@ -623,15 +702,25 @@ class BESConnection:
|
|
|
623
702
|
|
|
624
703
|
return url
|
|
625
704
|
|
|
705
|
+
def _to_result(self, response):
|
|
706
|
+
"""Wrap a response as a RESTResult, raising for errors if configured."""
|
|
707
|
+
# NOTE: RESTResult raises PermissionError for 403 first
|
|
708
|
+
result = RESTResult(response)
|
|
709
|
+
if self.raise_for_status:
|
|
710
|
+
response.raise_for_status()
|
|
711
|
+
return result
|
|
712
|
+
|
|
626
713
|
def get(self, path="help", **kwargs):
|
|
627
714
|
"""HTTP GET request."""
|
|
715
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
628
716
|
self.last_connected = datetime.datetime.now()
|
|
629
|
-
return
|
|
717
|
+
return self._to_result(
|
|
630
718
|
self.session.get(self.url(path), verify=self.verify, **kwargs)
|
|
631
719
|
)
|
|
632
720
|
|
|
633
721
|
def post(self, path, data, validate_xml=None, **kwargs):
|
|
634
722
|
"""HTTP POST request."""
|
|
723
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
635
724
|
|
|
636
725
|
# if validate_xml is true, data must validate to xml schema
|
|
637
726
|
# if validate_xml is false, no schema check will be made
|
|
@@ -646,12 +735,13 @@ class BESConnection:
|
|
|
646
735
|
besapi_logger.warning(err_msg)
|
|
647
736
|
|
|
648
737
|
self.last_connected = datetime.datetime.now()
|
|
649
|
-
return
|
|
738
|
+
return self._to_result(
|
|
650
739
|
self.session.post(self.url(path), data=data, verify=self.verify, **kwargs)
|
|
651
740
|
)
|
|
652
741
|
|
|
653
742
|
def put(self, path, data, validate_xml=None, **kwargs):
|
|
654
743
|
"""HTTP PUT request."""
|
|
744
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
655
745
|
self.last_connected = datetime.datetime.now()
|
|
656
746
|
|
|
657
747
|
# if validate_xml is true, data must validate to xml schema
|
|
@@ -666,14 +756,15 @@ class BESConnection:
|
|
|
666
756
|
# this is intended it validate_xml is None, but not used currently
|
|
667
757
|
besapi_logger.warning(err_msg)
|
|
668
758
|
|
|
669
|
-
return
|
|
759
|
+
return self._to_result(
|
|
670
760
|
self.session.put(self.url(path), data=data, verify=self.verify, **kwargs)
|
|
671
761
|
)
|
|
672
762
|
|
|
673
763
|
def delete(self, path, **kwargs):
|
|
674
764
|
"""HTTP DELETE request."""
|
|
765
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
675
766
|
self.last_connected = datetime.datetime.now()
|
|
676
|
-
return
|
|
767
|
+
return self._to_result(
|
|
677
768
|
self.session.delete(self.url(path), verify=self.verify, **kwargs)
|
|
678
769
|
)
|
|
679
770
|
|
|
@@ -699,15 +790,7 @@ class BESConnection:
|
|
|
699
790
|
"""
|
|
700
791
|
session_relevance = urllib.parse.quote(relevance, safe=":+")
|
|
701
792
|
rel_data = {"output": "json", "relevance": session_relevance}
|
|
702
|
-
self.
|
|
703
|
-
result = RESTResult(
|
|
704
|
-
self.session.post(
|
|
705
|
-
self.url("query"),
|
|
706
|
-
data=rel_data,
|
|
707
|
-
verify=self.verify,
|
|
708
|
-
**kwargs,
|
|
709
|
-
)
|
|
710
|
-
)
|
|
793
|
+
result = self.post("query", data=rel_data, **kwargs)
|
|
711
794
|
return json.loads(result.text)
|
|
712
795
|
|
|
713
796
|
def session_relevance_json_array(self, relevance, **kwargs):
|
|
@@ -731,18 +814,24 @@ class BESConnection:
|
|
|
731
814
|
|
|
732
815
|
def session_relevance_xml(self, relevance, **kwargs):
|
|
733
816
|
"""Get Session Relevance Results XML."""
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
)
|
|
817
|
+
# NOTE: the server URL decodes the query twice, so quote here and let
|
|
818
|
+
# requests form encode it again, same as session_relevance_json.
|
|
819
|
+
# Otherwise relevance escapes such as %22 are decoded too early.
|
|
820
|
+
return self.post(
|
|
821
|
+
"query",
|
|
822
|
+
data={"relevance": urllib.parse.quote(relevance, safe=":+")},
|
|
823
|
+
**kwargs,
|
|
742
824
|
)
|
|
743
825
|
|
|
744
|
-
def session_relevance_array(self, relevance, **kwargs):
|
|
745
|
-
"""Get Session Relevance Results array.
|
|
826
|
+
def session_relevance_array(self, relevance, raise_errors=False, **kwargs):
|
|
827
|
+
"""Get Session Relevance Results array.
|
|
828
|
+
|
|
829
|
+
Arguments:
|
|
830
|
+
raise_errors: if True, raise ValueError with the server's message
|
|
831
|
+
when the relevance has an error (such as a syntax error),
|
|
832
|
+
instead of returning ["ERROR: <message>"].
|
|
833
|
+
NOTE: the server returns these errors with HTTP 200.
|
|
834
|
+
"""
|
|
746
835
|
rel_result = self.session_relevance_xml(relevance, **kwargs)
|
|
747
836
|
# print(rel_result)
|
|
748
837
|
result = []
|
|
@@ -753,7 +842,7 @@ class BESConnection:
|
|
|
753
842
|
# print(err)
|
|
754
843
|
if "no such child: Answer" in str(err):
|
|
755
844
|
try:
|
|
756
|
-
|
|
845
|
+
error_text = rel_result.besobj.Query.Error.text
|
|
757
846
|
except AttributeError as err2:
|
|
758
847
|
if "no such child: Error" in str(err2):
|
|
759
848
|
result.append("<Nothing> Nothing returned, but no error.")
|
|
@@ -762,16 +851,28 @@ class BESConnection:
|
|
|
762
851
|
besapi_logger.error("%s\n%s", err2, rel_result.text)
|
|
763
852
|
result.append("ERROR: " + rel_result.text)
|
|
764
853
|
raise
|
|
854
|
+
else:
|
|
855
|
+
if raise_errors:
|
|
856
|
+
raise ValueError(
|
|
857
|
+
f"Session relevance error: {error_text}"
|
|
858
|
+
) from err
|
|
859
|
+
result.append("ERROR: " + error_text)
|
|
765
860
|
else:
|
|
766
861
|
besapi_logger.error("%s\n%s", err, rel_result.text)
|
|
767
862
|
result.append("ERROR: " + rel_result.text)
|
|
768
863
|
raise
|
|
769
864
|
return result
|
|
770
865
|
|
|
771
|
-
def session_relevance_string(self, relevance, **kwargs):
|
|
772
|
-
"""Get Session Relevance Results string.
|
|
866
|
+
def session_relevance_string(self, relevance, raise_errors=False, **kwargs):
|
|
867
|
+
"""Get Session Relevance Results string.
|
|
868
|
+
|
|
869
|
+
Arguments:
|
|
870
|
+
raise_errors: see session_relevance_array()
|
|
871
|
+
"""
|
|
773
872
|
rel_result_array = self.session_relevance_array(
|
|
774
|
-
"(it as string) of ( " + relevance + " )",
|
|
873
|
+
"(it as string) of ( " + relevance + " )",
|
|
874
|
+
raise_errors=raise_errors,
|
|
875
|
+
**kwargs,
|
|
775
876
|
)
|
|
776
877
|
return "\n".join(rel_result_array)
|
|
777
878
|
|
|
@@ -795,6 +896,9 @@ class BESConnection:
|
|
|
795
896
|
if not bool(self.last_connected):
|
|
796
897
|
result_login = self.get("login", timeout=timeout)
|
|
797
898
|
if not result_login.request.status_code == 200:
|
|
899
|
+
# NOTE: get() sets last_connected before the request, clear it
|
|
900
|
+
# so a failed login is not treated as connected on the next call
|
|
901
|
+
self.last_connected = None
|
|
798
902
|
result_login.request.raise_for_status()
|
|
799
903
|
if result_login.request.status_code == 200:
|
|
800
904
|
# set time of connection
|
|
@@ -821,10 +925,10 @@ class BESConnection:
|
|
|
821
925
|
|
|
822
926
|
dash_var_xml = f"""<BESAPI xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="BESAPI.xsd">
|
|
823
927
|
<DashboardData>
|
|
824
|
-
<Dashboard>{dashboard_name}</Dashboard>
|
|
825
|
-
<Name>{var_name}</Name>
|
|
928
|
+
<Dashboard>{xml_escape(str(dashboard_name))}</Dashboard>
|
|
929
|
+
<Name>{xml_escape(str(var_name))}</Name>
|
|
826
930
|
<IsPrivate>{str(private).lower()}</IsPrivate>
|
|
827
|
-
<Value>{var_value}</Value>
|
|
931
|
+
<Value>{xml_escape(str(var_value))}</Value>
|
|
828
932
|
</DashboardData>
|
|
829
933
|
</BESAPI>
|
|
830
934
|
"""
|
|
@@ -917,21 +1021,33 @@ class BESConnection:
|
|
|
917
1021
|
besapi_logger.error("%s is not readable", bes_file_path)
|
|
918
1022
|
raise FileNotFoundError(f"{bes_file_path} is not readable")
|
|
919
1023
|
|
|
1024
|
+
with open(bes_file_path, "rb") as f:
|
|
1025
|
+
content = f.read()
|
|
1026
|
+
|
|
1027
|
+
return self.import_bes_xml_to_site(content, site_path)
|
|
1028
|
+
|
|
1029
|
+
def import_bes_xml_to_site(self, bes_xml, site_path=None):
|
|
1030
|
+
"""Import BES XML content (str or bytes) to site.
|
|
1031
|
+
|
|
1032
|
+
This avoids writing a temporary .bes file for generated content.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
The RESTResult, or None if the BES XML is not valid.
|
|
1036
|
+
"""
|
|
1037
|
+
if isinstance(bes_xml, str):
|
|
1038
|
+
bes_xml = bes_xml.encode("utf-8")
|
|
1039
|
+
|
|
920
1040
|
site_path = self.get_current_site_path(site_path)
|
|
921
1041
|
|
|
922
1042
|
self.validate_site_path(site_path, False, True)
|
|
923
1043
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if not validate_xsd(content):
|
|
929
|
-
besapi_logger.error("%s is not valid", bes_file_path)
|
|
930
|
-
return None
|
|
1044
|
+
# validate BES XML contents:
|
|
1045
|
+
if not validate_xsd(bes_xml):
|
|
1046
|
+
besapi_logger.error("BES XML to import to %s is not valid", site_path)
|
|
1047
|
+
return None
|
|
931
1048
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
return result
|
|
1049
|
+
# https://developer.bigfix.com/rest-api/api/import.html
|
|
1050
|
+
return self.post(f"import/{site_path}", bes_xml)
|
|
935
1051
|
|
|
936
1052
|
def create_site_from_file(self, bes_file_path, site_type="custom"):
|
|
937
1053
|
"""Create new site."""
|
|
@@ -1010,7 +1126,7 @@ class BESConnection:
|
|
|
1010
1126
|
|
|
1011
1127
|
besapi_logger.debug("group creation result:\n%s", create_group_result)
|
|
1012
1128
|
|
|
1013
|
-
return self.get_computergroup(
|
|
1129
|
+
return self.get_computergroup(new_group_name, site_path)
|
|
1014
1130
|
|
|
1015
1131
|
def get_upload(self, file_name, file_hash):
|
|
1016
1132
|
"""
|
|
@@ -1045,8 +1161,8 @@ class BESConnection:
|
|
|
1045
1161
|
https://developer.bigfix.com/rest-api/api/upload.html
|
|
1046
1162
|
"""
|
|
1047
1163
|
if not os.access(file_path, os.R_OK):
|
|
1048
|
-
besapi_logger.error(
|
|
1049
|
-
raise FileNotFoundError
|
|
1164
|
+
besapi_logger.error("%s is not readable", file_path)
|
|
1165
|
+
raise FileNotFoundError(f"{file_path} is not readable")
|
|
1050
1166
|
|
|
1051
1167
|
# if file_name not specified, then get it from tail of file_path
|
|
1052
1168
|
if not file_name:
|
|
@@ -1134,7 +1250,14 @@ class BESConnection:
|
|
|
1134
1250
|
return content
|
|
1135
1251
|
|
|
1136
1252
|
def update_item_from_file(self, file_path, site_path=None):
|
|
1137
|
-
"""Update an item by name and last modified.
|
|
1253
|
+
"""Update an item by name and last modified.
|
|
1254
|
+
|
|
1255
|
+
NOTE: not implemented yet.
|
|
1256
|
+
"""
|
|
1257
|
+
raise NotImplementedError("besapi.update_item_from_file() is not implemented")
|
|
1258
|
+
|
|
1259
|
+
# pylint: disable=unreachable
|
|
1260
|
+
# NOTE: work in progress, kept for later, never runs because of the raise
|
|
1138
1261
|
site_path = self.get_current_site_path(site_path)
|
|
1139
1262
|
bes_tree = lxml.etree.parse(file_path)
|
|
1140
1263
|
|
|
@@ -1338,8 +1461,15 @@ class BESConnection:
|
|
|
1338
1461
|
)
|
|
1339
1462
|
|
|
1340
1463
|
__call__ = login
|
|
1341
|
-
|
|
1342
|
-
__enter__
|
|
1464
|
+
|
|
1465
|
+
def __enter__(self):
|
|
1466
|
+
"""Log in and return this connection, for use with `with`."""
|
|
1467
|
+
self.login()
|
|
1468
|
+
return self
|
|
1469
|
+
|
|
1470
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
1471
|
+
"""Log out when leaving a `with` block."""
|
|
1472
|
+
self.logout()
|
|
1343
1473
|
|
|
1344
1474
|
|
|
1345
1475
|
class RESTResult:
|
|
@@ -1348,7 +1478,8 @@ class RESTResult:
|
|
|
1348
1478
|
def __init__(self, request):
|
|
1349
1479
|
self.request = request
|
|
1350
1480
|
self.text = request.text
|
|
1351
|
-
|
|
1481
|
+
# computed on first access of `valid`, see the property:
|
|
1482
|
+
self._valid = None
|
|
1352
1483
|
self._besxml = None
|
|
1353
1484
|
self._besobj = None
|
|
1354
1485
|
self._besdict = None
|
|
@@ -1370,23 +1501,35 @@ class RESTResult:
|
|
|
1370
1501
|
except AttributeError as err:
|
|
1371
1502
|
besapi_logger.warning("Error (expected during tests) %s", err)
|
|
1372
1503
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if
|
|
1384
|
-
|
|
1504
|
+
@property
|
|
1505
|
+
def valid(self):
|
|
1506
|
+
"""Whether the result is BES XML, validated on first access.
|
|
1507
|
+
|
|
1508
|
+
NOTE: this is lazy since XSD validation of every result is costly,
|
|
1509
|
+
and many results (such as JSON queries) are never used as XML.
|
|
1510
|
+
"""
|
|
1511
|
+
if self._valid is None:
|
|
1512
|
+
headers = self.request.headers
|
|
1513
|
+
# NOTE: membership test, headers may not be a dict in all callers
|
|
1514
|
+
content_type = headers["content-type"] if "content-type" in headers else ""
|
|
1515
|
+
if content_type == "application/xml":
|
|
1516
|
+
self._valid = True
|
|
1517
|
+
elif content_type.startswith("application/json"):
|
|
1518
|
+
self._valid = False
|
|
1519
|
+
elif isinstance(self.text, str):
|
|
1520
|
+
self._valid = validate_xsd(self.text.encode("utf-8"))
|
|
1385
1521
|
else:
|
|
1522
|
+
self._valid = validate_xsd(self.text)
|
|
1523
|
+
|
|
1524
|
+
if not self._valid:
|
|
1386
1525
|
besapi_logger.debug(
|
|
1387
1526
|
"INFO: REST API Result does not appear to be XML, this could be expected."
|
|
1388
1527
|
)
|
|
1389
|
-
|
|
1528
|
+
return self._valid
|
|
1529
|
+
|
|
1530
|
+
@valid.setter
|
|
1531
|
+
def valid(self, value):
|
|
1532
|
+
self._valid = value
|
|
1390
1533
|
|
|
1391
1534
|
def __str__(self):
|
|
1392
1535
|
if self.valid:
|
|
@@ -1438,9 +1581,9 @@ class RESTResult:
|
|
|
1438
1581
|
|
|
1439
1582
|
def validate_xsd(self, doc):
|
|
1440
1583
|
"""Validate results using XML XSDs."""
|
|
1441
|
-
# return self.
|
|
1442
|
-
if
|
|
1443
|
-
return self.
|
|
1584
|
+
# return self._valid if already set
|
|
1585
|
+
if isinstance(self._valid, bool):
|
|
1586
|
+
return self._valid
|
|
1444
1587
|
return validate_xsd(doc)
|
|
1445
1588
|
|
|
1446
1589
|
def xmlparse_text(self, text):
|