1
0
Fork 0
eos-downloader/eos_downloader/logics/arista_server.py
Daniel Baumann 8d2f70e3c7
Merging upstream version 0.12.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-10 06:39:52 +01:00

272 lines
9.5 KiB
Python

#!/usr/bin/python
# coding: utf-8 -*-
# pylint: disable=too-many-positional-arguments
# pylint: disable=dangerous-default-value
"""Server module for handling interactions with Arista software download portal.
This module provides the AristaServer class which manages authentication and
file retrieval operations with the Arista software download portal. It handles
session management, XML data retrieval, and download URL generation.
Classes
-------
AristaServer
Main class for interacting with the Arista software portal.
Dependencies
-----------
- base64: For encoding authentication tokens
- json: For handling JSON data in requests
- xml.etree.ElementTree: For parsing XML responses
- loguru: For logging
- requests: For making HTTP requests
Example
-------
>>> from eos_downloader.logics.server import AristaServer
>>> server = AristaServer(token='my_auth_token')
>>> server.authenticate()
>>> xml_data = server.get_xml_data()
>>> download_url = server.get_url('/path/to/file')
Notes
-----
The module requires valid authentication credentials to interact with the Arista portal.
All server interactions are performed over HTTPS and follow Arista's API specifications.
"""
from __future__ import annotations
import base64
import logging
import json
from typing import Dict, Union, Any
import xml.etree.ElementTree as ET
from loguru import logger
import requests
import eos_downloader.exceptions
import eos_downloader.defaults
class AristaServer:
"""AristaServer class to handle authentication and interactions with Arista software download portal.
This class provides methods to authenticate with the Arista software portal,
retrieve XML data containing available software packages, and generate download URLs
for specific files.
Attributes
----------
token : str, optional
Authentication token for Arista portal access
timeout : int, default=5
Timeout in seconds for HTTP requests
session_server : str
URL of the authentication server
headers : Dict[str, any]
HTTP headers to use in requests
xml_url : str
URL to retrieve software package XML data
download_server : str
Base URL for file downloads
_session_id : str
Session ID obtained after authentication
Methods
-------
authenticate(token: Union[bool, None] = None) -> bool
Authenticates with the Arista portal using provided or stored token
get_xml_data() -> ET.ElementTree
Retrieves XML data containing available software packages
get_url(remote_file_path: str) -> Union[str, None]
Generates download URL for a specific file path
Raises
------
eos_downloader.exceptions.AuthenticationError
When authentication fails due to invalid or expired token
"""
def __init__(
self,
token: Union[str, None] = None,
timeout: int = 5,
session_server: str = eos_downloader.defaults.DEFAULT_SERVER_SESSION,
headers: Dict[str, Any] = eos_downloader.defaults.DEFAULT_REQUEST_HEADERS,
xml_url: str = eos_downloader.defaults.DEFAULT_SOFTWARE_FOLDER_TREE,
download_server: str = eos_downloader.defaults.DEFAULT_DOWNLOAD_URL,
) -> None:
"""Initialize the Server class with optional parameters.
Parameters
----------
token : Union[str, None], optional
Authentication token. Defaults to None.
timeout : int, optional
Request timeout in seconds. Defaults to 5.
session_server : str, optional
URL of the session server. Defaults to DEFAULT_SERVER_SESSION.
headers : Dict[str, any], optional
HTTP headers for requests. Defaults to DEFAULT_REQUEST_HEADERS.
xml_url : str, optional
URL of the software folder tree XML. Defaults to DEFAULT_SOFTWARE_FOLDER_TREE.
download_server : str, optional
Base URL for downloads. Defaults to DEFAULT_DOWNLOAD_URL.
Returns
-------
None
"""
self.token: Union[str, None] = token
self._session_server = session_server
self._headers = headers
self._timeout = timeout
self._xml_url = xml_url
self._download_server = download_server
self._session_id = None
logging.info(f"Initialized AristaServer with headers: {self._headers}")
def authenticate(self, token: Union[str, None] = None) -> bool:
"""Authenticate to the API server using access token.
The token is encoded in base64 and sent to the server for authentication.
A session ID is retrieved from the server response if authentication is successful.
Parameters
----------
token : Union[str, None], optional
Access token for authentication. If None, uses existing token stored in instance. Defaults to None.
Returns
-------
bool
True if authentication successful, False otherwise
Raises
------
eos_downloader.exceptions.AuthenticationError
If access token is invalid or expired
"""
if token is not None:
self.token = token
if self.token is None:
logger.error("No token provided for authentication")
return False
credentials = (base64.b64encode(self.token.encode())).decode("utf-8")
jsonpost = {"accessToken": credentials}
result = requests.post(
self._session_server,
data=json.dumps(jsonpost),
timeout=self._timeout,
headers=self._headers,
)
if result.json()["status"]["message"] in [
"Access token expired",
"Invalid access token",
]:
logging.critical(
f"Authentication failed: {result.json()['status']['message']}"
)
raise eos_downloader.exceptions.AuthenticationError
# return False
try:
if "data" in result.json():
self._session_id = result.json()["data"]["session_code"]
logging.info(f"Authenticated with session ID: {self._session_id}")
return True
except KeyError as error:
logger.error(
f"Key Error in parsing server response ({result.json()}): {error}"
)
return False
return False
def get_xml_data(self) -> Union[ET.ElementTree, None]:
"""Retrieves XML data from the server.
This method fetches XML data by making a POST request to the server's XML endpoint.
If not already authenticated, it will initiate the authentication process first.
Returns
-------
ET.ElementTree
An ElementTree object containing the parsed XML data from the server response.
Raises
------
KeyError
If the server response doesn't contain the expected data structure.
Notes
-----
The method requires a valid session ID which is obtained through authentication.
The XML data is expected to be in the response JSON under data.xml path.
"""
logging.info(f"Getting XML data from server {self._session_server}")
if self._session_id is None:
logging.debug("Not authenticated to server, start authentication process")
self.authenticate()
jsonpost = {"sessionCode": self._session_id}
result = requests.post(
self._xml_url,
data=json.dumps(jsonpost),
timeout=self._timeout,
headers=self._headers,
)
try:
folder_tree = result.json()["data"]["xml"]
logging.debug("XML data received from Arista server")
return ET.ElementTree(ET.fromstring(folder_tree))
except KeyError as error:
logger.error(f"Unkown key in server response: {error}")
return None
def get_url(self, remote_file_path: str) -> Union[str, None]:
"""Get download URL for a remote file from server.
This method retrieves the download URL for a specified remote file by making a POST request
to the server. If not authenticated, it will first authenticate before making the request.
Parameters
----------
remote_file_path : str
Path to the remote file on server to get download URL for
Returns
-------
Union[str, None]
The download URL if successful, None if request fails or URL not found in response
Raises
------
requests.exceptions.RequestException
If the request to server fails
json.JSONDecodeError
If server response is not valid JSON
requests.exceptions.Timeout
If server request times out
"""
logging.info(f"Getting download URL for {remote_file_path}")
if self._session_id is None:
logging.debug("Not authenticated to server, start authentication process")
self.authenticate()
jsonpost = {"sessionCode": self._session_id, "filePath": remote_file_path}
result = requests.post(
self._download_server,
data=json.dumps(jsonpost),
timeout=self._timeout,
headers=self._headers,
)
if "data" in result.json() and "url" in result.json()["data"]:
# logger.debug('URL to download file is: {}', result.json())
logging.info("Download URL received from server")
logging.debug(f'URL to download file is: {result.json()["data"]["url"]}')
return result.json()["data"]["url"]
return None