besapi 3.7.7__tar.gz → 3.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: besapi
3
- Version: 3.7.7
3
+ Version: 3.8.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
@@ -12,13 +12,13 @@ Library for communicating with the BES (BigFix) REST API.
12
12
  import configparser
13
13
  import datetime
14
14
  import hashlib
15
- import io
16
15
  import json
17
16
  import logging
18
17
  import os
19
18
  import random
20
19
  import site
21
20
  import string
21
+ import typing
22
22
 
23
23
  try:
24
24
  from urllib import parse
@@ -29,7 +29,7 @@ import requests
29
29
  from lxml import etree, objectify
30
30
  from pkg_resources import resource_filename
31
31
 
32
- __version__ = "3.7.7"
32
+ __version__ = "3.8.1"
33
33
 
34
34
  besapi_logger = logging.getLogger("besapi")
35
35
 
@@ -94,7 +94,7 @@ def elem2dict(node):
94
94
  def replace_text_between(
95
95
  original_text, first_delimiter, second_delimiter, replacement_text
96
96
  ):
97
- """Replace text between delimeters. Each delimiter should only appear once."""
97
+ """Replace text between delimiters. Each delimiter should only appear once."""
98
98
  leading_text = original_text.split(first_delimiter)[0]
99
99
  trailing_text = original_text.split(second_delimiter)[1]
100
100
 
@@ -120,7 +120,7 @@ def parse_bes_modtime(string_datetime):
120
120
  # """custom HTTPAdapter for requests to override blocksize
121
121
  # for Uploading or Downloading large files"""
122
122
 
123
- # # override inti_poolmanager from regular HTTPAdapter
123
+ # # override init_poolmanager from regular HTTPAdapter
124
124
  # # https://stackoverflow.com/questions/22915295/python-requests-post-and-big-content/22915488#comment125583017_22915488
125
125
  # def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
126
126
  # """Initializes a urllib3 PoolManager.
@@ -152,6 +152,92 @@ def parse_bes_modtime(string_datetime):
152
152
  # )
153
153
 
154
154
 
155
+ def get_action_combined_relevance(relevances: typing.List[str]):
156
+ """take array of ordered relevance clauses and return relevance string for action"""
157
+
158
+ relevance_combined = ""
159
+
160
+ if not relevances:
161
+ return "False"
162
+ if len(relevances) == 0:
163
+ return "False"
164
+ if len(relevances) == 1:
165
+ return relevances[0]
166
+ if len(relevances) > 0:
167
+ for clause in relevances:
168
+ if len(relevance_combined) == 0:
169
+ relevance_combined = clause
170
+ else:
171
+ relevance_combined = (
172
+ "( " + relevance_combined + " ) AND ( " + clause + " )"
173
+ )
174
+
175
+ return relevance_combined
176
+
177
+
178
+ def get_target_xml(targets=None):
179
+ """get target xml based upon input
180
+
181
+ Input can be a single string:
182
+ - starts with "<AllComputers>" if all computers should be targeted
183
+ - Otherwise will be interpreted as custom relevance
184
+
185
+ Input can be a single int:
186
+ - Single Computer ID Target
187
+
188
+ Input can be an array:
189
+ - Array of Strings: ComputerName
190
+ - Array of Integers: ComputerID
191
+ """
192
+ if targets is None or not targets:
193
+ logging.warning("No valid targeting found, will target no computers.")
194
+ # default if invalid:
195
+ return "<CustomRelevance>False</CustomRelevance>"
196
+
197
+ # if targets is int:
198
+ if isinstance(targets, int):
199
+ if targets == 0:
200
+ raise ValueError(
201
+ "Int 0 is not valid Computer ID, set targets to an array of strings of computer names or an array of ints of computer ids or custom relevance string or <AllComputers>"
202
+ )
203
+ return f"<ComputerID>{targets}</ComputerID>"
204
+
205
+ # if targets is str:
206
+ if isinstance(targets, str):
207
+ # if targets string starts with "<AllComputers>":
208
+ if targets.startswith("<AllComputers>"):
209
+ if "false" in targets.lower():
210
+ # In my testing, <AllComputers>false</AllComputers> does not work correctly
211
+ return "<CustomRelevance>False</CustomRelevance>"
212
+ # return "<AllComputers>false</AllComputers>"
213
+ return "<AllComputers>true</AllComputers>"
214
+ # treat as custom relevance:
215
+ return f"<CustomRelevance><![CDATA[{targets}]]></CustomRelevance>"
216
+
217
+ # if targets is array:
218
+ if isinstance(targets, list):
219
+ element_type = type(targets[0])
220
+ if element_type is int:
221
+ # array of computer ids
222
+ return (
223
+ "<ComputerID>"
224
+ + "</ComputerID><ComputerID>".join(map(str, targets))
225
+ + "</ComputerID>"
226
+ )
227
+ if element_type is str:
228
+ # array of computer names
229
+ return (
230
+ "<ComputerName>"
231
+ + "</ComputerName><ComputerName>".join(targets)
232
+ + "</ComputerName>"
233
+ )
234
+
235
+ logging.warning("No valid targeting found, will target no computers.")
236
+
237
+ # default if invalid:
238
+ return "<CustomRelevance>False</CustomRelevance>"
239
+
240
+
155
241
  def validate_xsd(doc):
