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 reflector utility #408

Merged
merged 14 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jobs:
name: Sequence tests
runs-on: ubuntu-latest
timeout-minutes: 15
needs: redirect # Access to UDMI-REFLECTOR is mutually exclusive
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
Expand Down
23 changes: 23 additions & 0 deletions bin/reset_config
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash -e

ROOT=$(realpath $(dirname $0)/..)

if [[ $# != 3 ]]; then
echo Usage: $0 site_dir project_id device_id
false
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you want exec false? or just exit 1?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I avoid "exit 1" because sometimes I'll end up doing "source" on a script and then "exit 1" will totally bork the shell session... the behavior of "false" is only that if the -e flag is specified. So, in this particular case they are functionally equivalent, but I'm not sure if there's really a compelling reason for one over the other in the big picture.

fi

site_dir=$(realpath $1)
project_id=$2
device_id=$3
shift 3
cd $ROOT

device_config=/tmp/${device_id}_config.json
cp $site_dir/devices/${device_id}/out/generated_config.json $device_config
Copy link
Collaborator

Choose a reason for hiding this comment

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

Be consistent and ${var} everywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

now_date=$(date -Ins -u | sed -E 's/.{6}\+.*/Z/' | tr , .)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I love shell manipulation but we have to worry about OSX compat (if we target that) and one needs to run it to see what it does.

We already depend on python. What do you think of this oneliner? We could throw it into bin/util_output_utcnow_8601 or whatever.

print(datetime.datetime.utcnow().isoformat())
2022-08-04T13:44:21.327511

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

echo Setting config timestamp $now_date
jq .timestamp=\"$now_date\" < $device_config | sponge $device_config
Copy link
Collaborator

Choose a reason for hiding this comment

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

OSX, if we care.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Update: Just comment sponge is a Linuxism or whatever

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added runtime check and warning message, would have to fix actual OSX compat in another round (with QA)


echo Resetting device $device_id config...
validator/bin/reflector $site_dir $project_id $device_id update/config:$device_config
2 changes: 1 addition & 1 deletion bin/setup_base
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash

sudo apt-get install -y hxtools
sudo apt-get install -y hxtools moreutils

python3 -m venv venv

Expand Down
2 changes: 2 additions & 0 deletions bin/test_redirect
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ if [[ -n $pids ]]; then
kill $pids
fi

bin/reset_config $site_path $project_id $device_id

echo Writing pubber output to $PUBBER_OUT
echo bin/pubber $site_path $project_id $device_id $serial_no

Expand Down
2 changes: 2 additions & 0 deletions bin/test_validator
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ vpid=$!
sleep 10
echo Started validator pid $vpid

bin/reset_config $site_path $project_id $device_id

pubber/bin/build

echo Writing pubber output to $PUBBER_OUT
Expand Down
17 changes: 17 additions & 0 deletions validator/.idea/runConfigurations/Reflector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions validator/bin/reflector
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash -e

ROOT=$(realpath $(dirname $0)/../..)

if [[ $# < 4 ]]; then
echo Usage: $0 site_dir project_id device_id directive [directives...]
echo " Directive is something like update/config:sites/udmi_site_model/devices/AHU-1/out/generated_config.json"
false
Copy link
Collaborator

Choose a reason for hiding this comment

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

exit 1?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

same as other comment... not sure if there's a clear reason for one over the other because they're just "different" in how they handle different edge use cases...

fi

site_dir=$(realpath $1)
project_id=$2
device_id=$3
shift 3
cd $ROOT

validator/bin/build

jarfile=validator/build/libs/validator-1.0-SNAPSHOT-all.jar
mainclass=com.google.daq.mqtt.validator.Reflector

cmd="java -cp $jarfile $mainclass -p $project_id -s $site_dir -d $device_id $*"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just invoke directly and "$*" on end?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

there was supposed to be another diagnostic "echo" in there... added!

$cmd
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public IotReflectorClient(String projectId, CloudIotConfig iotConfig, String key
mqttPublisher = new MqttPublisher(projectId, cloudRegion, UDMS_REFLECT,
siteName, keyBytes, IOT_KEY_ALGORITHM, this::messageHandler, this::errorHandler);
} catch (Exception e) {
throw new RuntimeException("While connecting subscription " + subscriptionId, e);
throw new RuntimeException("While connecting MQTT endpoint " + subscriptionId, e);
}

active = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.google.daq.mqtt.registrar;

import static com.google.daq.mqtt.validator.Validator.NO_SITE;
import static com.google.daq.mqtt.util.Common.NO_SITE;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
Expand All @@ -9,7 +9,6 @@
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.github.fge.jsonschema.core.load.configuration.LoadingConfiguration;
import com.github.fge.jsonschema.core.load.download.URIDownloader;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.google.api.services.cloudiot.v1.model.Device;
Expand All @@ -30,7 +29,6 @@
import java.io.FilenameFilter;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.net.URI;
import java.time.Duration;
Expand Down
43 changes: 43 additions & 0 deletions validator/src/main/java/com/google/daq/mqtt/util/Common.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.google.daq.mqtt.util;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import java.util.List;
import java.util.MissingFormatArgumentException;

/**
* Collection of common constants and minor utilities.
*/
public abstract class Common {

public static final String STATE_QUERY_TOPIC = "query/state";
public static final String TIMESTAMP_ATTRIBUTE = "timestamp";
public static final String NO_SITE = "--";
public static final ObjectMapper OBJECT_MAPPER =
new ObjectMapper()
.enable(Feature.ALLOW_COMMENTS)
.enable(SerializationFeature.INDENT_OUTPUT)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.setDateFormat(new ISO8601DateFormat())
.setSerializationInclusion(Include.NON_NULL);
public static final String JSON_SUFFIX = ".json";
public static final String GCP_REFLECT_KEY_PKCS8 = "validator/rsa_private.pkcs8";

/**
* Remove the next item from the list in an exception-safe way.
*
* @param argList list of arguments
* @return removed argument
*/
public static String removeNextArg(List<String> argList) {
if (argList.isEmpty()) {
throw new MissingFormatArgumentException("Missing argument");
}
return argList.remove(0);
}
}
140 changes: 140 additions & 0 deletions validator/src/main/java/com/google/daq/mqtt/validator/Reflector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.google.daq.mqtt.validator;

import static com.google.daq.mqtt.util.Common.GCP_REFLECT_KEY_PKCS8;
import static com.google.daq.mqtt.util.Common.NO_SITE;
import static com.google.daq.mqtt.util.Common.removeNextArg;

import com.google.bos.iot.core.proxy.IotReflectorClient;
import com.google.daq.mqtt.util.CloudIotConfig;
import com.google.daq.mqtt.util.CloudIotManager;
import com.google.daq.mqtt.util.ConfigUtil;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* General utility for working with UDMI Reflector messages.
*/
public class Reflector {

private final List<String> reflectCommands;
private String projectId;
private String siteDir;
private CloudIotConfig cloudIotConfig;
private File baseDir;
private IotReflectorClient client;
private String deviceId;

/**
* Create an instance of the Reflector class.
*
* @param argsList command-line arguments
*/
public Reflector(List<String> argsList) {
reflectCommands = parseArgs(argsList);
}

/**
* Let's go.
*
* @param args command-line arguments
*/
public static void main(String[] args) {
Reflector reflector = new Reflector(Arrays.asList(args));
reflector.initialize();
reflector.reflect();
reflector.shutdown();
}

private void shutdown() {
try {
Thread.sleep(5000);
} catch (Exception e) {
throw new RuntimeException("While sleeping", e);
}
client.close();
}

private void reflect() {
System.err.printf("Reflecting %d directives...%n", reflectCommands.size());
while (!reflectCommands.isEmpty()) {
reflect(reflectCommands.remove(0));
}
System.err.println("Done with all reflective directives!");
}

private void reflect(String directive) {
System.err.printf("Reflecting %s%n", directive);
String[] parts = directive.split(":", 2);
if (parts.length != 2) {
throw new RuntimeException("Expected topic:file format reflect directive " + directive);
}
File file = new File(parts[1]);
reflect(parts[0], file);
}

private void reflect(String topic, File dataFile) {
try (FileInputStream fis = new FileInputStream(dataFile)) {
String data = new String(fis.readAllBytes());
reflect(topic, data);
} catch (Exception e) {
throw new RuntimeException("While processing input file " + dataFile.getAbsolutePath(), e);
}
}

private void reflect(String topic, String data) {
client.publish(deviceId, topic, data);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this going to verify that the json input is well-formed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No -- it's not actually even strictly JSON. This was meant as a reasonably low-level utility that wasn't really opinionated about formats. Maybe future iterations could add a "validate schema" flag or something... E.g. this tool can be used to validate that a borked garbage config does not tank the system.

}

private void initialize() {
String keyFile = new File(siteDir, GCP_REFLECT_KEY_PKCS8).getAbsolutePath();
System.err.println("Loading reflector key file from " + keyFile);
client = new IotReflectorClient(projectId, cloudIotConfig, keyFile);
}

private List<String> parseArgs(List<String> argsList) {
List<String> listCopy = new ArrayList<>(argsList);
while (!listCopy.isEmpty()) {
String option = removeNextArg(listCopy);
try {
switch (option) {
case "-p":
projectId = removeNextArg(listCopy);
break;
case "-s":
setSiteDir(removeNextArg(listCopy));
break;
case "-d":
deviceId = removeNextArg(listCopy);
break;
default:
listCopy.add(option);
return listCopy; // default case is the remaining list of reflection directives
}
} catch (Exception e) {
throw new RuntimeException("While processing option " + option, e);
}
}
throw new IllegalArgumentException("No reflect directives specified!");
}

/**
* Set the site directory to use for this run.
*
* @param siteDir site model directory
*/
public void setSiteDir(String siteDir) {
if (NO_SITE.equals(siteDir)) {
siteDir = null;
baseDir = new File(".");
} else {
this.siteDir = siteDir;
baseDir = new File(siteDir);
File cloudConfig = new File(siteDir, "cloud_iot_config.json");
cloudIotConfig = CloudIotManager.validate(ConfigUtil.readCloudIotConfig(cloudConfig),
projectId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.daq.mqtt.util.CloudIotConfig;
import com.google.daq.mqtt.util.Common;
import com.google.daq.mqtt.util.ConfigUtil;
import com.google.daq.mqtt.util.ValidatorConfig;
import java.io.ByteArrayOutputStream;
Expand Down Expand Up @@ -148,6 +149,7 @@ public abstract class SequenceValidator {
resultSummary.delete();
System.err.println("Writing results to " + resultSummary.getAbsolutePath());

System.err.printf("Loading reflector key file from %s%n", new File(key_file).getAbsolutePath());
System.err.printf("Validating against device %s serial %s%n", deviceId, serialNo);
client = new IotReflectorClient(projectId, cloudIotConfig, key_file);
setReflectorState();
Expand Down Expand Up @@ -454,7 +456,7 @@ private String writeLogEntry(Entry logEntry, String filename) {
}

protected void queryState() {
client.publish(deviceId, Validator.STATE_QUERY_TOPIC, EMPTY_MESSAGE);
client.publish(deviceId, Common.STATE_QUERY_TOPIC, EMPTY_MESSAGE);
}

/**
Expand Down
Loading