workload-profile-controller 0.1.0__tar.gz → 1.0.0__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.
- {workload_profile_controller-0.1.0/src/workload_profile_controller.egg-info → workload_profile_controller-1.0.0}/PKG-INFO +2 -1
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/pyproject.toml +2 -1
- workload_profile_controller-1.0.0/src/workload_profile_controller/application.py +22 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/backend.py +51 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/config.py +10 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/config_loader.py +28 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/discovery.py +78 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/factory.py +21 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/inventory.py +44 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/resource_client.py +131 -0
- workload_profile_controller-1.0.0/src/workload_profile_controller/backends/proxmox/__init__.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/cli.py +19 -4
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/config.py +2 -1
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/config_loader.py +1 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/config_validator.py +6 -1
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0/src/workload_profile_controller.egg-info}/PKG-INFO +2 -1
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller.egg-info/SOURCES.txt +8 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller.egg-info/requires.txt +1 -0
- workload_profile_controller-0.1.0/src/workload_profile_controller/application.py +0 -13
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/LICENSE +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/README.md +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/setup.cfg +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backend.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/__init__.py +0 -0
- {workload_profile_controller-0.1.0/src/workload_profile_controller/backends/proxmox → workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws}/__init__.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/auth.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/backend.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/config.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/config_loader.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/discovery.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/errors.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/factory.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/http_client.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/inventory.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/resource_client.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/task.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/backends/proxmox/task_waiter.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/controller.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/errors.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller/policy.py +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller.egg-info/dependency_links.txt +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller.egg-info/entry_points.txt +0 -0
- {workload_profile_controller-0.1.0 → workload_profile_controller-1.0.0}/src/workload_profile_controller.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workload-profile-controller
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Safe and configurable workload profile controller
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/lack0fcode/workload-profile-controller
|
|
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: PyYAML>=6.0
|
|
24
24
|
Requires-Dist: python-dotenv>=1.2
|
|
25
|
+
Requires-Dist: boto3>=1.35
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
28
|
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "workload-profile-controller"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Safe and configurable workload profile controller"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -34,6 +34,7 @@ classifiers = [
|
|
|
34
34
|
dependencies = [
|
|
35
35
|
"PyYAML>=6.0",
|
|
36
36
|
"python-dotenv>=1.2",
|
|
37
|
+
"boto3>=1.35",
|
|
37
38
|
]
|
|
38
39
|
|
|
39
40
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .backends.aws.factory import create_aws_backend
|
|
2
|
+
from .backends.proxmox.factory import create_proxmox_backend
|
|
3
|
+
from .config_loader import load_config
|
|
4
|
+
from .controller import Controller
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_controller(config_path: str) -> Controller:
|
|
8
|
+
config = load_config(config_path)
|
|
9
|
+
|
|
10
|
+
if config.backend == "proxmox":
|
|
11
|
+
backend = create_proxmox_backend()
|
|
12
|
+
elif config.backend == "aws":
|
|
13
|
+
backend = create_aws_backend()
|
|
14
|
+
else:
|
|
15
|
+
raise ValueError(
|
|
16
|
+
f"Unsupported backend: {config.backend}"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return Controller(
|
|
20
|
+
config=config,
|
|
21
|
+
backend=backend,
|
|
22
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from ...backend import Backend, ResourceStatus
|
|
2
|
+
from .discovery import AwsDiscovery
|
|
3
|
+
from .resource_client import AwsResourceClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AwsBackend(Backend):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
discovery: AwsDiscovery,
|
|
10
|
+
resource_client: AwsResourceClient,
|
|
11
|
+
):
|
|
12
|
+
self._discovery = discovery
|
|
13
|
+
self._resource_client = resource_client
|
|
14
|
+
|
|
15
|
+
def get_status(self, resource_id: str) -> ResourceStatus:
|
|
16
|
+
inventory = self._discovery.discover()
|
|
17
|
+
resource = inventory.resolve(resource_id)
|
|
18
|
+
|
|
19
|
+
state = self._resource_client.get_instance_state(
|
|
20
|
+
resource.instance_id,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if state == "running":
|
|
24
|
+
return ResourceStatus.RUNNING
|
|
25
|
+
|
|
26
|
+
if state == "stopped":
|
|
27
|
+
return ResourceStatus.STOPPED
|
|
28
|
+
|
|
29
|
+
if state == "pending":
|
|
30
|
+
return ResourceStatus.STARTING
|
|
31
|
+
|
|
32
|
+
if state == "stopping":
|
|
33
|
+
return ResourceStatus.STOPPING
|
|
34
|
+
|
|
35
|
+
return ResourceStatus.UNKNOWN
|
|
36
|
+
|
|
37
|
+
def start(self, resource_id: str) -> None:
|
|
38
|
+
inventory = self._discovery.discover()
|
|
39
|
+
resource = inventory.resolve(resource_id)
|
|
40
|
+
|
|
41
|
+
self._resource_client.start_instance(
|
|
42
|
+
resource.instance_id,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def stop(self, resource_id: str) -> None:
|
|
46
|
+
inventory = self._discovery.discover()
|
|
47
|
+
resource = inventory.resolve(resource_id)
|
|
48
|
+
|
|
49
|
+
self._resource_client.stop_instance(
|
|
50
|
+
resource.instance_id,
|
|
51
|
+
)
|
workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/config_loader.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
from .config import AwsConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_aws_config() -> AwsConfig:
|
|
12
|
+
return AwsConfig(
|
|
13
|
+
region=os.environ["WPC_AWS_REGION"],
|
|
14
|
+
profile=os.environ.get("WPC_AWS_PROFILE"),
|
|
15
|
+
endpoint_url=os.environ.get("WPC_AWS_ENDPOINT_URL"),
|
|
16
|
+
timeout=float(
|
|
17
|
+
os.environ.get(
|
|
18
|
+
"WPC_AWS_TIMEOUT",
|
|
19
|
+
"5.0",
|
|
20
|
+
)
|
|
21
|
+
),
|
|
22
|
+
operation_timeout=float(
|
|
23
|
+
os.environ.get(
|
|
24
|
+
"WPC_AWS_OPERATION_TIMEOUT",
|
|
25
|
+
"120.0",
|
|
26
|
+
)
|
|
27
|
+
),
|
|
28
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
|
|
3
|
+
from .inventory import AwsInventory, AwsResource
|
|
4
|
+
from .resource_client import AwsResourceClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AwsDiscovery:
|
|
8
|
+
def __init__(self, resource_client: AwsResourceClient):
|
|
9
|
+
self._resource_client = resource_client
|
|
10
|
+
|
|
11
|
+
def discover(self) -> AwsInventory:
|
|
12
|
+
return self.build_inventory(
|
|
13
|
+
reservations=self._resource_client.list_instances(),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def build_inventory(
|
|
18
|
+
reservations: Sequence[Mapping[str, object]],
|
|
19
|
+
) -> AwsInventory:
|
|
20
|
+
resources = []
|
|
21
|
+
|
|
22
|
+
for reservation in reservations:
|
|
23
|
+
instances = reservation.get("Instances", [])
|
|
24
|
+
|
|
25
|
+
if not isinstance(instances, Sequence):
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
for instance in instances:
|
|
29
|
+
if not isinstance(instance, Mapping):
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
resource = AwsDiscovery._build_resource(instance)
|
|
33
|
+
|
|
34
|
+
if resource is not None:
|
|
35
|
+
resources.append(resource)
|
|
36
|
+
|
|
37
|
+
return AwsInventory(resources)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _build_resource(
|
|
41
|
+
data: Mapping[str, object],
|
|
42
|
+
) -> AwsResource | None:
|
|
43
|
+
instance_id = data.get("InstanceId")
|
|
44
|
+
tags = data.get("Tags", [])
|
|
45
|
+
|
|
46
|
+
if not isinstance(instance_id, str):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
name = AwsDiscovery._get_name_tag(tags)
|
|
50
|
+
|
|
51
|
+
if name is None:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
return AwsResource(
|
|
55
|
+
instance_id=instance_id,
|
|
56
|
+
name=name,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _get_name_tag(
|
|
61
|
+
tags: object,
|
|
62
|
+
) -> str | None:
|
|
63
|
+
if not isinstance(tags, Sequence):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
for tag in tags:
|
|
67
|
+
if not isinstance(tag, Mapping):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if tag.get("Key") == "Name":
|
|
71
|
+
value = tag.get("Value")
|
|
72
|
+
|
|
73
|
+
if isinstance(value, str):
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .backend import AwsBackend
|
|
2
|
+
from .config_loader import load_aws_config
|
|
3
|
+
from .discovery import AwsDiscovery
|
|
4
|
+
from .resource_client import AwsResourceClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_aws_backend() -> AwsBackend:
|
|
8
|
+
config = load_aws_config()
|
|
9
|
+
|
|
10
|
+
resource_client = AwsResourceClient(
|
|
11
|
+
config=config,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
discovery = AwsDiscovery(
|
|
15
|
+
resource_client=resource_client,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return AwsBackend(
|
|
19
|
+
discovery=discovery,
|
|
20
|
+
resource_client=resource_client,
|
|
21
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ...errors import ResourceAmbiguousError, ResourceNotFoundError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class AwsResource:
|
|
8
|
+
instance_id: str
|
|
9
|
+
name: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AwsInventory:
|
|
13
|
+
def __init__(self, resources: list[AwsResource]):
|
|
14
|
+
self._resources = tuple(
|
|
15
|
+
sorted(
|
|
16
|
+
resources,
|
|
17
|
+
key=lambda resource: (
|
|
18
|
+
resource.name,
|
|
19
|
+
resource.instance_id,
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def list_resources(self) -> tuple[AwsResource, ...]:
|
|
25
|
+
return self._resources
|
|
26
|
+
|
|
27
|
+
def resolve(self, name: str) -> AwsResource:
|
|
28
|
+
matches = tuple(
|
|
29
|
+
resource
|
|
30
|
+
for resource in self._resources
|
|
31
|
+
if resource.name == name
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if not matches:
|
|
35
|
+
raise ResourceNotFoundError(
|
|
36
|
+
f"AWS resource not found: {name}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if len(matches) > 1:
|
|
40
|
+
raise ResourceAmbiguousError(
|
|
41
|
+
f"AWS resource name is ambiguous: {name}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return matches[0]
|
workload_profile_controller-1.0.0/src/workload_profile_controller/backends/aws/resource_client.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
|
|
6
|
+
from ...errors import InvalidResourceStateError, TaskTimeoutError
|
|
7
|
+
from .config import AwsConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AwsResourceClient:
|
|
11
|
+
def __init__(self, config: AwsConfig):
|
|
12
|
+
session = boto3.Session(
|
|
13
|
+
profile_name=config.profile,
|
|
14
|
+
region_name=config.region,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
self._client = session.client(
|
|
18
|
+
"ec2",
|
|
19
|
+
endpoint_url=config.endpoint_url,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
self._operation_timeout = config.operation_timeout
|
|
23
|
+
|
|
24
|
+
def list_instances(self):
|
|
25
|
+
paginator = self._client.get_paginator(
|
|
26
|
+
"describe_instances",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
reservations = []
|
|
30
|
+
|
|
31
|
+
for page in paginator.paginate():
|
|
32
|
+
reservations.extend(page["Reservations"])
|
|
33
|
+
|
|
34
|
+
return reservations
|
|
35
|
+
|
|
36
|
+
def get_instance_state(self, instance_id: str) -> str:
|
|
37
|
+
response = self._client.describe_instances(
|
|
38
|
+
InstanceIds=[instance_id],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
reservations = response.get("Reservations", [])
|
|
42
|
+
|
|
43
|
+
if not isinstance(reservations, list):
|
|
44
|
+
raise InvalidResourceStateError(
|
|
45
|
+
f"AWS instance has no state: {instance_id}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
for reservation in reservations:
|
|
49
|
+
if not isinstance(reservation, Mapping):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
instances = reservation.get("Instances", [])
|
|
53
|
+
|
|
54
|
+
if not isinstance(instances, list):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
for instance in instances:
|
|
58
|
+
if not isinstance(instance, Mapping):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if instance.get("InstanceId") != instance_id:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
state = instance.get("State", {})
|
|
65
|
+
|
|
66
|
+
if not isinstance(state, Mapping):
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
state_name = state.get("Name")
|
|
70
|
+
|
|
71
|
+
if isinstance(state_name, str):
|
|
72
|
+
return state_name
|
|
73
|
+
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
raise InvalidResourceStateError(
|
|
77
|
+
f"AWS instance has no state: {instance_id}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def start_instance(self, instance_id: str) -> None:
|
|
81
|
+
self._client.start_instances(
|
|
82
|
+
InstanceIds=[instance_id],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self._wait_for_state(
|
|
86
|
+
instance_id=instance_id,
|
|
87
|
+
desired_state="running",
|
|
88
|
+
transitional_state="pending",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def stop_instance(self, instance_id: str) -> None:
|
|
92
|
+
self._client.stop_instances(
|
|
93
|
+
InstanceIds=[instance_id],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self._wait_for_state(
|
|
97
|
+
instance_id=instance_id,
|
|
98
|
+
desired_state="stopped",
|
|
99
|
+
transitional_state="stopping",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _wait_for_state(
|
|
103
|
+
self,
|
|
104
|
+
instance_id: str,
|
|
105
|
+
desired_state: str,
|
|
106
|
+
transitional_state: str,
|
|
107
|
+
) -> None:
|
|
108
|
+
started_at = time.monotonic()
|
|
109
|
+
|
|
110
|
+
while True:
|
|
111
|
+
state = self.get_instance_state(instance_id)
|
|
112
|
+
|
|
113
|
+
if state == desired_state:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if state != transitional_state:
|
|
117
|
+
raise InvalidResourceStateError(
|
|
118
|
+
"AWS instance entered unexpected state: "
|
|
119
|
+
f"{instance_id}: {state}",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
time.monotonic() - started_at
|
|
124
|
+
>= self._operation_timeout
|
|
125
|
+
):
|
|
126
|
+
raise TaskTimeoutError(
|
|
127
|
+
"AWS instance operation timed out: "
|
|
128
|
+
f"{instance_id}",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
time.sleep(1.0)
|
workload_profile_controller-1.0.0/src/workload_profile_controller/backends/proxmox/__init__.py
ADDED
|
File without changes
|
|
@@ -31,14 +31,19 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
31
31
|
help="Show the current resource status.",
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
+
subparsers.add_parser(
|
|
35
|
+
"reconcile",
|
|
36
|
+
help="Detect the current profile.",
|
|
37
|
+
)
|
|
38
|
+
|
|
34
39
|
plan_parser = subparsers.add_parser(
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
"plan",
|
|
41
|
+
help="Plan a transition without changing resources.",
|
|
37
42
|
)
|
|
38
43
|
|
|
39
44
|
plan_parser.add_argument(
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
"target_profile",
|
|
46
|
+
help="Target profile to plan.",
|
|
42
47
|
)
|
|
43
48
|
|
|
44
49
|
transition_parser = subparsers.add_parser(
|
|
@@ -79,6 +84,16 @@ def main(argv=None) -> int:
|
|
|
79
84
|
|
|
80
85
|
return 0
|
|
81
86
|
|
|
87
|
+
if args.command == "reconcile":
|
|
88
|
+
controller = create_controller(args.config)
|
|
89
|
+
current_profile = controller.reconcile()
|
|
90
|
+
|
|
91
|
+
print(
|
|
92
|
+
f"Current profile: {current_profile}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return 0
|
|
96
|
+
|
|
82
97
|
if args.command == "plan":
|
|
83
98
|
controller = create_controller(args.config)
|
|
84
99
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from .config import Config
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
def validate_config(config: Config) -> None:
|
|
4
|
+
# R-11 — Supported backend
|
|
5
|
+
if config.backend not in {"proxmox", "aws"}:
|
|
6
|
+
raise ValueError(
|
|
7
|
+
f"Unsupported backend: {config.backend}"
|
|
8
|
+
)
|
|
9
|
+
|
|
5
10
|
# R-01 — At least one resource
|
|
6
11
|
if not config.resources:
|
|
7
12
|
raise ValueError("At least one resource is required")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workload-profile-controller
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Safe and configurable workload profile controller
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/lack0fcode/workload-profile-controller
|
|
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: PyYAML>=6.0
|
|
24
24
|
Requires-Dist: python-dotenv>=1.2
|
|
25
|
+
Requires-Dist: boto3>=1.35
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
28
|
Requires-Dist: ruff>=0.12; extra == "dev"
|
|
@@ -17,6 +17,14 @@ src/workload_profile_controller.egg-info/entry_points.txt
|
|
|
17
17
|
src/workload_profile_controller.egg-info/requires.txt
|
|
18
18
|
src/workload_profile_controller.egg-info/top_level.txt
|
|
19
19
|
src/workload_profile_controller/backends/__init__.py
|
|
20
|
+
src/workload_profile_controller/backends/aws/__init__.py
|
|
21
|
+
src/workload_profile_controller/backends/aws/backend.py
|
|
22
|
+
src/workload_profile_controller/backends/aws/config.py
|
|
23
|
+
src/workload_profile_controller/backends/aws/config_loader.py
|
|
24
|
+
src/workload_profile_controller/backends/aws/discovery.py
|
|
25
|
+
src/workload_profile_controller/backends/aws/factory.py
|
|
26
|
+
src/workload_profile_controller/backends/aws/inventory.py
|
|
27
|
+
src/workload_profile_controller/backends/aws/resource_client.py
|
|
20
28
|
src/workload_profile_controller/backends/proxmox/__init__.py
|
|
21
29
|
src/workload_profile_controller/backends/proxmox/auth.py
|
|
22
30
|
src/workload_profile_controller/backends/proxmox/backend.py
|
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|