Skip to content

Commit 8cf94e8

Browse files
committed
feat(gce): implement both new methods for enabling file uploading and image creation
1 parent 035f89d commit 8cf94e8

File tree

2 files changed

+237
-11
lines changed

2 files changed

+237
-11
lines changed

examples/gce.py

+88-9
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
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 argparse
6+
import datetime
57
import logging
68
import os
79

810
import pycloudlib
9-
from pycloudlib.cloud import ImageType
11+
from pycloudlib import GCE
12+
from pycloudlib.cloud import ImageInfo, ImageType
1013

1114

12-
def manage_ssh_key(gce):
15+
def manage_ssh_key(gce: GCE):
1316
"""Manage ssh keys for gce instances."""
1417
pub_key_path = "gce-pubkey"
1518
priv_key_path = "gce-privkey"
@@ -27,7 +30,7 @@ def manage_ssh_key(gce):
2730
gce.use_key(public_key_path=pub_key_path, private_key_path=priv_key_path)
2831

2932

30-
def generic(gce):
33+
def generic(gce: GCE):
3134
"""Show example of using the GCE library.
3235
3336
Connects to GCE and finds the latest daily image. Then runs
@@ -39,32 +42,108 @@ def generic(gce):
3942
print(inst.execute("lsb_release -a"))
4043

4144

42-
def pro(gce):
45+
def pro(gce: GCE):
4346
"""Show example of running a GCE PRO machine."""
4447
daily = gce.daily_image("bionic", image_type=ImageType.PRO)
4548
with gce.launch(daily) as inst:
4649
inst.wait()
4750
print(inst.execute("sudo ua status --wait"))
4851

4952

50-
def pro_fips(gce):
53+
def pro_fips(gce: GCE):
5154
"""Show example of running a GCE PRO FIPS machine."""
5255
daily = gce.daily_image("bionic", image_type=ImageType.PRO_FIPS)
5356
with gce.launch(daily) as inst:
5457
inst.wait()
5558
print(inst.execute("sudo ua status --wait"))
5659

5760

58-
def demo():
61+
def custom_image(gce: GCE, image_name):
62+
"""Show example of running a GCE custom image."""
63+
image_id = gce.get_image_id_from_name(image_name)
64+
print(image_id)
65+
with gce.launch(image_id=image_id) as instance:
66+
instance.wait()
67+
print(instance.execute("hostname"))
68+
input("Press Enter to teardown instance")
69+
70+
71+
def upload_custom_image(gce: GCE, image_name, local_file_path, bucket_name):
72+
"""Show example of uploading a custom image to GCE."""
73+
new_image: ImageInfo = gce.create_image_from_local_file(
74+
local_file_path=local_file_path,
75+
image_name=image_name,
76+
intermediary_storage_name=bucket_name,
77+
)
78+
print("created new image:", new_image)
79+
return new_image
80+
81+
82+
def demo_image_creation(
83+
local_file_path: str,
84+
bucket_name: str,
85+
image_name_template: str = "gce-example-image-{}",
86+
):
87+
"""Show example of creating a custom image on GCE from a local image file."""
88+
# get short date and time for unique tag
89+
time_tag = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
90+
tag = f"gce-example-{time_tag}"
91+
92+
with pycloudlib.GCE(tag=tag) as gce:
93+
manage_ssh_key(gce)
94+
95+
new_image: ImageInfo = upload_custom_image(
96+
gce,
97+
image_name=image_name_template.format(time_tag),
98+
local_file_path=local_file_path,
99+
bucket_name=bucket_name,
100+
)
101+
102+
with gce.launch(new_image.id) as instance:
103+
instance.wait()
104+
print(instance.execute("hostname"))
105+
print(instance.execute("lsb_release -a"))
106+
input("Press Enter to teardown instance")
107+
108+
109+
def demo_instances():
59110
"""Show examples of launching GCP instances."""
60-
logging.basicConfig(level=logging.DEBUG)
61111
with pycloudlib.GCE(tag="examples") as gce:
62112
manage_ssh_key(gce)
63-
64113
generic(gce)
65114
pro(gce)
66115
pro_fips(gce)
67116

68117

118+
def main():
119+
"""Take in cli args and run GCE demo scripts."""
120+
parser = argparse.ArgumentParser(description="GCE Demo Script")
121+
parser.add_argument(
122+
"demo_type",
123+
choices=["instance", "image"],
124+
help="Type of demo to run: 'instance' for basic instance launch demo, or "
125+
"'image' for image creation demo",
126+
nargs="?",
127+
default="instance",
128+
)
129+
parser.add_argument(
130+
"--local_file_path", type=str, help="Local file path for the image creation demo"
131+
)
132+
parser.add_argument("--bucket_name", type=str, help="Bucket name for the image creation demo")
133+
args = parser.parse_args()
134+
135+
logging.basicConfig(level=logging.DEBUG)
136+
137+
if args.demo_type == "instance":
138+
demo_instances()
139+
elif args.demo_type == "image":
140+
if not args.local_file_path or not args.bucket_name:
141+
parser.error("Image creation demo requires --local_file_path and --bucket_name")
142+
demo_image_creation(
143+
local_file_path=args.local_file_path,
144+
bucket_name=args.bucket_name,
145+
)
146+
147+
69148
if __name__ == "__main__":
70-
demo()
149+
main()

pycloudlib/gce/cloud.py

+149-2
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,149 @@ 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+
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

Comments
 (0)