Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional gcloud miscellaneous tooling #491

Merged
merged 3 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME --display-name="UDMI Cl
gcloud projects add-iam-policy-binding $PROJECT_ID --member="serviceAccount:$SERVICE_ACCOUNT" --role="roles/pubsub.publisher"
gcloud projects add-iam-policy-binding $PROJECT_ID --member="serviceAccount:$SERVICE_ACCOUNT" --role="roles/cloudiot.provisioner"

$ROOT_DIR/dashboard/deploy_dashboard_gcloud $PROJECT_ID --service-account=$SERVICE_ACCOUNT
$ROOT_DIR/udmis/deploy_udmis_gcloud $PROJECT_ID --service-account=$SERVICE_ACCOUNT

$ROOT_DIR/bin/clone_model
registry_id=$(jq -r .registry_id $ROOT_DIR/sites/udmi_site_model/cloud_iot_config.json)
Expand Down
6 changes: 6 additions & 0 deletions misc/gcloud_iot_config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# GCloud IoT Config Store

Stores a complete history of all config messages sent to devices in a GCP Project in GCS

## Installation
`./deploy PROJECT_ID`
38 changes: 38 additions & 0 deletions misc/gcloud_iot_config/deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash

if (( $# < 1 )); then
echo $0 PROJECT_ID
exit
fi

PROJECT_ID=$1
shift 1

FUNCTION_NAME=gcloud_iot_config
ENTRY_POINT=storeConfig
TOPIC=config_updates
BUCKET=$(PROJECT_ID)-iot-configs

## storage buckets create gs://$BUCKET_NAME --project=$PROJECT_ID --default-storage-class=STANDARD --location=us-central1
gcloud storage buckets create gs://$BUCKET --project=$PROJECT_ID --default-storage-class=STANDARD --location=us-central1

# Create pub/sub topic
gcloud pubsub topics create $TOPIC --project=$PROJECT_ID

# create logs router
gcloud logging sinks create config_updates_logsink \
pubsub.googleapis.com/projects/$PROJECT_ID/topics/$TOPIC \
--log-filter='protoPayload.methodName="google.cloud.iot.v1.DeviceManager.ModifyCloudToDeviceConfig" AND severity=NOTICE' \
--project=$PROJECT_ID

LOGSINK_SERVICE_ACCOUNT=$(gcloud logging sinks describe config_updates_logsink --project=$PROJECT_ID | grep writerIdentity | sed "s/writerIdentity: //")
gcloud pubsub topics add-iam-policy-binding $TOPIC --project=$PROJECT_ID --member=$LOGSINK_SERVICE_ACCOUNT --role='roles/pubsub.publisher'

# Deploy Cloud Function
gcloud functions deploy $FUNCTION_NAME \
--trigger-topic=$TOPIC\
--entry-point=$ENTRY_POINT \
--runtime nodejs16 \
--project=$PROJECT_ID \
--source=functions/ \
--set-env-vars PROJECT_ID=$PROJECT_ID,DATASET_ID=$DATASET_ID,BUCKET=$BUCKET
58 changes: 58 additions & 0 deletions misc/gcloud_iot_config/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const PROJECT_ID = process.env.PROJECT_ID
const DATASET_ID = process.env.DATASET_ID
const BUCKET = process.env.BUCKET

const iot = require('@google-cloud/iot');
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const iotClient = new iot.v1.DeviceManagerClient({});
const bucket = storage.bucket(BUCKET);
var fs = require('fs');

// projects/daq1-273309/locations/us-central1/registries/ZZ-TRI-FECTA/devices/AHU-1

exports.storeConfig = async (event, context) => {
const pubsubMessage = event.data;
const objStr = Buffer.from(pubsubMessage, 'base64').toString();
const logData = JSON.parse(objStr);

devicePath = logData['protoPayload']['resourceName']
versionToUpdate = logData['protoPayload']['request']['versionToUpdate']

const [response] = await iotClient.listDeviceConfigVersions({
name: devicePath,
});

splitDevice = devicePath.split('/')
registryId = splitDevice[5]
deviceId = splitDevice[7]

const configs = response.deviceConfigs;

if (configs.length === 0) {
return null;
}

configs.forEach((config, index) => {
if(config.version == versionToUpdate){
configPayload = config.binaryData.toString('utf8');
localFileName = `/tmp/${registryId}_${deviceId}_${versionToUpdate}.txt`

fs.writeFile(localFileName, configPayload, function (err) {
console.log(err)
});


const options = {
destination: `${registryId}/${deviceId}/${versionToUpdate}.json`
};

bucket.upload(localFileName, options, function(err, file) {
console.log(err)
});

}
});

};

9 changes: 9 additions & 0 deletions misc/gcloud_iot_config/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "config_to_gcs",
"version": "0.0.1",
"dependencies": {
"@google-cloud/pubsub": "^0.18.0",
"@google-cloud/iot": "2.3.4",
"@google-cloud/storage": "6.5.2"
}
}
47 changes: 47 additions & 0 deletions misc/gcloud_iot_connection_log/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# GCloud IoT Connection Logs

Stores all device connections and disconnection from IoT Core into BigQuery

**NOTE**
- Requires **Cloud Logging** on devices or registry be set to `INFO` or more.
- IoT Core has a default log entries limit of 2000 per second. If a registry has `DEBUG` level logging, this may very quickly be exceeding, and will result in missing connection or disconnection log events

## Installation

`./deploy PROJECT_ID DATASET_ID LOCATION TRIGGER_TOPIC [--drop]`

**NOTE** cloud function deployed with default app-engine service account. This may need to change if this account does not have required permissions

## Example Queries

### List devices which were once connected that now offline

```sql
SELECT *,
Row_number() OVER(partition BY device_id, registry_id ORDER BY timestamp DESC) AS rank
FROM `PROJECT_ID.udmi.iot_connects` qualify rank = 1
AND event = 0
ORDER BY timestamp DESC
```

### List device outages exceeding X minutes

```sql
SELECT device_id,
qtimestamp disconnect_time,
timestamp reconnect_time,
outage_minutes
FROM (
SELECT device_id,
qtimestamp,
timestamp,
Datetime_diff(timestamp, qtimestamp, minute) AS outage_minutes
FROM (
SELECT *,
Lag(timestamp) OVER (partition BY device_id, registry_id ORDER BY timestamp, event) qtimestamp
FROM `PROJECT_ID.udmi.iot_connects`
WHERE logentry = 1
AND event = 1 )
WHERE timestamp IS NOT NULL )
WHERE outage_minutes > 10
```
50 changes: 50 additions & 0 deletions misc/gcloud_iot_connection_log/deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash

if (( $# < 4 )); then
echo $0 PROJECT_ID DATASET_ID LOCATION TRIGGER_TOPIC [--drop]
exit
fi

PROJECT_ID=$1
DATASET_ID=$2
LOCATION=$3
TRIGGER_TOPIC=$4
shift 4



if [[ $1 == "--drop" ]]; then
shift 1
#echo "WARNING: Dropping tables in 5 seconds. Data will be permanently lost"
#sleep 5 && echo "Deleting tables ..." && sleep 3
bq rm -t -f $PROJECT_ID:$DATASET_ID.iot_connects
fi

FUNCTION_NAME=gcloud_iot_connection_log
TOPIC=iot_connections

# State table
bq mk \
--table \
$PROJECT_ID:$DATASET_ID.iot_connects \
schema_iot_logs.json

# Create pub/sub topic
gcloud pubsub topics create $TOPIC

# create logs router
gcloud logging sinks create iot_connections_logsink \
pubsub.googleapis.com/projects/$PROJECT_ID/topics/$TOPIC \
--log-filter='resource.type="gce_instance"'

LOGSINK_SERVICE_ACCOUNT=$(gcloud logging sinks describe iot_connections_logsink --project=$PROJECT_ID | grep writerIdentity | sed "s/writerIdentity: //")
gcloud pubsub topics add-iam-policy-binding $TOPIC --project=$PROJECT_ID --member=$LOGSINK_SERVICE_ACCOUNT --role='roles/pubsub.publisher'

# Deploy Cloud Function
gcloud functions deploy $FUNCTION_NAME \
--trigger-topic=logtest\
--entry-point=logConnectionEvents \
--runtime nodejs16 \
--project=$PROJECT_ID \
--source=functions/ \
--set-env-vars PROJECT_ID=$PROJECT_ID,DATASET_ID=$DATASET_ID
76 changes: 76 additions & 0 deletions misc/gcloud_iot_connection_log/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const {BigQuery} = require('@google-cloud/bigquery');
const bigquery = new BigQuery();

const PROJECT_ID = process.env.PROJECT_ID
const DATASET_ID = process.env.DATASET_ID

exports.logConnectionEvents = async (event, context) => {
const pubsubMessage = event.data;
const objStr = Buffer.from(pubsubMessage, 'base64').toString();
const logData = JSON.parse(objStr);

if (logData['jsonPayload']['eventType'] == "CONNECT"){
oldState = 0;
newState = 1;
} else if (logData['jsonPayload']['eventType'] == "DISCONNECT") {
newState = 0;
oldState = 1;
} else {
return;
}

if (logData['jsonPayload']['status']['description'] == "OK" ){
description = null;
message = null;
} else {
description = logData['jsonPayload']['status']['description'];
message = logData['jsonPayload']['status']['message'];
}

// reduce nanoseconds to microseconds
ts_ts = logData['timestamp'].substring(0,19)
ts_ns = logData['timestamp'].substring(20,29)
ms = Math.floor(parseInt(ts_ns) / 1000).padStart(6, '0')
ts = ts_ts + '.' + String(ms).padStart(6, '0') + 'Z';

// Order events by timestamp
// We don't need microsecond accuracy, so either add or subtract 1 microsecond
// to avoid dealing with dates and set such that t1 < t2
if (ms == 0) {
t1 = ts
t2 = ts_ts + '.000001Z';
} else {
t2 = ts;
t1 = ts_ts + '.' + String(ms - 1).padStart(6, '0') + 'Z';
}


// the log event is always the newer entry
log_entry = {
timestamp: t2,
device_id: logData['labels']['device_id'],
device_num_id: logData['resource']['labels']['device_num_id'],
registry_id: logData['resource']['labels']['device_registry_id'],
event: newState,
logentry: 1,
logentry_description: description,
logentry_message: message
}


// the "derivative" is always preceding entry
log_derivative = {
timestamp: t1,
device_id: logData['labels']['device_id'],
device_num_id: logData['resource']['labels']['device_num_id'],
registry_id: logData['resource']['labels']['device_registry_id'],
event: oldState,
logentry: 0
}

return await Promise.all([
bigquery.dataset(DATASET_ID).table('iot_connects').insert([log_derivative, log_entry]),
]);

};

8 changes: 8 additions & 0 deletions misc/gcloud_iot_connection_log/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "iotlogs-to-bq",
"version": "0.0.1",
"dependencies": {
"@google-cloud/pubsub": "^0.18.0",
"@google-cloud/bigquery": "^3.0.0"
}
}
42 changes: 42 additions & 0 deletions misc/gcloud_iot_connection_log/schema_iot_logs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"mode": "NULLABLE",
"name": "timestamp",
"type": "TIMESTAMP"
},
{
"mode": "NULLABLE",
"name": "registry_id",
"type": "STRING"
},
{
"mode": "NULLABLE",
"name": "device_id",
"type": "STRING"
},
{
"mode": "NULLABLE",
"name": "device_num_id",
"type": "STRING"
},
{
"mode": "NULLABLE",
"name": "event",
"type": "INTEGER"
},
{
"mode": "NULLABLE",
"name": "logentry",
"type": "INTEGER"
},
{
"mode": "NULLABLE",
"name": "logentry_description",
"type": "STRING"
},
{
"mode": "NULLABLE",
"name": "logentry_message",
"type": "STRING"
}
]
14 changes: 14 additions & 0 deletions misc/gcloud_messages_telemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# GCloud Messages and Telemetry to BigQuery

Saves a record of all messages into BigQuery, aswell as telemetry into a narrow data structure

## Installation

`./deploy PROJECT_ID DATASET_ID LOCATION TRIGGER_TOPIC [--drop]`

**NOTE** cloud function deployed with default app-engine service account. This may need to change if this account does not have required permissions

## BigQuery Tables

- `messages` record of messages
- `telemetry` telemetry
Loading