From 1e34a649bb59fbcb2611ca12edf5d38341984174 Mon Sep 17 00:00:00 2001 From: Gus Class Date: Mon, 22 May 2017 19:47:13 -0700 Subject: [PATCH] Fixes lint, renames folder, updates readme --- iot/{api => api-client}/README.md | 14 +- iot/{api => api-client}/generate_keys.sh | 0 iot/{api => api-client}/manager/README.md | 5 +- .../cloudiot_device_manager_example.py | 330 ++++++++++++++++++ .../manager/requirements.txt | 0 .../mqtt_example/README.md | 4 +- .../mqtt_example/cloudiot_mqtt_example.py | 211 +++++++++++ .../mqtt_example/requirements.txt | 0 iot/{api => api-client}/scripts/README.rst | 0 iot/{api => api-client}/scripts/README.rst.in | 0 iot/{api => api-client}/scripts/iam.py | 3 +- .../scripts/requirements.txt | 0 .../cloudiot_device_manager_example.py | 316 ----------------- iot/api/mqtt_example/cloudiot_mqtt_example.py | 198 ----------- 14 files changed, 560 insertions(+), 521 deletions(-) rename iot/{api => api-client}/README.md (67%) rename iot/{api => api-client}/generate_keys.sh (100%) rename iot/{api => api-client}/manager/README.md (85%) create mode 100644 iot/api-client/manager/cloudiot_device_manager_example.py rename iot/{api => api-client}/manager/requirements.txt (100%) rename iot/{api => api-client}/mqtt_example/README.md (91%) create mode 100644 iot/api-client/mqtt_example/cloudiot_mqtt_example.py rename iot/{api => api-client}/mqtt_example/requirements.txt (100%) rename iot/{api => api-client}/scripts/README.rst (100%) rename iot/{api => api-client}/scripts/README.rst.in (100%) rename iot/{api => api-client}/scripts/iam.py (96%) rename iot/{api => api-client}/scripts/requirements.txt (100%) delete mode 100644 iot/api/manager/cloudiot_device_manager_example.py delete mode 100644 iot/api/mqtt_example/cloudiot_mqtt_example.py diff --git a/iot/api/README.md b/iot/api-client/README.md similarity index 67% rename from iot/api/README.md rename to iot/api-client/README.md index b98a0f107ffe..4af475a99808 100644 --- a/iot/api/README.md +++ b/iot/api-client/README.md @@ -3,7 +3,7 @@ This folder contains Python samples that demonstrate an overview of the Google Cloud IoT Core platform. ## Quickstart -1. Install the gCloud CLI as described in [the device manager guide](https://cloud-dot-devsite.googleplex.com/iot/docs/device_manager_guide). +1. Install the gCloud CLI as described in [the device manager guide](https://cloud.google.com/iot/docs/device_manager_guide). 2. Create a PubSub topic: gcloud beta pubsub topics create projects/my-iot-project/topics/device-events @@ -23,7 +23,15 @@ or by using the helper script in the /scripts folder. ./generate_keys.sh -6. Connect a sample device using the sample app in the `mqtt_example` folder. -7. Learn how to manage devices programatically with the sample app in the +6. Register a device: + + gcloud alpha iot devices create my-python-device \ + --project=my-iot-project \ + --region=us-central1 \ + --registry=my-registry \ + --public-key path=rsa_cert.pem,type=rs256 + +7. Connect a sample device using the sample app in the `mqtt_example` folder. +8. Learn how to manage devices programatically with the sample app in the `manager` folder. diff --git a/iot/api/generate_keys.sh b/iot/api-client/generate_keys.sh similarity index 100% rename from iot/api/generate_keys.sh rename to iot/api-client/generate_keys.sh diff --git a/iot/api/manager/README.md b/iot/api-client/manager/README.md similarity index 85% rename from iot/api/manager/README.md rename to iot/api-client/manager/README.md index 62036e6093c9..94a9c235840f 100644 --- a/iot/api/manager/README.md +++ b/iot/api-client/manager/README.md @@ -36,5 +36,8 @@ command would run the sample: python cloudiot_device_manager_example.py \ --project_id blue-jet-123 \ - --api_key=YOUR_API_KEY \ + --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/iot/api-client/manager/cloudiot_device_manager_example.py b/iot/api-client/manager/cloudiot_device_manager_example.py new file mode 100644 index 000000000000..5b8b7971684c --- /dev/null +++ b/iot/api-client/manager/cloudiot_device_manager_example.py @@ -0,0 +1,330 @@ +#!/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 googleapiclient import discovery +from googleapiclient.errors import HttpError +from oauth2client.service_account import ServiceAccountCredentials + +API_SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] +API_VERSION = 'v1alpha1' +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 = ServiceAccountCredentials.from_json_keyfile_name( + service_account_json, 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=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. + registry_info = { + 'eventNotificationConfig': { + 'pubsubTopicName': pubsub_topic + } + } + request = self._service.projects().locations().registries().create( + parent=self.parent, body=registry_info, id=registry_id) + + 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_id, device_template): + request = self._service.projects().locations().registries().devices( + ).create(parent=self.full_name, body=device_template, id=device_id) + 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 = { + 'credentials': [{ + 'publicKey': { + 'format': 'RSA_X509_PEM', + 'key': certificate + } + }] + } + return self._create_device(device_id, 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 = { + 'credentials': [{ + 'publicKey': { + 'format': 'ES256_PEM', + 'key': public_key + } + }] + } + return self._create_device(device_id, device_template) + + def create_device_with_no_auth(self, device_id): + """Create a new device with no authentication.""" + return self._create_device(device_id, {}) + + 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/iot/api/manager/requirements.txt b/iot/api-client/manager/requirements.txt similarity index 100% rename from iot/api/manager/requirements.txt rename to iot/api-client/manager/requirements.txt diff --git a/iot/api/mqtt_example/README.md b/iot/api-client/mqtt_example/README.md similarity index 91% rename from iot/api/mqtt_example/README.md rename to iot/api-client/mqtt_example/README.md index 85b247c6b908..d36ce358a5b1 100644 --- a/iot/api/mqtt_example/README.md +++ b/iot/api-client/mqtt_example/README.md @@ -3,7 +3,7 @@ This sample app publishes data to Cloud Pub/Sub using the MQTT bridge provided as part of Google Cloud IoT Core. -For detailed running instructions see the [MQTT code samples guide](https://cloud-dot-devsite.googleplex.com/iot/docs/protocol_bridge_guide). +For detailed running instructions see the [MQTT code samples guide](https://cloud.google.com/iot/docs/protocol_bridge_guide). # Setup @@ -37,6 +37,6 @@ how you would execute using the configuration from the MQTT code samples guide: python cloudiot_mqtt_example.py \ --registry_id my-registry \ --project_id=blue-jet-123 \ - --device_id my-rs256-device \ + --device_id my-python-device \ --algorithm RS256 \ --private_key_file ../rsa_private.pem diff --git a/iot/api-client/mqtt_example/cloudiot_mqtt_example.py b/iot/api-client/mqtt_example/cloudiot_mqtt_example.py new file mode 100644 index 000000000000..b15ab4792c83 --- /dev/null +++ b/iot/api-client/mqtt_example/cloudiot_mqtt_example.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python + +# Copyright 2017 Google Inc. +# +# 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. + +"""Python sample for connecting to Google Cloud IoT Core via MQTT, using JWT. + +This example connects to Google Cloud IoT Core via MQTT, using a JWT for device +authentication. After connecting, by default the device publishes 100 messages +to the device's MQTT topic at a rate of one per second, and then exits. + +Before you run the sample, you must register your device as described in the +README in the parent folder. + +After registering the device, download Google's CA root certificates with + + wget https://pki.google.com/roots.pem + +and run this script with the corresponding algorithm flag, for example: + + python cloudiot_mqtt_example.py --project_id=my-project-id \ + --registry_id=my-registry-id \ + --device_id=my-device-id \ + --private_key_file=rsa_private.pem \ + --algorithm=RS256 +""" + +import argparse +import datetime +import time + +import jwt +import paho.mqtt.client as mqtt + + +def create_jwt(project_id, private_key_file, algorithm): + """Creates a JWT (https://jwt.io) to establish an MQTT connection. + + Args: + project_id: The cloud project ID this device belongs to + + private_key_file: A path to a file containing either an RSA256 or ES256 + private key. + + algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256' + + Returns: + An MQTT generated from the given project_id and private key, which + expires in 20 minutes. After 20 minutes, your client will be + disconnected, and a new JWT will have to be generated. + + Raises: + ValueError: If the private_key_file does not contain a known key. + """ + + token = { + # The time that the token was issued at + 'iat': datetime.datetime.utcnow(), + # When this token expires. The device will be disconnected after the + # token expires, and will have to reconnect. + 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60), + # The audience field should always be set to the GCP project id. + 'aud': project_id + } + + # Read the private key file. + with open(private_key_file, 'r') as f: + private_key = f.read() + + print( + 'Creating JWT using {} from private key file {}'.format( + algorithm, private_key_file)) + + return jwt.encode(token, private_key, algorithm=algorithm) + + +def error_str(rc): + """Convert a Paho error to a human readable string.""" + return '{}: {}'.format(rc, mqtt.error_string(rc)) + + +def on_connect(unused_client, unused_userdata, unused_flags, rc): + """Callback for when a device connects.""" + print 'on_connect', error_str(rc) + + +def on_disconnect(unused_client, unused_userdata, rc): + """Paho callback for when a device disconnects.""" + print 'on_disconnect', error_str(rc) + + +def on_publish(unused_client, unused_userdata, unused_mid): + """Paho callback when a message is sent to the broker.""" + print 'on_publish' + + +def parse_command_line_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description=( + 'Example Google Cloud IoT Core MQTT device connection code.')) + parser.add_argument( + '--project_id', + required=True, + help='GCP cloud project name') + parser.add_argument( + '--registry_id', + required=True, + help='Cloud IoT Core registry id') + parser.add_argument( + '--device_id', + required=True, + help='Cloud IoT Core device id') + parser.add_argument( + '--private_key_file', + required=True, + help='Path to private key file.') + parser.add_argument( + '--algorithm', + choices=('RS256', 'ES256'), + required=True, + help='Which encryption algorithm to use to generate the JWT.') + parser.add_argument( + '--cloud_region', default='us-central1', help='GCP cloud region') + parser.add_argument( + '--ca_certs', + default='roots.pem', + help=('CA root certificate from https://pki.google.com/roots.pem')) + parser.add_argument( + '--num_messages', + type=int, + default=100, + help='Number of messages to publish.') + parser.add_argument( + '--mqtt_bridge_hostname', + default='mqtt.googleapis.com', + help='MQTT bridge hostname.') + parser.add_argument( + '--mqtt_bridge_port', default=8883, help='MQTT bridge port.') + + return parser.parse_args() + + +def main(): + args = parse_command_line_args() + + # Create our MQTT client. The client_id is a unique string that identifies + # this device. For Google Cloud IoT Core, it must be in the format below. + client = mqtt.Client( + client_id=( + 'projects/{}/locations/{}/registries/{}/devices/{}' + .format( + args.project_id, args.cloud_region, + args.registry_id, args.device_id))) + + # With Google Cloud IoT Core, the username field is ignored, and the + # password field is used to transmit a JWT to authorize the device. + client.username_pw_set( + username='unused', + password=create_jwt( + args.project_id, args.private_key_file, args.algorithm)) + + # Enable SSL/TLS support. + client.tls_set(ca_certs=args.ca_certs) + + # Register message callbacks. https://eclipse.org/paho/clients/python/docs/ + # describes additional callbacks that Paho supports. In this example, the + # callbacks just print to standard out. + client.on_connect = on_connect + client.on_publish = on_publish + client.on_disconnect = on_disconnect + + # Connect to the Google MQTT bridge. + client.connect(args.mqtt_bridge_hostname, args.mqtt_bridge_port) + + # Start the network loop. + client.loop_start() + + mqtt_topic = '/devices/{}/events'.format(args.device_id) + + # Publish num_messages mesages to the MQTT bridge once per second. + for i in range(1, args.num_messages + 1): + payload = '{}/{}-payload-{}'.format( + args.registry_id, args.device_id, i) + print( + 'Publishing message {}/{}: \'{}\''.format( + i, args.num_messages, payload)) + # Publish "payload" to the MQTT topic. qos=1 means at least once + # delivery. Cloud IoT Core also supports qos=0 for at most once + # delivery. + client.publish(mqtt_topic, payload, qos=1) + time.sleep(1) + + # End the network loop and finish. + client.loop_stop() + print('Finished.') + + +if __name__ == '__main__': + main() diff --git a/iot/api/mqtt_example/requirements.txt b/iot/api-client/mqtt_example/requirements.txt similarity index 100% rename from iot/api/mqtt_example/requirements.txt rename to iot/api-client/mqtt_example/requirements.txt diff --git a/iot/api/scripts/README.rst b/iot/api-client/scripts/README.rst similarity index 100% rename from iot/api/scripts/README.rst rename to iot/api-client/scripts/README.rst diff --git a/iot/api/scripts/README.rst.in b/iot/api-client/scripts/README.rst.in similarity index 100% rename from iot/api/scripts/README.rst.in rename to iot/api-client/scripts/README.rst.in diff --git a/iot/api/scripts/iam.py b/iot/api-client/scripts/iam.py similarity index 96% rename from iot/api/scripts/iam.py rename to iot/api-client/scripts/iam.py index 792208490407..0ce69c8f9109 100644 --- a/iot/api/scripts/iam.py +++ b/iot/api-client/scripts/iam.py @@ -49,7 +49,8 @@ def set_topic_policy(topic_name): formatter_class=argparse.RawDescriptionHelpFormatter ) - subparsers = parser.add_argument(dest='topic_name', + subparsers = parser.add_argument( + dest='topic_name', help='The PubSub topic to grant Cloud IoT Core access to') args = parser.parse_args() diff --git a/iot/api/scripts/requirements.txt b/iot/api-client/scripts/requirements.txt similarity index 100% rename from iot/api/scripts/requirements.txt rename to iot/api-client/scripts/requirements.txt diff --git a/iot/api/manager/cloudiot_device_manager_example.py b/iot/api/manager/cloudiot_device_manager_example.py deleted file mode 100644 index 2fa37d5c9051..000000000000 --- a/iot/api/manager/cloudiot_device_manager_example.py +++ /dev/null @@ -1,316 +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 googleapiclient import discovery -from googleapiclient.errors import HttpError -from oauth2client.service_account import ServiceAccountCredentials - -API_SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] -API_VERSION = 'v1alpha1' -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 = ServiceAccountCredentials.from_json_keyfile_name( - service_account_json, 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=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. - registry_info = { - 'eventNotificationConfig': { - 'pubsubTopicName': pubsub_topic - } - } - request = self._service.projects().locations().registries().create( - parent=self.parent, body=registry_info, id=registry_id) - 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_id, device_template): - request = self._service.projects().locations().registries().devices( - ).create(parent=self.full_name, body=device_template, id=device_id) - 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 = { - 'credentials': [{ - 'publicKey': { - 'format': 'RSA_X509_PEM', - 'key': certificate - } - }] - } - return self._create_device(device_id, 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 = { - 'credentials': [{ - 'publicKey': { - 'format': 'ES256_PEM', - 'key': public_key - } - }] - } - return self._create_device(device_id, device_template) - - def create_device_with_no_auth(self, device_id): - """Create a new device with no authentication.""" - return self._create_device(device_id, {}) - - 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 unique registry 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/iot/api/mqtt_example/cloudiot_mqtt_example.py b/iot/api/mqtt_example/cloudiot_mqtt_example.py deleted file mode 100644 index 5bd4a3ad6dca..000000000000 --- a/iot/api/mqtt_example/cloudiot_mqtt_example.py +++ /dev/null @@ -1,198 +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. - - -"""Python sample of connecting to Google Cloud IoT Core via MQTT, using JWT. - -This example connects to Google Cloud IoT Core via MQTT, using a JWT for device -authentication. After connecting, by default the device publishes 100 messages -to the device's MQTT topic at a rate of one per second, and then exits. - -Before you run the sample, you must register your device as described in the -README in the parent folder. - -After registering the device, download Google's CA root certificates with - - $ wget https://pki.google.com/roots.pem - -and run this script with the corresponding algorithm flag, for example: - - $ python cloudiot_mqtt_example.py --project_id=my-project-id \ - --registry_id=my-registry-id \ - --device_id=my-device-id \ - --private_key_file=rsa_private.pem \ - --algorithm=RS256 -""" - -import argparse -import datetime -import time -import jwt -import paho.mqtt.client as mqtt - - -def create_jwt(project_id, private_key_file, algorithm): - """Create a JWT (https://jwt.io) to establish an MQTT connection. - - Args: - project_id: The cloud project ID this device belongs to - - private_key_file: A path to a file containing either an RSA256 or ES256 - private key. - - algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256' - - Returns: - An MQTT generated from the given project_id and private key, which expires - in 20 minutes. After 20 minutes, your client will be disconnected, and a new - JWT will have to be generated. - - Raises: - ValueError: If the private_key_file does not contain a known key. - """ - - token = { - # The time that the token was issued at - 'iat': datetime.datetime.utcnow(), - # When this token expires. The device will be disconnected after the token - # expires, and will have to reconnect. - 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60), - # The audience field should always be set to the GCP project id. - 'aud': project_id - } - - # Read the private key file. - with open(private_key_file, 'r') as f: - private_key = f.read() - - print 'Creating JWT using {} from private key file {}'.format( - algorithm, private_key_file) - - return jwt.encode(token, private_key, algorithm=algorithm) - - -def error_str(rc): - """Convert a Paho error to a human readable string.""" - return '{}: {}'.format(rc, mqtt.error_string(rc)) - - -def on_connect(unused_client, unused_userdata, unused_flags, rc): - """Callback for when a device connects.""" - print 'on_connect', error_str(rc) - - -def on_disconnect(unused_client, unused_userdata, rc): - """Paho callback for when a device disconnects.""" - print 'on_disconnect', error_str(rc) - - -def on_publish(unused_client, unused_userdata, unused_mid): - """Paho callback when a message is sent to the broker.""" - print 'on_publish' - - -def parse_command_line_args(): - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description='Example Google Cloud IoT Core MQTT device connection code.') - parser.add_argument( - '--project_id', required=True, help='GCP cloud project name') - parser.add_argument( - '--registry_id', required=True, help='Cloud IoT Core registry id') - parser.add_argument('--device_id', required=True, - help='Cloud IoT Core device id') - parser.add_argument( - '--private_key_file', required=True, help='Path to private key file.') - parser.add_argument( - '--algorithm', - choices=('RS256', 'ES256'), - required=True, - help='Which encryption algorithm to use to generate the JWT.') - parser.add_argument( - '--cloud_region', default='us-central1', help='GCP cloud region') - parser.add_argument( - '--ca_certs', - default='roots.pem', - help='CA root certificate. Get from https://pki.google.com/roots.pem') - parser.add_argument( - '--num_messages', - type=int, - default=100, - help='Number of messages to publish.') - parser.add_argument( - '--mqtt_bridge_hostname', - default='mqtt.googleapis.com', - help='MQTT bridge hostname.') - parser.add_argument( - '--mqtt_bridge_port', default=8883, help='MQTT bridge port.') - - return parser.parse_args() - - -def main(): - args = parse_command_line_args() - - # Create our MQTT client. The client_id is a unique string that identifies - # this device. For Google Cloud IoT Core, it must be in the format below. - client = mqtt.Client( - client_id='projects/{}/locations/{}/registries/{}/devices/{}'.format( - args.project_id, args.cloud_region, args.registry_id, args.device_id)) - - # With Google Cloud IoT Core, the username field is ignored, and the - # password field is used to transmit a JWT to authorize the device. - client.username_pw_set( - username='unused', - password=create_jwt(args.project_id, args.private_key_file, - args.algorithm)) - - # Enable SSL/TLS support. - client.tls_set(ca_certs=args.ca_certs) - - # Register message callbacks. https://eclipse.org/paho/clients/python/docs/ - # describes additional callbacks that Paho supports. In this example, the - # callbacks just print to standard out. - client.on_connect = on_connect - client.on_publish = on_publish - client.on_disconnect = on_disconnect - - # Connect to the Google MQTT bridge. - client.connect(args.mqtt_bridge_hostname, args.mqtt_bridge_port) - - # Start the network loop. - client.loop_start() - - # The MQTT topic that this device will publish telemetry data to. The MQTT - # topic name is required to be in the format below. Note that this is not the - # same as the device registry's Cloud Pub/Sub topic. - mqtt_topic = '/devices/{}/events'.format(args.device_id) - - # Publish num_messages mesages to the MQTT bridge, at a rate of 1 per second. - for i in range(1, args.num_messages + 1): - payload = '{}/{}-payload-{}'.format(args.registry_id, args.device_id, i) - print 'Publishing message {}/{}: \'{}\''.format(i, args.num_messages, - payload) - # Publish "payload" to the MQTT topic. qos=1 means at least once delivery. - # Cloud IoT Core also supports qos=0 for at most once delivery. - client.publish(mqtt_topic, payload, qos=1) - time.sleep(1) - - # End the network loop and finish. - client.loop_stop() - print 'Finished loop successfully. Goodbye!' - - -if __name__ == '__main__': - main()