common/recipes-rest/rest-api/files/common_utils.py (95 lines of code) (raw):

import asyncio import json import re import os from aiohttp import web from concurrent.futures import ThreadPoolExecutor from typing import Dict, List, Optional, Set, Tuple, Union from common_webapp import WebApp common_executor = ThreadPoolExecutor(5) # cache for endpoint_children ENDPOINT_CHILDREN = {} # type: Dict[str, Set[str]] def common_force_async(func): # common handler will use its own executor (thread based), # we initentionally separated this from the executor of # board-specific REST handler, so that any problem in # common REST handlers will not interfere with board-specific # REST handler, and vice versa async def func_wrapper(*args, **kwargs): # Convert the possibly blocking helper function into async loop = asyncio.get_event_loop() result = await loop.run_in_executor(common_executor, func, *args, **kwargs) return result return func_wrapper # When we call request.json() in asynchronous function, a generator # will be returned. Upon calling next(), the generator will either : # # 1) return the next data as usual, # - OR - # 2) throw StopIteration, with its first argument as the data # (this is for indicating that no more data is available) # # Not sure why aiohttp's request generator is implemented this way, but # the following function will handle both of the cases mentioned above. def get_data_from_generator(data_generator): data = None try: data = next(data_generator) except StopIteration as e: data = e.args[0] return data def get_endpoints(path: str): app = WebApp.instance() endpoints = set() # type: Set[str] splitpaths = [] # type: List[str] splitpaths = path.split("/") position = len(splitpaths) if path in ENDPOINT_CHILDREN: endpoints = ENDPOINT_CHILDREN[path] # type: ignore else: for route in app.router.resources(): string = str(route) rest_route_path = string[string.index(" ") :] if rest_route_path.endswith(">"): rest_route_path = rest_route_path[:-1] rest_route_path_slices = rest_route_path.split("/") if len(rest_route_path_slices) > position and path in string: endpoints.add(rest_route_path_slices[position]) endpoints = sorted(endpoints) # type: ignore ENDPOINT_CHILDREN[path] = endpoints return endpoints # aiohttp allows users to pass a "dumps" function, which will convert # different data types to JSON. This new dumps function will simply call # the original dumps function, along with the new type handler that can # process byte strings. def dumps_bytestr(obj): # aiohttp's json_response function uses py3 JSON encoder, which # doesn't know how to handle a byte string. So we extend this function # to handle the case. This is a standard way to add a new type, # as stated in JSON encoder source code. def default_bytestr(o): # If the object is a byte string, it will be converted # to a regular string. Otherwise we move on (pass) to # the usual error generation routine try: o = o.decode("utf-8") return o except AttributeError: pass raise TypeError(repr(o) + " is not JSON serializable") # Just call default dumps function, but pass the new default function # that is capable of process byte strings. return json.dumps(obj, default=default_bytestr) def running_systemd(): return "systemd" in os.readlink("/proc/1/exe") async def async_exec( cmd: Union[List[str], str], shell: bool = False ) -> Tuple[Optional[int], str, str]: if shell: proc = await asyncio.create_subprocess_shell( cmd, # type: ignore stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) else: proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() data = stdout.decode() err = stderr.decode() await proc.wait() return proc.returncode, data, err def parse_expand_level(request: web.Request) -> int: # Redfish supports the $expand query parameter # Spec: http://redfish.dmtf.org/schemas/DSP0266_1.7.0.html#use-of-the-expand-query-parameter-a-id-expand-parameter-a- params = request.rel_url.query expand_level = params.get("$expand", None) if not expand_level: return 0 if expand_level == "*": return 999 # expands levels are handled downstream as int-s. Return 999 so we surely expand every child node matches = re.match(r"\.\(\$levels=(\d)\)", expand_level) if matches: return int(matches.group(1)) try: return int(expand_level) except ValueError: raise web.HTTPBadRequest( body=json.dumps( { "reason": "Invalid expand level supplied: {expand_level}".format( expand_level=expand_level ) } ), content_type="application/json", )