Skip to content

Commit 7950d3e

Browse files
committed
feat(gce): implement both new methods for enabling file uploading and image creation
1 parent ff4faae commit 7950d3e

File tree

2 files changed

+204
-10
lines changed

2 files changed

+204
-10
lines changed

examples/gce.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
# This file is part of pycloudlib. See LICENSE file for license information.
33
"""Basic examples of various lifecycle with an GCE instance."""
44

5+
import datetime
56
import logging
67
import os
78

89
import pycloudlib
9-
from pycloudlib.cloud import ImageType
10+
from pycloudlib import GCE
11+
from pycloudlib.cloud import ImageInfo, ImageType
1012

1113

12-
def manage_ssh_key(gce):
14+
def manage_ssh_key(gce: GCE):
1315
"""Manage ssh keys for gce instances."""
1416
pub_key_path = "gce-pubkey"
1517
priv_key_path = "gce-privkey"
@@ -27,7 +29,7 @@ def manage_ssh_key(gce):
2729
gce.use_key(public_key_path=pub_key_path, private_key_path=priv_key_path)
2830

2931

30-
def generic(gce):
32+
def generic(gce: GCE):
3133
"""Show example of using the GCE library.
3234
3335
Connects to GCE and finds the latest daily image. Then runs
@@ -39,32 +41,78 @@ def generic(gce):
3941
print(inst.execute("lsb_release -a"))
4042

4143

42-
def pro(gce):
44+
def pro(gce: GCE):
4345
"""Show example of running a GCE PRO machine."""
4446
daily = gce.daily_image("bionic", image_type=ImageType.PRO)
4547
with gce.launch(daily) as inst:
4648
inst.wait()
4749
print(inst.execute("sudo ua status --wait"))
4850

4951

50-
def pro_fips(gce):
52+
def pro_fips(gce: GCE):
5153
"""Show example of running a GCE PRO FIPS machine."""
5254
daily = gce.daily_image("bionic", image_type=ImageType.PRO_FIPS)
5355
with gce.launch(daily) as inst:
5456
inst.wait()
5557
print(inst.execute("sudo ua status --wait"))
5658

5759

60+
def custom_image(gce: GCE, image_name):
61+
"""Show example of running a GCE custom image."""
62+
image_id = gce.get_image_id_from_name(image_name)
63+
print(image_id)
64+
with gce.launch(image_id=image_id) as instance:
65+
instance.wait()
66+
print(instance.execute("hostname"))
67+
input("Press Enter to teardown instance")
68+
69+
70+
def upload_custom_image(gce: GCE, image_name, local_file_path, bucket_name):
71+
"""Show example of uploading a custom image to GCE."""
72+
new_image: ImageInfo = gce.create_image_from_local_file(
73+
local_file_path=local_file_path,
74+
image_name=image_name,
75+
intermediary_storage_name=bucket_name,
76+
)
77+
print("created new image:", new_image)
78+
return new_image
79+
80+
81+
def demo_image_creation(local_file_path: str, bucket_name: str):
82+
# get short date and time for unique tag
83+
time_tag = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
84+
tag = f"gce-example-{time_tag}"
85+
86+
with pycloudlib.GCE(tag=tag) as gce:
87+
manage_ssh_key(gce)
88+
89+
new_image: ImageInfo = upload_custom_image(
90+
gce,
91+
image_name=f"noble-gce-test-image-{time_tag}",
92+
local_file_path=local_file_path,
93+
bucket_name=bucket_name,
94+
)
95+
96+
with gce.launch(new_image.id) as instance:
97+
instance.wait()
98+
print(instance.execute("hostname"))
99+
print(instance.execute("lsb_release -a"))
100+
input("Press Enter to teardown instance")
101+
102+
58103
def demo():
59104
"""Show examples of launching GCP instances."""
60-
logging.basicConfig(level=logging.DEBUG)
61105
with pycloudlib.GCE(tag="examples") as gce:
62106
manage_ssh_key(gce)
63-
64107
generic(gce)
65108
pro(gce)
66109
pro_fips(gce)
67110

68111

69112
if __name__ == "__main__":
70-
demo()
113+
logging.basicConfig(level=logging.DEBUG)
114+
# demo()
115+
demo_image_creation(
116+
local_file_path="/home/a-dubs/.swift/noble-gce.tar.gz",
117+
bucket_name="a-dubs-jenkins-bucket",
118+
)

pycloudlib/gce/cloud.py

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414

1515
from google.api_core.exceptions import GoogleAPICallError
1616
from google.api_core.extended_operation import ExtendedOperation
17-
from google.cloud import compute_v1
17+
from google.cloud import compute_v1, storage
1818

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

0 commit comments

Comments
 (0)