primitive 0.2.10__py3-none-any.whl → 0.2.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- primitive/__about__.py +1 -1
- primitive/agent/actions.py +62 -120
- primitive/agent/commands.py +2 -1
- primitive/agent/runner.py +43 -34
- primitive/agent/uploader.py +2 -2
- primitive/cli.py +2 -0
- primitive/client.py +41 -16
- primitive/daemons/actions.py +48 -62
- primitive/daemons/commands.py +68 -22
- primitive/daemons/launch_agents.py +205 -122
- primitive/daemons/launch_service.py +224 -164
- primitive/daemons/ui.py +41 -0
- primitive/db/base.py +5 -0
- primitive/db/models.py +88 -0
- primitive/db/sqlite.py +34 -0
- primitive/exec/actions.py +0 -1
- primitive/files/actions.py +0 -1
- primitive/hardware/actions.py +11 -10
- primitive/hardware/commands.py +1 -68
- primitive/hardware/ui.py +67 -0
- primitive/monitor/actions.py +199 -0
- primitive/monitor/commands.py +13 -0
- primitive/reservations/actions.py +0 -2
- primitive/utils/auth.py +0 -2
- primitive/utils/daemons.py +54 -0
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/METADATA +3 -1
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/RECORD +30 -22
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/WHEEL +0 -0
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/licenses/LICENSE.txt +0 -0
| @@ -2,178 +2,238 @@ import os | |
| 2 2 | 
             
            import configparser
         | 
| 3 3 | 
             
            import subprocess
         | 
| 4 4 | 
             
            from pathlib import Path
         | 
| 5 | 
            +
            from loguru import logger
         | 
| 6 | 
            +
            from ..utils.daemons import Daemon
         | 
| 5 7 |  | 
| 6 8 | 
             
            HOME_DIRECTORY = Path.home()
         | 
| 7 | 
            -
             | 
| 8 9 | 
             
            PRIMITIVE_BINARY_PATH = Path(HOME_DIRECTORY / ".pyenv" / "shims" / "primitive")
         | 
| 9 10 |  | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
            )
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                 | 
| 17 | 
            -
            )
         | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
                 | 
| 22 | 
            -
                     | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 11 | 
            +
             | 
| 12 | 
            +
            class LaunchService(Daemon):
         | 
| 13 | 
            +
                def __init__(self, label: str):
         | 
| 14 | 
            +
                    self.label = label
         | 
| 15 | 
            +
                    self.name = label.split(".")[-1]
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                @property
         | 
| 18 | 
            +
                def service_name(self) -> str:
         | 
| 19 | 
            +
                    return f"{self.label}.service"
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                @property
         | 
| 22 | 
            +
                def file_path(self) -> Path:
         | 
| 23 | 
            +
                    return Path(HOME_DIRECTORY / ".config" / "systemd" / "user" / self.service_name)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                @property
         | 
| 26 | 
            +
                def logs(self) -> Path:
         | 
| 27 | 
            +
                    return Path(HOME_DIRECTORY / ".cache" / "primitive" / f"{self.label}.log")
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def stop(self) -> bool:
         | 
| 30 | 
            +
                    try:
         | 
| 31 | 
            +
                        if self.is_active():
         | 
| 32 | 
            +
                            stop_existing_service = f"systemctl --user stop {self.service_name}"
         | 
| 33 | 
            +
                            subprocess.check_output(
         | 
| 34 | 
            +
                                stop_existing_service.split(" "), stderr=subprocess.DEVNULL
         | 
| 35 | 
            +
                            )
         | 
| 36 | 
            +
                            logger.info(f":white_check_mark: {self.label} stopped successfully!")
         | 
| 37 | 
            +
                        return True
         | 
| 38 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 39 | 
            +
                        if exception.returncode == 4:
         | 
| 40 | 
            +
                            logger.debug(f"{self.label} is not running or does not exist.")
         | 
| 41 | 
            +
                            return True
         | 
| 42 | 
            +
                        else:
         | 
| 43 | 
            +
                            logger.error(f"Unable to stop {self.label}, {exception.returncode}")
         | 
| 44 | 
            +
                            logger.error(exception)
         | 
| 45 | 
            +
                            return False
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def start(self) -> bool:
         | 
| 48 | 
            +
                    try:
         | 
| 49 | 
            +
                        start_new_service = f"systemctl --user start {self.service_name}"
         | 
| 50 | 
            +
                        subprocess.check_output(start_new_service.split(" "))
         | 
