primitive 0.2.57__py3-none-any.whl → 0.2.59__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/runner.py +1 -7
- primitive/client.py +4 -0
- primitive/graphql/sdk.py +1 -1
- primitive/hardware/actions.py +96 -0
- primitive/hardware/commands.py +56 -19
- primitive/hardware/graphql/mutations.py +18 -0
- primitive/jobs/graphql/fragments.py +0 -4
- primitive/messaging/__init__.py +0 -0
- primitive/messaging/provider.py +127 -0
- primitive/monitor/actions.py +2 -0
- primitive/utils/actions.py +2 -2
- primitive/utils/printer.py +1 -1
- primitive/utils/x509.py +140 -0
- {primitive-0.2.57.dist-info → primitive-0.2.59.dist-info}/METADATA +2 -1
- {primitive-0.2.57.dist-info → primitive-0.2.59.dist-info}/RECORD +19 -16
- {primitive-0.2.57.dist-info → primitive-0.2.59.dist-info}/WHEEL +0 -0
- {primitive-0.2.57.dist-info → primitive-0.2.59.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.57.dist-info → primitive-0.2.59.dist-info}/licenses/LICENSE.txt +0 -0
primitive/__about__.py
CHANGED
primitive/agent/runner.py
CHANGED
@@ -2,7 +2,7 @@ import asyncio
|
|
2
2
|
import os
|
3
3
|
import shutil
|
4
4
|
import typing
|
5
|
-
from enum import Enum
|
5
|
+
from enum import Enum
|
6
6
|
from pathlib import Path, PurePath
|
7
7
|
from typing import Dict, List, TypedDict
|
8
8
|
|
@@ -31,12 +31,6 @@ class JobConfig(TypedDict):
|
|
31
31
|
stores: List[str]
|
32
32
|
|
33
33
|
|
34
|
-
# NOTE This must match FailureLevel subclass in JobSettings model
|
35
|
-
class FailureLevel(IntEnum):
|
36
|
-
ERROR = 1
|
37
|
-
WARNING = 2
|
38
|
-
|
39
|
-
|
40
34
|
class LogLevel(Enum):
|
41
35
|
INFO = "INFO"
|
42
36
|
ERROR = "ERROR"
|
primitive/client.py
CHANGED
@@ -5,6 +5,8 @@ from loguru import logger
|
|
5
5
|
from rich.logging import RichHandler
|
6
6
|
from rich.traceback import install
|
7
7
|
|
8
|
+
from primitive.messaging.provider import MessagingProvider
|
9
|
+
|
8
10
|
from .agent.actions import Agent
|
9
11
|
from .auth.actions import Auth
|
10
12
|
from .daemons.actions import Daemons
|
@@ -80,6 +82,8 @@ class Primitive:
|
|
80
82
|
else:
|
81
83
|
self.host_config = {"username": "", "token": token, "transport": transport}
|
82
84
|
|
85
|
+
self.messaging: MessagingProvider = MessagingProvider(self)
|
86
|
+
|
83
87
|
self.auth: Auth = Auth(self)
|
84
88
|
self.organizations: Organizations = Organizations(self)
|
85
89
|
self.projects: Projects = Projects(self)
|
primitive/graphql/sdk.py
CHANGED
@@ -26,7 +26,7 @@ def create_session(
|
|
26
26
|
if fingerprint:
|
27
27
|
headers["x-primitive-fingerprint"] = fingerprint
|
28
28
|
|
29
|
-
transport = AIOHTTPTransport(url=url, headers=headers)
|
29
|
+
transport = AIOHTTPTransport(url=url, headers=headers, ssl=True)
|
30
30
|
session = Client(
|
31
31
|
transport=transport,
|
32
32
|
fetch_schema_from_transport=fetch_schema_from_transport,
|
primitive/hardware/actions.py
CHANGED
@@ -4,15 +4,18 @@ import json
|
|
4
4
|
import platform
|
5
5
|
import subprocess
|
6
6
|
import typing
|
7
|
+
from dataclasses import dataclass
|
7
8
|
from shutil import which
|
8
9
|
from typing import Dict, List, Optional
|
9
10
|
|
10
11
|
import click
|
12
|
+
import psutil
|
11
13
|
from aiohttp import client_exceptions
|
12
14
|
from gql import gql
|
13
15
|
from loguru import logger
|
14
16
|
|
15
17
|
from primitive.graphql.relay import from_base64
|
18
|
+
from primitive.messaging.provider import MESSAGE_TYPES
|
16
19
|
from primitive.utils.memory_size import MemorySize
|
17
20
|
|
18
21
|
from ..utils.auth import guard
|
@@ -24,6 +27,7 @@ from .graphql.mutations import (
|
|
24
27
|
register_child_hardware_mutation,
|
25
28
|
register_hardware_mutation,
|
26
29
|
unregister_hardware_mutation,
|
30
|
+
hardware_certificate_create_mutation,
|
27
31
|
)
|
28
32
|
from .graphql.queries import (
|
29
33
|
hardware_details,
|
@@ -31,6 +35,7 @@ from .graphql.queries import (
|
|
31
35
|
nested_children_hardware_list,
|
32
36
|
)
|
33
37
|
|
38
|
+
|
34
39
|
if typing.TYPE_CHECKING:
|
35
40
|
pass
|
36
41
|
|
@@ -40,6 +45,12 @@ from primitive.utils.actions import BaseAction
|
|
40
45
|
from primitive.utils.shell import does_executable_exist
|
41
46
|
|
42
47
|
|
48
|
+
@dataclass
|
49
|
+
class CertificateCreateResult:
|
50
|
+
certificate_id: str
|
51
|
+
certificate_pem: str
|
52
|
+
|
53
|
+
|
43
54
|
class Hardware(BaseAction):
|
44
55
|
def __init__(self, *args, **kwargs) -> None:
|
45
56
|
super().__init__(*args, **kwargs)
|
@@ -280,6 +291,48 @@ class Hardware(BaseAction):
|
|
280
291
|
system_info["gpu_config"] = self._get_gpu_config()
|
281
292
|
return system_info
|
282
293
|
|
294
|
+
@guard
|
295
|
+
def certificate_create(
|
296
|
+
self, hardware_id: str, csr_pem: str
|
297
|
+
) -> CertificateCreateResult:
|
298
|
+
mutation = gql(hardware_certificate_create_mutation)
|
299
|
+
variables = {
|
300
|
+
"input": {
|
301
|
+
"hardwareId": hardware_id,
|
302
|
+
"csrPem": csr_pem,
|
303
|
+
}
|
304
|
+
}
|
305
|
+
|
306
|
+
if not self.primitive.session:
|
307
|
+
raise Exception("No active session available for certificate creation")
|
308
|
+
|
309
|
+
result = self.primitive.session.execute(
|
310
|
+
mutation,
|
311
|
+
variable_values=variables,
|
312
|
+
get_execution_result=True,
|
313
|
+
)
|
314
|
+
|
315
|
+
if result.errors:
|
316
|
+
message = " ".join([error.message for error in result.errors])
|
317
|
+
raise Exception(message)
|
318
|
+
|
319
|
+
if not result.data:
|
320
|
+
raise Exception(
|
321
|
+
"No data received from hardware certificate creation request"
|
322
|
+
)
|
323
|
+
|
324
|
+
hardware_certificate_create = result.data["hardwareCertificateCreate"]
|
325
|
+
|
326
|
+
if hardware_certificate_create["__typename"] == "OperationInfo":
|
327
|
+
messages = hardware_certificate_create["messages"]
|
328
|
+
message = " ".join([error["message"] for error in messages])
|
329
|
+
raise Exception(message)
|
330
|
+
|
331
|
+
return CertificateCreateResult(
|
332
|
+
certificate_id=hardware_certificate_create["id"],
|
333
|
+
certificate_pem=hardware_certificate_create["certificatePem"],
|
334
|
+
)
|
335
|
+
|
283
336
|
@guard
|
284
337
|
def register(self, organization_id: Optional[str] = None):
|
285
338
|
system_info = self.get_system_info()
|
@@ -375,6 +428,24 @@ class Hardware(BaseAction):
|
|
375
428
|
|
376
429
|
return result
|
377
430
|
|
431
|
+
def check_in(
|
432
|
+
self,
|
433
|
+
is_healthy: bool = True,
|
434
|
+
is_quarantined: bool = False,
|
435
|
+
is_available: bool = False,
|
436
|
+
is_online: bool = True,
|
437
|
+
stopping_agent: Optional[bool] = False,
|
438
|
+
):
|
439
|
+
message = {
|
440
|
+
"is_healthy": is_healthy,
|
441
|
+
"is_quarantined": is_quarantined,
|
442
|
+
"is_available": is_available,
|
443
|
+
"is_online": is_online,
|
444
|
+
}
|
445
|
+
self.primitive.messaging.send_message(
|
446
|
+
message_type=MESSAGE_TYPES.CHECK_IN, message=message
|
447
|
+
)
|
448
|
+
|
378
449
|
@guard
|
379
450
|
def check_in_http(
|
380
451
|
self,
|
@@ -667,3 +738,28 @@ class Hardware(BaseAction):
|
|
667
738
|
|
668
739
|
except Exception as exception:
|
669
740
|
logger.exception(f"Error checking in children: {exception}")
|
741
|
+
|
742
|
+
def push_metrics(self):
|
743
|
+
self.primitive.messaging.send_message(
|
744
|
+
message_type=MESSAGE_TYPES.METRICS,
|
745
|
+
message=self.get_metrics(),
|
746
|
+
)
|
747
|
+
|
748
|
+
def get_metrics(self):
|
749
|
+
cpu_percent = psutil.cpu_percent(interval=1, percpu=False)
|
750
|
+
virtual_memory = psutil.virtual_memory()
|
751
|
+
disk_usage = psutil.disk_usage("/")
|
752
|
+
|
753
|
+
metrics = {
|
754
|
+
"cpu_percent": cpu_percent,
|
755
|
+
"memory_percent": virtual_memory.percent,
|
756
|
+
"memory_total": virtual_memory.total,
|
757
|
+
"memory_available": virtual_memory.available,
|
758
|
+
"memory_used": virtual_memory.used,
|
759
|
+
"memory_free": virtual_memory.free,
|
760
|
+
"disk_percent": disk_usage.percent,
|
761
|
+
"disk_total": disk_usage.total,
|
762
|
+
"disk_used": disk_usage.used,
|
763
|
+
"disk_free": disk_usage.free,
|
764
|
+
}
|
765
|
+
return metrics
|
primitive/hardware/commands.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
from time import sleep
|
1
2
|
from typing import TYPE_CHECKING, Optional
|
2
3
|
|
3
4
|
import click
|
4
|
-
|
5
|
-
from
|
6
|
-
from .ui import render_hardware_table
|
5
|
+
from loguru import logger
|
6
|
+
from primitive.utils.printer import print_result
|
7
|
+
from primitive.hardware.ui import render_hardware_table
|
8
|
+
from primitive.utils.x509 import (
|
9
|
+
generate_csr_pem,
|
10
|
+
write_certificate_pem,
|
11
|
+
)
|
7
12
|
|
8
13
|
if TYPE_CHECKING:
|
9
14
|
from ..client import Primitive
|
10
15
|
|
11
|
-
from loguru import logger
|
12
|
-
|
13
16
|
|
14
17
|
@click.group()
|
15
18
|
@click.pass_context
|
@@ -33,8 +36,17 @@ def systeminfo_command(context):
|
|
33
36
|
type=str,
|
34
37
|
help="Organization [slug] to register hardware with",
|
35
38
|
)
|
39
|
+
@click.option(
|
40
|
+
"--issue-certificate",
|
41
|
+
is_flag=True,
|
42
|
+
show_default=True,
|
43
|
+
default=False,
|
44
|
+
help="Issue certificate.",
|
45
|
+
)
|
36
46
|
@click.pass_context
|
37
|
-
def register_command(
|
47
|
+
def register_command(
|
48
|
+
context, organization: Optional[str] = None, issue_certificate: bool = False
|
49
|
+
):
|
38
50
|
"""Register Hardware with Primitive"""
|
39
51
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
40
52
|
|
@@ -47,14 +59,30 @@ def register_command(context, organization: Optional[str] = None):
|
|
47
59
|
logger.info("Registering hardware with the default organization.")
|
48
60
|
|
49
61
|
result = primitive.hardware.register(organization_id=organization_id)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
62
|
+
hardware = result.data.get("registerHardware")
|
63
|
+
|
64
|
+
if not hardware:
|
65
|
+
print_result(
|
66
|
+
fg="red",
|
67
|
+
context=context,
|
68
|
+
message="There was an error registering this device. Please review the above logs.",
|
56
69
|
)
|
57
|
-
|
70
|
+
return
|
71
|
+
|
72
|
+
if issue_certificate:
|
73
|
+
certificate = primitive.hardware.certificate_create(
|
74
|
+
hardware_id=hardware["id"],
|
75
|
+
csr_pem=generate_csr_pem(
|
76
|
+
hardware_id=hardware["pk"],
|
77
|
+
),
|
78
|
+
)
|
79
|
+
write_certificate_pem(certificate.certificate_pem)
|
80
|
+
|
81
|
+
print_result(
|
82
|
+
fg="green",
|
83
|
+
context=context,
|
84
|
+
message="Hardware registered successfully.",
|
85
|
+
)
|
58
86
|
|
59
87
|
|
60
88
|
@cli.command("unregister")
|
@@ -77,12 +105,7 @@ def unregister_command(context):
|
|
77
105
|
def checkin_command(context):
|
78
106
|
"""Checkin Hardware with Primitive"""
|
79
107
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
80
|
-
|
81
|
-
if messages := check_in_http_result.data.get("checkIn").get("messages"):
|
82
|
-
print_result(message=messages, context=context, fg="yellow")
|
83
|
-
else:
|
84
|
-
message = "Hardware checked in successfully"
|
85
|
-
print_result(message=message, context=context, fg="green")
|
108
|
+
primitive.hardware.check_in()
|
86
109
|
|
87
110
|
|
88
111
|
@cli.command("list")
|
@@ -125,3 +148,17 @@ def get_command(context, hardware_identifier: str) -> None:
|
|
125
148
|
return
|
126
149
|
else:
|
127
150
|
render_hardware_table([hardware])
|
151
|
+
|
152
|
+
|
153
|
+
@cli.command("metrics")
|
154
|
+
@click.pass_context
|
155
|
+
@click.option("--watch", is_flag=True, help="Watch hardware metrics")
|
156
|
+
def metrics_command(context, watch: bool = False) -> None:
|
157
|
+
"""Get Hardware Metrics"""
|
158
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
159
|
+
if watch:
|
160
|
+
while True:
|
161
|
+
print_result(message=primitive.hardware.get_metrics(), context=context)
|
162
|
+
sleep(1)
|
163
|
+
else:
|
164
|
+
print_result(message=primitive.hardware.get_metrics(), context=context)
|
@@ -1,11 +1,29 @@
|
|
1
1
|
from primitive.graphql.utility_fragments import operation_info_fragment
|
2
2
|
|
3
|
+
hardware_certificate_create_mutation = (
|
4
|
+
operation_info_fragment
|
5
|
+
+ """
|
6
|
+
mutation hardwareCertificateCreate($input: HardwareCertificateCreateInput!) {
|
7
|
+
hardwareCertificateCreate(input: $input) {
|
8
|
+
__typename
|
9
|
+
... on HardwareCertificate {
|
10
|
+
id
|
11
|
+
certificatePem
|
12
|
+
}
|
13
|
+
...OperationInfoFragment
|
14
|
+
}
|
15
|
+
}
|
16
|
+
"""
|
17
|
+
)
|
18
|
+
|
3
19
|
register_hardware_mutation = (
|
4
20
|
operation_info_fragment
|
5
21
|
+ """
|
6
22
|
mutation registerHardware($input: RegisterHardwareInput!) {
|
7
23
|
registerHardware(input: $input) {
|
8
24
|
... on Hardware {
|
25
|
+
id
|
26
|
+
pk
|
9
27
|
fingerprint
|
10
28
|
}
|
11
29
|
...OperationInfoFragment
|
File without changes
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import enum
|
2
|
+
import json
|
3
|
+
from datetime import datetime, timezone
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
from uuid import uuid4
|
6
|
+
import pika
|
7
|
+
from pika import credentials
|
8
|
+
from ..utils.x509 import (
|
9
|
+
are_certificate_files_present,
|
10
|
+
create_ssl_context,
|
11
|
+
read_certificate_common_name,
|
12
|
+
)
|
13
|
+
from loguru import logger
|
14
|
+
|
15
|
+
from primitive.utils.actions import BaseAction
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
import primitive.client
|
19
|
+
|
20
|
+
EXCHANGE = "hardware"
|
21
|
+
ROUTING_KEY = "hardware"
|
22
|
+
VIRTUAL_HOST = "primitive"
|
23
|
+
CELERY_TASK_NAME = "hardware.tasks.task_receive_hardware_message"
|
24
|
+
|
25
|
+
|
26
|
+
class MESSAGE_TYPES(enum.Enum):
|
27
|
+
CHECK_IN = "CHECK_IN"
|
28
|
+
METRICS = "METRICS"
|
29
|
+
|
30
|
+
|
31
|
+
class MessagingProvider(BaseAction):
|
32
|
+
def __init__(self, primitive: "primitive.client.Primitive") -> None:
|
33
|
+
super().__init__(primitive=primitive)
|
34
|
+
self.ready = False
|
35
|
+
|
36
|
+
self.fingerprint = self.primitive.host_config.get("fingerprint", None)
|
37
|
+
if not self.fingerprint:
|
38
|
+
return
|
39
|
+
self.token = self.primitive.host_config.get("token", None)
|
40
|
+
if not self.token:
|
41
|
+
return
|
42
|
+
|
43
|
+
rabbitmq_host = "rabbitmq-cluster.primitive.tech"
|
44
|
+
RABBITMQ_PORT = 443
|
45
|
+
|
46
|
+
if primitive.host == "api.dev.primitive.tech":
|
47
|
+
rabbitmq_host = "rabbitmq-cluster.dev.primitive.tech"
|
48
|
+
elif primitive.host == "api.primitive.tech":
|
49
|
+
rabbitmq_host = "rabbitmq-cluster.primitive.tech"
|
50
|
+
elif primitive.host == "api.staging.primitive.tech":
|
51
|
+
rabbitmq_host = "rabbitmq-cluster.staging.primitive.tech"
|
52
|
+
elif primitive.host == "api.test.primitive.tech":
|
53
|
+
rabbitmq_host = "rabbitmq-cluster.test.primitive.tech"
|
54
|
+
elif primitive.host == "localhost:8000":
|
55
|
+
rabbitmq_host = "localhost"
|
56
|
+
RABBITMQ_PORT = 5671
|
57
|
+
|
58
|
+
if not are_certificate_files_present():
|
59
|
+
logger.warning(
|
60
|
+
"Certificate files not present or incomplete. MessagingProvider not initialized."
|
61
|
+
)
|
62
|
+
return
|
63
|
+
|
64
|
+
ssl_context = create_ssl_context()
|
65
|
+
ssl_options = pika.SSLOptions(ssl_context)
|
66
|
+
self.common_name = read_certificate_common_name()
|
67
|
+
|
68
|
+
self.parameters = pika.ConnectionParameters(
|
69
|
+
host=rabbitmq_host,
|
70
|
+
port=RABBITMQ_PORT,
|
71
|
+
virtual_host=VIRTUAL_HOST,
|
72
|
+
ssl_options=ssl_options,
|
73
|
+
credentials=credentials.ExternalCredentials(),
|
74
|
+
)
|
75
|
+
|
76
|
+
self.ready = True
|
77
|
+
|
78
|
+
def send_message(self, message_type: MESSAGE_TYPES, message: dict[str, any]): # type: ignore
|
79
|
+
if not self.ready:
|
80
|
+
logger.warning(
|
81
|
+
"send_message: cannot send message. MessagingProvider not initialized."
|
82
|
+
)
|
83
|
+
return
|
84
|
+
|
85
|
+
body = {
|
86
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
87
|
+
"message_type": message_type.value,
|
88
|
+
"message": message,
|
89
|
+
}
|
90
|
+
|
91
|
+
full_body_for_celery = [
|
92
|
+
[],
|
93
|
+
body,
|
94
|
+
{"callbacks": None, "errbacks": None, "chain": None, "chord": None},
|
95
|
+
]
|
96
|
+
with pika.BlockingConnection(parameters=self.parameters) as conn:
|
97
|
+
message_uuid = str(uuid4())
|
98
|
+
channel = conn.channel()
|
99
|
+
|
100
|
+
headers = {
|
101
|
+
"fingerprint": self.fingerprint,
|
102
|
+
"token": self.token,
|
103
|
+
"argsrepr": "()",
|
104
|
+
"id": message_uuid,
|
105
|
+
"ignore_result": False,
|
106
|
+
"kwargsrepr": str(body),
|
107
|
+
"lang": "py",
|
108
|
+
"replaced_task_nesting": 0,
|
109
|
+
"retries": 0,
|
110
|
+
"root_id": message_uuid,
|
111
|
+
"task": CELERY_TASK_NAME,
|
112
|
+
}
|
113
|
+
|
114
|
+
channel.basic_publish(
|
115
|
+
exchange=EXCHANGE,
|
116
|
+
routing_key=ROUTING_KEY,
|
117
|
+
body=json.dumps(full_body_for_celery),
|
118
|
+
properties=pika.BasicProperties(
|
119
|
+
user_id=self.common_name,
|
120
|
+
correlation_id=message_uuid,
|
121
|
+
priority=0,
|
122
|
+
delivery_mode=2,
|
123
|
+
headers=headers,
|
124
|
+
content_encoding="utf-8",
|
125
|
+
content_type="application/json",
|
126
|
+
),
|
127
|
+
)
|
primitive/monitor/actions.py
CHANGED
@@ -67,6 +67,8 @@ class Monitor(BaseAction):
|
|
67
67
|
# handles cleanup of old reservations
|
68
68
|
# obtains an active JobRun's ID
|
69
69
|
if not RUNNING_IN_CONTAINER:
|
70
|
+
self.primitive.hardware.push_metrics()
|
71
|
+
|
70
72
|
hardware = self.primitive.hardware.get_own_hardware_details()
|
71
73
|
# fetch the latest hardware and activeReservation details
|
72
74
|
if active_reservation_data := hardware["activeReservation"]:
|
primitive/utils/actions.py
CHANGED
primitive/utils/printer.py
CHANGED
@@ -4,7 +4,7 @@ import click
|
|
4
4
|
from loguru import logger
|
5
5
|
|
6
6
|
|
7
|
-
def print_result(message: str, context: click.Context, fg: str = None):
|
7
|
+
def print_result(message: str | dict, context: click.Context, fg: str = None):
|
8
8
|
"""Print message to stdout or stderr"""
|
9
9
|
if context.obj["DEBUG"]:
|
10
10
|
logger.info(json.dumps(message))
|
primitive/utils/x509.py
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
import ssl
|
2
|
+
from pathlib import Path
|
3
|
+
from loguru import logger
|
4
|
+
|
5
|
+
from cryptography.hazmat.primitives import serialization, hashes
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
7
|
+
from cryptography import x509
|
8
|
+
from cryptography.x509.oid import NameOID
|
9
|
+
|
10
|
+
PrivateKey = ec.EllipticCurvePrivateKey
|
11
|
+
|
12
|
+
HOME_DIRECTORY = Path.home()
|
13
|
+
|
14
|
+
PRIVATE_KEY_PATH = Path(HOME_DIRECTORY / ".config" / "primitive" / "private-key.pem")
|
15
|
+
CERTIFICATE_PATH = Path(HOME_DIRECTORY / ".config" / "primitive" / "certificate.pem")
|
16
|
+
|
17
|
+
# NOTE Only used with self-signed server certificate
|
18
|
+
# THIS IS FOR LOCAL TESTING
|
19
|
+
SELF_SIGNED_SERVER_CA_PATH = Path(
|
20
|
+
HOME_DIRECTORY / ".config" / "primitive" / "server-ca.crt.pem"
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
def are_certificate_files_present() -> bool:
|
25
|
+
return PRIVATE_KEY_PATH.exists() and CERTIFICATE_PATH.exists()
|
26
|
+
|
27
|
+
|
28
|
+
def generate_private_key() -> PrivateKey:
|
29
|
+
return ec.generate_private_key(
|
30
|
+
curve=ec.SECP521R1(),
|
31
|
+
)
|
32
|
+
|
33
|
+
|
34
|
+
def read_certificate() -> x509.Certificate:
|
35
|
+
cert = x509.load_pem_x509_certificate(CERTIFICATE_PATH.read_bytes())
|
36
|
+
return cert
|
37
|
+
|
38
|
+
|
39
|
+
def read_certificate_common_name() -> str:
|
40
|
+
cert = read_certificate()
|
41
|
+
|
42
|
+
names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
43
|
+
|
44
|
+
return str(names[0].value)
|
45
|
+
|
46
|
+
|
47
|
+
def create_ssl_context() -> ssl.SSLContext:
|
48
|
+
context = ssl.create_default_context(
|
49
|
+
cafile=SELF_SIGNED_SERVER_CA_PATH
|
50
|
+
if SELF_SIGNED_SERVER_CA_PATH.exists()
|
51
|
+
else None,
|
52
|
+
purpose=ssl.Purpose.SERVER_AUTH,
|
53
|
+
)
|
54
|
+
context.load_cert_chain(
|
55
|
+
certfile=CERTIFICATE_PATH,
|
56
|
+
keyfile=PRIVATE_KEY_PATH,
|
57
|
+
)
|
58
|
+
|
59
|
+
return context
|
60
|
+
|
61
|
+
|
62
|
+
def read_private_key() -> PrivateKey:
|
63
|
+
private_key = serialization.load_pem_private_key(
|
64
|
+
data=PRIVATE_KEY_PATH.read_bytes(),
|
65
|
+
password=None,
|
66
|
+
)
|
67
|
+
|
68
|
+
if not isinstance(private_key, PrivateKey):
|
69
|
+
raise Exception(
|
70
|
+
f"Expected private key type {PrivateKey.__name__}, got {type(private_key).__name__}"
|
71
|
+
)
|
72
|
+
|
73
|
+
return private_key
|
74
|
+
|
75
|
+
|
76
|
+
def ensure_private_key() -> PrivateKey:
|
77
|
+
private_key_path = PRIVATE_KEY_PATH
|
78
|
+
|
79
|
+
if private_key_path.exists():
|
80
|
+
private_key = read_private_key()
|
81
|
+
else:
|
82
|
+
logger.info("Generating private key.")
|
83
|
+
|
84
|
+
private_key = generate_private_key()
|
85
|
+
|
86
|
+
with private_key_path.open("wb") as f:
|
87
|
+
f.write(
|
88
|
+
private_key.private_bytes(
|
89
|
+
encoding=serialization.Encoding.PEM,
|
90
|
+
format=serialization.PrivateFormat.PKCS8,
|
91
|
+
encryption_algorithm=serialization.NoEncryption(),
|
92
|
+
)
|
93
|
+
)
|
94
|
+
|
95
|
+
return private_key
|
96
|
+
|
97
|
+
|
98
|
+
def write_certificate_pem(certificate_pem: str):
|
99
|
+
CERTIFICATE_PATH.write_text(
|
100
|
+
data=certificate_pem,
|
101
|
+
encoding="utf-8",
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
def check_certificate():
|
106
|
+
try:
|
107
|
+
if CERTIFICATE_PATH.exists():
|
108
|
+
with CERTIFICATE_PATH.open("rb") as file:
|
109
|
+
private_key = read_private_key()
|
110
|
+
crt = x509.load_pem_x509_certificate(file.read())
|
111
|
+
|
112
|
+
# NOTE: Make sure certificate match private key
|
113
|
+
return crt.public_key() == private_key.public_key()
|
114
|
+
except Exception:
|
115
|
+
return False
|
116
|
+
|
117
|
+
return False
|
118
|
+
|
119
|
+
|
120
|
+
def generate_csr_pem(hardware_id: str) -> str:
|
121
|
+
builder = x509.CertificateSigningRequestBuilder()
|
122
|
+
builder = builder.subject_name(
|
123
|
+
x509.Name(
|
124
|
+
[
|
125
|
+
x509.NameAttribute(NameOID.COMMON_NAME, hardware_id),
|
126
|
+
]
|
127
|
+
)
|
128
|
+
)
|
129
|
+
builder = builder.add_extension(
|
130
|
+
x509.BasicConstraints(ca=False, path_length=None),
|
131
|
+
critical=True,
|
132
|
+
)
|
133
|
+
csr = builder.sign(
|
134
|
+
algorithm=hashes.SHA512(),
|
135
|
+
private_key=ensure_private_key(),
|
136
|
+
)
|
137
|
+
|
138
|
+
return csr.public_bytes(encoding=serialization.Encoding.PEM).decode(
|
139
|
+
encoding="utf-8", errors="strict"
|
140
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: primitive
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.59
|
4
4
|
Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
|
5
5
|
Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
|
6
6
|
Project-URL: Source, https://github.com//primitivecorp/primitive-cli
|
@@ -22,6 +22,7 @@ Requires-Dist: click
|
|
22
22
|
Requires-Dist: gql[all]
|
23
23
|
Requires-Dist: loguru
|
24
24
|
Requires-Dist: paramiko[invoke]
|
25
|
+
Requires-Dist: pika>=1.3.2
|
25
26
|
Requires-Dist: psutil>=7.0.0
|
26
27
|
Requires-Dist: rich>=13.9.4
|
27
28
|
Requires-Dist: speedtest-cli
|
@@ -1,11 +1,11 @@
|
|
1
|
-
primitive/__about__.py,sha256=
|
1
|
+
primitive/__about__.py,sha256=DQFleay9CHBMqSyleA7yap__uZps7tWqIeLhtV6rCvw,130
|
2
2
|
primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
|
3
3
|
primitive/cli.py,sha256=g7EtHI9MATAB0qQu5w-WzbXtxz_8zu8z5E7sETmMkKU,2509
|
4
|
-
primitive/client.py,sha256=
|
4
|
+
primitive/client.py,sha256=gyZIj61qMtOi_s8y0WG3gDehGT-Ms3BtbDP2J_2lajU,3753
|
5
5
|
primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
primitive/agent/actions.py,sha256=f1sSDdFHOflfGQCwNJ5P51YWUTKs8EK5YXbMAvZ-KMM,8018
|
7
7
|
primitive/agent/commands.py,sha256=o847pK7v7EWQGG67tky6a33qtwoutX6LZrP2FIS_NOk,388
|
8
|
-
primitive/agent/runner.py,sha256=
|
8
|
+
primitive/agent/runner.py,sha256=vymeuIDiah6J9SHjTbsNQGi74PpnJw1MxWPd3TWjc-w,11512
|
9
9
|
primitive/agent/uploader.py,sha256=DT_Nzt5eOTm_uRcYKm1sjBBaQZzp5iNZ_uN5XktfQ30,3382
|
10
10
|
primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
primitive/auth/actions.py,sha256=9NIEXJ1BNJutJs6AMMSjMN_ziONUAUhY_xHwojYJCLA,942
|
@@ -36,25 +36,27 @@ primitive/git/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
36
36
|
primitive/git/graphql/queries.py,sha256=I1HGlqBb1lHIAWVSsC8tVY9JdsQ8DJVqs4nqSTcL30M,98
|
37
37
|
primitive/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
38
38
|
primitive/graphql/relay.py,sha256=bmij2AjdpURQ6GGVCxwWhauF-r_SxuAU2oJ4sDbLxpI,726
|
39
|
-
primitive/graphql/sdk.py,sha256=
|
39
|
+
primitive/graphql/sdk.py,sha256=dE4TD8KiTKw3Y0uiw5XrIcuZGqexE47eSlPaPD6jDGo,1545
|
40
40
|
primitive/graphql/utility_fragments.py,sha256=uIjwILC4QtWNyO5vu77VjQf_p0jvP3A9q_6zRq91zqs,303
|
41
41
|
primitive/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
42
|
-
primitive/hardware/actions.py,sha256=
|
42
|
+
primitive/hardware/actions.py,sha256=qDbwd8W6zMlEfp1IfxTWiyAFqV5iYomDisGVXTRfy8Y,29145
|
43
43
|
primitive/hardware/android.py,sha256=tu7pBPxWFrIwb_mm5CEdFFf1_veNDOKjOCQg13i_Lh4,2758
|
44
|
-
primitive/hardware/commands.py,sha256=
|
44
|
+
primitive/hardware/commands.py,sha256=lwO-cm3doGtuGvR30fib-nALJyNCScdCTXOVVbYVpkY,4604
|
45
45
|
primitive/hardware/ui.py,sha256=12rucuZ2s-w5R4bKyxON5dEbrdDnVf5sbj3K_nbdo44,2473
|
46
46
|
primitive/hardware/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
47
|
primitive/hardware/graphql/fragments.py,sha256=H315uv-ujOsZEbQR5tmstbRU_9OCa5SErZMwIckSiLc,383
|
48
|
-
primitive/hardware/graphql/mutations.py,sha256=
|
48
|
+
primitive/hardware/graphql/mutations.py,sha256=wOKNJN-1x_DGxvdHt2Bm3AUqyW8P1lNPkxLIQThbXUg,1874
|
49
49
|
primitive/hardware/graphql/queries.py,sha256=I86uLuOSjHSph11Y5MVCYko5Js7hoiEZ-cEoPTc4J-k,1392
|
50
50
|
primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
51
|
primitive/jobs/actions.py,sha256=Fej-rpdfIiHjs0SEFY2ZcMn64AWHn6kD90RC5wtRM3Q,6531
|
52
52
|
primitive/jobs/commands.py,sha256=MxPCkBEYW_eLNqgCRYeyj7ZcLOFAWfpVZlqDR2Y_S0o,830
|
53
53
|
primitive/jobs/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
|
-
primitive/jobs/graphql/fragments.py,sha256=
|
54
|
+
primitive/jobs/graphql/fragments.py,sha256=rWZWxZs9ZWjWal0eiY_XPWUS7i7f3fwSM_w8ZvDs2JQ,614
|
55
55
|
primitive/jobs/graphql/mutations.py,sha256=8ASvCmwQh7cMeeiykOdYaYVryG8FRIuVF6v_J8JJZuw,219
|
56
56
|
primitive/jobs/graphql/queries.py,sha256=ZxNmm-WovytbggNuKRnwa0kc26T34_0yhqkoqx-2uj0,1736
|
57
|
-
primitive/
|
57
|
+
primitive/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
|
+
primitive/messaging/provider.py,sha256=NCgl1KiV4Ow49Laz2grNGsP7Ij4vUUeorUjkhAKF1H4,4182
|
59
|
+
primitive/monitor/actions.py,sha256=vdjfA-3v1j4g-S-gs6mziE3pDf3eR6dtYtgEVRnSfBQ,9641
|
58
60
|
primitive/monitor/commands.py,sha256=VDlEL_Qpm_ysHxug7VpI0cVAZ0ny6AS91Y58D7F1zkU,409
|
59
61
|
primitive/organizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
62
|
primitive/organizations/actions.py,sha256=kVHOhG1oS2sI5p8uldSo5L-RUZsnG36eaulVuKLyZ-M,1863
|
@@ -82,7 +84,7 @@ primitive/reservations/graphql/fragments.py,sha256=h3edd0Es38SNldhHiRU3hAJNYwrUI
|
|
82
84
|
primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
|
83
85
|
primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
|
84
86
|
primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
85
|
-
primitive/utils/actions.py,sha256=
|
87
|
+
primitive/utils/actions.py,sha256=Gm84YgXFSpilpYqjG5nxcQTdRSp3waN7ZorzIzMBLtc,208
|
86
88
|
primitive/utils/auth.py,sha256=uBIZNPF2CpbaPV2UMi6eWVUKghV6WIm-pG3-UM29bNs,1465
|
87
89
|
primitive/utils/cache.py,sha256=FHGmVWYLJFQOazpXXcEwI0YJEZbdkgG39nOLdOv6VNk,1575
|
88
90
|
primitive/utils/chunk_size.py,sha256=PAuVuirUTA9oRXyjo1c6MWxo31WVBRkWMuWw-AS58Bw,2914
|
@@ -91,12 +93,13 @@ primitive/utils/daemons.py,sha256=mSoSHitiGfS4KYAEK9sKsiv_YcACHKgY3qISnDpUUIE,10
|
|
91
93
|
primitive/utils/exceptions.py,sha256=KSAR2zqWEjQhqb6OAtaMUvYejKPETdBVRRRIKzy90A8,554
|
92
94
|
primitive/utils/logging.py,sha256=vpwu-hByZC1BgJfUi6iSfAxzCobP_zg9-99EUf80KtY,1132
|
93
95
|
primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
|
94
|
-
primitive/utils/printer.py,sha256=
|
96
|
+
primitive/utils/printer.py,sha256=Iy9RkTyDZ6CcoTZfiNTPMAAT2rM8U0Reku4RjQf_DqM,377
|
95
97
|
primitive/utils/psutil.py,sha256=xa7ef435UL37jyjmUPbEqCO2ayQMpCs0HCrxVEvLcuM,763
|
96
98
|
primitive/utils/shell.py,sha256=Z4zxmOaSyGCrS0D6I436iQci-ewHLt4UxVg1CD9Serc,2171
|
97
99
|
primitive/utils/text.py,sha256=XiESMnlhjQ534xE2hMNf08WehE1SKaYFRNih0MmnK0k,829
|
98
|
-
primitive
|
99
|
-
primitive-0.2.
|
100
|
-
primitive-0.2.
|
101
|
-
primitive-0.2.
|
102
|
-
primitive-0.2.
|
100
|
+
primitive/utils/x509.py,sha256=HwHRPqakTHWd40ny-9O_yNknSL1Cxo50O0UCjXHFq04,3796
|
101
|
+
primitive-0.2.59.dist-info/METADATA,sha256=TK2BJRsrYXbcffM-S0oA9sBT03PjeiLEeDBwlaH1m6s,3540
|
102
|
+
primitive-0.2.59.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
103
|
+
primitive-0.2.59.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
|
104
|
+
primitive-0.2.59.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
|
105
|
+
primitive-0.2.59.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|