diff --git a/coretex/cli/modules/user.py b/coretex/cli/modules/user.py index da2051dc..92e3da73 100644 --- a/coretex/cli/modules/user.py +++ b/coretex/cli/modules/user.py @@ -88,7 +88,20 @@ def initializeUserSession() -> None: authenticateWithRefreshToken(userConfig) else: authenticateUser(userConfig) - except (ConfigurationNotFound, InvalidConfiguration): + except ConfigurationNotFound: + ui.errorEcho("User configuration not found.") + if not ui.clickPrompt("Would you like to configure the user? (Y/n)", type = bool, default = True, show_default = False): + raise + + userConfig = configUser() + except InvalidConfiguration as ex: + ui.errorEcho("Invalid user configuration found.") + for error in ex.errors: + ui.errorEcho(f"{error}") + + if not ui.clickPrompt("Would you like to reconfigure the user? (Y/n)", type = bool, default = True, show_default = False): + raise + userConfig = configUser() userConfig.save() diff --git a/coretex/configuration/__init__.py b/coretex/configuration/__init__.py index 5335f56f..0bd9e032 100644 --- a/coretex/configuration/__init__.py +++ b/coretex/configuration/__init__.py @@ -30,6 +30,7 @@ def configMigration(configPath: Path) -> None: with configPath.open("r") as file: oldConfig = json.load(file) + logging.warning(f"[Coretex] >> WARNING: Old configuration:\n{oldConfig}") userRaw = { "username": oldConfig.get("username"), @@ -66,7 +67,6 @@ def configMigration(configPath: Path) -> None: def _syncConfigWithEnv() -> None: - print("i am syncing") # If configuration doesn't exist default one will be created # Initialization of User and Node Configuration classes will do # the necessary sync between config properties and corresponding @@ -75,6 +75,10 @@ def _syncConfigWithEnv() -> None: # old configuration exists, fill out new config files with old configuration oldConfigPath = CONFIG_DIR / "config.json" if oldConfigPath.exists(): + logging.warning( + f"[Coretex] >> WARNING: Old configuration found at path: {oldConfigPath}. Migrating to new configuration." + f"\nFields with invalid values might be overrided in this process." + ) configMigration(oldConfigPath) try: @@ -84,7 +88,7 @@ def _syncConfigWithEnv() -> None: except (ConfigurationNotFound, InvalidConfiguration) as ex: if not isCliRuntime(): logging.error(f">> [Coretex] Error loading configuration. Reason: {ex}") - logging.info("\tIf this message from Coretex Node you can safely ignore it.") + logging.info("\tIf this message came from Coretex Node you can safely ignore it.") try: nodeConfig = NodeConfiguration.load() @@ -99,4 +103,4 @@ def _syncConfigWithEnv() -> None: except (ConfigurationNotFound, InvalidConfiguration) as ex: if not isCliRuntime(): logging.error(f">> [Coretex] Error loading configuration. Reason: {ex}") - logging.info("\tIf this message from Coretex Node you can safely ignore it.") + logging.info("\tIf this message came from Coretex Node you can safely ignore it.") diff --git a/coretex/configuration/base.py b/coretex/configuration/base.py index 7eeb9601..0ac56d68 100644 --- a/coretex/configuration/base.py +++ b/coretex/configuration/base.py @@ -51,10 +51,6 @@ def __init__(self, raw: Dict[str, Any]) -> None: def getConfigPath(cls) -> Path: pass - @abstractmethod - def _isConfigured(self) -> bool: - pass - @abstractmethod def _isConfigValid(self) -> Tuple[bool, List[str]]: pass @@ -71,10 +67,6 @@ def load(cls) -> Self: config = cls(raw) isValid, errors = config._isConfigValid() - - if not config._isConfigured(): - raise ConfigurationNotFound("Configuration not found.") - if not isValid: raise InvalidConfiguration("Invalid configuration found.", errors) diff --git a/coretex/configuration/config_defaults.py b/coretex/configuration/config_defaults.py index ee1cf3f2..57feaffc 100644 --- a/coretex/configuration/config_defaults.py +++ b/coretex/configuration/config_defaults.py @@ -28,9 +28,9 @@ DOCKER_CONTAINER_NAME = "coretex_node" DOCKER_CONTAINER_NETWORK = "coretex_node" DEFAULT_STORAGE_PATH = str(Path.home() / ".coretex") -DEFAULT_RAM_MEMORY = getAvailableRam() -MINIMUM_RAM_MEMORY = 6 -DEFAULT_SWAP_MEMORY = DEFAULT_RAM_MEMORY * 2 +DEFAULT_RAM = getAvailableRam() +MINIMUM_RAM = 6 +DEFAULT_SWAP_MEMORY = DEFAULT_RAM * 2 DEFAULT_SHARED_MEMORY = 2 DEFAULT_CPU_COUNT = cpuCount if cpuCount is not None else 0 DEFAULT_NODE_MODE = NodeMode.execution diff --git a/coretex/configuration/node.py b/coretex/configuration/node.py index 6594657f..bc46f478 100644 --- a/coretex/configuration/node.py +++ b/coretex/configuration/node.py @@ -33,6 +33,10 @@ def getEnvVar(key: str, default: str) -> str: return os.environ[key] +class DockerConfigurationException(Exception): + pass + + class NodeConfiguration(BaseConfiguration): def __init__(self, raw: Dict[str, Any]) -> None: @@ -87,7 +91,7 @@ def nodeRam(self) -> int: nodeRam = self.getOptValue("nodeRam", int) if nodeRam is None: - nodeRam = config_defaults.DEFAULT_RAM_MEMORY + nodeRam = config_defaults.DEFAULT_RAM return nodeRam @@ -195,78 +199,134 @@ def endpointInvocationPrice(self) -> Optional[float]: def endpointInvocationPrice(self, value: Optional[float]) -> None: self._raw["endpointInvocationPrice"] = value - def _isConfigured(self) -> bool: - return ( - isinstance(self._raw.get("nodeName"), str) and - isinstance(self._raw.get("image"), str) and - isinstance(self._raw.get("nodeAccessToken"), str) and - isinstance(self._raw.get("cpuCount"), int) and - isinstance(self._raw.get("nodeRam"), int) and - isinstance(self._raw.get("nodeSwap"), int) and - isinstance(self._raw.get("nodeSharedMemory"), int) and - isinstance(self._raw.get("nodeMode"), int) - ) - - def _isConfigValid(self) -> Tuple[bool, List[str]]: + def validateRamField(self, ramLimit: int) -> Tuple[bool, int, str]: isValid = True - errorMessages = [] - cpuLimit, ramLimit = docker.getResourceLimits() - swapLimit = docker.getDockerSwapLimit() + message = "" + + if ramLimit < config_defaults.MINIMUM_RAM: + isValid = False + raise DockerConfigurationException( + f"Minimum Node RAM requirement ({config_defaults.MINIMUM_RAM}GB) " + f"is higher than your current Docker desktop RAM limit ({ramLimit}GB). " + "Please adjust resource limitations in Docker Desktop settings to match Node requirements." + ) + + defaultRamValue = int(min(max(config_defaults.MINIMUM_RAM, ramLimit), config_defaults.DEFAULT_RAM)) if not isinstance(self._raw.get("nodeRam"), int): - errorMessages.append( + isValid = False + message = ( f"Invalid config \"nodeRam\" field type \"{type(self._raw.get('nodeRam'))}\". Expected: \"int\"" + f"Using default value of {defaultRamValue} GB" ) + + if self.nodeRam < config_defaults.MINIMUM_RAM: isValid = False + message = ( + f"WARNING: Minimum Node RAM requirement ({config_defaults.MINIMUM_RAM}GB) " + f"is higher than the configured value ({self._raw.get('nodeRam')}GB)" + f"Overriding \"nodeRam\" field to match node RAM requirements." + ) + + if self.nodeRam > ramLimit: + isValid = False + message = ( + f"WARNING: RAM limit in Docker Desktop ({ramLimit}GB) " + f"is lower than the configured value ({self._raw.get('nodeRam')}GB)" + f"Overriding \"nodeRam\" field to limit in Docker Desktop." + ) + + return isValid, defaultRamValue, message + + def validateCPUCount(self, cpuLimit: int) -> Tuple[bool, int, str]: + isValid = True + message = "" + defaultCPUCount = config_defaults.DEFAULT_CPU_COUNT if config_defaults.DEFAULT_CPU_COUNT <= cpuLimit else cpuLimit if not isinstance(self._raw.get("cpuCount"), int): - errorMessages.append( + isValid = False + message = ( f"Invalid config \"cpuCount\" field type \"{type(self._raw.get('cpuCount'))}\". Expected: \"int\"" + f"Using default value of {defaultCPUCount} cores" ) + + if self.cpuCount > cpuLimit: isValid = False + message = ( + f"WARNING: CPU limit in Docker Desktop ({cpuLimit}) " + f"is lower than the configured value ({self._raw.get('cpuCount')})" + f"Overriding \"cpuCount\" field to limit in Docker Desktop." + ) + + return isValid, cpuLimit, message + + def validateSWAPMemory(self, swapLimit: int) -> Tuple[bool, int, str]: + isValid = True + message = "" + defaultSWAPMemory = config_defaults.DEFAULT_SWAP_MEMORY if config_defaults.DEFAULT_SWAP_MEMORY <= swapLimit else swapLimit if not isinstance(self._raw.get("nodeSwap"), int): - errorMessages.append( + isValid = False + message = ( f"Invalid config \"nodeSwap\" field type \"{type(self._raw.get('nodeSwap'))}\". Expected: \"int\"" + f"Using default value of {defaultSWAPMemory} GB" ) - isValid = False - if self.cpuCount > cpuLimit: - errorMessages.append( - f"Configuration not valid. CPU limit in Docker Desktop ({cpuLimit}) " - "is lower than the configured value ({self._raw.get('cpuCount')})" + if self.nodeSwap > swapLimit: + isValid = False + message = ( + f"WARNING: SWAP limit in Docker Desktop ({swapLimit}GB) " + f"is lower than the configured value ({self.nodeSwap}GB)" + f"Overriding \"nodeSwap\" field to limit in Docker Desktop." ) + + return isValid, defaultSWAPMemory, message + + def _isConfigValid(self) -> Tuple[bool, List[str]]: + isValid = True + errorMessages = [] + cpuLimit, ramLimit = docker.getResourceLimits() + swapLimit = docker.getDockerSwapLimit() + + if not isinstance(self._raw.get("nodeName"), str): isValid = False + errorMessages.append("Required field \"nodeName\" missing") - if ramLimit < config_defaults.MINIMUM_RAM_MEMORY: - errorMessages.append( - f"Minimum Node RAM requirement ({config_defaults.MINIMUM_RAM_MEMORY}GB) " - "is higher than your current Docker desktop RAM limit ({ramLimit}GB). " - "Please adjust resource limitations in Docker Desktop settings to match Node requirements." - ) + if not isinstance(self._raw.get("image"), str): isValid = False + errorMessages.append("Required field \"image\" missing") - if self.nodeRam > ramLimit: - errorMessages.append( - f"Configuration not valid. RAM limit in Docker Desktop ({ramLimit}GB) " - "is lower than the configured value ({self._raw.get('nodeRam')}GB)" - ) + if not isinstance(self._raw.get("nodeAccessToken"), str): isValid = False + errorMessages.append("Required field \"nodeAccessToken\" missing") - if self.nodeRam < config_defaults.MINIMUM_RAM_MEMORY: - errorMessages.append( - f"Configuration not valid. Minimum Node RAM requirement ({config_defaults.MINIMUM_RAM_MEMORY}GB) " - "is higher than the configured value ({self._raw.get('nodeRam')}GB)" - ) + if not isinstance(self._raw.get("nodeName"), str): + errorMessages.append(f"Invalid configuration. Missing required field \"nodeName\".") isValid = False - if self.nodeSwap > swapLimit: - errorMessages.append( - f"Configuration not valid. SWAP limit in Docker Desktop ({swapLimit}GB) " - "is lower than the configured value ({self.nodeSwap}GB)" - ) + if not isinstance(self._raw.get("image"), str): + errorMessages.append(f"Invalid configuration. Missing required field \"image\".") + isValid = False + + if not isinstance(self._raw.get("nodeAccessToken"), str): + errorMessages.append(f"Invalid configuration. Missing required field \"nodeAccessToken\".") isValid = False + isRamValid, nodeRam, message = self.validateRamField(ramLimit) + if not isRamValid: + errorMessages.append(message) + self.nodeRam = nodeRam + + isCPUCountValid, cpuCount, message = self.validateCPUCount(cpuLimit) + if not isCPUCountValid: + errorMessages.append(message) + self.cpuCount = cpuCount + + isSWAPMemoryValid, nodeSwap, message = self.validateSWAPMemory(swapLimit) + if not isSWAPMemoryValid: + errorMessages.append(message) + self.nodeSwap = nodeSwap + return isValid, errorMessages def getInitScriptPath(self) -> Optional[Path]: diff --git a/coretex/configuration/user.py b/coretex/configuration/user.py index a0f3145c..74bfc17d 100644 --- a/coretex/configuration/user.py +++ b/coretex/configuration/user.py @@ -17,11 +17,8 @@ from pathlib import Path from typing import Dict, Any, List, Optional, Tuple -from typing_extensions import Self from datetime import datetime, timezone -import os - from .base import BaseConfiguration, CONFIG_DIR from ..utils import decodeDate @@ -34,9 +31,6 @@ class UserConfiguration(BaseConfiguration): def __init__(self, raw: Dict[str, Any]) -> None: super().__init__(raw) - if not "CTX_API_URL" in os.environ: - os.environ["CTX_API_URL"] = self.serverUrl - @classmethod def getConfigPath(cls) -> Path: return CONFIG_DIR / "user_config.json" @@ -105,20 +99,17 @@ def projectId(self) -> Optional[int]: def projectId(self, value: Optional[int]) -> None: self._raw["projectId"] = value - def _isConfigured(self) -> bool: - return self._raw.get("username") is not None and self._raw.get("password") is not None - def _isConfigValid(self) -> Tuple[bool, List[str]]: isValid = True errorMessages = [] if self._raw.get("username") is None or not isinstance(self._raw.get("username"), str): - errorMessages.append("Field \"username\" not found in configuration.") isValid = False + errorMessages.append("Missing required field \"username\" in user configuration.") if self._raw.get("password") is None or not isinstance(self._raw.get("password"), str): - errorMessages.append("Field \"password\" not found in configuration.") isValid = False + errorMessages.append("Missing required field \"password\" in user configuration.") return isValid, errorMessages