Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WORKING POC] Add upload and registering of images #448

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 88 additions & 9 deletions examples/gce.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
# This file is part of pycloudlib. See LICENSE file for license information.
"""Basic examples of various lifecycle with an GCE instance."""

import argparse
import datetime
import logging
import os

import pycloudlib
from pycloudlib.cloud import ImageType
from pycloudlib import GCE
from pycloudlib.cloud import ImageInfo, ImageType


def manage_ssh_key(gce):
def manage_ssh_key(gce: GCE):
"""Manage ssh keys for gce instances."""
pub_key_path = "gce-pubkey"
priv_key_path = "gce-privkey"
Expand All @@ -27,7 +30,7 @@ def manage_ssh_key(gce):
gce.use_key(public_key_path=pub_key_path, private_key_path=priv_key_path)


def generic(gce):
def generic(gce: GCE):
"""Show example of using the GCE library.

Connects to GCE and finds the latest daily image. Then runs
Expand All @@ -39,32 +42,108 @@ def generic(gce):
print(inst.execute("lsb_release -a"))


def pro(gce):
def pro(gce: GCE):
"""Show example of running a GCE PRO machine."""
daily = gce.daily_image("bionic", image_type=ImageType.PRO)
with gce.launch(daily) as inst:
inst.wait()
print(inst.execute("sudo ua status --wait"))


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


def demo():
def custom_image(gce: GCE, image_name):
"""Show example of running a GCE custom image."""
image_id = gce.get_image_id_from_name(image_name)
print(image_id)
with gce.launch(image_id=image_id) as instance:
instance.wait()
print(instance.execute("hostname"))
input("Press Enter to teardown instance")


def upload_custom_image(gce: GCE, image_name, local_file_path, bucket_name):
"""Show example of uploading a custom image to GCE."""
new_image: ImageInfo = gce.create_image_from_local_file(
local_file_path=local_file_path,
image_name=image_name,
intermediary_storage_name=bucket_name,
)
print("created new image:", new_image)
return new_image


def demo_image_creation(
local_file_path: str,
bucket_name: str,
image_name_template: str = "gce-example-image-{}",
):
"""Show example of creating a custom image on GCE from a local image file."""
# get short date and time for unique tag
time_tag = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
tag = f"gce-example-{time_tag}"

with pycloudlib.GCE(tag=tag) as gce:
manage_ssh_key(gce)

new_image: ImageInfo = upload_custom_image(
gce,
image_name=image_name_template.format(time_tag),
local_file_path=local_file_path,
bucket_name=bucket_name,
)

with gce.launch(new_image.id) as instance:
instance.wait()
print(instance.execute("hostname"))
print(instance.execute("lsb_release -a"))
input("Press Enter to teardown instance")


def demo_instances():
"""Show examples of launching GCP instances."""
logging.basicConfig(level=logging.DEBUG)
with pycloudlib.GCE(tag="examples") as gce:
manage_ssh_key(gce)

generic(gce)
pro(gce)
pro_fips(gce)


def main():
"""Take in cli args and run GCE demo scripts."""
parser = argparse.ArgumentParser(description="GCE Demo Script")
parser.add_argument(
"demo_type",
choices=["instance", "image"],
help="Type of demo to run: 'instance' for basic instance launch demo, or "
"'image' for image creation demo",
nargs="?",
default="instance",
)
parser.add_argument(
"--local_file_path", type=str, help="Local file path for the image creation demo"
)
parser.add_argument("--bucket_name", type=str, help="Bucket name for the image creation demo")
args = parser.parse_args()

logging.basicConfig(level=logging.DEBUG)

if args.demo_type == "instance":
demo_instances()
elif args.demo_type == "image":
if not args.local_file_path or not args.bucket_name:
parser.error("Image creation demo requires --local_file_path and --bucket_name")
demo_image_creation(
local_file_path=args.local_file_path,
bucket_name=args.bucket_name,
)


if __name__ == "__main__":
demo()
main()
67 changes: 57 additions & 10 deletions examples/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# This file is part of pycloudlib. See LICENSE file for license information.
"""Basic examples of various lifecycle with an OCI instance."""

import argparse
import logging
import sys
from base64 import b64encode

import pycloudlib
Expand Down Expand Up @@ -46,17 +46,64 @@ def demo(
new_instance.wait()
new_instance.execute("whoami")

def upload_and_create_image(
local_file_path: str,
image_name: str,
suite: str,
intermediary_storage_name: str,
availability_domain: str = None,
compartment_id: str = None,
):
"""Upload a local .img file and create an image from it on OCI."""
with pycloudlib.OCI(
"oracle-test",
availability_domain=availability_domain,
compartment_id=compartment_id,
) as client:
image_info = client.create_image_from_local_file(
local_file_path=local_file_path,
image_name=image_name,
intermediary_storage_name=intermediary_storage_name,
suite=suite,
)
print(f"Created image: {image_info}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="OCI example script")
subparsers = parser.add_subparsers(dest="command")

demo_parser = subparsers.add_parser("demo", help="Run the demo")
demo_parser.add_argument("--availability-domain", type=str, help="Availability domain", required=False)
demo_parser.add_argument("--compartment-id", type=str, help="Compartment ID", required=False)
demo_parser.add_argument("--vcn-name", type=str, help="VCN name", required=False)

create_image_parser = subparsers.add_parser("create_image", help="Create an image from a local file")
create_image_parser.add_argument("--local-file-path", type=str, required=True, help="Local file path")
create_image_parser.add_argument("--image-name", type=str, required=True, help="Image name")
create_image_parser.add_argument("--intermediary-storage-name", type=str, required=True, help="Intermediary storage name")
create_image_parser.add_argument("--suite", type=str, help="Suite of the image. I.e. 'jammy'", required=True)
create_image_parser.add_argument("--availability-domain", type=str, help="Availability domain")
create_image_parser.add_argument("--compartment-id", type=str, help="Compartment ID")

args = parser.parse_args()

logging.basicConfig(level=logging.DEBUG)
if len(sys.argv) != 3:
print(
"No arguments passed via command line. "
"Assuming values are set in pycloudlib configuration file."

if args.command == "demo":
demo(
availability_domain=args.availability_domain,
compartment_id=args.compartment_id,
vcn_name=args.vcn_name,
)
elif args.command == "create_image":
upload_and_create_image(
local_file_path=args.local_file_path,
image_name=args.image_name,
suite=args.suite,
intermediary_storage_name=args.intermediary_storage_name,
availability_domain=args.availability_domain,
compartment_id=args.compartment_id,
)
demo()
else:
passed_availability_domain = sys.argv[1]
passed_compartment_id = sys.argv[2]
passed_vcn_name = sys.argv[3] if len(sys.argv) == 4 else None
demo(passed_availability_domain, passed_compartment_id, passed_vcn_name)
parser.print_help()
91 changes: 91 additions & 0 deletions pycloudlib/cloud.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This file is part of pycloudlib. See LICENSE file for license information.
"""Base class for all other clouds to provide consistent set of functions."""

import dataclasses
import enum
import getpass
import io
Expand Down Expand Up @@ -38,6 +39,32 @@ class ImageType(enum.Enum):
PRO_FIPS = "Pro FIPS"


@dataclasses.dataclass
class ImageInfo:
"""Dataclass that represents an image on any given cloud."""

id: str
name: str

def __str__(self):
"""Return a human readable string representation of the image."""
return f"{self.name} [id: {self.id}]"

def __repr__(self):
"""Return a string representation of the image."""
return f"ImageInfo(id={self.id}, name={self.name})"

def __eq__(self, other):
"""Check if two ImageInfo objects are equal."""
if not isinstance(other, ImageInfo):
return False
return self.id == other.id

def __dict__(self):
"""Return a dictionary representation of the image."""
return {"id": self.id, "name": self.name}


class BaseCloud(ABC):
"""Base Cloud Class."""

Expand Down Expand Up @@ -371,3 +398,67 @@ def _get_ssh_keys(
private_key_path=private_key_path,
name=name,
)

# all actual "Clouds" and not just substrates like LXD and QEMU should support this method
def upload_local_file_to_cloud_storage(
self,
*,
local_file_path: str,
storage_name: str,
remote_file_name: Optional[str] = None,
) -> str:
"""
Upload a file to a storage destination on the Cloud.