| 51 | 
            +
                        logger.info(f":white_check_mark: {self.label} started successfully!")
         | 
| 52 | 
            +
                        return True
         | 
| 53 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 54 | 
            +
                        logger.error(f"Unable to start {self.label}")
         | 
| 55 | 
            +
                        logger.error(exception)
         | 
| 56 | 
            +
                        return False
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def disable(self) -> bool:
         | 
| 59 | 
            +
                    try:
         | 
| 60 | 
            +
                        if self.is_installed():
         | 
| 61 | 
            +
                            disable_existing_service = (
         | 
| 62 | 
            +
                                f"systemctl --user disable {self.service_name}"
         | 
| 63 | 
            +
                            )
         | 
| 64 | 
            +
                            subprocess.check_output(
         | 
| 65 | 
            +
                                disable_existing_service.split(" "), stderr=subprocess.DEVNULL
         | 
| 66 | 
            +
                            )
         | 
| 67 | 
            +
                        return True
         | 
| 68 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 69 | 
            +
                        logger.error(f"Unable to disable {self.label}")
         | 
| 70 | 
            +
                        logger.error(exception)
         | 
| 71 | 
            +
                        return False
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def enable(self) -> bool:
         | 
| 74 | 
            +
                    try:
         | 
| 75 | 
            +
                        enable_service = f"systemctl --user enable {self.service_name}"
         | 
| 76 | 
            +
                        subprocess.check_output(
         | 
| 77 | 
            +
                            enable_service.split(" "), stderr=subprocess.DEVNULL
         | 
| 78 | 
            +
                        )
         | 
| 79 | 
            +
                        return True
         | 
| 80 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 81 | 
            +
                        logger.error(f"Unable to enable {self.label}")
         | 
| 82 | 
            +
                        logger.error(exception)
         | 
| 83 | 
            +
                        return False
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def verify(self) -> bool:
         | 
| 86 | 
            +
                    systemctl_check = (
         | 
| 87 | 
            +
                        f"systemctl --user show {self.service_name} -p CanStart --value"
         | 
| 35 88 | 
             
                    )
         | 
| 36 | 
            -
                     | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                         | 
| 47 | 
            -
             | 
| 89 | 
            +
                    try:
         | 
| 90 | 
            +
                        output = (
         | 
| 91 | 
            +
                            subprocess.check_output(systemctl_check.split(" ")).decode().strip()
         | 
| 92 | 
            +
                        )
         | 
| 93 | 
            +
                        if output == "no":
         | 
| 94 | 
            +
                            raise Exception(f"{systemctl_check} yielded {output}")
         | 
| 95 | 
            +
                        return True
         | 
| 96 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 97 | 
            +
                        logger.error(f"Unable to verify {self.label}")
         | 
| 98 | 
            +
                        logger.error(exception)
         | 
| 99 | 
            +
                        return False
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def view_logs(self) -> None:
         | 
| 102 | 
            +
                    follow_logs = f"tail -f -n +1 {self.logs}"
         | 
| 103 | 
            +
                    os.system(follow_logs)
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def populate(self) -> bool:
         | 
| 106 | 
            +
                    self.file_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 107 | 
            +
                    self.file_path.touch()
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    if self.file_path.exists():
         | 
| 110 | 
            +
                        self.file_path.unlink()
         | 
| 111 | 
            +
                    self.file_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 112 | 
            +
                    self.file_path.touch()
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                    config = configparser.ConfigParser()
         | 
| 115 | 
            +
                    config.optionxform = str  # type: ignore
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    config["Unit"] = {
         | 
| 118 | 
            +
                        "Description": "Primitive Agent",
         | 
| 119 | 
            +
                        "After": "network.target",
         | 
| 120 | 
            +
                    }
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    found_primitive_binary_path = PRIMITIVE_BINARY_PATH
         | 
| 123 | 
            +
                    if not PRIMITIVE_BINARY_PATH.exists():
         | 
| 124 | 
            +
                        result = subprocess.run(["which", "primitive"], capture_output=True)
         | 
| 125 | 
            +
                        if result.returncode == 0:
         | 
| 126 | 
            +
                            found_primitive_binary_path = result.stdout.decode().rstrip("\n")
         | 
| 127 | 
            +
                        else:
         | 
| 128 | 
            +
                            print("primitive binary not found")
         | 
| 129 | 
            +
                            return False
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    config["Service"] = {
         | 
| 132 | 
            +
                        "ExecStart": f'/bin/sh -lc "{found_primitive_binary_path} agent"',
         | 
| 133 | 
            +
                        "Restart": "always",
         | 
| 134 | 
            +
                        "StandardError": f"append:{self.logs}",
         | 
| 135 | 
            +
                        "StandardOutput": f"append:{self.logs}",
         | 
| 136 | 
            +
                    }
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    config["Install"] = {
         | 
| 139 | 
            +
                        "WantedBy": "default.target",
         | 
| 140 | 
            +
                    }
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                    try:
         | 
| 143 | 
            +
                        with open(self.file_path, "w") as service_file:
         | 
| 144 | 
            +
                            config.write(service_file)
         | 
| 145 | 
            +
                    except IOError as exception:
         | 
| 146 | 
            +
                        print(f"populate_service_file: {exception}")
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                    self.file_path.chmod(0o644)
         | 
| 149 | 
            +
                    return self.verify()
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def create_stdout_file(self) -> bool:
         | 
| 152 | 
            +
                    try:
         | 
| 153 | 
            +
                        if not self.logs.exists():
         | 
| 154 | 
            +
                            self.logs.parent.mkdir(parents=True, exist_ok=True)
         | 
| 155 | 
            +
                            self.logs.touch()
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                        return True
         | 
| 158 | 
            +
                    except Exception as e:
         | 
| 159 | 
            +
                        logger.error(
         | 
| 160 | 
            +
                            f"Unable to create log file at {self.logs} for daemon {self.label}"
         | 
| 161 | 
            +
                        )
         | 
| 162 | 
            +
                        logger.error(e)
         | 
| 163 | 
            +
                        return False
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                def delete_stdout_file(self) -> bool:
         | 
| 166 | 
            +
                    try:
         | 
| 167 | 
            +
                        if self.logs.exists():
         | 
| 168 | 
            +
                            self.logs.unlink()
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                        return True
         | 
| 171 | 
            +
                    except Exception as e:
         | 
| 172 | 
            +
                        logger.error(
         | 
| 173 | 
            +
                            f"Unable to delete log file at {self.logs} for daemon {self.label}"
         | 
| 48 174 | 
             
                        )
         | 
| 49 | 
            -
                         | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
                     | 
| 175 | 
            +
                        logger.error(e)
         | 
| 176 | 
            +
                        return False
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                def delete_service_file(self) -> bool:
         | 
| 179 | 
            +
                    try:
         | 
| 180 | 
            +
                        if self.file_path.exists():
         | 
| 181 | 
            +
                            self.file_path.unlink()
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                        return True
         | 
| 184 | 
            +
                    except Exception as e:
         | 
| 185 | 
            +
                        logger.error(
         | 
| 186 | 
            +
                            f"Unable to delete service file at {self.file_path} for daemon {self.label}"
         | 
| 187 | 
            +
                        )
         | 
| 188 | 
            +
                        logger.error(e)
         | 
| 189 | 
            +
                        return False
         | 
| 54 190 |  | 
| 191 | 
            +
                def install(self) -> bool:
         | 
| 192 | 
            +
                    return all(
         | 
| 193 | 
            +
                        [
         | 
| 194 | 
            +
                            self.stop(),
         | 
| 195 | 
            +
                            self.disable(),
         | 
| 196 | 
            +
                            self.populate(),
         | 
| 197 | 
            +
                            self.create_stdout_file(),
         | 
| 198 | 
            +
                            self.enable(),
         | 
| 199 | 
            +
                            self.start(),
         | 
| 200 | 
            +
                        ]
         | 
| 201 | 
            +
                    )
         | 
| 55 202 |  | 
| 56 | 
            -
            def  | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 203 | 
            +
                def uninstall(self) -> bool:
         | 
| 204 | 
            +
                    return all(
         | 
| 205 | 
            +
                        [
         | 
| 206 | 
            +
                            self.stop(),
         | 
| 207 | 
            +
                            self.disable(),
         | 
| 208 | 
            +
                            self.delete_service_file(),
         | 
| 209 | 
            +
                            self.delete_stdout_file(),
         | 
| 210 | 
            +
                        ]
         | 
| 60 211 | 
             
                    )
         | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
                PRIMITIVE_AGENT_SERVICE_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
         | 
| 75 | 
            -
                PRIMITIVE_AGENT_SERVICE_FILEPATH.touch()
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                config = configparser.ConfigParser()
         | 
