diff --git a/samples/api-client/manager/README.md b/samples/api-client/manager/README.md deleted file mode 100644 index 94a9c235..00000000 --- a/samples/api-client/manager/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Cloud IoT Core Device Manager Python Sample - -This sample application shows you how to manage devices programmatically using -Python. - - -# Setup - -1. Use virtualenv to create a local Python environment. - - virtualenv env && source env/bin/activate - -2. Install the dependencies - - pip install -r requirements.txt - - -# Running the sample - -The following snippet summarizes usage for the device manager sample: - - usage: cloudiot_device_manager_example.py [-h] \ - --project_id PROJECT_ID \ - --pubsub_topic PUBSUB_TOPIC \ - --api_key API_KEY \ - [--ec_public_key_file EC_PUBLIC_KEY_FILE] \ - [--rsa_certificate_file RSA_CERTIFICATE_FILE] \ - [--cloud_region CLOUD_REGION] \ - [--service_account_json SERVICE_ACCOUNT_JSON] \ - [--registry_id REGISTRY_ID] - - -For example, if your project-id is `blue-jet-123` and your service account -credentials are stored in `creds.json` in your home folder, the following -command would run the sample: - - python cloudiot_device_manager_example.py \ - --project_id blue-jet-123 \ - --pubsub_topic projects/blue-jet-123/topics/device-events \ - --ec_public_key ../ec_public.pem \ - --rsa_certificate_file ../rsa_cert.pem \ - --api_key YOUR_API_KEY \ - --service_account_json $HOME/creds.json diff --git a/samples/api-client/manager/README.rst b/samples/api-client/manager/README.rst new file mode 100644 index 00000000..18e1653a --- /dev/null +++ b/samples/api-client/manager/README.rst @@ -0,0 +1,150 @@ +.. This file is automatically generated. Do not edit this file directly. + +Google Cloud IoT Core API Python Samples +=============================================================================== + +This directory contains samples for Google Cloud IoT Core API. `Google Cloud IoT Core`_ allows developers to easily integrate Publish and Subscribe functionality with devices and programmatically manage device authorization. + + + + +.. _Google Cloud IoT Core API: https://cloud.google.com/vision/docs + +Setup +------------------------------------------------------------------------------- + + +Authentication +++++++++++++++ + +Authentication is typically done through `Application Default Credentials`_, +which means you do not have to change the code to authenticate as long as +your environment has credentials. You have a few options for setting up +authentication: + +#. When running locally, use the `Google Cloud SDK`_ + + .. code-block:: bash + + gcloud auth application-default login + + +#. When running on App Engine or Compute Engine, credentials are already + set-up. However, you may need to configure your Compute Engine instance + with `additional scopes`_. + +#. You can create a `Service Account key file`_. This file can be used to + authenticate to Google Cloud Platform services from any environment. To use + the file, set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to + the path to the key file, for example: + + .. code-block:: bash + + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json + +.. _Application Default Credentials: https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow +.. _additional scopes: https://cloud.google.com/compute/docs/authentication#using +.. _Service Account key file: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount + +Install Dependencies +++++++++++++++++++++ + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. + +#. Create a virtualenv. Samples are compatible with Python 2.7 and 3.4+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ + +Samples +------------------------------------------------------------------------------- + +Manager ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +To run this sample: + +.. code-block:: bash + + $ python manager.py + + usage: manager.py [-h] --project_id PROJECT_ID --pubsub_topic PUBSUB_TOPIC + --api_key API_KEY [--ec_public_key_file EC_PUBLIC_KEY_FILE] + [--rsa_certificate_file RSA_CERTIFICATE_FILE] + [--cloud_region CLOUD_REGION] + [--service_account_json SERVICE_ACCOUNT_JSON] + [--registry_id REGISTRY_ID] [--device_id DEVICE_ID] + {create-es256,create-registry,create-rsa256,create-topic,create-unauth,delete-device,delete-registry,get,list,patch-es256,patch-rs256} + ... + + Example of using the Google Cloud IoT Core device manager to administer + devices. + + Usage example: + + python manager.py \ + --project_id=my-project-id \ + --pubsub_topic=projects/my-project-id/topics/my-topic-id \ + --api_key=YOUR_API_KEY \ + --ec_public_key_file=../ec_public.pem \ + --rsa_certificate_file=../rsa_cert.pem \ + --service_account_json=$HOME/service_account.json + list + + positional arguments: + {create-es256,create-registry,create-rsa256,create-topic,create-unauth,delete-device,delete-registry,get,list,patch-es256,patch-rs256} + create-es256 Create a new device with the given id, using ES256 for + authentication. + create-registry Gets or creates a device registry. + create-rsa256 Create a new device with the given id, using RS256 for + authentication. + create-topic Creates a PubSub Topic and grants access to Cloud IoT + Core. + create-unauth Create a new device without authentication. + delete-device Delete the device with the given id. + delete-registry Deletes the specified registry. + get Retrieve the device with the given id. + list List all devices in the registry. + patch-es256 Patch the device to add an ES256 public key to the + device. + patch-rs256 Patch the device to add an RSA256 public key to the + device. + + optional arguments: + -h, --help show this help message and exit + --project_id PROJECT_ID + GCP cloud project name. + --pubsub_topic PUBSUB_TOPIC + Google Cloud Pub/Sub topic. Format is + projects/project_id/topics/topic-id + --api_key API_KEY Your API key. + --ec_public_key_file EC_PUBLIC_KEY_FILE + Path to public ES256 key file. + --rsa_certificate_file RSA_CERTIFICATE_FILE + Path to RS256 certificate file. + --cloud_region CLOUD_REGION + GCP cloud region + --service_account_json SERVICE_ACCOUNT_JSON + Path to service account json file. + --registry_id REGISTRY_ID + Registry id. If not set, a name will be generated. + --device_id DEVICE_ID + Device id. + + + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ \ No newline at end of file diff --git a/samples/api-client/manager/README.rst.in b/samples/api-client/manager/README.rst.in new file mode 100644 index 00000000..27ca855b --- /dev/null +++ b/samples/api-client/manager/README.rst.in @@ -0,0 +1,21 @@ +# This file is used to generate README.rst + +product: + name: Google Cloud IoT Core API + short_name: Cloud IoT Core + url: https://cloud.google.com/vision/docs + description: > + `Google Cloud IoT Core`_ allows developers to easily integrate Publish and + Subscribe functionality with devices and programmatically manage device + authorization. + +setup: +- auth +- install_deps + +samples: +- name: Manager + file: manager.py + show_help: True + +cloud_client_library: false diff --git a/samples/api-client/manager/cloudiot_device_manager_example.py b/samples/api-client/manager/cloudiot_device_manager_example.py deleted file mode 100644 index ce7130dc..00000000 --- a/samples/api-client/manager/cloudiot_device_manager_example.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2017 Google Inc. All Rights Reserved. -# -# 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. - - -"""Example of using the Google Cloud IoT Core device manager to administer -devices. - -This example uses the Device Manager API to create, retrieve, disable, list and -delete Cloud IoT Core devices and registries, using both RSA and eliptic curve -keys for authentication. - -Before you run the sample, configure Cloud IoT Core as described in the -documentation at https://cloud.google.com/iot or by following the instructions -in the README located in the parent folder. - -Usage example: - - $ python cloudiot_device_manager_example.py \ - --project_id=my-project-id \ - --pubsub_topic=projects/my-project-id/topics/my-topic-id \ - --api_key=YOUR_API_KEY \ - --ec_public_key_file=ec_public.pem \ - --rsa_certificate_file=rsa_cert.pem \ - --service_account_json=service_account.json - -Troubleshooting: - - - If you get a 400 error when running the example, with the message "The API - Key and the authentication credential are from different projects" it means - that you are using the wrong API Key. Ensure that you are using the API key - from Google Cloud Platform's API Manager's Credentials page. -""" - -import argparse -import sys -import time - -from google.oauth2 import service_account -from googleapiclient import discovery -from googleapiclient.errors import HttpError - -API_SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] -API_VERSION = 'v1beta1' -DISCOVERY_API = 'https://cloudiot.googleapis.com/$discovery/rest' -SERVICE_NAME = 'cloudiot' - - -def discovery_url(api_key): - """Construct the discovery url for the given api key.""" - return '{}?version={}&key={}'.format(DISCOVERY_API, API_VERSION, api_key) - - -class DeviceRegistry(object): - """Administer a set of devices for a device registry.""" - - def __init__( - self, project_id, registry_id, cloud_region, - service_account_json, api_key, pubsub_topic): - """Lookup or create a device registry for the given project.""" - self.parent = 'projects/{}/locations/{}'.format( - project_id, cloud_region) - self.full_name = '{}/registries/{}'.format(self.parent, registry_id) - credentials = service_account.Credentials.from_service_account_file( - service_account_json) - scoped_credentials = credentials.with_scopes(API_SCOPES) - - if not credentials: - sys.exit( - 'Could not load service account credential from {}' - .format(service_account_json)) - - self._service = discovery.build( - SERVICE_NAME, - API_VERSION, - discoveryServiceUrl=discovery_url(api_key), - credentials=scoped_credentials) - - # Lookup or create the device registry. Here we bind the registry to - # the given Cloud Pub/Sub topic. All devices within a registry will - # have their telemetry data published to this topic, using attributes - # to indicate which device the data originated from. - body = { - 'eventNotificationConfig': { - 'pubsubTopicName': pubsub_topic - }, - 'id': registry_id - } - request = self._service.projects().locations().registries().create( - parent=self.parent, body=body) - - try: - response = request.execute() - print('Created registry', registry_id) - print(response) - except HttpError as e: - if e.resp.status == 409: - # Device registry already exists - print( - 'Registry', registry_id, - 'already exists - looking it up instead.') - request = self._service.projects().locations().registries( - ).get(name=self.full_name) - request.execute() - - else: - raise - - def delete(self): - """Delete this registry.""" - request = self._service.projects().locations().registries().delete( - name=self.full_name) - return request.execute() - - def list_devices(self): - """List all devices in the registry.""" - request = self._service.projects().locations().registries().devices( - ).list(parent=self.full_name) - response = request.execute() - return response.get('devices', []) - - def _create_device(self, device_template): - request = self._service.projects().locations().registries().devices( - ).create(parent=self.full_name, body=device_template) - return request.execute() - - def create_device_with_rs256(self, device_id, certificate_file): - """Create a new device with the given id, using RS256 for - authentication.""" - with open(certificate_file) as f: - certificate = f.read() - - # Create a device with the given certificate. Note that you can have - # multiple credentials associated with a device. - device_template = { - 'id': device_id, - 'credentials': [{ - 'publicKey': { - 'format': 'RSA_X509_PEM', - 'key': certificate - } - }] - } - return self._create_device(device_template) - - def create_device_with_es256(self, device_id, public_key_file): - """Create a new device with the given id, using ES256 for - authentication.""" - with open(public_key_file) as f: - public_key = f.read() - - # Create a device with the given public key. Note that you can have - # multiple credentials associated with a device. - device_template = { - 'id': device_id, - 'credentials': [{ - 'publicKey': { - 'format': 'ES256_PEM', - 'key': public_key - } - }] - } - return self._create_device(device_template) - - def create_device_with_no_auth(self, device_id): - """Create a new device with no authentication.""" - device_template = { - 'id': device_id, - } - return self._create_device(device_template) - - def patch_es256_for_auth(self, device_id, public_key_file): - """Patch the device to add an ES256 public key to the device.""" - with open(public_key_file) as f: - public_key = f.read() - - patch = { - 'credentials': [{ - 'publicKey': { - 'format': 'ES256_PEM', - 'key': public_key - } - }] - } - - device_name = '{}/devices/{}'.format(self.full_name, device_id) - - # Patch requests use a FieldMask to determine which fields to update. - # In this case, we're updating the device's credentials with a new - # entry. - request = self._service.projects().locations().registries().devices( - ).patch(name=device_name, updateMask='credentials', body=patch) - - return request.execute() - - def delete_device(self, device_id): - """Delete the device with the given id.""" - device_name = '{}/devices/{}'.format(self.full_name, device_id) - request = self._service.projects().locations().registries().devices( - ).delete(name=device_name) - return request.execute() - - -def parse_command_line_args(): - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description='Example of Google Cloud IoT Core device management.') - # Required arguments - parser.add_argument( - '--project_id', required=True, help='GCP cloud project name.') - parser.add_argument( - '--pubsub_topic', - required=True, - help=('Google Cloud Pub/Sub topic. ' - 'Format is projects/project_id/topics/topic-id')) - parser.add_argument('--api_key', required=True, help='Your API key.') - - # Optional arguments - parser.add_argument( - '--ec_public_key_file', - default='ec_public.pem', - help='Path to public ES256 key file.') - parser.add_argument( - '--rsa_certificate_file', - default='rsa_cert.pem', - help='Path to RS256 certificate file.') - parser.add_argument( - '--cloud_region', default='us-central1', help='GCP cloud region') - parser.add_argument( - '--service_account_json', - default='service_account.json', - help='Path to service account json file.') - parser.add_argument( - '--registry_id', - default=None, - help='Registry id. If not set, a name will be generated.') - - return parser.parse_args() - - -def main(): - args = parse_command_line_args() - - # The example id for our registry. - if args.registry_id is None: - registry_id = 'cloudiot_device_manager_example_registry_{}'.format( - int(time.time())) - else: - registry_id = args.registry_id - - # Lookup or create the registry. - print 'Creating registry', registry_id, 'in project', args.project_id - device_registry = DeviceRegistry( - args.project_id, registry_id, args.cloud_region, - args.service_account_json, args.api_key, args.pubsub_topic) - - # List devices for the (empty) registry - print('Current devices in the registry:') - for device in device_registry.list_devices(): - print device - - # Create an RS256 authenticated device. Note that for security, it is very - # important that you use unique public/private key pairs for each device - # (do not reuse a key pair for multiple devices). This way if a private key - # is compromised, only a single device will be affected. - rs256_device_id = 'rs256-device' - print('Creating RS256 authenticated device', rs256_device_id) - device_registry.create_device_with_rs256( - rs256_device_id, args.rsa_certificate_file) - - # Create an ES256 authenticated device. To demonstrate updating a device, - # we will create the device with no authentication, and then update it to - # use ES256 for authentication. Note that while one can create a device - # without authentication, the MQTT client will not be able to connect to - # it. - es256_device_id = 'es256-device' - print('Creating device without authentication', es256_device_id) - device_registry.create_device_with_no_auth(es256_device_id) - - # Now list devices again - print('Current devices in the registry:') - for device in device_registry.list_devices(): - print(device) - - # Patch the device with authentication - print('Updating device', es256_device_id, 'to use ES256 authentication.') - device_registry.patch_es256_for_auth( - es256_device_id, args.ec_public_key_file) - - # Now list devices again - print('Current devices in the registry:') - for device in device_registry.list_devices(): - print(device) - - # Delete the ES256 device - print('Deleting device', es256_device_id) - device_registry.delete_device(es256_device_id) - - # List devices - will only show the RS256 device. - print('Current devices in the registry:') - for device in device_registry.list_devices(): - print(device) - - # Try to delete the registry. This will fail however, since the registry is - # not empty. - print('Trying to delete non-empty registry') - try: - device_registry.delete() - except HttpError as e: - # This will say that the registry is not empty. - print(e) - - # Delete the RSA devices from the registry - print('Deleting device', rs256_device_id) - device_registry.delete_device(rs256_device_id) - - # Now actually delete registry - print('Deleting registry') - device_registry.delete() - - print 'Completed successfully. Goodbye!' - - -if __name__ == '__main__': - main() diff --git a/samples/api-client/manager/manager.py b/samples/api-client/manager/manager.py new file mode 100644 index 00000000..e8a755a5 --- /dev/null +++ b/samples/api-client/manager/manager.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python + +# Copyright 2017 Google Inc. All Rights Reserved. +# +# 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. + + +""" +Example of using the Google Cloud IoT Core device manager to administer +devices. + +Usage example: + + python manager.py \\ + --project_id=my-project-id \\ + --pubsub_topic=projects/my-project-id/topics/my-topic-id \\ + --api_key=YOUR_API_KEY \\ + --ec_public_key_file=../ec_public.pem \\ + --rsa_certificate_file=../rsa_cert.pem \\ + --service_account_json=$HOME/service_account.json + list +""" + +import argparse +import io +import sys +import time + +from google.cloud import pubsub +from google.oauth2 import service_account +from googleapiclient import discovery +from googleapiclient.errors import HttpError + + +def create_iot_topic(topic_name): + """Creates a PubSub Topic and grants access to Cloud IoT Core.""" + pubsub_client = pubsub.Client() + topic = pubsub_client.topic(topic_name) + topic.create() + + topic = pubsub_client.topic(topic_name) + policy = topic.get_iam_policy() + publishers = policy.get('roles/pubsub.publisher', []) + publishers.append(policy.service_account( + 'cloud-iot@system.gserviceaccount.com')) + policy['roles/pubsub.publisher'] = publishers + topic.set_iam_policy(policy) + + return topic + + +def get_client(service_account_json, api_key): + """Returns an authorized API client by discovering the IoT API using the + provided API key and creating a service object using the service account + credentials JSON.""" + # [START authorize] + api_scopes = ['https://www.googleapis.com/auth/cloud-platform'] + api_version = 'v1beta1' + discovery_api = 'https://cloudiot.googleapis.com/$discovery/rest' + service_name = 'cloudiotcore' + + credentials = service_account.Credentials.from_service_account_file( + service_account_json) + scoped_credentials = credentials.with_scopes(api_scopes) + + discovery_url = '{}?version={}&key={}'.format( + discovery_api, api_version, api_key) + + return discovery.build( + service_name, + api_version, + discoveryServiceUrl=discovery_url, + credentials=scoped_credentials) + # [END authorize] + + +def create_rs256_device( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id, certificate_file): + """Create a new device with the given id, using RS256 for + authentication.""" + # [START create_rs256_device] + registry_name = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + client = get_client(service_account_json, api_key) + with io.open(certificate_file) as f: + certificate = f.read() + + # Note: You can have multiple credentials associated with a device. + device_template = { + 'id': device_id, + 'credentials': [{ + 'publicKey': { + 'format': 'RSA_X509_PEM', + 'key': certificate + } + }] + } + + devices = client.projects().locations().registries().devices() + return devices.create(parent=registry_name, body=device_template).execute() + # [END create_rs256_device] + + +def create_es256_device( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id, public_key_file): + """Create a new device with the given id, using ES256 for + authentication.""" + # [START create_rs256_device] + registry_name = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + client = get_client(service_account_json, api_key) + with io.open(public_key_file) as f: + public_key = f.read() + + # Note: You can have multiple credentials associated with a device. + device_template = { + 'id': device_id, + 'credentials': [{ + 'publicKey': { + 'format': 'ES256_PEM', + 'key': public_key + } + }] + } + + devices = client.projects().locations().registries().devices() + return devices.create(parent=registry_name, body=device_template).execute() + # [END create_rs256_device] + + +def create_unauth_device( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id): + """Create a new device without authentication.""" + # [START create_noauth_device] + registry_name = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + client = get_client(service_account_json, api_key) + device_template = { + 'id': device_id, + } + + devices = client.projects().locations().registries().devices() + return devices.create(parent=registry_name, body=device_template).execute() + # [END create_noauth_device] + + +def delete_device( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id): + """Delete the device with the given id.""" + # [START delete_device] + print('Delete device') + client = get_client(service_account_json, api_key) + registry_name = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + device_name = '{}/devices/{}'.format(registry_name, device_id) + + devices = client.projects().locations().registries().devices() + return devices.delete(name=device_name).execute() + # [END delete_device] + + +def delete_registry( + service_account_json, api_key, project_id, cloud_region, registry_id): + """Deletes the specified registry.""" + # [START delete_registry] + print('Delete registry') + client = get_client(service_account_json, api_key) + registry_name = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + registries = client.projects().locations().registries() + return registries.delete(name=registry_name).execute() + # [END delete_registry] + + +def get_device( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id): + """Retrieve the device with the given id.""" + # [START delete_device] + print('Getting device') + client = get_client(service_account_json, api_key) + registry_name = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + device_name = '{}/devices/{}'.format(registry_name, device_id) + devices = client.projects().locations().registries().devices() + device = devices.get(name=device_name).execute() + + print('Id : {}'.format(device.get('id'))) + print('Name : {}'.format(device.get('name'))) + print('Credentials:') + if device.get('credentials') is not None: + for credential in device.get('credentials'): + keyinfo = credential.get('publicKey') + print('\tcertificate: \n{}'.format(keyinfo.get('key'))) + print('\tformat : {}'.format(keyinfo.get('format'))) + print('\texpiration: {}'.format(credential.get('expirationTime'))) + + print('Config:') + print('\tdata: {}'.format(device.get('config').get('data'))) + print('\tversion: {}'.format(device.get('config').get('version'))) + print('\tcloudUpdateTime: {}'.format(device.get('config').get( + 'cloudUpdateTime'))) + + return device + # [END delete_device] + + +def list_devices( + service_account_json, api_key, project_id, cloud_region, registry_id): + """List all devices in the registry.""" + # [START list_devices] + print('Listing devices') + registry_path = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + client = get_client(service_account_json, api_key) + devices = client.projects().locations().registries().devices( + ).list(parent=registry_path).execute().get('devices', []) + + for device in devices: + print('Device: {} : {}'.format( + device.get('numId'), + device.get('id'))) + + return devices + # [list_devices] + + +def open_registry( + service_account_json, api_key, project_id, cloud_region, pubsub_topic, + registry_id): + """Gets or creates a device registry.""" + print('Creating registry') + client = get_client(service_account_json, api_key) + registry_parent = 'projects/{}/locations/{}'.format( + project_id, + cloud_region) + body = { + 'eventNotificationConfig': { + 'pubsubTopicName': pubsub_topic + }, + 'id': registry_id + } + request = client.projects().locations().registries().create( + parent=registry_parent, body=body) + + try: + response = request.execute() + print('Created registry', registry_id) + print(response) + except HttpError as e: + if e.resp.status == 409: + # Device registry already exists + print( + 'Registry', registry_id, + 'already exists - looking it up instead.') + topic_name = '{}/registries/{}'.format( + registry_parent, registry_id) + request = client.projects().locations().registries( + ).get(name=topic_name) + request.execute() + + +def patch_es256_auth( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id, public_key_file): + """Patch the device to add an ES256 public key to the device.""" + print('Patch device with ES256 certificate') + client = get_client(service_account_json, api_key) + registry_path = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + with io.open(public_key_file) as f: + public_key = f.read() + + patch = { + 'credentials': [{ + 'publicKey': { + 'format': 'ES256_PEM', + 'key': public_key + } + }] + } + + device_name = '{}/devices/{}'.format(registry_path, device_id) + + return client.projects().locations().registries().devices().patch( + name=device_name, updateMask='credentials', body=patch).execute() + + +def patch_rsa256_auth( + service_account_json, api_key, project_id, cloud_region, registry_id, + device_id, public_key_file): + """Patch the device to add an RSA256 public key to the device.""" + print('Patch device with RSA256 certificate') + client = get_client(service_account_json, api_key) + registry_path = 'projects/{}/locations/{}/registries/{}'.format( + project_id, cloud_region, registry_id) + + with io.open(public_key_file) as f: + public_key = f.read() + + patch = { + 'credentials': [{ + 'publicKey': { + 'format': 'RSA_X509_PEM', + 'key': public_key + } + }] + } + + device_name = '{}/devices/{}'.format(registry_path, device_id) + + return client.projects().locations().registries().devices().patch( + name=device_name, updateMask='credentials', body=patch).execute() + + +def parse_command_line_args(): + """Parse command line arguments.""" + default_registry = 'cloudiot_device_manager_example_registry_{}'.format( + int(time.time())) + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + # Required arguments + parser.add_argument( + '--project_id', required=True, help='GCP cloud project name.') + parser.add_argument( + '--pubsub_topic', + required=True, + help=('Google Cloud Pub/Sub topic. ' + 'Format is projects/project_id/topics/topic-id')) + parser.add_argument('--api_key', required=True, help='Your API key.') + + # Optional arguments + parser.add_argument( + '--ec_public_key_file', + default=None, + help='Path to public ES256 key file.') + parser.add_argument( + '--rsa_certificate_file', + default=None, + help='Path to RS256 certificate file.') + parser.add_argument( + '--cloud_region', default='us-central1', help='GCP cloud region') + parser.add_argument( + '--service_account_json', + default='service_account.json', + help='Path to service account json file.') + parser.add_argument( + '--registry_id', + default=default_registry, + help='Registry id. If not set, a name will be generated.') + parser.add_argument( + '--device_id', + default=None, + help='Device id.') + + # Command subparser + command = parser.add_subparsers(dest='command') + + command.add_parser('create-es256', help=create_es256_device.__doc__) + command.add_parser('create-registry', help=open_registry.__doc__) + command.add_parser('create-rsa256', help=create_rs256_device.__doc__) + command.add_parser('create-topic', help=create_iot_topic.__doc__) + command.add_parser('create-unauth', help=create_unauth_device.__doc__) + command.add_parser('delete-device', help=delete_device.__doc__) + command.add_parser('delete-registry', help=delete_registry.__doc__) + command.add_parser('get', help=get_device.__doc__) + command.add_parser('list', help=list_devices.__doc__) + command.add_parser('patch-es256', help=patch_es256_auth.__doc__) + command.add_parser('patch-rs256', help=patch_rsa256_auth.__doc__) + + return parser.parse_args() + + +def run_command(args): + """Calls the program using the specified command.""" + if args.command == 'create-rsa256': + create_rs256_device( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id, + args.rsa_certificate_file) + + elif args.command == 'create-es256': + create_es256_device( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id, + args.ec_public_key_file) + + elif args.command == 'create-unauth': + create_unauth_device( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id) + + elif args.command == 'create-registry': + open_registry( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.pubsub_topic, args.registry_id) + + elif args.command == 'create-topic': + create_iot_topic(args.pubsub_topic) + + elif args.command == 'delete-device': + delete_device( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id) + + elif args.command == 'delete-registry': + delete_registry( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id) + + elif args.command == 'get': + get_device( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id) + + elif args.command == 'list': + list_devices( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id) + + elif args.command == 'patch-es256': + if (args.ec_public_key_file is None): + sys.exit('Error: specify --ec_public_key_file') + patch_es256_auth( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id, + args.ec_public_key_file) + + elif args.command == 'patch-rs256': + if (args.rsa_certificate_file is None): + sys.exit('Error: specify --rsa_certificate_file') + patch_rsa256_auth( + args.service_account_json, args.api_key, args.project_id, + args.cloud_region, args.registry_id, args.device_id, + args.rsa_certificate_file) + + +def main(): + args = parse_command_line_args() + run_command(args) + + +if __name__ == '__main__': + main() diff --git a/samples/api-client/manager/manager_test.py b/samples/api-client/manager/manager_test.py new file mode 100644 index 00000000..ab7c2985 --- /dev/null +++ b/samples/api-client/manager/manager_test.py @@ -0,0 +1,209 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# 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 time + +import pytest + +import manager + + +cloud_region = 'us-central1' +device_id_template = 'test-device-{}' +es_cert_path = 'resources/ec_public.pem' +rsa_cert_path = 'resources/rsa_cert.pem' +topic_id = 'test-device-events-{}'.format(int(time.time())) + +api_key = os.environ['API_KEY'] +project_id = os.environ['GCLOUD_PROJECT'] +service_account_json = os.environ['GOOGLE_APPLICATION_CREDENTIALS'] + +pubsub_topic = 'projects/{}/topics/{}'.format(project_id, topic_id) +registry_id = 'test-registry-{}'.format(int(time.time())) + + +@pytest.fixture(scope='module') +def test_topic(): + topic = manager.create_iot_topic(topic_id) + + yield topic + + if topic.exists(): + topic.delete() + + +def test_create_delete_registry(test_topic, capsys): + manager.open_registry( + service_account_json, api_key, project_id, cloud_region, + pubsub_topic, registry_id) + + manager.list_devices( + service_account_json, api_key, project_id, cloud_region, + registry_id) + + out, _ = capsys.readouterr() + + # Check that create / list worked + assert 'Created registry' in out + assert 'eventNotificationConfig' in out + + # Clean up + manager.delete_registry( + service_account_json, api_key, project_id, cloud_region, + registry_id) + + +def test_add_delete_unauth_device(test_topic, capsys): + device_id = device_id_template.format('UNAUTH') + manager.open_registry( + service_account_json, api_key, project_id, cloud_region, + pubsub_topic, registry_id) + + manager.create_unauth_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + out, _ = capsys.readouterr() + assert 'UNAUTH' in out + + +def test_add_delete_rs256_device(test_topic, capsys): + device_id = device_id_template.format('RSA256') + manager.open_registry( + service_account_json, api_key, project_id, cloud_region, + pubsub_topic, registry_id) + + manager.create_rs256_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id, rsa_cert_path) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_registry( + service_account_json, api_key, project_id, cloud_region, + registry_id) + + out, _ = capsys.readouterr() + assert 'format : RSA_X509_PEM' in out + + +def test_add_delete_es256_device(test_topic, capsys): + device_id = device_id_template.format('ES256') + manager.open_registry( + service_account_json, api_key, project_id, cloud_region, + pubsub_topic, registry_id) + + manager.create_es256_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id, es_cert_path) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_registry( + service_account_json, api_key, project_id, cloud_region, + registry_id) + + out, _ = capsys.readouterr() + assert 'format : ES256_PEM' in out + + +def test_add_patch_delete_rs256(test_topic, capsys): + device_id = device_id_template.format('PATCHME') + manager.open_registry( + service_account_json, api_key, project_id, cloud_region, + pubsub_topic, registry_id) + + manager.create_rs256_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id, rsa_cert_path) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + out, _ = capsys.readouterr() + assert 'format : RSA_X509_PEM' in out + + manager.patch_es256_auth( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id, es_cert_path) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + out, _ = capsys.readouterr() + assert 'format : ES256_PEM' in out + + manager.delete_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_registry( + service_account_json, api_key, project_id, cloud_region, + registry_id) + + +def test_add_patch_delete_es256(test_topic, capsys): + device_id = device_id_template.format('PATCHME') + manager.open_registry( + service_account_json, api_key, project_id, cloud_region, + pubsub_topic, registry_id) + + manager.create_es256_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id, es_cert_path) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + out, _ = capsys.readouterr() + assert 'format : ES256_PEM' in out + + manager.patch_rsa256_auth( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id, rsa_cert_path) + + manager.get_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + out, _ = capsys.readouterr() + assert 'format : RSA_X509_PEM' in out + + manager.delete_device( + service_account_json, api_key, project_id, cloud_region, + registry_id, device_id) + + manager.delete_registry( + service_account_json, api_key, project_id, cloud_region, + registry_id) diff --git a/samples/api-client/manager/requirements.txt b/samples/api-client/manager/requirements.txt index 00e4531f..b54e248f 100644 --- a/samples/api-client/manager/requirements.txt +++ b/samples/api-client/manager/requirements.txt @@ -1,3 +1,4 @@ google-api-python-client==1.6.2 google-auth-httplib2==0.0.2 google-auth==1.0.1 +google-cloud==0.25.0 diff --git a/samples/api-client/manager/resources/README.md b/samples/api-client/manager/resources/README.md new file mode 100644 index 00000000..ba272cca --- /dev/null +++ b/samples/api-client/manager/resources/README.md @@ -0,0 +1,4 @@ +# Test public certificate files + +The public certificates in this folder are only provided for testing and should +not be used for registering your devices. diff --git a/samples/api-client/manager/resources/ec_public.pem b/samples/api-client/manager/resources/ec_public.pem new file mode 100644 index 00000000..6a977071 --- /dev/null +++ b/samples/api-client/manager/resources/ec_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6npM9ajyCA6YZjSFJvMZ7aGa2Y3N +MO39LKPGRLY12+gZmNc6GgReoFDJ3QL6UxBFToTm57kj6aMDyOiqejM+4A== +-----END PUBLIC KEY----- diff --git a/samples/api-client/manager/resources/rsa_cert.pem b/samples/api-client/manager/resources/rsa_cert.pem new file mode 100644 index 00000000..13116d76 --- /dev/null +++ b/samples/api-client/manager/resources/rsa_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIJAKOoH/TJUTxrMA0GCSqGSIb3DQEBBQUAMBExDzANBgNV +BAMTBnVudXNlZDAeFw0xNzA2MDIxOTE1MzVaFw0xNzA3MDIxOTE1MzVaMBExDzAN +BgNVBAMTBnVudXNlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMjb +stMRhg2weKsVgetWsLg7kjR6OoQUeLB5sREFuO4UgII2prBpBaARPst6jzccVvUB +Q54r+NSa6qIo/Wmmg1IqAA/ygv6Zm6Ol/HDRiOBUoeYDU+RozURQVpoBqW/AKwuX +2PizA/XXFLVZJMoykse6HSOmY5vgeNGPWifC6KruEJu5rlvD02l7XTKQp+RVZP0g +eAdRn6G3K8zud+RLTfmDM8seWq3KdFj02FJc8RtFLXMj8gf3fy6lRVya6Yuf/ZxA +avj7sw6BsWyHQIKJsLNgW/SluNewLPrWMhERZN05CGe8oIOpYojN9G22kgNJoY2D +9sLEjf8OE4y4vqBL9YMCAwEAAaNyMHAwHQYDVR0OBBYEFKC/Is/p72gSAPXNk0p3 +V0iztMbHMEEGA1UdIwQ6MDiAFKC/Is/p72gSAPXNk0p3V0iztMbHoRWkEzARMQ8w +DQYDVQQDEwZ1bnVzZWSCCQCjqB/0yVE8azAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBBQUAA4IBAQAitbMSIgTDVRJKoez7cEZfvvxU64zHaVs76GkX+9MzrHM711YX +PTYx+I1kwYnanceJZAj0TeoE1/h234t2y4veE7N2bRFP3SFVGdFigs+mVheFzoGE +jpTE1Ew/6I6CBHKmF9tN3YunRCs5H+WJ7uh4VE/PArKVb9JtqudmNxNLCvnGdP+u +0v6rMyTJsonyw+8vj1cJDz83HBJSt0DZpzbV/IwA/cs9bff2YCAiHJCPcPqHPUWs +qOZCI+N5aoV5un3Iex2ew0SX/1WKSEDxeU7VQau0y2JsqgE58748+Nf/sWTUco8a +78FmQcFOS27+VJn9ODmGgbnoqGKBc5P+tZWf +-----END CERTIFICATE-----