From e33a206190af7f274ac07e5ec1b89731a82519dd Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Wed, 19 May 2021 14:24:59 +0200 Subject: [PATCH 1/2] Adding code samples to repository and nox. --- .github/CODEOWNERS | 2 +- noxfile.py | 24 ++- samples/snippets/README.md | 38 ++++ samples/snippets/quickstart.py | 246 +++++++++++++++++++++++++ samples/snippets/requirements-test.txt | 2 + samples/snippets/requirements.txt | 1 + samples/snippets/test_quickstart.py | 36 ++++ 7 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 samples/snippets/README.md create mode 100644 samples/snippets/quickstart.py create mode 100644 samples/snippets/requirements-test.txt create mode 100644 samples/snippets/requirements.txt create mode 100644 samples/snippets/test_quickstart.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d08d98456..779791b16 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,4 @@ * @googleapis/yoshi-python -/samples/ @googleapis/python-samples-owners \ No newline at end of file +/samples/ m-strzelczyk @googleapis/python-samples-owners \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index ae624edf3..2cd8d65a1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -25,11 +25,12 @@ BLACK_VERSION = "black==19.10b0" -BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] +BLACK_PATHS = ["docs", "google", "samples", "tests", "noxfile.py", "setup.py"] DEFAULT_PYTHON_VERSION = "3.8" SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"] UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] +SAMPLE_TEST_PYTHON_VERSIONS = ["3.8", "3.9"] CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() @@ -59,7 +60,7 @@ def lint(session): session.run( "black", "--check", *BLACK_PATHS, ) - session.run("flake8", "google", "tests") + session.run("flake8", "google", "tests", "samples") @nox.session(python=DEFAULT_PYTHON_VERSION) @@ -112,6 +113,25 @@ def unit(session): default(session) +@nox.session(python=SAMPLE_TEST_PYTHON_VERSIONS) +def samples(session): + """Run tests for samples""" + samples_test_folder_path = CURRENT_DIRECTORY / "samples" + requirements_path = ( + CURRENT_DIRECTORY / "samples" / "snippets" / "requirements-test.txt" + ) + + if not samples_test_folder_path.is_dir(): + session.skip("Sample tests not found.") + return + + session.install("-U", "pip", "setuptools") + session.install("-Ur", str(requirements_path)) + session.install("-e", ".") + + session.run("py.test", "--quiet", str(samples_test_folder_path), *session.posargs) + + @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): """Run the system test suite.""" diff --git a/samples/snippets/README.md b/samples/snippets/README.md new file mode 100644 index 000000000..db5baf403 --- /dev/null +++ b/samples/snippets/README.md @@ -0,0 +1,38 @@ +# google-cloud-compute library samples + +These samples demonstrate usage of the google-cloud-compute library to interact +with the Google Compute Engine API. + +## Running the quickstart script + +### Before you begin + +1. If you haven't already, set up a Python Development Environment by following the [python setup guide](https://cloud.google.com/python/setup) and +[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project). + +1. Create a service account with the 'Editor' permissions by following these +[instructions](https://cloud.google.com/iam/docs/creating-managing-service-accounts). + +1. [Download a JSON key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) to use to authenticate your script. + +1. Configure your local environment to use the acquired key. +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account/key.json +``` + +### Install requirements + +Create a new virtual environment and install the required libraries. +```bash +virtualenv --python python3 name-of-your-virtualenv +source name-of-your-virtualenv/bin/activate +pip install -r requirements.txt +``` + +### Run the demo + +Run the quickstart script, providing it with your project name, a GCP zone and a name for the instance that will be created and destroyed: +```bash +# For example, to create machine "test-instance" in europe-central2-a in project "my-test-project": +python quickstart.py my-test-project europe-central2-a test-instance +``` diff --git a/samples/snippets/quickstart.py b/samples/snippets/quickstart.py new file mode 100644 index 000000000..85f774f97 --- /dev/null +++ b/samples/snippets/quickstart.py @@ -0,0 +1,246 @@ +#!/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 create, list and delete Google Compute Engine +instances using the google-cloud-compute library. It can be run from command +line to create, list and delete an instance in a given project in a given zone. +""" + +import argparse + +# [START compute_instances_list] +# [START compute_instances_list_all] +# [START compute_instances_create] +# [START compute_instances_delete] +# [START compute_instances_operation_check] +import typing + +import google.cloud.compute_v1 as gce + +# [END compute_instances_operation_check] +# [END compute_instances_delete] +# [END compute_instances_create] +# [END compute_instances_list_all] +# [END compute_instances_list] + + +# [START compute_instances_list] +def list_instances(project: str, zone: str) -> typing.Iterable[gce.Instance]: + """ + Gets a list of instances created in given project in given zone. + Returns an iterable collection of Instance objects. + + Args: + project: Name of the project you want to use. + zone: Name of the zone you want to check, for example: us-west3-b + + Returns: + An iterable collection of Instance objects. + """ + instance_client = gce.InstancesClient() + instance_list = instance_client.list(project=project, zone=zone) + return instance_list + + +# [END compute_instances_list] + + +# [START compute_instances_list_all] +def list_all_instances(project: str) -> typing.Dict[str, typing.Iterable[gce.Instance]]: + """ + Returns a dictionary of all instances present in a project, grouped by their zone. + + Args: + project: Name of the project you want to use. + + Returns: + A dictionary with zone names as keys (in form of "zones/{zone_name}") and + iterable collections of Instance objects as values. + """ + instance_client = gce.InstancesClient() + agg_list = instance_client.aggregated_list(project=project) + all_instances = {} + for zone, response in agg_list: + if response.instances: + all_instances[zone] = response.instances + return all_instances + + +# [END compute_instances_list_all] + + +# [START compute_instances_create] +def create_instance( + project: str, zone: str, machine_type: str, machine_name: str, source_image: str +) -> gce.Instance: + """ + Sends an instance creation request to GCP and waits for it to complete. + + Args: + project: Name of the project you want to use. + zone: Name of the zone you want to use, for example: us-west3-b + machine_type: Machine type you want to create in following format: + "zones/{zone}/machineTypes/{type_name}". For example: + "zones/europe-west3-c/machineTypes/f1-micro" + machine_name: Name of the new machine. + source_image: Path the the disk image you want to use for your boot + disk. This can be one of the public images + (e.g. "projects/debian-cloud/global/images/family/debian-10") + or a private image you have access to. + + Returns: + Instance object. + """ + instance_client = gce.InstancesClient() + + # Every machine requires at least one persistent disk + disk = gce.AttachedDisk() + initialize_params = gce.AttachedDiskInitializeParams() + initialize_params.source_image = ( + 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_ = gce.AttachedDisk.Type.PERSISTENT + + # Every machine needs to be connected to a VPC network. + # The 'default' network is created automatically in every project. + network_interface = gce.NetworkInterface() + network_interface.name = "default" + + # Collecting all the information into the Instance object + instance = gce.Instance() + instance.name = machine_name + instance.disks = [disk] + instance.machine_type = ( + machine_type # "zones/europe-central2-a/machineTypes/n1-standard-8" + ) + instance.network_interfaces = [network_interface] + + # Preparing the InsertInstanceRequest + request = gce.InsertInstanceRequest() + request.zone = zone # "europe-central2-a" + request.project = project # "diregapic-mestiv" + request.instance_resource = instance + + print(f"Creating the {machine_name} instance in {zone}...") + operation = instance_client.insert(request=request) + # wait_result = operation_client.wait(operation=operation.name, zone=zone, project=project) + operation = wait_for_operation(operation, project) + if operation.error: + pass + if operation.warnings: + pass + print(f"Instance {machine_name} created.") + return instance + + +# [END compute_instances_create] + + +# [START compute_instances_delete] +def delete_instance(project: str, zone: str, machine_name: str) -> None: + """ + Sends a delete request to GCP and waits for it to complete. + + Args: + project: Name of the project you want to use. + zone: Name of the zone you want to use, for example: us-west3-b + machine_name: Name of the machine you want to delete. + """ + instance_client = gce.InstancesClient() + + print(f"Deleting {machine_name} from {zone}...") + operation = instance_client.delete( + project=project, zone=zone, instance=machine_name + ) + operation = wait_for_operation(operation, project) + if operation.error: + pass + if operation.warnings: + pass + print(f"Instance {machine_name} deleted.") + return + + +# [END compute_instances_delete] + + +# [START compute_instances_operation_check] +def wait_for_operation(operation: gce.Operation, project: str) -> gce.Operation: + """ + This method waits for an operation to be completed. Calling this function + will block until the operation is finished. + + Args: + operation: The Operation object representing the operation you want to + wait on. + project: Name of the project owning the operation. + + Returns: + Finished Operation object. + """ + kwargs = {"project": project, "operation": operation.name} + if operation.zone: + client = gce.ZoneOperationsClient() + # Operation.zone is a full URL address of a zone, so we need to extract just the name + kwargs["zone"] = operation.zone.rsplit("/", maxsplit=1)[1] + elif operation.region: + client = gce.RegionOperationsClient() + # Operation.region is a full URL address of a zone, so we need to extract just the name + kwargs["region"] = operation.region.rsplit("/", maxsplit=1)[1] + else: + client = gce.GlobalOperationsClient() + return client.wait(**kwargs) + + +# [END compute_instances_operation_check] + + +def main(project: str, zone: str, machine_name: str) -> None: + # You can find the list of available machine types using: + # https://cloud.google.com/sdk/gcloud/reference/compute/machine-types/list + machine_type = f"zones/{zone}/machineTypes/f1-micro" + # You can check the list of available public images using: + # gcloud compute images list + source_image = "projects/debian-cloud/global/images/family/debian-10" + + create_instance(project, zone, machine_type, machine_name, source_image) + + zone_instances = list_instances(project, zone) + print(f"Instances found in {zone}:", ", ".join(i.name for i in zone_instances)) + + all_instances = list_all_instances(project) + print(f"Instances found in project {project}:") + for i_zone, instances in all_instances.items(): + print(f"{i_zone}:", ", ".join(i.name for i in instances)) + + delete_instance(project, zone, machine_name) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="Google Cloud project ID") + parser.add_argument("zone", help="Google Cloud zone name") + parser.add_argument("machine_name", help="Name for the demo machine") + + args = parser.parse_args() + + main(args.project_id, args.zone, args.machine_name) diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt new file mode 100644 index 000000000..1de9c6f0d --- /dev/null +++ b/samples/snippets/requirements-test.txt @@ -0,0 +1,2 @@ +google-cloud-compute +pytest \ No newline at end of file diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt new file mode 100644 index 000000000..8a2294e1e --- /dev/null +++ b/samples/snippets/requirements.txt @@ -0,0 +1 @@ +google-cloud-compute==0.3.0 \ No newline at end of file diff --git a/samples/snippets/test_quickstart.py b/samples/snippets/test_quickstart.py new file mode 100644 index 000000000..253e87be8 --- /dev/null +++ b/samples/snippets/test_quickstart.py @@ -0,0 +1,36 @@ +# 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 os +import re +import typing +import uuid +import google.auth + +from samples.snippets.quickstart import main + +PROJECT = google.auth.default()[1] +INSTANCE_NAME = "i" + uuid.uuid4().hex[:10] +INSTANCE_ZONE = "europe-central2-b" + + +def test_main(capsys: typing.Any) -> None: + main(PROJECT, INSTANCE_ZONE, INSTANCE_NAME) + + out, _ = capsys.readouterr() + + assert f"Instance {INSTANCE_NAME} created." in out + assert re.search(f"Instances found in {INSTANCE_ZONE}:.+{INSTANCE_NAME}", out) + assert re.search(f"zones/{INSTANCE_ZONE}:.+{INSTANCE_NAME}", out) + assert f"Instance {INSTANCE_NAME} deleted." in out From 5cd4429c1e4009b5cde6b29cb8c06449e895d913 Mon Sep 17 00:00:00 2001 From: Maciej Strzelczyk Date: Wed, 19 May 2021 14:32:32 +0200 Subject: [PATCH 2/2] Update README with samples section. --- CONTRIBUTING.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index afc0cdfb1..c8bd4fe5f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -182,6 +182,24 @@ Build the docs via: $ nox -s docs +************************* +Samples and code snippets +************************* + +Code samples and snippets live in the `samples/` catalogue. Feel free to +provide more examples, but make sure to write tests for those examples. + +The tests will run against a real Google Cloud Project, so you should +configure them just like the System Tests. + +- To run sample tests, you can execute:: + + # Run all system tests + $ nox -s samples-3.8 + + # Run a single sample test + $ nox -s system-3.8 -- -k + ******************************************** Note About ``README`` as it pertains to PyPI ********************************************