Coverage for /home/fedora/jumpstarter/packages/jumpstarter-driver-http/jumpstarter_driver_http/driver.py: 59%
82 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 20:29 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 20:29 +0000
1import os
2from dataclasses import dataclass, field
3from typing import Optional
5import anyio
6from aiohttp import web
7from jumpstarter_driver_opendal.driver import Opendal
9from jumpstarter.driver import Driver, export
12class HttpServerError(Exception):
13 """Base exception for HTTP server errors"""
16class FileWriteError(HttpServerError):
17 """Exception raised when file writing fails"""
20@dataclass(kw_only=True)
21class HttpServer(Driver):
22 """HTTP Server driver for Jumpstarter"""
24 root_dir: str = "/var/www"
25 host: str | None = field(default=None)
26 port: int = 8080
27 timeout: int = field(default=600)
28 app: web.Application = field(init=False, default_factory=web.Application)
29 runner: Optional[web.AppRunner] = field(init=False, default=None)
31 def __post_init__(self):
32 if hasattr(super(), "__post_init__"):
33 super().__post_init__()
35 os.makedirs(self.root_dir, exist_ok=True)
37 self.children["storage"] = Opendal(scheme="fs", kwargs={"root": self.root_dir})
38 self.app.router.add_routes(
39 [
40 web.static("/", self.root_dir),
41 ]
42 )
43 if self.host is None:
44 self.host = self.get_default_ip()
46 def get_default_ip(self):
47 try:
48 import socket
50 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
51 s.connect(("8.8.8.8", 80))
52 return s.getsockname()[0]
53 except Exception:
54 self.logger.warning("Could not determine default IP address, falling back to 0.0.0.0")
55 return "0.0.0.0"
57 @classmethod
58 def client(cls) -> str:
59 """Return the import path of the corresponding client"""
60 return "jumpstarter_driver_http.client.HttpServerClient"
62 @export
63 async def start(self):
64 """
65 Start the HTTP server.
67 Raises:
68 HttpServerError: If the server fails to start.
69 """
70 if self.runner is not None:
71 self.logger.warning("HTTP server is already running.")
72 return
74 self.runner = web.AppRunner(self.app)
75 if self.runner:
76 await self.runner.setup()
78 site = web.TCPSite(self.runner, self.host, self.port)
79 await site.start()
80 self.logger.info(f"HTTP server started at http://{self.host}:{self.port}")
82 @export
83 async def stop(self):
84 """
85 Stop the HTTP server.
87 Raises:
88 HttpServerError: If the server fails to stop.
89 """
90 if self.runner is None:
91 self.logger.warning("HTTP server is not running.")
92 return
94 await self.runner.cleanup()
95 self.logger.info("HTTP server stopped.")
96 self.runner = None
98 @export
99 def get_url(self) -> str:
100 """
101 Get the base URL of the HTTP server.
103 Returns:
104 str: Base URL of the HTTP server.
105 """
106 return f"http://{self.host}:{self.port}"
108 @export
109 def get_host(self) -> str | None:
110 """
111 Get the host IP address of the HTTP server.
113 Returns:
114 str: Host IP address.
115 """
116 return self.host
118 @export
119 def get_port(self) -> int:
120 """
121 Get the port number of the HTTP server.
123 Returns:
124 int: Port number.
125 """
126 return self.port
128 def close(self):
129 if self.runner:
130 try:
131 if anyio.get_current_task():
132 anyio.from_thread.run(self._async_cleanup)
133 except Exception as e:
134 self.logger.warning(f"HTTP server cleanup failed synchronously: {e}")
135 self.runner = None
136 super().close()
138 async def _async_cleanup(self):
139 try:
140 if self.runner:
141 await self.runner.shutdown()
142 await self.runner.cleanup()
143 self.logger.info("HTTP server cleanup completed asynchronously.")
144 except Exception as e:
145 self.logger.error(f"HTTP server cleanup failed asynchronously: {e}")