primitive 0.2.58__py3-none-any.whl → 0.2.60__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/client.py +4 -0
- primitive/files/actions.py +73 -7
- primitive/files/commands.py +59 -2
- primitive/files/graphql/fragments.py +5 -2
- primitive/files/ui.py +37 -0
- primitive/graphql/sdk.py +1 -1
- primitive/hardware/actions.py +97 -0
- primitive/hardware/commands.py +56 -19
- primitive/hardware/graphql/mutations.py +18 -0
- primitive/jobs/actions.py +1 -0
- 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.58.dist-info → primitive-0.2.60.dist-info}/METADATA +2 -1
- {primitive-0.2.58.dist-info → primitive-0.2.60.dist-info}/RECORD +22 -18
- {primitive-0.2.58.dist-info → primitive-0.2.60.dist-info}/WHEEL +0 -0
- {primitive-0.2.58.dist-info → primitive-0.2.60.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.58.dist-info → primitive-0.2.60.dist-info}/licenses/LICENSE.txt +0 -0
primitive/__about__.py
CHANGED
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/files/actions.py
CHANGED
@@ -124,15 +124,39 @@ class Files(BaseAction):
|
|
124
124
|
return result
|
125
125
|
|
126
126
|
@guard
|
127
|
-
def
|
127
|
+
def files(
|
128
|
+
self,
|
129
|
+
file_id: Optional[str] = None,
|
130
|
+
file_name: Optional[str] = None,
|
131
|
+
organization_id: Optional[str] = None,
|
132
|
+
organization_slug: Optional[str] = None,
|
133
|
+
):
|
128
134
|
query = gql(files_list)
|
129
135
|
|
130
136
|
filters = {}
|
137
|
+
if not organization_id and not organization_slug:
|
138
|
+
whoami_result = self.primitive.auth.whoami()
|
139
|
+
default_organization = whoami_result.data["whoami"]["defaultOrganization"]
|
140
|
+
organization_id = default_organization["id"]
|
141
|
+
logger.info(
|
142
|
+
f"Using default organization ID: {default_organization.get('slug')} ({organization_id})"
|
143
|
+
)
|
144
|
+
if organization_slug and not organization_id:
|
145
|
+
organization = self.primitive.organizations.get_organization(
|
146
|
+
slug=organization_slug
|
147
|
+
)
|
148
|
+
organization_id = organization.get("id")
|
149
|
+
|
150
|
+
if organization_id:
|
151
|
+
filters["organization"] = {"id": organization_id}
|
152
|
+
|
131
153
|
if file_id:
|
132
|
-
filters["id"] =
|
154
|
+
filters["id"] = file_id
|
155
|
+
if file_name:
|
156
|
+
filters["fileName"] = {"exact": file_name}
|
133
157
|
|
134
158
|
variables = {
|
135
|
-
"first":
|
159
|
+
"first": 25,
|
136
160
|
"filters": filters,
|
137
161
|
}
|
138
162
|
result = self.primitive.session.execute(
|
@@ -242,9 +266,9 @@ class Files(BaseAction):
|
|
242
266
|
file_id = pending_file_create.get("id")
|
243
267
|
parts_details = pending_file_create.get("partsDetails")
|
244
268
|
else:
|
245
|
-
|
269
|
+
file_result = self.files(file_id=file_id)
|
246
270
|
parts_details = (
|
247
|
-
|
271
|
+
file_result.data.get("files")
|
248
272
|
.get("edges")[0]
|
249
273
|
.get("node")
|
250
274
|
.get("partsDetails")
|
@@ -340,14 +364,56 @@ class Files(BaseAction):
|
|
340
364
|
"fileObject": (path.name, open(path, "rb")),
|
341
365
|
}
|
342
366
|
|
343
|
-
session = create_requests_session(self.primitive.host_config)
|
367
|
+
session = create_requests_session(host_config=self.primitive.host_config)
|
344
368
|
transport = self.primitive.host_config.get("transport")
|
345
369
|
url = f"{transport}://{self.primitive.host}/"
|
346
370
|
response = session.post(url, files=body)
|
347
371
|
return response
|
348
372
|
|
349
|
-
def get_presigned_url(self, file_pk: str):
|
373
|
+
def get_presigned_url(self, file_pk: str) -> str:
|
350
374
|
transport = self.primitive.host_config.get("transport")
|
351
375
|
host = self.primitive.host_config.get("host")
|
352
376
|
file_access_url = f"{transport}://{host}/files/{file_pk}/presigned-url/"
|
353
377
|
return file_access_url
|
378
|
+
|
379
|
+
def download_file(
|
380
|
+
self,
|
381
|
+
file_name: str = "",
|
382
|
+
file_id: str = "",
|
383
|
+
organization_id: str = "",
|
384
|
+
organization_slug: str = "",
|
385
|
+
output_path: Path = Path().cwd(),
|
386
|
+
) -> Path:
|
387
|
+
file_pk = None
|
388
|
+
|
389
|
+
files_result = self.primitive.files.files(
|
390
|
+
file_id=file_id,
|
391
|
+
file_name=file_name,
|
392
|
+
organization_id=organization_id,
|
393
|
+
organization_slug=organization_slug,
|
394
|
+
)
|
395
|
+
if files_data := files_result.data:
|
396
|
+
file = files_data["files"]["edges"][0]["node"]
|
397
|
+
file_pk = file["pk"]
|
398
|
+
file_name = file["fileName"]
|
399
|
+
|
400
|
+
if not file_pk:
|
401
|
+
raise Exception(
|
402
|
+
"File not found on remote server. Please check file name or file id"
|
403
|
+
)
|
404
|
+
|
405
|
+
session = create_requests_session(host_config=self.primitive.host_config)
|
406
|
+
transport = self.primitive.host_config.get("transport")
|
407
|
+
url = f"{transport}://{self.primitive.host}/files/{file_pk}/stream/"
|
408
|
+
|
409
|
+
downloaded_file = output_path / file_name
|
410
|
+
|
411
|
+
with session.get(url, stream=True) as response:
|
412
|
+
response.raise_for_status()
|
413
|
+
with open(downloaded_file, "wb") as partial_downloaded_file:
|
414
|
+
for chunk in response.iter_content(chunk_size=8192):
|
415
|
+
if chunk:
|
416
|
+
partial_downloaded_file.write(chunk)
|
417
|
+
partial_downloaded_file.flush()
|
418
|
+
|
419
|
+
return downloaded_file
|
primitive/files/commands.py
CHANGED
@@ -4,6 +4,8 @@ from pathlib import Path
|
|
4
4
|
|
5
5
|
import click
|
6
6
|
|
7
|
+
from primitive.files.ui import render_files_table
|
8
|
+
|
7
9
|
from ..utils.printer import print_result
|
8
10
|
|
9
11
|
if typing.TYPE_CHECKING:
|
@@ -22,7 +24,7 @@ def cli(context):
|
|
22
24
|
@click.argument("path", type=click.Path(exists=True))
|
23
25
|
@click.option("--public", "-p", help="Is this a Public file", is_flag=True)
|
24
26
|
@click.option("--key-prefix", "-k", help="Key Prefix", default="")
|
25
|
-
@click.option("--direct",
|
27
|
+
@click.option("--direct", help="direct", is_flag=True)
|
26
28
|
def file_upload_command(context, path, public, key_prefix, direct):
|
27
29
|
"""File Upload"""
|
28
30
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
@@ -36,8 +38,63 @@ def file_upload_command(context, path, public, key_prefix, direct):
|
|
36
38
|
path, is_public=public, key_prefix=key_prefix
|
37
39
|
)
|
38
40
|
try:
|
39
|
-
message = json.dumps(result.
|
41
|
+
message = json.dumps(result.json())
|
40
42
|
except AttributeError:
|
41
43
|
message = "File Upload Failed"
|
42
44
|
|
43
45
|
print_result(message=message, context=context)
|
46
|
+
|
47
|
+
|
48
|
+
@cli.command("download")
|
49
|
+
@click.pass_context
|
50
|
+
@click.option("--file-id", help="File ID", required=False)
|
51
|
+
@click.option("--file-name", help="File Name", required=False)
|
52
|
+
@click.option("--output", help="Output Path", required=False, type=click.Path())
|
53
|
+
@click.option("--organization-id", help="Organization ID", required=False)
|
54
|
+
@click.option("--organization", help="Organization Slug", required=False)
|
55
|
+
def file_download_command(
|
56
|
+
context,
|
57
|
+
file_id=None,
|
58
|
+
file_name=None,
|
59
|
+
output=None,
|
60
|
+
organization_id=None,
|
61
|
+
organization=None,
|
62
|
+
):
|
63
|
+
"""File Download"""
|
64
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
65
|
+
|
66
|
+
if not file_id and not file_name:
|
67
|
+
raise click.UsageError("Either --id or --file-name must be provided.")
|
68
|
+
|
69
|
+
if not output:
|
70
|
+
output = Path().cwd()
|
71
|
+
else:
|
72
|
+
output = Path(output)
|
73
|
+
|
74
|
+
downloaded_file = primitive.files.download_file(
|
75
|
+
output_path=output,
|
76
|
+
file_id=file_id,
|
77
|
+
file_name=file_name,
|
78
|
+
organization_id=organization_id,
|
79
|
+
organization_slug=organization,
|
80
|
+
)
|
81
|
+
print_result(message=f"File downloaded to {downloaded_file}", context=context)
|
82
|
+
|
83
|
+
|
84
|
+
@cli.command("list")
|
85
|
+
@click.pass_context
|
86
|
+
@click.option("--organization-id", help="Organization ID", required=False)
|
87
|
+
@click.option("--organization", help="Organization Slug", required=False)
|
88
|
+
def list_command(context, organization_id=None, organization=None):
|
89
|
+
"""List Files"""
|
90
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
91
|
+
files_result = primitive.files.files(
|
92
|
+
organization_id=organization_id, organization_slug=organization
|
93
|
+
)
|
94
|
+
|
95
|
+
files = [file.get("node") for file in files_result.data.get("files").get("edges")]
|
96
|
+
|
97
|
+
if context.obj["JSON"]:
|
98
|
+
print_result(message=files, context=context)
|
99
|
+
else:
|
100
|
+
render_files_table(files)
|
@@ -4,7 +4,11 @@ fragment FileFragment on File {
|
|
4
4
|
pk
|
5
5
|
createdAt
|
6
6
|
updatedAt
|
7
|
-
createdBy
|
7
|
+
createdBy {
|
8
|
+
id
|
9
|
+
pk
|
10
|
+
username
|
11
|
+
}
|
8
12
|
location
|
9
13
|
fileName
|
10
14
|
fileSize
|
@@ -13,6 +17,5 @@ fragment FileFragment on File {
|
|
13
17
|
isComplete
|
14
18
|
partsDetails
|
15
19
|
humanReadableMemorySize
|
16
|
-
contents
|
17
20
|
}
|
18
21
|
"""
|
primitive/files/ui.py
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.table import Table
|
3
|
+
|
4
|
+
|
5
|
+
def render_files_table(file_list) -> None:
|
6
|
+
console = Console()
|
7
|
+
|
8
|
+
table = Table(show_header=True, header_style="bold #FFA800")
|
9
|
+
table.add_column("File Name")
|
10
|
+
table.add_column("File ID")
|
11
|
+
table.add_column("File Size (bytes)", justify="right")
|
12
|
+
|
13
|
+
for file in file_list:
|
14
|
+
file_name = file.get("fileName")
|
15
|
+
file_id = file.get("id")
|
16
|
+
file_size = file.get("fileSize")
|
17
|
+
|
18
|
+
table.add_row(
|
19
|
+
file_name,
|
20
|
+
file_id,
|
21
|
+
file_size,
|
22
|
+
)
|
23
|
+
|
24
|
+
console.print(table)
|
25
|
+
|
26
|
+
|
27
|
+
def file_status_string(file) -> str:
|
28
|
+
if file.get("isQuarantined"):
|
29
|
+
return "Quarantined"
|
30
|
+
if not file.get("isOnline"):
|
31
|
+
return "Offline"
|
32
|
+
if not file.get("isHealthy"):
|
33
|
+
return "Not healthy"
|
34
|
+
if not file.get("isAvailable"):
|
35
|
+
return "Not available"
|
36
|
+
else:
|
37
|
+
return "Available"
|
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,29 @@ 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
|
+
if self.primitive.messaging.ready:
|
744
|
+
self.primitive.messaging.send_message(
|
745
|
+
message_type=MESSAGE_TYPES.METRICS,
|
746
|
+
message=self.get_metrics(),
|
747
|
+
)
|
748
|
+
|
749
|
+
def get_metrics(self):
|
750
|
+
cpu_percent = psutil.cpu_percent(interval=1, percpu=False)
|
751
|
+
virtual_memory = psutil.virtual_memory()
|
752
|
+
disk_usage = psutil.disk_usage("/")
|
753
|
+
|
754
|
+
metrics = {
|
755
|
+
"cpu_percent": cpu_percent,
|
756
|
+
"memory_percent": virtual_memory.percent,
|
757
|
+
"memory_total": virtual_memory.total,
|
758
|
+
"memory_available": virtual_memory.available,
|
759
|
+
"memory_used": virtual_memory.used,
|
760
|
+
"memory_free": virtual_memory.free,
|
761
|
+
"disk_percent": disk_usage.percent,
|
762
|
+
"disk_total": disk_usage.total,
|
763
|
+
"disk_used": disk_usage.used,
|
764
|
+
"disk_free": disk_usage.free,
|
765
|
+
}
|
766
|
+
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
|
primitive/jobs/actions.py
CHANGED
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.60
|
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,7 +1,7 @@
|
|
1
|
-
primitive/__about__.py,sha256=
|
1
|
+
primitive/__about__.py,sha256=PQj7AICmnMMGLJ9kvGOHRMY1k1xLtWYcSN-xiyaE_qY,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
|
@@ -23,10 +23,11 @@ primitive/exec/actions.py,sha256=4d_TCjNDcVFoZ9Zw7ZuBa6hKMv2Xzm7_UX_8wcX1aSk,412
|
|
23
23
|
primitive/exec/commands.py,sha256=66LO2kkJC-ynNZQpUCXv4Ol15QoacdSZAHblePDcmLo,510
|
24
24
|
primitive/exec/interactive.py,sha256=TscY6s2ZysijidKPheq6y-fCErUVLS0zcdTW8XyFWGI,2435
|
25
25
|
primitive/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
-
primitive/files/actions.py,sha256=
|
27
|
-
primitive/files/commands.py,sha256
|
26
|
+
primitive/files/actions.py,sha256=tuf5PXgNrcxegfzC-CZ6r3iWBkQV9gxZpALU-hjyGpY,14865
|
27
|
+
primitive/files/commands.py,sha256=-U0ArpZXDWltmkELG2SYIOLbaiMC6Zv24X9zZ29aqCE,3021
|
28
|
+
primitive/files/ui.py,sha256=lYnfu6gnZ5f-C28wps_egVDz8tdjL5P4361uxpSyfvY,919
|
28
29
|
primitive/files/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
-
primitive/files/graphql/fragments.py,sha256=
|
30
|
+
primitive/files/graphql/fragments.py,sha256=h_Gfi1a3o1_tTJfvW_HUvWmeo3VSSMcj38ObcUv7X7c,253
|
30
31
|
primitive/files/graphql/mutations.py,sha256=Da_e6WSp-fsCYVE9A6SGkIQy9WDzjeQycNyHEn7vJqE,935
|
31
32
|
primitive/files/graphql/queries.py,sha256=_ky-IRz928sKeSJuqaggTPxV4CGgmho3OyaAFu1z7nw,397
|
32
33
|
primitive/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -36,25 +37,27 @@ primitive/git/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
36
37
|
primitive/git/graphql/queries.py,sha256=I1HGlqBb1lHIAWVSsC8tVY9JdsQ8DJVqs4nqSTcL30M,98
|
37
38
|
primitive/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
38
39
|
primitive/graphql/relay.py,sha256=bmij2AjdpURQ6GGVCxwWhauF-r_SxuAU2oJ4sDbLxpI,726
|
39
|
-
primitive/graphql/sdk.py,sha256=
|
40
|
+
primitive/graphql/sdk.py,sha256=dE4TD8KiTKw3Y0uiw5XrIcuZGqexE47eSlPaPD6jDGo,1545
|
40
41
|
primitive/graphql/utility_fragments.py,sha256=uIjwILC4QtWNyO5vu77VjQf_p0jvP3A9q_6zRq91zqs,303
|
41
42
|
primitive/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
42
|
-
primitive/hardware/actions.py,sha256=
|
43
|
+
primitive/hardware/actions.py,sha256=4xswcLaHk4IhEwRK5Pw_rpEgn6XciT5K2apnHi8_cI4,29204
|
43
44
|
primitive/hardware/android.py,sha256=tu7pBPxWFrIwb_mm5CEdFFf1_veNDOKjOCQg13i_Lh4,2758
|
44
|
-
primitive/hardware/commands.py,sha256=
|
45
|
+
primitive/hardware/commands.py,sha256=lwO-cm3doGtuGvR30fib-nALJyNCScdCTXOVVbYVpkY,4604
|
45
46
|
primitive/hardware/ui.py,sha256=12rucuZ2s-w5R4bKyxON5dEbrdDnVf5sbj3K_nbdo44,2473
|
46
47
|
primitive/hardware/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
48
|
primitive/hardware/graphql/fragments.py,sha256=H315uv-ujOsZEbQR5tmstbRU_9OCa5SErZMwIckSiLc,383
|
48
|
-
primitive/hardware/graphql/mutations.py,sha256=
|
49
|
+
primitive/hardware/graphql/mutations.py,sha256=wOKNJN-1x_DGxvdHt2Bm3AUqyW8P1lNPkxLIQThbXUg,1874
|
49
50
|
primitive/hardware/graphql/queries.py,sha256=I86uLuOSjHSph11Y5MVCYko5Js7hoiEZ-cEoPTc4J-k,1392
|
50
51
|
primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
|
-
primitive/jobs/actions.py,sha256
|
52
|
+
primitive/jobs/actions.py,sha256=-IRNO7KM63cPAbGRNQycyFsugW9hAyg-TDuK0g5AiSw,6542
|
52
53
|
primitive/jobs/commands.py,sha256=MxPCkBEYW_eLNqgCRYeyj7ZcLOFAWfpVZlqDR2Y_S0o,830
|
53
54
|
primitive/jobs/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
55
|
primitive/jobs/graphql/fragments.py,sha256=rWZWxZs9ZWjWal0eiY_XPWUS7i7f3fwSM_w8ZvDs2JQ,614
|
55
56
|
primitive/jobs/graphql/mutations.py,sha256=8ASvCmwQh7cMeeiykOdYaYVryG8FRIuVF6v_J8JJZuw,219
|
56
57
|
primitive/jobs/graphql/queries.py,sha256=ZxNmm-WovytbggNuKRnwa0kc26T34_0yhqkoqx-2uj0,1736
|
57
|
-
primitive/
|
58
|
+
primitive/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
|
+
primitive/messaging/provider.py,sha256=NCgl1KiV4Ow49Laz2grNGsP7Ij4vUUeorUjkhAKF1H4,4182
|
60
|
+
primitive/monitor/actions.py,sha256=AZOoNzASorvye5309h6BJbsup_jR8YunQ19EiG5SAQg,9643
|
58
61
|
primitive/monitor/commands.py,sha256=VDlEL_Qpm_ysHxug7VpI0cVAZ0ny6AS91Y58D7F1zkU,409
|
59
62
|
primitive/organizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
63
|
primitive/organizations/actions.py,sha256=kVHOhG1oS2sI5p8uldSo5L-RUZsnG36eaulVuKLyZ-M,1863
|
@@ -82,7 +85,7 @@ primitive/reservations/graphql/fragments.py,sha256=h3edd0Es38SNldhHiRU3hAJNYwrUI
|
|
82
85
|
primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
|
83
86
|
primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
|
84
87
|
primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
85
|
-
primitive/utils/actions.py,sha256=
|
88
|
+
primitive/utils/actions.py,sha256=Gm84YgXFSpilpYqjG5nxcQTdRSp3waN7ZorzIzMBLtc,208
|
86
89
|
primitive/utils/auth.py,sha256=uBIZNPF2CpbaPV2UMi6eWVUKghV6WIm-pG3-UM29bNs,1465
|
87
90
|
primitive/utils/cache.py,sha256=FHGmVWYLJFQOazpXXcEwI0YJEZbdkgG39nOLdOv6VNk,1575
|
88
91
|
primitive/utils/chunk_size.py,sha256=PAuVuirUTA9oRXyjo1c6MWxo31WVBRkWMuWw-AS58Bw,2914
|
@@ -91,12 +94,13 @@ primitive/utils/daemons.py,sha256=mSoSHitiGfS4KYAEK9sKsiv_YcACHKgY3qISnDpUUIE,10
|
|
91
94
|
primitive/utils/exceptions.py,sha256=KSAR2zqWEjQhqb6OAtaMUvYejKPETdBVRRRIKzy90A8,554
|
92
95
|
primitive/utils/logging.py,sha256=vpwu-hByZC1BgJfUi6iSfAxzCobP_zg9-99EUf80KtY,1132
|
93
96
|
primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
|
94
|
-
primitive/utils/printer.py,sha256=
|
97
|
+
primitive/utils/printer.py,sha256=Iy9RkTyDZ6CcoTZfiNTPMAAT2rM8U0Reku4RjQf_DqM,377
|
95
98
|
primitive/utils/psutil.py,sha256=xa7ef435UL37jyjmUPbEqCO2ayQMpCs0HCrxVEvLcuM,763
|
96
99
|
primitive/utils/shell.py,sha256=Z4zxmOaSyGCrS0D6I436iQci-ewHLt4UxVg1CD9Serc,2171
|
97
100
|
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.
|
101
|
+
primitive/utils/x509.py,sha256=HwHRPqakTHWd40ny-9O_yNknSL1Cxo50O0UCjXHFq04,3796
|
102
|
+
primitive-0.2.60.dist-info/METADATA,sha256=8_ftdlVATEzZHXucRmr6BnzxWhjjCPNhGju0EnVFX9c,3540
|
103
|
+
primitive-0.2.60.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
104
|
+
primitive-0.2.60.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
|
105
|
+
primitive-0.2.60.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
|
106
|
+
primitive-0.2.60.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|