sensorlog_lite: Add client CLI.

CLI runs against master device Syncbase, with support for adding
measuring devices, creating streams, and listing measured data.
Updated README.

Change-Id: Icc096466735a590d836aa78b7219ebfc284740a5
diff --git a/go/src/v.io/x/sensorlog/Makefile b/go/src/v.io/x/sensorlog/Makefile
index 84bb5a2..1585537 100644
--- a/go/src/v.io/x/sensorlog/Makefile
+++ b/go/src/v.io/x/sensorlog/Makefile
@@ -6,12 +6,16 @@
 .DELETE_ON_ERROR:
 
 .PHONY: all
-all: measured
+all: measured slcli
 
 .PHONY: measured
 measured:
 	jiri go install v.io/x/sensorlog_lite/measured
 
+.PHONY: slcli
+slcli:
+	jiri go install v.io/x/sensorlog_lite/slcli
+
 .PHONY: run-deps
 run-deps:
 	jiri go install v.io/x/ref/services/mounttable/mounttabled
diff --git a/go/src/v.io/x/sensorlog/README.md b/go/src/v.io/x/sensorlog/README.md
index 141d9a0..23e403a 100644
--- a/go/src/v.io/x/sensorlog/README.md
+++ b/go/src/v.io/x/sensorlog/README.md
@@ -4,25 +4,81 @@
 time series data on a group of devices.
 
 A Sensor Log Lite system consists of a master and any number of measuring
-devices. The master device runs Syncbase and the command line client, while
-each measuring device runs Syncbase and the measuring daemon (`measured`).
-The client controls the measuring daemon through Syncbase.
+devices. The master device runs Syncbase and the command line client
+(`slcli`), while each measuring device runs Syncbase and the measuring
+daemon (`measured`). The client controls the measuring daemon through
+Syncbase.
 
 # Measuring data
 
-TODO(ivanpi): Skeleton, expand.
+Each measuring device runs an instance of `measured`, which samples data
+points for one or more measuring streams. A stream is an ordered sequence
+of data points that are sampled by running a shell script at a set
+frequency. The `slcli` tool can be used to write the sampling configuration
+to the master Syncbase instance, which is then synced to the appropriate
+measuring device Syncbase. Measured data points are synced back to the
+master Syncbase, where they can be examined using `slcli`.
 
-## Starting `measured`
+### Starting `measured`
+
+In the instructions below, replace `<creds>` with the path to
+Vanadium credentials obtained using `principal seekblessings`.
 
 `measured`, along with the required `mounttabled` and `syncbased` services,
 can be started by:
 
-    $ V23_CREDENTIALS=<creds> ./scripts/run_measured.sh
+    $ V23_CREDENTIALS=<creds> SL_DEVID=dev1 ./scripts/run_measured.sh
 
-## Using the client
+By default, it starts a local mounttable at port 8707 (override using
+`$SL_IPADDR_PORT`), mounts it at
+`/ns.dev.v.io:8101/users/<email-from-blessing>/sl/measured/<devid>`,
+starts a Syncbase instance mounted in the local mounttable at
+`sl/measured/<devid>`, and a measuring daemon against the Syncbase instance.
 
-Before using the client, `syncbased` must be started by:
+### Using the client
+
+Before using the client, the master `syncbased` must be started by:
 
     $ V23_CREDENTIALS=<creds> ./scripts/run_cli_syncbased.sh
 