Args:
local_file_path: The local file path of the image to upload.
storage_name: The name of the storage destination on the Cloud to upload the file to.
remote_file_name: The name of the file in the storage destination. If not provided,
the base name of the local file path will be used.

Returns:
str: URL of the uploaded file in the storage destination.
"""
raise NotImplementedError

# most clouds except for like lxd should support this method
def create_image_from_local_file(
self,
*,
local_file_path: str,
image_name: str,
intermediary_storage_name: str,
):
"""
Upload local image file to storage on the Cloud and then create a custom image from it.

Args:
local_file_path: The local file path of the image to upload.
image_name: The name to upload the image as and to register.
intermediary_storage_name: The intermediary storage destination on the Cloud to upload
the file to before creating the image.

Returns:
ImageInfo: Information about the created image.
"""
raise NotImplementedError

# not all clouds will support this method - depends on how image creation is handled
def _create_image_from_cloud_storage(
self,
*,
image_name: str,
remote_image_file_url: str,
image_description: Optional[str] = None,
) -> ImageInfo:
"""
Register a custom image in the Cloud from a file in Cloud storage using its url.

Ideally, this url would be returned from the upload_local_file_to_cloud_storage method.

Args:
image_name: The name the image will be created with.
remote_image_file_url: The URL of the image file in the Cloud storage.
image_description: (Optional) A description of the image.
"""
raise NotImplementedError
Loading
Loading