|
14 | 14 |
|
15 | 15 | from google.api_core.exceptions import GoogleAPICallError
|
16 | 16 | from google.api_core.extended_operation import ExtendedOperation
|
17 |
| -from google.cloud import compute_v1 |
| 17 | +from google.cloud import compute_v1, storage |
18 | 18 |
|
19 |
| -from pycloudlib.cloud import BaseCloud, ImageType |
| 19 | +from pycloudlib.cloud import BaseCloud, ImageInfo, ImageType |
20 | 20 | from pycloudlib.config import ConfigFile
|
21 | 21 | from pycloudlib.errors import (
|
22 | 22 | CloudSetupError,
|
@@ -109,6 +109,7 @@ def __init__(
|
109 | 109 | self._instances_client = compute_v1.InstancesClient(credentials=credentials)
|
110 | 110 | self._zone_operations_client = compute_v1.ZoneOperationsClient(credentials=credentials)
|
111 | 111 | self._global_operations_client = compute_v1.GlobalOperationsClient(credentials=credentials)
|
| 112 | + self._storage_client = storage.Client(credentials=credentials) |
112 | 113 | region = region or self.config.get("region") or "us-west2"
|
113 | 114 | zone = zone or self.config.get("zone") or "a"
|
114 | 115 | self.project = project
|
@@ -498,3 +499,149 @@ def _wait_for_operation(self, operation, operation_type="global", sleep_seconds=
|
498 | 499 | "Expected DONE state, but found {} after waiting {} seconds. "
|
499 | 500 | "Check GCE console for more details. \n".format(response.status, sleep_seconds)
|
500 | 501 | )
|
| 502 | + |
| 503 | + def upload_local_file_to_cloud_storage( |
| 504 | + self, |
| 505 | + *, |
| 506 | + local_file_path: str, |
| 507 | + storage_name: str, |
| 508 | + remote_file_name: Optional[str] = None, |
| 509 | + ): |
| 510 | + """ |
| 511 | + Upload a file to a storage destination on the Cloud. |
| 512 | +
|
| 513 | + Args: |
| 514 | + local_file_path: The local file path of the image to upload. |
| 515 | + storage_name: The name of the storage destination on the Cloud to upload the file to. |
| 516 | + remote_file_name: The name of the file in the storage destination. If not provided, |
| 517 | + the base name of the local file path will be used. |
| 518 | +
|
| 519 | + Returns: |
| 520 | + str: URL of the uploaded file in the storage destination. |
| 521 | + """ |
| 522 | + if not remote_file_name: |
| 523 | + remote_file_name = os.path.basename(local_file_path) |
| 524 | + |
| 525 | + bucket = self._storage_client.bucket(storage_name) |
| 526 | + blob = bucket.blob(remote_file_name) |
| 527 | + |
| 528 | + # Check if the file already exists in the destination bucket |
| 529 | + if blob.exists(): |
| 530 | + self._log.info( |
| 531 | + f"File '{remote_file_name}' already exists in bucket '{storage_name}', " |
| 532 | + "skipping upload." |
| 533 | + ) |
| 534 | + else: |
| 535 | + self._log.info( |
| 536 | + f"Uploading {local_file_path} to {remote_file_name} in bucket {storage_name}..." |
| 537 | + ) |
| 538 | + blob.upload_from_filename(local_file_path) |
| 539 | + self._log.info(f"File {local_file_path} uploaded successfully to {remote_file_name}.") |
| 540 | + |
| 541 | + return f"http://storage.googleapis.com/{storage_name}/{remote_file_name}" |
| 542 | + |
| 543 | + def create_image_from_local_file( |
| 544 | + self, |
| 545 | + *, |
| 546 | + local_file_path: str, |
| 547 | + image_name: str, |
| 548 | + intermediary_storage_name: str, |
| 549 | + description: Optional[str] = None, |
| 550 | + ) -> ImageInfo: |
| 551 | + """ |
| 552 | + Upload local image file to storage on the Cloud and then create a custom image from it. |
| 553 | +
|
| 554 | + Args: |
| 555 | + local_file_path: The local file path of the image to upload. |
| 556 | + image_name: The name to upload the image as and to register. |
| 557 | + intermediary_storage_name: The intermediary storage destination on the Cloud to upload |
| 558 | + the file to before creating the image. |
| 559 | +
|
| 560 | + Returns: |
| 561 | + ImageInfo: Information about the created image. |
| 562 | + """ |
| 563 | + # Upload the image to GCS |
| 564 | + remote_file_name = f"{image_name}.tar.gz" |
| 565 | + self._log.info( |
| 566 | + "Uploading image '%s' to '%s' as '%s'", |
| 567 | + image_name, |
| 568 | + intermediary_storage_name, |
| 569 | + remote_file_name, |
| 570 | + ) |
| 571 | + gcs_image_path = self.upload_local_file_to_cloud_storage( |
| 572 | + local_file_path=local_file_path, |
| 573 | + storage_name=intermediary_storage_name, |
| 574 | + remote_file_name=remote_file_name, |
| 575 | + ) |
| 576 | + # Register the custom image from GCS |
| 577 | + return self._create_image_from_cloud_storage( |
| 578 | + image_name=image_name, |
| 579 | + remote_image_file_url=gcs_image_path, |
| 580 | + ) |
| 581 | + |
| 582 | + def _create_image_from_cloud_storage( |
| 583 | + self, |
| 584 | + *, |
| 585 | + image_name: str, |
| 586 | + remote_image_file_url: str, |
| 587 | + image_description: Optional[str] = None, |
| 588 | + image_family: Optional[str] = None, |
| 589 | + ) -> ImageInfo: |
| 590 | + """ |
| 591 | + Register a custom image in GCE from a file in GCE's Cloud storage using its url. |
| 592 | +
|
| 593 | + Ideally, this url would be returned from the upload_local_file_to_cloud_storage method. |
| 594 | +
|
| 595 | + Args: |
| 596 | + image_name: The name the image will be created with. |
| 597 | + remote_image_file_url: The URL of the image file in the Cloud storage. |
| 598 | + image_description: (Optional) A description of the image. |
| 599 | + image_family: (Optional) The family name of the image. |
| 600 | +
|
| 601 | + Returns: |
| 602 | + ImageInfo: Information about the created image. |
| 603 | + """ |
| 604 | + image_body = compute_v1.Image( |
| 605 | + name=image_name, |
| 606 | + raw_disk=compute_v1.RawDisk(source=remote_image_file_url), |
| 607 | + description=image_description, |
| 608 | + architecture="x86_64", |
| 609 | + ) |
| 610 | + if image_family: |
| 611 | + image_body.family = image_family |
| 612 | + |
| 613 | + self._log.info( |
| 614 | + "Attempting to register custom image '%s' from GCS path '%s'...", |
| 615 | + image_name, |
| 616 | + remote_image_file_url, |
| 617 | + ) |
| 618 | + |
| 619 | + try: |
| 620 | + operation = self._images_client.insert( |
| 621 | + project=self.project, |
| 622 | + image_resource=image_body, |
| 623 | + ) |
| 624 | + self._wait_for_operation(operation) |
| 625 | + self._log.info(f"Custom image '{image_name}' registered successfully.") |
| 626 | + |
| 627 | + except Exception as e: |
| 628 | + self._log.error(f"Failed to create custom image from GCS: {e}") |
| 629 | + raise e |
| 630 | + |
| 631 | + return ImageInfo( |
| 632 | + id=f"projects/{self.project}/global/images/{image_name}", |
| 633 | + name=image_name, |
| 634 | + ) |
| 635 | + |
| 636 | + def _wait_for_operation(self, operation: ExtendedOperation): |
| 637 | + """ |
| 638 | + Wait for a GCE operation to complete. |
| 639 | +
|
| 640 | + Args: |
| 641 | + operation: The operation to wait for. |
| 642 | + """ |
| 643 | + self._log.debug("Waiting for operation to complete...") |
| 644 | + while not operation.done(): |
| 645 | + self._log.debug("Operation is still in progress...") |
| 646 | + time.sleep(5) |
| 647 | + self._log.debug("Operation completed.") |
0 commit comments