-TODO(ivanpi): Skeleton, expand.
+By default, it starts a local mounttable at port 8202 (override using
+`$SL_IPADDR_PORT`) and starts a Syncbase instance mounted in it at
+`sl/client/main` (override using `$SL_DEVID`).
+
+Running
+
+    $ V23_CREDENTIALS=<creds> ./scripts/slcli.sh <args>
+
+will invoke the `slcli` tool with blessings and flags set appropriately
+for a master stack run with the same environment.
+
+### Adding a device
+
+Each measuring device creates a syncgroup which can be joined by a master
+device. Using the above default configuration, run:
+
+    $ V23_CREDENTIALS=<creds> ./scripts/slcli.sh device add /ns.dev.v.io:8101/users/<email-from-blessing>/sl/measured/dev1/sl/measured/dev1/syncbased dev1
+
+### Creating a stream
+
+Once a device has been added, streams can be configured on it to start
+sampling data. The sampling script is expected to output a single floating
+point value and exit with a zero status code on every invocation, otherwise
+an error is logged instead of data.
+
+For example, to sample the Answer to Life, the Universe, and Everything
+every two seconds:
+
+    $ V23_CREDENTIALS=<creds> ./scripts/slcli.sh stream create dev1 stream42 2s <<< "echo 42;"
+
+### Listing data
+
+To list all data sampled on the stream, run
+
+    $ V23_CREDENTIALS=<creds> ./scripts/slcli.sh list dev1 stream42
+
+### Debugging
+
+The internal state of the master Syncbase can be examined using `sb`:
+
+    $ V23_CREDENTIALS=<creds> ${JIRI_ROOT}/release/go/bin/vbecome -name sl/client/main ${JIRI_ROOT}/release/go/bin/sb -service /$(dig $(hostname) +short):8202/sl/client/main/syncbased sh sensorlog_lite sldb
diff --git a/go/src/v.io/x/sensorlog/internal/client/list.go b/go/src/v.io/x/sensorlog/internal/client/list.go
index ebf55ea..92919a6 100644
--- a/go/src/v.io/x/sensorlog/internal/client/list.go
+++ b/go/src/v.io/x/sensorlog/internal/client/list.go
@@ -23,6 +23,7 @@
 // in chronological order, calling listCb for each.
 // TODO(ivanpi): Allow specifying time interval.
 func ListStreamData(ctx *context.T, db nosql.Database, streamKey *sbmodel.KStreamDef, listCb ListCallback) error {
+	tableName := sbmodel.KDataPoint{}.Table()
 	dataPrefix := keyutil.Join(streamKey.DevId, streamKey.StreamId, "")
 
 	bdb, err := db.BeginBatch(ctx, nosql_wire.BatchOptions{ReadOnly: true})
@@ -38,7 +39,7 @@
 		return verror.New(verror.ErrNoExist, ctx, "Stream '"+streamKey.Key()+"' does not exist")
 	}
 