| 78 | 
            -
                config.optionxform = str  # type: ignore
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                config["Unit"] = {
         | 
| 81 | 
            -
                    "Description": "Primitive Agent",
         | 
| 82 | 
            -
                    "After": "network.target",
         | 
| 83 | 
            -
                }
         | 
| 84 | 
            -
             | 
| 85 | 
            -
                found_primitive_binary_path = PRIMITIVE_BINARY_PATH
         | 
| 86 | 
            -
                if not PRIMITIVE_BINARY_PATH.exists():
         | 
| 87 | 
            -
                    result = subprocess.run(["which", "primitive"], capture_output=True)
         | 
| 88 | 
            -
                    if result.returncode == 0:
         | 
| 89 | 
            -
                        found_primitive_binary_path = result.stdout.decode().rstrip("\n")
         | 
| 90 | 
            -
                    else:
         | 
| 91 | 
            -
                        print("primitive binary not found")
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                def is_active(self) -> bool:
         | 
| 214 | 
            +
                    try:
         | 
| 215 | 
            +
                        is_service_active = (
         | 
| 216 | 
            +
                            f"systemctl --user show {self.service_name} -p ActiveState --value"
         | 
| 217 | 
            +
                        )
         | 
| 218 | 
            +
                        output = (
         | 
| 219 | 
            +
                            subprocess.check_output(is_service_active.split(" ")).decode().strip()
         | 
| 220 | 
            +
                        )
         | 
| 221 | 
            +
                        return output == "active"
         | 
| 222 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 223 | 
            +
                        logger.error(f"Unable to check if {self.label} is active")
         | 
| 224 | 
            +
                        logger.error(exception)
         | 
| 92 225 | 
             
                        return False
         | 
| 93 226 |  | 
| 94 | 
            -
                 | 
| 95 | 
            -
                     | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 107 | 
            -
                        config.write(service_file)
         | 
| 108 | 
            -
                except IOError as exception:
         | 
| 109 | 
            -
                    print(f"populate_service_file: {exception}")
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                PRIMITIVE_AGENT_SERVICE_FILEPATH.chmod(0o644)
         | 
| 112 | 
            -
                verify_service_file()
         | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
            def verify_service_file():
         | 
| 116 | 
            -
                systemctl_check = (
         | 
| 117 | 
            -
                    f"systemctl --user show {PRIMITIVE_AGENT_SERVICE} -p CanStart --value"
         | 
| 118 | 
            -
                )
         | 
| 119 | 
            -
                try:
         | 
| 120 | 
            -
                    output = subprocess.check_output(systemctl_check.split(" ")).decode().strip()
         | 
| 121 | 
            -
                    if output == "no":
         | 
| 122 | 
            -
                        raise Exception(f"{systemctl_check} yielded {output}")
         | 
| 123 | 
            -
                    return True
         | 
| 124 | 
            -
                except subprocess.CalledProcessError as exception:
         | 
| 125 | 
            -
                    print("verify_service_file: ", exception)
         | 
| 126 | 
            -
                    return False
         | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
            def create_stdout_file():
         | 
| 130 | 
            -
                if not PRIMITIVE_AGENT_LOGS_FILEPATH.exists():
         | 
| 131 | 
            -
                    PRIMITIVE_AGENT_LOGS_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
         | 
| 132 | 
            -
                    PRIMITIVE_AGENT_LOGS_FILEPATH.touch()
         | 
| 133 | 
            -
             | 
| 134 | 
            -
             | 
| 135 | 
            -
            def delete_stdout_file():
         | 
| 136 | 
            -
                if PRIMITIVE_AGENT_LOGS_FILEPATH.exists():
         | 
| 137 | 
            -
                    PRIMITIVE_AGENT_LOGS_FILEPATH.unlink()
         | 
| 138 | 
            -
             | 
| 139 | 
            -
             | 
| 140 | 
            -
            def enable_service():
         | 
| 141 | 
            -
                try:
         | 
| 142 | 
            -
                    enable_service = f"systemctl --user enable {PRIMITIVE_AGENT_SERVICE}"
         | 
| 143 | 
            -
                    subprocess.check_output(enable_service.split(" "))
         | 
| 144 | 
            -
                    return True
         | 
| 145 | 
            -
                except subprocess.CalledProcessError as exception:
         | 
| 146 | 
            -
                    print("enable_service: ", exception)
         | 
