workload-profile-controller 0.1.0__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.
- workload_profile_controller/application.py +13 -0
- workload_profile_controller/backend.py +21 -0
- workload_profile_controller/backends/__init__.py +0 -0
- workload_profile_controller/backends/proxmox/__init__.py +0 -0
- workload_profile_controller/backends/proxmox/auth.py +15 -0
- workload_profile_controller/backends/proxmox/backend.py +97 -0
- workload_profile_controller/backends/proxmox/config.py +16 -0
- workload_profile_controller/backends/proxmox/config_loader.py +46 -0
- workload_profile_controller/backends/proxmox/discovery.py +54 -0
- workload_profile_controller/backends/proxmox/errors.py +13 -0
- workload_profile_controller/backends/proxmox/factory.py +43 -0
- workload_profile_controller/backends/proxmox/http_client.py +107 -0
- workload_profile_controller/backends/proxmox/inventory.py +52 -0
- workload_profile_controller/backends/proxmox/resource_client.py +87 -0
- workload_profile_controller/backends/proxmox/task.py +16 -0
- workload_profile_controller/backends/proxmox/task_waiter.py +53 -0
- workload_profile_controller/cli.py +154 -0
- workload_profile_controller/config.py +21 -0
- workload_profile_controller/config_loader.py +40 -0
- workload_profile_controller/config_validator.py +106 -0
- workload_profile_controller/controller.py +154 -0
- workload_profile_controller/errors.py +26 -0
- workload_profile_controller/policy.py +67 -0
- workload_profile_controller-0.1.0.dist-info/METADATA +500 -0
- workload_profile_controller-0.1.0.dist-info/RECORD +29 -0
- workload_profile_controller-0.1.0.dist-info/WHEEL +5 -0
- workload_profile_controller-0.1.0.dist-info/entry_points.txt +2 -0
- workload_profile_controller-0.1.0.dist-info/licenses/LICENSE +21 -0
- workload_profile_controller-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .backends.proxmox.factory import create_proxmox_backend
|
|
2
|
+
from .config_loader import load_config
|
|
3
|
+
from .controller import Controller
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_controller(config_path: str) -> Controller:
|
|
7
|
+
config = load_config(config_path)
|
|
8
|
+
backend = create_proxmox_backend()
|
|
9
|
+
|
|
10
|
+
return Controller(
|
|
11
|
+
config=config,
|
|
12
|
+
backend=backend,
|
|
13
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ResourceStatus(Enum):
|
|
6
|
+
RUNNING = "running"
|
|
7
|
+
STOPPED = "stopped"
|
|
8
|
+
STARTING = "starting"
|
|
9
|
+
STOPPING = "stopping"
|
|
10
|
+
UNKNOWN = "unknown"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Backend(Protocol):
|
|
14
|
+
def get_status(self, resource_id: str) -> ResourceStatus:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def start(self, resource_id: str) -> None:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def stop(self, resource_id: str) -> None:
|
|
21
|
+
...
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class ProxmoxToken:
|
|
6
|
+
user: str
|
|
7
|
+
token_name: str
|
|
8
|
+
secret: str
|
|
9
|
+
|
|
10
|
+
def authorization_header(self) -> str:
|
|
11
|
+
return (
|
|
12
|
+
"PVEAPIToken="
|
|
13
|
+
f"{self.user}!{self.token_name}="
|
|
14
|
+
f"{self.secret}"
|
|
15
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from ...backend import Backend, ResourceStatus
|
|
2
|
+
from .discovery import ProxmoxDiscovery
|
|
3
|
+
from .inventory import ProxmoxResourceType
|
|
4
|
+
from .resource_client import ProxmoxResourceClient
|
|
5
|
+
from .task_waiter import TaskWaiter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProxmoxBackend(Backend):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
discovery: ProxmoxDiscovery,
|
|
12
|
+
resource_client: ProxmoxResourceClient,
|
|
13
|
+
task_waiter: TaskWaiter,
|
|
14
|
+
):
|
|
15
|
+
self._discovery = discovery
|
|
16
|
+
self._resource_client = resource_client
|
|
17
|
+
self._task_waiter = task_waiter
|
|
18
|
+
|
|
19
|
+
def get_status(self, resource_id: str) -> ResourceStatus:
|
|
20
|
+
inventory = self._discovery.discover()
|
|
21
|
+
resource = inventory.resolve(resource_id)
|
|
22
|
+
|
|
23
|
+
if resource.resource_type == ProxmoxResourceType.QEMU:
|
|
24
|
+
data = self._resource_client.get_qemu_status(
|
|
25
|
+
resource.proxmox_id
|
|
26
|
+
)
|
|
27
|
+
elif resource.resource_type == ProxmoxResourceType.LXC:
|
|
28
|
+
data = self._resource_client.get_lxc_status(
|
|
29
|
+
resource.proxmox_id
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
f"Unsupported Proxmox resource type: "
|
|
34
|
+
f"{resource.resource_type}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
status = data["status"]
|
|
38
|
+
|
|
39
|
+
if status == "running":
|
|
40
|
+
return ResourceStatus.RUNNING
|
|
41
|
+
|
|
42
|
+
if status == "stopped":
|
|
43
|
+
return ResourceStatus.STOPPED
|
|
44
|
+
|
|
45
|
+
if status == "starting":
|
|
46
|
+
return ResourceStatus.STARTING
|
|
47
|
+
|
|
48
|
+
if status == "stopping":
|
|
49
|
+
return ResourceStatus.STOPPING
|
|
50
|
+
|
|
51
|
+
return ResourceStatus.UNKNOWN
|
|
52
|
+
|
|
53
|
+
def start(self, resource_id: str) -> None:
|
|
54
|
+
inventory = self._discovery.discover()
|
|
55
|
+
resource = inventory.resolve(resource_id)
|
|
56
|
+
|
|
57
|
+
if resource.resource_type == ProxmoxResourceType.QEMU:
|
|
58
|
+
upid = self._resource_client.start_qemu(
|
|
59
|
+
resource.proxmox_id
|
|
60
|
+
)
|
|
61
|
+
self._task_waiter.wait(upid)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if resource.resource_type == ProxmoxResourceType.LXC:
|
|
65
|
+
upid = self._resource_client.start_lxc(
|
|
66
|
+
resource.proxmox_id
|
|
67
|
+
)
|
|
68
|
+
self._task_waiter.wait(upid)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
f"Unsupported Proxmox resource type: "
|
|
73
|
+
f"{resource.resource_type}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def stop(self, resource_id: str) -> None:
|
|
77
|
+
inventory = self._discovery.discover()
|
|
78
|
+
resource = inventory.resolve(resource_id)
|
|
79
|
+
|
|
80
|
+
if resource.resource_type == ProxmoxResourceType.QEMU:
|
|
81
|
+
upid = self._resource_client.stop_qemu(
|
|
82
|
+
resource.proxmox_id
|
|
83
|
+
)
|
|
84
|
+
self._task_waiter.wait(upid)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if resource.resource_type == ProxmoxResourceType.LXC:
|
|
88
|
+
upid = self._resource_client.stop_lxc(
|
|
89
|
+
resource.proxmox_id
|
|
90
|
+
)
|
|
91
|
+
self._task_waiter.wait(upid)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
f"Unsupported Proxmox resource type: "
|
|
96
|
+
f"{resource.resource_type}"
|
|
97
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class ProxmoxConfig:
|
|
6
|
+
host: str
|
|
7
|
+
node: str
|
|
8
|
+
user: str
|
|
9
|
+
token_name: str
|
|
10
|
+
token_secret: str
|
|
11
|
+
verify_tls: bool = True
|
|
12
|
+
ca_file: str | None = None
|
|
13
|
+
tls_server_name: str | None = None
|
|
14
|
+
timeout: float = 5.0
|
|
15
|
+
task_timeout: float = 120.0
|
|
16
|
+
shutdown_timeout: float = 120.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
from .config import ProxmoxConfig
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_proxmox_config() -> ProxmoxConfig:
|
|
11
|
+
return ProxmoxConfig(
|
|
12
|
+
host=os.environ["PVE_PROXMOX_HOST"],
|
|
13
|
+
node=os.environ["PVE_PROXMOX_NODE"],
|
|
14
|
+
user=os.environ["PVE_PROXMOX_USER"],
|
|
15
|
+
token_name=os.environ["PVE_PROXMOX_TOKEN_NAME"],
|
|
16
|
+
token_secret=os.environ["PVE_PROXMOX_TOKEN_SECRET"],
|
|
17
|
+
verify_tls=os.environ.get(
|
|
18
|
+
"PVE_PROXMOX_VERIFY_TLS",
|
|
19
|
+
"true",
|
|
20
|
+
).lower() == "true",
|
|
21
|
+
ca_file=os.environ.get(
|
|
22
|
+
"PVE_PROXMOX_CA_FILE",
|
|
23
|
+
),
|
|
24
|
+
tls_server_name=os.environ.get(
|
|
25
|
+
"PVE_PROXMOX_TLS_SERVER_NAME",
|
|
26
|
+
),
|
|
27
|
+
timeout=float(
|
|
28
|
+
os.environ.get(
|
|
29
|
+
"PVE_PROXMOX_TIMEOUT",
|
|
30
|
+
"5.0",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
),
|
|
34
|
+
task_timeout=float(
|
|
35
|
+
os.environ.get(
|
|
36
|
+
"PVE_PROXMOX_TASK_TIMEOUT",
|
|
37
|
+
"120.0",
|
|
38
|
+
)
|
|
39
|
+
),
|
|
40
|
+
shutdown_timeout=float(
|
|
41
|
+
os.environ.get(
|
|
42
|
+
"PVE_PROXMOX_SHUTDOWN_TIMEOUT",
|
|
43
|
+
"120.0",
|
|
44
|
+
)
|
|
45
|
+
),
|
|
46
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
|
|
3
|
+
from .inventory import (
|
|
4
|
+
ProxmoxInventory,
|
|
5
|
+
ProxmoxResource,
|
|
6
|
+
ProxmoxResourceType,
|
|
7
|
+
)
|
|
8
|
+
from .resource_client import ProxmoxResourceClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProxmoxDiscovery:
|
|
12
|
+
def __init__(self, resource_client: ProxmoxResourceClient):
|
|
13
|
+
self._resource_client = resource_client
|
|
14
|
+
|
|
15
|
+
def discover(self) -> ProxmoxInventory:
|
|
16
|
+
return self.build_inventory(
|
|
17
|
+
qemu_data=self._resource_client.list_qemu(),
|
|
18
|
+
lxc_data=self._resource_client.list_lxc(),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def build_inventory(
|
|
23
|
+
qemu_data: Sequence[Mapping[str, object]],
|
|
24
|
+
lxc_data: Sequence[Mapping[str, object]],
|
|
25
|
+
) -> ProxmoxInventory:
|
|
26
|
+
resources = [
|
|
27
|
+
*(
|
|
28
|
+
ProxmoxDiscovery._build_resource(
|
|
29
|
+
item,
|
|
30
|
+
ProxmoxResourceType.QEMU,
|
|
31
|
+
)
|
|
32
|
+
for item in qemu_data
|
|
33
|
+
),
|
|
34
|
+
*(
|
|
35
|
+
ProxmoxDiscovery._build_resource(
|
|
36
|
+
item,
|
|
37
|
+
ProxmoxResourceType.LXC,
|
|
38
|
+
)
|
|
39
|
+
for item in lxc_data
|
|
40
|
+
),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
return ProxmoxInventory(resources)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _build_resource(
|
|
47
|
+
data: Mapping[str, object],
|
|
48
|
+
resource_type: ProxmoxResourceType,
|
|
49
|
+
) -> ProxmoxResource:
|
|
50
|
+
return ProxmoxResource(
|
|
51
|
+
resource_type=resource_type,
|
|
52
|
+
proxmox_id=int(data["vmid"]),
|
|
53
|
+
name=str(data["name"]),
|
|
54
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .auth import ProxmoxToken
|
|
2
|
+
from .backend import ProxmoxBackend
|
|
3
|
+
from .config_loader import load_proxmox_config
|
|
4
|
+
from .discovery import ProxmoxDiscovery
|
|
5
|
+
from .http_client import ProxmoxHttpClient
|
|
6
|
+
from .resource_client import ProxmoxResourceClient
|
|
7
|
+
from .task_waiter import TaskWaiter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_proxmox_backend() -> ProxmoxBackend:
|
|
11
|
+
config = load_proxmox_config()
|
|
12
|
+
|
|
13
|
+
token = ProxmoxToken(
|
|
14
|
+
user=config.user,
|
|
15
|
+
token_name=config.token_name,
|
|
16
|
+
secret=config.token_secret,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
http_client = ProxmoxHttpClient(
|
|
20
|
+
config=config,
|
|
21
|
+
token=token,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
resource_client = ProxmoxResourceClient(
|
|
25
|
+
http_client=http_client,
|
|
26
|
+
node=config.node,
|
|
27
|
+
shutdown_timeout=config.shutdown_timeout,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
discovery = ProxmoxDiscovery(
|
|
31
|
+
resource_client=resource_client,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
task_waiter = TaskWaiter(
|
|
35
|
+
get_status=resource_client.get_task_status,
|
|
36
|
+
timeout=config.task_timeout,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return ProxmoxBackend(
|
|
40
|
+
discovery=discovery,
|
|
41
|
+
resource_client=resource_client,
|
|
42
|
+
task_waiter=task_waiter,
|
|
43
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import http.client
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import ssl
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .auth import ProxmoxToken
|
|
10
|
+
from .config import ProxmoxConfig
|
|
11
|
+
from .errors import ProxmoxHttpError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _ProxmoxHTTPSConnection(http.client.HTTPSConnection):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
connect_host: str,
|
|
18
|
+
tls_server_name: str | None,
|
|
19
|
+
context: ssl.SSLContext,
|
|
20
|
+
timeout: float,
|
|
21
|
+
):
|
|
22
|
+
super().__init__(
|
|
23
|
+
host=connect_host,
|
|
24
|
+
port=8006,
|
|
25
|
+
timeout=timeout,
|
|
26
|
+
context=context,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self._tls_server_name = tls_server_name
|
|
30
|
+
|
|
31
|
+
def connect(self):
|
|
32
|
+
self.sock = socket.create_connection(
|
|
33
|
+
(self.host, self.port),
|
|
34
|
+
self.timeout,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self.sock = self._context.wrap_socket(
|
|
38
|
+
self.sock,
|
|
39
|
+
server_hostname=self._tls_server_name or self.host,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProxmoxHttpClient:
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
config: ProxmoxConfig,
|
|
47
|
+
token: ProxmoxToken,
|
|
48
|
+
):
|
|
49
|
+
self._config = config
|
|
50
|
+
self._token = token
|
|
51
|
+
|
|
52
|
+
def request(
|
|
53
|
+
self,
|
|
54
|
+
method: str,
|
|
55
|
+
path: str,
|
|
56
|
+
data: Mapping[str, Any] | None = None,
|
|
57
|
+
) -> Any:
|
|
58
|
+
body = None
|
|
59
|
+
|
|
60
|
+
if data is not None:
|
|
61
|
+
body = urllib.parse.urlencode(data).encode("utf-8")
|
|
62
|
+
|
|
63
|
+
context = self._create_ssl_context()
|
|
64
|
+
|
|
65
|
+
connection = _ProxmoxHTTPSConnection(
|
|
66
|
+
connect_host=self._config.host,
|
|
67
|
+
tls_server_name=self._config.tls_server_name,
|
|
68
|
+
context=context,
|
|
69
|
+
timeout=self._config.timeout,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
connection.request(
|
|
74
|
+
method,
|
|
75
|
+
f"/api2/json{path}",
|
|
76
|
+
body=body,
|
|
77
|
+
headers={
|
|
78
|
+
"Authorization": (
|
|
79
|
+
self._token.authorization_header()
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
response = connection.getresponse()
|
|
85
|
+
if response.status < 200 or response.status >= 300:
|
|
86
|
+
reason = response.reason or "Unknown error"
|
|
87
|
+
response.read()
|
|
88
|
+
|
|
89
|
+
raise ProxmoxHttpError(
|
|
90
|
+
status_code=response.status,
|
|
91
|
+
reason=reason,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
payload = json.load(response)
|
|
95
|
+
|
|
96
|
+
return payload["data"]
|
|
97
|
+
|
|
98
|
+
finally:
|
|
99
|
+
connection.close()
|
|
100
|
+
|
|
101
|
+
def _create_ssl_context(self) -> ssl.SSLContext:
|
|
102
|
+
if self._config.verify_tls:
|
|
103
|
+
return ssl.create_default_context(
|
|
104
|
+
cafile=self._config.ca_file,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return ssl._create_unverified_context()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from ...errors import ResourceAmbiguousError, ResourceNotFoundError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProxmoxResourceType(Enum):
|
|
8
|
+
QEMU = "qemu"
|
|
9
|
+
LXC = "lxc"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ProxmoxResource:
|
|
14
|
+
resource_type: ProxmoxResourceType
|
|
15
|
+
proxmox_id: int
|
|
16
|
+
name: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProxmoxInventory:
|
|
20
|
+
def __init__(self, resources: list[ProxmoxResource]):
|
|
21
|
+
self._resources = tuple(
|
|
22
|
+
sorted(
|
|
23
|
+
resources,
|
|
24
|
+
key=lambda resource: (
|
|
25
|
+
resource.name,
|
|
26
|
+
resource.resource_type.value,
|
|
27
|
+
resource.proxmox_id,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def list_resources(self) -> tuple[ProxmoxResource, ...]:
|
|
33
|
+
return self._resources
|
|
34
|
+
|
|
35
|
+
def resolve(self, name: str) -> ProxmoxResource:
|
|
36
|
+
matches = tuple(
|
|
37
|
+
resource
|
|
38
|
+
for resource in self._resources
|
|
39
|
+
if resource.name == name
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not matches:
|
|
43
|
+
raise ResourceNotFoundError(
|
|
44
|
+
f"Proxmox resource not found: {name}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if len(matches) > 1:
|
|
48
|
+
raise ResourceAmbiguousError(
|
|
49
|
+
f"Proxmox resource name is ambiguous: {name}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return matches[0]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
|
|
3
|
+
from .http_client import ProxmoxHttpClient
|
|
4
|
+
from .task import TaskStatus
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProxmoxResourceClient:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
http_client: ProxmoxHttpClient,
|
|
11
|
+
node: str,
|
|
12
|
+
shutdown_timeout: float = 120.0,
|
|
13
|
+
):
|
|
14
|
+
self._http_client = http_client
|
|
15
|
+
self._node = node
|
|
16
|
+
self._shutdown_timeout = shutdown_timeout
|
|
17
|
+
|
|
18
|
+
def list_qemu(self):
|
|
19
|
+
return self._http_client.request(
|
|
20
|
+
"GET",
|
|
21
|
+
f"/nodes/{self._node}/qemu",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def list_lxc(self):
|
|
25
|
+
return self._http_client.request(
|
|
26
|
+
"GET",
|
|
27
|
+
f"/nodes/{self._node}/lxc",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def get_qemu_status(self, proxmox_id: int):
|
|
31
|
+
return self._http_client.request(
|
|
32
|
+
"GET",
|
|
33
|
+
f"/nodes/{self._node}/qemu/{proxmox_id}/status/current",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def get_lxc_status(self, proxmox_id: int):
|
|
37
|
+
return self._http_client.request(
|
|
38
|
+
"GET",
|
|
39
|
+
f"/nodes/{self._node}/lxc/{proxmox_id}/status/current",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def start_qemu(self, proxmox_id: int):
|
|
43
|
+
return self._http_client.request(
|
|
44
|
+
"POST",
|
|
45
|
+
f"/nodes/{self._node}/qemu/{proxmox_id}/status/start",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def start_lxc(self, proxmox_id: int):
|
|
49
|
+
return self._http_client.request(
|
|
50
|
+
"POST",
|
|
51
|
+
f"/nodes/{self._node}/lxc/{proxmox_id}/status/start",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def stop_qemu(self, proxmox_id: int):
|
|
55
|
+
return self._http_client.request(
|
|
56
|
+
"POST",
|
|
57
|
+
f"/nodes/{self._node}/qemu/{proxmox_id}/status/shutdown",
|
|
58
|
+
data={
|
|
59
|
+
"timeout": int(self._shutdown_timeout),
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def stop_lxc(self, proxmox_id: int):
|
|
64
|
+
return self._http_client.request(
|
|
65
|
+
"POST",
|
|
66
|
+
f"/nodes/{self._node}/lxc/{proxmox_id}/status/shutdown",
|
|
67
|
+
data={
|
|
68
|
+
"timeout": int(self._shutdown_timeout),
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def get_task_status(self, upid: str) -> TaskStatus:
|
|
73
|
+
encoded_upid = urllib.parse.quote(
|
|
74
|
+
upid,
|
|
75
|
+
safe="",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
data = self._http_client.request(
|
|
79
|
+
"GET",
|
|
80
|
+
f"/nodes/{self._node}/tasks/{encoded_upid}/status",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return TaskStatus(
|
|
84
|
+
upid=upid,
|
|
85
|
+
status=str(data["status"]),
|
|
86
|
+
exitstatus=data.get("exitstatus"),
|
|
87
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class TaskStatus:
|
|
6
|
+
upid: str
|
|
7
|
+
status: str
|
|
8
|
+
exitstatus: str | None
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def is_finished(self) -> bool:
|
|
12
|
+
return self.status == "stopped"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def is_successful(self) -> bool:
|
|
16
|
+
return self.is_finished and self.exitstatus == "OK"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from time import sleep, time
|
|
3
|
+
|
|
4
|
+
from ...errors import TaskFailedError, TaskTimeoutError
|
|
5
|
+
from .task import TaskStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskWaiter:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
get_status: Callable[[str], TaskStatus],
|
|
12
|
+
timeout: float = 30.0,
|
|
13
|
+
sleep_fn: Callable[[float], None] = sleep,
|
|
14
|
+
time_fn: Callable[[], float] = time,
|
|
15
|
+
):
|
|
16
|
+
self._get_status = get_status
|
|
17
|
+
self._timeout = timeout
|
|
18
|
+
self._sleep = sleep_fn
|
|
19
|
+
self._time = time_fn
|
|
20
|
+
|
|
21
|
+
def wait(
|
|
22
|
+
self,
|
|
23
|
+
upid: str,
|
|
24
|
+
interval: float = 1.0,
|
|
25
|
+
timeout: float | None = None,
|
|
26
|
+
) -> TaskStatus:
|
|
27
|
+
effective_timeout = (
|
|
28
|
+
self._timeout
|
|
29
|
+
if timeout is None
|
|
30
|
+
else timeout
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
started_at = self._time()
|
|
34
|
+
|
|
35
|
+
while True:
|
|
36
|
+
status = self._get_status(upid)
|
|
37
|
+
|
|
38
|
+
if status.is_finished:
|
|
39
|
+
if not status.is_successful:
|
|
40
|
+
raise TaskFailedError(
|
|
41
|
+
f"Task failed: {upid}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return status
|
|
45
|
+
|
|
46
|
+
elapsed = self._time() - started_at
|
|
47
|
+
|
|
48
|
+
if elapsed >= effective_timeout:
|
|
49
|
+
raise TaskTimeoutError(
|
|
50
|
+
f"Task timed out: {upid}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self._sleep(interval)
|