Skip to content
This repository has been archived by the owner on Dec 31, 2023. It is now read-only.

feat: Adding code samples and tests for them #55

Merged
merged 18 commits into from Jun 6, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -8,4 +8,4 @@
* @googleapis/yoshi-python


/samples/ @googleapis/python-samples-owners
/samples/ m-strzelczyk @googleapis/python-samples-owners
m-strzelczyk marked this conversation as resolved.
Show resolved Hide resolved
18 changes: 18 additions & 0 deletions CONTRIBUTING.rst
Expand Up @@ -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 <name of test>

********************************************
Note About ``README`` as it pertains to PyPI
********************************************
Expand Down
24 changes: 22 additions & 2 deletions noxfile.py
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand Down
38 changes: 38 additions & 0 deletions 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend using gcloud auth application-default login instead. It's more secure if you don't have to download a JSON key. It's also easier. You can skip the next 3 steps with this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Good point. Done.

[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
```
246 changes: 246 additions & 0 deletions 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style suggestion for consistency with existing python samples.

Suggested change
import google.cloud.compute_v1 as gce
from google.cloud import compute_v1


# [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.
busunkim96 marked this conversation as resolved.
Show resolved Hide resolved
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)
busunkim96 marked this conversation as resolved.
Show resolved Hide resolved
return instance_list


# [END compute_instances_list]


# [START compute_instances_list_all]
m-strzelczyk marked this conversation as resolved.
Show resolved Hide resolved
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
m-strzelczyk marked this conversation as resolved.
Show resolved Hide resolved
) -> 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()
m-strzelczyk marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a stray comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Changed that part already.

operation = wait_for_operation(operation, project)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LRO experience looks different from existing GAPICs. @vam-google Does the Compute API have a non-standard LRO representation? It doesn't look like the generator produced the usual google.api_core.Operation type.

For comparison, see videointelligence where you can do operation.result(timeout=90) to poll for the result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how I saw the team use the operations in their test code. It is an instance of google.cloud.compute_v1.types.Operation and not google.api_core.Operation.

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the sample show error handling? (Even if it is just printing out the error/warnings?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)
m-strzelczyk marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions samples/snippets/requirements-test.txt
@@ -0,0 +1,2 @@
google-cloud-compute
busunkim96 marked this conversation as resolved.
Show resolved Hide resolved
pytest
m-strzelczyk marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions samples/snippets/requirements.txt
@@ -0,0 +1 @@
google-cloud-compute==0.3.0