From a2508727ae8fc0edfa699506df11d31f3f3901c6 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Tue, 6 Jul 2021 15:35:34 +0200 Subject: [PATCH 1/7] feat: Adding start/stop compute samples. --- samples/snippets/sample_start_stop.py | 116 ++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 samples/snippets/sample_start_stop.py diff --git a/samples/snippets/sample_start_stop.py b/samples/snippets/sample_start_stop.py new file mode 100644 index 000000000..815f84ce2 --- /dev/null +++ b/samples/snippets/sample_start_stop.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python + +# Copyright 2021 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. + +""" +A sample script showing how to start and stop Google Compute Engine instances. +""" +from google.cloud import compute_v1 + + +# [START compute_start_instance] +def start_instance(project_id: str, zone: str, instance_name: str): + """ + Starts a stopped Google Compute Engine instance (with unencrypted disks). + + Args: + project_id: project ID or project number of the Cloud project your instance belongs to. + zone: name of the zone your instance belongs to. + instance_name: name of the instance your want to start. + """ + instance_client = compute_v1.InstancesClient() + op_client = compute_v1.ZoneOperationsClient() + + op = instance_client.start(project=project_id, zone=zone, instance=instance_name) + + op_client.wait(project=project_id, zone=zone, operation=op.name) + return +# [END compute_start_instance] + + +# [START compute_start_enc_instance] +def start_instance_with_encryption_key(project_id: str, zone: str, instance_name: str, key: bytes): + """ + Starts a stopped Google Compute Engine instance (with encrypted disks). + + Args: + project_id: project ID or project number of the Cloud project your instance belongs to. + zone: name of the zone your instance belongs to. + instance_name: name of the instance your want to start. + key: bytes object representing a raw base64 encoded key to your machines boot disk. + For more information about disk encryption see: + https://cloud.google.com/compute/docs/disks/customer-supplied-encryption#specifications + """ + instance_client = compute_v1.InstancesClient() + op_client = compute_v1.ZoneOperationsClient() + + instance_data = instance_client.get(project=project_id, zone=zone, instance=instance_name) + + # Prepare the information about disk encryption + disk_data = compute_v1.CustomerEncryptionKeyProtectedDisk() + disk_data.source = instance_data.disks[0].source + disk_data.disk_encryption_key = compute_v1.CustomerEncryptionKey() + # Use raw_key to send over the key to unlock the disk + # To use a key stored in KMS, you need to provide `kms_key_name` and `kms_key_service_account` + disk_data.disk_encryption_key.raw_key = key + enc_data = compute_v1.InstancesStartWithEncryptionKeyRequest() + enc_data.disks = [disk_data] + + op = instance_client.start_with_encryption_key(project=project_id, zone=zone, instance=instance_name, + instances_start_with_encryption_key_request_resource=enc_data) + + op_client.wait(project=project_id, zone=zone, operation=op.name) + return +# [END compute_start_enc_instance] + + +# [START compute_stop_instance] +def stop_instance(project_id: str, zone: str, instance_name: str): + """ + Stops a stopped Google Compute Engine instance. + + Args: + project_id: project ID or project number of the Cloud project your instance belongs to. + zone: name of the zone your instance belongs to. + instance_name: name of the instance your want to stop. + """ + instance_client = compute_v1.InstancesClient() + op_client = compute_v1.ZoneOperationsClient() + + op = instance_client.stop(project=project_id, zone=zone, instance=instance_name) + + op_client.wait(project=project_id, zone=zone, operation=op.name) + return +# [END compute_stop_instance] + + +# [START compute_reset_instance] +def reset_instance(project_id: str, zone: str, instance_name: str): + """ + Resets a stopped Google Compute Engine instance (with unencrypted disks). + + Args: + project_id: project ID or project number of the Cloud project your instance belongs to. + zone: name of the zone your instance belongs to. + instance_name: name of the instance your want to reset. + """ + instance_client = compute_v1.InstancesClient() + op_client = compute_v1.ZoneOperationsClient() + + op = instance_client.reset(project=project_id, zone=zone, instance=instance_name) + + op_client.wait(project=project_id, zone=zone, operation=op.name) + return +# [END compute_reset_instance] From 69d6f838150123b388825c9b847663dd1a563594 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Mon, 12 Jul 2021 15:26:40 +0200 Subject: [PATCH 2/7] feat: Adding tests to samples_start_stop --- samples/snippets/test_sample_start_stop.py | 150 +++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 samples/snippets/test_sample_start_stop.py diff --git a/samples/snippets/test_sample_start_stop.py b/samples/snippets/test_sample_start_stop.py new file mode 100644 index 000000000..ea28bfa3f --- /dev/null +++ b/samples/snippets/test_sample_start_stop.py @@ -0,0 +1,150 @@ +# Copyright 2021 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. +import base64 +import random +import string +import time +import uuid + +import google.auth +from google.cloud import compute_v1 + +import pytest + +from samples.snippets.sample_start_stop import start_instance, start_instance_with_encryption_key, stop_instance + +PROJECT = google.auth.default()[1] + +INSTANCE_ZONE = "europe-central2-b" + +KEY = "".join(random.sample(string.ascii_letters, 32)) +KEY_B64 = base64.b64encode(KEY.encode()) # for example: b'VEdORldtY3NKellPdWRDcUF5YlNVREtJdm5qaFJYSFA=' + + +def _make_disk(raw_key: bytes = None): + disk = compute_v1.AttachedDisk() + initialize_params = compute_v1.AttachedDiskInitializeParams() + initialize_params.source_image = ( + "projects/debian-cloud/global/images/family/debian-10" + ) + initialize_params.disk_size_gb = "10" + disk.initialize_params = initialize_params + disk.auto_delete = True + disk.boot = True + disk.type_ = compute_v1.AttachedDisk.Type.PERSISTENT + + if raw_key: + disk.disk_encryption_key = compute_v1.CustomerEncryptionKey() + disk.disk_encryption_key.raw_key = raw_key + + return disk + + +def _make_request(disk: compute_v1.AttachedDisk): + network_interface = compute_v1.NetworkInterface() + network_interface.name = 'default' + network_interface.access_configs = [] + + # Collect information into the Instance object. + instance = compute_v1.Instance() + instance.name = "i" + uuid.uuid4().hex[:10] + instance.disks = [disk] + full_machine_type_name = f"zones/{INSTANCE_ZONE}/machineTypes/e2-micro" + instance.machine_type = full_machine_type_name + instance.network_interfaces = [network_interface] + + # Prepare the request to insert an instance. + request = compute_v1.InsertInstanceRequest() + request.zone = INSTANCE_ZONE + request.project = PROJECT + request.instance_resource = instance + return request + + +def _create_instance(request: compute_v1.InsertInstanceRequest): + instance_client = compute_v1.InstancesClient() + operation_client = compute_v1.ZoneOperationsClient() + + operation = instance_client.insert(request=request) + operation_client.wait(operation=operation.name, zone=INSTANCE_ZONE, project=PROJECT) + + return instance_client.get(project=PROJECT, zone=INSTANCE_ZONE, instance=request.instance_resource.name) + + +def _delete_instance(instance: compute_v1.Instance): + instance_client = compute_v1.InstancesClient() + operation_client = compute_v1.ZoneOperationsClient() + + operation = instance_client.delete(project=PROJECT, zone=INSTANCE_ZONE, instance=instance.name) + operation_client.wait(operation=operation.name, zone=INSTANCE_ZONE, project=PROJECT) + + +def _get_status(instance: compute_v1.Instance) -> compute_v1.Instance.Status: + instance_client = compute_v1.InstancesClient() + return instance_client.get(project=PROJECT, zone=INSTANCE_ZONE, instance=instance.name).status + + +@pytest.fixture +def compute_instance(): + disk = _make_disk() + request = _make_request(disk) + + instance = _create_instance(request) + + yield instance + + _delete_instance(instance) + + +@pytest.fixture +def compute_encrypted_instance(): + disk = _make_disk(KEY_B64) + request = _make_request(disk) + + instance = _create_instance(request) + + yield instance + + _delete_instance(instance) + + +def test_instance_operations(compute_instance): + assert _get_status(compute_instance) == compute_v1.Instance.Status.RUNNING + + stop_instance(PROJECT, INSTANCE_ZONE, compute_instance.name) + + while _get_status(compute_instance) == compute_v1.Instance.Status.STOPPING: + # Since we can't configure timeout parameter for operation wait() (b/188037306) + # We need to do some manual waiting for the stopping to finish... + time.sleep(5) + + assert _get_status(compute_instance) == compute_v1.Instance.Status.TERMINATED + + start_instance(PROJECT, INSTANCE_ZONE, compute_instance.name) + assert _get_status(compute_instance) == compute_v1.Instance.Status.RUNNING + + +def test_instance_encrypted(compute_encrypted_instance): + assert _get_status(compute_encrypted_instance) == compute_v1.Instance.Status.RUNNING + + stop_instance(PROJECT, INSTANCE_ZONE, compute_encrypted_instance.name) + while _get_status(compute_encrypted_instance) == compute_v1.Instance.Status.STOPPING: + # Since we can't configure timeout parameter for operation wait() (b/188037306) + # We need to do some manual waiting for the stopping to finish... + time.sleep(5) + + assert _get_status(compute_encrypted_instance) == compute_v1.Instance.Status.TERMINATED + + start_instance_with_encryption_key(PROJECT, INSTANCE_ZONE, compute_encrypted_instance.name, KEY_B64) + assert _get_status(compute_encrypted_instance) == compute_v1.Instance.Status.RUNNING From 164379ca425e22eac11c70443fd8fb0e295cf878 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Thu, 22 Jul 2021 13:34:58 +0200 Subject: [PATCH 3/7] chore: reverting back to use BUILD_SPECIFIC_GCLOUD_PROJECT --- samples/snippets/noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 6a8ccdae2..2776c1e51 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -49,8 +49,8 @@ # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string # to use your own Cloud project. - 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', - # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', + 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', # If you need to use a specific version of pip, # change pip_version_override to the string representation # of the version number, for example, "20.2.4" From 41145c12ec39de15d7dcef7bda5b2f7905c5614d Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Thu, 22 Jul 2021 15:08:02 +0200 Subject: [PATCH 4/7] Revert "chore: reverting back to use BUILD_SPECIFIC_GCLOUD_PROJECT" This reverts commit 164379ca425e22eac11c70443fd8fb0e295cf878. --- samples/snippets/noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 2776c1e51..6a8ccdae2 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -49,8 +49,8 @@ # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string # to use your own Cloud project. - # 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', - 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', # If you need to use a specific version of pip, # change pip_version_override to the string representation # of the version number, for example, "20.2.4" From deb87d18a1f3d0759a44b0d5e1dccdfaaa48f8a8 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Thu, 22 Jul 2021 15:51:50 +0200 Subject: [PATCH 5/7] fix: fixing the noxfile_config situation. --- noxfile_config.py | 1 + 1 file changed, 1 insertion(+) create mode 120000 noxfile_config.py diff --git a/noxfile_config.py b/noxfile_config.py new file mode 120000 index 000000000..b4df055af --- /dev/null +++ b/noxfile_config.py @@ -0,0 +1 @@ +noxfile_config.py \ No newline at end of file From 0ec928e341f97c59fa5f932006079d5fc909f7c6 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Mon, 26 Jul 2021 17:56:29 +0200 Subject: [PATCH 6/7] chore: removing symlink --- noxfile_config.py | 1 - 1 file changed, 1 deletion(-) delete mode 120000 noxfile_config.py diff --git a/noxfile_config.py b/noxfile_config.py deleted file mode 120000 index b4df055af..000000000 --- a/noxfile_config.py +++ /dev/null @@ -1 +0,0 @@ -noxfile_config.py \ No newline at end of file From b0bdbc37619b36a2359eafc49d36b2be76d6cf43 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Mon, 2 Aug 2021 13:48:34 +0200 Subject: [PATCH 7/7] chore: Making the default_values tests be more stable. --- samples/snippets/test_sample_default_values.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/samples/snippets/test_sample_default_values.py b/samples/snippets/test_sample_default_values.py index b6d2f0acc..613c6efa3 100644 --- a/samples/snippets/test_sample_default_values.py +++ b/samples/snippets/test_sample_default_values.py @@ -11,6 +11,7 @@ # 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 time import typing import uuid @@ -37,6 +38,7 @@ def temp_bucket(): def test_set_usage_export_bucket_default(capsys: typing.Any, temp_bucket: storage.Bucket) -> None: set_usage_export_bucket(project_id=PROJECT, bucket_name=temp_bucket.name) + time.sleep(5) # To make sure the settings are properly updated uel = get_usage_export_bucket(project_id=PROJECT) assert(uel.bucket_name == temp_bucket.name) assert(uel.report_name_prefix == 'usage_gce') @@ -44,6 +46,7 @@ def test_set_usage_export_bucket_default(capsys: typing.Any, assert('default prefix of `usage_gce`.' in out) disable_usage_export(project_id=PROJECT) + time.sleep(5) # To make sure the settings are properly updated uel = get_usage_export_bucket(project_id=PROJECT) assert(uel.bucket_name == '') assert(uel.report_name_prefix == '') @@ -53,6 +56,7 @@ def test_set_usage_export_bucket_custom(capsys: typing.Any, temp_bucket: storage.Bucket) -> None: set_usage_export_bucket(project_id=PROJECT, bucket_name=temp_bucket.name, report_name_prefix=TEST_PREFIX) + time.sleep(5) # To make sure the settings are properly updated uel = get_usage_export_bucket(project_id=PROJECT) assert(uel.bucket_name == temp_bucket.name) assert(uel.report_name_prefix == TEST_PREFIX) @@ -60,6 +64,7 @@ def test_set_usage_export_bucket_custom(capsys: typing.Any, assert('usage_gce' not in out) disable_usage_export(project_id=PROJECT) + time.sleep(5) # To make sure the settings are properly updated uel = get_usage_export_bucket(project_id=PROJECT) assert(uel.bucket_name == '') assert(uel.report_name_prefix == '')