pyxecm 1.4__py3-none-any.whl → 1.5__py3-none-any.whl

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.

Potentially problematic release.


This version of pyxecm might be problematic. Click here for more details.

pyxecm/otac.py CHANGED
@@ -8,10 +8,21 @@ __init__ : class initializer
8
8
  config : returns config data set
9
9
  hostname: returns the Archive Center hostname
10
10
  set_hostname: sets the Archive Center hostname
11
+ credentials: Get credentials (username + password)
12
+ set_credentials: Set the credentials for Archive Center for the "ds" and "admin" users
13
+ base_url: Returns the Archive Center base URL
14
+ exec_url: Returns the Archive Center URL to execute commands
15
+ request_form_header: Deliver the FORM request header used for the SOAP calls.
16
+ request_json_header: Deliver the JSON request header used for the CRUD REST API calls.
17
+ parse_request_response: Converts the text property of a request response object to a
18
+ Python dict in a safe way that also handles exceptions.
19
+ authenticate: Authenticates at Archive Center and retrieve Ticket
20
+ authenticate_soap: Authenticate via SOAP with admin User
11
21
  exec_command: exec a command on Archive Center
12
22
  put_cert: put Certificate on Archive Center
13
- enable_cert: enables Certitificate on Archive Center
14
-
23
+ enable_cert: enables Certitificate on Archive Center via SOAP
24
+ enable_certificate: Enable a certificate via the new REST API
25
+ (replacing the old SOAP interface)
15
26
  """
16
27
 
17
28
  __author__ = "Dr. Marc Diefenbruch"
@@ -23,6 +34,7 @@ __email__ = "mdiefenb@opentext.com"
23
34
  import logging
24
35
  import os
25
36
  import base64
37
+ import json
26
38
  import requests
27
39
 
28
40
  from suds.client import Client
@@ -30,14 +42,21 @@ from suds import WebFault
30
42
 
31
43
  logger = logging.getLogger("pyxecm.otac")
32
44
 
33
- requestHeaders = {"Content-Type": "application/x-www-form-urlencoded"}
45
+ REQUEST_FORM_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
46
+
47
+ REQUEST_JSON_HEADERS = {
48
+ "accept": "application/json;charset=utf-8",
49
+ "Content-Type": "application/json",
50
+ }
34
51
 
52
+ REQUEST_TIMEOUT = 60
35
53
 
36
54
  class OTAC:
37
55
  """Used to automate stettings in OpenText Archive Center."""
38
56
 
39
57
  _config = None
40
58
  _soap_token: str = ""
59
+ _otac_ticket = None
41
60
 
42
61
  def __init__(
43
62
  self,
@@ -48,6 +67,7 @@ class OTAC:
48
67
  ds_password: str,
49
68
  admin_username: str,
50
69
  admin_password: str,
70
+ otds_ticket: str | None = None,
51
71
  ):
52
72
  """Initialize the OTAC object
53
73
 
@@ -104,8 +124,12 @@ class OTAC:
104
124
  otac_exec_url = otac_base_url + "/archive/admin/exec"
105
125
  otac_config["execUrl"] = otac_exec_url
106
126
  otac_config["baseUrl"] = otac_base_url
127
+ otac_config["restUrl"] = otac_base_url + "/ot-admin/rest"
128
+ otac_config["certUrl"] = otac_config["restUrl"] + "/keystore/cert/status"
129
+ otac_config["authenticationUrl"] = otac_config["restUrl"] + "/auth/users/login"
107
130
 
108
131
  self._config = otac_config
132
+ self._otac_ticket = otds_ticket
109
133
 
110
134
  def config(self) -> dict:
111
135
  """Returns the configuration dictionary
@@ -131,6 +155,17 @@ class OTAC:
131
155
  """
132
156
  self.config()["hostname"] = hostname
133
157
 