-	sstr := bdb.Table(sbmodel.KDataPoint{}.Table()).Scan(ctx, nosql.Prefix(dataPrefix))
+	sstr := bdb.Table(tableName).Scan(ctx, nosql.Prefix(dataPrefix))
 	defer sstr.Cancel()
 
 	for sstr.Advance() {
diff --git a/go/src/v.io/x/sensorlog/internal/config/defaults.go b/go/src/v.io/x/sensorlog/internal/config/defaults.go
index 8050794..a948ae2 100644
--- a/go/src/v.io/x/sensorlog/internal/config/defaults.go
+++ b/go/src/v.io/x/sensorlog/internal/config/defaults.go
@@ -19,6 +19,8 @@
 
 	SyncPriority = 42
 
+	TimeOutputFormat = "2006-01-02 15:04:05.000"
+
 	// Delay between SIGINT and SIGKILL when stopping measuring script.
 	ScriptKillDelay = 100 * time.Millisecond
 
diff --git a/go/src/v.io/x/sensorlog/measured/measured.go b/go/src/v.io/x/sensorlog/measured/measured.go
index b8e3bf7..7c75976 100644
--- a/go/src/v.io/x/sensorlog/measured/measured.go
+++ b/go/src/v.io/x/sensorlog/measured/measured.go
@@ -26,7 +26,7 @@
 )
 
 var (
-	flagSbService = flag.String("service", config.DefaultSbService, "Location of the Syncbase service to connect to. Can be absolute or relative to the namespace root.")
+	flagSbService = flag.String("service", config.DefaultSbService, "Name of the Syncbase service to connect to. Can be absolute or relative to the namespace root.")
 	flagDevId     = flag.String("devid", "", "DevId to be claimed by this measured. Must be specified.")
 	// Flags below are only applied the first time measured is run against a Syncbase service with the same devid.
 	flagAdmin     = flag.String("admin", "", "Blessing of admin user allowed to join the syncgroup. Must be specified.")
diff --git a/go/src/v.io/x/sensorlog/scripts/run_cli_syncbased.sh b/go/src/v.io/x/sensorlog/scripts/run_cli_syncbased.sh
index 8c5b07a..e8ca090 100755
--- a/go/src/v.io/x/sensorlog/scripts/run_cli_syncbased.sh
+++ b/go/src/v.io/x/sensorlog/scripts/run_cli_syncbased.sh
@@ -13,7 +13,7 @@
 # Optional environment variables: SL_PREFIX, SL_DEVID, SL_IPADDR_PORT, SL_TMPDIR
 function main() {
   local -r PREFIX="${SL_PREFIX:-sl/client}"
-  local -r DEVID="${SL_DEVID:-$(gen_uuid)}"
+  local -r DEVID="${SL_DEVID:-main}"
   local -r NAME="${PREFIX}/${DEVID}"
   local -r IPADDR_PORT="${SL_IPADDR_PORT:-$(dig $(hostname) +short):8202}"
   local -r TMPDIR="${SL_TMPDIR:-sltmp}/${NAME}"
diff --git a/go/src/v.io/x/sensorlog/scripts/runner_lib.sh b/go/src/v.io/x/sensorlog/scripts/runner_lib.sh
index b4e69f2..f898362 100644
--- a/go/src/v.io/x/sensorlog/scripts/runner_lib.sh
+++ b/go/src/v.io/x/sensorlog/scripts/runner_lib.sh
@@ -110,3 +110,15 @@
   sleep 1
 }
 export -f run_measured
+
+# Runs slcli against master Syncbase with specified $NAME.
+# run_slcli MT NAME [args...]
+function run_slcli() {
+  local -r MT="$1"
+  local -r NAME="$2"
+  shift 2
+  "${JIRI_ROOT}"/release/go/bin/vbecome -name "$(name_to_blessing "${NAME}")" \
+    "${JIRI_ROOT}"/experimental/projects/sensorlog_lite/bin/slcli -v23.namespace.root "${MT}" \
+    -service "${NAME}/syncbased" "$@"
+}
+export -f run_slcli
diff --git a/go/src/v.io/x/sensorlog/scripts/slcli.sh b/go/src/v.io/x/sensorlog/scripts/slcli.sh
new file mode 100755
index 0000000..04fe39c
--- /dev/null
+++ b/go/src/v.io/x/sensorlog/scripts/slcli.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# Copyright 2015 The Vanadium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Runs slcli against master device Syncbase.
+
+set -eu
+
+source "${JIRI_ROOT}/experimental/projects/sensorlog_lite/src/v.io/x/sensorlog_lite/scripts/runner_lib.sh"
+
+# Must be run with V23_CREDENTIALS set or through the agent.
+# Optional environment variables: SL_PREFIX, SL_DEVID, SL_IPADDR_PORT
+function main() {
+  local -r PREFIX="${SL_PREFIX:-sl/client}"
+  local -r DEVID="${SL_DEVID:-main}"
+  local -r NAME="${PREFIX}/${DEVID}"
+  local -r IPADDR_PORT="${SL_IPADDR_PORT:-$(dig $(hostname) +short):8202}"
+  run_slcli "/${IPADDR_PORT}" "${NAME}" "$@"
+}
+
+main "$@"
diff --git a/go/src/v.io/x/sensorlog/slcli/device.go b/go/src/v.io/x/sensorlog/slcli/device.go
new file mode 100644
index 0000000..ee658f0
--- /dev/null
+++ b/go/src/v.io/x/sensorlog/slcli/device.go
@@ -0,0 +1,74 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// slcli device configuration.
+
+package main
+
+import (
+	"fmt"
+
+	"v.io/v23/context"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+	"v.io/x/sensorlog_lite/internal/client"
+	"v.io/x/sensorlog_lite/internal/sbmodel"
+	"v.io/x/sensorlog_lite/internal/sbutil"
+)
+
+var cmdSLDevice = &cmdline.Command{
+	Name:  "device",
+	Short: "Manage measuring devices",
+	Long: `
+Add measuring devices.
+
+TODO(ivanpi): List.
+`,
+	Children: []*cmdline.Command{cmdSLDeviceAdd /*, cmdSLDeviceList */},
+}
+
+var cmdSLDeviceAdd = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runSLDeviceAdd),
+	Name:   "add",
+	Short:  "Add new measuring device",
+	Long: `
+Adds a new measuring device and outputs its identifier.
+`,
+	ArgsName: "<publish_sb> <device_id> [<device_desc>]",
+	ArgsLong: `
+<publish_sb> is the rooted name of the Syncbase instance where the device
+             syncgroup is published.
+
+<device_id> is the identifier of the device to add.
+
+<device_desc> is a human-readable description of the device.
+              It doesn't need to be unique.
+`,
+}
+
+func runSLDeviceAdd(ctx *context.T, env *cmdline.Env, args []string) error {
+	if len(args) < 2 || len(args) > 3 {
+		return env.UsageErrorf("expects between 2 and 3 arguments")
+	}
+	sgPublishSb := args[0]
+	devId := args[1]
+	desc := ""
+	if len(args) > 2 {
+		desc = args[2]
+	}
+
+	db, err := sbutil.CreateOrOpenDB(ctx, *flagSbService, sbmodel.MasterTables)
+	if err != nil {
+		return fmt.Errorf("failed opening Syncbase db: %v", err)
+	}
+
+	devKey, err := client.AddDevice(ctx, db, devId, sgPublishSb, desc)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(env.Stdout, "%s\n", devKey.DevId)
+
+	return nil
+}
diff --git a/go/src/v.io/x/sensorlog/slcli/list.go b/go/src/v.io/x/sensorlog/slcli/list.go
new file mode 100644
index 0000000..d65e1e6
--- /dev/null
+++ b/go/src/v.io/x/sensorlog/slcli/list.go
@@ -0,0 +1,55 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// slcli measured data listing support.
+
+package main
+
+import (
+	"fmt"
+
+	"v.io/v23/context"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+	"v.io/x/sensorlog_lite/internal/client"
+	"v.io/x/sensorlog_lite/internal/config"
+	"v.io/x/sensorlog_lite/internal/sbmodel"
+	"v.io/x/sensorlog_lite/internal/sbutil"
+)
+
+var cmdSLList = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runSLList),
+	Name:   "list",
+	Short:  "List measured data",
+	Long: `
+Prints all measured data points for the given stream and device, sorted
+chronologically.
+`,
+	ArgsName: "<device_id> <stream_id>",
+	ArgsLong: `
+<device_id> and <stream_id> specify the data stream to print the data points
+from.
+`,
+}
+
+// TODO(ivanpi): Add time interval querying and aggregation functions.
+func runSLList(ctx *context.T, env *cmdline.Env, args []string) error {
+	if len(args) != 2 {
+		return env.UsageErrorf("expects exactly 2 arguments")
+	}
+	streamKey := &sbmodel.KStreamDef{
+		DevId:    args[0],
+		StreamId: args[1],
+	}
+
+	db, err := sbutil.CreateOrOpenDB(ctx, *flagSbService, sbmodel.MasterTables)
+	if err != nil {
+		return fmt.Errorf("failed opening Syncbase db: %v", err)
+	}
+
+	return client.ListStreamData(ctx, db, streamKey, func(key *sbmodel.KDataPoint, val sbmodel.VDataPoint) error {
+		fmt.Fprintf(env.Stdout, "%s %v\n", key.Timestamp.Format(config.TimeOutputFormat), val.Interface())
+		return nil
+	})
+}
diff --git a/go/src/v.io/x/sensorlog/slcli/slcli.go b/go/src/v.io/x/sensorlog/slcli/slcli.go
new file mode 100644
index 0000000..20c65f6
--- /dev/null
+++ b/go/src/v.io/x/sensorlog/slcli/slcli.go
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// slcli is the Sensor Log Lite command line configuration tool and client.
+// Must be run against master device Syncbase.
+// Supports configuring sampling streams by writing configuration to Syncbase,
+// which is then read by measured on the target device.
+// Also supports listing measured data.
+// TODO(ivanpi): Add data querying and graph plotting.
+package main
+
+import (
+	"flag"
+	"regexp"
+
+	"v.io/x/lib/cmdline"
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/sensorlog_lite/internal/config"
+)
+
+var (
+	flagSbService = flag.String("service", config.DefaultSbService, "Name of the Syncbase service to connect to. Can be absolute or relative to the namespace root.")
+)
+
+var cmdSensorLogLite = &cmdline.Command{
+	Name:  "slcli",
+	Short: "Sensor Log Lite command line configuration tool and client",
+	Long: `
+Command line interface for Sensor Log Lite, used for listing data and
+manipulating configuration via a master device Syncbase.
+`,
+	Children: []*cmdline.Command{cmdSLDevice, cmdSLStream, cmdSLList},
+}
+
+func main() {
+	cmdline.HideGlobalFlagsExcept(regexp.MustCompile(`^((service)|(v23\.namespace\.root)|(v23\.credentials))$`))
+	cmdline.Main(cmdSensorLogLite)
+}
diff --git a/go/src/v.io/x/sensorlog/slcli/stream.go b/go/src/v.io/x/sensorlog/slcli/stream.go
new file mode 100644
index 0000000..23c75bc
--- /dev/null
+++ b/go/src/v.io/x/sensorlog/slcli/stream.go
@@ -0,0 +1,97 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// slcli stream configuration.
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+	"v.io/x/sensorlog_lite/internal/client"
+	"v.io/x/sensorlog_lite/internal/sbmodel"
+	"v.io/x/sensorlog_lite/internal/sbutil"
+)
+
+var cmdSLStream = &cmdline.Command{
+	Name:  "stream",
+	Short: "Manage sampling streams",
+	Long: `
+Create sampling streams.
+
+TODO(ivanpi): Enable/disable, list.
+`,
+	Children: []*cmdline.Command{cmdSLStreamCreate /*, cmdSLStreamEnable, cmdSLStreamDisable, cmdSLStreamList*/},
+}
+
+var cmdSLStreamCreate = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runSLStreamCreate),
+	Name:   "create",
+	Short:  "Create new data stream",
+	Long: `
+Creates a new sampling stream and outputs its identifier.
+
+The sampling script is read from stdin. It must be a bash script that prints
+a single floating point result per invocation and exits with a zero status,
+otherwise an error is logged instead of the data point. The script will be
+run on the specified device at the specified frequency whenever the measuring
+daemon is running on the device, as long as the stream is enabled.
+`,
+	ArgsName: "<device_id> <stream_id> <interval> [<stream_desc>]",
+	ArgsLong: `
+<device_id> is the identifier of the device to use for sampling.
+
+<stream_id> is the identifier of the stream to be created. It must be unique
+            per measuring device.
+
+<interval> is the time interval between two subsequent invocations of the
+           measuring script. Measurements may be skipped if the interval is
+           too short compared to the device and sampling script speed.
+
+<stream_desc> is a human-readable description of the stream.
+              It doesn't need to be unique.
+`,
+}
+
+func runSLStreamCreate(ctx *context.T, env *cmdline.Env, args []string) error {
+	if len(args) < 3 || len(args) > 4 {
+		return env.UsageErrorf("expects between 3 and 4 arguments")
+	}
+	devKey := &sbmodel.KDeviceCfg{
+		DevId: args[0],
+	}
+	streamId := args[1]
+	interval, err := time.ParseDuration(args[2])
+	if err != nil {
+		return fmt.Errorf("failed parsing interval %q: %v", args[2], err)
+	}
+	desc := ""
+	if len(args) > 3 {
+		desc = args[3]
+	}
+
+	script, err := ioutil.ReadAll(env.Stdin)
+	if err != nil {
+		return err
+	}
+
+	db, err := sbutil.CreateOrOpenDB(ctx, *flagSbService, sbmodel.MasterTables)
+	if err != nil {
+		return fmt.Errorf("failed opening Syncbase db: %v", err)
+	}
+
+	streamKey, err := client.CreateStream(ctx, db, devKey, streamId, string(script), interval, desc)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(env.Stdout, "%s\n", streamKey.StreamId)
+
+	return nil
+}