Coverage for melissa/launcher/monitoring/rest.py: 34%
67 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-09-22 10:36 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-09-22 10:36 +0000
1#!/usr/bin/python3
3# Copyright (c) 2022, Institut National de Recherche en Informatique et en Automatique (Inria),
4# Poznan Supercomputing and Networking Center (PSNC)
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are met:
9#
10# * Redistributions of source code must retain the above copyright notice, this
11# list of conditions and the following disclaimer.
12#
13# * Redistributions in binary form must reproduce the above copyright notice,
14# this list of conditions and the following disclaimer in the documentation
15# and/or other materials provided with the distribution.
16#
17# * Neither the name of the copyright holder nor the names of its
18# contributors may be used to endorse or promote products derived from
19# this software without specific prior written permission.
20#
21# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32from collections import namedtuple
33import rapidjson
34import logging
35import re
36from http import HTTPStatus
37from http.server import BaseHTTPRequestHandler, HTTPServer
38from typing import Any, Dict, List, Tuple
40from melissa.launcher import __version__ as version
42from melissa.launcher import event
43from melissa.launcher.action import Action
44from melissa.launcher.event import Event
45from melissa.launcher.state_machine import State
47logger = logging.getLogger(__name__)
50Job = namedtuple("Job", ["id", "unique_id", "state"])
53class RestHttpHandler(BaseHTTPRequestHandler):
54 """Incoming request handler for REST API"""
56 def __init__(self, request, client_address, server: HTTPServer) -> None:
57 # ensure the base class constructor is called AFTER initializing all
58 # properties
59 self.server_version = "melissa-launcher/%s" % version
60 super().__init__(request, client_address, server)
62 def log_error(self, format: str, *args, **kwargs) -> None:
63 logger.error(format, *args, **kwargs)
65 def log_message(self, format: str, *args) -> None:
66 logger.debug(format, *args)
68 def _respond(self, data: Dict[str, Any]) -> None:
69 response_str = rapidjson.dumps(data).encode() + b"\n"
70 self.send_response(HTTPStatus.OK)
71 self.send_header("content-type", "application/json")
72 self.send_header("content-length", str(len(response_str)))
73 self.end_headers()
74 self.wfile.write(response_str)
76 def do_GET(self) -> None:
77 """Callable triggered everytime HTTP GET
78 request hits the server.
79 """
80 forbidden = True
81 if "token" not in self.headers:
82 logger.debug("GET refused: no token in header")
83 elif self.headers["token"] != self.server._token: # type: ignore
84 logger.debug("GET refused: wrong token")
85 else:
86 forbidden = False
88 if forbidden:
89 self.send_response(HTTPStatus.FORBIDDEN)
90 self.end_headers()
91 return
93 m = re.fullmatch("/jobs", self.path)
94 if m is not None:
95 self._respond({"jobs": [j for j in self.server._jobs.keys()]}) # type: ignore
96 return
98 m = re.fullmatch("/jobs/([0-9]+)", self.path)
99 if m is not None:
100 uid = int(m.group(1))
101 jobs = [j for j in self.server._jobs.keys() if j == uid] # type: ignore
102 assert len(jobs) <= 1
103 if len(jobs) == 1:
104 self._respond(self.server._jobs[jobs[0]]) # type: ignore
105 return
107 self.send_response(HTTPStatus.NOT_FOUND)
108 self.end_headers()
111class RestHttpServer(HTTPServer):
112 def __init__(
113 self, server_address: Tuple[str, int], token: str, state: State
114 ) -> None:
115 super().__init__(server_address, RestHttpHandler)
116 self._token = token
117 self._state = state
118 self._jobs: Dict[int, Dict[str, Any]] = {}
120 def notify(
121 self, ev: Event, new_state: State, actions: List[Action]
122 ) -> None:
123 self._state = new_state
125 if isinstance(ev, event.JobSubmission):
126 self._jobs[ev.job.unique_id()] = {
127 'id': int(ev.job.id()),
128 'unique_id': int(ev.job.unique_id()),
129 'state': str(ev.job.state())
130 }
132 if isinstance(ev, event.JobUpdate):
133 for job in ev.jobs:
134 self._jobs[job.unique_id()]['state'] = str(job.state())