| 147 | 
            -
                    return False
         | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
            def start_service():
         | 
| 151 | 
            -
                try:
         | 
| 152 | 
            -
                    start_new_service = f"systemctl --user start {PRIMITIVE_AGENT_SERVICE}"
         | 
| 153 | 
            -
                    subprocess.check_output(start_new_service.split(" "))
         | 
| 154 | 
            -
                    return True
         | 
| 155 | 
            -
                except subprocess.CalledProcessError as exception:
         | 
| 156 | 
            -
                    print("start_service: ", exception)
         | 
| 157 | 
            -
                    return False
         | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
            def view_service_logs():
         | 
| 161 | 
            -
                follow_logs = f"tail -f -n +1 {PRIMITIVE_AGENT_LOGS_FILEPATH}"
         | 
| 162 | 
            -
                os.system(follow_logs)
         | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 165 | 
            -
            def full_service_install():
         | 
| 166 | 
            -
                stop_service()
         | 
| 167 | 
            -
                disable_service()
         | 
| 168 | 
            -
                populate_service_file()
         | 
| 169 | 
            -
                create_stdout_file()
         | 
| 170 | 
            -
                enable_service()
         | 
| 171 | 
            -
                start_service()
         | 
| 172 | 
            -
             | 
| 173 | 
            -
             | 
| 174 | 
            -
            def full_service_uninstall():
         | 
| 175 | 
            -
                stop_service()
         | 
| 176 | 
            -
                disable_service()
         | 
| 177 | 
            -
                if PRIMITIVE_AGENT_SERVICE_FILEPATH.exists():
         | 
| 178 | 
            -
                    PRIMITIVE_AGENT_SERVICE_FILEPATH.unlink()
         | 
| 179 | 
            -
                delete_stdout_file()
         | 
| 227 | 
            +
                def is_installed(self) -> bool:
         | 
| 228 | 
            +
                    try:
         | 
| 229 | 
            +
                        is_service_active = (
         | 
| 230 | 
            +
                            f"systemctl --user show {self.service_name} -p UnitFileState --value"  # noqa
         | 
| 231 | 
            +
                        )
         | 
| 232 | 
            +
                        output = (
         | 
| 233 | 
            +
                            subprocess.check_output(is_service_active.split(" ")).decode().strip()
         | 
| 234 | 
            +
                        )
         | 
| 235 | 
            +
                        return output == "enabled"
         | 
| 236 | 
            +
                    except subprocess.CalledProcessError as exception:
         | 
| 237 | 
            +
                        logger.error(f"Unable to check if {self.label} is enabled")
         | 
| 238 | 
            +
                        logger.error(exception)
         | 
| 239 | 
            +
                        return False
         | 
    
        primitive/daemons/ui.py
    ADDED
    
    | @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            from rich.console import Console
         | 
| 2 | 
            +
            from rich.table import Table
         | 
| 3 | 
            +
            from typing import List
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            from ..utils.daemons import Daemon
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
            def render_daemon_list(daemons: List[Daemon]) -> None:
         | 
| 9 | 
            +
                console = Console()
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                table = Table(show_header=True, header_style="bold #FFA800")
         | 
| 12 | 
            +
                table.add_column("Name")
         | 
| 13 | 
            +
                table.add_column("Label")
         | 
| 14 | 
            +
                table.add_column("Installed")
         | 
| 15 | 
            +
                table.add_column("Active")
         | 
| 16 | 
            +
                table.add_column("File Path")
         | 
| 17 | 
            +
                table.add_column("Log Path")
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                for daemon in daemons:
         | 
| 20 | 
            +
                    child_table = Table(show_header=False, header_style="bold #FFA800")
         | 
| 21 | 
            +
                    child_table.add_column("Name")
         | 
| 22 | 
            +
                    child_table.add_column("Label")
         | 
| 23 | 
            +
                    child_table.add_column("Installed")
         | 
| 24 | 
            +
                    child_table.add_column("Active")
         | 
| 25 | 
            +
                    child_table.add_column("File Path")
         | 
| 26 | 
            +
                    child_table.add_column("Log Path")
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    table.add_row(
         | 
| 29 | 
            +
                        daemon.name,
         | 
| 30 | 
            +
                        daemon.label,
         | 
| 31 | 
            +
                        "[bold green]Yes[/bold green]"
         | 
| 32 | 
            +
                        if daemon.is_installed()
         | 
| 33 | 
            +
                        else "[bold red]No[/bold red]",
         | 
| 34 | 
            +
                        "[bold green]Yes[/bold green]"
         | 
| 35 | 
            +
                        if daemon.is_active()
         | 
| 36 | 
            +
                        else "[bold red]No[/bold red]",
         | 
| 37 | 
            +
                        str(daemon.file_path),
         | 
| 38 | 
            +
                        str(daemon.logs),
         | 
| 39 | 
            +
                    )
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                console.print(table)
         | 
    
        primitive/db/base.py
    ADDED
    
    
    
        primitive/db/models.py
    ADDED
    
    | @@ -0,0 +1,88 @@ | |
| 1 | 
            +
            from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from sqlalchemy import Column, Integer, String
         | 
| 4 | 
            +
            from sqlalchemy.orm import Mapped, Query, mapped_column
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            from .base import Base
         | 
| 7 | 
            +
            from .sqlite import Session
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            T = TypeVar("T", bound="Base")
         | 
| 10 | 
            +
             | 
| 11 | 
            +
             | 
| 12 | 
            +
            class Manager(Generic[T]):
         | 
| 13 | 
            +
                def __init__(self, model_cls_lambda: Callable[[], Type[T]]) -> None:
         | 
| 14 | 
            +
                    self.model_cls_lambda = model_cls_lambda
         | 
| 15 | 
            +
                    self.filters: Dict[str, Any] = {}
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def create(self, **kwargs) -> T:
         | 
| 18 | 
            +
                    with Session() as session:
         | 
| 19 | 
            +
                        model = self.model_cls_lambda()
         | 
| 20 | 
            +
                        obj = model(**kwargs)
         | 
| 21 | 
            +
                        session.add(obj)
         | 
| 22 | 
            +
                        session.commit()
         | 
| 23 | 
            +
                        session.refresh(obj)
         | 
| 24 | 
            +
                        return obj
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def filter_by(self, **kwargs) -> "Manager[T]":
         | 
| 27 | 
            +
                    self.filters = kwargs
         | 
| 28 | 
            +
                    return self
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def exists(self) -> bool:
         | 
| 31 | 
            +
                    with Session() as session:
         | 
| 32 | 
            +
                        model = self.model_cls_lambda()
         | 
| 33 | 
            +
                        query = session.query(model)
         | 
| 34 | 
            +
                        query.filter_by(**self.filters)
         | 
| 35 | 
            +
                        self.filters.clear()
         | 
| 36 | 
            +
                        return query.count() > 0
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def all(self) -> List[T]:
         | 
| 39 | 
            +
                    with Session() as session:
         | 
| 40 | 
            +
                        model = self.model_cls_lambda()
         | 
| 41 | 
            +
                        query = session.query(model)
         | 
| 42 | 
            +
                        query.filter_by(**self.filters)
         | 
| 43 | 
            +
                        self.filters.clear()
         | 
| 44 | 
            +
                        return query.all()
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def first(self) -> Union[T, None]:
         | 
| 47 | 
            +
                    with Session() as session:
         | 
| 48 | 
            +
                        model = self.model_cls_lambda()
         | 
| 49 | 
            +
                        query = session.query(model)
         | 
| 50 | 
            +
                        query.filter_by(**self.filters)
         | 
| 51 | 
            +
                        self.filters.clear()
         | 
| 52 | 
            +
                        return query.first()
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def update(self, update: Dict[Any, Any]) -> Query[T]:
         | 
| 55 | 
            +
                    with Session() as session:
         | 
| 56 | 
            +
                        model = self.model_cls_lambda()
         | 
| 57 | 
            +
                        query = session.query(model).filter_by(**self.filters)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        if query.count() > 0:
         | 
| 60 | 
            +
                            query.update(update)
         | 
| 61 | 
            +
                            session.commit()
         | 
| 62 | 
            +
                            return query
         | 
| 63 | 
            +
                        else:
         | 
| 64 | 
            +
                            raise ValueError(f"Update failed, {model.__name__} not found")
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def delete(self) -> None:
         | 
| 67 | 
            +
                    with Session() as session:
         | 
| 68 | 
            +
                        model = self.model_cls_lambda()
         | 
| 69 | 
            +
                        query = session.query(model).filter_by(**self.filters)
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                        if query.count() > 0:
         | 
| 72 | 
            +
                            query.delete()
         | 
| 73 | 
            +
                            session.commit()
         | 
| 74 | 
            +
                        else:
         | 
| 75 | 
            +
                            raise ValueError(f"Delete failed, {model.__name__} not found")
         | 
| 76 | 
            +
             | 
| 77 | 
            +
             | 
| 78 | 
            +
            class JobRun(Base):
         | 
| 79 | 
            +
                __tablename__ = "JobRun"
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                id = Column(Integer, primary_key=True)
         | 
| 82 | 
            +
                job_run_id: Mapped[str] = mapped_column(String, nullable=False)
         | 
| 83 | 
            +
                pid: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                objects: Manager["JobRun"] = Manager(lambda: JobRun)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def __repr__(self):
         | 
| 88 | 
            +
                    return f"<JobRun(id={self.id} job_run_id={self.job_run_id}, pid={self.pid})>"
         | 
    
        primitive/db/sqlite.py
    ADDED
    
    | @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            from pathlib import Path
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from loguru import logger
         | 
| 4 | 
            +
            from sqlalchemy import Engine, create_engine
         | 
| 5 | 
            +
            from sqlalchemy.orm import Session as SQLAlchemySession
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            from ..utils.cache import get_cache_dir
         | 
| 8 | 
            +
            from .base import Base
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
            def init() -> None:
         | 
| 12 | 
            +
                db_path: Path = get_cache_dir() / "primitive.sqlite3"
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                # Drop DB existing database if it exists
         | 
| 15 | 
            +
                # if db_path.exists():
         | 
| 16 | 
            +
                #     logger.warning(f"[*] Deleting existing SQLite database at {db_path}")
         | 
| 17 | 
            +
                #     db_path.unlink()
         | 
| 18 | 
            +
                if db_path.exists():
         | 
| 19 | 
            +
                    return
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                logger.info(f"[*] Initializing SQLite database at {db_path}")
         | 
| 22 | 
            +
                engine = create_engine(f"sqlite:///{db_path}", echo=False)
         | 
| 23 | 
            +
                Base.metadata.create_all(engine)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            def engine() -> Engine:
         | 
| 27 | 
            +
                db_path: Path = get_cache_dir() / "primitive.sqlite3"
         | 
| 28 | 
            +
                return create_engine(f"sqlite:///{db_path}", echo=False)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 31 | 
            +
            def Session() -> SQLAlchemySession:
         | 
| 32 | 
            +
                from sqlalchemy.orm import sessionmaker
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                return sessionmaker(bind=engine())()
         | 
    
        primitive/exec/actions.py
    CHANGED
    
    | @@ -58,7 +58,6 @@ class Exec(BaseAction): | |
| 58 58 |  | 
| 59 59 | 
             
                    reservation = reservation_result.data["reservation"]
         | 
| 60 60 | 
             
                    if reservation.get("status") != "in_progress":
         | 
| 61 | 
            -
                        logger.enable("primitive")
         | 
| 62 61 | 
             
                        logger.info(
         | 
| 63 62 | 
             
                            f"Reservation {reservation.get('id')} is in status {reservation.get('status')}, cannot execute command at this time."
         | 
| 64 63 | 
             
                        )
         | 
    
        primitive/files/actions.py
    CHANGED
    
    
    
        primitive/hardware/actions.py
    CHANGED
    
    | @@ -293,7 +293,6 @@ class Hardware(BaseAction): | |
| 293 293 | 
             
                    )
         | 
| 294 294 | 
             
                    if messages := result.data.get("registerHardware").get("messages"):
         | 
| 295 295 | 
             
                        for message in messages:
         | 
| 296 | 
            -
                            logger.enable("primitive")
         | 
| 297 296 | 
             
                            if message.get("kind") == "ERROR":
         | 
| 298 297 | 
             
                                logger.error(message.get("message"))
         | 
| 299 298 | 
             
                            else:
         | 
| @@ -327,7 +326,6 @@ class Hardware(BaseAction): | |
| 327 326 |  | 
| 328 327 | 
             
                    if messages := result.data.get("unregisterHardware").get("messages"):
         | 
| 329 328 | 
             
                        for message in messages:
         | 
| 330 | 
            -
                            logger.enable("primitive")
         | 
| 331 329 | 
             
                            if message.get("kind") == "ERROR":
         | 
| 332 330 | 
             
                                logger.error(message.get("message"))
         | 
| 333 331 | 
             
                            else:
         | 
| @@ -368,11 +366,11 @@ class Hardware(BaseAction): | |
| 368 366 | 
             
                            mutation, variable_values=variables, get_execution_result=True
         | 
| 369 367 | 
             
                        )
         | 
| 370 368 | 
             
                    except client_exceptions.ClientConnectorError as exception:
         | 
| 371 | 
            -
                        message = " | 
| 369 | 
            +
                        message = "[*] Failed to update hardware system info! "
         | 
| 372 370 | 
             
                        logger.exception(message)
         | 
| 373 371 | 
             
                        raise exception
         | 
| 374 372 |  | 
| 375 | 
            -
                    message = " | 
| 373 | 
            +
                    message = "[*] Updated hardware system info successfully! "
         | 
| 376 374 | 
             
                    logger.info(message)
         | 
| 377 375 |  | 
| 378 376 | 
             
                    return result
         | 
| @@ -416,7 +414,6 @@ class Hardware(BaseAction): | |
| 416 414 | 
             
                        checkin_success = result.data.get("checkIn").get("lastCheckIn")
         | 
| 417 415 | 
             
                        if messages := result.data.get("checkIn").get("messages"):
         | 
| 418 416 | 
             
                            for message in messages:
         | 
| 419 | 
            -
                                logger.enable("primitive")
         | 
| 420 417 | 
             
                                if message.get("kind") == "ERROR":
         | 
| 421 418 | 
             
                                    logger.error(message.get("message"))
         | 
| 422 419 | 
             
                                else:
         | 
| @@ -435,7 +432,7 @@ class Hardware(BaseAction): | |
| 435 432 | 
             
                                }
         | 
| 436 433 | 
             
                            self.status_cache[fingerprint] = new_state.copy()
         | 
| 437 434 |  | 
| 438 | 
            -
                            message = f" | 
| 435 | 
            +
                            message = f"[*] Checked in successfully for {fingerprint}: "
         | 
| 439 436 | 
             
                            is_new_status = False
         | 
| 440 437 | 
             
                            for key, value in new_state.items():
         | 
| 441 438 | 
             
                                if value != previous_status.get(key, None):
         | 
| @@ -463,7 +460,7 @@ class Hardware(BaseAction): | |
| 463 460 | 
             
                        return result
         | 
| 464 461 | 
             
                    except client_exceptions.ClientConnectorError as exception:
         | 
| 465 462 | 
             
                        if not stopping_agent:
         | 
| 466 | 
            -
                            message = " | 
| 463 | 
            +
                            message = "[*] Failed to check in! "
         | 
| 467 464 | 
             
                            logger.error(message)
         | 
| 468 465 | 
             
                            raise exception
         | 
| 469 466 | 
             
                        else:
         | 
| @@ -578,7 +575,6 @@ class Hardware(BaseAction): | |
| 578 575 |  | 
| 579 576 | 
             
                    if messages := result.data.get("registerChildHardware").get("messages"):
         | 
| 580 577 | 
             
                        for message in messages:
         | 
| 581 | 
            -
                            logger.enable("primitive")
         | 
| 582 578 | 
             
                            if message.get("kind") == "ERROR":
         | 
| 583 579 | 
             
                                logger.error(message.get("message"))
         | 
| 584 580 | 
             
                            else:
         | 
| @@ -596,12 +592,17 @@ class Hardware(BaseAction): | |
| 596 592 | 
             
                    pass
         | 
| 597 593 |  | 
| 598 594 | 
             
                @guard
         | 
| 599 | 
            -
                def _sync_children(self):
         | 
| 595 | 
            +
                def _sync_children(self, hardware: Optional[Dict[str, str]] = None):
         | 
| 600 596 | 
             
                    # get the existing children if any from the hardware details
         | 
| 601 597 | 
             
                    # get the latest children from the node
         | 
| 602 598 | 
             
                    # compare the two and update the node with the latest children
         | 
| 603 599 | 
             
                    # remove any children from remote that are not in the latest children
         | 
| 604 | 
            -
                     | 
| 600 | 
            +
                    if not hardware:
         | 
| 601 | 
            +
                        hardware = self.primitive.hardware.get_own_hardware_details()
         | 
| 602 | 
            +
                    if not hardware:
         | 
| 603 | 
            +
                        logger.error("No hardware found.")
         | 
| 604 | 
            +
                        return
         | 
| 605 | 
            +
             | 
| 605 606 | 
             
                    remote_children = hardware.get("children", [])
         | 
| 606 607 | 
             
                    local_children = self.primitive.hardware._list_local_children()
         | 
| 607 608 |  |