diff --git a/src/aquarium.py b/src/aquarium.py index 276ecea55..0c1428e27 100755 --- a/src/aquarium.py +++ b/src/aquarium.py @@ -191,6 +191,9 @@ async def bootstrap(self): logger.error(f"Unable to pre-init the node: {e.message}") sys.exit(1) + self.config.init() + self.kvstore.init() + self._init_task = asyncio.create_task(self.init()) async def init(self): @@ -210,8 +213,6 @@ async def init(self): sys.exit(1) logger.info("Init Node Manager.") - self.config.init() - self.kvstore.init() self.gstate_init() self.nodemgr.init() @@ -292,8 +293,16 @@ async def shutdown(self): async def start_uvicorn(self): logger.debug("Starting uvicorn") - # TODO(jhesketh): Check if we need https or not - uvicorn_config = UvicornConfig(self.app, host="0.0.0.0", port=80) + if self.gstate.config.options.ssl.use_ssl: + uvicorn_config = UvicornConfig( + self.app, + host="0.0.0.0", + port=443, + ssl_keyfile=self.gstate.config.ssl_keypath, + ssl_certfile=self.gstate.config.ssl_certpath, + ) + else: + uvicorn_config = UvicornConfig(self.app, host="0.0.0.0", port=80) self.uvicorn = AquariumUvicorn(config=uvicorn_config) asyncio.create_task(self.uvicorn.serve()) # TODO(jhesketh): When in https mode, test if running a second uvicorn diff --git a/src/gravel/api/local.py b/src/gravel/api/local.py index c3ecc428a..1d2bb4f7a 100644 --- a/src/gravel/api/local.py +++ b/src/gravel/api/local.py @@ -21,6 +21,8 @@ from gravel.api import install_gate, jwt_auth_scheme from gravel.cephadm.models import VolumeDeviceModel +from gravel.controllers.config import SSLOptionsModel +from gravel.controllers.gstate import GlobalState from gravel.controllers.inventory.nodeinfo import NodeInfoModel from gravel.controllers.nodes.mgr import NodeMgr from gravel.controllers.nodes.requirements import ( @@ -48,6 +50,12 @@ class EventModel(BaseModel): message: str +class SSLConfigModel(BaseModel): + use_ssl: bool = Field(False, title="Whether https is enabled.") + key_contents: str = Field(title="The SSL private key.") + cert_contents: str = Field(title="The SSL certificate.") + + @router.get( "/volumes", name="Obtain local volumes", @@ -187,6 +195,30 @@ async def get_events( return [EventModel.parse_obj(event) for event in events] +@router.get( + "/ssl", + name="Get current SSL configuration", + response_model=SSLOptionsModel, +) +async def get_ssl_config( + request: Request, _: Callable = Depends(jwt_auth_scheme) +) -> SSLOptionsModel: + return request.app.state.gstate.config.options.ssl + + +@router.post("/ssl", name="Update SSL configuration") +async def apply_ssl_config( + request: Request, + req_params: SSLConfigModel, + _: Callable = Depends(jwt_auth_scheme), +) -> bool: + request.app.state.gstate.config.write_ssl_key(req_params.key_contents) + request.app.state.gstate.config.write_ssl_cert(req_params.cert_contents) + request.app.state.gstate.config.options.ssl.use_ssl = req_params.use_ssl + request.app.state.gstate.request_restart_uvicorn() + return request.app.state.gstate.config.saveConfig() + + @router.post("/reboot", name="Reboot the system") async def reboot(_: Callable = Depends(jwt_auth_scheme)) -> None: ret, _, stderr = await aqr_run_cmd(["systemctl", "reboot"]) # type: ignore diff --git a/src/gravel/controllers/config.py b/src/gravel/controllers/config.py index cd8e32a7a..06741e9d7 100644 --- a/src/gravel/controllers/config.py +++ b/src/gravel/controllers/config.py @@ -77,6 +77,10 @@ def get_image(self) -> str: return f"{self.registry}/{self.image}" +class SSLOptionsModel(BaseModel): + use_ssl: bool = Field(False, title="Whether https is enabled.") + + class OptionsModel(BaseModel): inventory: InventoryOptionsModel = Field(InventoryOptionsModel()) storage: StorageOptionsModel = Field(StorageOptionsModel()) @@ -85,6 +89,7 @@ class OptionsModel(BaseModel): network: NetworkOptionsModel = Field(NetworkOptionsModel()) auth: AuthOptionsModel = Field(AuthOptionsModel()) containers: ContainersOptionsModel = Field(ContainersOptionsModel()) + ssl: SSLOptionsModel = Field(SSLOptionsModel()) class ConfigModel(BaseModel): @@ -99,6 +104,8 @@ def __init__(self, path: Optional[str] = None): path = _get_default_confdir() self._confdir = Path(path) self.confpath = self._confdir.joinpath(Path("config.json")) + self.ssl_keypath = self._confdir.joinpath(Path("ssl_key.pem")) + self.ssl_certpath = self._confdir.joinpath(Path("ssl_cert.pem")) logger.debug(f"Aquarium config dir: {self._confdir}") def init(self) -> None: @@ -134,9 +141,21 @@ def _registry_config_from_env(self): if len(keys) > 0: logger.warning("Using custom registry config from environment") - def _saveConfig(self, conf: ConfigModel) -> None: + def _saveConfig(self, conf: ConfigModel) -> bool: logger.debug(f"Writing Aquarium config: {self.confpath}") self.confpath.write_text(conf.json(indent=2)) + return True + + def saveConfig(self) -> bool: + return self._saveConfig(self.config) + + def write_ssl_key(self, contents: str) -> None: + self.ssl_keypath.write_text(contents) + self.ssl_keypath.chmod(0o600) + + def write_ssl_cert(self, contents: str) -> None: + self.ssl_certpath.write_text(contents) + self.ssl_certpath.chmod(0o644) @property def options(self) -> OptionsModel: