primitive 0.2.68__py3-none-any.whl → 0.2.72__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.

Potentially problematic release.


This version of primitive might be problematic. Click here for more details.

@@ -0,0 +1,473 @@
1
+ from enum import Enum
2
+
3
+ from gql import gql
4
+ import requests
5
+
6
+ from primitive.operating_systems.graphql.mutations import (
7
+ operating_system_create_mutation,
8
+ )
9
+ from primitive.operating_systems.graphql.queries import operating_system_list_query
10
+ from primitive.utils.actions import BaseAction
11
+ from primitive.utils.auth import guard
12
+
13
+ from primitive.utils.cache import get_operating_systems_cache
14
+ from pathlib import Path
15
+ from urllib.request import urlopen
16
+ import os
17
+ from loguru import logger
18
+
19
+ from primitive.utils.checksums import get_checksum_from_file, calculate_sha256
20
+ from primitive.utils.text import slugify
21
+
22
+
23
+ class OperatingSystems(BaseAction):
24
+ def __init__(self, primitive):
25
+ super().__init__(primitive)
26
+ self.operating_systems_key_prefix = "operating-systems"
27
+ self.remote_operating_systems = {
28
+ "ubuntu-24-04-3": {
29
+ "slug": "ubuntu-24-04-3",
30
+ "iso": "https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-desktop-amd64.iso",
31
+ "checksum": "https://releases.ubuntu.com/24.04.3/SHA256SUMS",
32
+ "checksum_file_type": self.OperatingSystemChecksumFileType.SHA256SUMS,
33
+ },
34
+ }
35
+
36
+ class OperatingSystemChecksumFileType(Enum):
37
+ SHA256SUMS = "SHA256SUMS"
38
+
39
+ def list_remotes(self):
40
+ return self.remote_operating_systems.values()
41
+
42
+ def get_remote_info(self, slug: str):
43
+ return self.remote_operating_systems[slug]
44
+
45
+ def _download_remote_operating_system_iso(
46
+ self, remote_operating_system_name: str, directory: str | None = None
47
+ ):
48
+ cache_dir = Path(directory) if directory else get_operating_systems_cache()
49
+ operating_system_dir = Path(cache_dir / remote_operating_system_name)
50
+ iso_dir = Path(operating_system_dir / "iso")
51
+ os.makedirs(iso_dir, exist_ok=True)
52
+
53
+ operating_system_info = self.remote_operating_systems[
54
+ remote_operating_system_name
55
+ ]
56
+ iso_remote_url = operating_system_info["iso"]
57
+ iso_filename = iso_remote_url.split("/")[-1]
58
+ iso_file_path = Path(iso_dir / iso_filename)
59
+
60
+ if iso_file_path.exists() and iso_file_path.is_file():
61
+ logger.info("Operating system iso already downloaded.")
62
+ return iso_file_path
63
+
64
+ logger.info(
65
+ f"Downloading operating system '{remote_operating_system_name}' iso. This may take a few minutes..."
66
+ )
67
+
68
+ session = requests.Session()
69
+ with session.get(iso_remote_url, stream=True) as response:
70
+ response.raise_for_status()
71
+ with open(iso_file_path, "wb") as f:
72
+ for chunk in response.iter_content(chunk_size=8192):
73
+ if chunk:
74
+ f.write(chunk)
75
+ f.flush()
76
+
77
+ logger.info(
78
+ f"Successfully downloaded operating system iso to '{iso_file_path}'."
79
+ )
80
+
81
+ return iso_file_path
82
+
83
+ def _download_remote_operating_system_checksum(
84
+ self, remote_operating_system_name: str, directory: str | None = None
85
+ ):
86
+ cache_dir = Path(directory) if directory else get_operating_systems_cache()
87
+ operating_system_dir = Path(cache_dir / remote_operating_system_name)
88
+ checksum_dir = Path(operating_system_dir / "checksum")
89
+ os.makedirs(checksum_dir, exist_ok=True)
90
+
91
+ operating_system_info = self.remote_operating_systems[
92
+ remote_operating_system_name
93
+ ]
94
+ checksum_filename = operating_system_info["checksum"].split("/")[-1]
95
+
96
+ checksum_file_path = Path(checksum_dir / checksum_filename)
97
+ if checksum_file_path.exists() and checksum_file_path.is_file():
98
+ logger.info("Operating system checksum already downloaded.")
99
+ return checksum_file_path
100
+
101
+ logger.info(
102
+ f"Downloading operating system '{remote_operating_system_name}' checksum."
103
+ )
104
+
105
+ checksum_response = urlopen(operating_system_info["checksum"])
106
+ checksum_file_content = checksum_response.read()
107
+ with open(checksum_file_path, "wb") as f:
108
+ f.write(checksum_file_content)
109
+
110
+ logger.info(f"Successfully downloaded checksum to '{checksum_file_path}'.")
111
+
112
+ return checksum_file_path
113
+
114
+ def download_remote(
115
+ self, remote_operating_system_name: str, directory: str | None = None
116
+ ):
117
+ remote_operating_system_names = list(self.remote_operating_systems.keys())
118
+
119
+ if remote_operating_system_name not in remote_operating_system_names:
120
+ logger.error(
121
+ f"No such operating system '{remote_operating_system_name}'. Run 'primitive operating-systems list' for available operating systems."
122
+ )
123
+ raise ValueError(
124
+ f"No such operating system '{remote_operating_system_name}'."
125
+ )
126
+
127
+ iso_file_path = self._download_remote_operating_system_iso(
128
+ remote_operating_system_name,
129
+ directory=directory,
130
+ )
131
+ checksum_file_path = self._download_remote_operating_system_checksum(
132
+ remote_operating_system_name,
133
+ directory=directory,
134
+ )
135
+
136
+ logger.info("Validating iso checksum")
137
+ checksum_valid = self.primitive.operating_systems._validate_checksum(
138
+ remote_operating_system_name, str(iso_file_path), str(checksum_file_path)
139
+ )
140
+
141
+ if not checksum_valid:
142
+ raise Exception(
143
+ "Checksums did not match: file may have been corrupted during download."
144
+ + f"\nTry deleting the directory {get_operating_systems_cache()}/{remote_operating_system_name} and running this command again."
145
+ )
146
+
147
+ logger.info("Checksum valid")
148
+
149
+ return iso_file_path, checksum_file_path
150
+
151
+ def _validate_checksum(
152
+ self,
153
+ operating_system_name: str,
154
+ iso_file_path: str,
155
+ checksum_file_path: str,
156
+ checksum_file_type: OperatingSystemChecksumFileType | None = None,
157
+ ):
158
+ checksum_file_type = (
159
+ checksum_file_type
160
+ if checksum_file_type
161
+ else self.get_remote_info(operating_system_name)["checksum_file_type"]
162
+ )
163
+
164
+ match checksum_file_type:
165
+ case self.OperatingSystemChecksumFileType.SHA256SUMS:
166
+ return self._validate_sha256_sums_checksum(
167
+ iso_file_path, checksum_file_path
168
+ )
169
+ case _:
170
+ logger.error(f"Invalid checksum file type: {checksum_file_type}")
171
+ raise ValueError(f"Invalid checksum file type: {checksum_file_type}")
172
+
173
+ def _validate_sha256_sums_checksum(self, iso_file_path, checksum_file_path):
174
+ iso_file_name = Path(iso_file_path).name
175
+
176
+ remote_checksum = get_checksum_from_file(checksum_file_path, iso_file_name)
177
+ local_checksum = calculate_sha256(iso_file_path)
178
+ return remote_checksum == local_checksum
179
+
180
+ def _upload_iso_file(
181
+ self, iso_file_path: Path, organization_id: str, operating_system_slug: str
182
+ ):
183
+ iso_upload_result = self.primitive.files.upload_file_direct(
184
+ path=iso_file_path,
185
+ organization_id=organization_id,
186
+ key_prefix=f"{self.operating_systems_key_prefix}/{operating_system_slug}",
187
+ )
188
+
189
+ if not iso_upload_result or iso_upload_result.data is None:
190
+ logger.error("Unable to upload iso file")
191
+ raise Exception("Unable to upload iso file")
192
+
193
+ iso_upload_data = iso_upload_result.data
194
+ iso_file_id = iso_upload_data.get("fileUpdate", {}).get("id")
195
+
196
+ if not iso_file_id:
197
+ logger.error("Unable to upload iso file")
198
+ raise Exception("Unable to upload iso file")
199
+
200
+ return iso_file_id
201
+
202
+ def _upload_checksum_file(
203
+ self, checksum_file_path: Path, organization_id: str, operating_system_slug: str
204
+ ):
205
+ checksum_upload_response = self.primitive.files.upload_file_via_api(
206
+ path=checksum_file_path,
207
+ organization_id=organization_id,
208
+ key_prefix=f"{self.operating_systems_key_prefix}/{operating_system_slug}",
209
+ )
210
+
211
+ if not checksum_upload_response.ok:
212
+ logger.error("Unable to upload checksum file")
213
+ raise Exception("Unable to upload checksum file")
214
+
215
+ checksum_file_id = (
216
+ checksum_upload_response.json()
217
+ .get("data", {})
218
+ .get("fileUpload", {})
219
+ .get("id", {})
220
+ )
221
+
222
+ if not checksum_file_id:
223
+ logger.error("Unable to upload checksum file")
224
+ raise Exception("Unable to upload checksum file")
225
+
226
+ return checksum_file_id
227
+
228
+ @guard
229
+ def create(
230
+ self,
231
+ slug: str,
232
+ iso_file: str,
233
+ checksum_file: str,
234
+ checksum_file_type: str,
235
+ organization_id: str,
236
+ ):
237
+ formatted_slug = slugify(slug)
238
+ is_slug_available = self.primitive.operating_systems._is_slug_available(
239
+ slug=formatted_slug,
240
+ organization_id=organization_id,
241
+ )
242
+
243
+ if not is_slug_available:
244
+ raise Exception(
245
+ f"Operating system with slug {formatted_slug} already exists."
246
+ )
247
+
248
+ is_known_checksum_file_type = (
249
+ checksum_file_type
250
+ in self.primitive.operating_systems.OperatingSystemChecksumFileType._value2member_map_
251
+ )
252
+
253
+ if not is_known_checksum_file_type:
254
+ raise Exception(
255
+ f"Operating system checksum file type {checksum_file_type} is not supported."
256
+ + f" Supported types are: {''.join([type.value for type in self.primitive.operating_systems.OperatingSystemChecksumFileType])}"
257
+ )
258
+
259
+ iso_file_path = Path(iso_file)
260
+ checksum_file_path = Path(checksum_file)
261
+
262
+ if not iso_file_path.is_file():
263
+ raise Exception(
264
+ f"ISO file {iso_file_path} does not exist or is not a file."
265
+ )
266
+
267
+ if not checksum_file_path.is_file():
268
+ raise Exception(
269
+ f"Checksum file {checksum_file_path} does not exist or is not a file."
270
+ )
271
+
272
+ logger.info("Uploading iso file. This may take a while...")
273
+ iso_file_id = self.primitive.operating_systems._upload_iso_file(
274
+ iso_file_path=iso_file_path,
275
+ organization_id=organization_id,
276
+ operating_system_slug=formatted_slug,
277
+ )
278
+
279
+ logger.info("Uploading checksum file")
280
+ checksum_file_id = self.primitive.operating_systems._upload_checksum_file(
281
+ checksum_file_path=checksum_file_path,
282
+ organization_id=organization_id,
283
+ operating_system_slug=formatted_slug,
284
+ )
285
+
286
+ logger.info("Creating operating system in primitive.")
287
+ operating_system_create_response = (
288
+ self.primitive.operating_systems._create_query(
289
+ slug=formatted_slug,
290
+ checksum_file_id=checksum_file_id,
291
+ checksum_file_type=checksum_file_type,
292
+ organization_id=organization_id,
293
+ iso_file_id=iso_file_id,
294
+ )
295
+ )
296
+
297
+ if "id" not in operating_system_create_response:
298
+ raise Exception("Failed to create operating system")
299
+
300
+ return operating_system_create_response
301
+
302
+ @guard
303
+ def _create_query(
304
+ self,
305
+ slug: str,
306
+ organization_id: str,
307
+ checksum_file_id: str,
308
+ checksum_file_type: str,
309
+ iso_file_id: str,
310
+ ):
311
+ mutation = gql(operating_system_create_mutation)
312
+ input = {
313
+ "slug": slug,
314
+ "organization": organization_id,
315
+ "checksumFile": checksum_file_id,
316
+ "checksumFileType": checksum_file_type,
317
+ "isoFile": iso_file_id,
318
+ }
319
+ variables = {"input": input}
320
+ result = self.primitive.session.execute(
321
+ mutation, variable_values=variables, get_execution_result=True
322
+ )
323
+ return result.data.get("operatingSystemCreate")
324
+
325
+ @guard
326
+ def list(
327
+ self,
328
+ organization_id: str,
329
+ slug: str | None = None,
330
+ id: str | None = None,
331
+ ):
332
+ query = gql(operating_system_list_query)
333
+
334
+ variables = {
335
+ "filters": {
336
+ "organization": {"id": organization_id},
337
+ }
338
+ }
339
+
340
+ if slug:
341
+ variables["filters"]["slug"] = {"exact": slug}
342
+
343
+ if id:
344
+ variables["filters"]["id"] = id
345
+
346
+ result = self.primitive.session.execute(
347
+ query, variable_values=variables, get_execution_result=True
348
+ )
349
+
350
+ edges = result.data.get("operatingSystemList").get("edges", [])
351
+
352
+ nodes = [edge.get("node") for edge in edges]
353
+
354
+ return nodes
355
+
356
+ @guard
357
+ def download(
358
+ self,
359
+ organization_id: str,
360
+ id: str | None = None,
361
+ slug: str | None = None,
362
+ directory: str | None = None,
363
+ ):
364
+ operating_system = self.primitive.operating_systems.get(
365
+ organization_id=organization_id, slug=slug, id=id
366
+ )
367
+
368
+ is_cached = self.primitive.operating_systems.is_operating_system_cached(
369
+ slug=operating_system["slug"],
370
+ directory=directory,
371
+ )
372
+
373
+ if is_cached:
374
+ raise Exception(
375
+ "Operating system already exists in cache, aborting download."
376
+ )
377
+
378
+ download_directory = (
379
+ Path(directory) / operating_system["slug"]
380
+ if directory
381
+ else (get_operating_systems_cache() / operating_system["slug"])
382
+ )
383
+ checksum_directory = download_directory / "checksum"
384
+ checksum_file_path = (
385
+ checksum_directory / operating_system["checksumFile"]["fileName"]
386
+ )
387
+ iso_directory = download_directory / "iso"
388
+ iso_file_path = iso_directory / operating_system["isoFile"]["fileName"]
389
+
390
+ if not iso_directory.exists():
391
+ iso_directory.mkdir(parents=True)
392
+
393
+ if not checksum_directory.exists():
394
+ checksum_directory.mkdir(parents=True)
395
+
396
+ logger.info("Downloading operating system iso")
397
+ self.primitive.files.download_file(
398
+ file_id=operating_system["isoFile"]["id"],
399
+ output_path=iso_directory,
400
+ organization_id=organization_id,
401
+ )
402
+
403
+ logger.info("Downloading operating system checksum")
404
+ self.primitive.files.download_file(
405
+ file_id=operating_system["checksumFile"]["id"],
406
+ output_path=checksum_directory,
407
+ organization_id=organization_id,
408
+ )
409
+
410
+ logger.info("Validating iso checksum")
411
+ checksum_file_type = (
412
+ self.primitive.operating_systems.OperatingSystemChecksumFileType[
413
+ operating_system["checksumFileType"]
414
+ ]
415
+ )
416
+ checksum_valid = self.primitive.operating_systems._validate_checksum(
417
+ operating_system["slug"],
418
+ iso_file_path,
419
+ checksum_file_path,
420
+ checksum_file_type=checksum_file_type,
421
+ )
422
+
423
+ if not checksum_valid:
424
+ raise Exception(
425
+ "Checksums did not match: file may have been corrupted during download."
426
+ + f"\nTry deleting the directory {get_operating_systems_cache()}/{operating_system['slug']} and running this command again."
427
+ )
428
+
429
+ return download_directory
430
+
431
+ @guard
432
+ def get(self, organization_id: str, slug: str | None = None, id: str | None = None):
433
+ if not (slug or id):
434
+ raise Exception("Slug or id must be provided.")
435
+ if slug and id:
436
+ raise Exception("Only one of slug or id must be provided.")
437
+
438
+ operating_systems = self.list(organization_id=organization_id, slug=slug, id=id)
439
+
440
+ if len(operating_systems) == 0:
441
+ if slug:
442
+ logger.error(f"No operating system found for slug '{slug}'.")
443
+ raise Exception(f"No operating system found for slug {slug}.")
444
+ else:
445
+ logger.error(f"No operating system found for ID {id}.")
446
+ raise Exception(f"No operating system found for ID {id}.")
447
+
448
+ return operating_systems[0]
449
+
450
+ @guard
451
+ def _is_slug_available(self, slug: str, organization_id: str):
452
+ query = gql(operating_system_list_query)
453
+
454
+ variables = {
455
+ "filters": {
456
+ "slug": {"exact": slug},
457
+ "organization": {"id": organization_id},
458
+ }
459
+ }
460
+
461
+ result = self.primitive.session.execute(
462
+ query, variable_values=variables, get_execution_result=True
463
+ )
464
+
465
+ count = result.data.get("operatingSystemList").get("totalCount")
466
+
467
+ return count == 0
468
+
469
+ def is_operating_system_cached(self, slug: str, directory: str | None = None):
470
+ cache_dir = Path(directory) if directory else get_operating_systems_cache()
471
+ cache_path = cache_dir / slug
472
+
473
+ return cache_path.exists()