|
10 | 10 |
|
11 | 11 | import oci
|
12 | 12 |
|
13 |
| -from pycloudlib.cloud import BaseCloud |
| 13 | +from pycloudlib.cloud import BaseCloud, ImageInfo |
14 | 14 | from pycloudlib.config import ConfigFile
|
15 | 15 | from pycloudlib.errors import (
|
16 | 16 | CloudSetupError,
|
@@ -367,3 +367,168 @@ def _validate_tag(tag: str):
|
367 | 367 |
|
368 | 368 | if rules_failed:
|
369 | 369 | raise InvalidTagNameError(tag=tag, rules_failed=rules_failed)
|
| 370 | + |
| 371 | + # all actual "Clouds" and not just substrates like LXD and QEMU should support this method |
| 372 | + def upload_local_file_to_cloud_storage( |
| 373 | + self, |
| 374 | + *, |
| 375 | + local_file_path: str, |
| 376 | + storage_name: str, |
| 377 | + remote_file_name: Optional[str] = None, |
| 378 | + overwrite_existing: bool = False, |
| 379 | + ) -> str: |
| 380 | + """ |
| 381 | + Upload a file to a storage destination on the Cloud. |
| 382 | +
|
| 383 | + Args: |
| 384 | + local_file_path: The local file path of the image to upload. |
| 385 | + storage_name: The name of the storage destination on the Cloud to upload the file to. |
| 386 | + remote_file_name: The name of the file in the storage destination. If not provided, |
| 387 | + the base name of the local file path will be used. |
| 388 | +
|
| 389 | + Returns: |
| 390 | + str: URL of the uploaded file in the storage destination. |
| 391 | + """ |
| 392 | + object_storage_client = oci.object_storage.ObjectStorageClient(self.oci_config) |
| 393 | + namespace = object_storage_client.get_namespace().data |
| 394 | + bucket_name = storage_name |
| 395 | + remote_file_name = remote_file_name or os.path.basename(local_file_path) |
| 396 | + |
| 397 | + # if remote file name is missing the extension, add it from the local file path |
| 398 | + if not remote_file_name.endswith(ext:="."+local_file_path.split('.')[-1]): |
| 399 | + remote_file_name += ext |
| 400 | + |
| 401 | + # check if object already exists in the bucket |
| 402 | + try: |
| 403 | + object_storage_client.get_object( |
| 404 | + namespace, |
| 405 | + bucket_name, |
| 406 | + remote_file_name |
| 407 | + ) |
| 408 | + if overwrite_existing: |
| 409 | + self._log.warning( |
| 410 | + f"Object {remote_file_name} already exists in the bucket {bucket_name}. " |
| 411 | + "Overwriting it." |
| 412 | + ) |
| 413 | + else: |
| 414 | + self._log.info( |
| 415 | + "Skipping upload as the object already exists in the bucket." |
| 416 | + ) |
| 417 | + return f"https://objectstorage.{self.region}.oraclecloud.com/n/{namespace}/b/{bucket_name}/o/{remote_file_name}" |
| 418 | + except oci.exceptions.ServiceError as e: |
| 419 | + if e.status != 404: |
| 420 | + raise e |
| 421 | + |
| 422 | + with open(local_file_path, 'rb') as file: |
| 423 | + object_storage_client.put_object( |
| 424 | + namespace, |
| 425 | + bucket_name, |
| 426 | + remote_file_name, |
| 427 | + file |
| 428 | + ) |
| 429 | + |
| 430 | + return f"https://objectstorage.{self.region}.oraclecloud.com/n/{namespace}/b/{bucket_name}/o/{remote_file_name}" |
| 431 | + |
| 432 | + def create_image_from_local_file( |
| 433 | + self, |
| 434 | + *, |
| 435 | + local_file_path: str, |
| 436 | + image_name: str, |
| 437 | + intermediary_storage_name: str, |
| 438 | + suite: str, |
| 439 | + ) -> ImageInfo: |
| 440 | + """ |
| 441 | + Upload local image file to storage on the Cloud and then create a custom image from it. |
| 442 | +
|
| 443 | + Args: |
| 444 | + local_file_path: The local file path of the image to upload. |
| 445 | + image_name: The name to upload the image as and to register. |
| 446 | + intermediary_storage_name: The intermediary storage destination on the Cloud to upload |
| 447 | + the file to before creating the image. |
| 448 | + suite: The suite of the image to create. I.e. "noble", "jammy", or "focal". |
| 449 | +
|
| 450 | + Returns: |
| 451 | + ImageInfo: Information about the created image. |
| 452 | + """ |
| 453 | + remote_file_url = self.upload_local_file_to_cloud_storage( |
| 454 | + local_file_path=local_file_path, |
| 455 | + storage_name=intermediary_storage_name, |
| 456 | + remote_file_name=image_name, |
| 457 | + ) |
| 458 | + return self._create_image_from_cloud_storage( |
| 459 | + image_name=image_name, |
| 460 | + remote_image_file_url=remote_file_url, |
| 461 | + suite=suite, |
| 462 | + ) |
| 463 | + |
| 464 | + def parse_remote_object_url(self, remote_object_url: str) -> tuple[str, str, str]: |
| 465 | + """ |
| 466 | + Parse the remote object URL to extract the namespace, bucket name, and object name. |
| 467 | +
|
| 468 | + Args: |
| 469 | + remote_object_url: The URL of the object in the Cloud storage. |
| 470 | +
|
| 471 | + Returns: |
| 472 | + tuple[str, str, str]: The namespace, bucket name, and object name. |
| 473 | + """ |
| 474 | + if not remote_object_url.startswith("https://objectstorage"): |
| 475 | + raise ValueError("Invalid URL. Expected a URL from the Oracle Cloud object storage.") |
| 476 | + parts = remote_object_url.split("/") |
| 477 | + # find the "n/", "b/", and "o/" parts of the URL |
| 478 | + namespace_index = parts.index("n") + 1 |
| 479 | + bucket_index = parts.index("b") + 1 |
| 480 | + object_index = parts.index("o") + 1 |
| 481 | + return parts[namespace_index], parts[bucket_index], parts[object_index] |
| 482 | + |
| 483 | + def _create_image_from_cloud_storage( |
| 484 | + self, |
| 485 | + *, |
| 486 | + image_name: str, |
| 487 | + remote_image_file_url: str, |
| 488 | + suite: str, |
| 489 | + image_description: Optional[str] = None, |
| 490 | + ) -> ImageInfo: |
| 491 | + """ |
| 492 | + Register a custom image in the Cloud from a file in Cloud storage using its url. |
| 493 | +
|
| 494 | + Ideally, this url would be returned from the upload_local_file_to_cloud_storage method. |
| 495 | +
|
| 496 | + Args: |
| 497 | + image_name: The name the image will be created with. |
| 498 | + remote_image_file_url: The URL of the image file in the Cloud storage. |
| 499 | + image_description: (Optional) A description of the image. |
| 500 | + """ |
| 501 | + suites = { |
| 502 | + "noble": "24.04", |
| 503 | + "jammy": "22.04", |
| 504 | + "focal": "20.04", |
| 505 | + } |
| 506 | + if suite not in suites: |
| 507 | + raise ValueError(f"Invalid suite. Expected one of {list(suites.keys())}. Found: {suite}") |
| 508 | + # parse object name and bucket name from the url |
| 509 | + object_namespace, bucket_name, object_name = self.parse_remote_object_url(remote_image_file_url) |
| 510 | + self._log.debug(f"Bucket name: {bucket_name}, Object name: {object_name}, Object namespace: {object_namespace}") |
| 511 | + image_details = oci.core.models.CreateImageDetails( |
| 512 | + compartment_id=self.compartment_id, |
| 513 | + display_name=image_name, |
| 514 | + image_source_details=oci.core.models.ImageSourceViaObjectStorageTupleDetails( |
| 515 | + source_type="objectStorageTuple", |
| 516 | + bucket_name=bucket_name, |
| 517 | + object_name=object_name, |
| 518 | + namespace_name=object_namespace, |
| 519 | + operating_system="Canonical Ubuntu", |
| 520 | + operating_system_version=suites[suite], |
| 521 | + ), |
| 522 | + launch_mode="PARAVIRTUALIZED", |
| 523 | + freeform_tags={"Description": image_description} if image_description else None |
| 524 | + ) |
| 525 | + |
| 526 | + image_data = self.compute_client.create_image(image_details).data |
| 527 | + image_data = wait_till_ready( |
| 528 | + func=self.compute_client.get_image, |
| 529 | + current_data=image_data, |
| 530 | + desired_state="AVAILABLE", |
| 531 | + ) |
| 532 | + |
| 533 | + return ImageInfo(id=image_data.id, name=image_data.display_name) |
| 534 | + |
0 commit comments