From 44691142d4670896631dc83567d70182b35a0750 Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 18:23:23 +0000 Subject: [PATCH 1/9] Refactor API helpers to use Appliance methods Replaces direct calls to discoRequests in all feature modules with the top-level Appliance REST wrapper methods (get, post, patch, put, delete). Adds support for file and multipart uploads via Appliance.post. Deprecation warnings are added for legacy helpers, and the new AGENTS file documents API usage and style guidelines. This change centralizes request logic, improves maintainability, and prepares for future enhancements. --- AGENTS | 17 ++++++++++ tideway/admin.py | 47 +++++++++++++------------- tideway/credentials.py | 45 +++++++++---------------- tideway/data.py | 48 +++++++++++++-------------- tideway/discoRequests.py | 30 ++++++++++++----- tideway/discovery.py | 71 ++++++++++++++++++++-------------------- tideway/events.py | 4 +-- tideway/kerberos.py | 43 +++++++++++++++--------- tideway/knowledge.py | 16 +++++---- tideway/main.py | 55 +++++++++++++++++-------------- tideway/models.py | 37 ++++++++++----------- tideway/security.py | 34 +++++++++---------- tideway/taxonomy.py | 23 +++++++------ tideway/topology.py | 13 ++++---- tideway/vault.py | 5 ++- 15 files changed, 259 insertions(+), 229 deletions(-) create mode 100644 AGENTS diff --git a/AGENTS b/AGENTS new file mode 100644 index 0000000..28ae80e --- /dev/null +++ b/AGENTS @@ -0,0 +1,17 @@ +Repository: Tideway (BMC Discovery API client) + +Mission +- Add or adjust API helpers using the top-level REST wrappers in `tideway/main.py` (`Appliance.get/post/patch/put/delete`). Do not call `requests` or `discoRequests` directly from feature modules, except the explicit non-standard schema endpoints (`/about`, swagger/openapi) which are intentionally direct in `main.py`. +- Preserve parameter handling: set `self.params[...]` before calling a wrapper; the appliance layer resets params after each request. +- Maintain existing deprecation warnings; when adding aliases, warn with `DeprecationWarning` and point to the preferred helper. + +Uploads and special payloads +- Use `Appliance.post(..., files=..., content_type=...)` for multipart uploads (TKU/knowledge, Kerberos keytabs/ccaches). Do not reintroduce `discoRequests` helpers in new code. +- When a response needs a specific MIME type (e.g., licensing CSV/raw), pass `response="application/zip"` or similar via the wrapper. + +Style and safety +- Keep code ASCII-only unless the file already uses Unicode. Keep comments minimal and clarifying, not redundant. +- Do not remove or overwrite user changes; avoid destructive git commands. + +Validation +- Preferred quick check: `python3 -m compileall tideway`. diff --git a/tideway/admin.py b/tideway/admin.py index 89d0132..c984212 100644 --- a/tideway/admin.py +++ b/tideway/admin.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import tideway +import warnings -dr = tideway.discoRequests appliance = tideway.main.Appliance class Admin(appliance): @@ -10,64 +10,67 @@ class Admin(appliance): def baseline(self): '''Get a summary of the appliance status, and details of which baseline checks have passed or failed.''' - response = dr.discoRequest(self, "/admin/baseline") - return response + warnings.warn( + "baseline() is deprecated; use get_admin_baseline instead.", + DeprecationWarning, + ) + return self.get("/admin/baseline") get_admin_baseline = property(baseline) def admin(self): '''Get information about the appliance, like its version and versions of the installed packages.''' - response = dr.discoRequest(self, "/admin/about") - return response + warnings.warn( + "admin() is deprecated; use get_admin_about instead.", + DeprecationWarning, + ) + return self.get("/admin/about") get_admin_about = property(admin) def licensing(self,content_type="text/plain"): '''Get the latest signed licensing report.''' + warnings.warn( + "licensing() is deprecated; use get_admin_licensing* helpers instead.", + DeprecationWarning, + ) if content_type == "csv": - response = dr.discoRequest(self, "/admin/licensing/csv",response="application/zip") + response = self.get("/admin/licensing/csv", response="application/zip") elif content_type == "raw": - response = dr.discoRequest(self, "/admin/licensing/raw",response="application/zip") + response = self.get("/admin/licensing/raw", response="application/zip") else: - response = dr.discoRequest(self, "/admin/licensing",response=content_type) + response = self.get("/admin/licensing", response=content_type) return response def instance(self): '''Get details about the appliance instance.''' - response = dr.discoRequest(self, "/admin/instance") - return response + return self.get("/admin/instance") get_admin_instance = property(instance) def cluster(self): '''Get cluster configuration and status.''' - response = dr.discoRequest(self, "/admin/cluster") - return response + return self.get("/admin/cluster") get_admin_cluster = property(cluster) def organizations(self): '''Get configured organizations.''' - response = dr.discoRequest(self, "/admin/organizations") - return response + return self.get("/admin/organizations") get_admin_organizations = property(organizations) def preferences(self): '''Get global appliance preferences.''' - response = dr.discoRequest(self, "/admin/preferences") - return response + return self.get("/admin/preferences") get_admin_preferences = property(preferences) def builtin_reports(self): '''Get built-in report definitions.''' - response = dr.discoRequest(self, "/admin/builtin_reports") - return response + return self.get("/admin/builtin_reports") get_admin_builtin_reports = property(builtin_reports) def custom_reports(self): '''Get custom report definitions.''' - response = dr.discoRequest(self, "/admin/custom_reports") - return response + return self.get("/admin/custom_reports") get_admin_custom_reports = property(custom_reports) def smtp(self): '''Get SMTP configuration.''' - response = dr.discoRequest(self, "/admin/smtp") - return response + return self.get("/admin/smtp") get_admin_smtp = property(smtp) diff --git a/tideway/credentials.py b/tideway/credentials.py index 84301f6..13f51da 100644 --- a/tideway/credentials.py +++ b/tideway/credentials.py @@ -2,7 +2,6 @@ import tideway -dr = tideway.discoRequests appliance = tideway.main.Appliance class Credentials(appliance): @@ -12,80 +11,68 @@ def get_vault_credential_type(self, group=None, category=None): '''Altnernate API call for /vault/credential_types.''' self.params['group'] = group self.params['category'] = category - req = dr.discoRequest(self, "/vault/credential_types") - return req + return self.get("/vault/credential_types") get_vault_credential_types = property(get_vault_credential_type) def listCredentialTypes(self, group=None, category=None): '''Get a list of all credential types and filter by group and/or category.''' self.params['group'] = group self.params['category'] = category - response = dr.discoRequest(self, "/vault/credential_types") - return response + return self.get("/vault/credential_types") def get_vault_credential_type_name(self, cred_type_name): '''Altnernate API call for /vault/credential_types/cred_type_name.''' - req = dr.discoRequest(self, "/vault/credential_types/{}".format(cred_type_name)) - return req + return self.get("/vault/credential_types/{}".format(cred_type_name)) def credentialType(self, cred_type_name): '''Get the properties of a specific credential type.''' - response = dr.discoRequest(self, "/vault/credential_types/{}".format(cred_type_name)) - return response + return self.get("/vault/credential_types/{}".format(cred_type_name)) def get_vault_credential(self, cred_id=None): '''Altnernate API call for /vault/credentials.''' if cred_id: - req = dr.discoRequest(self, "/vault/credentials/{}".format(cred_id)) + req = self.get("/vault/credentials/{}".format(cred_id)) else: - req = dr.discoRequest(self, "/vault/credentials") + req = self.get("/vault/credentials") return req get_vault_credentials = property(get_vault_credential) def listCredentials(self, cred_id=None): '''Get a list of all credentials.''' if cred_id: - response = dr.discoRequest(self, "/vault/credentials/{}".format(cred_id)) + response = self.get("/vault/credentials/{}".format(cred_id)) else: - response = dr.discoRequest(self, "/vault/credentials") + response = self.get("/vault/credentials") return response def post_vault_credential(self, body): '''Altnernate API call for /vault/credentials.''' - req = dr.discoPost(self, "/vault/credentials", body) - return req + return self.post("/vault/credentials", body) def newCredential(self, body): '''Create a new credential.''' - response = dr.discoPost(self, "/vault/credentials", body) - return response + return self.post("/vault/credentials", body) def delete_vault_credential(self, cred_id): '''Altnernate API call for /vault/credentials.''' - req = dr.discoDelete(self, "/vault/credentials/{}".format(cred_id)) - return req + return self.delete("/vault/credentials/{}".format(cred_id)) def deleteCredential(self, cred_id): '''Delete a credential.''' - response = dr.discoDelete(self, "/vault/credentials/{}".format(cred_id)) - return response + return self.delete("/vault/credentials/{}".format(cred_id)) def patch_vault_credential(self, cred_id, body): '''Altnernate API call for /vault/credentials.''' - req = dr.discoPatch(self, "/vault/credentials/{}".format(cred_id), body) - return req + return self.patch("/vault/credentials/{}".format(cred_id), body) def updateCredential(self, cred_id, body): '''Updates partial resources of a credential. Missing properties are left unchanged.''' - response = dr.discoPatch(self, "/vault/credentials/{}".format(cred_id), body) - return response + return self.patch("/vault/credentials/{}".format(cred_id), body) def put_vault_credential(self, cred_id, body): '''Altnernate API call for /vault/credentials.''' - req = dr.discoPut(self, "/vault/credentials/{}".format(cred_id), body) - return req + return self.put("/vault/credentials/{}".format(cred_id), body) def replaceCredential(self, cred_id, body): '''Replaces a single credential. All required credential properties must be present. Optional properties that are missing will be reset to their defaults.''' - response = dr.discoPut(self, "/vault/credentials/{}".format(cred_id), body) - return response + return self.put("/vault/credentials/{}".format(cred_id), body) diff --git a/tideway/data.py b/tideway/data.py index b86e507..75a31bc 100644 --- a/tideway/data.py +++ b/tideway/data.py @@ -3,8 +3,6 @@ import tideway import warnings import json - -dr = tideway.discoRequests appliance = tideway.main.Appliance class Data(appliance): @@ -44,10 +42,10 @@ def _search_once(self, query, offset=None, results_id=None, format=None, limit=1 try: body = query _ = query["query"] - response = dr.discoPost(self, "/data/search", body) + response = self.post("/data/search", body) except Exception: self.params['query'] = query - response = dr.discoRequest(self, "/data/search") + response = self.get("/data/search") return response def _search_all(self, query, format=None, limit=100, delete=False, record_limit=None, call_limit=None): @@ -116,26 +114,26 @@ def post_data_condition(self, body, offset=None, results_id=None, format=None, l self.params['format'] = format self.params['delete'] = delete self.params['limit'] = limit - response = dr.discoPost(self, "/data/condition", body) + response = self.post("/data/condition", body) return response def post_data_condition_param_values(self, body): '''Get possible parameter values for a condition''' - response = dr.discoPost(self, "/data/condition/param_values", body) + response = self.post("/data/condition/param_values", body) return response def get_data_condition_template(self, template_id=None): '''Get a template or a list of all templates.''' if template_id: - req = dr.discoRequest(self, "/data/condition/templates/{}".format(template_id)) + req = self.get("/data/condition/templates/{}".format(template_id)) else: - req = dr.discoRequest(self, "/data/condition/templates") + req = self.get("/data/condition/templates") return req get_data_condition_templates = property(get_data_condition_template) def post_data_candidate(self, body): '''Alternate API call for POST /data/candidate.''' - response = dr.discoPost(self, "/data/candidate", body) + response = self.post("/data/candidate", body) return response def best_candidate(self, body): @@ -150,7 +148,7 @@ def best_candidate(self, body): def post_data_candidates(self, body): '''Alternate API call for POST /data/candidates.''' - response = dr.discoPost(self, "/data/candidates", body) + response = self.post("/data/candidates", body) return response def top_candidates(self, body): @@ -170,9 +168,9 @@ def get_data_nodes(self, node_id, relationships=False, traverse=None, flags=None self.params['flags'] = flags self.params['attributes'] = attributes if relationships: - response = dr.discoRequest(self, "/data/nodes/{}?relationships=true".format(node_id)) + response = self.get("/data/nodes/{}?relationships=true".format(node_id)) else: - response = dr.discoRequest(self, "/data/nodes/{}".format(node_id)) + response = self.get("/data/nodes/{}".format(node_id)) return response def nodeLookup(self, node_id, relationships=False, traverse=None, flags=None, attributes=None): @@ -194,7 +192,7 @@ def get_data_nodes_graph(self, node_id, focus="software-connected", apply_rules= self.params['focus'] = focus self.params['apply_rules'] = apply_rules self.params['complete'] = complete - response = dr.discoRequest(self, "/data/nodes/{}/graph".format(node_id)) + response = self.get("/data/nodes/{}/graph".format(node_id)) return response def graphNode(self, node_id, focus="software-connected", apply_rules=True): @@ -217,7 +215,7 @@ def get_data_kinds(self, kind, offset=None, results_id=None, format=None, limit self.params['format'] = format self.params['limit'] = limit self.params['delete'] = delete - response = dr.discoRequest(self, "/data/kinds/{}".format(kind)) + response = self.get("/data/kinds/{}".format(kind)) return response def lookupNodeKind(self, kind, offset=None, results_id=None, format=None, limit = 100, delete = False): @@ -237,18 +235,18 @@ def lookupNodeKind(self, kind, offset=None, results_id=None, format=None, limit def partitions(self): '''Get names and ids of partitions.''' - response = dr.discoRequest(self, "/data/partitions") + response = self.get("/data/partitions") return response get_data_partitions = property(partitions) def post_data_partitions(self, body): '''Create a partition.''' - response = dr.discoPost(self, "/data/partitions", body) + response = self.post("/data/partitions", body) return response def post_data_import(self, body): '''Alternate API call for /data/import.''' - response = dr.discoPost(self, "/data/import", body) + response = self.post("/data/import", body) return response def twImport(self, body): @@ -263,7 +261,7 @@ def twImport(self, body): def post_data_write(self, body): '''Alternate API call for /data/write.''' - response = dr.discoPost(self, "/data/write", body) + response = self.post("/data/write", body) return response def twWrite(self, body): @@ -278,12 +276,12 @@ def twWrite(self, body): def get_data_condition_params(self): '''Retrieve the list of available condition parameters.''' - response = dr.discoRequest(self, "/data/condition/params") + response = self.get("/data/condition/params") return response def post_data_import_graph(self, body): '''Import graph data and return the import UUID.''' - response = dr.discoPost(self, "/data/import/graph", body) + response = self.post("/data/import/graph", body) return response def get_data_external_consumer(self, consumer_name=None, path=None): @@ -293,7 +291,7 @@ def get_data_external_consumer(self, consumer_name=None, path=None): endpoint += f"/{consumer_name}" if path: endpoint += f"/{path}" - response = dr.discoRequest(self, endpoint) + response = self.get( endpoint) return response get_data_external_consumers = property(get_data_external_consumer) @@ -304,7 +302,7 @@ def post_data_external_consumer(self, body, consumer_name=None, path=None): endpoint += f"/{consumer_name}" if path: endpoint += f"/{path}" - response = dr.discoPost(self, endpoint, body) + response = self.post( endpoint, body) return response def patch_data_external_consumer(self, consumer_name, body, path=None): @@ -312,7 +310,7 @@ def patch_data_external_consumer(self, consumer_name, body, path=None): endpoint = f"/data/external_consumers/{consumer_name}" if path: endpoint += f"/{path}" - response = dr.discoPatch(self, endpoint, body) + response = self.patch( endpoint, body) return response def delete_data_external_consumer(self, consumer_name, path=None): @@ -320,7 +318,7 @@ def delete_data_external_consumer(self, consumer_name, path=None): endpoint = f"/data/external_consumers/{consumer_name}" if path: endpoint += f"/{path}" - response = dr.discoDelete(self, endpoint) + response = self.delete( endpoint) return response def get_data_kinds_values(self, kind, attribute, offset=None, results_id=None, format=None, limit=100, delete=False): @@ -331,5 +329,5 @@ def get_data_kinds_values(self, kind, attribute, offset=None, results_id=None, f self.params['limit'] = limit self.params['delete'] = delete endpoint = f"/data/kinds/{kind}/values/{attribute}" - response = dr.discoRequest(self, endpoint) + response = self.get( endpoint) return response diff --git a/tideway/discoRequests.py b/tideway/discoRequests.py index a92543a..8f5df1c 100644 --- a/tideway/discoRequests.py +++ b/tideway/discoRequests.py @@ -9,47 +9,59 @@ def url_and_headers(target,token,api_endpoint,response): return url, headers def discoRequest(appliance, api_endpoint, response="application/json"): + """Issue a GET request.""" url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) req = requests.get(url, headers=heads, params=appliance.params.copy(), verify=appliance.verify) appliance.reset_params() return req -def discoPost(appliance, api_endpoint, jsoncode, response="application/json"): +def discoPost(appliance, api_endpoint, jsoncode=None, response="application/json", files=None, data=None, content_type=None): + """Issue a POST request with optional JSON, form data, or files.""" url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) - req = requests.post(url, json=jsoncode, headers=heads, params=appliance.params.copy(), verify=appliance.verify) + if content_type: + heads['Content-type'] = content_type + req = requests.post( + url, + json=jsoncode if files is None else None, + files=files, + data=data, + headers=heads, + params=appliance.params.copy(), + verify=appliance.verify, + ) appliance.reset_params() return req def filePost(appliance, api_endpoint, file, response="text/html"): - url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) + """Backward compatible helper for file uploads.""" with open(file, 'rb') as f: files = {"file": f} - req = requests.post(url, files=files, headers=heads, params=appliance.params.copy(), verify=appliance.verify) - appliance.reset_params() + req = discoPost(appliance, api_endpoint, files=files, response=response) return req def keytabPost(appliance, api_endpoint, file, username, response="application/json", content_type="multipart/form-data"): - url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) - heads['Content-type'] = content_type + """Backward compatible helper for Kerberos uploads.""" with open(file, 'rb') as f: form_data = {"keytab": f, "username": username} - req = requests.post(url, files=form_data, headers=heads, params=appliance.params.copy(), verify=appliance.verify) - appliance.reset_params() + req = discoPost(appliance, api_endpoint, files=form_data, response=response, content_type=content_type) return req def discoPatch(appliance, api_endpoint, jsoncode, response="application/json"): + """Issue a PATCH request.""" url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) req = requests.patch(url, json=jsoncode, headers=heads, params=appliance.params.copy(), verify=appliance.verify) appliance.reset_params() return req def discoPut(appliance, api_endpoint, jsoncode, response="application/json"): + """Issue a PUT request.""" url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) req = requests.put(url, json=jsoncode, headers=heads, params=appliance.params.copy(), verify=appliance.verify) appliance.reset_params() return req def discoDelete(appliance, api_endpoint, response="application/json"): + """Issue a DELETE request.""" url, heads = url_and_headers(appliance.url, appliance.token, api_endpoint, response) req = requests.delete(url, headers=heads, params=appliance.params.copy(), verify=appliance.verify) appliance.reset_params() diff --git a/tideway/discovery.py b/tideway/discovery.py index dc0b6e1..bf7479a 100644 --- a/tideway/discovery.py +++ b/tideway/discovery.py @@ -2,7 +2,6 @@ import tideway -dr = tideway.discoRequests appliance = tideway.main.Appliance class Discovery(appliance): @@ -10,20 +9,20 @@ class Discovery(appliance): def getDiscoveryStatus(self): '''Get the current status of the discovery process. JSON Output.''' - response = dr.discoRequest(self, "/discovery") + response = self.get("/discovery") return response get_discovery = property(getDiscoveryStatus) def patch_discovery(self, body): '''Alternate API call for PATCH /discovery.''' - response = dr.discoPatch(self, "/discovery", body) + response = self.patch("/discovery", body) return response def setDiscoveryStatus(self, body): ''' Set the Discovery status using JSON format. ''' - response = dr.discoPatch(self, "/discovery", body) + response = self.patch("/discovery", body) return response.ok def getApiProviderMetadata(self): @@ -33,7 +32,7 @@ def getApiProviderMetadata(self): /discovery/runs and /vault/credentials endpoints. Support for new API providers is available in TKU knowledge updates. ''' - response = dr.discoRequest(self, "/discovery/api_provider_metadata") + response = self.get("/discovery/api_provider_metadata") return response get_discovery_api_provider_metadata = property(getApiProviderMetadata) @@ -42,71 +41,71 @@ def getDiscoveryCloudMetaData(self): Get metadata for the cloud providers currently supported by BMC Discovery. ''' - response = dr.discoRequest(self, "/discovery/cloud_metadata") + response = self.get("/discovery/cloud_metadata") return response get_discovery_api_cloud_metadata = property(getDiscoveryCloudMetaData) def get_discovery_exclude(self, exclude_id=None): '''Get a list of all excludes or specific.''' if exclude_id: - req = dr.discoRequest(self, "/discovery/excludes/{}".format(exclude_id)) + req = self.get("/discovery/excludes/{}".format(exclude_id)) else: - req = dr.discoRequest(self, "/discovery/excludes") + req = self.get("/discovery/excludes") return req get_discovery_excludes = property(get_discovery_exclude) def post_discovery_exclude(self, body): '''Create an exclude.''' - response = dr.discoPost(self, "/discovery/excludes", body) + response = self.post("/discovery/excludes", body) return response def delete_discovery_exclude(self, exclude_id): '''Delete an exclude.''' - response = dr.discoDelete(self, "/discovery/excludes/{}".format(exclude_id)) + response = self.delete("/discovery/excludes/{}".format(exclude_id)) return response def patch_discovery_exclude(self, exclude_id, body): '''Update an exclude.''' - response = dr.discoPatch(self, "/discovery/excludes/{}".format(exclude_id), body) + response = self.patch("/discovery/excludes/{}".format(exclude_id), body) return response def get_discovery_run(self, run_id=None): '''Get details of all or specific currently processing discovery runs.''' if run_id: - req = dr.discoRequest(self, "/discovery/runs/{}".format(run_id)) + req = self.get("/discovery/runs/{}".format(run_id)) else: - req = dr.discoRequest(self, "/discovery/runs") + req = self.get("/discovery/runs") return req get_discovery_runs = property(get_discovery_run) def getDiscoveryRuns(self): '''Get details of all currently processing discovery runs.''' - response = dr.discoRequest(self, "/discovery/runs") + response = self.get("/discovery/runs") return response def getDiscoveryRun(self, runid): '''Get details of specific currently processing discovery run.''' - response = dr.discoRequest(self, "/discovery/runs/{}".format(runid)) + response = self.get("/discovery/runs/{}".format(runid)) return response def post_discovery_run(self, body): '''Alternative API call for POST /discovery/runs.''' - response = dr.discoPost(self, "/discovery/runs", body) + response = self.post("/discovery/runs", body) return response def discoveryRun(self, body): '''Create a new snapshot discovery run.''' - response = dr.discoPost(self, "/discovery/runs", body) + response = self.post("/discovery/runs", body) return response def patch_discovery_run(self, run_id, body): '''Alternate API call for PATCH /discovery/runs.''' - response = dr.discoPatch(self, "/discovery/runs/{}".format(run_id), body) + response = self.patch("/discovery/runs/{}".format(run_id), body) return response def updateDiscoveryRun(self, runid, body): '''Update the state of a specific discovery run.''' - response = dr.discoPatch(self, "/discovery/runs/{}".format(runid), body) + response = self.patch("/discovery/runs/{}".format(runid), body) return response def get_discovery_run_results(self, run_id, result=None, offset=None, results_id=None, format=None, limit = 100, delete = False): @@ -117,14 +116,14 @@ def get_discovery_run_results(self, run_id, result=None, offset=None, results_id self.params['format'] = format self.params['limit'] = limit self.params['delete'] = delete - response = dr.discoRequest(self, "/discovery/runs/{}/results/{}".format(run_id,result)) + response = self.get("/discovery/runs/{}/results/{}".format(run_id,result)) else: - response = dr.discoRequest(self, "/discovery/runs/{}/results".format(run_id)) + response = self.get("/discovery/runs/{}/results".format(run_id)) return response def getDiscoveryRunResults(self, runid): '''Get a summary of the results from scanning all endpoints in the run, partitioned by result type.''' - response = dr.discoRequest(self, "/discovery/runs/{}/results".format(runid)) + response = self.get("/discovery/runs/{}/results".format(runid)) return response def getDiscoveryRunResult(self, runid, result="Success", offset=None, results_id=None, format=None, limit = 100, delete = False): @@ -134,7 +133,7 @@ def getDiscoveryRunResult(self, runid, result="Success", offset=None, results_id self.params['format'] = format self.params['limit'] = limit self.params['delete'] = delete - response = dr.discoRequest(self, "/discovery/runs/{}/results/{}".format(runid,result)) + response = self.get("/discovery/runs/{}/results/{}".format(runid,result)) return response def get_discovery_run_inferred(self, run_id, inferred_kind, offset=None, results_id=None, format=None, limit = 100, delete = False): @@ -145,14 +144,14 @@ def get_discovery_run_inferred(self, run_id, inferred_kind, offset=None, results self.params['format'] = format self.params['limit'] = limit self.params['delete'] = delete - response = dr.discoRequest(self, "/discovery/runs/{}/inferred/{}".format(run_id,inferred_kind)) + response = self.get("/discovery/runs/{}/inferred/{}".format(run_id,inferred_kind)) else: - response = dr.discoRequest(self, "/discovery/runs/{}/inferred".format(run_id)) + response = self.get("/discovery/runs/{}/inferred".format(run_id)) return response def getDiscoveryRunInferred(self, runid): '''Get a summary of all inferred devices from a discovery run, partitioned by device type.''' - response = dr.discoRequest(self, "/discovery/runs/{}/inferred".format(runid)) + response = self.get("/discovery/runs/{}/inferred".format(runid)) return response def getDiscoveryRunInferredKind(self, runid, inferred_kind, offset=None, results_id=None, format=None, limit = 100, delete = False): @@ -162,48 +161,48 @@ def getDiscoveryRunInferredKind(self, runid, inferred_kind, offset=None, results self.params['format'] = format self.params['limit'] = limit self.params['delete'] = delete - response = dr.discoRequest(self, "/discovery/runs/{}/inferred/{}".format(runid,inferred_kind)) + response = self.get("/discovery/runs/{}/inferred/{}".format(runid,inferred_kind)) return response def get_discovery_run_schedule(self, run_id=None): '''Get a list of all scheduled runs or specific.''' if run_id: - req = dr.discoRequest(self, "/discovery/runs/scheduled/{}".format(run_id)) + req = self.get("/discovery/runs/scheduled/{}".format(run_id)) else: - req = dr.discoRequest(self, "/discovery/runs/scheduled") + req = self.get("/discovery/runs/scheduled") return req get_discovery_run_schedules = property(get_discovery_run_schedule) def post_discovery_run_schedule(self, body): '''Add a new scheduled run.''' - response = dr.discoPost(self, "/discovery/runs/scheduled", body) + response = self.post("/discovery/runs/scheduled", body) return response def delete_discovery_run_schedule(self, run_id): '''Delete a specific scheduled discovery run.''' - response = dr.discoDelete(self, "/discovery/runs/scheduled/{}".format(run_id)) + response = self.delete("/discovery/runs/scheduled/{}".format(run_id)) return response def patch_discovery_run_schedule(self, run_id, body): '''Update the parameters of a specific scheduled discovery run.''' - response = dr.discoPatch(self, "/discovery/runs/scheduled/{}".format(run_id), body) + response = self.patch("/discovery/runs/scheduled/{}".format(run_id), body) return response def get_discovery_outpost(self, outpost_id=None): '''Get all configured Outposts or a specific one.''' if outpost_id: - req = dr.discoRequest(self, "/discovery/outposts/{}".format(outpost_id)) + req = self.get("/discovery/outposts/{}".format(outpost_id)) else: - req = dr.discoRequest(self, "/discovery/outposts") + req = self.get("/discovery/outposts") return req get_discovery_outposts = property(get_discovery_outpost) def post_discovery_outpost(self, body): '''Register a new Outpost.''' - response = dr.discoPost(self, "/discovery/outposts", body) + response = self.post("/discovery/outposts", body) return response def delete_discovery_outpost(self, outpost_id): '''Delete an Outpost.''' - response = dr.discoDelete(self, "/discovery/outposts/{}".format(outpost_id)) + response = self.delete("/discovery/outposts/{}".format(outpost_id)) return response diff --git a/tideway/events.py b/tideway/events.py index efba9a2..faf2160 100644 --- a/tideway/events.py +++ b/tideway/events.py @@ -2,8 +2,6 @@ import tideway import warnings - -dr = tideway.discoRequests appliance = tideway.main.Appliance class Events(appliance): @@ -11,7 +9,7 @@ class Events(appliance): def post_events(self, body): '''An alternate API call for POST /events''' - response = dr.discoPost(self, "/events", body) + response = self.post("/events", body) return response def status(self, body): diff --git a/tideway/kerberos.py b/tideway/kerberos.py index 85aacba..339753a 100644 --- a/tideway/kerberos.py +++ b/tideway/kerberos.py @@ -2,7 +2,6 @@ import tideway -dr = tideway.discoRequests appliance = tideway.main.Appliance class Kerberos(appliance): @@ -11,58 +10,70 @@ class Kerberos(appliance): def get_vault_kerberos_realm(self, realm_name=None): '''Retrieve all or specific realm.''' if realm_name: - req = dr.discoRequest(self, "/vault/kerberos/realms/{}".format(realm_name)) + req = self.get("/vault/kerberos/realms/{}".format(realm_name)) else: - req = dr.discoRequest(self, "/vault/kerberos/realms") + req = self.get("/vault/kerberos/realms") return req get_vault_kerberos_realms = property(get_vault_kerberos_realm) def delete_vault_kerberos_realm(self, realm_name): '''Delete a realm.''' - req = dr.discoDelete(self, "/vault/kerberos/realms/{}".format(realm_name)) + req = self.delete("/vault/kerberos/realms/{}".format(realm_name)) return req def patch_vault_kerberos_realm(self, realm_name, body): '''Update a Kerberos realm.''' - req = dr.discoPatch(self, "/vault/kerberos/realms/{}".format(realm_name), body) + req = self.patch("/vault/kerberos/realms/{}".format(realm_name), body) return req def post_vault_kerberos_realm(self, realm_name, body, test=False): '''Create a realm and Test user credentials by attempting to acquire a new Kerberos Ticket Granting Ticket (TGT)''' - req = dr.discoPost(self, "/vault/kerberos/realms/{}".format(realm_name), body) + req = self.post("/vault/kerberos/realms/{}".format(realm_name), body) if test: - req = dr.discoPost(self, "/vault/kerberos/realms/{}/test".format(realm_name), body) + req = self.post("/vault/kerberos/realms/{}/test".format(realm_name), body) return req def get_vault_kerberos_keytabs(self, realm_name): '''List users with a Kerberos keytab file''' - req = dr.discoRequest(self, "/vault/kerberos/realms/{}/keytabs".format(realm_name)) + req = self.get("/vault/kerberos/realms/{}/keytabs".format(realm_name)) return req def post_vault_kerberos_keytab(self, realm_name, username, keytab): '''Upload a Kerberos keytab file''' # Not Tested - req = dr.keytabPost(self, "/vault/kerberos/realms/{}/keytabs".format(realm_name), keytab, username) - return req + with open(keytab, "rb") as kf: + files = {"keytab": kf, "username": (None, username)} + return self.post( + "/vault/kerberos/realms/{}/keytabs".format(realm_name), + files=files, + response="application/json", + content_type="multipart/form-data", + ) def delete_vault_kerberos_keytab(self, realm_name, username): '''Delete a keytab file''' # Not Tested - req = dr.discoDelete(self, "/vault/kerberos/realms/{}/keytabs/{}".format(realm_name, username)) + req = self.delete("/vault/kerberos/realms/{}/keytabs/{}".format(realm_name, username)) return req def get_vault_kerberos_ccaches(self, realm_name): '''List users with a Kerberos credential cache file.''' - req = dr.discoRequest(self, "/vault/kerberos/realms/{}/ccaches".format(realm_name)) + req = self.get("/vault/kerberos/realms/{}/ccaches".format(realm_name)) return req def post_vault_kerberos_ccache(self, realm_name, username, ccache): '''Upload a Kerberos credential cache file''' # Not Tested - req = dr.keytabPost(self, "/vault/kerberos/realms/{}/ccaches".format(realm_name), ccache, username) - return req + with open(ccache, "rb") as cache_file: + files = {"keytab": cache_file, "username": (None, username)} + return self.post( + "/vault/kerberos/realms/{}/ccaches".format(realm_name), + files=files, + response="application/json", + content_type="multipart/form-data", + ) def delete_vault_kerberos_ccache(self, realm_name, username): '''Delete a credential cache file''' - req = dr.discoDelete(self, "/vault/kerberos/realms/{}/ccaches/{}".format(realm_name, username)) - return req \ No newline at end of file + req = self.delete("/vault/kerberos/realms/{}/ccaches/{}".format(realm_name, username)) + return req diff --git a/tideway/knowledge.py b/tideway/knowledge.py index 17e1762..3429bd9 100644 --- a/tideway/knowledge.py +++ b/tideway/knowledge.py @@ -3,7 +3,6 @@ import tideway import warnings -dr = tideway.discoRequests appliance = tideway.main.Appliance class Knowledge(appliance): @@ -11,7 +10,7 @@ class Knowledge(appliance): def get_knowledge(self): '''Get the current state of the appliance's knowledge, including TKU versions.''' - return dr.discoRequest(self, "/knowledge") + return self.get("/knowledge") def getKnowledgeManagement(self): '''Get the current state of the appliance's knowledge, including TKU versions.''' @@ -32,7 +31,7 @@ def getUploadStatus(self): def get_knowledge_status(self): '''Get the current state of a knowledge upload.''' - return dr.discoRequest(self, "/knowledge/status") + return self.get("/knowledge/status") get_knowledge_status_property = property(get_knowledge_status) @@ -40,8 +39,13 @@ def post_knowledge(self, filename, file, activate=True, allow_restart=False): '''Alternate API call for POST /knowledge/filename''' self.params['activate'] = activate self.params['allow_restart'] = allow_restart - response = dr.filePost(self, "/knowledge/{}".format(filename), file) - return response + with open(file, "rb") as upload: + files = {"file": upload} + return self.post( + "/knowledge/{}".format(filename), + files=files, + response="text/html", + ) def uploadKnowledge(self, filename, file, activate=True, allow_restart=False): '''Upload a TKU or pattern module to the appliance.''' @@ -54,6 +58,6 @@ def uploadKnowledge(self, filename, file, activate=True, allow_restart=False): def getKnowledgeTriggerPatterns(self, lookup_data_sources=None): '''Get a list of all knowledge trigger patterns.''' self.params['lookup_data_sources'] = lookup_data_sources - response = dr.discoRequest(self, "/knowledge/trigger_patterns") + response = self.get("/knowledge/trigger_patterns") return response get_knowledge_trigger_patterns = property(getKnowledgeTriggerPatterns) diff --git a/tideway/main.py b/tideway/main.py index f640a96..3f846ec 100644 --- a/tideway/main.py +++ b/tideway/main.py @@ -29,33 +29,41 @@ def reset_params(self): self.params['limit'] = self.default_limit self.params['delete'] = self.default_delete - def get(self,endpoint): + def get(self, endpoint, response="application/json"): '''Request any endpoint.''' - req = dr.discoRequest(self,endpoint) + req = dr.discoRequest(self, endpoint, response=response) self.reset_params() return req - def post(self,endpoint,body): + def post(self, endpoint, body=None, response="application/json", files=None, data=None, content_type=None): '''Post any endpoint.''' - req = dr.discoPost(self, endpoint, body) + req = dr.discoPost( + self, + endpoint, + body, + response=response, + files=files, + data=data, + content_type=content_type, + ) self.reset_params() return req - def delete(self,endpoint): + def delete(self, endpoint, response="application/json"): '''Delete any endpoint.''' - req = dr.discoDelete(self, endpoint) + req = dr.discoDelete(self, endpoint, response=response) self.reset_params() return req - def patch(self,endpoint,body): + def patch(self, endpoint, body, response="application/json"): '''Patch any endpoint.''' - req = dr.discoPatch(self, endpoint, body) + req = dr.discoPatch(self, endpoint, body, response=response) self.reset_params() return req - def put(self,endpoint,body): + def put(self, endpoint, body, response="application/json"): '''Update any endpoint.''' - req = dr.discoPut(self, endpoint, body) + req = dr.discoPut(self, endpoint, body, response=response) self.reset_params() return req @@ -163,8 +171,7 @@ def api_paths(self, path=None): @property def get_admin_baseline(self): '''Alternate API call for baseline.''' - response = dr.discoRequest(self, "/admin/baseline") - return response + return self.get("/admin/baseline") def baseline(self): '''Get a summary of the appliance status, and details of which baseline checks have passed or failed.''' @@ -177,35 +184,35 @@ def baseline(self): @property def get_admin_about(self): '''Alternate API call for /admin/about.''' - response = dr.discoRequest(self, "/admin/about") - return response + return self.get("/admin/about") @property def get_admin_licensing(self): '''Alternate API call for licensing report.''' - response = dr.discoRequest(self, "/admin/licensing",response="text/plain") - return response + return self.get("/admin/licensing", response="text/plain") @property def get_admin_licensing_csv(self): '''Alternate API call for licensing report CSV.''' - response = dr.discoRequest(self, "/admin/licensing/csv",response="application/zip") - return response + return self.get("/admin/licensing/csv", response="application/zip") @property def get_admin_licensing_raw(self): '''Alternate API call for licensing report raw.''' - response = dr.discoRequest(self, "/admin/licensing/raw",response="application/zip") - return response + return self.get("/admin/licensing/raw", response="application/zip") def licensing(self,content_type="text/plain"): '''Get the latest signed licensing report.''' + warnings.warn( + "licensing() is deprecated; use get_admin_licensing or the CSV/RAW helpers instead.", + DeprecationWarning, + ) if content_type == "csv": - response = dr.discoRequest(self, "/admin/licensing/csv",response="application/zip") + response = self.get("/admin/licensing/csv", response="application/zip") elif content_type == "raw": - response = dr.discoRequest(self, "/admin/licensing/raw",response="application/zip") + response = self.get("/admin/licensing/raw", response="application/zip") else: - response = dr.discoRequest(self, "/admin/licensing",response=content_type) + response = self.get("/admin/licensing", response=content_type) return response @property @@ -220,4 +227,4 @@ def help(*args): endpoints.docs(args[1]) else: endpoints.docs() - #print("\n") \ No newline at end of file + #print("\n") diff --git a/tideway/models.py b/tideway/models.py index a78fb91..286bf3c 100644 --- a/tideway/models.py +++ b/tideway/models.py @@ -2,7 +2,6 @@ import tideway -dr = tideway.discoRequests appliance = tideway.main.Appliance class Models(appliance): @@ -30,45 +29,45 @@ def get_model(self,name=None,type=None,kind=None,published=None,review_suggested self.params['results_id'] = results_id if delete: self.params['delete'] = delete - response = dr.discoRequest(self, "/models") + response = self.get("/models") return response get_models = property(get_model) def post_model(self, body): '''Create a new model.''' - response = dr.discoPost(self, "/models", body) + response = self.post("/models", body) return response def post_model_multi(self, body): '''Manipulate multiple models in a single request.''' - response = dr.discoPost(self, "/models/multi", body) + response = self.post("/models/multi", body) return response def delete_model(self, key): '''Delete a model.''' - response = dr.discoDelete(self, "/models/{}".format(key)) + response = self.delete("/models/{}".format(key)) return response def get_model_key(self, key): '''Get model definition for the specified key.''' - req = dr.discoRequest(self, "/models/{}".format(key)) + req = self.get("/models/{}".format(key)) return req def patch_model(self, key, body): '''Modify a model.''' - response = dr.discoPatch(self, "/models/{}".format(key), body) + response = self.patch("/models/{}".format(key), body) return response def get_model_topology(self, key, attributes=None): '''Get topology for the model definition specified by key.''' if attributes: self.params['attributes']=attributes - req = dr.discoRequest(self, "/models/{}/topology".format(key)) + req = self.get("/models/{}/topology".format(key)) return req def get_model_nodecount(self, key): '''Get node count for the model definition specified by key.''' - req = dr.discoRequest(self, "/models/{}/nodecount".format(key)) + req = self.get("/models/{}/nodecount".format(key)) return req def get_model_nodes(self, key, format=None, limit=100, results_id=None, delete=False, kind=None): @@ -80,38 +79,38 @@ def get_model_nodes(self, key, format=None, limit=100, results_id=None, delete=F self.params['limit'] = limit self.params['delete'] = delete if kind: - response = dr.discoRequest(self, "/models/{}/nodes/{}".format(key,kind)) + response = self.get("/models/{}/nodes/{}".format(key,kind)) else: - response = dr.discoRequest(self, "/models/{}/nodes".format(key)) + response = self.get("/models/{}/nodes".format(key)) return response def delete_model_by_node_id(self, node_id): '''Delete a model.''' - response = dr.discoDelete(self, "/models/by_node_id/{}".format(node_id)) + response = self.delete("/models/by_node_id/{}".format(node_id)) return response def get_model_by_node_id(self, node_id, expand_related=None): '''Get model definition for the specified node id.''' if expand_related: self.params['expand_related'] = expand_related - response = dr.discoRequest(self, "/models/by_node_id/{}".format(node_id)) + response = self.get("/models/by_node_id/{}".format(node_id)) return response def patch_model_by_node_id(self, node_id, body): '''Modify a model.''' - response = dr.discoPatch(self, "/models/by_node_id/{}".format(node_id), body) + response = self.patch("/models/by_node_id/{}".format(node_id), body) return response def get_topology_by_node_id(self, node_id, attributes=None): '''Get topology for the model definition specified by node id.''' if attributes: self.params['attributes']=attributes - response = dr.discoRequest(self, "/models/by_node_id/{}/topology".format(node_id)) + response = self.get("/models/by_node_id/{}/topology".format(node_id)) return response def get_nodecount_by_node_id(self, node_id): '''Get node count for the model definition specified by node id.''' - response = dr.discoRequest(self, "/models/by_node_id/{}/nodecount".format(node_id)) + response = self.get("/models/by_node_id/{}/nodecount".format(node_id)) return response def get_nodes_by_node_id(self, node_id, format=None, limit=100, results_id=None, delete=False, kind=None): @@ -123,8 +122,8 @@ def get_nodes_by_node_id(self, node_id, format=None, limit=100, results_id=None, self.params['limit'] = limit self.params['delete'] = delete if kind: - response = dr.discoRequest(self, "/models/by_node_id/{}/nodes/{}".format(node_id,kind)) + response = self.get("/models/by_node_id/{}/nodes/{}".format(node_id,kind)) else: - response = dr.discoRequest(self, "/models/by_node_id/{}/nodes".format(node_id)) + response = self.get("/models/by_node_id/{}/nodes".format(node_id)) return response - \ No newline at end of file + diff --git a/tideway/security.py b/tideway/security.py index 22ae76f..bc177c9 100644 --- a/tideway/security.py +++ b/tideway/security.py @@ -2,8 +2,6 @@ import tideway -# Request helpers -dr = tideway.discoRequests appliance = tideway.main.Appliance class Security(appliance): @@ -11,62 +9,62 @@ class Security(appliance): def get_security_ldap(self): '''Retrieve LDAP configuration.''' - return dr.discoRequest(self, "/security/ldap") + return self.get("/security/ldap") get_security_ldaps = property(get_security_ldap) def put_security_ldap(self, body): '''Replace LDAP configuration.''' - return dr.discoPut(self, "/security/ldap", body) + return self.put("/security/ldap", body) def patch_security_ldap(self, body): '''Update LDAP configuration.''' - return dr.discoPatch(self, "/security/ldap", body) + return self.patch("/security/ldap", body) def get_security_group(self, group_name=None): '''Retrieve all groups or a specific group.''' if group_name: - return dr.discoRequest(self, f"/security/groups/{group_name}") - return dr.discoRequest(self, "/security/groups") + return self.get( f"/security/groups/{group_name}") + return self.get("/security/groups") get_security_groups = property(get_security_group) def post_security_group(self, body): '''Create a new group.''' - return dr.discoPost(self, "/security/groups", body) + return self.post("/security/groups", body) def patch_security_group(self, group_name, body): '''Update a group.''' - return dr.discoPatch(self, f"/security/groups/{group_name}", body) + return self.patch( f"/security/groups/{group_name}", body) def delete_security_group(self, group_name): '''Delete a group.''' - return dr.discoDelete(self, f"/security/groups/{group_name}") + return self.delete( f"/security/groups/{group_name}") def get_security_permission(self, permission=None): '''Retrieve permissions or a specific permission set.''' if permission: - return dr.discoRequest(self, f"/security/permissions/{permission}") - return dr.discoRequest(self, "/security/permissions") + return self.get( f"/security/permissions/{permission}") + return self.get("/security/permissions") get_security_permissions = property(get_security_permission) def get_security_user(self, username=None): '''Retrieve users or a specific user.''' if username: - return dr.discoRequest(self, f"/security/users/{username}") - return dr.discoRequest(self, "/security/users") + return self.get( f"/security/users/{username}") + return self.get("/security/users") get_security_users = property(get_security_user) def post_security_user(self, body): '''Create a new user.''' - return dr.discoPost(self, "/security/users", body) + return self.post("/security/users", body) def patch_security_user(self, username, body): '''Update a user.''' - return dr.discoPatch(self, f"/security/users/{username}", body) + return self.patch( f"/security/users/{username}", body) def delete_security_user(self, username): '''Delete a user.''' - return dr.discoDelete(self, f"/security/users/{username}") + return self.delete( f"/security/users/{username}") def post_security_token(self, body): '''Retrieve an authentication token for a user.''' - return dr.discoPost(self, "/security/token", body) + return self.post("/security/token", body) diff --git a/tideway/taxonomy.py b/tideway/taxonomy.py index 6ac8848..43c928c 100644 --- a/tideway/taxonomy.py +++ b/tideway/taxonomy.py @@ -2,7 +2,6 @@ import tideway -dr = tideway.discoRequests appliance = tideway.main.Appliance class Taxonomy(appliance): @@ -11,13 +10,13 @@ class Taxonomy(appliance): @property def get_taxonomy_sections(self): '''Get list of taxonomy model sections.''' - req = dr.discoRequest(self, "/taxonomy/sections") + req = self.get("/taxonomy/sections") return req @property def get_taxonomy_locales(self): '''Get list of known taxonomy locales.''' - req = dr.discoRequest(self, "/taxonomy/locales") + req = self.get("/taxonomy/locales") return req def get_taxonomy_nodekind(self, format=None, section=None, locale=None, kind=None, fieldlists=False): @@ -26,21 +25,21 @@ def get_taxonomy_nodekind(self, format=None, section=None, locale=None, kind=Non self.params['format']=format self.params['section']=section self.params['locale']=locale - req = dr.discoRequest(self, "/taxonomy/nodekinds") + req = self.get("/taxonomy/nodekinds") elif kind: self.params['locale']=locale if fieldlists: - req = dr.discoRequest(self, "/taxonomy/nodekinds/{}/fieldlists".format(kind)) + req = self.get("/taxonomy/nodekinds/{}/fieldlists".format(kind)) else: - req = dr.discoRequest(self, "/taxonomy/nodekinds/{}".format(kind)) + req = self.get("/taxonomy/nodekinds/{}".format(kind)) else: - req = dr.discoRequest(self, "/taxonomy/nodekinds") + req = self.get("/taxonomy/nodekinds") return req get_taxonomy_nodekinds = property(get_taxonomy_nodekind) def get_taxonomy_nodekind_fieldlist(self, kind, fieldlist): '''Get list of fields for a node kind field list.''' - req = dr.discoRequest(self, "/taxonomy/nodekinds/{}/fieldlists/{}".format(kind,fieldlist)) + req = self.get("/taxonomy/nodekinds/{}/fieldlists/{}".format(kind,fieldlist)) return req def get_taxonomy_relkind(self, format=None, locale=None, kind=None): @@ -48,11 +47,11 @@ def get_taxonomy_relkind(self, format=None, locale=None, kind=None): if format: self.params['format']=format self.params['locale']=locale - req = dr.discoRequest(self, "/taxonomy/relkinds") + req = self.get("/taxonomy/relkinds") elif kind: self.params['locale']=locale - req = dr.discoRequest(self, "/taxonomy/relkinds/{}".format(kind)) + req = self.get("/taxonomy/relkinds/{}".format(kind)) else: - req = dr.discoRequest(self, "/taxonomy/relkinds") + req = self.get("/taxonomy/relkinds") return req - get_taxonomy_relkinds = property(get_taxonomy_relkind) \ No newline at end of file + get_taxonomy_relkinds = property(get_taxonomy_relkind) diff --git a/tideway/topology.py b/tideway/topology.py index 02d69ae..0468ca8 100644 --- a/tideway/topology.py +++ b/tideway/topology.py @@ -3,7 +3,6 @@ import tideway import warnings -dr = tideway.discoRequests appliance = tideway.main.Appliance class Topology(appliance): @@ -14,7 +13,7 @@ def get_data_nodes_graph(self, node_id, focus="software-connected", apply_rules= self.params['focus'] = focus self.params['apply_rules'] = apply_rules self.params['complete'] = complete - response = dr.discoRequest(self, "/data/nodes/{}/graph".format(node_id)) + response = self.get("/data/nodes/{}/graph".format(node_id)) return response def graphNode(self, node_id, focus="software-connected", apply_rules=True): @@ -35,7 +34,7 @@ def graphNode(self, node_id, focus="software-connected", apply_rules=True): def post_topology_nodes(self, body): '''Alternate API call for POST /topology/nodes.''' - response = dr.discoPost(self, "/topology/nodes", body) + response = self.post("/topology/nodes", body) return response def getNodes(self, body): @@ -48,7 +47,7 @@ def getNodes(self, body): def post_topology_nodes_kinds(self, body): '''Alternate API call for POST /topology/nodes/kinds.''' - response = dr.discoPost(self, "/topology/nodes/kinds", body) + response = self.post("/topology/nodes/kinds", body) return response def getNodeKinds(self, body): @@ -71,12 +70,12 @@ def visualizationState(self): "visualizationState() is deprecated; use get_topology_viz_state instead.", DeprecationWarning, ) - return dr.discoRequest(self, "/topology/visualization_state") + return self.get("/topology/visualization_state") get_topology_viz_state = property(visualizationState) def patch_topology_viz_state(self, body): '''Alternate API call for PATCH /topology/visualization_state''' - response = dr.discoPatch(self, "/topology/visualization_state", body) + response = self.patch("/topology/visualization_state", body) return response def updateVizState(self, body): @@ -92,7 +91,7 @@ def updateVizState(self, body): def put_topology_viz_state(self, body): '''Alternate API call for PUT /topology/visualization_state''' - response = dr.discoPut(self, "/topology/visualization_state", body) + response = self.put("/topology/visualization_state", body) return response def replaceVizState(self, body): diff --git a/tideway/vault.py b/tideway/vault.py index 91a889d..d62e7c7 100644 --- a/tideway/vault.py +++ b/tideway/vault.py @@ -3,7 +3,6 @@ import tideway import warnings -dr = tideway.discoRequests appliance = tideway.main.Appliance class Vault(appliance): @@ -11,7 +10,7 @@ class Vault(appliance): def get_vault(self): '''Get details of the state of the vault.''' - return dr.discoRequest(self, "/vault") + return self.get("/vault") def getVault(self): '''Get details of the state of the vault.''' @@ -24,7 +23,7 @@ def getVault(self): def patch_vault(self, body): '''Alternate API call for PATCH /vault''' - response = dr.discoPatch(self, "/vault", body) + response = self.patch("/vault", body) return response def updateVault(self, body): From ab084ebad0095ec992de3a1bda4d177d3ff587b6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:33:29 +0000 Subject: [PATCH 2/9] Add tabulate to install_requires (#35) * Initial plan * Add tabulate to install_requires in setup.py Co-authored-by: codefitz <4305115+codefitz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codefitz <4305115+codefitz@users.noreply.github.com> --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3c4014d..e385ca1 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ packages=setuptools.find_packages(exclude=["tests*"]), install_requires=[ "requests", + "tabulate", ], classifiers=[ "Programming Language :: Python :: 3", From bfcdd4ee393b54a9fa2216f8e83f4a9f1eff7331 Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 18:46:30 +0000 Subject: [PATCH 3/9] Remove deprecated helper functions and update docs Deprecated helper aliases and functions have been removed from all modules, including admin, data, events, knowledge, main, topology, and vault. All API calls are now routed through the top-level REST wrappers. Documentation and examples have been updated to reflect the leaner API surface, and references to deprecated methods have been eliminated from the docs. --- README.md | 9 ++-- docs/endpoints/admin.md | 43 +++++++---------- docs/endpoints/appliance.md | 55 +-------------------- docs/endpoints/data.md | 56 +--------------------- docs/endpoints/topology.md | 37 --------------- docs/index.md | 7 +-- docs/quickstart/responses.md | 38 +++++++-------- setup.py | 2 +- tideway/admin.py | 28 ++--------- tideway/data.py | 92 ------------------------------------ tideway/events.py | 12 ----- tideway/knowledge.py | 25 ---------- tideway/main.py | 39 --------------- tideway/topology.py | 70 +-------------------------- tideway/vault.py | 17 ------- 15 files changed, 52 insertions(+), 478 deletions(-) diff --git a/README.md b/README.md index 58b5fee..b7a479d 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Simplified Python library for BMC Discovery API Interface that makes use of the ```python >>> import tideway >>> tw = tideway.appliance('appliance-hostname','auth-token') ->>> tw.about().url +>>> tw.api_about.url 'https://appliance-hostname/api/about' ->>> tw.about().status_code +>>> tw.api_about.status_code 200 ->>> tw.about().text +>>> tw.api_about.text { "api_versions": [ "1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7","1.8","1.9","1.10","1.11","1.12","1.13","1.14" @@ -50,4 +50,5 @@ $ python -m pip install tideway | 0.1.4 | Search bulk update | Discovery 12.3 (21.3) enforces strict case for "Bearer" header - api calls will not current work. | Now includes headers for non-formatted search. | | 0.1.5 | Updated to support Discovery 12.3 (API version 1.3) | - Missing 'complete' parameter option on graphNode() function. | - Fixed issue with Bearer capitalisation.
- Search Bulk will now return the full response on failure | | 0.2.0 | Updated to include Kerberos, Models and Taxonomy endpoints.

Added new high level generic endpoint function calls

Refactored function names/decorators to match API endpoints as close as possible.

Supports Discovery 22.2 (12.5) (API version 1.5) and Outpost API version 1.0 | Project missing tkinter module: https://github.com/traversys/Tideway/issues/15 | Added 'complete' parameter to `get_data_nodes_graph()` (replaces `graphNode()`) | -| 0.2.1 | Added `complete` flag for graph calls, bug fixes to pagination and default focus.

Can retrieve condition templates without an ID.

Kerberos realm detection fixed and parameters are reset after each request.

Removed unused Tkinter library.

Updated to support API version 1.14 | May not work with all new endpoints. | | Issue: https://github.com/traversys/Tideway/issues/15 | \ No newline at end of file +| 0.2.1 | Added `complete` flag for graph calls, bug fixes to pagination and default focus.

Can retrieve condition templates without an ID.

Kerberos realm detection fixed and parameters are reset after each request.

Removed unused Tkinter library.

Updated to support API version 1.14 | May not work with all new endpoints. | | Issue: https://github.com/traversys/Tideway/issues/15 | +| 0.3.0 | Removed deprecated helper aliases and routed all modules through the top-level REST wrappers.

Documentation refreshed to reflect the lean API surface. | | Deprecated helper functions removed; docs and examples updated. | diff --git a/docs/endpoints/admin.md b/docs/endpoints/admin.md index 89de382..b51435c 100644 --- a/docs/endpoints/admin.md +++ b/docs/endpoints/admin.md @@ -52,45 +52,34 @@ Example: {'api_versions': ['1.0', '1.1', '1.2'], 'component': 'REST API', 'product': 'BMC Discovery', 'version': '12.2'} ``` -## baseline() +## get_admin_licensing -[Deprecated] See [get_admin_baseline](#get_admin_baseline) for usage. +Get the latest signed licensing report (plain text by default). -Syntax: `.baseline()` - -## about() - -[Deprecated] See [get_admin_about](#get_admin_about) for usage. - -Syntax: `.about()` +Syntax: -## licensing() +``` +.get_admin_licensing +``` -Get the latest signed licensing report. +## get_admin_licensing_csv -- CSV option returns raw license data in CSV format as a zip file for offline analysis. -- RAW option return an encrypted raw license object for import to another appliance. +Get the latest raw license data in CSV format as a zip file for offline analysis. Syntax: ``` -.licensing([ _content_type_ ]) +.get_admin_licensing_csv ``` -| Parameters | Type | Required | Default Value | Options | -| ------------ | ------ | :------: | ------------- | ------- | -| content_type | String | No | "text/plain" | +## get_admin_licensing_raw -Example: -```python ->>> tw.licensing() ------BEGIN LICENSE REPORT----- -License report -============== +Get the latest license data as an encrypted raw license object for import to another appliance. + +Syntax: -Report start time: 2021-01-18 23:00:00.409987+00:00 -Report end time : 2021-01-21 23:00:00.410085+00:00 -... +``` +.get_admin_licensing_raw ``` ## get_admin_instance @@ -217,4 +206,4 @@ Example: { ... } -``` \ No newline at end of file +``` diff --git a/docs/endpoints/appliance.md b/docs/endpoints/appliance.md index 8371d24..690c0dd 100644 --- a/docs/endpoints/appliance.md +++ b/docs/endpoints/appliance.md @@ -355,59 +355,6 @@ Example: ``` -## about() - -[Deprecated] See [api_about](#api_about) for usage. - -Syntax: `.about()` - -## admin() - -[Deprecated] See [get_admin_about](#get_admin_about) for usage. - -Syntax: `.admin()` - -## swagger() - -[Deprecated] See [api_swagger](#api_swagger) for usage. - -Syntax: `.swagger()` - -## baseline() - -[Deprecated] See [get_admin_baseline](#get_admin_baseline) for usage. - -Syntax: `.baseline()` - -## licensing() - -Get the latest signed licensing report. - -- CSV option returns raw license data in CSV format as a zip file for offline analysis. -- RAW option return an encrypted raw license object for import to another appliance. - -Syntax: - -``` -.licensing([ _content_type_ ]) -``` - -| Parameters | Type | Required | Default Value | Options | -| ------------ | ------ | :------: | ------------- | ------- | -| content_type | String | No | "text/plain" | - -Example: -```python ->>> tw.licensing() ------BEGIN LICENSE REPORT----- -License report -============== - -Report start time: 2021-01-18 23:00:00.409987+00:00 -Report end time : 2021-01-21 23:00:00.410085+00:00 -... -``` - ## help() - Get help on specific Discovery API endpoint and function to use. Outputs full list by default. @@ -433,4 +380,4 @@ Endpoint Function Description /vault/credentials/{cred_id} updateCredential(cred_id, body) Updates partial resources of a credential. Missing properties are left unchanged. /vault/credentials/{cred_id} replaceCredential(cred_id, body) Replaces a single credential. All required credential properties must be present. -``` \ No newline at end of file +``` diff --git a/docs/endpoints/data.md b/docs/endpoints/data.md index 0faae0b..7ff9726 100644 --- a/docs/endpoints/data.md +++ b/docs/endpoints/data.md @@ -334,7 +334,7 @@ Syntax: ## get_data_partitions -Graph data represents a set of nodes and relationships that are associated to the given node. +Get names and IDs of partitions available on the appliance. Syntax: @@ -359,36 +359,6 @@ Example: } ``` -## searchQuery() - -[Deprecated] See [search](#search) for usage. - -Syntax: `.searchQuery(__json__ [, _offset_ ] [, _results_id_ ] [, _format_ ] [, _limit_ ] [, _delete_ ])` - -## nodeLookup() - -[Deprecated] See [get_data_nodes](#get_data_nodes) for usage. - -Syntax: `.nodeLookup(__node_id__ [, _relationships_ ] [, _traverse_ ] [, _flags_ ])` - -## lookupNodeKind() - -[Deprecated] See [get_data_kinds](#get_data_kinds) for usage. - -Syntax: `.lookupNodeKind(__kind__ [, _offset_ ] [, _results_id_ ] [, _format_ ] [, _limit_ ] [, _delete_ ])` - -## graphNode() - -[Deprecated] See [get_data_nodes_graph](#get_data_nodes_graph) for usage. - -Syntax: `.graphNode(__node_id__ [, _focus_ ] [, _apply_rules_ ]))` - -## partitions() - -[Deprecated] See [get_data_nodes_graph](#get_data_nodes_graph) for usage. - -Syntax: `.partitions()` - ## post_data_partitions() Create a Partition. @@ -507,27 +477,3 @@ Syntax: | ------------- | ------ | :------: | ------------- | ------- | | consumer_name | String | Yes | N/A | N/A | | path | String | No | N/A | N/A | - -## best_candidate() - -[Deprecated] See [post_data_candidate](#post_data_candidate) for usage. - -Syntax: `.post_data_candidate(__JSON__)` - -## top_candidates() - -[Deprecated] See [post_data_candidates](#post_data_candidates) for usage. - -Syntax: `.post_data_candidates(__json__)` - -## twImport() - -[Deprecated] See [post_data_import](#post_data_import) for usage. - -Syntax: `.twImport(__json__)` - -## twWrite() - -[Deprecated] See [post_data_write](#post_data_write) for usage. - -Syntax: `.twWrite(__json__)` \ No newline at end of file diff --git a/docs/endpoints/topology.md b/docs/endpoints/topology.md index f152660..bed678f 100644 --- a/docs/endpoints/topology.md +++ b/docs/endpoints/topology.md @@ -65,24 +65,6 @@ Syntax: | ------------- | ----------- | :------: | ------------- | -------- | | json | JSON Object | Yes | N/A | N/A | -## graphNode() - -[Deprecated] See [get_data_nodes_graph](#get_data_nodes_graph) for usage. - -Syntax: `.graphNode(__node_id__ [, _focus_ ] [, _apply_rules_ ])` - -## getNodes() - -[Deprecated] See [post_topology_nodes](#post_topology_nodes) for usage. - -Syntax: `.getNodes(__json__)` - -## getNodeKinds() - -[Deprecated] See [post_topology_nodes_kinds](#post_topology_nodes_kinds) for usage. - -Syntax: `.getNodeKinds(__json__)` - ## get_topology_viz_state Get the current state of the visualization for the authenticated user. @@ -106,7 +88,6 @@ Syntax: | ------------- | ----------- | :------: | ------------- | -------- | | json | JSON Object | Yes | N/A | N/A | - ## put_topology_viz_state() Update any or all of the attributes of the current state of the visualization for the authenticated user. @@ -119,21 +100,3 @@ put_topology_viz_state(__json__) | Parameters | Type | Required | Default Value | Options | | ------------- | ----------- | :------: | ------------- | -------- | | json | JSON Object | Yes | N/A | N/A | - -## visualizationState() - -[Deprecated] See [get_topology_viz_state](#get_topology_viz_state) for usage. - -Syntax: `.visualizationState()` - -## updateVizState() - -[Deprecated] See [patch_topology_viz_state](#patch_topology_viz_state) for usage. - -Syntax: `.updateVizState(__json__)` - -## replaceVizState() - -[Deprecated] See [put_topology_viz_state](#put_topology_viz_state) for usage. - -Syntax: `.replaceVizState(__json__)` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index f9c19a6..3c30e93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,11 +6,11 @@ Simplified Python library for BMC Discovery API Interface that makes use of the ```python >>> import tideway >>> tw = tideway.appliance('appliance-hostname','auth-token') ->>> tw.about().url +>>> tw.api_about.url 'https://appliance-hostname/api/about' ->>> tw.about().status_code +>>> tw.api_about.status_code 200 ->>> tw.about().text +>>> tw.api_about.text { "api_versions": [ "1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7","1.8","1.9","1.10","1.11","1.12","1.13","1.14" @@ -51,3 +51,4 @@ $ python -m pip install tideway | 0.1.5 | Updated to support Discovery 12.3 (API version 1.3) | - Missing 'complete' parameter option on graphNode() function. | - Fixed issue with Bearer capitalisation.
- Search Bulk will now return the full response on failure | | 0.2.0 | Updated to include Kerberos, Models and Taxonomy endpoints.

Added new high level generic endpoint function calls

Refactored function names/decorators to match API endpoints as close as possible.

Supports Discovery 22.2 (12.5) (API version 1.5) and Outpost API version 1.0 | Project missing tkinter module: https://github.com/traversys/Tideway/issues/15 | Added 'complete' parameter to `get_data_nodes_graph()` (replaces `graphNode()`) | | 0.2.1 | Added `complete` flag for graph calls, bug fixes to pagination and default focus.

Can retrieve condition templates without an ID.

Kerberos realm detection fixed and parameters are reset after each request.

Removed unused Tkinter library.

Updated to support API version 1.14 | May not work with all new endpoints. | | Issue: https://github.com/traversys/Tideway/issues/15 | +| 0.3.0 | Removed deprecated helper aliases and routed all modules through the top-level REST wrappers.

Documentation refreshed to reflect the lean API surface. | | Deprecated helper functions removed; docs and examples updated. | diff --git a/docs/quickstart/responses.md b/docs/quickstart/responses.md index e2e5d09..8abdec8 100644 --- a/docs/quickstart/responses.md +++ b/docs/quickstart/responses.md @@ -9,67 +9,67 @@ sort: 2 ```python >>> tw = tideway.appliance('appliance-hostname','auth-token',api_version='1.2') ->>> response = tw.about() +>>> response = tw.api_about ``` ## .headers ```python ->>> tw.about().headers +>>> tw.api_about.headers {'Date': 'Sun, 06 Jun 2021 18:43:31 GMT', 'Server': 'waitress', 'X-Content-Type-Options': 'nosniff', 'Content-Length': '160', 'Content-Type': 'application/json', 'Content-security-policy': "default-src https: 'self'; style-src https: 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; img-src 'self' data:; base-uri 'none'; object-src 'none'; connect-src https: 'self'; frame-ancestors 'self';", 'Keep-Alive': 'timeout=15, max=100', 'Connection': 'Keep-Alive'} ``` ## .encoding ```python ->>> tw.about().encoding +>>> tw.api_about.encoding None ``` ## .elapsed ```python ->>> tw.about().elapsed +>>> tw.api_about.elapsed 0:00:00.028861 ``` ## .content ```python ->>> tw.about().content +>>> tw.api_about.content b'{\n "api_versions": [\n "1.0",\n "1.1",\n "1.2"\n ],\n "component": "REST API",\n "product": "BMC Discovery",\n "version": "12.2"\n}\n' ``` ## .cookies ```python ->>> tw.about().cookies +>>> tw.api_about.cookies ``` ## .history ```python ->>> tw.about().history +>>> tw.api_about.history [] ``` ## .is_permanent_redirect ```python ->>> tw.about().is_permanent_redirect +>>> tw.api_about.is_permanent_redirect False ``` ## .is_redirect ```python ->>> tw.about().is_redirect +>>> tw.api_about.is_redirect False ``` ## .iter_content() ```python ->>> tw.about().iter_content() +>>> tw.api_about.iter_content() ``` ## .json() ```python ->>> tw.about().json() +>>> tw.api_about.json() {'api_versions': ['1.0', '1.1', '1.2'], 'component': 'REST API', 'product': 'BMC Discovery', 'version': '12.2'} ``` ## .url ```python ->>> tw.about().url +>>> tw.api_about.url https://appliance-hostname/api/about ``` ## .text ```python ->>> tw.about().text +>>> tw.api_about.text { "api_versions": [ "1.0", @@ -83,31 +83,31 @@ https://appliance-hostname/api/about ``` ## .status_code ```python ->>> tw.about().status_code +>>> tw.api_about.status_code 200 ``` ## .request ```python ->>> tw.about().request +>>> tw.api_about.request ``` ## .reason ```python ->>> tw.about().reason +>>> tw.api_about.reason OK ``` ## .raise_for_status() ```python ->>> tw.about().raise_for_status() +>>> tw.api_about.raise_for_status() None ``` ## .ok ```python ->>> tw.about().ok +>>> tw.api_about.ok True ``` ## links ```python ->>> tw.about().links +>>> tw.api_about.links {} ``` \ No newline at end of file diff --git a/setup.py b/setup.py index e385ca1..e1fe902 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="tideway", - version="0.2.1", + version="0.3.0", author="Wes Moskal-Fitzpatrick", author_email="wes@traversys.io", description="Library for BMC Discovery API Interface.", diff --git a/tideway/admin.py b/tideway/admin.py index c984212..46f2253 100644 --- a/tideway/admin.py +++ b/tideway/admin.py @@ -1,37 +1,22 @@ # -*- coding: utf-8 -*- import tideway -import warnings appliance = tideway.main.Appliance class Admin(appliance): '''Manage the BMC Discovery appliance.''' - def baseline(self): + def get_admin_baseline(self): '''Get a summary of the appliance status, and details of which baseline checks have passed or failed.''' - warnings.warn( - "baseline() is deprecated; use get_admin_baseline instead.", - DeprecationWarning, - ) return self.get("/admin/baseline") - get_admin_baseline = property(baseline) - def admin(self): + def get_admin_about(self): '''Get information about the appliance, like its version and versions of the installed packages.''' - warnings.warn( - "admin() is deprecated; use get_admin_about instead.", - DeprecationWarning, - ) return self.get("/admin/about") - get_admin_about = property(admin) - def licensing(self,content_type="text/plain"): + def get_admin_licensing(self, content_type="text/plain"): '''Get the latest signed licensing report.''' - warnings.warn( - "licensing() is deprecated; use get_admin_licensing* helpers instead.", - DeprecationWarning, - ) if content_type == "csv": response = self.get("/admin/licensing/csv", response="application/zip") elif content_type == "raw": @@ -43,34 +28,27 @@ def licensing(self,content_type="text/plain"): def instance(self): '''Get details about the appliance instance.''' return self.get("/admin/instance") - get_admin_instance = property(instance) def cluster(self): '''Get cluster configuration and status.''' return self.get("/admin/cluster") - get_admin_cluster = property(cluster) def organizations(self): '''Get configured organizations.''' return self.get("/admin/organizations") - get_admin_organizations = property(organizations) def preferences(self): '''Get global appliance preferences.''' return self.get("/admin/preferences") - get_admin_preferences = property(preferences) def builtin_reports(self): '''Get built-in report definitions.''' return self.get("/admin/builtin_reports") - get_admin_builtin_reports = property(builtin_reports) def custom_reports(self): '''Get custom report definitions.''' return self.get("/admin/custom_reports") - get_admin_custom_reports = property(custom_reports) def smtp(self): '''Get SMTP configuration.''' return self.get("/admin/smtp") - get_admin_smtp = property(smtp) diff --git a/tideway/data.py b/tideway/data.py index 75a31bc..8ecf72d 100644 --- a/tideway/data.py +++ b/tideway/data.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import tideway -import warnings import json appliance = tideway.main.Appliance @@ -95,14 +94,6 @@ def search(self, query, offset=None, results_id=None, format=None, limit=100, de return response return self._search_all(query, format, limit, delete, record_limit, call_limit) - def searchQuery(self, body, offset=None, results_id=None, format=None, limit = 100, delete = False): - '''An alternative to GET /data/search, for search queries which are too long for urls.''' - warnings.warn( - "searchQuery() is deprecated; use search() instead.", - DeprecationWarning, - ) - return Data.search(self, body, offset, results_id, format, limit, delete) - def search_bulk(self, query, format=None, limit=100, delete=False, record_limit=None, call_limit=None): '''Performs a bulk search, looping through paginated results.''' return self._search_all(query, format, limit, delete, record_limit, call_limit) @@ -136,32 +127,11 @@ def post_data_candidate(self, body): response = self.post("/data/candidate", body) return response - def best_candidate(self, body): - ''' - The node object of the best candidate based on the provided parameters. - ''' - warnings.warn( - "best_candidate() is deprecated; use post_data_candidate() instead.", - DeprecationWarning, - ) - return self.post_data_candidate(body) - def post_data_candidates(self, body): '''Alternate API call for POST /data/candidates.''' response = self.post("/data/candidates", body) return response - def top_candidates(self, body): - ''' - Enter parameters to identify a device, the response is a list of - candidate nodes ordered by descending score. - ''' - warnings.warn( - "top_candidates() is deprecated; use post_data_candidates() instead.", - DeprecationWarning, - ) - return self.post_data_candidates(body) - def get_data_nodes(self, node_id, relationships=False, traverse=None, flags=None, attributes=None): '''Alternate API call for /data/nodes/node_id''' self.params['traverse'] = traverse @@ -173,20 +143,6 @@ def get_data_nodes(self, node_id, relationships=False, traverse=None, flags=None response = self.get("/data/nodes/{}".format(node_id)) return response - def nodeLookup(self, node_id, relationships=False, traverse=None, flags=None, attributes=None): - '''Get the state of a node with specified id.''' - warnings.warn( - "nodeLookup() is deprecated; use get_data_nodes() instead.", - DeprecationWarning, - ) - return self.get_data_nodes( - node_id, - relationships=relationships, - traverse=traverse, - flags=flags, - attributes=attributes, - ) - def get_data_nodes_graph(self, node_id, focus="software-connected", apply_rules=True, complete=False): '''Alternate API call for /data/nodes/node_id/graph''' self.params['focus'] = focus @@ -195,19 +151,6 @@ def get_data_nodes_graph(self, node_id, focus="software-connected", apply_rules= response = self.get("/data/nodes/{}/graph".format(node_id)) return response - def graphNode(self, node_id, focus="software-connected", apply_rules=True): - '''Graph data represents a set of nodes and relationships that are associated to the given node.''' - warnings.warn( - "graphNode() is deprecated; use get_data_nodes_graph() instead.", - DeprecationWarning, - ) - return self.get_data_nodes_graph( - node_id, - focus=focus, - apply_rules=apply_rules, - complete=False, - ) - def get_data_kinds(self, kind, offset=None, results_id=None, format=None, limit = 100, delete = False): '''Alternate API call for /data/kinds.''' self.params['offset'] = offset @@ -218,21 +161,6 @@ def get_data_kinds(self, kind, offset=None, results_id=None, format=None, limit response = self.get("/data/kinds/{}".format(kind)) return response - def lookupNodeKind(self, kind, offset=None, results_id=None, format=None, limit = 100, delete = False): - '''Finds all nodes of a specified node kind.''' - warnings.warn( - "lookupNodeKind() is deprecated; use get_data_kinds() instead.", - DeprecationWarning, - ) - return self.get_data_kinds( - kind, - offset=offset, - results_id=results_id, - format=format, - limit=limit, - delete=delete, - ) - def partitions(self): '''Get names and ids of partitions.''' response = self.get("/data/partitions") @@ -249,31 +177,11 @@ def post_data_import(self, body): response = self.post("/data/import", body) return response - def twImport(self, body): - ''' - Imports data. Returns the import UUID. - ''' - warnings.warn( - "twImport() is deprecated; use post_data_import() instead.", - DeprecationWarning, - ) - return self.post_data_import(body) - def post_data_write(self, body): '''Alternate API call for /data/write.''' response = self.post("/data/write", body) return response - def twWrite(self, body): - ''' - Perform arbitrary write operations. - ''' - warnings.warn( - "twWrite() is deprecated; use post_data_write() instead.", - DeprecationWarning, - ) - return self.post_data_write(body) - def get_data_condition_params(self): '''Retrieve the list of available condition parameters.''' response = self.get("/data/condition/params") diff --git a/tideway/events.py b/tideway/events.py index faf2160..fc09c48 100644 --- a/tideway/events.py +++ b/tideway/events.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import tideway -import warnings appliance = tideway.main.Appliance class Events(appliance): @@ -11,14 +10,3 @@ def post_events(self, body): '''An alternate API call for POST /events''' response = self.post("/events", body) return response - - def status(self, body): - ''' - Returns a unique ID if the event has been recorded, otherwise an - empty string is returned e.g. if the event source has been disabled. - ''' - warnings.warn( - "status() is deprecated; use post_events() instead.", - DeprecationWarning, - ) - return self.post_events(body) diff --git a/tideway/knowledge.py b/tideway/knowledge.py index 3429bd9..81cd9c7 100644 --- a/tideway/knowledge.py +++ b/tideway/knowledge.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import tideway -import warnings appliance = tideway.main.Appliance @@ -11,24 +10,8 @@ class Knowledge(appliance): def get_knowledge(self): '''Get the current state of the appliance's knowledge, including TKU versions.''' return self.get("/knowledge") - - def getKnowledgeManagement(self): - '''Get the current state of the appliance's knowledge, including TKU versions.''' - warnings.warn( - "getKnowledgeManagement() is deprecated; use get_knowledge() instead.", - DeprecationWarning, - ) - return self.get_knowledge() get_knowledge_property = property(get_knowledge) - def getUploadStatus(self): - '''Get the current state of a knowledge upload.''' - warnings.warn( - "getUploadStatus() is deprecated; use get_knowledge_status() instead.", - DeprecationWarning, - ) - return self.get_knowledge_status() - def get_knowledge_status(self): '''Get the current state of a knowledge upload.''' return self.get("/knowledge/status") @@ -47,14 +30,6 @@ def post_knowledge(self, filename, file, activate=True, allow_restart=False): response="text/html", ) - def uploadKnowledge(self, filename, file, activate=True, allow_restart=False): - '''Upload a TKU or pattern module to the appliance.''' - warnings.warn( - "uploadKnowledge() is deprecated; use post_knowledge() instead.", - DeprecationWarning, - ) - return self.post_knowledge(filename, file, activate, allow_restart) - def getKnowledgeTriggerPatterns(self, lookup_data_sources=None): '''Get a list of all knowledge trigger patterns.''' self.params['lookup_data_sources'] = lookup_data_sources diff --git a/tideway/main.py b/tideway/main.py index 3f846ec..5146df9 100644 --- a/tideway/main.py +++ b/tideway/main.py @@ -5,7 +5,6 @@ from . import discoRequests as dr from . import endpoints import tideway -import warnings class Appliance: '''An appliance instance.''' @@ -120,14 +119,6 @@ def api_about(self): req = requests.get(url, verify=self.verify) return req - def about(self): - '''Return about data.''' - warnings.warn( - "about() is deprecated; use api_about instead.", - DeprecationWarning, - ) - return self.api_about - def _get_api_schema(self): '''Helper to fetch API schema, trying /swagger.json first, then /openapi.json.''' for path in ["/swagger.json", "/openapi.json"]: @@ -142,14 +133,6 @@ def api_swagger(self): '''Alternate API call for swagger.''' return self._get_api_schema() - def swagger(self): - '''Fetch API schema, trying /swagger.json first, then /openapi.json.''' - warnings.warn( - "swagger() is deprecated; use api_swagger instead.", - DeprecationWarning, - ) - return self.api_swagger - def _load_schema(self): '''Return cached API schema as dict, fetching it if necessary.''' if getattr(self, '_api_schema', None) is None: @@ -173,14 +156,6 @@ def get_admin_baseline(self): '''Alternate API call for baseline.''' return self.get("/admin/baseline") - def baseline(self): - '''Get a summary of the appliance status, and details of which baseline checks have passed or failed.''' - warnings.warn( - "baseline() is deprecated; use get_admin_baseline instead.", - DeprecationWarning, - ) - return self.get_admin_baseline - @property def get_admin_about(self): '''Alternate API call for /admin/about.''' @@ -201,20 +176,6 @@ def get_admin_licensing_raw(self): '''Alternate API call for licensing report raw.''' return self.get("/admin/licensing/raw", response="application/zip") - def licensing(self,content_type="text/plain"): - '''Get the latest signed licensing report.''' - warnings.warn( - "licensing() is deprecated; use get_admin_licensing or the CSV/RAW helpers instead.", - DeprecationWarning, - ) - if content_type == "csv": - response = self.get("/admin/licensing/csv", response="application/zip") - elif content_type == "raw": - response = self.get("/admin/licensing/raw", response="application/zip") - else: - response = self.get("/admin/licensing", response=content_type) - return response - @property def api_help(self): '''Help on endpoints.''' diff --git a/tideway/topology.py b/tideway/topology.py index 0468ca8..36197c1 100644 --- a/tideway/topology.py +++ b/tideway/topology.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import tideway -import warnings appliance = tideway.main.Appliance @@ -16,91 +15,26 @@ def get_data_nodes_graph(self, node_id, focus="software-connected", apply_rules= response = self.get("/data/nodes/{}/graph".format(node_id)) return response - def graphNode(self, node_id, focus="software-connected", apply_rules=True): - ''' - Graph data represents a set of nodes and relationships that are - associated to the given node. - ''' - warnings.warn( - "graphNode() is deprecated; use get_data_nodes_graph() instead.", - DeprecationWarning, - ) - return self.get_data_nodes_graph( - node_id, - focus=focus, - apply_rules=apply_rules, - complete=False, - ) - def post_topology_nodes(self, body): '''Alternate API call for POST /topology/nodes.''' response = self.post("/topology/nodes", body) return response - def getNodes(self, body): - '''Get topology data from one or more starting nodes.''' - warnings.warn( - "getNodes() is deprecated; use post_topology_nodes() instead.", - DeprecationWarning, - ) - return self.post_topology_nodes(body) - def post_topology_nodes_kinds(self, body): '''Alternate API call for POST /topology/nodes/kinds.''' response = self.post("/topology/nodes/kinds", body) return response - def getNodeKinds(self, body): - ''' - Get nodes of the specified kinds which are related to a given set of - nodes. - ''' - warnings.warn( - "getNodeKinds() is deprecated; use post_topology_nodes_kinds() instead.", - DeprecationWarning, - ) - return self.post_topology_nodes_kinds(body) - - def visualizationState(self): - ''' - Get the current state of the visualization for the authenticated - user. - ''' - warnings.warn( - "visualizationState() is deprecated; use get_topology_viz_state instead.", - DeprecationWarning, - ) + def get_topology_viz_state(self): + '''Get the current visualization state for the authenticated user.''' return self.get("/topology/visualization_state") - get_topology_viz_state = property(visualizationState) def patch_topology_viz_state(self, body): '''Alternate API call for PATCH /topology/visualization_state''' response = self.patch("/topology/visualization_state", body) return response - def updateVizState(self, body): - ''' - Update one or more attributes of the current state of the - visualization for the authenticated user. - ''' - warnings.warn( - "updateVizState() is deprecated; use patch_topology_viz_state() instead.", - DeprecationWarning, - ) - return self.patch_topology_viz_state(body) - def put_topology_viz_state(self, body): '''Alternate API call for PUT /topology/visualization_state''' response = self.put("/topology/visualization_state", body) return response - - def replaceVizState(self, body): - ''' - Update any or all of the attributes of the current state of the - visualization for the authenticated user. - ''' - warnings.warn( - "replaceVizState() is deprecated; use put_topology_viz_state() instead.", - DeprecationWarning, - ) - return self.put_topology_viz_state(body) diff --git a/tideway/vault.py b/tideway/vault.py index d62e7c7..1fa006b 100644 --- a/tideway/vault.py +++ b/tideway/vault.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import tideway -import warnings appliance = tideway.main.Appliance @@ -11,25 +10,9 @@ class Vault(appliance): def get_vault(self): '''Get details of the state of the vault.''' return self.get("/vault") - - def getVault(self): - '''Get details of the state of the vault.''' - warnings.warn( - "getVault() is deprecated; use get_vault() instead.", - DeprecationWarning, - ) - return self.get_vault() get_vault_property = property(get_vault) def patch_vault(self, body): '''Alternate API call for PATCH /vault''' response = self.patch("/vault", body) return response - - def updateVault(self, body): - '''Change the state of the vault.''' - warnings.warn( - "updateVault() is deprecated; use patch_vault() instead.", - DeprecationWarning, - ) - return self.patch_vault(body) From f49dc304d8790e87ca46711293098479de2c548c Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 19:20:44 +0000 Subject: [PATCH 4/9] Update API version support to 1.16 and add admin API notebook Extended supported API versions to 1.16 in documentation and code defaults. Added a Jupyter notebook with admin API usage examples. Refactored and clarified admin-related methods in Appliance class. --- README.md | 4 +- docs/index.md | 4 +- docs/quickstart/initiation.md | 4 +- notebooks/admin_api_examples.ipynb | 120 +++++++++++++++++++++++++++++ tideway/main.py | 33 ++++---- 5 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 notebooks/admin_api_examples.ipynb diff --git a/README.md b/README.md index b7a479d..469191e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Simplified Python library for BMC Discovery API Interface that makes use of the >>> tw.api_about.text { "api_versions": [ - "1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7","1.8","1.9","1.10","1.11","1.12","1.13","1.14" + "1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7","1.8","1.9","1.10","1.11","1.12","1.13","1.14","1.15","1.16" ], "component": "REST API", "version":"DaaS", @@ -38,7 +38,7 @@ Documentation can be found at [https://traversys.github.io/Tideway/](https://tra $ python -m pip install tideway ``` -- Tideway supports BMC Discovery 11.3+, API v1.0-1.14 using Python 3. +- Tideway supports BMC Discovery 11.3+, API v1.0-1.16 using Python 3. ## Releases diff --git a/docs/index.md b/docs/index.md index 3c30e93..31c7a09 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ Simplified Python library for BMC Discovery API Interface that makes use of the >>> tw.api_about.text { "api_versions": [ - "1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7","1.8","1.9","1.10","1.11","1.12","1.13","1.14" + "1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7","1.8","1.9","1.10","1.11","1.12","1.13","1.14","1.15","1.16" ], "component": "REST API", "version":"DaaS", @@ -34,7 +34,7 @@ Tideway removes the extra layer of manually constructing a URL and parameters fo $ python -m pip install tideway ``` -- Tideway supports BMC Discovery 11.3+, API v1.0-1.14 using Python 3. +- Tideway supports BMC Discovery 11.3+, API v1.0-1.16 using Python 3. ## Contents diff --git a/docs/quickstart/initiation.md b/docs/quickstart/initiation.md index 3079ac6..c24ad6e 100644 --- a/docs/quickstart/initiation.md +++ b/docs/quickstart/initiation.md @@ -32,5 +32,5 @@ Upon initiation the following parameters can be used: | - | - | - | - | - | target | Required | String | | The Hostname, FQDN or IP Address of the Discovery instance. | token | Required | String | | The authentication token of the API user. It is not necessary to include the "bearer" pre-text. -| api_version | | String | "1.5" | This should be the supported version of the API. Discovery 22.2 supports API versions up to 1.5 (outpost 1.0). -| ssl_verify | | Boolean | False | Choose whether to query the API using a valid SSL certificate. If you are using self-signed HTTPS then you should leave this with the default value. \ No newline at end of file +| api_version | | String | "1.16" | This should be the supported version of the API. Discovery 25.x supports API versions up to 1.16 (outpost 1.0). +| ssl_verify | | Boolean | False | Choose whether to query the API using a valid SSL certificate. If you are using self-signed HTTPS then you should leave this with the default value. diff --git a/notebooks/admin_api_examples.ipynb b/notebooks/admin_api_examples.ipynb new file mode 100644 index 0000000..2ad1ee7 --- /dev/null +++ b/notebooks/admin_api_examples.ipynb @@ -0,0 +1,120 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Admin API Examples\n", + "\n", + "This notebook shows how to initialise a Tideway appliance client and call common Admin endpoints using the top-level REST wrappers. Replace the placeholder values before running." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "- Ensure `tideway` is installed in your environment (e.g. `pip install -e .` from the repo root).\n", + "- Provide your appliance hostname and API token below. Do **not** commit tokens." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "tw.api_about.json() # quick sanity check" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Admin calls\n", + "Examples of common admin endpoints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Appliance baseline summary\n", + "baseline = tw.get_admin_baseline\n", + "baseline_status = baseline.json() if baseline.ok else baseline.text\n", + "baseline_status" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Appliance details\n", + "about = tw.get_admin_about\n", + "about.json() if about.ok else about.text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Licensing reports\n", + "licensing_text = tw.get_admin_licensing\n", + "licensing_text.text[:400] if licensing_text.ok else licensing_text.text\n", + "\n", + "licensing_csv = tw.get_admin_licensing_csv # ZIP content; handle as needed\n", + "licensing_raw = tw.get_admin_licensing_raw # encrypted raw license object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Instance, cluster, and other admin metadata\n", + "instance = tw.admin().instance() # alternative: tw.get_admin_instance if using the Admin class directly\n", + "cluster = tw.admin().cluster()\n", + "preferences = tw.admin().preferences()\n", + "\n", + "instance.json() if instance.ok else instance.text" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tideway/main.py b/tideway/main.py index 5146df9..18daacc 100644 --- a/tideway/main.py +++ b/tideway/main.py @@ -9,7 +9,7 @@ class Appliance: '''An appliance instance.''' - def __init__(self, target, token, limit = 100, delete = False, api_version = "1.14", ssl_verify = False): + def __init__(self, target, token, limit = 100, delete = False, api_version = "1.16", ssl_verify = False): self.target = target self.token = token self.default_limit = limit @@ -110,7 +110,7 @@ def vault(self): v = tideway.vault(self.target, self.token, api_version=self.api_version, ssl_verify=self.verify) return v - ### Admin ### + ### API Admin ### @property def api_about(self): @@ -151,6 +151,22 @@ def api_paths(self, path=None): return paths.get(path) return paths + @property + def api_help(self): + '''Help on endpoints.''' + endpoints.docs() + #print("") + + def help(*args): + '''Help on endpoints.''' + if len(args) > 1: + endpoints.docs(args[1]) + else: + endpoints.docs() + #print("\n") + +### Discovery Admin ### + @property def get_admin_baseline(self): '''Alternate API call for baseline.''' @@ -176,16 +192,3 @@ def get_admin_licensing_raw(self): '''Alternate API call for licensing report raw.''' return self.get("/admin/licensing/raw", response="application/zip") - @property - def api_help(self): - '''Help on endpoints.''' - endpoints.docs() - #print("") - - def help(*args): - '''Help on endpoints.''' - if len(args) > 1: - endpoints.docs(args[1]) - else: - endpoints.docs() - #print("\n") From afcc905934f8b1c5b524e7a55f1959e1ec8fb76a Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 20:07:24 +0000 Subject: [PATCH 5/9] Update admin API example notebook and docs Added a link to the admin API example notebook in the README and documentation. Refactored and expanded the admin_api_examples.ipynb notebook with improved endpoint usage, response handling, and additional admin API examples for better clarity and coverage. --- README.md | 2 + docs/index.md | 2 + notebooks/admin_api_examples.ipynb | 145 ++++++++++++++++++++++++----- 3 files changed, 128 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 469191e..2b7bc52 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Tideway removes the extra layer of manually constructing a URL and parameters fo Documentation can be found at [https://traversys.github.io/Tideway/](https://traversys.github.io/Tideway/). +- Example notebook: [`notebooks/admin_api_examples.ipynb`](https://github.com/traversys/Tideway/blob/main/notebooks/admin_api_examples.ipynb) (download via `curl -O https://raw.githubusercontent.com/traversys/Tideway/main/notebooks/admin_api_examples.ipynb`) + ## Installation - Tideway can be installed via PyPI: diff --git a/docs/index.md b/docs/index.md index 31c7a09..b212ea8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,6 +26,8 @@ Tideway follows BMC Discovery's well-structured and documented REST API which ca Tideway removes the extra layer of manually constructing a URL and parameters for python requests allowing you to query API supported features of Discovery seamlessly and faster than if you were to navigate via the GUI. +Example notebook: [`notebooks/admin_api_examples.ipynb`](https://github.com/traversys/Tideway/blob/main/notebooks/admin_api_examples.ipynb) (download via `curl -O https://raw.githubusercontent.com/traversys/Tideway/main/notebooks/admin_api_examples.ipynb`) + ## Installation - Tideway can be installed via PyPI: diff --git a/notebooks/admin_api_examples.ipynb b/notebooks/admin_api_examples.ipynb index 2ad1ee7..0eaf4e0 100644 --- a/notebooks/admin_api_examples.ipynb +++ b/notebooks/admin_api_examples.ipynb @@ -21,23 +21,38 @@ { "cell_type": "code", "execution_count": null, + "id": "735d498b", "metadata": {}, "outputs": [], "source": [ "import tideway\n", "\n", "# Configuration\n", - "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", - "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "TARGET = '' # e.g. 'discovery.example.com'\n", + "TOKEN = '' # keep secrets out of source control\n", "API_VERSION = '1.16' # latest supported API\n", "SSL_VERIFY = False # set to True when using valid certs\n", "\n", "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", - "tw.api_about.json() # quick sanity check" + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" ] }, { "cell_type": "markdown", + "id": "a113ac85", "metadata": {}, "source": [ "## Admin calls\n", @@ -47,58 +62,146 @@ { "cell_type": "code", "execution_count": null, + "id": "c15278d6", "metadata": {}, "outputs": [], "source": [ - "# Appliance baseline summary\n", - "baseline = tw.get_admin_baseline\n", - "baseline_status = baseline.json() if baseline.ok else baseline.text\n", - "baseline_status" + "# Appliance baseline summary (appliance only - won't work on Helix)\n", + "admin_baseline = tw.get('/admin/baseline')\n", + "show_response(admin_baseline, '/admin/baseline')\n" ] }, { "cell_type": "code", "execution_count": null, + "id": "eb9be7cf", "metadata": {}, "outputs": [], "source": [ "# Appliance details\n", - "about = tw.get_admin_about\n", - "about.json() if about.ok else about.text" + "admin_about = tw.get('/admin/about')\n", + "show_response(admin_about, '/admin/about')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc1acb67", + "metadata": {}, + "outputs": [], + "source": [ + "# Licensing endpoints with explicit response types\n", + "try:\n", + " admin_licensing_text = tw.get('/admin/licensing', response='text/plain')\n", + " admin_licensing_csv = tw.get('/admin/licensing/csv', response='application/zip')\n", + " admin_licensing_raw = tw.get('/admin/licensing/raw', response='application/zip')\n", + "except TypeError:\n", + " admin_licensing_text = tw.get('/admin/licensing')\n", + " admin_licensing_csv = tw.get('/admin/licensing/csv')\n", + " admin_licensing_raw = tw.get('/admin/licensing/raw')\n", + "\n", + "# Render plain text with real line breaks when available\n", + "if admin_licensing_text.ok:\n", + " print(admin_licensing_text.text)\n", + "else:\n", + " show_response(admin_licensing_text, '/admin/licensing')\n" ] }, { "cell_type": "code", "execution_count": null, + "id": "0c38dd77", "metadata": {}, "outputs": [], "source": [ - "# Licensing reports\n", - "licensing_text = tw.get_admin_licensing\n", - "licensing_text.text[:400] if licensing_text.ok else licensing_text.text\n", + "# Fetch API schema (swagger/openapi)\n", + "api_swagger = tw.api_swagger\n", + "api_swagger.status_code, api_swagger.ok\n", "\n", - "licensing_csv = tw.get_admin_licensing_csv # ZIP content; handle as needed\n", - "licensing_raw = tw.get_admin_licensing_raw # encrypted raw license object" + "if api_swagger.ok:\n", + " schema = api_swagger.json()\n", + " list(schema.keys())[:10]\n", + "else:\n", + " api_swagger.text\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3c71ce7", + "metadata": {}, + "outputs": [], + "source": [ + "# Parsed schema via helper\n", + "parsed_schema = tw.api_schema()\n", + "list(parsed_schema.keys())[:10]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3080dc2", + "metadata": {}, + "outputs": [], + "source": [ + "# Inspect available API paths\n", + "paths = tw.api_paths()\n", + "list(paths.keys())[:10]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d097b99", + "metadata": {}, + "outputs": [], + "source": [ + "# Endpoint help (prints to stdout)\n", + "tw.api_help\n" + ] + }, + { + "cell_type": "markdown", + "id": "2546f0ed", + "metadata": {}, + "source": [ + "## Direct admin GET calls\n", + "Raw examples using the generic `get` wrapper without helper methods. Adjust `response` headers as needed." ] }, { "cell_type": "code", "execution_count": null, + "id": "1c05acea", "metadata": {}, "outputs": [], "source": [ - "# Instance, cluster, and other admin metadata\n", - "instance = tw.admin().instance() # alternative: tw.get_admin_instance if using the Admin class directly\n", - "cluster = tw.admin().cluster()\n", - "preferences = tw.admin().preferences()\n", + "# Direct admin endpoints\n", + "admin_instance = tw.get('/admin/instance')\n", + "admin_cluster = tw.get('/admin/cluster')\n", + "admin_organizations = tw.get('/admin/organizations')\n", + "admin_preferences = tw.get('/admin/preferences')\n", + "admin_builtin_reports = tw.get('/admin/builtin_reports')\n", + "admin_custom_reports = tw.get('/admin/custom_reports')\n", + "admin_smtp = tw.get('/admin/smtp')\n", "\n", - "instance.json() if instance.ok else instance.text" + "# Collect all responses with context\n", + "admin_results = {\n", + " '/admin/instance': show_response(admin_instance, '/admin/instance'),\n", + " '/admin/cluster': show_response(admin_cluster, '/admin/cluster'),\n", + " '/admin/organizations': show_response(admin_organizations, '/admin/organizations'),\n", + " '/admin/preferences': show_response(admin_preferences, '/admin/preferences'),\n", + " '/admin/builtin_reports': show_response(admin_builtin_reports, '/admin/builtin_reports'),\n", + " '/admin/custom_reports': show_response(admin_custom_reports, '/admin/custom_reports'),\n", + " '/admin/smtp': show_response(admin_smtp, '/admin/smtp'),\n", + "}\n", + "admin_results\n" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -112,7 +215,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3" + "version": "3.12.3" } }, "nbformat": 4, From 7b207581f9058b855a8e81067888269c048ca1a5 Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 20:21:16 +0000 Subject: [PATCH 6/9] Add admin consolidation notebook and update API examples Added a new notebook with raw GET/POST examples for Tideway admin and consolidation API endpoints. Updated the admin_api_examples notebook to include explicit JSON response handling for the licensing endpoint. --- notebooks/admin_api_examples.ipynb | 2 + notebooks/admin_consolidation.ipynb | 137 ++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 notebooks/admin_consolidation.ipynb diff --git a/notebooks/admin_api_examples.ipynb b/notebooks/admin_api_examples.ipynb index 0eaf4e0..7894b46 100644 --- a/notebooks/admin_api_examples.ipynb +++ b/notebooks/admin_api_examples.ipynb @@ -93,10 +93,12 @@ "# Licensing endpoints with explicit response types\n", "try:\n", " admin_licensing_text = tw.get('/admin/licensing', response='text/plain')\n", + " admin_licensing_json = tw.get('/admin/licensing/json', response='application/json')\n", " admin_licensing_csv = tw.get('/admin/licensing/csv', response='application/zip')\n", " admin_licensing_raw = tw.get('/admin/licensing/raw', response='application/zip')\n", "except TypeError:\n", " admin_licensing_text = tw.get('/admin/licensing')\n", + " admin_licensing_json = tw.get('/admin/licensing/json')\n", " admin_licensing_csv = tw.get('/admin/licensing/csv')\n", " admin_licensing_raw = tw.get('/admin/licensing/raw')\n", "\n", diff --git a/notebooks/admin_consolidation.ipynb b/notebooks/admin_consolidation.ipynb new file mode 100644 index 0000000..08bb925 --- /dev/null +++ b/notebooks/admin_consolidation.ipynb @@ -0,0 +1,137 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Admin & Consolidation API Examples\n", + "\n", + "Using raw `get`/`post` calls (no helper functions) to hit admin and consolidation endpoints. Adjust paths/bodies to match your appliance and the published API (see BMC docs: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "- Install tideway (e.g. `pip install -e .` from repo root).\n", + "- Set `TARGET` and `TOKEN` below. Do **not** commit secrets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + " \n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Admin examples (raw GET/POST)\n", + "Edit the paths as needed; these use `tw.get`/`tw.post` directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example admin POST (edit body/endpoint per docs)\n", + "sample_preferences_body = {\n", + " # Fill in fields per API schema, e.g. 'timezone': 'UTC'\n", + "}\n", + "if sample_preferences_body:\n", + " admin_preferences_post = tw.post('/admin/preferences', sample_preferences_body)\n", + " show_response(admin_preferences_post, '/admin/preferences (POST)')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Consolidation examples (adjust endpoints per docs)\n", + "These are raw calls; update paths/bodies to match the consolidation endpoints you are using." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Typical consolidation GETs (edit as needed)\n", + "consolidation_status = tw.get('/admin/consolidation/status')\n", + "consolidation_targets = tw.get('/admin/consolidation/targets')\n", + "consolidation_results = {\n", + " '/admin/consolidation/status': show_response(consolidation_status, '/admin/consolidation/status'),\n", + " '/admin/consolidation/targets': show_response(consolidation_targets, '/admin/consolidation/targets'),\n", + "}\n", + "consolidation_results\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example consolidation POST (edit endpoint/body per docs)\n", + "consolidation_body = {\n", + " # e.g. 'target': 'consolidation-host', 'action': 'push'\n", + "}\n", + "if consolidation_body:\n", + " consolidation_post = tw.post('/admin/consolidation/actions', consolidation_body)\n", + " show_response(consolidation_post, '/admin/consolidation/actions')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From ab38e3f192ddd6234c9be5491ec95bbc40997858 Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 20:21:53 +0000 Subject: [PATCH 7/9] Rename admin API notebook for clarity Renamed 'admin_api_examples.ipynb' to 'admin_api.ipynb' to better reflect its contents and improve organization. --- notebooks/{admin_api_examples.ipynb => admin_api.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename notebooks/{admin_api_examples.ipynb => admin_api.ipynb} (100%) diff --git a/notebooks/admin_api_examples.ipynb b/notebooks/admin_api.ipynb similarity index 100% rename from notebooks/admin_api_examples.ipynb rename to notebooks/admin_api.ipynb From 9bd079c4312b26c22ce02a540d3e91d7ef0563bc Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Tue, 9 Dec 2025 20:28:36 +0000 Subject: [PATCH 8/9] Add Tideway Credentials API example notebook Introduces a Jupyter notebook with example code for interacting with the Tideway Credentials API, including GET, POST, PATCH, and DELETE operations for credential types and credentials. The notebook provides setup instructions and demonstrates raw API calls using the tideway Python package. --- notebooks/credentials_api.ipynb | 180 ++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 notebooks/credentials_api.ipynb diff --git a/notebooks/credentials_api.ipynb b/notebooks/credentials_api.ipynb new file mode 100644 index 0000000..6d5cd4b --- /dev/null +++ b/notebooks/credentials_api.ipynb @@ -0,0 +1,180 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Credentials API Examples\n", + "\n", + "Raw `get`/`post`/`patch`/`delete` calls for vault credentials. Adjust endpoints/bodies per your appliance (see BMC docs)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "- Install tideway (e.g. `pip install -e .` from repo root).\n", + "- Set `TARGET` and `TOKEN` below. Do **not** commit secrets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Credential type endpoints (GET)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# List credential types\n", + "cred_types = tw.get('/vault/credential_types')\n", + "show_response(cred_types, '/vault/credential_types')\n", + "\n", + "# Get specific credential type (edit name)\n", + "cred_type_name = 'SNMPv2' # example\n", + "cred_type = tw.get(f\"/vault/credential_types/{cred_type_name}\")\n", + "show_response(cred_type, f\"/vault/credential_types/{cred_type_name}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Credential endpoints (GET/POST/PATCH/DELETE)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# List credentials\n", + "creds = tw.get('/vault/credentials')\n", + "show_response(creds, '/vault/credentials')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "937b16bd", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Get a credential by id\n", + "if cred_id:\n", + " cred = tw.get(f\"/vault/credentials/{cred_id}\")\n", + " show_response(cred, f\"/vault/credentials/{cred_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "719c9493", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Create a credential (edit body)\n", + "new_cred_body = {\n", + " # 'name': 'example', 'type': 'SNMPv2', 'attributes': {...}\n", + "}\n", + "if new_cred_body:\n", + " created = tw.post('/vault/credentials', new_cred_body)\n", + " show_response(created, '/vault/credentials (POST)')\n", + " cred_id = created.json().get('id') if created.ok else None\n", + "else:\n", + " cred_id = None\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8436d9f9", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Patch a credential (edit body)\n", + "patch_body = {\n", + " # 'attributes': {'community': 'public'}\n", + "}\n", + "if cred_id and patch_body:\n", + " patched = tw.patch(f\"/vault/credentials/{cred_id}\", patch_body)\n", + " show_response(patched, f\"/vault/credentials/{cred_id} (PATCH)\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69333ed9", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Delete a credential\n", + "if cred_id:\n", + " deleted = tw.delete(f\"/vault/credentials/{cred_id}\")\n", + " show_response(deleted, f\"/vault/credentials/{cred_id} (DELETE)\")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 67ee438e3b12480386e7862327817f66d9c8fdd4 Mon Sep 17 00:00:00 2001 From: Wes Moskal-Fitzpatrick Date: Wed, 10 Dec 2025 12:57:56 +0000 Subject: [PATCH 9/9] Add example notebooks for Tideway REST APIs Introduces six Jupyter notebooks demonstrating usage of the Tideway Python client for Data, Discovery, Events, Knowledge, Models, Taxonomy, and Topology REST APIs. Each notebook provides setup instructions, helper and direct API call examples, and usage notes for common endpoints, facilitating development and integration with BMC Helix Discovery. --- notebooks/data_api.ipynb | 369 ++++++++++++++++++++++++++ notebooks/discovery_api.ipynb | 380 +++++++++++++++++++++++++++ notebooks/events_knowledge_api.ipynb | 205 +++++++++++++++ notebooks/models_api.ipynb | 276 +++++++++++++++++++ notebooks/taxonomy_api.ipynb | 184 +++++++++++++ notebooks/topology_api.ipynb | 215 +++++++++++++++ 6 files changed, 1629 insertions(+) create mode 100644 notebooks/data_api.ipynb create mode 100644 notebooks/discovery_api.ipynb create mode 100644 notebooks/events_knowledge_api.ipynb create mode 100644 notebooks/models_api.ipynb create mode 100644 notebooks/taxonomy_api.ipynb create mode 100644 notebooks/topology_api.ipynb diff --git a/notebooks/data_api.ipynb b/notebooks/data_api.ipynb new file mode 100644 index 0000000..2fec02a --- /dev/null +++ b/notebooks/data_api.ipynb @@ -0,0 +1,369 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Data API Examples\n", + "\n", + "Data search, node lookups, kinds, and partitions via `tideway.data`. Reference: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "- Install tideway (e.g. `pip install -e .` from repo root).\n", + "- Set `TARGET` and `TOKEN` below. Do **not** commit secrets.\n", + "- Adjust queries/endpoints to match your appliance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0c6fdc6", + "metadata": {}, + "outputs": [], + "source": [ + "import tideway\n", + "from urllib.parse import quote\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "data = tw.data()\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "id": "78471131", + "metadata": {}, + "source": [ + "## Data search endpoints\n", + "- Use TPL search syntax for `/data/search`.\n", + "- `format` supports `object`, `tree`, or default tabular rows.\n", + "- BMC doc reference above for query capabilities.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac07b485", + "metadata": {}, + "outputs": [], + "source": [ + "# GET /data/search with query string\n", + "search_query = \"search Host show name, os_type, ip_addr\" # edit query per docs\n", + "search_query_encoded = quote(search_query)\n", + "\n", + "helper_search = data.search(search_query, format=\"object\", limit=50, bulk=False)\n", + "direct_search = data.get(f\"/data/search?query={search_query_encoded}&format=object&limit=50\")\n", + "\n", + "search_calls = {\n", + " \"helper\": show_response(helper_search, \"/data/search?format=object\"),\n", + " \"direct_get\": show_response(direct_search, \"/data/search (GET)\"),\n", + "}\n", + "search_calls\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b91e755", + "metadata": {}, + "outputs": [], + "source": [ + "# Bulk search to iterate through paginated results\n", + "bulk_results = data.search_bulk(search_query, format=\"object\", limit=200, record_limit=200)\n", + "\n", + "if hasattr(bulk_results, \"ok\"):\n", + " bulk_preview = show_response(bulk_results, \"/data/search bulk\")\n", + "else:\n", + " bulk_preview = bulk_results[:5] # preview first rows/objects\n", + "\n", + "direct_first_page = data.get(f\"/data/search?query={search_query_encoded}&format=object&limit=200\")\n", + "\n", + "bulk_calls = {\n", + " \"helper_bulk_preview\": bulk_preview,\n", + " \"direct_first_page\": show_response(direct_first_page, \"/data/search (GET, first page)\"),\n", + "}\n", + "bulk_calls\n" + ] + }, + { + "cell_type": "markdown", + "id": "0efaa56a", + "metadata": {}, + "source": [ + "## POST /data/search (long queries or alternate formats)\n", + "Use POST when the search string is long or when sending JSON payloads.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "109f3ae1", + "metadata": {}, + "outputs": [], + "source": [ + "long_query_body = {\n", + " \"query\": \"search SoftwareInstance where type matches \\\"Database\\\" show name, version, host.name\"\n", + "}\n", + "\n", + "helper_long_query = data.search(long_query_body, format=\"tree\", limit=50, bulk=False)\n", + "direct_long_query = data.post('/data/search', long_query_body)\n", + "\n", + "long_query_calls = {\n", + " \"/data/search (helper, tree)\": show_response(helper_long_query, \"/data/search (POST, tree)\"),\n", + " \"/data/search (direct POST)\": show_response(direct_long_query, \"/data/search (POST)\"),\n", + "}\n", + "long_query_calls\n" + ] + }, + { + "cell_type": "markdown", + "id": "7b7eac0d", + "metadata": {}, + "source": [ + "## Condition endpoints\n", + "- Discover available condition parameters/templates first.\n", + "- Fill `condition_body` with a condition and attributes per the API schema.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43ce63d4", + "metadata": {}, + "outputs": [], + "source": [ + "condition_params = data.get_data_condition_params()\n", + "condition_templates = data.get_data_condition_templates\n", + "\n", + "direct_condition_params = data.get('/data/condition/params')\n", + "direct_condition_templates = data.get('/data/condition/templates')\n", + "\n", + "condition_meta = {\n", + " 'helpers': {\n", + " '/data/condition/params': show_response(condition_params, '/data/condition/params'),\n", + " '/data/condition/templates': show_response(condition_templates, '/data/condition/templates'),\n", + " },\n", + " 'direct': {\n", + " '/data/condition/params (GET)': show_response(direct_condition_params, '/data/condition/params'),\n", + " '/data/condition/templates (GET)': show_response(direct_condition_templates, '/data/condition/templates'),\n", + " }\n", + "}\n", + "condition_meta\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ded2f1e4", + "metadata": {}, + "outputs": [], + "source": [ + "condition_body = {\n", + " # Example shape - adjust to your condition\n", + " # \"condition\": {\"attribute\": \"Host:os\", \"operator\": \"=\", \"value\": \"Linux\"},\n", + " # \"attributes\": [\"name\", \"os\", \"serial_number\"],\n", + " # \"parameters\": {},\n", + "}\n", + "\n", + "if condition_body:\n", + " condition_results_helper = data.post_data_condition(condition_body)\n", + " condition_results_direct = data.post('/data/condition', condition_body)\n", + " condition_results = {\n", + " 'helper': show_response(condition_results_helper, \"/data/condition (helper)\"),\n", + " 'direct': show_response(condition_results_direct, \"/data/condition (direct POST)\"),\n", + " }\n", + "else:\n", + " condition_results = \"Set condition_body to post /data/condition\"\n", + "condition_results\n" + ] + }, + { + "cell_type": "markdown", + "id": "d7c84536", + "metadata": {}, + "source": [ + "## Node lookups and graphs\n", + "Set `node_id` to inspect node state and graph relationships.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40acb372", + "metadata": {}, + "outputs": [], + "source": [ + "node_id = '' # e.g. '1234567890abcdef'\n", + "\n", + "if node_id:\n", + " node_details_helper = data.get_data_nodes(node_id, relationships=True)\n", + " node_graph_helper = data.get_data_nodes_graph(node_id, focus=\"software-connected\", complete=False)\n", + "\n", + " node_details_direct = data.get(f\"/data/nodes/{node_id}?relationships=true\")\n", + " node_graph_direct = data.get(f\"/data/nodes/{node_id}/graph?focus=software-connected&complete=false\")\n", + "\n", + " node_info = {\n", + " f\"/data/nodes/{node_id} (helper)\": show_response(node_details_helper, f\"/data/nodes/{node_id}?relationships=true\"),\n", + " f\"/data/nodes/{node_id}/graph (helper)\": show_response(node_graph_helper, f\"/data/nodes/{node_id}/graph\"),\n", + " f\"/data/nodes/{node_id} (direct)\": show_response(node_details_direct, f\"/data/nodes/{node_id}?relationships=true\"),\n", + " f\"/data/nodes/{node_id}/graph (direct)\": show_response(node_graph_direct, f\"/data/nodes/{node_id}/graph\"),\n", + " }\n", + "else:\n", + " node_info = \"Set node_id to inspect node details.\"\n", + "node_info\n" + ] + }, + { + "cell_type": "markdown", + "id": "2e8262bc", + "metadata": {}, + "source": [ + "## Node kinds and attribute values\n", + "Use these to inspect specific node kinds and enumerate attribute values.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6770c398", + "metadata": {}, + "outputs": [], + "source": [ + "kind = 'Host' # edit node kind per docs\n", + "attribute = 'os' # attribute to enumerate values for\n", + "\n", + "kind_resp_helper = data.get_data_kinds(kind, format=\"object\", limit=25)\n", + "kind_values_resp_helper = data.get_data_kinds_values(kind, attribute)\n", + "\n", + "kind_resp_direct = data.get(f\"/data/kinds/{kind}?format=object&limit=25\")\n", + "kind_values_resp_direct = data.get(f\"/data/kinds/{kind}/values/{attribute}\")\n", + "\n", + "kind_calls = {\n", + " f\"/data/kinds/{kind} (helper)\": show_response(kind_resp_helper, f\"/data/kinds/{kind}?format=object\"),\n", + " f\"/data/kinds/{kind} (direct)\": show_response(kind_resp_direct, f\"/data/kinds/{kind}?format=object&limit=25\"),\n", + " f\"/data/kinds/{kind}/values/{attribute} (helper)\": show_response(kind_values_resp_helper, f\"/data/kinds/{kind}/values/{attribute}\"),\n", + " f\"/data/kinds/{kind}/values/{attribute} (direct)\": show_response(kind_values_resp_direct, f\"/data/kinds/{kind}/values/{attribute}\"),\n", + "}\n", + "kind_calls\n" + ] + }, + { + "cell_type": "markdown", + "id": "53aa2f36", + "metadata": {}, + "source": [ + "## Partitions, candidates, and external consumers\n", + "GET calls are safe to run; POST bodies are placeholders until filled.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb528ce2", + "metadata": {}, + "outputs": [], + "source": [ + "partitions = data.get_data_partitions\n", + "external_consumers = data.get_data_external_consumers\n", + "\n", + "direct_partitions = data.get('/data/partitions')\n", + "direct_external_consumers = data.get('/data/external_consumers')\n", + "\n", + "data_admin_calls = {\n", + " '/data/partitions (helper)': show_response(partitions, '/data/partitions'),\n", + " '/data/partitions (direct)': show_response(direct_partitions, '/data/partitions'),\n", + " '/data/external_consumers (helper)': show_response(external_consumers, '/data/external_consumers'),\n", + " '/data/external_consumers (direct)': show_response(direct_external_consumers, '/data/external_consumers'),\n", + "}\n", + "data_admin_calls\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35542e1e", + "metadata": {}, + "outputs": [], + "source": [ + "partition_body = {\n", + " # 'name': 'example_partition',\n", + " # 'label': 'Example partition description',\n", + "}\n", + "candidate_body = {\n", + " # 'ip': '10.0.0.1',\n", + " # 'dns_name': 'host.example.com',\n", + "}\n", + "external_consumer_body = {\n", + " # 'name': 'consumer_id',\n", + " # 'endpoint': 'https://example.com/ingest',\n", + "}\n", + "\n", + "post_responses = {}\n", + "if partition_body:\n", + " created_partition_helper = data.post_data_partitions(partition_body)\n", + " created_partition_direct = data.post('/data/partitions', partition_body)\n", + " post_responses['/data/partitions (helper POST)'] = show_response(created_partition_helper, '/data/partitions (POST)')\n", + " post_responses['/data/partitions (direct POST)'] = show_response(created_partition_direct, '/data/partitions (POST)')\n", + "\n", + "if candidate_body:\n", + " best_candidate_helper = data.post_data_candidate(candidate_body)\n", + " best_candidate_direct = data.post('/data/candidate', candidate_body)\n", + " post_responses['/data/candidate (helper POST)'] = show_response(best_candidate_helper, '/data/candidate')\n", + " post_responses['/data/candidate (direct POST)'] = show_response(best_candidate_direct, '/data/candidate')\n", + "\n", + "if external_consumer_body:\n", + " created_consumer_helper = data.post_data_external_consumer(external_consumer_body)\n", + " created_consumer_direct = data.post('/data/external_consumers', external_consumer_body)\n", + " post_responses['/data/external_consumers (helper POST)'] = show_response(created_consumer_helper, '/data/external_consumers')\n", + " post_responses['/data/external_consumers (direct POST)'] = show_response(created_consumer_direct, '/data/external_consumers')\n", + "\n", + "post_responses or \"Set request bodies to run POST examples.\"\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/discovery_api.ipynb b/notebooks/discovery_api.ipynb new file mode 100644 index 0000000..61a33fc --- /dev/null +++ b/notebooks/discovery_api.ipynb @@ -0,0 +1,380 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Discovery API Examples\n", + "\n", + "Helper methods paired with direct `get`/`post`/`patch` calls for discovery endpoints. Reference: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "- Install tideway (e.g. `pip install -e .` from repo root).\n", + "- Set `TARGET` and `TOKEN` below. Do **not** commit secrets.\n", + "- Adjust request bodies to match your appliance and the published API.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "discovery = tw.discovery()\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discovery status\n", + "Helper vs direct calls for the discovery state.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "status_helper = discovery.get_discovery\n", + "status_direct = discovery.get('/discovery')\n", + "\n", + "status_calls = {\n", + " '/discovery (helper)': show_response(status_helper, '/discovery'),\n", + " '/discovery (direct GET)': show_response(status_direct, '/discovery'),\n", + "}\n", + "status_calls\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discovery_patch_body = {\n", + " # 'state': 'start', # or 'stop'\n", + " # 'reason': 'Maintenance complete',\n", + "}\n", + "\n", + "if discovery_patch_body:\n", + " discovery_patch_helper = discovery.patch_discovery(discovery_patch_body)\n", + " discovery_patch_direct = discovery.patch('/discovery', discovery_patch_body)\n", + " discovery_patch_calls = {\n", + " '/discovery (helper PATCH)': show_response(discovery_patch_helper, '/discovery'),\n", + " '/discovery (direct PATCH)': show_response(discovery_patch_direct, '/discovery'),\n", + " }\n", + "else:\n", + " discovery_patch_calls = 'Set discovery_patch_body to start/stop discovery'\n", + "discovery_patch_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Provider and cloud metadata\n", + "Metadata helpers vs direct calls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "provider_meta_helper = discovery.get_discovery_api_provider_metadata\n", + "provider_meta_direct = discovery.get('/discovery/api_provider_metadata')\n", + "\n", + "cloud_meta_helper = discovery.get_discovery_api_cloud_metadata\n", + "cloud_meta_direct = discovery.get('/discovery/cloud_metadata')\n", + "\n", + "metadata_calls = {\n", + " '/discovery/api_provider_metadata (helper)': show_response(provider_meta_helper, '/discovery/api_provider_metadata'),\n", + " '/discovery/api_provider_metadata (direct)': show_response(provider_meta_direct, '/discovery/api_provider_metadata'),\n", + " '/discovery/cloud_metadata (helper)': show_response(cloud_meta_helper, '/discovery/cloud_metadata'),\n", + " '/discovery/cloud_metadata (direct)': show_response(cloud_meta_direct, '/discovery/cloud_metadata'),\n", + "}\n", + "metadata_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Excludes\n", + "List and manage excludes via helpers and direct calls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "excludes_list_helper = discovery.get_discovery_excludes\n", + "excludes_list_direct = discovery.get('/discovery/excludes')\n", + "\n", + "exclude_calls = {\n", + " '/discovery/excludes (helper)': show_response(excludes_list_helper, '/discovery/excludes'),\n", + " '/discovery/excludes (direct)': show_response(excludes_list_direct, '/discovery/excludes'),\n", + "}\n", + "\n", + "exclude_body = {\n", + " # 'value': '10.0.0.0/24',\n", + " # 'reason': 'Example exclude',\n", + "}\n", + "exclude_id = '' # set to an existing exclude id for patch/delete\n", + "\n", + "if exclude_body:\n", + " exclude_post_helper = discovery.post_discovery_exclude(exclude_body)\n", + " exclude_post_direct = discovery.post('/discovery/excludes', exclude_body)\n", + " exclude_calls['/discovery/excludes (helper POST)'] = show_response(exclude_post_helper, '/discovery/excludes')\n", + " exclude_calls['/discovery/excludes (direct POST)'] = show_response(exclude_post_direct, '/discovery/excludes')\n", + "\n", + "if exclude_id and exclude_body:\n", + " exclude_patch_helper = discovery.patch_discovery_exclude(exclude_id, exclude_body)\n", + " exclude_patch_direct = discovery.patch(f\"/discovery/excludes/{exclude_id}\", exclude_body)\n", + " exclude_calls[f\"/discovery/excludes/{exclude_id} (helper PATCH)\"] = show_response(exclude_patch_helper, f\"/discovery/excludes/{exclude_id}\")\n", + " exclude_calls[f\"/discovery/excludes/{exclude_id} (direct PATCH)\"] = show_response(exclude_patch_direct, f\"/discovery/excludes/{exclude_id}\")\n", + "\n", + "if exclude_id:\n", + " exclude_delete_helper = discovery.delete_discovery_exclude(exclude_id)\n", + " exclude_delete_direct = discovery.delete(f\"/discovery/excludes/{exclude_id}\")\n", + " exclude_calls[f\"/discovery/excludes/{exclude_id} (helper DELETE)\"] = show_response(exclude_delete_helper, f\"/discovery/excludes/{exclude_id}\")\n", + " exclude_calls[f\"/discovery/excludes/{exclude_id} (direct DELETE)\"] = show_response(exclude_delete_direct, f\"/discovery/excludes/{exclude_id}\")\n", + "\n", + "exclude_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discovery runs\n", + "List runs, create or update run state via helper vs direct calls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "runs_helper = discovery.get_discovery_runs\n", + "runs_direct = discovery.get('/discovery/runs')\n", + "\n", + "run_body = {\n", + " # 'type': 'snapshot',\n", + " # 'label': 'Example run',\n", + " # 'endpoints': ['10.0.0.0/24'],\n", + "}\n", + "run_patch_body = {\n", + " # 'state': 'cancelled',\n", + "}\n", + "run_id = '' # set to an existing run id for patching\n", + "\n", + "run_calls = {\n", + " '/discovery/runs (helper)': show_response(runs_helper, '/discovery/runs'),\n", + " '/discovery/runs (direct)': show_response(runs_direct, '/discovery/runs'),\n", + "}\n", + "\n", + "if run_body:\n", + " run_post_helper = discovery.post_discovery_run(run_body)\n", + " run_post_direct = discovery.post('/discovery/runs', run_body)\n", + " run_calls['/discovery/runs (helper POST)'] = show_response(run_post_helper, '/discovery/runs')\n", + " run_calls['/discovery/runs (direct POST)'] = show_response(run_post_direct, '/discovery/runs')\n", + "\n", + "if run_id and run_patch_body:\n", + " run_patch_helper = discovery.patch_discovery_run(run_id, run_patch_body)\n", + " run_patch_direct = discovery.patch(f\"/discovery/runs/{run_id}\", run_patch_body)\n", + " run_calls[f\"/discovery/runs/{run_id} (helper PATCH)\"] = show_response(run_patch_helper, f\"/discovery/runs/{run_id}\")\n", + " run_calls[f\"/discovery/runs/{run_id} (direct PATCH)\"] = show_response(run_patch_direct, f\"/discovery/runs/{run_id}\")\n", + "\n", + "run_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run results and inferred devices\n", + "Requires a `run_id`; choose result type or inferred kind per docs.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run_id_for_results = '' # e.g. '1234567890abcdef'\n", + "result_type = 'Success'\n", + "inferred_kind = '' # e.g. 'Host'\n", + "\n", + "if run_id_for_results:\n", + " run_results_helper = discovery.get_discovery_run_results(run_id_for_results, result_type)\n", + " run_results_direct = discovery.get(f\"/discovery/runs/{run_id_for_results}/results/{result_type}\")\n", + "\n", + " inferred_helper = discovery.get_discovery_run_inferred(run_id_for_results, inferred_kind)\n", + " inferred_direct = discovery.get(f\"/discovery/runs/{run_id_for_results}/inferred\" + (f\"/{inferred_kind}\" if inferred_kind else ''))\n", + "\n", + " results_calls = {\n", + " f\"/discovery/runs/{run_id_for_results}/results/{result_type} (helper)\": show_response(run_results_helper, f\"/discovery/runs/{run_id_for_results}/results/{result_type}\"),\n", + " f\"/discovery/runs/{run_id_for_results}/results/{result_type} (direct)\": show_response(run_results_direct, f\"/discovery/runs/{run_id_for_results}/results/{result_type}\"),\n", + " f\"/discovery/runs/{run_id_for_results}/inferred (helper)\": show_response(inferred_helper, f\"/discovery/runs/{run_id_for_results}/inferred\"),\n", + " f\"/discovery/runs/{run_id_for_results}/inferred (direct)\": show_response(inferred_direct, f\"/discovery/runs/{run_id_for_results}/inferred\"),\n", + " }\n", + "else:\n", + " results_calls = 'Set run_id_for_results to inspect results and inferred devices.'\n", + "results_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scheduled runs\n", + "List, create, update, or delete scheduled runs (helper vs direct).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "schedules_helper = discovery.get_discovery_run_schedules\n", + "schedules_direct = discovery.get('/discovery/runs/scheduled')\n", + "\n", + "schedule_body = {\n", + " # 'label': 'Weekly scan',\n", + " # 'cron': '0 2 * * 1',\n", + " # 'endpoints': ['10.0.0.0/24'],\n", + "}\n", + "schedule_patch_body = {\n", + " # 'label': 'Updated label',\n", + "}\n", + "schedule_id = '' # set to a scheduled run id for patch/delete\n", + "\n", + "schedule_calls = {\n", + " '/discovery/runs/scheduled (helper)': show_response(schedules_helper, '/discovery/runs/scheduled'),\n", + " '/discovery/runs/scheduled (direct)': show_response(schedules_direct, '/discovery/runs/scheduled'),\n", + "}\n", + "\n", + "if schedule_body:\n", + " schedule_post_helper = discovery.post_discovery_run_schedule(schedule_body)\n", + " schedule_post_direct = discovery.post('/discovery/runs/scheduled', schedule_body)\n", + " schedule_calls['/discovery/runs/scheduled (helper POST)'] = show_response(schedule_post_helper, '/discovery/runs/scheduled')\n", + " schedule_calls['/discovery/runs/scheduled (direct POST)'] = show_response(schedule_post_direct, '/discovery/runs/scheduled')\n", + "\n", + "if schedule_id and schedule_patch_body:\n", + " schedule_patch_helper = discovery.patch_discovery_run_schedule(schedule_id, schedule_patch_body)\n", + " schedule_patch_direct = discovery.patch(f\"/discovery/runs/scheduled/{schedule_id}\", schedule_patch_body)\n", + " schedule_calls[f\"/discovery/runs/scheduled/{schedule_id} (helper PATCH)\"] = show_response(schedule_patch_helper, f\"/discovery/runs/scheduled/{schedule_id}\")\n", + " schedule_calls[f\"/discovery/runs/scheduled/{schedule_id} (direct PATCH)\"] = show_response(schedule_patch_direct, f\"/discovery/runs/scheduled/{schedule_id}\")\n", + "\n", + "if schedule_id:\n", + " schedule_delete_helper = discovery.delete_discovery_run_schedule(schedule_id)\n", + " schedule_delete_direct = discovery.delete(f\"/discovery/runs/scheduled/{schedule_id}\")\n", + " schedule_calls[f\"/discovery/runs/scheduled/{schedule_id} (helper DELETE)\"] = show_response(schedule_delete_helper, f\"/discovery/runs/scheduled/{schedule_id}\")\n", + " schedule_calls[f\"/discovery/runs/scheduled/{schedule_id} (direct DELETE)\"] = show_response(schedule_delete_direct, f\"/discovery/runs/scheduled/{schedule_id}\")\n", + "\n", + "schedule_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Outposts\n", + "List, create, or delete Outposts via helpers vs direct calls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "outposts_helper = discovery.get_discovery_outposts\n", + "outposts_direct = discovery.get('/discovery/outposts')\n", + "\n", + "outpost_body = {\n", + " # 'name': 'example-outpost',\n", + " # 'address': 'outpost.example.com',\n", + "}\n", + "outpost_id = '' # set to an existing outpost id for delete\n", + "\n", + "outpost_calls = {\n", + " '/discovery/outposts (helper)': show_response(outposts_helper, '/discovery/outposts'),\n", + " '/discovery/outposts (direct)': show_response(outposts_direct, '/discovery/outposts'),\n", + "}\n", + "\n", + "if outpost_body:\n", + " outpost_post_helper = discovery.post_discovery_outpost(outpost_body)\n", + " outpost_post_direct = discovery.post('/discovery/outposts', outpost_body)\n", + " outpost_calls['/discovery/outposts (helper POST)'] = show_response(outpost_post_helper, '/discovery/outposts')\n", + " outpost_calls['/discovery/outposts (direct POST)'] = show_response(outpost_post_direct, '/discovery/outposts')\n", + "\n", + "if outpost_id:\n", + " outpost_delete_helper = discovery.delete_discovery_outpost(outpost_id)\n", + " outpost_delete_direct = discovery.delete(f\"/discovery/outposts/{outpost_id}\")\n", + " outpost_calls[f\"/discovery/outposts/{outpost_id} (helper DELETE)\"] = show_response(outpost_delete_helper, f\"/discovery/outposts/{outpost_id}\")\n", + " outpost_calls[f\"/discovery/outposts/{outpost_id} (direct DELETE)\"] = show_response(outpost_delete_direct, f\"/discovery/outposts/{outpost_id}\")\n", + "\n", + "outpost_calls\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/events_knowledge_api.ipynb b/notebooks/events_knowledge_api.ipynb new file mode 100644 index 0000000..583d2d8 --- /dev/null +++ b/notebooks/events_knowledge_api.ipynb @@ -0,0 +1,205 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Events & Knowledge API Examples\n", + "\n", + "Helper methods paired with direct `get`/`post` calls for events and knowledge endpoints. Reference: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "- Install tideway (e.g. `pip install -e .` from repo root).\n", + "- Set `TARGET` and `TOKEN` below. Do **not** commit secrets.\n", + "- Adjust request bodies and file paths to match your appliance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tideway\n", + "from pathlib import Path\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "events = tw.events()\n", + "knowledge = tw.knowledge()\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Events\n", + "Send events using helper vs direct POST.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "event_body = {\n", + " # 'status': 'warning',\n", + " # 'message': 'Example event from notebook',\n", + " # 'source': 'notebook',\n", + "}\n", + "\n", + "if event_body:\n", + " event_helper = events.post_events(event_body)\n", + " event_direct = events.post('/events', event_body)\n", + " event_calls = {\n", + " '/events (helper POST)': show_response(event_helper, '/events'),\n", + " '/events (direct POST)': show_response(event_direct, '/events'),\n", + " }\n", + "else:\n", + " event_calls = 'Set event_body to post /events.'\n", + "event_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Knowledge state\n", + "Helpers vs direct GET for knowledge info and upload status.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "knowledge_helper = knowledge.get_knowledge_property\n", + "knowledge_direct = knowledge.get('/knowledge')\n", + "\n", + "knowledge_status_helper = knowledge.get_knowledge_status_property\n", + "knowledge_status_direct = knowledge.get('/knowledge/status')\n", + "\n", + "knowledge_calls = {\n", + " '/knowledge (helper)': show_response(knowledge_helper, '/knowledge'),\n", + " '/knowledge (direct GET)': show_response(knowledge_direct, '/knowledge'),\n", + " '/knowledge/status (helper)': show_response(knowledge_status_helper, '/knowledge/status'),\n", + " '/knowledge/status (direct GET)': show_response(knowledge_status_direct, '/knowledge/status'),\n", + "}\n", + "knowledge_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Upload knowledge (TKU/pattern module)\n", + "Provide a filename and local path to upload; set `activate`/`allow_restart` flags per your process.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "upload_filename = 'tku.zip' # name shown in the endpoint URL\n", + "upload_path = '' # local path to the TKU file\n", + "activate = True\n", + "allow_restart = False\n", + "\n", + "if upload_path and Path(upload_path).exists():\n", + " upload_helper = knowledge.post_knowledge(upload_filename, upload_path, activate=activate, allow_restart=allow_restart)\n", + " # Direct call mirrors the helper: set params then send multipart file\n", + " knowledge.params['activate'] = activate\n", + " knowledge.params['allow_restart'] = allow_restart\n", + " with open(upload_path, 'rb') as upload_file:\n", + " upload_direct = knowledge.post(f\"/knowledge/{upload_filename}\", files={'file': upload_file}, response='text/html')\n", + " upload_calls = {\n", + " f\"/knowledge/{upload_filename} (helper POST)\": show_response(upload_helper, f\"/knowledge/{upload_filename}\"),\n", + " f\"/knowledge/{upload_filename} (direct POST)\": show_response(upload_direct, f\"/knowledge/{upload_filename}\"),\n", + " }\n", + "else:\n", + " upload_calls = 'Set upload_path to a TKU file to post /knowledge/{filename}.'\n", + "upload_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Trigger patterns\n", + "List knowledge trigger patterns via helper vs direct GET.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lookup_data_sources = None # e.g. True to include lookup data sources\n", + "\n", + "# Helper allows passing lookup_data_sources param\n", + "trigger_patterns_helper = knowledge.get_knowledge_trigger_patterns(lookup_data_sources=lookup_data_sources)\n", + "\n", + "# Direct GET (set params manually if you need lookup_data_sources)\n", + "if lookup_data_sources is not None:\n", + " knowledge.params['lookup_data_sources'] = lookup_data_sources\n", + "trigger_patterns_direct = knowledge.get('/knowledge/trigger_patterns')\n", + "\n", + "trigger_calls = {\n", + " '/knowledge/trigger_patterns (helper)': show_response(trigger_patterns_helper, '/knowledge/trigger_patterns'),\n", + " '/knowledge/trigger_patterns (direct)': show_response(trigger_patterns_direct, '/knowledge/trigger_patterns'),\n", + "}\n", + "trigger_calls\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/models_api.ipynb b/notebooks/models_api.ipynb new file mode 100644 index 0000000..9a4b21a --- /dev/null +++ b/notebooks/models_api.ipynb @@ -0,0 +1,276 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Models API Examples\n\nHelper methods paired with direct calls for model CRUD, topology, and node queries. Reference: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n- Install tideway (e.g. `pip install -e .` from repo root).\n- Set `TARGET` and `TOKEN` below. Do **not** commit secrets.\n- Adjust request bodies/ids per your appliance models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "models = tw.models()\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List or filter models\nHelper vs direct GET with optional filters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "model_filters = {\n", + " # 'name': 'Example',\n", + " # 'type': 'business_service',\n", + " # 'kind': 'custom',\n", + "}\n", + "\n", + "models_helper = models.get_model(**model_filters)\n", + "models_direct = models.get('/models')\n", + "\n", + "model_list_calls = {\n", + " '/models (helper)': show_response(models_helper, '/models'),\n", + " '/models (direct GET)': show_response(models_direct, '/models'),\n", + "}\n", + "model_list_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create, update, delete models\nProvide bodies/ids to exercise helper vs direct calls." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "model_body = {\n", + " # 'name': 'Example Model',\n", + " # 'type': 'business_service',\n", + " # 'definition': {},\n", + "}\n", + "model_key = '' # set to a model key for patch/delete\n", + "model_patch_body = {\n", + " # 'favorite': True,\n", + "}\n", + "\n", + "model_calls = {}\n", + "\n", + "if model_body:\n", + " create_helper = models.post_model(model_body)\n", + " create_direct = models.post('/models', model_body)\n", + " model_calls['/models (helper POST)'] = show_response(create_helper, '/models')\n", + " model_calls['/models (direct POST)'] = show_response(create_direct, '/models')\n", + "\n", + "if model_key and model_patch_body:\n", + " patch_helper = models.patch_model(model_key, model_patch_body)\n", + " patch_direct = models.patch(f\"/models/{model_key}\", model_patch_body)\n", + " model_calls[f\"/models/{model_key} (helper PATCH)\"] = show_response(patch_helper, f\"/models/{model_key}\")\n", + " model_calls[f\"/models/{model_key} (direct PATCH)\"] = show_response(patch_direct, f\"/models/{model_key}\")\n", + "\n", + "if model_key:\n", + " delete_helper = models.delete_model(model_key)\n", + " delete_direct = models.delete(f\"/models/{model_key}\")\n", + " model_calls[f\"/models/{model_key} (helper DELETE)\"] = show_response(delete_helper, f\"/models/{model_key}\")\n", + " model_calls[f\"/models/{model_key} (direct DELETE)\"] = show_response(delete_direct, f\"/models/{model_key}\")\n", + "\n", + "model_calls or 'Set model_body/model_key to run create/update/delete.'\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bulk/multi model operations\nUse `/models/multi` for multi-action payloads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "multi_body = {\n", + " # 'create': [ ... ],\n", + " # 'update': [ ... ],\n", + "}\n", + "\n", + "if multi_body:\n", + " multi_helper = models.post_model_multi(multi_body)\n", + " multi_direct = models.post('/models/multi', multi_body)\n", + " multi_calls = {\n", + " '/models/multi (helper POST)': show_response(multi_helper, '/models/multi'),\n", + " '/models/multi (direct POST)': show_response(multi_direct, '/models/multi'),\n", + " }\n", + "else:\n", + " multi_calls = 'Set multi_body to post /models/multi.'\n", + "multi_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model details by key\nHelper vs direct for model definition and topology/node info." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "model_key_lookup = '' # e.g. 'Model-1234'\n", + "attributes = None # e.g. 'name,kind' for topology\n", + "node_kind = None # e.g. 'Host'\n", + "\n", + "if model_key_lookup:\n", + " model_def_helper = models.get_model_key(model_key_lookup)\n", + " model_def_direct = models.get(f\"/models/{model_key_lookup}\")\n", + "\n", + " if attributes:\n", + " models.params['attributes'] = attributes\n", + " topo_helper = models.get_model_topology(model_key_lookup, attributes=attributes)\n", + " topo_direct = models.get(f\"/models/{model_key_lookup}/topology\")\n", + "\n", + " nodecount_helper = models.get_model_nodecount(model_key_lookup)\n", + " nodecount_direct = models.get(f\"/models/{model_key_lookup}/nodecount\")\n", + "\n", + " nodes_helper = models.get_model_nodes(model_key_lookup, format='object', limit=25, kind=node_kind)\n", + " nodes_direct = models.get(f\"/models/{model_key_lookup}/nodes\" + (f\"/{node_kind}\" if node_kind else ''))\n", + "\n", + " model_detail_calls = {\n", + " f\"/models/{model_key_lookup} (helper)\": show_response(model_def_helper, f\"/models/{model_key_lookup}\"),\n", + " f\"/models/{model_key_lookup} (direct)\": show_response(model_def_direct, f\"/models/{model_key_lookup}\"),\n", + " f\"/models/{model_key_lookup}/topology (helper)\": show_response(topo_helper, f\"/models/{model_key_lookup}/topology\"),\n", + " f\"/models/{model_key_lookup}/topology (direct)\": show_response(topo_direct, f\"/models/{model_key_lookup}/topology\"),\n", + " f\"/models/{model_key_lookup}/nodecount (helper)\": show_response(nodecount_helper, f\"/models/{model_key_lookup}/nodecount\"),\n", + " f\"/models/{model_key_lookup}/nodecount (direct)\": show_response(nodecount_direct, f\"/models/{model_key_lookup}/nodecount\"),\n", + " f\"/models/{model_key_lookup}/nodes (helper)\": show_response(nodes_helper, f\"/models/{model_key_lookup}/nodes\"),\n", + " f\"/models/{model_key_lookup}/nodes (direct)\": show_response(nodes_direct, f\"/models/{model_key_lookup}/nodes\"),\n", + " }\n", + "else:\n", + " model_detail_calls = 'Set model_key_lookup to inspect a model definition/topology/nodes.'\n", + "model_detail_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model lookup by node id\nHelper vs direct for models tied to a node id." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "node_id = '' # e.g. '1234567890abcdef'\n", + "expand_related = None # e.g. True\n", + "node_kind = None # optional kind filter for nodes\n", + "\n", + "if node_id:\n", + " model_by_node_helper = models.get_model_by_node_id(node_id, expand_related=expand_related)\n", + " model_by_node_direct = models.get(f\"/models/by_node_id/{node_id}\")\n", + "\n", + " if expand_related:\n", + " models.params['expand_related'] = expand_related\n", + " topology_helper = models.get_topology_by_node_id(node_id)\n", + " topology_direct = models.get(f\"/models/by_node_id/{node_id}/topology\")\n", + "\n", + " nodecount_helper = models.get_nodecount_by_node_id(node_id)\n", + " nodecount_direct = models.get(f\"/models/by_node_id/{node_id}/nodecount\")\n", + "\n", + " nodes_helper = models.get_nodes_by_node_id(node_id, format='object', limit=25, kind=node_kind)\n", + " nodes_direct = models.get(f\"/models/by_node_id/{node_id}/nodes\" + (f\"/{node_kind}\" if node_kind else ''))\n", + "\n", + " model_by_node_calls = {\n", + " f\"/models/by_node_id/{node_id} (helper)\": show_response(model_by_node_helper, f\"/models/by_node_id/{node_id}\"),\n", + " f\"/models/by_node_id/{node_id} (direct)\": show_response(model_by_node_direct, f\"/models/by_node_id/{node_id}\"),\n", + " f\"/models/by_node_id/{node_id}/topology (helper)\": show_response(topology_helper, f\"/models/by_node_id/{node_id}/topology\"),\n", + " f\"/models/by_node_id/{node_id}/topology (direct)\": show_response(topology_direct, f\"/models/by_node_id/{node_id}/topology\"),\n", + " f\"/models/by_node_id/{node_id}/nodecount (helper)\": show_response(nodecount_helper, f\"/models/by_node_id/{node_id}/nodecount\"),\n", + " f\"/models/by_node_id/{node_id}/nodecount (direct)\": show_response(nodecount_direct, f\"/models/by_node_id/{node_id}/nodecount\"),\n", + " f\"/models/by_node_id/{node_id}/nodes (helper)\": show_response(nodes_helper, f\"/models/by_node_id/{node_id}/nodes\"),\n", + " f\"/models/by_node_id/{node_id}/nodes (direct)\": show_response(nodes_direct, f\"/models/by_node_id/{node_id}/nodes\"),\n", + " }\n", + "else:\n", + " model_by_node_calls = 'Set node_id to inspect models tied to a node id.'\n", + "model_by_node_calls\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/taxonomy_api.ipynb b/notebooks/taxonomy_api.ipynb new file mode 100644 index 0000000..ad44334 --- /dev/null +++ b/notebooks/taxonomy_api.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Taxonomy API Examples\n\nHelper methods paired with direct calls for taxonomy endpoints (sections, locales, node kinds, relationship kinds, field lists). Reference: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n- Install tideway (e.g. `pip install -e .` from repo root).\n- Set `TARGET` and `TOKEN` below. Do **not** commit secrets.\n- Adjust filters (section/locale/kind) as needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "taxonomy = tw.taxonomy()\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sections and locales\nList taxonomy sections/locales via helper vs direct GET." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "sections_helper = taxonomy.get_taxonomy_sections\n", + "sections_direct = taxonomy.get('/taxonomy/sections')\n", + "\n", + "locales_helper = taxonomy.get_taxonomy_locales\n", + "locales_direct = taxonomy.get('/taxonomy/locales')\n", + "\n", + "meta_calls = {\n", + " '/taxonomy/sections (helper)': show_response(sections_helper, '/taxonomy/sections'),\n", + " '/taxonomy/sections (direct)': show_response(sections_direct, '/taxonomy/sections'),\n", + " '/taxonomy/locales (helper)': show_response(locales_helper, '/taxonomy/locales'),\n", + " '/taxonomy/locales (direct)': show_response(locales_direct, '/taxonomy/locales'),\n", + "}\n", + "meta_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Node kinds\nFetch node kinds (optionally filtered) and specific kind or field lists." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "node_format = None # e.g. 'object'\n", + "node_section = None # e.g. 'foundation'\n", + "node_locale = None # e.g. 'en_US'\n", + "node_kind = None # e.g. 'Host'\n", + "fieldlists = False\n", + "fieldlist_name = None # e.g. 'default'\n", + "\n", + "nodekinds_helper = taxonomy.get_taxonomy_nodekind(format=node_format, section=node_section, locale=node_locale)\n", + "nodekinds_direct = taxonomy.get('/taxonomy/nodekinds')\n", + "\n", + "if node_kind:\n", + " kind_helper = taxonomy.get_taxonomy_nodekind(kind=node_kind, locale=node_locale, fieldlists=fieldlists)\n", + " kind_direct = taxonomy.get(f\"/taxonomy/nodekinds/{node_kind}\" + ('/fieldlists' if fieldlists else ''))\n", + "else:\n", + " kind_helper = kind_direct = 'Set node_kind to fetch a specific node kind or fieldlists.'\n", + "\n", + "if node_kind and fieldlist_name:\n", + " fieldlist_helper = taxonomy.get_taxonomy_nodekind_fieldlist(node_kind, fieldlist_name)\n", + " fieldlist_direct = taxonomy.get(f\"/taxonomy/nodekinds/{node_kind}/fieldlists/{fieldlist_name}\")\n", + "else:\n", + " fieldlist_helper = fieldlist_direct = 'Set node_kind and fieldlist_name to fetch a field list.'\n", + "\n", + "node_calls = {\n", + " '/taxonomy/nodekinds (helper)': show_response(nodekinds_helper, '/taxonomy/nodekinds'),\n", + " '/taxonomy/nodekinds (direct)': show_response(nodekinds_direct, '/taxonomy/nodekinds'),\n", + " 'specific_kind_helper': show_response(kind_helper, f\"/taxonomy/nodekinds/{node_kind}\" if isinstance(kind_helper, object) else 'set node_kind'),\n", + " 'specific_kind_direct': show_response(kind_direct, f\"/taxonomy/nodekinds/{node_kind}\" if isinstance(kind_direct, object) else 'set node_kind'),\n", + " 'fieldlist_helper': show_response(fieldlist_helper, f\"/taxonomy/nodekinds/{node_kind}/fieldlists/{fieldlist_name}\" if isinstance(fieldlist_helper, object) else 'set node_kind/fieldlist_name'),\n", + " 'fieldlist_direct': show_response(fieldlist_direct, f\"/taxonomy/nodekinds/{node_kind}/fieldlists/{fieldlist_name}\" if isinstance(fieldlist_direct, object) else 'set node_kind/fieldlist_name'),\n", + "}\n", + "node_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Relationship kinds\nFetch relationship kinds (optionally filtered) and specific kind details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "rel_format = None # e.g. 'object'\n", + "rel_locale = None # e.g. 'en_US'\n", + "rel_kind = None # e.g. 'Containment'\n", + "\n", + "relkinds_helper = taxonomy.get_taxonomy_relkind(format=rel_format, locale=rel_locale)\n", + "relkinds_direct = taxonomy.get('/taxonomy/relkinds')\n", + "\n", + "if rel_kind:\n", + " relkind_helper = taxonomy.get_taxonomy_relkind(kind=rel_kind, locale=rel_locale)\n", + " relkind_direct = taxonomy.get(f\"/taxonomy/relkinds/{rel_kind}\")\n", + "else:\n", + " relkind_helper = relkind_direct = 'Set rel_kind to fetch a specific relationship kind.'\n", + "\n", + "rel_calls = {\n", + " '/taxonomy/relkinds (helper)': show_response(relkinds_helper, '/taxonomy/relkinds'),\n", + " '/taxonomy/relkinds (direct)': show_response(relkinds_direct, '/taxonomy/relkinds'),\n", + " 'relkind_helper': show_response(relkind_helper, f\"/taxonomy/relkinds/{rel_kind}\" if isinstance(relkind_helper, object) else 'set rel_kind'),\n", + " 'relkind_direct': show_response(relkind_direct, f\"/taxonomy/relkinds/{rel_kind}\" if isinstance(relkind_direct, object) else 'set rel_kind'),\n", + "}\n", + "rel_calls\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/topology_api.ipynb b/notebooks/topology_api.ipynb new file mode 100644 index 0000000..c885b62 --- /dev/null +++ b/notebooks/topology_api.ipynb @@ -0,0 +1,215 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tideway Topology API Examples\n\nHelper methods paired with direct calls for topology endpoints: graphs, node lookups by criteria, node kinds, and visualization state. Reference: https://docs.bmc.com/xwiki/bin/view/IT-Operations-Management/Discovery/BMC-Helix-Discovery/DAAS/Integrating/Using-the-REST-APIs/Endpoints-in-the-REST-API/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n- Install tideway (e.g. `pip install -e .` from repo root).\n- Set `TARGET` and `TOKEN` below. Do **not** commit secrets.\n- Adjust ids, filters, and bodies per your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import tideway\n", + "\n", + "# Configuration\n", + "TARGET = 'appliance-hostname-or-ip' # e.g. 'discovery.example.com'\n", + "TOKEN = 'your-api-token' # keep secrets out of source control\n", + "API_VERSION = '1.16' # latest supported API\n", + "SSL_VERIFY = False # set to True when using valid certs\n", + "\n", + "tw = tideway.appliance(TARGET, TOKEN, api_version=API_VERSION, ssl_verify=SSL_VERIFY)\n", + "topology = tw.topology()\n", + "\n", + "def show_response(resp, label):\n", + " if resp.ok:\n", + " try:\n", + " return resp.json()\n", + " except Exception:\n", + " return resp.text\n", + " try:\n", + " body = resp.json()\n", + " except Exception:\n", + " body = resp.text\n", + " return {'endpoint': label, 'status': resp.status_code, 'body': body}\n", + "\n", + "tw.api_about.json() # quick sanity check\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Node graph by node id\nHelper vs direct GET for `/data/nodes/{id}/graph` with focus/apply_rules/complete flags." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "node_id = '' # e.g. '1234567890abcdef'\n", + "focus = 'software-connected'\n", + "apply_rules = True\n", + "complete = False\n", + "\n", + "if node_id:\n", + " graph_helper = topology.get_data_nodes_graph(node_id, focus=focus, apply_rules=apply_rules, complete=complete)\n", + " graph_direct = topology.get(\n", + " f\"/data/nodes/{node_id}/graph?focus={focus}&apply_rules={str(apply_rules).lower()}&complete={str(complete).lower()}\"\n", + " )\n", + " graph_calls = {\n", + " f\"/data/nodes/{node_id}/graph (helper)\": show_response(graph_helper, f\"/data/nodes/{node_id}/graph\"),\n", + " f\"/data/nodes/{node_id}/graph (direct)\": show_response(graph_direct, f\"/data/nodes/{node_id}/graph\"),\n", + " }\n", + "else:\n", + " graph_calls = 'Set node_id to fetch a topology graph.'\n", + "graph_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Topology nodes (criteria search)\nPost criteria to `/topology/nodes` using helper vs direct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "topology_nodes_body = {\n", + " # 'criteria': {'kind': 'Host', 'ip_address': '10.0.0.1'},\n", + " # 'attributes': ['name', 'kind'],\n", + "}\n", + "\n", + "if topology_nodes_body:\n", + " topo_nodes_helper = topology.post_topology_nodes(topology_nodes_body)\n", + " topo_nodes_direct = topology.post('/topology/nodes', topology_nodes_body)\n", + " topo_nodes_calls = {\n", + " '/topology/nodes (helper POST)': show_response(topo_nodes_helper, '/topology/nodes'),\n", + " '/topology/nodes (direct POST)': show_response(topo_nodes_direct, '/topology/nodes'),\n", + " }\n", + "else:\n", + " topo_nodes_calls = 'Set topology_nodes_body to post /topology/nodes.'\n", + "topo_nodes_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Topology nodes by kinds\nPost kind filters to `/topology/nodes/kinds` using helper vs direct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "topology_kinds_body = {\n", + " # 'kinds': ['Host', 'SoftwareInstance'],\n", + " # 'attributes': ['name', 'kind'],\n", + "}\n", + "\n", + "if topology_kinds_body:\n", + " topo_kinds_helper = topology.post_topology_nodes_kinds(topology_kinds_body)\n", + " topo_kinds_direct = topology.post('/topology/nodes/kinds', topology_kinds_body)\n", + " topo_kinds_calls = {\n", + " '/topology/nodes/kinds (helper POST)': show_response(topo_kinds_helper, '/topology/nodes/kinds'),\n", + " '/topology/nodes/kinds (direct POST)': show_response(topo_kinds_direct, '/topology/nodes/kinds'),\n", + " }\n", + "else:\n", + " topo_kinds_calls = 'Set topology_kinds_body to post /topology/nodes/kinds.'\n", + "topo_kinds_calls\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization state\nGet or update visualization state via helper vs direct calls." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# GET visualization state\n", + "viz_helper = topology.get_topology_viz_state()\n", + "viz_direct = topology.get('/topology/visualization_state')\n", + "\n", + "viz_calls = {\n", + " '/topology/visualization_state (helper)': show_response(viz_helper, '/topology/visualization_state'),\n", + " '/topology/visualization_state (direct)': show_response(viz_direct, '/topology/visualization_state'),\n", + "}\n", + "\n", + "viz_patch_body = {\n", + " # 'layout': {},\n", + " # 'filters': {},\n", + "}\n", + "\n", + "viz_put_body = {\n", + " # 'layout': {},\n", + " # 'filters': {},\n", + "}\n", + "\n", + "if viz_patch_body:\n", + " viz_patch_helper = topology.patch_topology_viz_state(viz_patch_body)\n", + " viz_patch_direct = topology.patch('/topology/visualization_state', viz_patch_body)\n", + " viz_calls['/topology/visualization_state (helper PATCH)'] = show_response(viz_patch_helper, '/topology/visualization_state')\n", + " viz_calls['/topology/visualization_state (direct PATCH)'] = show_response(viz_patch_direct, '/topology/visualization_state')\n", + "\n", + "if viz_put_body:\n", + " viz_put_helper = topology.put_topology_viz_state(viz_put_body)\n", + " viz_put_direct = topology.put('/topology/visualization_state', viz_put_body)\n", + " viz_calls['/topology/visualization_state (helper PUT)'] = show_response(viz_put_helper, '/topology/visualization_state')\n", + " viz_calls['/topology/visualization_state (direct PUT)'] = show_response(viz_put_direct, '/topology/visualization_state')\n", + "\n", + "viz_calls\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}