diff --git a/VERSION b/VERSION index d43e29f5..c2be2304 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1!10.10.1 +1!10.11.0 diff --git a/examples/az.py b/examples/az.py index 7903ee87..b66b4540 100644 --- a/examples/az.py +++ b/examples/az.py @@ -109,6 +109,29 @@ def demo_pro_fips(): print(instance.execute("sudo ua status --wait")) +def demo_pro_fips_updates(): + """Show example of launchig a Ubuntu PRO FIPS image through Azure.""" + with pycloudlib.Azure(tag="azure") as client: + image_id = client.daily_image(release="jammy", image_type=ImageType.PRO_FIPS_UPDATES) + + pub_key, priv_key = client.create_key_pair(key_name="test_pro_fips") + pub_path, priv_path = save_keys( + key_name="test_pro_fips", + pub_key=pub_key, + priv_key=priv_key, + ) + client.use_key(pub_path, priv_path) + + print("Launching Focal Pro FIPS Updates instance.") + with client.launch( + image_id=image_id, + instance_type="Standard_DS2_v2", # default is Standard_DS1_v2 + ) as instance: + instance.wait() + print(instance.ip) + print(instance.execute("sudo ua status --wait")) + + if __name__ == "__main__": # Avoid polluting the log with azure info logging.getLogger("adal-python").setLevel(logging.WARNING) @@ -118,3 +141,4 @@ def demo_pro_fips(): demo() demo_pro() demo_pro_fips() + demo_pro_fips_updates() diff --git a/examples/ec2.py b/examples/ec2.py index 659e8f2a..7bffbe0d 100755 --- a/examples/ec2.py +++ b/examples/ec2.py @@ -92,22 +92,13 @@ def launch_basic(ec2, daily): print(instance.availability_zone) -def launch_pro(ec2, daily): - """Show basic functionality on PRO instances.""" - print("Launching Pro instance...") - with ec2.launch(daily) as instance: - instance.wait() - print(instance.execute("sudo ua status --wait")) - print("Deleting Pro instance...") - - -def launch_pro_fips(ec2, daily): - """Show basic functionality on PRO instances.""" - print("Launching Pro FIPS instance...") - with ec2.launch(daily) as instance: +def launch_pro(ec2, name, image): + """Show basic functionality on Pro instances.""" + print("Launching {} instance...".format(name)) + with ec2.launch(image) as instance: instance.wait() - print(instance.execute("sudo ua status --wait")) - print("Deleting Pro FIPS instance...") + print(instance.execute("sudo pro status --wait")) + print("Deleting {} instance...".format(name)) def handle_ssh_key(ec2, key_name): @@ -140,13 +131,17 @@ def demo(): key_name = "test-ec2" handle_ssh_key(ec2, key_name) - daily = ec2.daily_image(release="bionic") - daily_pro = ec2.daily_image(release="bionic", image_type=ImageType.PRO) - daily_pro_fips = ec2.daily_image(release="bionic", image_type=ImageType.PRO_FIPS) + daily = ec2.daily_image(release="focal") + daily_pro = ec2.daily_image(release="focal", image_type=ImageType.PRO) + daily_pro_fips = ec2.daily_image(release="focal", image_type=ImageType.PRO_FIPS) + daily_pro_fips_updates = ec2.daily_image( + release="focal", image_type=ImageType.PRO_FIPS_UPDATES + ) launch_basic(ec2, daily) - launch_pro(ec2, daily_pro) - launch_pro_fips(ec2, daily_pro_fips) + launch_pro(ec2, "PRO", daily_pro) + launch_pro(ec2, "PRO FIPS", daily_pro_fips) + launch_pro(ec2, "PRO FIPS UPDATES", daily_pro_fips_updates) custom_vpc(ec2, daily) snapshot(ec2, daily) launch_multiple(ec2, daily) diff --git a/examples/gce.py b/examples/gce.py index 3eca925b..b95d81cf 100755 --- a/examples/gce.py +++ b/examples/gce.py @@ -39,20 +39,12 @@ def generic(gce): print(inst.execute("lsb_release -a")) -def pro(gce): +def pro(gce, series, image_type): """Show example of running a GCE PRO machine.""" - daily = gce.daily_image("bionic", image_type=ImageType.PRO) + daily = gce.daily_image(series, image_type) with gce.launch(daily) as inst: inst.wait() - print(inst.execute("sudo ua status --wait")) - - -def pro_fips(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")) + print(inst.execute("sudo pro status --wait")) def demo(): @@ -62,8 +54,9 @@ def demo(): manage_ssh_key(gce) generic(gce) - pro(gce) - pro_fips(gce) + pro(gce, "focal", ImageType.PRO) + pro(gce, "focal", ImageType.PRO_FIPS) + pro(gce, "jammy", ImageType.PRO_FIPS_UPDATES) if __name__ == "__main__": diff --git a/pycloudlib/azure/cloud.py b/pycloudlib/azure/cloud.py index fb75655d..304b51e4 100644 --- a/pycloudlib/azure/cloud.py +++ b/pycloudlib/azure/cloud.py @@ -62,6 +62,10 @@ "focal": "Canonical:0001-com-ubuntu-pro-focal-fips:pro-fips-20_04:latest", } +UBUNTU_DAILY_PRO_FIPS_UPDATES_IMAGES = { + "jammy": "Canonical:0001-com-ubuntu-pro-jammy-fips:pro-fips-22_04-gen1:latest", +} + UBUNTU_RELEASE_IMAGES = { "xenial": "Canonical:UbuntuServer:16.04-LTS:latest", "bionic": "Canonical:UbuntuServer:18.04-LTS:latest", @@ -683,6 +687,8 @@ def _get_images_dict(self, image_type: ImageType): return UBUNTU_DAILY_PRO_IMAGES if image_type == ImageType.PRO_FIPS: return UBUNTU_DAILY_PRO_FIPS_IMAGES + if image_type == ImageType.PRO_FIPS_UPDATES: + return UBUNTU_DAILY_PRO_FIPS_UPDATES_IMAGES if image_type == ImageType.MINIMAL: return UBUNTU_MINIMAL_DAILY_IMAGES diff --git a/pycloudlib/cloud.py b/pycloudlib/cloud.py index f75cd2f1..d1e2c289 100644 --- a/pycloudlib/cloud.py +++ b/pycloudlib/cloud.py @@ -36,6 +36,7 @@ class ImageType(enum.Enum): MINIMAL = "minimal" PRO = "Pro" PRO_FIPS = "Pro FIPS" + PRO_FIPS_UPDATES = "Pro FIPS Updates" class BaseCloud(ABC): diff --git a/pycloudlib/ec2/cloud.py b/pycloudlib/ec2/cloud.py index d479bcd3..7a8fc314 100644 --- a/pycloudlib/ec2/cloud.py +++ b/pycloudlib/ec2/cloud.py @@ -161,7 +161,10 @@ def _get_name_for_image_type(self, release: str, image_type: ImageType, daily: b return f"ubuntu-pro-server/images/{disk_type}/ubuntu-{release}-{release_ver}-*" if image_type == ImageType.PRO_FIPS: - return f"ubuntu-pro-fips*/images/{disk_type}/ubuntu-{release}-{release_ver}-*" + return f"ubuntu-pro-fips-server/images/{disk_type}/ubuntu-{release}-{release_ver}-*" + + if image_type == ImageType.PRO_FIPS_UPDATES: + return f"ubuntu-pro-fips-updates-server/images/{disk_type}/ubuntu-{release}-{release_ver}-*" raise ValueError("Invalid image_type") diff --git a/pycloudlib/gce/cloud.py b/pycloudlib/gce/cloud.py index c5f4feff..b4e01925 100644 --- a/pycloudlib/gce/cloud.py +++ b/pycloudlib/gce/cloud.py @@ -163,6 +163,11 @@ def _get_name_filter(self, release: str, image_type: ImageType): UBUNTU_RELEASE_VERSION_MAP[release].replace(".", ""), release ) + if image_type == ImageType.PRO_FIPS_UPDATES: + return "ubuntu-pro-fips-updates-{}-{}-*".format( + UBUNTU_RELEASE_VERSION_MAP[release].replace(".", ""), release + ) + raise ValueError("Invalid image_type: {}".format(image_type.value)) def _query_image_list(self, release: str, project: str, name_filter: str, arch: str): diff --git a/tests/integration_tests/ec2/__init__.py b/tests/integration_tests/ec2/__init__.py new file mode 100644 index 00000000..ea323f90 --- /dev/null +++ b/tests/integration_tests/ec2/__init__.py @@ -0,0 +1 @@ +"""EC2 integration tests.""" diff --git a/tests/integration_tests/ec2/test_images.py b/tests/integration_tests/ec2/test_images.py new file mode 100644 index 00000000..16720aac --- /dev/null +++ b/tests/integration_tests/ec2/test_images.py @@ -0,0 +1,51 @@ +"""EC2 integration tests testing image related functionality.""" + +import logging + +import pytest + +from pycloudlib.cloud import ImageType +from pycloudlib.ec2.cloud import EC2 + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def ec2_cloud(): + """ + Fixture to create an EC2 instance for testing. + + Yields: + EC2: An instance of the EC2 cloud class. + """ + with EC2(tag="integration-test-images") as ec2: + yield ec2 + + +def test_finding_all_image_types_focal(ec2_cloud: EC2): + """ + Tests that all image types are available for the focal suite and that they are all unique. + + As per issue #481, focal has both `fips` and `fips-updates` image types and previous to + introducing the `PRO_FIPS_UPDATES` image type, the `PRO_FIPS` image type could return a + `PRO_FIPS_UPDATES` image if it was newer. This test asserts that PR #483 prevents this from + happening. + + Test assertions: + - All image types are available for the focal suite (exception is raised if not). + - No daily images returned per image type are the same (same image ID). + """ + suite = "focal" + images: dict[ImageType, str] = {} + # iterate through all ImageType enum values + for image_type in ImageType: + images[image_type] = ec2_cloud.daily_image(release=suite, image_type=image_type) + logger.info( + "Found %s image for %s: %s", + image_type, + suite, + images[image_type], + ) + + # make sure that none of the images are the same + assert len(set(images.values())) == len(images), f"Not all images are unique: {images}" diff --git a/tests/integration_tests/gce/test_images.py b/tests/integration_tests/gce/test_images.py new file mode 100644 index 00000000..3391b740 --- /dev/null +++ b/tests/integration_tests/gce/test_images.py @@ -0,0 +1,73 @@ +"""GCE integration tests testing image related functionality.""" + +import logging + +import pytest + +from pycloudlib.cloud import ImageType +from pycloudlib.errors import ImageNotFoundError +from pycloudlib.gce.cloud import GCE + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def gce_cloud(): + """ + Fixture to create a GCE instance for testing. + + Yields: + GCE: An instance of the GCE cloud class. + """ + with GCE(tag="integration-test-images") as gce: + yield gce + +@pytest.mark.parametrize( + "release, unavailable_image_types", + ( + pytest.param( + "focal", + [ImageType.PRO_FIPS_UPDATES], + id="focal", + ), + pytest.param( + "jammy", + [ImageType.PRO_FIPS], + id="jammy", + ), + ), +) +def test_finding_all_image_types_focal( + gce_cloud: GCE, + release: str, + unavailable_image_types: list[ImageType], +): + """ + Tests that all image types are available for the focal suite and that they are all unique. + + Test assertions: + - Certain image types are unavailable for the given release (exception is raised if not). + - No daily images returned per image type are the same (same image ID). + """ + images: dict[ImageType, str] = {} + # iterate through all ImageType enum values + for image_type in ImageType: + if image_type in unavailable_image_types: + with pytest.raises(ImageNotFoundError) as exc_info: + gce_cloud.daily_image(release=release, image_type=image_type) + logger.info( + "Confirmed that %s image for %s is unavailable.", + image_type, + release, + ) + else: + images[image_type] = gce_cloud.daily_image(release=release, image_type=image_type) + logger.info( + "Found %s image for %s: %s", + image_type, + release, + images[image_type], + ) + + # make sure that none of the images are the same + assert len(set(images.values())) == len(images), f"Not all images are unique: {images}" diff --git a/tests/unit_tests/ec2/test_cloud.py b/tests/unit_tests/ec2/test_cloud.py index 0e6dc30c..c214ce58 100644 --- a/tests/unit_tests/ec2/test_cloud.py +++ b/tests/unit_tests/ec2/test_cloud.py @@ -70,9 +70,16 @@ class TestEC2: "noble", ImageType.PRO_FIPS, False, - "ubuntu-pro-fips*/images/hvm-ssd-gp3/ubuntu-noble-24.04-*", + "ubuntu-pro-fips-server/images/hvm-ssd-gp3/ubuntu-noble-24.04-*", id="pro-fips-lts", ), + pytest.param( + "jammy", + ImageType.PRO_FIPS_UPDATES, + False, + "ubuntu-pro-fips-updates-server/images/hvm-ssd/ubuntu-jammy-22.04-*", + id="pro-fips-updates-lts", + ), # Test GENERIC with non-LTS release and daily = False pytest.param( "oracular", @@ -140,6 +147,7 @@ def test_get_owner_for_all_image_types(self): ImageType.MINIMAL: "099720109477", ImageType.PRO: "099720109477", ImageType.PRO_FIPS: "aws-marketplace", + ImageType.PRO_FIPS_UPDATES: "aws-marketplace", } ec2 = FakeEC2() diff --git a/tests/unit_tests/gce/test_cloud.py b/tests/unit_tests/gce/test_cloud.py index ae9e6264..4771e975 100644 --- a/tests/unit_tests/gce/test_cloud.py +++ b/tests/unit_tests/gce/test_cloud.py @@ -305,6 +305,11 @@ def test_daily_image_returns_latest_from_query( # noqa: D102 ImageType.PRO_FIPS, "ubuntu-pro-fips-2004-focal-*", ), + pytest.param( + "jammy", + ImageType.PRO_FIPS_UPDATES, + "ubuntu-pro-fips-updates-2204-jammy-*", + ), ], ) def test_get_name_filter(self, release, image_type, expected_name_filter, gce):