272 lines
9.5 KiB
Python
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
|