156
242
  """validate results using XML XSDs"""
157
243
  try:
@@ -257,7 +343,7 @@ class BESConnection:
257
343
  def __repr__(self):
258
344
  """object representation"""
259
345
  # https://stackoverflow.com/a/2626364/861745
260
- return f"Object: besapi.BESConnction( username={self.username}, rootserver={self.rootserver} )"
346
+ return f"Object: besapi.BESConnection( username={self.username}, rootserver={self.rootserver} )"
261
347
 
262
348
  def __eq__(self, other):
263
349
  if (
@@ -669,7 +755,7 @@ class BESConnection:
669
755
  file_url = str(result_upload.besobj.FileUpload.URL)
670
756
  if use_https:
671
757
  file_url = file_url.replace("http://", "https://")
672
- # there are 3 different posibilities for the server FQDN
758
+ # there are 3 different possibilities for the server FQDN
673
759
  # localhost
674
760
  # self.rootserver (without port number)
675
761
  # the returned value from the upload result
@@ -927,7 +1013,7 @@ class RESTResult:
927
1013
  f"\n - HTTP Response Status Code: `403` Forbidden\n - ERROR: `{self.text}`\n - URL: `{self.request.url}`"
928
1014
  )
929
1015
 
930
- besapi_logger.info(
1016
+ besapi_logger.debug(
931
1017
  "HTTP Request Status Code `%d` from URL `%s`",
932
1018
  self.request.status_code,
933
1019
  self.request.url,
@@ -58,7 +58,7 @@ def get_invoke_file_name(verbose=0):
58
58
  def setup_plugin_argparse(plugin_args_required=False):
59
59
  """setup argparse for plugin use"""
60
60
  arg_parser = argparse.ArgumentParser(
61
- description="Provde command line arguments for REST URL, username, and password"
61
+ description="Provide command line arguments for REST URL, username, and password"
62
62
  )
63
63
  arg_parser.add_argument(
64
64
  "-v",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: besapi
3
- Version: 3.7.7
3
+ Version: 3.8.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
@@ -10,6 +10,7 @@ Simple command line interface for the BES (BigFix) REST API.
10
10
  """
11
11
 
12
12
  import getpass
13
+ import json
13
14
  import logging
14
15
  import os
15
16
  import site
@@ -56,8 +57,50 @@ class BESCLInterface(Cmd):
56
57
  # set default config file path
57
58
  self.conf_path = os.path.expanduser("~/.besapi.conf")
58
59
  self.CONFPARSER = SafeConfigParser()
60
+ # for completion:
61
+ self.api_resources = []
59
62
  self.do_conf()
60
63
 
64
+ def parse_help_resources(self):
65
+ """get api resources from help"""
66
+ if self.bes_conn:
67
+ help_result = self.bes_conn.get("help")
68
+ help_result = help_result.text.split("\n")
69
+ # print(help_result)
70
+ help_resources = []
71
+ for item in help_result:
72
+ if "/api/" in item:
73
+ _, _, res = item.partition("/api/")
74
+ # strip whitespace just in case:
75
+ help_resources.append(res.strip())
76
+
77
+ return help_resources
78
+ else:
79
+ return [
80
+ "actions",
81
+ "clientqueryresults",
82
+ "dashboardvariables",
83
+ "help",
84
+ "login",
85
+ "query",
86
+ "relaysites",
87
+ "serverinfo",
88
+ "sites",
89
+ ]
90
+
91
+ def complete_api_resources(self, text, line, begidx, endidx):
92
+ """define completion for apis"""
93
+
94
+ # only initialize once
95
+ if not self.api_resources:
96
+ self.api_resources = self.parse_help_resources()
97
+
98
+ # TODO: make this work to complete only the first word after get/post/delete
99
+ # return the matching subset:
100
+ return [name for name in self.api_resources if name.startswith(text)]
101
+
102
+ complete_get = complete_api_resources
103
+
61
104
  def do_get(self, line):
62
105
  """Perform get request to BigFix server using provided api endpoint argument"""
63
106
 
@@ -80,16 +123,18 @@ class BESCLInterface(Cmd):
80
123
  b = self.bes_conn.get(robjs[0])
81
124
  # print objectify.ObjectPath(robjs[1:])
82
125
  if b:
83
- print(eval("b()." + ".".join(robjs[1:])))
126
+ self.poutput(eval("b()." + ".".join(robjs[1:])))
84
127
  else:
85
128
  output_item = self.bes_conn.get(line)
86
- # print(type(output_item))
87
- print(output_item)
88
- # print(output_item.besdict)
89
- # print(output_item.besjson)
129
+ # self.poutput(type(output_item))
130
+ self.poutput(output_item)
131
+ # self.poutput(output_item.besdict)
132
+ # self.poutput(output_item.besjson)
90
133
  else:
91
134
  self.pfeedback("Not currently logged in. Type 'login'.")
92
135
 
136
+ complete_delete = complete_api_resources
137
+
93
138
  def do_delete(self, line):
94
139
  """Perform delete request to BigFix server using provided api endpoint argument"""
95
140
 
@@ -104,16 +149,18 @@ class BESCLInterface(Cmd):
104
149
  if self.bes_conn:
105
150
  output_item = self.bes_conn.delete(line)
106
151
 
107
- print(output_item)
108
- # print(output_item.besdict)
109
- # print(output_item.besjson)
152
+ self.poutput(output_item)
153
+ # self.poutput(output_item.besdict)
154
+ # self.poutput(output_item.besjson)
110
155
  else:
111
156
  self.pfeedback("Not currently logged in. Type 'login'.")
112
157
 
158
+ complete_post = complete_api_resources
159
+
113
160
  def do_post(self, statement):
114
161
  """post file as data to path"""
115
- print(statement)
116
- print("not yet implemented")
162
+ self.poutput(statement)
163
+ self.poutput("not yet implemented")
117
164
 
118
165
  def do_config(self, conf_file=None):
119
166
  """Attempt to load config info from file and login"""
@@ -244,7 +291,7 @@ class BESCLInterface(Cmd):
244
291
  else:
245
292
  self.perror("Login Error!")
246
293
 
247
- def do_logout(self, arg=None):
294
+ def do_logout(self, _=None):
248
295
  """Logout and clear session"""
249
296
  if self.bes_conn:
250
297
  self.bes_conn.logout()
@@ -254,7 +301,7 @@ class BESCLInterface(Cmd):
254
301
 
255
302
  def do_debug(self, setting):
256
303
  """Enable or Disable Debug Mode"""
257
- print(bool(setting))
304
+ self.poutput(bool(setting))
258
305
  self.debug = bool(setting)
259
306
  self.echo = bool(setting)
260
307
  self.quiet = bool(setting)
@@ -288,12 +335,12 @@ class BESCLInterface(Cmd):
288
335
  """save current config to file"""
289
336
  self.do_saveconf(arg)
290
337
 
291
- def do_saveconf(self, arg=None):
338
+ def do_saveconf(self, _=None):
292
339
  """save current config to file"""
293
340
  if not self.bes_conn:
294
341
  self.do_login()
295
342
  if not self.bes_conn:
296
- print("Can't save config without working login")
343
+ self.poutput("Can't save config without working login")
297
344
  else:
298
345
  conf_file_path = self.conf_path
299
346
  self.pfeedback(f"Saving Config File to: {conf_file_path}")
@@ -304,29 +351,31 @@ class BESCLInterface(Cmd):
304
351
  """List the current settings and connection status"""
305
352
  self.do_ls(arg)
306
353
 
307
- def do_ls(self, arg=None):
354
+ def do_ls(self, _=None):
308
355
  """List the current settings and connection status"""
309
- print(" Connected: " + str(bool(self.bes_conn)))
310
- print(
356
+ self.poutput(" Connected: " + str(bool(self.bes_conn)))
357
+ self.poutput(
311
358
  " BES_ROOT_SERVER: "
312
359
  + (self.BES_ROOT_SERVER if self.BES_ROOT_SERVER else "")
313
360
  )
314
- print(
361
+ self.poutput(
315
362
  " BES_USER_NAME: " + (self.BES_USER_NAME if self.BES_USER_NAME else "")
316
363
  )
317
- print(
364
+ self.poutput(
318
365
  " Password Length: "
319
366
  + str(len(self.BES_PASSWORD if self.BES_PASSWORD else ""))
320
367
  )
321
- print(" Config File Path: " + self.conf_path)
368
+ self.poutput(" Config File Path: " + self.conf_path)
322
369
  if self.bes_conn:
323
- print("Current Site Path: " + self.bes_conn.get_current_site_path(None))
370
+ self.poutput(
371
+ "Current Site Path: " + self.bes_conn.get_current_site_path(None)
372
+ )
324
373
 
325
- def do_error_count(self, arg=None):
374
+ def do_error_count(self, _=None):
326
375
  """Output the number of errors"""
327
376
  self.poutput(f"Error Count: {self.num_errors}")
328
377
 
329
- def do_exit(self, arg=None):
378
+ def do_exit(self, _=None):
330
379
  """Exit this application"""
331
380
  self.exit_code = self.num_errors
332
381
  # no matter what I try I can't get anything but exit code 0 on windows
@@ -347,7 +396,7 @@ class BESCLInterface(Cmd):
347
396
  self.pfeedback("A: ")
348
397
  self.poutput(rel_result)
349
398
 
350
- def do_version(self, statement=None):
399
+ def do_version(self, _=None):
351
400
  """output version of besapi"""
352
401
  self.poutput(f"besapi version: {__version__}")
353
402
 
@@ -361,7 +410,7 @@ class BESCLInterface(Cmd):
361
410
  result_op = self.bes_conn.get_user(statement)
362
411
  self.poutput(result_op)
363
412
 
364
- def do_get_current_site(self, statement=None):
413
+ def do_get_current_site(self, _=None):
365
414
  """output current site path context"""
366
415
  self.poutput(
367
416
  f"Current Site Path: `{ self.bes_conn.get_current_site_path(None) }`"
@@ -375,11 +424,11 @@ class BESCLInterface(Cmd):
375
424
 
376
425
  def do_get_content(self, resource_url):
377
426
  """get a specific item by resource url"""
378
- print(self.bes_conn.get_content_by_resource(resource_url))
427
+ self.poutput(self.bes_conn.get_content_by_resource(resource_url))
379
428
 
380
429
  def do_export_item_by_resource(self, statement):
381
430
  """export content itemb to current folder"""
382
- print(self.bes_conn.export_item_by_resource(statement))
431
+ self.poutput(self.bes_conn.export_item_by_resource(statement))
383
432
 
384
433
  def do_export_site(self, site_path):
385
434
  """export site contents to current folder"""
@@ -387,7 +436,7 @@ class BESCLInterface(Cmd):
387
436
  site_path, verbose=True, include_site_folder=False, include_item_ids=False
388
437
  )
389
438
 
390
- def do_export_all_sites(self, statement=None):
439
+ def do_export_all_sites(self, _=None):
391
440
  """export site contents to current folder"""
392
441
  self.bes_conn.export_all_sites(verbose=False)
393
442
 
@@ -409,47 +458,58 @@ class BESCLInterface(Cmd):
409
458
  def do_upload(self, file_path):
410
459
  """upload file to root server"""
411
460
  if not os.access(file_path, os.R_OK):
412
- print(file_path, "is not a readable file")
461
+ self.poutput(file_path, "is not a readable file")
413
462
  else:
414
463
  upload_result = self.bes_conn.upload(file_path)
415
- print(upload_result)
416
- print(self.bes_conn.parse_upload_result_to_prefetch(upload_result))
464
+ self.poutput(upload_result)
465
+ self.poutput(self.bes_conn.parse_upload_result_to_prefetch(upload_result))
417
466
 
418
467
  complete_create_group = Cmd.path_complete
419
468
 
420
469
  def do_create_group(self, file_path):
421
470
  """create bigfix group from bes file"""
422
471
  if not os.access(file_path, os.R_OK):
423
- print(file_path, "is not a readable file")
472
+ self.poutput(file_path, "is not a readable file")
424
473
  else:
425
- print(self.bes_conn.create_group_from_file(file_path))
474
+ self.poutput(self.bes_conn.create_group_from_file(file_path))
426
475
 
427
476
  complete_create_user = Cmd.path_complete
428
477
 
429
478
  def do_create_user(self, file_path):
430
479
  """create bigfix user from bes file"""
431
480
  if not os.access(file_path, os.R_OK):
432
- print(file_path, "is not a readable file")
481
+ self.poutput(file_path, "is not a readable file")
433
482
  else:
434
- print(self.bes_conn.create_user_from_file(file_path))
483
+ self.poutput(self.bes_conn.create_user_from_file(file_path))
435
484
 
436
485
  complete_create_site = Cmd.path_complete
437
486
 
438
487
  def do_create_site(self, file_path):
439
488
  """create bigfix site from bes file"""
440
489
  if not os.access(file_path, os.R_OK):
441
- print(file_path, "is not a readable file")
490
+ self.poutput(file_path, "is not a readable file")
442
491
  else:
443
- print(self.bes_conn.create_site_from_file(file_path))
492
+ self.poutput(self.bes_conn.create_site_from_file(file_path))
444
493
 
445
494
  complete_update_item = Cmd.path_complete
446
495
 
447
496
  def do_update_item(self, file_path):
448
497
  """update bigfix content item from bes file"""
449
498
  if not os.access(file_path, os.R_OK):
450
- print(file_path, "is not a readable file")
499
+ self.poutput(file_path, "is not a readable file")
451
500
  else:
452
- print(self.bes_conn.update_item_from_file(file_path))
501
+ self.poutput(self.bes_conn.update_item_from_file(file_path))
502
+
503
+ def do_serverinfo(self, _=None):
504
+ """get server info and return formatted"""
505
+
506
+ # not sure what the minimum version for this is:
507
+ result = self.bes_conn.get("serverinfo")
508
+
509
+ result_json = json.loads(result.text)
510
+
511
+ self.poutput(f"\nServer Info for {self.BES_ROOT_SERVER}")
512
+ self.poutput(json.dumps(result_json, indent=2))
453
513
 
454
514
 
455
515
  def main():
@@ -91,6 +91,37 @@ assert b"<BES>Example</BES>" in rest_result.xmlparse_text("<BES>Example</BES>")
91
91
 
92
92
  assert rest_result.text == "this is just a test"
93
93
 
94
+ # test date parsing function:
95
+ assert 2017 == besapi.besapi.parse_bes_modtime("Tue, 05 Sep 2017 23:31:48 +0000").year
96
+
97
+ # test action combined relevance
98
+ assert (
99
+ "( ( True ) AND ( windows of operating system ) ) AND ( False )"
100
+ == besapi.besapi.get_action_combined_relevance(
101
+ ["True", "windows of operating system", "False"]
102
+ )
103
+ )
104
+
105
+ # test target xml
106
+ assert "<CustomRelevance>False</CustomRelevance>" == besapi.besapi.get_target_xml()
107
+ assert "<AllComputers>true</AllComputers>" == besapi.besapi.get_target_xml(
108
+ "<AllComputers>"
109
+ )
110
+ assert "<ComputerID>1</ComputerID>" == besapi.besapi.get_target_xml(1)
111
+ assert (
112
+ "<CustomRelevance><![CDATA[not windows of operating system]]></CustomRelevance>"
113
+ == besapi.besapi.get_target_xml("not windows of operating system")
114
+ )
115
+ assert (
116
+ "<ComputerID>1</ComputerID><ComputerID>2</ComputerID>"
117
+ == besapi.besapi.get_target_xml([1, 2])
118
+ )
119
+ assert (
120
+ "<ComputerName>Computer 1</ComputerName><ComputerName>Another Computer</ComputerName>"
121
+ == besapi.besapi.get_target_xml(["Computer 1", "Another Computer"])
122
+ )
123
+
124
+ # test bescli:
94
125
  import bescli
95
126
 
96
127
  bigfix_cli = bescli.bescli.BESCLInterface()
@@ -151,8 +182,10 @@ if bigfix_cli.bes_conn:
151
182
  'CMD /C python -m besapi ls clear ls conf "query number of bes computers" version error_count exit',
152
183
  check=True,
153
184
  )
154
- bes_conn = besapi.besapi.get_bes_conn_using_config_file()
155
- print("login succeeded:", bes_conn.login())
185
+
186
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
187
+ print("login succeeded:", bes_conn.login())
188
+ assert bes_conn.login()
156
189
 
157
190
  # test plugin_utilities:
158
191
  print(besapi.plugin_utilities.get_invoke_folder())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes