easyrunner-cli 0.0.1.dev86__tar.gz

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.
Files changed (133) hide show
  1. easyrunner_cli-0.0.1.dev86/PKG-INFO +50 -0
  2. easyrunner_cli-0.0.1.dev86/README.md +30 -0
  3. easyrunner_cli-0.0.1.dev86/easyrunner/cloud_providers/__init__.py +0 -0
  4. easyrunner_cli-0.0.1.dev86/easyrunner/cloud_providers/cloud_provider_base.py +194 -0
  5. easyrunner_cli-0.0.1.dev86/easyrunner/cloud_providers/cloud_providers.py +14 -0
  6. easyrunner_cli-0.0.1.dev86/easyrunner/cloud_providers/hetzner_provider.py +77 -0
  7. easyrunner_cli-0.0.1.dev86/easyrunner/command_executor.py +99 -0
  8. easyrunner_cli-0.0.1.dev86/easyrunner/command_executor_local.py +150 -0
  9. easyrunner_cli-0.0.1.dev86/easyrunner/commands/__init__.py +15 -0
  10. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/__init__.py +39 -0
  11. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/archive_commands.py +63 -0
  12. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/caddy_api_curl_commands.py +19 -0
  13. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/caddy_commands.py +19 -0
  14. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/command_base.py +41 -0
  15. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/curl_commands.py +19 -0
  16. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/dir_commands.py +52 -0
  17. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/docker_compose_commands.py +48 -0
  18. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/file_commands.py +282 -0
  19. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/git_commands.py +117 -0
  20. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/ip_tables_commands.py +161 -0
  21. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/ip_tables_persistent_commands.py +34 -0
  22. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/null_command.py +22 -0
  23. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/os_package_manager_commands.py +62 -0
  24. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/podman_commands.py +146 -0
  25. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/ssh_agent_commands.py +141 -0
  26. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/ssh_keygen_commands.py +60 -0
  27. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/systemctl_commands.py +146 -0
  28. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/user_commands.py +134 -0
  29. easyrunner_cli-0.0.1.dev86/easyrunner/commands/base/utility_commands.py +25 -0
  30. easyrunner_cli-0.0.1.dev86/easyrunner/commands/runnable_command_string.py +21 -0
  31. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/__init__.py +31 -0
  32. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/archive_commands_ubuntu.py +28 -0
  33. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/caddy_api_curl_commands_ubuntu.py +209 -0
  34. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/caddy_commands_container_ubuntu.py +63 -0
  35. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/curl_commands_ubuntu.py +369 -0
  36. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/dir_commands_ubuntu.py +21 -0
  37. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/docker_compose_commands_ubuntu.py +34 -0
  38. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/file_commands_ubuntu.py +11 -0
  39. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/git_commands_ubuntu.py +24 -0
  40. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/ip_tables_commands_ubuntu.py +9 -0
  41. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/ip_tables_persistent_commands_ubuntu.py +9 -0
  42. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/os_package_manager_commands_ubuntu.py +66 -0
  43. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/podman_commands_ubuntu.py +29 -0
  44. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/ssh_agent_commands_ubuntu.py +20 -0
  45. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/ssh_keygen_commands_ubuntu.py +31 -0
  46. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/systemctl_commands_ubuntu.py +17 -0
  47. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/user_commands_ubuntu.py +20 -0
  48. easyrunner_cli-0.0.1.dev86/easyrunner/commands/ubuntu/utility_commands_ubuntu.py +20 -0
  49. easyrunner_cli-0.0.1.dev86/easyrunner/format_utils.py +15 -0
  50. easyrunner_cli-0.0.1.dev86/easyrunner/http_client.py +185 -0
  51. easyrunner_cli-0.0.1.dev86/easyrunner/known_host_ssh_keys.py +21 -0
  52. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/__init__.py +28 -0
  53. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/cloud_firewall_base.py +11 -0
  54. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/cloud_resource_api_base.py +10 -0
  55. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/cloud_resource_pulumi_base.py +16 -0
  56. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/cloud_virtual_machine_base.py +20 -0
  57. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/github/github_api_client.py +135 -0
  58. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/github/github_api_client_dtos.py +76 -0
  59. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/github/github_repo.py +145 -0
  60. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/hetzner/__init__.py +9 -0
  61. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/hetzner/hetzner_firewall.py +82 -0
  62. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/hetzner/hetzner_firewall_rule.py +14 -0
  63. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/hetzner/hetzner_resource_factory.py +76 -0
  64. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/hetzner/hetzner_stack.py +360 -0
  65. easyrunner_cli-0.0.1.dev86/easyrunner/resources/cloud_resources/hetzner/hetzner_virtual_machine.py +72 -0
  66. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/__init__.py +25 -0
  67. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/caddy.py +349 -0
  68. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/directory.py +232 -0
  69. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/docker_compose.py +18 -0
  70. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/file.py +203 -0
  71. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/git_repo.py +231 -0
  72. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/host_server_ubuntu.py +3448 -0
  73. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/ip_tables.py +260 -0
  74. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/os_package_manager.py +59 -0
  75. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/os_resource_base.py +20 -0
  76. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/podman.py +235 -0
  77. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/podman_network.py +64 -0
  78. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/ssh_agent.py +723 -0
  79. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/systemd_service.py +112 -0
  80. easyrunner_cli-0.0.1.dev86/easyrunner/resources/os_resources/user.py +269 -0
  81. easyrunner_cli-0.0.1.dev86/easyrunner/resources/resource_base.py +5 -0
  82. easyrunner_cli-0.0.1.dev86/easyrunner/resources/web_security_scanner.py +954 -0
  83. easyrunner_cli-0.0.1.dev86/easyrunner/ssh.py +284 -0
  84. easyrunner_cli-0.0.1.dev86/easyrunner/ssh_key.py +340 -0
  85. easyrunner_cli-0.0.1.dev86/easyrunner/store/__init__.py +11 -0
  86. easyrunner_cli-0.0.1.dev86/easyrunner/store/data_models/__init__.py +4 -0
  87. easyrunner_cli-0.0.1.dev86/easyrunner/store/data_models/app.py +37 -0
  88. easyrunner_cli-0.0.1.dev86/easyrunner/store/data_models/database_dto_base.py +27 -0
  89. easyrunner_cli-0.0.1.dev86/easyrunner/store/data_models/server.py +37 -0
  90. easyrunner_cli-0.0.1.dev86/easyrunner/store/db_config.py +25 -0
  91. easyrunner_cli-0.0.1.dev86/easyrunner/store/db_ctx.py +94 -0
  92. easyrunner_cli-0.0.1.dev86/easyrunner/store/easyrunner_store.py +90 -0
  93. easyrunner_cli-0.0.1.dev86/easyrunner/store/json_encoder.py +16 -0
  94. easyrunner_cli-0.0.1.dev86/easyrunner/store/object_id.py +48 -0
  95. easyrunner_cli-0.0.1.dev86/easyrunner/store/uuid7.py +26 -0
  96. easyrunner_cli-0.0.1.dev86/easyrunner/tool_paths.py +50 -0
  97. easyrunner_cli-0.0.1.dev86/easyrunner/types/__init__.py +23 -0
  98. easyrunner_cli-0.0.1.dev86/easyrunner/types/caddy/caddy_config.py +19 -0
  99. easyrunner_cli-0.0.1.dev86/easyrunner/types/caddy/caddy_site.py +16 -0
  100. easyrunner_cli-0.0.1.dev86/easyrunner/types/compose_project/__init__.py +12 -0
  101. easyrunner_cli-0.0.1.dev86/easyrunner/types/compose_project/compose_network.py +36 -0
  102. easyrunner_cli-0.0.1.dev86/easyrunner/types/compose_project/compose_project.py +128 -0
  103. easyrunner_cli-0.0.1.dev86/easyrunner/types/compose_project/compose_service.py +46 -0
  104. easyrunner_cli-0.0.1.dev86/easyrunner/types/compose_project/compose_volume.py +22 -0
  105. easyrunner_cli-0.0.1.dev86/easyrunner/types/cpu_arch_types.py +7 -0
  106. easyrunner_cli-0.0.1.dev86/easyrunner/types/dir_info.py +38 -0
  107. easyrunner_cli-0.0.1.dev86/easyrunner/types/dto_base.py +49 -0
  108. easyrunner_cli-0.0.1.dev86/easyrunner/types/exec_result.py +57 -0
  109. easyrunner_cli-0.0.1.dev86/easyrunner/types/file_info.py +60 -0
  110. easyrunner_cli-0.0.1.dev86/easyrunner/types/json.py +80 -0
  111. easyrunner_cli-0.0.1.dev86/easyrunner/types/jsonobject_to_dataclass.py +90 -0
  112. easyrunner_cli-0.0.1.dev86/easyrunner/types/os_type.py +8 -0
  113. easyrunner_cli-0.0.1.dev86/easyrunner/types/podman_network_driver.py +8 -0
  114. easyrunner_cli-0.0.1.dev86/easyrunner/types/security_scan_result.py +54 -0
  115. easyrunner_cli-0.0.1.dev86/easyrunner/types/ssh_key_type.py +7 -0
  116. easyrunner_cli-0.0.1.dev86/easyrunner/types/vm_config.py +28 -0
  117. easyrunner_cli-0.0.1.dev86/pyproject.toml +67 -0
  118. easyrunner_cli-0.0.1.dev86/source/__init__.py +19 -0
  119. easyrunner_cli-0.0.1.dev86/source/app_sub_command.py +557 -0
  120. easyrunner_cli-0.0.1.dev86/source/auth/__init__.py +12 -0
  121. easyrunner_cli-0.0.1.dev86/source/auth/auth_sub_command.py +226 -0
  122. easyrunner_cli-0.0.1.dev86/source/auth/github_oauth_config.py +24 -0
  123. easyrunner_cli-0.0.1.dev86/source/auth/github_oauth_flow.py +165 -0
  124. easyrunner_cli-0.0.1.dev86/source/auth/github_token_manager.py +226 -0
  125. easyrunner_cli-0.0.1.dev86/source/auth/oauth_callback_server.py +109 -0
  126. easyrunner_cli-0.0.1.dev86/source/auth_sub_command.py +226 -0
  127. easyrunner_cli-0.0.1.dev86/source/infrastructure_deps.py +107 -0
  128. easyrunner_cli-0.0.1.dev86/source/license_sub_command.py +198 -0
  129. easyrunner_cli-0.0.1.dev86/source/licensing/__init__.py +5 -0
  130. easyrunner_cli-0.0.1.dev86/source/licensing/license_manager.py +207 -0
  131. easyrunner_cli-0.0.1.dev86/source/main.py +101 -0
  132. easyrunner_cli-0.0.1.dev86/source/servers_sub_command.py +1515 -0
  133. easyrunner_cli-0.0.1.dev86/source/ssh_config.py +76 -0
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyrunner_cli
3
+ Version: 0.0.1.dev86
4
+ Summary: EasyRunner CLI.
5
+ Author: Janaka Abeywardhana
6
+ Author-email: contact@janaka.co.uk
7
+ Requires-Python: >=3.13,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Dist: cryptography (>=44.0.0,<45.0.0)
12
+ Requires-Dist: keyring (>=25.6.0,<26.0.0)
13
+ Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
14
+ Requires-Dist: pyobjc-framework-Security (>=10.3.1,<11.0.0) ; sys_platform == "darwin"
15
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
16
+ Requires-Dist: typer (>=0.15.1,<0.16.0)
17
+ Project-URL: Homepage, https://github.com/janaka/easyrunner
18
+ Project-URL: Repository, https://github.com/janaka/easyrunner
19
+ Description-Content-Type: text/markdown
20
+
21
+ # EasyRunner CLI
22
+
23
+ Application hosting platform that run on a single server.
24
+
25
+ Copyright (c) 2024 - 2025 Janaka Abeywardhana
26
+
27
+ ## Contribution
28
+
29
+ Setup python tools on a new machine
30
+
31
+ - `brew install pyenv` - python virtual environment manager
32
+ - `brew install pipx` - pipx python package manager, for install poetry
33
+ - `pipx install poetry` (pipx installs global packages in isolated environments)
34
+ - add `export PATH="$HOME/.local/bin:$PATH"` to ~/.zshrc for poetry.
35
+
36
+
37
+ Setup python environment for an application
38
+
39
+ - `pyenv install 3.13` install this version of python.
40
+ - `pyenv local` show the version in this environment
41
+ - `poetry env use $(pyenv which python)` to create a poetry environment in this project for dependencies. the `.venv`
42
+ - `source $(poetry env info --path)/bin/activate` to activate the environment (avail on path etc.)
43
+ - `poetry config virtualenvs.in-project true`
44
+ - `poetry install`
45
+
46
+ if the location of the repo changes on your local machine then the virtual env will get disconnected. Therefore remove and recreate
47
+
48
+ - `poetry env remove $(poetry env list --full-path | grep -Eo '/.*')` Remove the current Poetry environment to force a clean rebuild:
49
+ - `poetry install` Recreate the environment and install dependencies
50
+ - `source $(poetry env info --path)/bin/activate` activate the environment
@@ -0,0 +1,30 @@
1
+ # EasyRunner CLI
2
+
3
+ Application hosting platform that run on a single server.
4
+
5
+ Copyright (c) 2024 - 2025 Janaka Abeywardhana
6
+
7
+ ## Contribution
8
+
9
+ Setup python tools on a new machine
10
+
11
+ - `brew install pyenv` - python virtual environment manager
12
+ - `brew install pipx` - pipx python package manager, for install poetry
13
+ - `pipx install poetry` (pipx installs global packages in isolated environments)
14
+ - add `export PATH="$HOME/.local/bin:$PATH"` to ~/.zshrc for poetry.
15
+
16
+
17
+ Setup python environment for an application
18
+
19
+ - `pyenv install 3.13` install this version of python.
20
+ - `pyenv local` show the version in this environment
21
+ - `poetry env use $(pyenv which python)` to create a poetry environment in this project for dependencies. the `.venv`
22
+ - `source $(poetry env info --path)/bin/activate` to activate the environment (avail on path etc.)
23
+ - `poetry config virtualenvs.in-project true`
24
+ - `poetry install`
25
+
26
+ if the location of the repo changes on your local machine then the virtual env will get disconnected. Therefore remove and recreate
27
+
28
+ - `poetry env remove $(poetry env list --full-path | grep -Eo '/.*')` Remove the current Poetry environment to force a clean rebuild:
29
+ - `poetry install` Recreate the environment and install dependencies
30
+ - `source $(poetry env info --path)/bin/activate` activate the environment
@@ -0,0 +1,194 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Optional
4
+
5
+ import pulumi.automation as auto
6
+
7
+ from ..tool_paths import EasyRunnerPaths
8
+ from ..types.exec_result import ExecResult
9
+
10
+
11
+ class CloudProviderBase(ABC):
12
+ """Base class for cloud provider implementations."""
13
+
14
+ def __init__(
15
+ self, api_key: str, region: str, project_name: Optional[str] = None
16
+ ) -> None:
17
+ self.api_key = api_key
18
+ self.region = region
19
+ self.project_name = project_name or f"easyrunner-{self.name()}"
20
+
21
+ @abstractmethod
22
+ def _create_provider(self) -> Any:
23
+ """Create the cloud provider instance."""
24
+ pass
25
+
26
+ def _get_pulumi_command(self) -> str:
27
+ """Get the Pulumi root directory for PulumiCommand, which will append /bin/pulumi."""
28
+ return str(EasyRunnerPaths.get_pulumi_root())
29
+
30
+ def _create_workspace_options(
31
+ self, backend_url: Optional[str] = None
32
+ ) -> auto.LocalWorkspaceOptions:
33
+ """Create workspace options with custom Pulumi command and optional backend."""
34
+ pulumi_command_path = self._get_pulumi_command()
35
+
36
+ # Set default backend to EasyRunner's local backend if not provided
37
+ if backend_url is None or backend_url == "":
38
+ backend_url = f"file://{EasyRunnerPaths.get_pulumi_local_backend_dir()}"
39
+
40
+ # Set Pulumi config passphrase if not already set
41
+ if "PULUMI_CONFIG_PASSPHRASE" not in os.environ:
42
+ os.environ["PULUMI_CONFIG_PASSPHRASE"] = (
43
+ "klshdf324£$@::D£$mcmoWERXakhk234%basdmnbqqvffadqWEQAO[]£N!#sdff"
44
+ )
45
+
46
+ return auto.LocalWorkspaceOptions(
47
+ pulumi_command=auto.PulumiCommand(pulumi_command_path),
48
+ pulumi_home=str(
49
+ EasyRunnerPaths.get_pulumi_root()
50
+ ), # Control where all Pulumi data is stored
51
+ secrets_provider="passphrase",
52
+ work_dir=str(
53
+ EasyRunnerPaths.get_pulumi_local_backend_dir()
54
+ ), # Set workspace directory
55
+ project_settings=auto.ProjectSettings(
56
+ name=self.project_name,
57
+ runtime="python",
58
+ backend=auto.ProjectBackend(url=backend_url),
59
+ ),
60
+ )
61
+
62
+ def _ensure_cloud_tools_available(self) -> ExecResult:
63
+ """Ensure cloud tools are available before executing Pulumi operations."""
64
+ from easyrunner_cli.source.infrastructure_deps import InfrastructureDependencies
65
+
66
+ return InfrastructureDependencies.ensure_cloud_tools_available()
67
+
68
+ def get_provider_instance(self) -> Any:
69
+ """Get the provider instance for programmatic use."""
70
+
71
+ return self._create_provider()
72
+
73
+ def _convert_pulumi_outputs_to_dict(
74
+ self, outputs: auto.OutputMap
75
+ ) -> dict[str, Any]:
76
+ """Convert Pulumi OutputMap to standard dictionary with extracted values.
77
+
78
+ Args:
79
+ outputs: Pulumi OutputMap from stack.outputs()
80
+
81
+ Returns:
82
+ Dictionary with extracted values from Pulumi OutputValue objects
83
+ """
84
+ result: dict[str, Any] = {}
85
+ for key, output_value in outputs.items():
86
+ # Pulumi automation API returns OutputValue objects with a 'value' field
87
+ result[key] = (
88
+ output_value.value if hasattr(output_value, "value") else output_value
89
+ )
90
+ return result
91
+
92
+ @abstractmethod
93
+ def name(self) -> str:
94
+ """Get the name of the cloud provider."""
95
+ pass
96
+
97
+ # def execute_pulumi_program(
98
+ # self,
99
+ # program: Callable[[], None],
100
+ # stack_name: str,
101
+ # backend_url: Optional[str] = None,
102
+ # ) -> ExecResult:
103
+ # """Execute a Pulumi program using EasyRunner's Pulumi installation.
104
+
105
+ # Args:
106
+ # program: The Pulumi program function to execute
107
+ # stack_name: Name of the stack
108
+ # backend_url: Optional backend URL (e.g., s3://bucket-name for S3 backend)
109
+ # Returns:
110
+ # ExecResult[dict[str, Any]]: The result of the Pulumi program execution stack.outputs as a dictionary.
111
+ # """
112
+ # # Ensure cloud tools are available before executing
113
+ # deps_result = self._ensure_cloud_tools_available()
114
+ # if not deps_result.success:
115
+ # return deps_result
116
+
117
+ # try:
118
+ # # Create or select stack with custom workspace options
119
+ # stack = auto.create_or_select_stack(
120
+ # stack_name=stack_name,
121
+ # project_name=self.project_name,
122
+ # program=program,
123
+ # opts=self._create_workspace_options(backend_url=backend_url),
124
+ # )
125
+
126
+ # # Deploy
127
+ # stack.up()
128
+
129
+ # result = ExecResult[dict[str, Any]](
130
+ # success=True,
131
+ # return_code=0,
132
+ # stdout="Stack deployed successfully",
133
+ # stderr=None,
134
+ # )
135
+
136
+ # # Convert Pulumi OutputMap to standard dict
137
+ # result.result = self._convert_pulumi_outputs_to_dict(stack.outputs())
138
+
139
+ # return result
140
+
141
+ # except Exception as e:
142
+ # return ExecResult(success=False, return_code=1, stdout=None, stderr=str(e))
143
+
144
+ # def destroy_pulumi_stack(
145
+ # self, stack_name: str, backend_url: Optional[str] = None
146
+ # ) -> ExecResult:
147
+ # """Destroy a Pulumi stack using EasyRunner's Pulumi installation."""
148
+ # try:
149
+
150
+ # def dummy_program() -> None:
151
+ # """Empty program required for stack selection."""
152
+ # pass
153
+
154
+ # stack = auto.select_stack(
155
+ # stack_name=stack_name,
156
+ # project_name=self.project_name,
157
+ # program=dummy_program,
158
+ # opts=self._create_workspace_options(backend_url=backend_url),
159
+ # )
160
+
161
+ # stack.destroy()
162
+
163
+ # return ExecResult(
164
+ # success=True,
165
+ # return_code=0,
166
+ # stdout=f"Stack {stack_name} destroyed",
167
+ # stderr=None,
168
+ # )
169
+
170
+ # except Exception as e:
171
+ # return ExecResult(success=False, return_code=1, stdout=None, stderr=str(e))
172
+
173
+ def get_stack_outputs(
174
+ self, stack_name: str, backend_url: Optional[str] = None
175
+ ) -> Optional[dict[str, Any]]:
176
+ """Get outputs from a Pulumi stack using EasyRunner's Pulumi installation."""
177
+ try:
178
+ stack = auto.select_stack(
179
+ stack_name=stack_name,
180
+ project_name=self.project_name,
181
+ opts=self._create_workspace_options(backend_url=backend_url),
182
+ )
183
+
184
+ outputs = stack.outputs()
185
+ result = self._convert_pulumi_outputs_to_dict(outputs)
186
+ return result if result else None
187
+
188
+ except Exception:
189
+ return None
190
+
191
+ # @abstractmethod
192
+ # def create_select_state_backend_bucket(self) -> ExecResult:
193
+ # """Setup the Pulumi state backend."""
194
+ # pass
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CloudProviders(str, Enum):
5
+ """Supported cloud providers for automated server creation."""
6
+ #AZURE = "azure"
7
+ #AWS = "aws"
8
+ #DIGITAL_OCEAN = "digitalocean"
9
+ #LINODE = "linode"
10
+ #VULTR = "vultr"
11
+ HETZNER = "hetzner"
12
+ #GCP = "gcp"
13
+ #OVH = "ovh"
14
+
@@ -0,0 +1,77 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ from .cloud_provider_base import CloudProviderBase
5
+ from .cloud_providers import CloudProviders
6
+
7
+ if TYPE_CHECKING:
8
+ import pulumi_hcloud
9
+
10
+
11
+ class HetznerProvider(CloudProviderBase):
12
+ """Hetzner cloud provider implementation using Pulumi."""
13
+
14
+ def __init__(self, api_key: str, region: str = "nbg1"):
15
+ super().__init__(api_key, region)
16
+ # Store the API key for programmatic provider configuration
17
+ self._hetzner_token = api_key
18
+
19
+ def _create_provider(self) -> "pulumi_hcloud.Provider":
20
+ """Create Hetzner provider with explicit token configuration."""
21
+ import pulumi_hcloud as hetzner
22
+
23
+ return hetzner.Provider("hetzner-provider", token=self._hetzner_token)
24
+
25
+ def name(self) -> str:
26
+ """Get the name of the cloud provider."""
27
+ return CloudProviders.HETZNER.value
28
+
29
+ @classmethod
30
+ def from_env(cls, region: str = "nbg1") -> "HetznerProvider":
31
+ """Create HetznerProvider instance from HETZNER_API_KEY environment variable."""
32
+ api_key = os.getenv("HETZNER_API_KEY")
33
+ if not api_key:
34
+ raise ValueError("HETZNER_API_KEY environment variable is required")
35
+ return cls(api_key=api_key, region=region)
36
+
37
+ # def create_select_state_backend_bucket(self) -> ExecResult:
38
+ # """Setup Pulumi state backend - local for initial setup, S3 for production.
39
+
40
+ # Retrurns:
41
+ # ExecResult[dict[str, Any]]
42
+
43
+ # results keys:
44
+ # - bucket_name: Name of the created S3 bucket for state storage.
45
+ # """
46
+
47
+ # # Create S3 compatible object storage backend using the base class execute_pulumi_program method
48
+ # def create_state_backend_object_store() -> None:
49
+ # """there currently no pulumi object storage support for Hetzner as it's a new service."""
50
+ # pass
51
+
52
+ # # Execute the program using local backend (for creating the S3 bucket)
53
+ # result: ExecResult[dict[str, Any]] = self.execute_pulumi_program(
54
+ # program=create_state_backend_object_store,
55
+ # stack_name="state-backend",
56
+ # backend_url=None, # Use local backend to create the S3 bucket
57
+ # )
58
+
59
+ # if not result.success:
60
+ # return result
61
+
62
+ # # Get the bucket name from outputs
63
+ # outputs = self.get_stack_outputs(
64
+ # stack_name="state-backend",
65
+ # backend_url=None,
66
+ # )
67
+
68
+ # bucket_name = ""
69
+ # if outputs and "state_bucket_name" in outputs:
70
+ # bucket_name = outputs["state_bucket_name"]
71
+
72
+ # return ExecResult(
73
+ # success=True,
74
+ # return_code=0,
75
+ # stdout=f"S3 state bucket created: {bucket_name}. Use backend URL: s3://{bucket_name}",
76
+ # stderr=None,
77
+ # )
@@ -0,0 +1,99 @@
1
+ import logging
2
+ from io import BytesIO
3
+ from pathlib import Path
4
+
5
+ from fabric import Result as InvokeResult
6
+ from fabric.transfer import Result as TransferResult
7
+
8
+ from .commands.runnable_command_string import RunnableCommandString
9
+ from .ssh import Ssh
10
+ from .types.exec_result import ExecResult
11
+
12
+
13
+ class CommandExecutor:
14
+ """This class is responsible for executing commands on the remote machine"""
15
+ def __init__(self, ssh_client: Ssh):
16
+ # setup logger for this class with correct logger namespace hierarchy
17
+ self._logger: logging.Logger = logging.getLogger(__name__)
18
+ # Critical for libs to prevent log messages from propagating to the root logger and causing dup logs and config issues.
19
+ self._logger.addHandler(logging.NullHandler())
20
+
21
+ self.ssh_client: Ssh = ssh_client
22
+
23
+ def execute(self, command: RunnableCommandString) -> ExecResult:
24
+ """
25
+ Execute a command on the remote host.
26
+
27
+ Args:
28
+ command: Command to execute
29
+ """
30
+
31
+ if command.output_to_file is not None:
32
+ redirect_operator = ">>" if command.append_or_overwrite == "APPEND" else ">"
33
+ command = RunnableCommandString(
34
+ command=f"{command.command} {redirect_operator} {command.output_to_file}"
35
+ )
36
+
37
+ with self.ssh_client as ssh:
38
+ result: InvokeResult = (
39
+ ssh.run_sudo(command=command)
40
+ if command.sudo
41
+ else ssh.run(command=command)
42
+ )
43
+ mapped_result = self._map_invoke_result(result, command=command)
44
+
45
+ self._logger.debug(
46
+ "Command Executed > '%r', On Remote Host: '%s:%s'\n\n",
47
+ mapped_result,
48
+ ssh.hostname,
49
+ ssh.port,
50
+ )
51
+ return mapped_result
52
+
53
+ def put_file(self, source: Path | str | BytesIO, remote_path: str) -> ExecResult:
54
+ """
55
+ Transfer a file from local machine where this method is executed to remote host.
56
+
57
+ See https://docs.fabfile.org/en/latest/api/transfer.html#fabric.transfer.Transfer.put
58
+ Args:
59
+ local_path: Path to local file or a file-like object as io.StringIO(content_str).
60
+ remote_path: Remote destination path
61
+ """
62
+ try:
63
+ with self.ssh_client as ssh:
64
+ self._logger.debug(
65
+ "Transferring file to remote host. Remote path: '%s'",
66
+ remote_path,
67
+ )
68
+ result: TransferResult = ssh.connection.put(
69
+ local=source, remote=remote_path, preserve_mode=False
70
+ )
71
+
72
+ return ExecResult(
73
+ stdout=f"Transferring file '{result.local}' to remote '{result.remote}' successfully",
74
+ return_code=0,
75
+ success=True,
76
+ )
77
+
78
+ except Exception as e:
79
+ return ExecResult(
80
+ stdout="",
81
+ stderr=str(e),
82
+ return_code=1,
83
+ success=False,
84
+ )
85
+
86
+ # raise RuntimeError(
87
+ # f"CommandExecutor.put_file() - Failed to transfer file to remote host: {str(e)}"
88
+ # )
89
+
90
+ def _map_invoke_result(
91
+ self, result: InvokeResult, command: RunnableCommandString
92
+ ) -> ExecResult:
93
+ return ExecResult(
94
+ stdout=result.stdout,
95
+ stderr=result.stderr,
96
+ return_code=result.return_code,
97
+ success=result.return_code == 0,
98
+ command=command,
99
+ )
@@ -0,0 +1,150 @@
1
+ import logging
2
+ from io import BytesIO
3
+ from pathlib import Path
4
+
5
+ from invoke import run
6
+ from invoke.runners import Result as InvokeResult
7
+
8
+ from .commands.runnable_command_string import RunnableCommandString
9
+ from .types.exec_result import ExecResult
10
+
11
+
12
+ class CommandExecutorLocal:
13
+ """ This class is responsible for executing commands on the local machine"""
14
+ def __init__(self, debug: bool = False, silent: bool = True):
15
+ # setup logger for this class with correct logger namespace hierarchy
16
+ self._logger: logging.Logger = logging.getLogger(__name__)
17
+ # Critical for libs to prevent log messages from propagating to the root logger and causing dup logs and config issues.
18
+ self._logger.addHandler(logging.NullHandler())
19
+
20
+ self.debug: bool = debug
21
+ self.silent: bool = silent
22
+
23
+ # Configure hide behavior like your SSH class
24
+ if self.debug:
25
+ self._hide: bool | str = False
26
+ elif self.silent:
27
+ self._hide = "both"
28
+ else:
29
+ # self._hide = False
30
+ self._hide = "both"
31
+
32
+ def execute(self, command: RunnableCommandString) -> ExecResult:
33
+ """
34
+ Execute a command on the local machine.
35
+
36
+ Args:
37
+ command: Command to execute
38
+ """
39
+
40
+ if command.output_to_file is not None:
41
+ redirect_operator = ">>" if command.append_or_overwrite == "APPEND" else ">"
42
+ command = RunnableCommandString(
43
+ command=f"{command.command} {redirect_operator} {command.output_to_file}"
44
+ )
45
+
46
+ # Build the actual command to execute
47
+ cmd = f"sudo {command.command}" if command.sudo else command.command
48
+
49
+ try:
50
+ result: InvokeResult | None = run(
51
+ command=cmd,
52
+ env=command.env,
53
+ hide=self._hide,
54
+ warn=True # Don't raise on non-zero exit codes
55
+ )
56
+ mapped_result = self._map_invoke_result(result, command=command)
57
+
58
+ self._logger.debug(
59
+ "Command Executed > '%r', On Local Machine\n\n",
60
+ mapped_result,
61
+ )
62
+ return mapped_result
63
+ except Exception as e:
64
+ self._logger.error(
65
+ "Command didn't reach an exit code. Command: %s", cmd
66
+ )
67
+ return ExecResult(
68
+ stdout="",
69
+ stderr=str(e),
70
+ return_code=1,
71
+ success=False,
72
+ command=command,
73
+ )
74
+
75
+ def put_file(self, source: Path | str | BytesIO, remote_path: str) -> ExecResult:
76
+ """
77
+ Copy a file locally from source to destination.
78
+
79
+ Args:
80
+ source: Path to source file or a file-like object as BytesIO.
81
+ remote_path: Local destination path
82
+ """
83
+ try:
84
+ dest_path = Path(remote_path)
85
+
86
+ # Create parent directories if they don't exist
87
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
88
+
89
+ if isinstance(source, (str, Path)):
90
+ # Copy from file path
91
+ source_path = Path(source)
92
+ if not source_path.exists():
93
+ return ExecResult(
94
+ stdout="",
95
+ stderr=f"Source file does not exist: {source}",
96
+ return_code=1,
97
+ success=False,
98
+ )
99
+
100
+ import shutil
101
+ shutil.copy2(source_path, dest_path)
102
+ self._logger.debug(
103
+ "File copied locally from '%s' to '%s'",
104
+ source_path,
105
+ dest_path,
106
+ )
107
+
108
+ elif isinstance(source, BytesIO):
109
+ # Write from BytesIO object
110
+ with open(dest_path, 'wb') as f:
111
+ f.write(source.getvalue())
112
+ self._logger.debug(
113
+ "Content written to file '%s' from BytesIO object",
114
+ dest_path,
115
+ )
116
+
117
+ return ExecResult(
118
+ stdout=f"File copied successfully from '{source}' to '{dest_path}'",
119
+ stderr="",
120
+ return_code=0,
121
+ success=True,
122
+ )
123
+
124
+ except Exception as e:
125
+ return ExecResult(
126
+ stdout="",
127
+ stderr=str(e),
128
+ return_code=1,
129
+ success=False,
130
+ )
131
+
132
+ def _map_invoke_result(
133
+ self, result: InvokeResult | None, command: RunnableCommandString
134
+ ) -> ExecResult:
135
+ if result is None:
136
+ return ExecResult(
137
+ stdout="",
138
+ stderr="InvokeResult was None",
139
+ return_code=1,
140
+ success=False,
141
+ command=command,
142
+ )
143
+
144
+ return ExecResult(
145
+ stdout=result.stdout or "",
146
+ stderr=result.stderr or "",
147
+ return_code=result.return_code,
148
+ success=result.return_code == 0,
149
+ command=command,
150
+ )
@@ -0,0 +1,15 @@
1
+ # from .base.caddy_commands import CaddyCommands
2
+ # from .base.command_base import CommandBase
3
+ # from .base.docker_compose_commands import DockerComposeCommands
4
+ # from .base.git_commands import GitCommands
5
+ # from .base.os_package_manager_commands import OsPackageManagerCommands
6
+ # from .base.podman_commands import PodmanCommands
7
+
8
+ # __all__ = [
9
+ # "CaddyCommands",
10
+ # "CommandBase",
11
+ # "DockerComposeCommands",
12
+ # "PodmanCommands",
13
+ # "GitCommands",
14
+ # "OsPackageManagerCommands",
15
+ # ]
@@ -0,0 +1,39 @@
1
+ from .archive_commands import ArchiveCommands
2
+ from .caddy_api_curl_commands import CaddyApiCurlCommands
3
+ from .caddy_commands import CaddyCommands
4
+ from .command_base import CommandBase
5
+ from .curl_commands import CurlCommands
6
+ from .dir_commands import DirCommands
7
+ from .docker_compose_commands import DockerComposeCommands
8
+ from .file_commands import FileCommands
9
+ from .git_commands import GitCommands
10
+ from .ip_tables_commands import IpTablesCommands
11
+ from .ip_tables_persistent_commands import IpTablesPersistentCommands
12
+ from .null_command import NullCommand
13
+ from .os_package_manager_commands import OsPackageManagerCommands
14
+ from .podman_commands import PodmanCommands
15
+ from .ssh_agent_commands import SshAgentCommands
16
+ from .ssh_keygen_commands import SshKeygenCommands
17
+ from .systemctl_commands import SystemctlCommands
18
+ from .utility_commands import UtilityCommands
19
+
20
+ __all__ = [
21
+ "ArchiveCommands",
22
+ "CaddyApiCurlCommands",
23
+ "CaddyCommands",
24
+ "CommandBase",
25
+ "CurlCommands",
26
+ "DirCommands",
27
+ "DockerComposeCommands",
28
+ "FileCommands",
29
+ "GitCommands",
30
+ "IpTablesCommands",
31
+ "IpTablesPersistentCommands",
32
+ "NullCommand",
33
+ "OsPackageManagerCommands",
34
+ "PodmanCommands",
35
+ "SshAgentCommands",
36
+ "SystemctlCommands",
37
+ "UtilityCommands",
38
+ "SshKeygenCommands",
39
+ ]