From 83d1c5b72246a639e7b2203ad4a1f4f7b9a96eab Mon Sep 17 00:00:00 2001 From: Gus Class Date: Mon, 6 Nov 2017 10:27:49 -0800 Subject: [PATCH] Adds token refresh to MQTT example. (#1197) * Adds token refresh to MQTT example. * Fixes lint error. --- iot/api-client/mqtt_example/README.md | 51 --------- iot/api-client/mqtt_example/README.rst | 107 ++++++++++++++++++ iot/api-client/mqtt_example/README.rst.in | 30 +++++ .../mqtt_example/cloudiot_mqtt_example.py | 101 +++++++++++------ 4 files changed, 205 insertions(+), 84 deletions(-) delete mode 100644 iot/api-client/mqtt_example/README.md create mode 100644 iot/api-client/mqtt_example/README.rst create mode 100644 iot/api-client/mqtt_example/README.rst.in diff --git a/iot/api-client/mqtt_example/README.md b/iot/api-client/mqtt_example/README.md deleted file mode 100644 index 1d8760eb3620..000000000000 --- a/iot/api-client/mqtt_example/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Cloud IoT Core Python MQTT example - -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.google.com/iot/docs/protocol_bridge_guide). - -# Setup - -1. Follow the instructions in the [parent README](../README.md). - -2. Use virtualenv to create a local Python environment. - - virtualenv env && source env/bin/activate - -3. Install the dependencies - - pip install -r requirements.txt - -4. Download the CA root certificates from pki.google.com into the same - directory as the example script: - - wget https://pki.google.com/roots.pem - -# Running the Sample - -The following snippet summarizes usage: - - cloudiot_mqtt_example.py [-h] - --project_id=PROJECT_ID - --registry_id=REGISTRY_ID - --device_id=DEVICE_ID - --private_key_file=PRIVATE_KEY_FILE - --algorithm={RS256,ES256} - [--cloud_region=CLOUD_REGION] - [--ca_certs=CA_CERTS] - [--num_messages=NUM_MESSAGES] - [--mqtt_bridge_hostname=MQTT_BRIDGE_HOSTNAME] - [--mqtt_bridge_port=MQTT_BRIDGE_PORT] - [--message_type={event,state}] - -For example, if your project ID is `blue-jet-123`, the following example shows -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-python-device \ - --algorithm=RS256 \ - --private_key_file=../rsa_private.pem diff --git a/iot/api-client/mqtt_example/README.rst b/iot/api-client/mqtt_example/README.rst new file mode 100644 index 000000000000..aab1e6453a26 --- /dev/null +++ b/iot/api-client/mqtt_example/README.rst @@ -0,0 +1,107 @@ +.. 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. +The following example runs the sample using the project ID `blue-jet-123` and the device name `my-python-device`: + + python cloudiot_mqtt_example.py \ + --registry_id=my-registry \ + --project_id=blue-jet-123 \ + --device_id=my-python-device \ + --algorithm=RS256 \ + --private_key_file=../rsa_private.pem + + + + +.. _Google Cloud IoT Core API: https://cloud.google.com/iot/docs + +Setup +------------------------------------------------------------------------------- + + +Install Dependencies +++++++++++++++++++++ + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. You may want to refer to the `Python Development Environment Setup Guide`_ for Google Cloud Platform for instructions. + + .. _Python Development Environment Setup Guide: + https://cloud.google.com/python/setup + +#. 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 +------------------------------------------------------------------------------- + +MQTT Device Client Example ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +To run this sample: + +.. code-block:: bash + + $ python cloudiot_mqtt_example.py + + usage: cloudiot_mqtt_example.py [-h] [--project_id PROJECT_ID] --registry_id + REGISTRY_ID --device_id DEVICE_ID + --private_key_file PRIVATE_KEY_FILE + --algorithm {RS256,ES256} + [--cloud_region CLOUD_REGION] + [--ca_certs CA_CERTS] + [--num_messages NUM_MESSAGES] + [--message_type {event,state}] + [--mqtt_bridge_hostname MQTT_BRIDGE_HOSTNAME] + [--mqtt_bridge_port {8883,443}] + [--jwt_expires_minutes JWT_EXPIRES_MINUTES] + + Example Google Cloud IoT Core MQTT device connection code. + + optional arguments: + -h, --help show this help message and exit + --project_id PROJECT_ID + GCP cloud project name + --registry_id REGISTRY_ID + Cloud IoT Core registry id + --device_id DEVICE_ID + Cloud IoT Core device id + --private_key_file PRIVATE_KEY_FILE + Path to private key file. + --algorithm {RS256,ES256} + Which encryption algorithm to use to generate the JWT. + --cloud_region CLOUD_REGION + GCP cloud region + --ca_certs CA_CERTS CA root from https://pki.google.com/roots.pem + --num_messages NUM_MESSAGES + Number of messages to publish. + --message_type {event,state} + Indicates whether the message to be published is a + telemetry event or a device state message. + --mqtt_bridge_hostname MQTT_BRIDGE_HOSTNAME + MQTT bridge hostname. + --mqtt_bridge_port {8883,443} + MQTT bridge port. + --jwt_expires_minutes JWT_EXPIRES_MINUTES + Expiration time, in minutes, for JWT tokens. + + + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ \ No newline at end of file diff --git a/iot/api-client/mqtt_example/README.rst.in b/iot/api-client/mqtt_example/README.rst.in new file mode 100644 index 000000000000..412b5b3103d4 --- /dev/null +++ b/iot/api-client/mqtt_example/README.rst.in @@ -0,0 +1,30 @@ +# 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/iot/docs + description: > + `Google Cloud IoT Core`_ allows developers to easily integrate Publish and + Subscribe functionality with devices and programmatically manage device + authorization. + + The following example runs the sample using the project ID `blue-jet-123` + and the device name `my-python-device`: + + python cloudiot_mqtt_example.py \ + --registry_id=my-registry \ + --project_id=blue-jet-123 \ + --device_id=my-python-device \ + --algorithm=RS256 \ + --private_key_file=../rsa_private.pem + +setup: +- install_deps + +samples: +- name: MQTT Device Client Example + file: cloudiot_mqtt_example.py + show_help: True + +cloud_client_library: false diff --git a/iot/api-client/mqtt_example/cloudiot_mqtt_example.py b/iot/api-client/mqtt_example/cloudiot_mqtt_example.py index 4b31c899c8dc..17b08382fe8d 100644 --- a/iot/api-client/mqtt_example/cloudiot_mqtt_example.py +++ b/iot/api-client/mqtt_example/cloudiot_mqtt_example.py @@ -29,6 +29,7 @@ import jwt import paho.mqtt.client as mqtt + # [START iot_mqtt_jwt] def create_jwt(project_id, private_key_file, algorithm): """Creates a JWT (https://jwt.io) to establish an MQTT connection. @@ -64,6 +65,8 @@ def create_jwt(project_id, private_key_file, algorithm): return jwt.encode(token, private_key, algorithm=algorithm) # [END iot_mqtt_jwt] + +# [START iot_mqtt_config] def error_str(rc): """Convert a Paho error to a human readable string.""" return '{}: {}'.format(rc, mqtt.error_string(rc)) @@ -84,6 +87,46 @@ def on_publish(unused_client, unused_userdata, unused_mid): print('on_publish') +def get_client( + project_id, cloud_region, registry_id, device_id, private_key_file, + algorithm, ca_certs, mqtt_bridge_hostname, mqtt_bridge_port): + """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( + project_id, + cloud_region, + registry_id, + 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( + project_id, private_key_file, algorithm)) + + # Enable SSL/TLS support. + client.tls_set(ca_certs=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(mqtt_bridge_hostname, mqtt_bridge_port) + + # Start the network loop. + client.loop_start() + + return client +# [END iot_mqtt_config] + + def parse_command_line_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser(description=( @@ -131,57 +174,48 @@ def parse_command_line_args(): default=8883, type=int, help='MQTT bridge port.') + parser.add_argument( + '--jwt_expires_minutes', + default=20, + type=int, + help=('Expiration time, in minutes, for JWT tokens.')) return parser.parse_args() +# [START iot_mqtt_run] 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() - # Publish to the events or state topic based on the flag. sub_topic = 'events' if args.message_type == 'event' else 'state' mqtt_topic = '/devices/{}/{}'.format(args.device_id, sub_topic) + jwt_iat = datetime.datetime.utcnow() + jwt_exp_mins = args.jwt_expires_minutes + client = get_client( + args.project_id, args.cloud_region, args.registry_id, args.device_id, + args.private_key_file, args.algorithm, args.ca_certs, + args.mqtt_bridge_hostname, args.mqtt_bridge_port) + # 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)) + seconds_since_issue = (datetime.datetime.utcnow() - jwt_iat).seconds + if seconds_since_issue > 60 * jwt_exp_mins: + print('Refreshing token after {}s').format(seconds_since_issue) + client.loop_stop() + jwt_iat = datetime.datetime.utcnow() + client = get_client( + args.project_id, args.cloud_region, + args.registry_id, args.device_id, args.private_key_file, + args.algorithm, args.ca_certs, args.mqtt_bridge_hostname, + args.mqtt_bridge_port) + # 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. @@ -193,6 +227,7 @@ def main(): # End the network loop and finish. client.loop_stop() print('Finished.') +# [END iot_mqtt_run] if __name__ == '__main__':