158
+ def credentials(self) -> dict:
159
+ """Get credentials (username + password)
160
+
161
+ Returns:
162
+ dict: dictionary with username and password
163
+ """
164
+ return {
165
+ "username": self.config()["admin_username"],
166
+ "password": self.config()["admin_password"],
167
+ }
168
+
134
169
  def set_credentials(
135
170
  self,
136
171
  ds_username: str = "",
@@ -182,7 +217,162 @@ class OTAC:
182
217
  """
183
218
  return self.config()["execUrl"]
184
219
 
185
- def _soap_login(self):
220
+ def request_form_header(self) -> dict:
221
+ """Deliver the FORM request header used for the SOAP calls.
222
+ Consists of Token + Form Headers (see global variable)
223
+
224
+ Args:
225
+ None.
226
+ Return:
227
+ dict: request header values
228
+ """
229
+
230
+ # create union of two dicts: cookie and headers
231
+ # (with Python 3.9 this would be easier with the "|" operator)
232
+ request_header = {}
233
+ request_header.update("token" + self._otac_ticket)
234
+ request_header.update(REQUEST_FORM_HEADERS)
235
+
236
+ return request_header
237
+
238
+ # end method definition
239
+
240
+ def request_json_header(self) -> dict:
241
+ """Deliver the JSON request header used for the CRUD REST API calls.
242
+ Consists of Cookie + JSON Headers (see global variable)
243
+
244
+ Args:
245
+ None.
246
+ Return:
247
+ dict: request header values
248
+ """
249
+
250
+ if not self._otac_ticket:
251
+ self.authenticate(revalidate=True)
252
+
253
+ # create union of two dicts: cookie and headers
254
+ # (with Python 3.9 this would be easier with the "|" operator)
255
+ request_header = {}
256
+ request_header["Authorization"] = "token " + self._otac_ticket
257
+ request_header.update(REQUEST_JSON_HEADERS)
258
+
259
+ return request_header
260
+
261
+ # end method definition
262
+
263
+ def parse_request_response(
264
+ self,
265
+ response_object: object,
266
+ additional_error_message: str = "",
267
+ show_error: bool = True,
268
+ ) -> dict | None:
269
+ """Converts the text property of a request response object to a
270
+ Python dict in a safe way that also handles exceptions.
271
+ Args:
272
+ response_object (object): this is reponse object delivered by the request call
273
+ additional_error_message (str): print a custom error message
274
+ show_error (bool): if True log an error, if False log a warning
275
+
276
+ Returns:
277
+ dict: response or None in case of an error
278
+ """
279
+
280
+ if not response_object:
281
+ return None
282
+
283
+ try:
284
+ dict_object = json.loads(response_object.text)
285
+ except json.JSONDecodeError as exception:
286
+ if additional_error_message:
287
+ message = "Cannot decode response as JSon. {}; error -> {}".format(
288
+ additional_error_message, exception
289
+ )
290
+ else:
291
+ message = "Cannot decode response as JSon; error -> {}".format(
292
+ exception
293
+ )
294
+ if show_error:
295
+ logger.error(message)
296
+ else:
297
+ logger.debug(message)
298
+ return None
299
+ else:
300
+ return dict_object
301
+
302
+ # end method definition
303
+
304
+ def authenticate(self, revalidate: bool = False) -> dict | None:
305
+ """Authenticates at Archive Center and retrieve Ticket.
306
+
307
+ Args:
308
+ revalidate (bool, optional): determinse if a re-athentication is enforced
309
+ (e.g. if session has timed out with 401 error)
310
+ By default we use the OTDS ticket (if exists) for the authentication with OTCS.
311
+ This switch allows the forced usage of username / password for the authentication.
312
+ Returns:
313
+ dict: Cookie information of None in case of an error.
314
+ Also stores cookie information in self._cookie
315
+ """
316
+
317
+ # Already authenticated and session still valid?
318
+ if self._otac_ticket and not revalidate:
319
+ logger.debug(
320
+ "Session still valid - return existing ticket -> %s",
321
+ str(self._otac_ticket),
322
+ )
323
+ return self._otac_ticket
324
+
325
+ otac_ticket = None
326
+
327
+ request_url = self.config()["authenticationUrl"]
328
+ # Check if previous authentication was not successful.
329
+ # Then we do the normal username + password authentication:
330
+ logger.debug(
331
+ "Requesting OTAC ticket with User/Password; calling -> %s",
332
+ request_url,
333
+ )
334
+
335
+ response = None
336
+ try:
337
+ response = requests.post(
338
+ url=request_url,
339
+ data=json.dumps(
340
+ self.credentials()
341
+ ), # this includes username + password
342
+ headers=REQUEST_JSON_HEADERS,
343
+ timeout=REQUEST_TIMEOUT,
344
+ )
345
+ except requests.exceptions.RequestException as exception:
346
+ logger.warning(
347
+ "Unable to connect to -> %s; error -> %s",
348
+ request_url,
349
+ exception.strerror,
350
+ )
351
+ logger.warning("OTAC service may not be ready yet.")
352
+ return None
353
+
354
+ if response.ok:
355
+ authenticate_list = self.parse_request_response(
356
+ response, "This can be normal during restart", False
357
+ )
358
+ if not authenticate_list:
359
+ return None
360
+ else:
361
+ authenticate_dict = authenticate_list[1]
362
+ otac_ticket = authenticate_dict["TOKEN"]
363
+ logger.debug("Ticket -> %s", otac_ticket)
364
+ else:
365
+ logger.error("Failed to request an OTAC ticket; error -> %s", response.text)
366
+ return None
367
+
368
+ # Store authentication ticket:
369
+ self._otac_ticket = otac_ticket
370
+
371
+ return self._otac_ticket
372
+
373
+ # end method definition
374
+
375
+ def authenticate_soap(self) -> str:
186
376
  """Authenticate via SOAP with admin User
187
377
 
188
378
  Args:
@@ -202,13 +392,13 @@ class OTAC:
202
392
 
203
393
  # end method definition
204
394
 
205
- def exec_command(self, command: str):
395
+ def exec_command(self, command: str) -> dict:
206
396
  """Execute a command on Archive Center
207
397
 
208
398
  Args:
209
399
  command (str): command to execute
210
400
  Returns:
211
- _type_: _description_
401
+ dict: Response of the HTTP request.
212
402
  """
213
403
 
214
404
  payload = {
@@ -225,7 +415,7 @@ class OTAC:
225
415
  request_url,
226
416
  )
227
417
  response = requests.post(
228
- url=request_url, data=payload, headers=requestHeaders, timeout=None
418
+ url=request_url, data=payload, headers=REQUEST_FORM_HEADERS, timeout=None
229
419
  )
230
420
  if not response.ok:
231
421
  logger.error(
@@ -245,7 +435,7 @@ class OTAC:
245
435
  cert_path: str,
246
436
  permissions: str = "rcud",
247
437
  ):
248
- """Put Certificate on Archive Center
438
+ """Put Certificate on Archive Center via SOAP Call
249
439
 
250
440
  Args:
251
441
  auth_id (str): ID of Certification
@@ -259,7 +449,7 @@ class OTAC:
259
449
 
260
450
  # Check if the photo file exists
261
451
  if not os.path.isfile(cert_path):
262
- logger.error("Certificate file -> %s not found!", cert_path)
452
+ logger.error("Certificate file -> '%s' not found!", cert_path)
263
453
  return None
264
454
 
265
455
  with open(file=cert_path, mode="r", encoding="utf-8") as cert_file:
@@ -268,14 +458,16 @@ class OTAC:
268
458
  # Check that we have the pem certificate file - this is what OTAC expects.
269
459
  # If the file content is base64 encoded we will decode it
270
460
  if "BEGIN CERTIFICATE" in cert_content:
271
- logger.info("Certificate file -> %s is not base64 encoded", cert_path)
461
+ logger.debug("Certificate file -> '%s' is not base64 encoded", cert_path)
272
462
  elif "BEGIN CERTIFICATE" in base64.b64decode(
273
463
  cert_content, validate=True
274
464
  ).decode("utf-8"):
275
- logger.info("Certificate file -> %s is base64 encoded", cert_path)
465
+ logger.debug("Certificate file -> '%s' is base64 encoded", cert_path)
276
466
  cert_content = base64.b64decode(cert_content, validate=True).decode("utf-8")
277
467
  else:
278
- logger.error("Certificate file -> %s is not in the right format", cert_path)
468
+ logger.error(
469
+ "Certificate file -> '%s' is not in the right format", cert_path
470
+ )
279
471
  return None
280
472
 
281
473
  request_url = (
@@ -287,14 +479,17 @@ class OTAC:
287
479
  + "&permissions="
288
480
  + permissions
289
481
  )
290
- logger.info(
291
- "Putting certificate -> %s on Archive -> %s; calling -> %s",
482
+ logger.debug(
483
+ "Putting certificate -> '%s' on Archive -> '%s'; calling -> %s",
292
484
  cert_path,
293
485
  logical_archive,
294
486
  request_url,
295
487
  )
296
488
  response = requests.put(
297
- url=request_url, data=cert_content, headers=requestHeaders, timeout=None
489
+ url=request_url,
490
+ data=cert_content,
491
+ headers=REQUEST_FORM_HEADERS,
492
+ timeout=None,
298
493
  )
299
494
 
300
495
  if not response.ok:
@@ -302,7 +497,7 @@ class OTAC:
302
497
  '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN'
303
498
  )[0]
304
499
  logger.error(
305
- "Failed to put certificate -> %s on Archive -> %s; error -> %s",
500
+ "Failed to put certificate -> '%s' on Archive -> '%s'; error -> %s",
306
501
  cert_path,
307
502
  logical_archive,
308
503
  message,
@@ -312,19 +507,21 @@ class OTAC:
312
507
 
313
508
  # end method definition
314
509
 
315
- def enable_cert(self, auth_id: str, logical_archive: str, enable: bool = True):
316
- """Enables Certitificate on Archive Center
510
+ def enable_cert(
511
+ self, auth_id: str, logical_archive: str, enable: bool = True
512
+ ) -> bool:
513
+ """Enables Certitificate on Archive Center via SOAP call
317
514
 
318
515
  Args:
319
516
  auth_id (str): Client ID
320
517
  logical_archive (str): Archive ID
321
518
  enable (bool, optional): Enable or Disable certificate. Defaults to True.
322
519
  Returns:
323
- response or None if request fails.
520
+ True if certificate has been activated, False if an error has occured.
324
521
  """
325
522
 
326
523
  if not self._soap_token:
327
- self._soap_login()
524
+ self.authenticate_soap()
328
525
 
329
526
  if enable:
330
527
  enabled: int = 1
@@ -347,15 +544,104 @@ class OTAC:
347
544
  {"key": "CERT_FLAGS", "data": enabled},
348
545
  ],
349
546
  )
350
- return response
547
+ # With SOAP, no response is a good response!
548
+ if not response:
549
+ logger.debug("Archive Center certificate has been activated.")
550
+ return True
551
+ elif response.code == 500:
552
+ logger.error(
553
+ "Failed to activate Archive Center certificate for Client -> %s on Archive -> '%s'!",
554
+ auth_id,
555
+ logical_archive,
556
+ )
557
+ return False
351
558
 
352
559
  except WebFault as exception:
353
560
  logger.error(
354
- "Failed to execute SetCertificateFlags for Client -> %s on Archive -> %s; error -> %s",
561
+ "Failed to execute SetCertificateFlags for Client -> %s on Archive -> '%s'; error -> %s",
355
562
  auth_id,
356
563
  logical_archive,
357
564
  exception,
358
565
  )
359
- return None
566
+ return False
567
+
568
+ # end method definition
569
+
570
+ def enable_certificate(
571
+ self, cert_name: str, cert_type: str, logical_archive: str | None = None
572
+ ) -> dict | None:
573
+ """Enable a certificate via the new REST API (replacing the old SOAP interface)
574
+
575
+ Args:
576
+ cert_name (str): Name of the certificate
577
+ cert_type (str): Type of the certificate
578
+ logical_archive (str, optional): Logical archive name. If empty it is a global certificate
579
+ for all logical archives in Archive Center.
580
+
581
+ Returns:
582
+ dict | None: REST response or None if the request fails
583
+
584
+ Example response:
585
+ {
586
+ 'IDNO': '3',
587
+ 'CERT_NAME': 'SP_otcs-admin-0',
588
+ 'IMPORT_TIMESTAMP': '1714092017',
589
+ 'CERT_TYPE': 'ARC',
590
+ 'ASSIGNED_ARCHIVE': None,
591
+ 'FINGER_PRINT': 'B9F5 AF66 7CE6 C613 2B3C CAEE 96B6 4F79 97BB 5470 ',
592
+ 'ENABLED': True,
593
+ 'CERTIFICATE': '...',
594
+ 'PRIVILEGES': {'read': True, 'create': True, 'update': True, 'delete': True}
595
+ }
596
+ """
597
+
598
+ request_url = (
599
+ self.config()["certUrl"]
600
+ + "?cert_name="
601
+ + cert_name
602
+ + "&cert_type="
603
+ + cert_type
604
+ )
605
+ if logical_archive:
606
+ request_url += "&assigned_archive=" + logical_archive
607
+
608
+ request_header = self.request_json_header()
609
+
610
+ payload = {"ENABLED": True}
611
+
612
+ logger.debug(
613
+ "Enabling certificate -> '%s' of type -> '%s' to Archive Center; calling -> %s",
614
+ cert_name,
615
+ cert_type,
616
+ request_url,
617
+ )
618
+
619
+ retries = 0
620
+ while True:
621
+ response = requests.put(
622
+ url=request_url,
623
+ headers=request_header,
624
+ data=json.dumps(payload),
625
+ timeout=REQUEST_TIMEOUT,
626
+ )
627
+ if response.ok:
628
+ logger.debug(
629
+ "Certificate -> '%s' has been enabled on Archive Center keystore",
630
+ cert_name,
631
+ )
632
+ return self.parse_request_response(response)
633
+ # Check if Session has expired - then re-authenticate and try once more
634
+ elif response.status_code == 401 and retries == 0:
635
+ logger.debug("Session has expired - try to re-authenticate...")
636
+ self.authenticate(revalidate=True)
637
+ retries += 1
638
+ else:
639
+ logger.error(
640
+ "Failed to enable certificate -> '%s' in Archive Center; status -> %s; error -> %s",
641
+ cert_name,
642
+ response.status_code,
643
+ response.text,
644
+ )
645
+ return None
360
646
 
361
647
  # end method definition