diff options
| author | eduardormorais <eduardoromorais@gmail.com> | 2025-08-28 18:01:24 -0300 |
|---|---|---|
| committer | eduardormorais <eduardoromorais@gmail.com> | 2025-08-28 18:01:24 -0300 |
| commit | 7b147d6964e6390a93b069c6cc2a06bdc2c7651f (patch) | |
| tree | df4cf9c9570378635578cd161bfb304e1ad1da89 /pyvyos | |
| parent | e770412a93d2bbbc95bbb5a5edcdbb23036fd7dd (diff) | |
| download | pyvyos-7b147d6964e6390a93b069c6cc2a06bdc2c7651f.tar.gz pyvyos-7b147d6964e6390a93b069c6cc2a06bdc2c7651f.zip | |
Feature - Separation of API communication logic into a separate class. Code adjustments and improvements.
Diffstat (limited to 'pyvyos')
| -rw-r--r-- | pyvyos/__init__.py | 2 | ||||
| -rw-r--r-- | pyvyos/device.py | 241 | ||||
| -rw-r--r-- | pyvyos/rest.py | 182 |
3 files changed, 248 insertions, 177 deletions
diff --git a/pyvyos/__init__.py b/pyvyos/__init__.py index 7c327e5..44bd563 100644 --- a/pyvyos/__init__.py +++ b/pyvyos/__init__.py @@ -1,2 +1,2 @@ from .device import VyDevice -from .device import ApiResponse
\ No newline at end of file +from .device import ApiResponse diff --git a/pyvyos/device.py b/pyvyos/device.py index 6579587..6f177d4 100644 --- a/pyvyos/device.py +++ b/pyvyos/device.py @@ -1,26 +1,9 @@ -import urllib3 -import requests -import json -import pprint -from dataclasses import dataclass - -@dataclass -class ApiResponse: - """ - Represents an API response. +from typing import List - Attributes: - status (int): The HTTP status code of the response. - request (dict): The request payload sent to the API. - result (dict): The data result of the API response. - error (str): Any error message in case of a failed response. - """ - status: int - request: dict - result: dict - error: str +from .rest import ApiResponse, RestClient -class VyDevice: + +class VyDevice(RestClient): """ Represents a device for interacting with the VyOS API. @@ -51,8 +34,8 @@ class VyDevice: image_delete(name, url=None, file=None, path=[]): Delete a specific image. show(path=[]): Show configuration information. generate(path=[]): Generate configuration based on specified path. - configure_set(path=[]): Sets configuration based on the specified path. This method is versatile, accepting - either a single configuration path or a list of configuration paths. This flexibility + configure_set(path=[]): Sets configuration based on the specified path. This method is versatile, accepting + either a single configuration path or a list of configuration paths. This flexibility allows for setting both individual and multiple configurations in a single operation. configure_delete(path=[]): Delete configuration based on specified path. config_file_save(file=None): Save the configuration to a file. @@ -61,136 +44,18 @@ class VyDevice: poweroff(path=["now"]): Power off the device. """ - def __init__(self, hostname, apikey, protocol='https', port=443, verify=True, timeout=10): - """ - Initializes a VyDevice instance. - - Args: - hostname (str): The hostname or IP address of the VyOS device. - apikey (str): The API key for authentication. - protocol (str, optional): The protocol to use (default is 'https'). - port (int, optional): The port to use (default is 443). - verify (bool, optional): Whether to verify SSL certificates (default is True). - timeout (int, optional): The request timeout in seconds (default is 10). - """ - self.hostname = hostname - self.apikey = apikey - self.protocol = protocol - self.port = port - self.verify = verify - self.timeout = timeout - - def _get_url(self, command): - """ - Get the full URL for a specific API command. - - Args: - command (str): The API command to construct the URL for. - - Returns: - str: The full URL for the API command. - """ - return f"{self.protocol}://{self.hostname}:{self.port}/{command}" - - def _get_payload(self, op, path=[], file=None, url=None, name=None): - """ - Generate the payload for an API request. - - Args: - op (str): The operation to perform in the API request. - path (list, optional): The path elements for the API request. This can be a single list for a single - configuration path or a list of lists for multiple configuration paths. - file (str, optional): The file to include in the request (default is None). - url (str, optional): The URL to include in the request (default is None). - name (str, optional): The name to include in the request (default is None). + def __init__( + self, + hostname: str, + apikey: str, + protocol: str = "https", + port: int = 443, + verify: bool = True, + timeout: int = 10, + ): + super().__init__(hostname, apikey, protocol, port, verify, timeout) - Returns: - dict: The payload for the API request. - """ - # Adding option to pass multiple operation (eg:delete and set) commands - if op: - # Adjusting the data structure based on whether path is single or multiple - if path and isinstance(path, list) and isinstance(path[0], list): # Handling multiple paths - data = [{'op': op, 'path': p} for p in path] - else: # Handling a single path - data = {'op': op, 'path': path} - else: - if path and isinstance(path[0], dict): - data = path - - # Including the optional parameters if provided - if file: - if isinstance(data, list): # If data is a list of dicts (multiple paths) - for d in data: - d['file'] = file - else: # If data is a single dict (single path) - data['file'] = file - - if url: - if isinstance(data, list): - for d in data: - d['url'] = url - else: - data['url'] = url - - if name: - if isinstance(data, list): - for d in data: - d['name'] = name - else: - data['name'] = name - - payload = { - 'data': json.dumps(data), - 'key': self.apikey - } - - return payload - - def _api_request(self, command, op, path=[], method='POST', file=None, url=None, name=None): - """ - Make an API request. - - Args: - command (str): The API command to execute. - op (str): The operation to perform in the API request. - path (list, optional): The path elements for the API request (default is an empty list). - method (str, optional): The HTTP method to use for the request (default is 'POST'). - file (str, optional): The file to include in the request (default is None). - url (str, optional): The URL to include in the request (default is None). - name (str, optional): The name to include in the request (default is None). - - Returns: - ApiResponse: An ApiResponse object representing the API response. - """ - url = self._get_url(command) - payload = self._get_payload(op, path=path, file=file, url=url, name=name) - - headers = {} - error = False - result = {} - - try: - resp = requests.post(url, verify=self.verify, data=payload, timeout=self.timeout, headers=headers) - resp_decoded = resp.json() - - if resp_decoded['success']: - result = resp_decoded['data'] - error = False - else: - error = resp_decoded['error'] - - status = resp.status_code - - except (requests.exceptions.ConnectionError, json.JSONDecodeError) as e: - error = 'Error: ' + str(e) - status = 0 - - # Removing apikey from payload for security reasons - del(payload['key']) - return ApiResponse(status=status, request=payload, result=result, error=error) - - def retrieve_show_config(self, path=[]): + def retrieve_show_config(self, path: List = None): """ Retrieve and show the device configuration. @@ -200,9 +65,12 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="retrieve", op='showConfig', path=path, method="POST") - def retrieve_return_values(self, path=[]): + return self._api_request( + command="retrieve", op="showConfig", path=path, method="POST" + ) + + def retrieve_return_values(self, path: List = None): """ Retrieve and return specific configuration values. @@ -212,9 +80,11 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="retrieve", op='returnValues', path=path, method="POST") + return self._api_request( + command="retrieve", op="returnValues", path=path, method="POST" + ) - def reset(self, path=[]): + def reset(self, path: List = None): """ Reset a specific configuration element. @@ -224,7 +94,7 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="reset", op='reset', path=path, method="POST") + return self._api_request(command="reset", op="reset", path=path, method="POST") def image_add(self, url=None, file=None, path=[]): """ @@ -238,7 +108,7 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="image", op='add', url=url, method="POST") + return self._api_request(command="image", op="add", url=url, method="POST") def image_delete(self, name, url=None, file=None, path=[]): """ @@ -253,9 +123,9 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="image", op='delete', name=name, method="POST") + return self._api_request(command="image", op="delete", name=name, method="POST") - def show(self, path=[]): + def show(self, path: List = None): """ Show configuration information. @@ -265,9 +135,9 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="show", op='show', path=path, method="POST") + return self._api_request(command="show", op="show", path=path, method="POST") - def generate(self, path=[]): + def generate(self, path: List = None): """ Generate configuration based on the given path. @@ -277,9 +147,11 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="generate", op='generate', path=path, method="POST") + return self._api_request( + command="generate", op="generate", path=path, method="POST" + ) - def configure_set(self, path=[]): + def configure_set(self, path: List = None): """ Set configuration based on the given path. @@ -289,10 +161,11 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="configure", op='set', path=path, method="POST") + return self._api_request( + command="configure", op="set", path=path, method="POST" + ) - - def configure_delete(self, path=[]): + def configure_delete(self, path: List = None): """ Delete configuration based on the given path. @@ -302,9 +175,11 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="configure", op='delete', path=path, method="POST") + return self._api_request( + command="configure", op="delete", path=path, method="POST" + ) - def configure_multiple_op(self, op_path=[]): + def configure_multiple_op(self, op_path: List = None): """ Set configuration based on the given {operation : path} for multiple operation. @@ -327,7 +202,9 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="config-file", op='save', file=file, method="POST") + return self._api_request( + command="config-file", op="save", file=file, method="POST" + ) def config_file_load(self, file=None): """ @@ -339,9 +216,11 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="config-file", op='load', file=file, method="POST") + return self._api_request( + command="config-file", op="load", file=file, method="POST" + ) - def reboot(self, path=["now"]): + def reboot(self, path: List = None): """ Reboot the device. @@ -351,9 +230,14 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="reboot", op='reboot', path=path, method="POST") - - def poweroff(self, path=["now"]): + if path is None: + path = ["now"] + + return self._api_request( + command="reboot", op="reboot", path=path, method="POST" + ) + + def poweroff(self, path: List = None): """ Power off the device. @@ -363,4 +247,9 @@ class VyDevice: Returns: ApiResponse: An ApiResponse object representing the API response. """ - return self._api_request(command="poweroff", op='poweroff', path=path, method="POST") + if path is None: + path = ["now"] + + return self._api_request( + command="poweroff", op="poweroff", path=path, method="POST" + ) diff --git a/pyvyos/rest.py b/pyvyos/rest.py new file mode 100644 index 0000000..f19c026 --- /dev/null +++ b/pyvyos/rest.py @@ -0,0 +1,182 @@ +import json +from dataclasses import dataclass +from typing import List, Tuple + +from requests import Request, Response +from requests.exceptions import HTTPError, ConnectionError +import requests + + +@dataclass +class ApiResponse: + """ + Represents an API response. + + Attributes: + status (int): The HTTP status code of the response. + request (dict): The request payload sent to the API. + result (dict): The data result of the API response. + error (str): Any error message in case of a failed response. + """ + + status: int + request: dict + result: dict + error: str + + +class RestClient: + + def __init__( + self, + hostname: str, + apikey: str, + protocol: str, + port: int, + verify: bool, + timeout: int, + ): + """ + Args: + hostname (str): The hostname or IP address of the VyOS device. + apikey (str): The API key for authentication. + protocol (str, optional): The protocol to use (default is 'https'). + port (int, optional): The port to use (default is 443). + verify (bool, optional): Whether to verify SSL certificates (default is True). + timeout (int, optional): The request timeout in seconds (default is 10). + """ + self.hostname = hostname + self.apikey = apikey + self.protocol = protocol + self.port = port + self.verify = verify + self.timeout = timeout + + def _get_url(self, command): + """ + Get the full URL for a specific API command. + + Args: + command (str): The API command to construct the URL for. + + Returns: + str: The full URL for the API command. + """ + return f"{self.protocol}://{self.hostname}:{self.port}/{command}" + + def _get_payload(self, op, path: List = None, file=None, url=None, name=None): + """ + Generate the payload for an API request. + + Args: + op (str): The operation to perform in the API request. + path (list, optional): The path elements for the API request. This can be a single list for a single + configuration path or a list of lists for multiple configuration paths. + file (str, optional): The file to include in the request (default is None). + url (str, optional): The URL to include in the request (default is None). + name (str, optional): The name to include in the request (default is None). + + Returns: + dict: The payload for the API request. + """ + # Adding option to pass multiple operation (eg:delete and set) commands + if path is None: + path = [] + + if op: + # Adjusting the data structure based on whether path is single or multiple + if ( + path and isinstance(path, list) and isinstance(path[0], list) + ): # Handling multiple paths + data = [{"op": op, "path": p} for p in path] + else: # Handling a single path + data = {"op": op, "path": path} + else: + if path and isinstance(path[0], dict): + data = path + + # Including the optional parameters if provided + if file: + if isinstance(data, list): # If data is a list of dicts (multiple paths) + for d in data: + d["file"] = file + else: # If data is a single dict (single path) + data["file"] = file + + if url: + if isinstance(data, list): + for d in data: + d["url"] = url + else: + data["url"] = url + + if name: + if isinstance(data, list): + for d in data: + d["name"] = name + else: + data["name"] = name + + payload = {"data": json.dumps(data), "key": self.apikey} + + return payload + + def _api_request( + self, command, op, path=[], method="POST", file=None, url=None, name=None + ): + """ + Make an API request. + + Args: + command (str): The API command to execute. + op (str): The operation to perform in the API request. + path (list, optional): The path elements for the API request (default is an empty list). + method (str, optional): The HTTP method to use for the request (default is 'POST'). + file (str, optional): The file to include in the request (default is None). + url (str, optional): The URL to include in the request (default is None). + name (str, optional): The name to include in the request (default is None). + + Returns: + ApiResponse: An ApiResponse object representing the API response. + """ + url = self._get_url(command) + payload = self._get_payload(op, path=path, file=file, url=url, name=name) + headers = {} + + resp = requests.post( + url, + verify=self.verify, + data=payload, + timeout=self.timeout, + headers=headers, + ) + status, result, error = self._validate_response(resp) + + # Removing apikey from payload for security reasons + del payload["key"] + return ApiResponse(status=status, request=payload, result=result, error=error) + + @classmethod + def _validate_response(cls, resp: Response) -> Tuple: + status = None + result = {} + + try: + resp.raise_for_status() + resp_decoded = resp.json() + if resp_decoded["success"]: + result = resp_decoded["data"] + error = False + else: + error = resp_decoded["error"] + + status = resp.status_code + + except json.JSONDecodeError as exc: + error = "JSONDecodeError: " + str(exc) + + except (ConnectionError, HTTPError) as exc: + error = "API Error: " + str(exc.response.text) + status = exc.response.status_code + + return status, result, error |
