From ab7da40a13c3bade70bc36f8e531eca553b70f63 Mon Sep 17 00:00:00 2001 From: baechir Date: Wed, 19 Jun 2024 07:31:34 +0200 Subject: [PATCH 1/8] Avelon: Add new REST API interface for avelon cloud. --- avelon.py | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 avelon.py diff --git a/avelon.py b/avelon.py new file mode 100644 index 0000000..9f0912d --- /dev/null +++ b/avelon.py @@ -0,0 +1,164 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.rst + +"""Provides functions using the Avelon REST-API. +""" + +__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' +__version__ = '2024061201' + +import requests # type: ignore +import json + +BASE_URL = 'https://avelon.cloud' + +def get_token(client_id, client_secret, username, password, verify=True, proxies={}, timeout=8): + url = '{}/oauth/token'.format( + BASE_URL + ) + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + data = { + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "password", + "username": username, + "password": password + } + + try: + response = requests.post(url, headers=headers, data=data, verify=verify, proxies=proxies, timeout=timeout) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + try: + message = e.response.json().get("message", "An error occurred") + except (ValueError, AttributeError): + message = str(e) + return { + "status_code": getattr(e.response, 'status_code', 'N/A'), + "message": message + } + +def get_tickets(access_token, verify=True, proxies={}, timeout=8): + devices = _get_devices(access_token) + tickets_response = [] + + for device in devices: + tickets_response.extend(_get_ticket_info(access_token, device['clientId'])) + + return tickets_response + +def get_data_points(access_token, data_point_id=None, data_point_name=None, verify=True, proxies={}, timeout=8): + devices = _get_devices(access_token) + data_points_info = [] + data_points_value = [] + data_points_response = [] + + for device in devices: + data_points_info.extend(_get_data_point_info(access_token, device['id'])) + + for data_point in data_points_info: + if not (data_point_name or data_point_id): + data_points_value = _get_data_point_value(access_token, data_point['id']) + elif data_point_id and data_point['id'] in data_point_id: + data_points_value = _get_data_point_value(access_token, data_point['id']) + elif data_point_name and data_point['systemName'] in data_point_name: + data_points_value = _get_data_point_value(access_token, data_point['id']) + + if data_points_value: + data_points_response.append({**data_point, **data_points_value[0]}) + + data_points_value = None + + return data_points_response + + + +def _get_devices(access_token, verify=True, proxies={}, timeout=8): + url = '{}/public-api/v1/devices'.format( + BASE_URL + ) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers, verify=verify, proxies=proxies, timeout=timeout) + + if response.status_code == 200: + return response.json() + else: + return { + "status_code": response.status_code, + "message": response.json().get("message", "An error occurred") + } + + +def _get_ticket_info(access_token, client_id, verify=True, proxies={}, timeout=8): + url = '{}/public-api/v1/tickets'.format( + BASE_URL + ) + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + body = { + "filterScope": "CLIENT", + "id": client_id + } + + response = requests.post(url, headers=headers, data=json.dumps(body), verify=verify, proxies=proxies, timeout=timeout) + + if response.status_code == 200: + return response.json() + else: + return { + "status_code": response.status_code, + "message": response.json().get("message", "An error occurred") + } + + +def _get_data_point_info(access_token, device_id, verify=True, proxies={}, timeout=8): + url = '{}/public-api/v1/data-points?deviceIds={}'.format( + BASE_URL, + device_id + ) + + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers, verify=verify, proxies=proxies, timeout=timeout) + + if response.status_code == 200: + return response.json() + else: + return { + "status_code": response.status_code, + "message": response.json().get("message", "An error occurred") + } + +def _get_data_point_value(access_token, data_point_id, verify=True, proxies={}, timeout=8): + url = '{}/public-api/v1/data-points/{}/records/latest?limit=1'.format( + BASE_URL, + data_point_id + ) + headers = { + "Authorization": f"Bearer {access_token}" + } + response = requests.get(url, headers=headers, verify=verify, proxies=proxies, timeout=timeout) + + if response.status_code == 200: + return response.json() + else: + return [response.json()] \ No newline at end of file From 6497b644a0d8ac978be55c83146b6c5e3c125f43 Mon Sep 17 00:00:00 2001 From: baechir Date: Fri, 21 Jun 2024 15:25:38 +0200 Subject: [PATCH 2/8] avelon: change from request to lib.url.fetch_json --- avelon.py | 164 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 85 insertions(+), 79 deletions(-) diff --git a/avelon.py b/avelon.py index 9f0912d..8da1838 100644 --- a/avelon.py +++ b/avelon.py @@ -14,16 +14,17 @@ __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' __version__ = '2024061201' -import requests # type: ignore import json +import lib.url + BASE_URL = 'https://avelon.cloud' -def get_token(client_id, client_secret, username, password, verify=True, proxies={}, timeout=8): - url = '{}/oauth/token'.format( +def get_token(client_id, client_secret, username, password, insecure=False, no_proxy=False, timeout=8): + uri = '{}/oauth/token'.format( BASE_URL ) - headers = { + header = { "Content-Type": "application/x-www-form-urlencoded" } data = { @@ -33,46 +34,43 @@ def get_token(client_id, client_secret, username, password, verify=True, proxies "username": username, "password": password } + success, token = lib.url.fetch_json( + uri, + header=header, + data=data, + extended=True, + insecure=insecure, + no_proxy=no_proxy, + timeout=timeout, + ) + + return (True, token['response_json']) + - try: - response = requests.post(url, headers=headers, data=data, verify=verify, proxies=proxies, timeout=timeout) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - try: - message = e.response.json().get("message", "An error occurred") - except (ValueError, AttributeError): - message = str(e) - return { - "status_code": getattr(e.response, 'status_code', 'N/A'), - "message": message - } - -def get_tickets(access_token, verify=True, proxies={}, timeout=8): - devices = _get_devices(access_token) +def get_tickets(access_token, insecure=False, no_proxy=False, timeout=8): + success, devices = _get_devices(access_token, insecure, no_proxy, timeout) tickets_response = [] for device in devices: - tickets_response.extend(_get_ticket_info(access_token, device['clientId'])) + tickets_response.extend(_get_ticket_info(access_token, device['clientId'], insecure, no_proxy, timeout)) return tickets_response -def get_data_points(access_token, data_point_id=None, data_point_name=None, verify=True, proxies={}, timeout=8): - devices = _get_devices(access_token) +def get_data_points(access_token, data_point_id=None, data_point_name=None, insecure=False, no_proxy=False, timeout=8): + success, devices = _get_devices(access_token, insecure, no_proxy, timeout) data_points_info = [] data_points_value = [] data_points_response = [] for device in devices: - data_points_info.extend(_get_data_point_info(access_token, device['id'])) + success, data_point_info = _get_data_point_info(access_token, device['id'], insecure, no_proxy, timeout) + data_points_info.extend(data_point_info) for data_point in data_points_info: - if not (data_point_name or data_point_id): - data_points_value = _get_data_point_value(access_token, data_point['id']) - elif data_point_id and data_point['id'] in data_point_id: - data_points_value = _get_data_point_value(access_token, data_point['id']) - elif data_point_name and data_point['systemName'] in data_point_name: - data_points_value = _get_data_point_value(access_token, data_point['id']) + if (data_point_id and data_point['id'] in data_point_id) or (data_point_name and data_point['systemName'] in data_point_name): + success, data_points_value = _get_data_point_value(access_token, data_point['id'], insecure, no_proxy, timeout) + elif not (data_point_name or data_point_id): + success, data_points_value = _get_data_point_value(access_token, data_point['id'], insecure, no_proxy, timeout) if data_points_value: data_points_response.append({**data_point, **data_points_value[0]}) @@ -83,31 +81,31 @@ def get_data_points(access_token, data_point_id=None, data_point_name=None, veri -def _get_devices(access_token, verify=True, proxies={}, timeout=8): - url = '{}/public-api/v1/devices'.format( +def _get_devices(access_token, insecure, no_proxy, timeout): + uri = '{}/public-api/v1/devices'.format( BASE_URL ) - headers = { + header = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } + success, devices = lib.url.fetch_json( + uri, + header=header, + extended=True, + insecure=insecure, + no_proxy=no_proxy, + timeout=timeout, + ) - response = requests.get(url, headers=headers, verify=verify, proxies=proxies, timeout=timeout) - - if response.status_code == 200: - return response.json() - else: - return { - "status_code": response.status_code, - "message": response.json().get("message", "An error occurred") - } + return (True, devices['response_json']) -def _get_ticket_info(access_token, client_id, verify=True, proxies={}, timeout=8): - url = '{}/public-api/v1/tickets'.format( +def _get_ticket_info(access_token, client_id, insecure, no_proxy, timeout): + uri = '{}/public-api/v1/tickets'.format( BASE_URL ) - headers = { + header = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } @@ -115,50 +113,58 @@ def _get_ticket_info(access_token, client_id, verify=True, proxies={}, timeout=8 "filterScope": "CLIENT", "id": client_id } - - response = requests.post(url, headers=headers, data=json.dumps(body), verify=verify, proxies=proxies, timeout=timeout) + success, devices = lib.url.fetch_json( + uri, + header=header, + data = body, + extended=True, + insecure=insecure, + no_proxy=no_proxy, + timeout=timeout, + encoding = 'serialized-json', + ) - if response.status_code == 200: - return response.json() - else: - return { - "status_code": response.status_code, - "message": response.json().get("message", "An error occurred") - } + return (True, devices['response_json']) -def _get_data_point_info(access_token, device_id, verify=True, proxies={}, timeout=8): - url = '{}/public-api/v1/data-points?deviceIds={}'.format( +def _get_data_point_info(access_token, device_id, insecure, no_proxy, timeout): + uri = '{}/public-api/v1/data-points?deviceIds={}'.format( BASE_URL, device_id ) - - headers = { + header ={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } - - response = requests.get(url, headers=headers, verify=verify, proxies=proxies, timeout=timeout) - - if response.status_code == 200: - return response.json() - else: - return { - "status_code": response.status_code, - "message": response.json().get("message", "An error occurred") - } - -def _get_data_point_value(access_token, data_point_id, verify=True, proxies={}, timeout=8): - url = '{}/public-api/v1/data-points/{}/records/latest?limit=1'.format( + success, data_point_info = lib.url.fetch_json( + uri, + header=header, + extended=True, + insecure=insecure, + no_proxy=no_proxy, + timeout=timeout, + ) + + return (True, data_point_info['response_json']) + +def _get_data_point_value(access_token, data_point_id, insecure, no_proxy, timeout): + uri = '{}/public-api/v1/data-points/{}/records/latest?limit=1'.format( BASE_URL, data_point_id ) - headers = { - "Authorization": f"Bearer {access_token}" + header = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" } - response = requests.get(url, headers=headers, verify=verify, proxies=proxies, timeout=timeout) - - if response.status_code == 200: - return response.json() - else: - return [response.json()] \ No newline at end of file + success, data_point_value = lib.url.fetch_json( + uri, + header=header, + extended=True, + insecure=insecure, + no_proxy=no_proxy, + timeout=timeout, + ) + + if not success: + return (success, [{'value': data_point_value}]) + return (True, data_point_value['response_json']) \ No newline at end of file From 97f1963dc9c9d9bd66c4bb66867013c7c254362f Mon Sep 17 00:00:00 2001 From: baechir Date: Mon, 24 Jun 2024 13:08:34 +0200 Subject: [PATCH 3/8] avelon: Add error handling --- avelon.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/avelon.py b/avelon.py index 8da1838..41b0535 100644 --- a/avelon.py +++ b/avelon.py @@ -8,18 +8,17 @@ # https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.rst -"""Provides functions using the Avelon REST-API. +"""Provides functions using the Avelon Cloud REST-API. """ __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' -__version__ = '2024061201' - -import json +__version__ = '2024062401' import lib.url BASE_URL = 'https://avelon.cloud' + def get_token(client_id, client_secret, username, password, insecure=False, no_proxy=False, timeout=8): uri = '{}/oauth/token'.format( BASE_URL @@ -44,11 +43,16 @@ def get_token(client_id, client_secret, username, password, insecure=False, no_p timeout=timeout, ) + if not success: + return (success, token) return (True, token['response_json']) def get_tickets(access_token, insecure=False, no_proxy=False, timeout=8): success, devices = _get_devices(access_token, insecure, no_proxy, timeout) + if not success: + return (success, devices) + tickets_response = [] for device in devices: @@ -56,8 +60,12 @@ def get_tickets(access_token, insecure=False, no_proxy=False, timeout=8): return tickets_response + def get_data_points(access_token, data_point_id=None, data_point_name=None, insecure=False, no_proxy=False, timeout=8): success, devices = _get_devices(access_token, insecure, no_proxy, timeout) + if not success: + return (success, devices) + data_points_info = [] data_points_value = [] data_points_response = [] @@ -74,13 +82,11 @@ def get_data_points(access_token, data_point_id=None, data_point_name=None, inse if data_points_value: data_points_response.append({**data_point, **data_points_value[0]}) - data_points_value = None return data_points_response - def _get_devices(access_token, insecure, no_proxy, timeout): uri = '{}/public-api/v1/devices'.format( BASE_URL @@ -98,6 +104,9 @@ def _get_devices(access_token, insecure, no_proxy, timeout): timeout=timeout, ) + if not devices['response_json']: + return (False, 'No devices found. Check if devices under this ' + 'mandate are present in avelon.cloud.') return (True, devices['response_json']) @@ -113,7 +122,7 @@ def _get_ticket_info(access_token, client_id, insecure, no_proxy, timeout): "filterScope": "CLIENT", "id": client_id } - success, devices = lib.url.fetch_json( + success, tickets = lib.url.fetch_json( uri, header=header, data = body, @@ -124,7 +133,7 @@ def _get_ticket_info(access_token, client_id, insecure, no_proxy, timeout): encoding = 'serialized-json', ) - return (True, devices['response_json']) + return (True, tickets['response_json']) def _get_data_point_info(access_token, device_id, insecure, no_proxy, timeout): From ba5fc88cfb2992cce31f3859da99dbb8490f0d8c Mon Sep 17 00:00:00 2001 From: baechir Date: Mon, 24 Jun 2024 13:17:00 +0200 Subject: [PATCH 4/8] avelon: add avelon lib to global doc --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 38d8079..ed3bfa2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ For example by browsing http://localhost:8080/ after starting `pydoc -p 8080`. * args.py: Extends argparse by new input argument data types on demand. +* avelon.py: +This library interacts with the Avelon Cloud API. + * base.py: Provides very common every-day functions. From b2638a18676280c2f96ad23b6108b866e1fa0079 Mon Sep 17 00:00:00 2001 From: baechir Date: Wed, 24 Jul 2024 09:26:34 +0200 Subject: [PATCH 5/8] avelon: add ThreadPoolExecutor() to get_data_points --- avelon.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/avelon.py b/avelon.py index 41b0535..2a11ee2 100644 --- a/avelon.py +++ b/avelon.py @@ -14,6 +14,7 @@ __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' __version__ = '2024062401' +import concurrent.futures import lib.url BASE_URL = 'https://avelon.cloud' @@ -74,16 +75,21 @@ def get_data_points(access_token, data_point_id=None, data_point_name=None, inse success, data_point_info = _get_data_point_info(access_token, device['id'], insecure, no_proxy, timeout) data_points_info.extend(data_point_info) - for data_point in data_points_info: - if (data_point_id and data_point['id'] in data_point_id) or (data_point_name and data_point['systemName'] in data_point_name): - success, data_points_value = _get_data_point_value(access_token, data_point['id'], insecure, no_proxy, timeout) - elif not (data_point_name or data_point_id): - success, data_points_value = _get_data_point_value(access_token, data_point['id'], insecure, no_proxy, timeout) - - if data_points_value: - data_points_response.append({**data_point, **data_points_value[0]}) - data_points_value = None - + with concurrent.futures.ThreadPoolExecutor() as executor: + future_to_data_point = { + executor.submit(_get_data_point_value, access_token, data_point['id'], insecure, no_proxy, timeout): data_point + for data_point in data_points_info + if (not data_point_id and not data_point_name) or + (data_point_id and data_point['id'] in data_point_id) or + (data_point_name and data_point['systemName'] in data_point_name) + } + + for future in concurrent.futures.as_completed(future_to_data_point): + data_point = future_to_data_point[future] + success, data_points_value = future.result() + if data_points_value: + data_points_response.append({**data_point, **data_points_value[0]}) + return data_points_response From fe3dffa74af6b7b80b809b96063b16dcdc1accf2 Mon Sep 17 00:00:00 2001 From: baechir Date: Wed, 24 Jul 2024 09:31:24 +0200 Subject: [PATCH 6/8] avelon: code structure --- avelon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/avelon.py b/avelon.py index 2a11ee2..c558ca9 100644 --- a/avelon.py +++ b/avelon.py @@ -162,6 +162,7 @@ def _get_data_point_info(access_token, device_id, insecure, no_proxy, timeout): return (True, data_point_info['response_json']) + def _get_data_point_value(access_token, data_point_id, insecure, no_proxy, timeout): uri = '{}/public-api/v1/data-points/{}/records/latest?limit=1'.format( BASE_URL, From 5cf5e5c24f017923c64d6ac594458694a437a8c6 Mon Sep 17 00:00:00 2001 From: baechir Date: Wed, 24 Jul 2024 11:07:42 +0200 Subject: [PATCH 7/8] avelon: add success in get_data_points return --- avelon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avelon.py b/avelon.py index c558ca9..fb89427 100644 --- a/avelon.py +++ b/avelon.py @@ -90,7 +90,7 @@ def get_data_points(access_token, data_point_id=None, data_point_name=None, inse if data_points_value: data_points_response.append({**data_point, **data_points_value[0]}) - return data_points_response + return (True, data_points_response) def _get_devices(access_token, insecure, no_proxy, timeout): From b5a060a59949eb64af997bcf61ae10c80a405365 Mon Sep 17 00:00:00 2001 From: baechir Date: Wed, 24 Jul 2024 11:12:48 +0200 Subject: [PATCH 8/8] avelon: add version --- avelon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avelon.py b/avelon.py index fb89427..8d02381 100644 --- a/avelon.py +++ b/avelon.py @@ -12,7 +12,7 @@ """ __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' -__version__ = '2024062401' +__version__ = '2024072401' import concurrent.futures import lib.url