139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
import abc
|
|
import json
|
|
import os
|
|
import posixpath
|
|
from typing import Any, Dict, Optional # noqa: F401
|
|
from urllib.parse import urlencode
|
|
|
|
from uaclient import config, http, system, util, version
|
|
|
|
|
|
class UAServiceClient(metaclass=abc.ABCMeta):
|
|
|
|
url_timeout = 30 # type: Optional[int]
|
|
# Cached serviceclient_url_responses if provided in uaclient.conf
|
|
# via features: {serviceclient_url_responses: /some/file.json}
|
|
_response_overlay = None # type: Dict[str, Any]
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def cfg_url_base_attr(self) -> str:
|
|
"""String in subclasses, the UAConfig attribute containing base url"""
|
|
pass
|
|
|
|
def __init__(self, cfg: Optional[config.UAConfig] = None) -> None:
|
|
if not cfg:
|
|
self.cfg = config.UAConfig()
|
|
else:
|
|
self.cfg = cfg
|
|
|
|
def headers(self):
|
|
return {
|
|
"user-agent": "UA-Client/{}".format(version.get_version()),
|
|
"accept": "application/json",
|
|
"content-type": "application/json",
|
|
}
|
|
|
|
def request_url(
|
|
self,
|
|
path,
|
|
data=None,
|
|
headers=None,
|
|
method=None,
|
|
query_params=None,
|
|
log_response_body: bool = True,
|
|
timeout: Optional[int] = None,
|
|
) -> http.HTTPResponse:
|
|
path = path.lstrip("/")
|
|
if not headers:
|
|
headers = self.headers()
|
|
if headers.get("content-type") == "application/json" and data:
|
|
data = json.dumps(data, cls=util.DatetimeAwareJSONEncoder).encode(
|
|
"utf-8"
|
|
)
|
|
url = posixpath.join(getattr(self.cfg, self.cfg_url_base_attr), path)
|
|
|
|
fake_response = self._get_fake_responses(url)
|
|
if fake_response:
|
|
return fake_response # URL faked by uaclient.conf
|
|
|
|
if query_params:
|
|
# filter out None values
|
|
filtered_params = {
|
|
k: v for k, v in sorted(query_params.items()) if v is not None
|
|
}
|
|
url += "?" + urlencode(filtered_params)
|
|
timeout_to_use = timeout if timeout is not None else self.url_timeout
|
|
|
|
return http.readurl(
|
|
url=url,
|
|
data=data,
|
|
headers=headers,
|
|
method=method,
|
|
timeout=timeout_to_use,
|
|
log_response_body=log_response_body,
|
|
)
|
|
|
|
def _get_response_overlay(self, url: str):
|
|
"""Return a list of fake response dicts for a given URL.
|
|
|
|
serviceclient_url_responses in uaclient.conf should be a path
|
|
to a json file which contains a dictionary keyed by full URL path.
|
|
Each value will be a list of dicts representing each faked response
|
|
for the given URL.
|
|
|
|
The response dict item will have a code: <HTTP_STATUS_CODE> and
|
|
response: "some string of content".
|
|
The JSON string below fakes the available_resources URL on the
|
|
contract server:
|
|
'{"https://contracts.canonical.com/v1/resources": \
|
|
[{"code": 200, "response": {"key": "val1", "key2": "val2"}}]}'
|
|
|
|
:return: List of dicts for each faked response matching the url, or
|
|
and empty list when no matching url found.
|
|
"""
|
|
if self._response_overlay is not None:
|
|
# Cache it so we don't re-read config every readurl call
|
|
return self._response_overlay.get(url, [])
|
|
response_overlay_path = self.cfg.features.get(
|
|
"serviceclient_url_responses"
|
|
)
|
|
if not response_overlay_path:
|
|
self._response_overlay = {}
|
|
elif not os.path.exists(response_overlay_path):
|
|
self._response_overlay = {}
|
|
else:
|
|
self._response_overlay = json.loads(
|
|
system.load_file(response_overlay_path)
|
|
)
|
|
return self._response_overlay.get(url, [])
|
|
|
|
def _get_fake_responses(self, url: str) -> Optional[http.HTTPResponse]:
|
|
"""Return response if faked for this URL in uaclient.conf."""
|
|
responses = self._get_response_overlay(url)
|
|
if not responses:
|
|
return None
|
|
|
|
if len(responses) == 1:
|
|
# When only one response is defined, repeat it for all calls
|
|
response = responses[0]
|
|
else:
|
|
# When multiple responses defined pop the first one off the list.
|
|
response = responses.pop(0)
|
|
|
|
json_dict = {}
|
|
json_list = []
|
|
resp = response["response"]
|
|
if isinstance(resp, dict):
|
|
json_dict = resp
|
|
elif isinstance(resp, list):
|
|
json_list = resp
|
|
|
|
return http.HTTPResponse(
|
|
code=response["code"],
|
|
headers=response.get("headers", {}),
|
|
body=json.dumps(response["response"], sort_keys=True),
|
|
json_dict=json_dict,
|
|
json_list=json_list,
|
|
)
|