diff --git a/compute/compute/ingredients/images/create.py b/compute/compute/ingredients/images/create.py new file mode 100644 index 000000000000..805f1e9c7b48 --- /dev/null +++ b/compute/compute/ingredients/images/create.py @@ -0,0 +1,83 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# This is an ingredient file. It is not meant to be run directly. Check the samples/snippets +# folder for complete code samples that are ready to be used. +# Disabling flake8 for the ingredients file, as it would fail F821 - undefined name check. +# flake8: noqa +import time + +from google.cloud import compute_v1 +import warnings + +# +STOPPED_MACHINE_STATUS = ( + compute_v1.Instance.Status.TERMINATED.name, + compute_v1.Instance.Status.STOPPED.name +) + + +def create_image(project_id: str, zone: str, source_disk_name: str, image_name: str, + storage_location: str = None, force_create: bool = False) -> compute_v1.Image: + """ + Creates a new disk image. + + Args: + project_id: project ID or project number of the Cloud project you use. + zone: zone of the disk you copy from. + source_disk_name: name of the source disk you copy from. + image_name: name of the image you want to create. + storage_location: storage location for the image. If the value is undefined, + function will store the image in the multi-region closest to your image's + source location. + force_create: create the image even if the source disk is attached to a + running instance. + """ + image_client = compute_v1.ImagesClient() + disk_client = compute_v1.DisksClient() + instance_client = compute_v1.InstancesClient() + + # Get source disk + disk = disk_client.get(project=project_id, zone=zone, disk=source_disk_name) + + for disk_user in disk.users: + instance = instance_client.get(project=project_id, zone=zone, instance=disk_user) + if instance.status in STOPPED_MACHINE_STATUS: + continue + if not force_create: + raise RuntimeError(f"Instance {disk_user} should be stopped. For Windows instances please " + f"stop the instance using `GCESysprep` command. For Linux instances just " + f"shut it down normally. You can supress this error and create an image of" + f"the disk by setting `force_create` parameter to true (not recommended). \n" + f"More information here: \n" + f" * https://cloud.google.com/compute/docs/instances/windows/creating-windows-os-image#api \n" + f" * https://cloud.google.com/compute/docs/images/create-delete-deprecate-private-images#prepare_instance_for_image") + else: + warnings.warn(f"Warning: The `force_create` option may compromise the integrity of your image. " + f"Stop the {disk_user} instance before you create the image if possible.") + + # Create image + image = compute_v1.Image() + image.source_disk = disk.self_link + image.name = image_name + if storage_location: + image.storage_locations = [storage_location] + + operation = image_client.insert(project=project_id, image_resource=image) + + wait_for_extended_operation(operation, "image creation") + + return image_client.get(project=project_id, image=image_name) +# diff --git a/compute/compute/ingredients/images/delete.py b/compute/compute/ingredients/images/delete.py new file mode 100644 index 000000000000..18059fd5348f --- /dev/null +++ b/compute/compute/ingredients/images/delete.py @@ -0,0 +1,37 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# This is an ingredient file. It is not meant to be run directly. Check the samples/snippets +# folder for complete code samples that are ready to be used. +# Disabling flake8 for the ingredients file, as it would fail F821 - undefined name check. +# flake8: noqa +from typing import NoReturn + +from google.cloud import compute_v1 + + +# +def delete_image(project_id: str, image_name: str) -> NoReturn: + """ + Deletes a disk image. + + Args: + project_id: project ID or project number of the Cloud project you use. + image_name: name of the image you want to delete. + """ + image_client = compute_v1.ImagesClient() + operation = image_client.delete(project=project_id, image=image_name) + wait_for_extended_operation(operation, "image deletion") +# diff --git a/compute/compute/recipes/images/create.py b/compute/compute/recipes/images/create.py new file mode 100644 index 000000000000..22c0d19cc502 --- /dev/null +++ b/compute/compute/recipes/images/create.py @@ -0,0 +1,24 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# flake8: noqa + +# +# +# + +# + +# +# +# diff --git a/compute/compute/recipes/images/delete.py b/compute/compute/recipes/images/delete.py new file mode 100644 index 000000000000..6796dae9eea8 --- /dev/null +++ b/compute/compute/recipes/images/delete.py @@ -0,0 +1,22 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# flake8: noqa + +# +# + +# + +# +# diff --git a/compute/compute/snippets/images/create.py b/compute/compute/snippets/images/create.py new file mode 100644 index 000000000000..af2a96bd8118 --- /dev/null +++ b/compute/compute/snippets/images/create.py @@ -0,0 +1,151 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# flake8: noqa + + +# This file is automatically generated. Please do not modify it directly. +# Find the relevant recipe file in the samples/recipes or samples/ingredients +# directory and apply your changes there. + + +# [START compute_windows_image_create] +# [START compute_images_create] +import sys +import time +from typing import Any +import warnings + +from google.api_core.extended_operation import ExtendedOperation +from google.cloud import compute_v1 + + +def wait_for_extended_operation( + operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300 +) -> Any: + """ + This method will wait for the extended (long-running) operation to + complete. If the operation is successful, it will return its result. + If the operation ends with an error, an exception will be raised. + If there were any warnings during the execution of the operation + they will be printed to sys.stderr. + + Args: + operation: a long-running operation you want to wait on. + verbose_name: (optional) a more verbose name of the operation, + used only during error and warning reporting. + timeout: how long (in seconds) to wait for operation to finish. + If None, wait indefinitely. + + Returns: + Whatever the operation.result() returns. + + Raises: + This method will raise the exception received from `operation.exception()` + or RuntimeError if there is no exception set, but there is an `error_code` + set for the `operation`. + + In case of an operation taking longer than `timeout` seconds to complete, + a `concurrent.futures.TimeoutError` will be raised. + """ + result = operation.result(timeout=timeout) + + if operation.error_code: + print( + f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}", + file=sys.stderr, + ) + print(f"Operation ID: {operation.name}") + raise operation.exception() or RuntimeError(operation.error_message) + + if operation.warnings: + print(f"Warnings during {verbose_name}:\n", file=sys.stderr) + for warning in operation.warnings: + print(f" - {warning.code}: {warning.message}", file=sys.stderr) + + return result + + +STOPPED_MACHINE_STATUS = ( + compute_v1.Instance.Status.TERMINATED.name, + compute_v1.Instance.Status.STOPPED.name, +) + + +def create_image( + project_id: str, + zone: str, + source_disk_name: str, + image_name: str, + storage_location: str = None, + force_create: bool = False, +) -> compute_v1.Image: + """ + Creates a new disk image. + + Args: + project_id: project ID or project number of the Cloud project you use. + zone: zone of the disk you copy from. + source_disk_name: name of the source disk you copy from. + image_name: name of the image you want to create. + storage_location: storage location for the image. If the value is undefined, + function will store the image in the multi-region closest to your image's + source location. + force_create: create the image even if the source disk is attached to a + running instance. + """ + image_client = compute_v1.ImagesClient() + disk_client = compute_v1.DisksClient() + instance_client = compute_v1.InstancesClient() + + # Get source disk + disk = disk_client.get(project=project_id, zone=zone, disk=source_disk_name) + + for disk_user in disk.users: + instance = instance_client.get( + project=project_id, zone=zone, instance=disk_user + ) + if instance.status in STOPPED_MACHINE_STATUS: + continue + if not force_create: + raise RuntimeError( + f"Instance {disk_user} should be stopped. For Windows instances please " + f"stop the instance using `GCESysprep` command. For Linux instances just " + f"shut it down normally. You can supress this error and create an image of" + f"the disk by setting `force_create` parameter to true (not recommended). \n" + f"More information here: \n" + f" * https://cloud.google.com/compute/docs/instances/windows/creating-windows-os-image#api \n" + f" * https://cloud.google.com/compute/docs/images/create-delete-deprecate-private-images#prepare_instance_for_image" + ) + else: + warnings.warn( + f"Warning: The `force_create` option may compromise the integrity of your image. " + f"Stop the {disk_user} instance before you create the image if possible." + ) + + # Create image + image = compute_v1.Image() + image.source_disk = disk.self_link + image.name = image_name + if storage_location: + image.storage_locations = [storage_location] + + operation = image_client.insert(project=project_id, image_resource=image) + + wait_for_extended_operation(operation, "image creation") + + return image_client.get(project=project_id, image=image_name) + + +# [END compute_images_create] +# [END compute_windows_image_create] diff --git a/compute/compute/snippets/images/delete.py b/compute/compute/snippets/images/delete.py new file mode 100644 index 000000000000..7216674a2e1d --- /dev/null +++ b/compute/compute/snippets/images/delete.py @@ -0,0 +1,89 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# flake8: noqa + + +# This file is automatically generated. Please do not modify it directly. +# Find the relevant recipe file in the samples/recipes or samples/ingredients +# directory and apply your changes there. + + +# [START compute_images_delete] +import sys +from typing import Any, NoReturn + +from google.api_core.extended_operation import ExtendedOperation +from google.cloud import compute_v1 + + +def wait_for_extended_operation( + operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300 +) -> Any: + """ + This method will wait for the extended (long-running) operation to + complete. If the operation is successful, it will return its result. + If the operation ends with an error, an exception will be raised. + If there were any warnings during the execution of the operation + they will be printed to sys.stderr. + + Args: + operation: a long-running operation you want to wait on. + verbose_name: (optional) a more verbose name of the operation, + used only during error and warning reporting. + timeout: how long (in seconds) to wait for operation to finish. + If None, wait indefinitely. + + Returns: + Whatever the operation.result() returns. + + Raises: + This method will raise the exception received from `operation.exception()` + or RuntimeError if there is no exception set, but there is an `error_code` + set for the `operation`. + + In case of an operation taking longer than `timeout` seconds to complete, + a `concurrent.futures.TimeoutError` will be raised. + """ + result = operation.result(timeout=timeout) + + if operation.error_code: + print( + f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}", + file=sys.stderr, + ) + print(f"Operation ID: {operation.name}") + raise operation.exception() or RuntimeError(operation.error_message) + + if operation.warnings: + print(f"Warnings during {verbose_name}:\n", file=sys.stderr) + for warning in operation.warnings: + print(f" - {warning.code}: {warning.message}", file=sys.stderr) + + return result + + +def delete_image(project_id: str, image_name: str) -> NoReturn: + """ + Deletes a disk image. + + Args: + project_id: project ID or project number of the Cloud project you use. + image_name: name of the image you want to delete. + """ + image_client = compute_v1.ImagesClient() + operation = image_client.delete(project=project_id, image=image_name) + wait_for_extended_operation(operation, "image deletion") + + +# [END compute_images_delete] diff --git a/compute/compute/snippets/tests/test_images.py b/compute/compute/snippets/tests/test_images.py index 18852ac09a08..394114b29723 100644 --- a/compute/compute/snippets/tests/test_images.py +++ b/compute/compute/snippets/tests/test_images.py @@ -11,10 +11,37 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import uuid +import google.auth +from google.cloud import compute_v1 +import pytest + +from ..disks.create_from_image import create_disk_from_image +from ..disks.delete import delete_disk +from ..images.create import create_image +from ..images.delete import delete_image from ..images.get import get_image +from ..images.get import get_image_from_family from ..images.list import list_images +PROJECT = google.auth.default()[1] +ZONE = 'europe-central2-c' + + +@pytest.fixture +def test_disk(): + """ + Get the newest version of debian 11 and make a disk from it. + """ + new_debian = get_image_from_family('debian-cloud', 'debian-11') + test_disk_name = "test-disk-" + uuid.uuid4().hex[:10] + disk = create_disk_from_image(PROJECT, ZONE, test_disk_name, + f"zones/{ZONE}/diskTypes/pd-standard", + 20, new_debian.self_link) + yield disk + delete_disk(PROJECT, ZONE, test_disk_name) + def test_list_images(): images = list_images("debian-cloud") @@ -32,3 +59,18 @@ def test_get_image(): image2 = get_image("debian-cloud", image.name) assert image == image2 + + +def test_create_delete_image(test_disk): + test_image_name = "test-image-" + uuid.uuid4().hex[:10] + new_image = create_image(PROJECT, ZONE, test_disk.name, test_image_name) + try: + assert new_image.name == test_image_name + assert new_image.disk_size_gb == 20 + assert isinstance(new_image, compute_v1.Image) + finally: + delete_image(PROJECT, test_image_name) + + for image in list_images(PROJECT): + if image.name == test_image_name: + pytest.fail(f"Image {test_image_name} should have been deleted.")