Import of https://github.com/ciena/voltctl at commit 40d61fbf3f910ed4017cf67c9c79e8e1f82a33a5

Change-Id: I8464c59e60d76cb8612891db3303878975b5416c
diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go
new file mode 100644
index 0000000..5aa24ad
--- /dev/null
+++ b/pkg/filter/filter.go
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package filter
+
+import (
+	"fmt"
+	"reflect"
+	"regexp"
+	"strings"
+)
+
+type Operation int
+
+const (
+	UK Operation = iota
+	EQ
+	NE
+	GT
+	LT
+	GE
+	LE
+	RE
+)
+
+func toOp(op string) Operation {
+	switch op {
+	case "=":
+		return EQ
+	case "!=":
+		return NE
+	case ">":
+		return GT
+	case "<":
+		return LT
+	case ">=":
+		return GE
+	case "<=":
+		return LE
+	case "~":
+		return RE
+	default:
+		return UK
+	}
+}
+
+type FilterTerm struct {
+	Op    Operation
+	Value string
+	re    *regexp.Regexp
+}
+
+type Filter map[string]FilterTerm
+
+var termRE = regexp.MustCompile("^\\s*([a-zA-Z_][.a-zA-Z0-9_]*)\\s*(~|<=|>=|<|>|!=|=)\\s*(.+)\\s*$")
+
+// Parse parses a comma separated list of filter terms
+func Parse(spec string) (Filter, error) {
+	filter := make(map[string]FilterTerm)
+	terms := strings.Split(spec, ",")
+	var err error
+
+	// Each term is in the form <key><op><value>
+	for _, term := range terms {
+		parts := termRE.FindAllStringSubmatch(term, -1)
+		if parts == nil {
+			return nil, fmt.Errorf("Unable to parse filter term '%s'", term)
+		}
+		ft := FilterTerm{
+			Op:    toOp(parts[0][2]),
+			Value: parts[0][3],
+		}
+		if ft.Op == RE {
+			ft.re, err = regexp.Compile(ft.Value)
+			if err != nil {
+				return nil, fmt.Errorf("Unable to parse regexp filter value '%s'", ft.Value)
+			}
+		}
+		filter[parts[0][1]] = ft
+	}
+	return filter, nil
+}
+
+func (f Filter) Process(data interface{}) (interface{}, error) {
+	slice := reflect.ValueOf(data)
+	if slice.Kind() != reflect.Slice {
+		if f.Evaluate(data) {
+			return data, nil
+		}
+		return nil, nil
+	}
+
+	var result []interface{}
+
+	for i := 0; i < slice.Len(); i++ {
+		if f.Evaluate(slice.Index(i).Interface()) {
+			result = append(result, slice.Index(i).Interface())
+		}
+	}
+
+	return result, nil
+}
+
+func (f Filter) Evaluate(item interface{}) bool {
+	val := reflect.ValueOf(item)
+
+	for k, v := range f {
+		field := val.FieldByName(k)
+		if !field.IsValid() {
+			return false
+		}
+
+		switch v.Op {
+		case RE:
+			if !v.re.MatchString(fmt.Sprintf("%v", field)) {
+				return false
+			}
+		case EQ:
+			// This seems to work for most comparisons
+			if fmt.Sprintf("%v", field) != v.Value {
+				return false
+			}
+		default:
+			// For unsupported operations, always pass
+		}
+	}
+	return true
+}
diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go
new file mode 100644
index 0000000..c4c02f5
--- /dev/null
+++ b/pkg/filter/filter_test.go
@@ -0,0 +1,214 @@
+package filter
+
+import (
+	"testing"
+)
+
+type TestFilterStruct struct {
+	One   string
+	Two   string
+	Three string
+}
+
+func TestFilterList(t *testing.T) {
+	f, err := Parse("One=a,Two=b")
+	if err != nil {
+		t.Errorf("Unable to parse filter: %s", err.Error())
+	}
+
+	data := []interface{}{
+		TestFilterStruct{
+			One:   "a",
+			Two:   "b",
+			Three: "c",
+		},
+		TestFilterStruct{
+			One:   "1",
+			Two:   "2",
+			Three: "3",
+		},
+		TestFilterStruct{
+			One:   "a",
+			Two:   "b",
+			Three: "z",
+		},
+	}
+
+	r, _ := f.Process(data)
+
+	if _, ok := r.([]interface{}); !ok {
+		t.Errorf("Expected list, but didn't get one")
+	}
+
+	if len(r.([]interface{})) != 2 {
+		t.Errorf("Expected %d got %d", 2, len(r.([]interface{})))
+	}
+
+	if r.([]interface{})[0] != data[0] {
+		t.Errorf("Filtered list did not match, item %d", 0)
+	}
+	if r.([]interface{})[1] != data[2] {
+		t.Errorf("Filtered list did not match, item %d", 1)
+	}
+}
+
+func TestFilterItem(t *testing.T) {
+	f, err := Parse("One=a,Two=b")
+	if err != nil {
+		t.Errorf("Unable to parse filter: %s", err.Error())
+	}
+
+	data := TestFilterStruct{
+		One:   "a",
+		Two:   "b",
+		Three: "c",
+	}
+
+	r, _ := f.Process(data)
+
+	if r == nil {
+		t.Errorf("Expected item, got nil")
+	}
+
+	if _, ok := r.([]interface{}); ok {
+		t.Errorf("Expected item, but got list")
+	}
+}
+
+func TestGoodFilters(t *testing.T) {
+	var f Filter
+	var err error
+	f, err = Parse("One=a,Two=b")
+	if err != nil {
+		t.Errorf("1. Unable to parse filter: %s", err.Error())
+	}
+	if len(f) != 2 ||
+		f["One"].Value != "a" ||
+		f["One"].Op != EQ ||
+		f["Two"].Value != "b" ||
+		f["Two"].Op != EQ {
+		t.Errorf("1. Filter did not parse correctly")
+	}
+
+	f, err = Parse("One=a")
+	if err != nil {
+		t.Errorf("2. Unable to parse filter: %s", err.Error())
+	}
+	if len(f) != 1 ||
+		f["One"].Value != "a" ||
+		f["One"].Op != EQ {
+		t.Errorf("2. Filter did not parse correctly")
+	}
+
+	f, err = Parse("One<a")
+	if err != nil {
+		t.Errorf("3. Unable to parse filter: %s", err.Error())
+	}
+	if len(f) != 1 ||
+		f["One"].Value != "a" ||
+		f["One"].Op != LT {
+		t.Errorf("3. Filter did not parse correctly")
+	}
+
+	f, err = Parse("One!=a")
+	if err != nil {
+		t.Errorf("4. Unable to parse filter: %s", err.Error())
+	}
+	if len(f) != 1 ||
+		f["One"].Value != "a" ||
+		f["One"].Op != NE {
+		t.Errorf("4. Filter did not parse correctly")
+	}
+}
+
+func TestBadFilters(t *testing.T) {
+	_, err := Parse("One%a")
+	if err == nil {
+		t.Errorf("Parsed filter when it shouldn't have")
+	}
+}
+
+func TestSingleRecord(t *testing.T) {
+	f, err := Parse("One=d")
+	if err != nil {
+		t.Errorf("Unable to parse filter: %s", err.Error())
+	}
+
+	data := TestFilterStruct{
+		One:   "a",
+		Two:   "b",
+		Three: "c",
+	}
+
+	r, err := f.Process(data)
+	if err != nil {
+		t.Errorf("Error processing data")
+	}
+
+	if r != nil {
+		t.Errorf("expected no results, got some")
+	}
+}
+
+// Invalid fields are ignored (i.e. an error is returned, but need to
+// cover the code path in tests
+func TestInvalidField(t *testing.T) {
+	f, err := Parse("Four=a")
+	if err != nil {
+		t.Errorf("Unable to parse filter: %s", err.Error())
+	}
+
+	data := TestFilterStruct{
+		One:   "a",
+		Two:   "b",
+		Three: "c",
+	}
+
+	r, err := f.Process(data)
+	if err != nil {
+		t.Errorf("Error processing data")
+	}
+
+	if r != nil {
+		t.Errorf("expected no results, got some")
+	}
+}
+
+func TestREFilter(t *testing.T) {
+	var f Filter
+	var err error
+	f, err = Parse("One~a")
+	if err != nil {
+		t.Errorf("Unable to parse RE expression")
+	}
+	if len(f) != 1 {
+		t.Errorf("filter parsed incorrectly")
+	}
+
+	data := []interface{}{
+		TestFilterStruct{
+			One:   "a",
+			Two:   "b",
+			Three: "c",
+		},
+		TestFilterStruct{
+			One:   "1",
+			Two:   "2",
+			Three: "3",
+		},
+		TestFilterStruct{
+			One:   "a",
+			Two:   "b",
+			Three: "z",
+		},
+	}
+
+	f.Process(data)
+}
+
+func TestBadRE(t *testing.T) {
+	_, err := Parse("One~(qs*")
+	if err == nil {
+		t.Errorf("Expected RE parse error, got none")
+	}
+}
diff --git a/pkg/format/formatter.go b/pkg/format/formatter.go
new file mode 100644
index 0000000..af92a03
--- /dev/null
+++ b/pkg/format/formatter.go
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package format
+
+import (
+	"io"
+	"reflect"
+	"regexp"
+	"strings"
+	"text/tabwriter"
+	"text/template"
+	"text/template/parse"
+)
+
+var nameFinder = regexp.MustCompile(`\.([\._A-Za-z0-9]*)}}`)
+
+type Format string
+
+func (f Format) IsTable() bool {
+	return strings.HasPrefix(string(f), "table")
+}
+
+func (f Format) Execute(writer io.Writer, withHeaders bool, nameLimit int, data interface{}) error {
+	var tabWriter *tabwriter.Writer = nil
+	format := f
+
+	if f.IsTable() {
+		tabWriter = tabwriter.NewWriter(writer, 0, 4, 4, ' ', 0)
+		format = Format(strings.TrimPrefix(string(f), "table"))
+	}
+
+	tmpl, err := template.New("output").Parse(string(format))
+	if err != nil {
+		return err
+	}
+
+	if f.IsTable() && withHeaders {
+		var header string
+		for _, n := range tmpl.Tree.Root.Nodes {
+			switch n.Type() {
+			case parse.NodeText:
+				header += n.String()
+			case parse.NodeString:
+				header += n.String()
+			case parse.NodeAction:
+				found := nameFinder.FindStringSubmatch(n.String())
+				if len(found) == 2 {
+					if nameLimit > 0 {
+						parts := strings.Split(found[1], ".")
+						start := len(parts) - nameLimit
+						if start < 0 {
+							start = 0
+						}
+						header += strings.ToUpper(strings.Join(parts[start:], "."))
+					} else {
+						header += strings.ToUpper(found[1])
+					}
+				}
+			}
+		}
+		tabWriter.Write([]byte(header))
+		tabWriter.Write([]byte("\n"))
+
+		slice := reflect.ValueOf(data)
+		if slice.Kind() == reflect.Slice {
+			for i := 0; i < slice.Len(); i++ {
+				tmpl.Execute(tabWriter, slice.Index(i).Interface())
+				tabWriter.Write([]byte("\n"))
+			}
+		} else {
+			tmpl.Execute(tabWriter, data)
+			tabWriter.Write([]byte("\n"))
+		}
+		tabWriter.Flush()
+		return nil
+	}
+
+	slice := reflect.ValueOf(data)
+	if slice.Kind() == reflect.Slice {
+		for i := 0; i < slice.Len(); i++ {
+			tmpl.Execute(writer, slice.Index(i).Interface())
+			writer.Write([]byte("\n"))
+		}
+	} else {
+		tmpl.Execute(writer, data)
+		writer.Write([]byte("\n"))
+	}
+	return nil
+
+}
diff --git a/pkg/format/formatter_test.go b/pkg/format/formatter_test.go
new file mode 100644
index 0000000..0819f22
--- /dev/null
+++ b/pkg/format/formatter_test.go
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package format
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+type TestSubStructure struct {
+	Value string
+}
+
+type TestStructure struct {
+	Field1 string
+	Field2 *string
+	Field3 bool
+	Field4 int
+	Field5 []string
+	Field6 [][]string
+	Field7 TestSubStructure
+	Field8 []TestSubStructure
+	Field9 *TestSubStructure
+}
+
+func generateTestData(rows int) []TestStructure {
+	data := make([]TestStructure, rows)
+
+	abc := "abc"
+	for i := 0; i < rows; i += 1 {
+		data[i].Field1 = fmt.Sprintf("0x%05x", i)
+		data[i].Field2 = &abc
+		if i%2 == 0 {
+			data[i].Field3 = true
+		}
+		data[i].Field4 = i
+		data[i].Field5 = []string{"a", "b", "c", "d"}
+		data[i].Field6 = [][]string{{"x", "y", "z"}}
+		data[i].Field7.Value = "abc"
+		data[i].Field8 = []TestSubStructure{{Value: "abc"}}
+		data[i].Field9 = &TestSubStructure{Value: "abc"}
+	}
+	return data
+}
+
+func TestTableFormat(t *testing.T) {
+	expected := "" +
+		"FIELD1     FIELD2    FIELD3    FIELD4    FIELD5       FIELD6       VALUE    FIELD8     FIELD9\n" +
+		"0x00000    abc       true      0         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00001    abc       false     1         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00002    abc       true      2         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00003    abc       false     3         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00004    abc       true      4         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00005    abc       false     5         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00006    abc       true      6         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00007    abc       false     7         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00008    abc       true      8         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n" +
+		"0x00009    abc       false     9         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n"
+	got := &strings.Builder{}
+	format := Format("table{{.Field1}}\t{{.Field2}}\t{{.Field3}}\t{{.Field4}}\t{{.Field5}}\t{{.Field6}}\t{{.Field7.Value}}\t{{.Field8}}\t{{.Field9}}")
+	data := generateTestData(10)
+	err := format.Execute(got, true, 1, data)
+	if err != nil {
+		t.Errorf("%s: unexpected error result: %s", t.Name(), err)
+	}
+	if got.String() != expected {
+		t.Logf("RECEIVED:\n%s\n", got.String())
+		t.Logf("EXPECTED:\n%s\n", expected)
+		t.Errorf("%s: expected and received did not match", t.Name())
+	}
+}
+
+func TestNoTableFormat(t *testing.T) {
+	expected := "" +
+		"0x00000,abc,true,0,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00001,abc,false,1,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00002,abc,true,2,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00003,abc,false,3,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00004,abc,true,4,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00005,abc,false,5,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00006,abc,true,6,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00007,abc,false,7,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00008,abc,true,8,[a b c d],[[x y z]],abc,[{abc}],{abc}\n" +
+		"0x00009,abc,false,9,[a b c d],[[x y z]],abc,[{abc}],{abc}\n"
+	got := &strings.Builder{}
+	format := Format("{{.Field1}},{{.Field2}},{{.Field3}},{{.Field4}},{{.Field5}},{{.Field6}},{{.Field7.Value}},{{.Field8}},{{.Field9}}")
+	data := generateTestData(10)
+	err := format.Execute(got, false, 0, data)
+	if err != nil {
+		t.Errorf("%s: unexpected error result: %s", t.Name(), err)
+	}
+	if got.String() != expected {
+		t.Logf("RECEIVED:\n%s\n", got.String())
+		t.Logf("EXPECTED:\n%s\n", expected)
+		t.Errorf("%s: expected and received did not match", t.Name())
+	}
+}
+
+func TestTableSingleFormat(t *testing.T) {
+	expected := "" +
+		"FIELD1     FIELD2    FIELD3    FIELD4    FIELD5       FIELD6       VALUE    FIELD8     FIELD9\n" +
+		"0x00000    abc       true      0         [a b c d]    [[x y z]]    abc      [{abc}]    {abc}\n"
+	got := &strings.Builder{}
+	format := Format("table{{.Field1}}\t{{.Field2}}\t{{.Field3}}\t{{.Field4}}\t{{.Field5}}\t{{.Field6}}\t{{.Field7.Value}}\t{{.Field8}}\t{{.Field9}}")
+	data := generateTestData(1)
+	err := format.Execute(got, true, 1, data[0])
+	if err != nil {
+		t.Errorf("%s: unexpected error result: %s", t.Name(), err)
+	}
+	if got.String() != expected {
+		t.Logf("RECEIVED:\n%s\n", got.String())
+		t.Logf("EXPECTED:\n%s\n", expected)
+		t.Errorf("%s: expected and received did not match", t.Name())
+	}
+}
+
+func TestNoTableSingleFormat(t *testing.T) {
+	expected := "0x00000,abc,true,0,[a b c d],[[x y z]],abc,[{abc}],{abc}\n"
+	got := &strings.Builder{}
+	format := Format("{{.Field1}},{{.Field2}},{{.Field3}},{{.Field4}},{{.Field5}},{{.Field6}},{{.Field7.Value}},{{.Field8}},{{.Field9}}")
+	data := generateTestData(1)
+	err := format.Execute(got, false, 0, data[0])
+	if err != nil {
+		t.Errorf("%s: unexpected error result: %s", t.Name(), err)
+	}
+	if got.String() != expected {
+		t.Logf("RECEIVED:\n%s\n", got.String())
+		t.Logf("EXPECTED:\n%s\n", expected)
+		t.Errorf("%s: expected and received did not match", t.Name())
+	}
+}
+
+func TestBadFormat(t *testing.T) {
+	format := Format("table{{.Field1}\t{{.Field2}}\t{{.Field3}}\t{{.Field4}}\t{{.Field5}}\t{{.Field6}}\t{{.Field7.Value}}\t{{.Field8}}\t{{.Field9}}")
+	got := &strings.Builder{}
+	data := generateTestData(10)
+	err := format.Execute(got, true, 0, data)
+	if err == nil {
+		t.Errorf("%s: expected error (bad format) got none", t.Name())
+	}
+}
diff --git a/pkg/model/adapter.go b/pkg/model/adapter.go
new file mode 100644
index 0000000..d46039b
--- /dev/null
+++ b/pkg/model/adapter.go
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"github.com/jhump/protoreflect/dynamic"
+)
+
+type Adapter struct {
+	Id       string
+	Vendor   string
+	Version  string
+	LogLevel string
+}
+
+func (adapter *Adapter) PopulateFrom(val *dynamic.Message) {
+	adapter.Id = val.GetFieldByName("id").(string)
+	adapter.Vendor = val.GetFieldByName("vendor").(string)
+	adapter.Version = val.GetFieldByName("version").(string)
+	var config *dynamic.Message = val.GetFieldByName("config").(*dynamic.Message)
+	if config != nil {
+		adapter.LogLevel = GetEnumValue(config, "log_level")
+	}
+}
diff --git a/pkg/model/component.go b/pkg/model/component.go
new file mode 100644
index 0000000..87a1e34
--- /dev/null
+++ b/pkg/model/component.go
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"fmt"
+	corev1 "k8s.io/api/core/v1"
+	"time"
+)
+
+type ComponentInstance struct {
+	Id        string `json:"id"`
+	Namespace string `json:"namespace"`
+	Name      string `json:"name"`
+	Ready     string `json:"ready"`
+	Status    string `json:"status"`
+	Restarts  int    `json:"restarts"`
+	Component string `json:"component"`
+	Version   string `json:"version"`
+	StartTime string `json:"starttime"`
+	Age       string `json:"age"`
+}
+
+func (c *ComponentInstance) PopulateFrom(val corev1.Pod) {
+	c.Id = val.ObjectMeta.Name
+	c.Namespace = val.ObjectMeta.Namespace
+	c.Name = val.ObjectMeta.Labels["app.kubernetes.io/name"]
+	c.Component = val.ObjectMeta.Labels["app.kubernetes.io/component"]
+	c.Version = val.ObjectMeta.Labels["app.kubernetes.io/version"]
+	c.Status = string(val.Status.Phase)
+
+	ready := 0
+	var restarts int = 0
+	for _, d := range val.Status.ContainerStatuses {
+		if d.Ready {
+			ready += 1
+		}
+		restarts += int(d.RestartCount)
+	}
+	c.Ready = fmt.Sprintf("%d/%d", ready, len(val.Status.ContainerStatuses))
+	c.Restarts = restarts
+
+	c.StartTime = val.Status.StartTime.Time.String()
+	c.Age = time.Since(val.Status.StartTime.Time).Truncate(time.Second).String()
+}
diff --git a/pkg/model/device.go b/pkg/model/device.go
new file mode 100644
index 0000000..716cbc9
--- /dev/null
+++ b/pkg/model/device.go
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"github.com/jhump/protoreflect/dynamic"
+)
+
+type PeerPort struct {
+	DeviceId string `json:"deviceid"`
+	PortNo   uint32 `json:"portno"`
+}
+
+type ProxyAddress struct {
+	DeviceId           string `json:"deviceId"`
+	DeviceType         string `json:"devicetype,omitempty"`
+	ChannelId          uint32 `json:"channelid"`
+	ChannelGroupId     uint32 `json:"channelgroup"`
+	ChannelTermination string `json:"channeltermination,omitempty"`
+	OnuId              uint32 `json:"onuid"`
+	OnuSessionId       uint32 `json:"onusessionid"`
+}
+
+type Device struct {
+	Id              string        `json:"id"`
+	Type            string        `json:"type"`
+	Root            bool          `json:"root"`
+	ParentId        string        `json:"parentid"`
+	ParentPortNo    uint32        `json:"parentportno"`
+	Vendor          string        `json:"vendor"`
+	Model           string        `json:"model"`
+	HardwareVersion string        `json:"hardwareversion"`
+	FirmwareVersion string        `json:"firmwareversion"`
+	SerialNumber    string        `json:"serialnumber"`
+	VendorId        string        `json:"vendorid"`
+	Adapter         string        `json:"adapter"`
+	Vlan            uint32        `json:"vlan"`
+	MacAddress      string        `json:"macaddress"`
+	Address         string        `json:"address"`
+	ExtraArgs       string        `json:"extraargs"`
+	ProxyAddress    *ProxyAddress `json:"proxyaddress,omitempty"`
+	AdminState      string        `json:"adminstate"`
+	OperStatus      string        `json:"operstatus"`
+	Reason          string        `json:"reason"`
+	ConnectStatus   string        `json:"connectstatus"`
+	Ports           []DevicePort  `json:"ports"`
+	Flows           []Flow        `json:"flows"`
+}
+
+type DevicePort struct {
+	PortNo     uint32     `json:"portno"`
+	Label      string     `json:"label"`
+	Type       string     `json:"type"`
+	AdminState string     `json:"adminstate"`
+	OperStatus string     `json:"operstatus"`
+	DeviceId   string     `json:"deviceid"`
+	Peers      []PeerPort `json:"peers"`
+}
+
+func (d *Device) PopulateFrom(val *dynamic.Message) {
+	d.Id = val.GetFieldByName("id").(string)
+	d.Type = val.GetFieldByName("type").(string)
+	d.Root = val.GetFieldByName("root").(bool)
+	d.ParentId = val.GetFieldByName("parent_id").(string)
+	d.ParentPortNo = val.GetFieldByName("parent_port_no").(uint32)
+	d.Vendor = val.GetFieldByName("vendor").(string)
+	d.Model = val.GetFieldByName("model").(string)
+	d.HardwareVersion = val.GetFieldByName("hardware_version").(string)
+	d.FirmwareVersion = val.GetFieldByName("firmware_version").(string)
+	d.SerialNumber = val.GetFieldByName("serial_number").(string)
+	d.VendorId = val.GetFieldByName("vendor_id").(string)
+	d.Adapter = val.GetFieldByName("adapter").(string)
+	d.MacAddress = val.GetFieldByName("mac_address").(string)
+	d.Vlan = val.GetFieldByName("vlan").(uint32)
+	d.Address = val.GetFieldByName("host_and_port").(string)
+	if len(d.Address) == 0 {
+		d.Address = val.GetFieldByName("ipv4_address").(string)
+	}
+	if len(d.Address) == 0 {
+		d.Address = val.GetFieldByName("ipv6_address").(string)
+	}
+	if len(d.Address) == 0 {
+		d.Address = "unknown"
+	}
+	d.ExtraArgs = val.GetFieldByName("extra_args").(string)
+	proxy := val.GetFieldByName("proxy_address").(*dynamic.Message)
+	d.ProxyAddress = nil
+	if proxy != nil {
+		d.ProxyAddress = &ProxyAddress{
+			DeviceId:       proxy.GetFieldByName("device_id").(string),
+			ChannelId:      proxy.GetFieldByName("channel_id").(uint32),
+			ChannelGroupId: proxy.GetFieldByName("channel_group_id").(uint32),
+			OnuId:          proxy.GetFieldByName("onu_id").(uint32),
+			OnuSessionId:   proxy.GetFieldByName("onu_session_id").(uint32),
+		}
+		v, err := proxy.TryGetFieldByName("device_type")
+		if err == nil {
+			d.ProxyAddress.DeviceType = v.(string)
+		}
+		v, err = proxy.TryGetFieldByName("channel_termination")
+		if err == nil {
+			d.ProxyAddress.ChannelTermination = v.(string)
+		}
+	}
+	d.AdminState = GetEnumValue(val, "admin_state")
+	d.OperStatus = GetEnumValue(val, "oper_status")
+	d.Reason = val.GetFieldByName("reason").(string)
+	d.ConnectStatus = GetEnumValue(val, "connect_status")
+
+	ports := val.GetFieldByName("ports").([]interface{})
+	d.Ports = make([]DevicePort, len(ports))
+	for i, port := range ports {
+		d.Ports[i].PopulateFrom(port.(*dynamic.Message))
+	}
+	flows := val.GetFieldByName("flows").(*dynamic.Message)
+	if flows == nil {
+		d.Flows = make([]Flow, 0)
+	} else {
+		items := flows.GetFieldByName("items").([]interface{})
+		d.Flows = make([]Flow, len(items))
+		for i, flow := range items {
+			d.Flows[i].PopulateFrom(flow.(*dynamic.Message))
+		}
+	}
+}
+
+func (port *DevicePort) PopulateFrom(val *dynamic.Message) {
+	port.PortNo = val.GetFieldByName("port_no").(uint32)
+	port.Type = GetEnumValue(val, "type")
+	port.Label = val.GetFieldByName("label").(string)
+	port.AdminState = GetEnumValue(val, "admin_state")
+	port.OperStatus = GetEnumValue(val, "oper_status")
+	port.DeviceId = val.GetFieldByName("device_id").(string)
+	peers := val.GetFieldByName("peers").([]interface{})
+	port.Peers = make([]PeerPort, len(peers))
+	for j, peer := range peers {
+		p := peer.(*dynamic.Message)
+		port.Peers[j].DeviceId = p.GetFieldByName("device_id").(string)
+		port.Peers[j].PortNo = p.GetFieldByName("port_no").(uint32)
+	}
+}
diff --git a/pkg/model/devicegroup.go b/pkg/model/devicegroup.go
new file mode 100644
index 0000000..a433a08
--- /dev/null
+++ b/pkg/model/devicegroup.go
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"github.com/jhump/protoreflect/dynamic"
+)
+
+type DeviceGroup struct {
+	Id             string   `json:"id"`
+	LogicalDevices []string `json:"logicaldevices"`
+	Devices        []string `json:"devices"`
+}
+
+func getId(val *dynamic.Message) string {
+	return val.GetFieldByName("id").(string)
+}
+
+func (d *DeviceGroup) PopulateFrom(val *dynamic.Message) {
+	d.Id = val.GetFieldByName("id").(string)
+	logicaldevices := val.GetFieldByName("logical_devices").([]interface{})
+	d.LogicalDevices = make([]string, len(logicaldevices))
+	for i, logicaldevice := range logicaldevices {
+		d.LogicalDevices[i] = getId(logicaldevice.(*dynamic.Message))
+	}
+	devices := val.GetFieldByName("logical_devices").([]interface{})
+	d.Devices = make([]string, len(devices))
+	for i, device := range devices {
+		d.Devices[i] = getId(device.(*dynamic.Message))
+	}
+}
diff --git a/pkg/model/flow.go b/pkg/model/flow.go
new file mode 100644
index 0000000..11b1b97
--- /dev/null
+++ b/pkg/model/flow.go
@@ -0,0 +1,453 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"fmt"
+	"github.com/jhump/protoreflect/dynamic"
+)
+
+type FlowFieldFlag uint64
+
+const (
+
+	// Define bit flags for flow fields to determine what is set and
+	// what is not
+	FLOW_FIELD_UNSUPPORTED_MATCH FlowFieldFlag = 1 << iota
+	FLOW_FIELD_UNSUPPORTED_INSTRUCTION
+	FLOW_FIELD_UNSUPPORTED_ACTION
+	FLOW_FIELD_UNSUPPORTED_SET_FIELD
+	FLOW_FIELD_ID
+	FLOW_FIELD_TABLE_ID
+	FLOW_FIELD_DURATION_SEC
+	FLOW_FIELD_DURATION_NSEC
+	FLOW_FIELD_IDLE_TIMEOUT
+	FLOW_FIELD_HARD_TIMEOUT
+	FLOW_FIELD_PACKET_COUNT
+	FLOW_FIELD_BYTE_COUNT
+	FLOW_FIELD_PRIORITY
+	FLOW_FIELD_COOKIE
+	FLOW_FIELD_IN_PORT
+	FLOW_FIELD_ETH_TYPE
+	FLOW_FIELD_VLAN_ID
+	FLOW_FIELD_IP_PROTO
+	FLOW_FIELD_UDP_SRC
+	FLOW_FIELD_UDP_DST
+	FLOW_FIELD_METADATA
+	FLOW_FIELD_SET_VLAN_ID
+	FLOW_FIELD_POP_VLAN
+	FLOW_FIELD_PUSH_VLAN_ID
+	FLOW_FIELD_OUTPUT
+	FLOW_FIELD_GOTO_TABLE
+	FLOW_FIELD_CLEAR_ACTIONS
+	FLOW_FIELD_TUNNEL_ID
+	FLOW_FIELD_VLAN_PCP
+
+	FLOW_FIELD_HEADER = FLOW_FIELD_ID | FLOW_FIELD_TABLE_ID |
+		FLOW_FIELD_PRIORITY | FLOW_FIELD_COOKIE
+
+	FLOW_FIELD_STATS = FLOW_FIELD_DURATION_SEC | FLOW_FIELD_DURATION_NSEC |
+		FLOW_FIELD_IDLE_TIMEOUT | FLOW_FIELD_HARD_TIMEOUT |
+		FLOW_FIELD_PACKET_COUNT | FLOW_FIELD_BYTE_COUNT
+)
+
+var (
+	// Provide an array of all flags that can be used for iteration
+	AllFlowFieldFlags = []FlowFieldFlag{
+		FLOW_FIELD_UNSUPPORTED_MATCH,
+		FLOW_FIELD_UNSUPPORTED_INSTRUCTION,
+		FLOW_FIELD_UNSUPPORTED_ACTION,
+		FLOW_FIELD_UNSUPPORTED_SET_FIELD,
+		FLOW_FIELD_ID,
+		FLOW_FIELD_TABLE_ID,
+		FLOW_FIELD_DURATION_SEC,
+		FLOW_FIELD_DURATION_NSEC,
+		FLOW_FIELD_IDLE_TIMEOUT,
+		FLOW_FIELD_HARD_TIMEOUT,
+		FLOW_FIELD_PACKET_COUNT,
+		FLOW_FIELD_BYTE_COUNT,
+		FLOW_FIELD_PRIORITY,
+		FLOW_FIELD_COOKIE,
+		FLOW_FIELD_IN_PORT,
+		FLOW_FIELD_ETH_TYPE,
+		FLOW_FIELD_VLAN_ID,
+		FLOW_FIELD_IP_PROTO,
+		FLOW_FIELD_UDP_SRC,
+		FLOW_FIELD_UDP_DST,
+		FLOW_FIELD_METADATA,
+		FLOW_FIELD_SET_VLAN_ID,
+		FLOW_FIELD_POP_VLAN,
+		FLOW_FIELD_PUSH_VLAN_ID,
+		FLOW_FIELD_OUTPUT,
+		FLOW_FIELD_GOTO_TABLE,
+		FLOW_FIELD_CLEAR_ACTIONS,
+		FLOW_FIELD_TUNNEL_ID,
+		FLOW_FIELD_VLAN_PCP,
+	}
+)
+
+func (f *FlowFieldFlag) Count() int {
+	var count int
+	var bit uint64 = 1
+	var asUint64 = uint64(*f)
+	for i := 0; i < 64; i += 1 {
+		if asUint64&bit > 0 {
+			count += 1
+		}
+		bit <<= 1
+	}
+	return count
+}
+func (f *FlowFieldFlag) IsSet(flag FlowFieldFlag) bool {
+	return *f&flag > 0
+}
+
+func (f *FlowFieldFlag) Set(flag FlowFieldFlag) {
+	*f |= flag
+}
+
+func (f *FlowFieldFlag) Clear(flag FlowFieldFlag) {
+	var mask = ^(flag)
+	*f &= mask
+}
+
+func (f *FlowFieldFlag) Reset() {
+	*f = 0
+}
+
+func (f FlowFieldFlag) String() string {
+	switch f {
+	case FLOW_FIELD_UNSUPPORTED_MATCH:
+		return "UnsupportedMatch"
+	case FLOW_FIELD_UNSUPPORTED_INSTRUCTION:
+		return "UnsupportedInstruction"
+	case FLOW_FIELD_UNSUPPORTED_ACTION:
+		return "UnsupportedAction"
+	case FLOW_FIELD_UNSUPPORTED_SET_FIELD:
+		return "UnsupportedSetField"
+	case FLOW_FIELD_ID:
+		return "Id"
+	case FLOW_FIELD_TABLE_ID:
+		return "TableId"
+	case FLOW_FIELD_DURATION_SEC:
+		return "DurationSec"
+	case FLOW_FIELD_DURATION_NSEC:
+		return "DurationNsec"
+	case FLOW_FIELD_IDLE_TIMEOUT:
+		return "IdleTimeout"
+	case FLOW_FIELD_HARD_TIMEOUT:
+		return "HardTimeout"
+	case FLOW_FIELD_PACKET_COUNT:
+		return "PacketCount"
+	case FLOW_FIELD_BYTE_COUNT:
+		return "ByteCount"
+	case FLOW_FIELD_PRIORITY:
+		return "Priority"
+	case FLOW_FIELD_COOKIE:
+		return "Cookie"
+	case FLOW_FIELD_IN_PORT:
+		return "InPort"
+	case FLOW_FIELD_ETH_TYPE:
+		return "EthType"
+	case FLOW_FIELD_VLAN_ID:
+		return "VlanId"
+	case FLOW_FIELD_IP_PROTO:
+		return "IpProto"
+	case FLOW_FIELD_UDP_SRC:
+		return "UdpSrc"
+	case FLOW_FIELD_UDP_DST:
+		return "UdpDst"
+	case FLOW_FIELD_METADATA:
+		return "Metadata"
+	case FLOW_FIELD_SET_VLAN_ID:
+		return "SetVlanId"
+	case FLOW_FIELD_POP_VLAN:
+		return "PopVlan"
+	case FLOW_FIELD_PUSH_VLAN_ID:
+		return "PushVlanId"
+	case FLOW_FIELD_OUTPUT:
+		return "Output"
+	case FLOW_FIELD_GOTO_TABLE:
+		return "GotoTable"
+	case FLOW_FIELD_CLEAR_ACTIONS:
+		return "ClearActions"
+	case FLOW_FIELD_TUNNEL_ID:
+		return "TunnelId"
+	case FLOW_FIELD_VLAN_PCP:
+		return "VlanPcp"
+	default:
+		return "UnknownFieldFlag"
+	}
+}
+
+/*
+ * This is a partial list of OF match/action values. This list will be
+ * expanded as new fields are needed within VOLTHA
+ *
+ * Strings are used in the output structure so that on output the table
+ * can be "sparsely" populated with "empty" cells as opposed to 0 (zeros)
+ * all over the place.
+ */
+type Flow struct {
+	Id                     string `json:"id"`
+	TableId                uint32 `json:"tableid"`
+	DurationSec            uint32 `json:"durationsec"`
+	DurationNsec           uint32 `json:"durationnsec"`
+	IdleTimeout            uint32 `json:"idletimeout"`
+	HardTimeout            uint32 `json:"hardtimeout"`
+	PacketCount            uint64 `json:"packetcount"`
+	ByteCount              uint64 `json:"bytecount"`
+	Priority               uint32 `json:"priority"`
+	Cookie                 string `json:"cookie"`
+	UnsupportedMatch       string `json:"unsupportedmatch,omitempty"`
+	InPort                 string `json:"inport,omitempty"`
+	EthType                string `json:"ethtype,omitempty"`
+	VlanId                 string `json:"vlanid,omitempty"`
+	IpProto                string `json:"ipproto,omitempty"`
+	UdpSrc                 string `json:"udpsrc,omitempty"`
+	UdpDst                 string `json:"dstsrc,omitempty"`
+	Metadata               string `json:"metadata,omitempty"`
+	UnsupportedInstruction string `json:"unsupportedinstruction,omitempty"`
+	UnsupportedAction      string `json:"unsupportedaction,omitempty"`
+	UnsupportedSetField    string `json:"unsupportedsetfield,omitempty"`
+	SetVlanId              string `json:"setvlanid,omitempty"`
+	PopVlan                string `json:"popvlan,omitempty"`
+	PushVlanId             string `json:"pushvlanid,omitempty"`
+	Output                 string `json:"output,omitempty"`
+	GotoTable              string `json:"gototable,omitempty"`
+	ClearActions           string `json:"clear,omitempty"`
+	TunnelId               string `json:"tunnelid,omitempty"`
+	VlanPcp                string `json:"vlanpcp,omitempty"`
+
+	populated FlowFieldFlag
+}
+
+func (f *Flow) Count() int {
+	return f.populated.Count()
+}
+
+func (f *Flow) IsSet(flag FlowFieldFlag) bool {
+	return f.populated.IsSet(flag)
+}
+
+func (f *Flow) Set(flag FlowFieldFlag) {
+	f.populated.Set(flag)
+}
+
+func (f *Flow) Clear(flag FlowFieldFlag) {
+	f.populated.Clear(flag)
+}
+
+func (f *Flow) Reset() {
+	f.populated.Reset()
+}
+
+func (f *Flow) Populated() FlowFieldFlag {
+	return f.populated
+}
+
+func toVlanId(vid uint32) string {
+	if vid == 0 {
+		return "untagged"
+	} else if vid&0x1000 > 0 {
+		return fmt.Sprintf("%d", vid-4096)
+	}
+	return fmt.Sprintf("%d", vid)
+}
+
+func appendInt32(base string, val int32) string {
+	if len(base) > 0 {
+		return fmt.Sprintf("%s,%d", base, val)
+	}
+	return fmt.Sprintf("%d", val)
+}
+
+func appendUint32(base string, val uint32) string {
+	if len(base) > 0 {
+		return fmt.Sprintf("%s,%d", base, val)
+	}
+	return fmt.Sprintf("%d", val)
+}
+
+func (f *Flow) PopulateFrom(val *dynamic.Message) {
+
+	f.Reset()
+	f.Id = fmt.Sprintf("%016x", val.GetFieldByName("id").(uint64))
+	f.TableId = val.GetFieldByName("table_id").(uint32)
+	f.Priority = val.GetFieldByName("priority").(uint32)
+	// mask the lower 8 for the cookie, why?
+	cookie := val.GetFieldByName("cookie").(uint64)
+	if cookie == 0 {
+		f.Cookie = "0"
+	} else {
+		f.Cookie = fmt.Sprintf("~%08x", val.GetFieldByName("cookie").(uint64)&0xffffffff)
+	}
+	f.DurationSec = val.GetFieldByName("duration_sec").(uint32)
+	f.DurationNsec = val.GetFieldByName("duration_nsec").(uint32)
+	f.IdleTimeout = val.GetFieldByName("idle_timeout").(uint32)
+	f.HardTimeout = val.GetFieldByName("hard_timeout").(uint32)
+	f.PacketCount = val.GetFieldByName("packet_count").(uint64)
+	f.ByteCount = val.GetFieldByName("byte_count").(uint64)
+	f.Set(FLOW_FIELD_HEADER | FLOW_FIELD_STATS)
+
+	match := val.GetFieldByName("match").(*dynamic.Message)
+	fields := match.GetFieldByName("oxm_fields")
+	for _, ifield := range fields.([]interface{}) {
+		field := ifield.(*dynamic.Message)
+
+		// Only support OFPXMC_OPENFLOW_BASIC (0x8000)
+		if field.GetFieldByName("oxm_class").(int32) != 0x8000 {
+			continue
+		}
+
+		basic := field.GetFieldByName("ofb_field").(*dynamic.Message)
+		switch basic.GetFieldByName("type").(int32) {
+		case 0: // IN_PORT
+			f.Set(FLOW_FIELD_IN_PORT)
+			f.InPort = fmt.Sprintf("%d", basic.GetFieldByName("port").(uint32))
+		case 2: // METADATA
+			f.Set(FLOW_FIELD_METADATA)
+			f.Metadata = fmt.Sprintf("0x%016x", basic.GetFieldByName("table_metadata").(uint64))
+		case 5: // ETH_TYPE
+			f.Set(FLOW_FIELD_ETH_TYPE)
+			f.EthType = fmt.Sprintf("0x%04x", basic.GetFieldByName("eth_type").(uint32))
+		case 6: // VLAN_ID
+			f.Set(FLOW_FIELD_VLAN_ID)
+			f.VlanId = toVlanId(basic.GetFieldByName("vlan_vid").(uint32))
+		case 7: // VLAN_PCP
+			f.Set(FLOW_FIELD_VLAN_PCP)
+			f.VlanPcp = fmt.Sprintf("%d", basic.GetFieldByName("vlan_pcp").(uint32))
+		case 10: // IP_PROTO
+			f.Set(FLOW_FIELD_IP_PROTO)
+			f.IpProto = fmt.Sprintf("%d", basic.GetFieldByName("ip_proto").(uint32))
+		case 15: // UDP_SRC
+			f.Set(FLOW_FIELD_UDP_SRC)
+			f.UdpSrc = fmt.Sprintf("%d", basic.GetFieldByName("udp_src").(uint32))
+		case 16: // UDP_DST
+			f.Set(FLOW_FIELD_UDP_DST)
+			f.UdpDst = fmt.Sprintf("%d", basic.GetFieldByName("udp_dst").(uint32))
+		case 38: // TUNNEL_ID
+			f.Set(FLOW_FIELD_TUNNEL_ID)
+			f.TunnelId = fmt.Sprintf("%d", basic.GetFieldByName("tunnel_id").(uint64))
+		default:
+			/*
+			 * For unsupported match types put them into an
+			 * "Unsupported field so the table/json still
+			 * outputs relatively correctly as opposed to
+			 * having log messages.
+			 */
+			f.Set(FLOW_FIELD_UNSUPPORTED_MATCH)
+			f.UnsupportedMatch = appendInt32(f.UnsupportedMatch, basic.GetFieldByName("type").(int32))
+		}
+	}
+	for _, instruction := range val.GetFieldByName("instructions").([]interface{}) {
+		inst := instruction.(*dynamic.Message)
+		switch inst.GetFieldByName("type").(uint32) {
+		case 1: // GOTO_TABLE
+			f.Set(FLOW_FIELD_GOTO_TABLE)
+			goto_table := inst.GetFieldByName("goto_table").(*dynamic.Message)
+			f.GotoTable = fmt.Sprintf("%d", goto_table.GetFieldByName("table_id").(uint32))
+		case 4: // APPLY_ACTIONS
+			actions := inst.GetFieldByName("actions").(*dynamic.Message)
+			for _, action := range actions.GetFieldByName("actions").([]interface{}) {
+				a := action.(*dynamic.Message)
+				switch a.GetFieldByName("type").(int32) {
+				case 0: // OUTPUT
+					f.Set(FLOW_FIELD_OUTPUT)
+					output := a.GetFieldByName("output").(*dynamic.Message)
+					out := output.GetFieldByName("port").(uint32)
+					switch out & 0x7fffffff {
+					case 0:
+						f.Output = "INVALID"
+					case 0x7ffffff8:
+						f.Output = "IN_PORT"
+					case 0x7ffffff9:
+						f.Output = "TABLE"
+					case 0x7ffffffa:
+						f.Output = "NORMAL"
+					case 0x7ffffffb:
+						f.Output = "FLOOD"
+					case 0x7ffffffc:
+						f.Output = "ALL"
+					case 0x7ffffffd:
+						f.Output = "CONTROLLER"
+					case 0x7ffffffe:
+						f.Output = "LOCAL"
+					case 0x7fffffff:
+						f.Output = "ANY"
+					default:
+						f.Output = fmt.Sprintf("%d", output.GetFieldByName("port").(uint32))
+					}
+				case 17: // PUSH_VLAN
+					f.Set(FLOW_FIELD_PUSH_VLAN_ID)
+					push := a.GetFieldByName("push").(*dynamic.Message)
+					f.PushVlanId = fmt.Sprintf("0x%x", push.GetFieldByName("ethertype").(uint32))
+				case 18: // POP_VLAN
+					f.Set(FLOW_FIELD_POP_VLAN)
+					f.PopVlan = "yes"
+				case 25: // SET_FIELD
+					set := a.GetFieldByName("set_field").(*dynamic.Message).GetFieldByName("field").(*dynamic.Message)
+
+					// Only support OFPXMC_OPENFLOW_BASIC (0x8000)
+					if set.GetFieldByName("oxm_class").(int32) != 0x8000 {
+						continue
+					}
+					basic := set.GetFieldByName("ofb_field").(*dynamic.Message)
+
+					switch basic.GetFieldByName("type").(int32) {
+					case 6: // VLAN_ID
+						f.Set(FLOW_FIELD_SET_VLAN_ID)
+						f.SetVlanId = toVlanId(basic.GetFieldByName("vlan_vid").(uint32))
+					default: // Unsupported
+						/*
+						 * For unsupported match types put them into an
+						 * "Unsupported field so the table/json still
+						 * outputs relatively correctly as opposed to
+						 * having log messages.
+						 */
+						f.Set(FLOW_FIELD_UNSUPPORTED_SET_FIELD)
+						f.UnsupportedSetField = appendInt32(f.UnsupportedSetField,
+							basic.GetFieldByName("type").(int32))
+					}
+				default: // Unsupported
+					/*
+					 * For unsupported match types put them into an
+					 * "Unsupported field so the table/json still
+					 * outputs relatively correctly as opposed to
+					 * having log messages.
+					 */
+					f.Set(FLOW_FIELD_UNSUPPORTED_ACTION)
+					f.UnsupportedAction = appendInt32(f.UnsupportedAction,
+						a.GetFieldByName("type").(int32))
+				}
+			}
+		case 5: // CLEAR_ACTIONS
+			// Following current CLI, just assigning empty list
+			f.Set(FLOW_FIELD_CLEAR_ACTIONS)
+			f.ClearActions = "[]"
+		default: // Unsupported
+			/*
+			 * For unsupported match types put them into an
+			 * "Unsupported field so the table/json still
+			 * outputs relatively correctly as opposed to
+			 * having log messages.
+			 */
+			f.Set(FLOW_FIELD_UNSUPPORTED_INSTRUCTION)
+			f.UnsupportedInstruction = appendUint32(f.UnsupportedInstruction,
+				inst.GetFieldByName("type").(uint32))
+		}
+	}
+}
diff --git a/pkg/model/logicaldevice.go b/pkg/model/logicaldevice.go
new file mode 100644
index 0000000..6afa40f
--- /dev/null
+++ b/pkg/model/logicaldevice.go
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"fmt"
+	"github.com/jhump/protoreflect/dynamic"
+	"strings"
+)
+
+type LogicalDevice struct {
+	Id           string `json:"id"`
+	DatapathId   string `json:"datapathid"`
+	RootDeviceId string `json:"rootdeviceid"`
+	SerialNumber string `json:"serialnumber"`
+	Features     struct {
+		NBuffers     uint32 `json:"nbuffers"`
+		NTables      uint32 `json:"ntables"`
+		Capabilities string `json:"capabilities"`
+	} `json:"features"`
+	Ports []LogicalPort `json:"ports"`
+	Flows []Flow        `json:"flows"`
+}
+
+type LogicalPort struct {
+	Id           string `json:"id"`
+	DeviceId     string `json:"deviceid"`
+	DevicePortNo uint32 `json:"deviceportno"`
+	RootPort     bool   `json:"rootport"`
+	Openflow     struct {
+		PortNo   uint32 `json:"portno"`
+		HwAddr   string `json:"hwaddr"`
+		Name     string `json:"name"`
+		Config   string `json:"config"`
+		State    string `json:"state"`
+		Features struct {
+			Advertised string `json:"advertised"`
+			Current    string `json:"current"`
+			Supported  string `json:"supported"`
+			Peer       string `json:"peer"`
+		} `json:"features"`
+		Bitrate struct {
+			Current uint32 `json:"current"`
+			Max     uint32 `json:"max"`
+		}
+	} `json:"openflow"`
+}
+
+func (device *LogicalDevice) PopulateFrom(val *dynamic.Message) {
+	device.Id = val.GetFieldByName("id").(string)
+	device.DatapathId = fmt.Sprintf("%016x", val.GetFieldByName("datapath_id").(uint64))
+	device.RootDeviceId = val.GetFieldByName("root_device_id").(string)
+	desc := val.GetFieldByName("desc").(*dynamic.Message)
+	device.SerialNumber = desc.GetFieldByName("serial_num").(string)
+	features := val.GetFieldByName("switch_features").(*dynamic.Message)
+	device.Features.NBuffers = features.GetFieldByName("n_buffers").(uint32)
+	device.Features.NTables = features.GetFieldByName("n_tables").(uint32)
+	device.Features.Capabilities = fmt.Sprintf("0x%08x", features.GetFieldByName("capabilities").(uint32))
+
+	ports := val.GetFieldByName("ports").([]interface{})
+	device.Ports = make([]LogicalPort, len(ports))
+	for i, port := range ports {
+		device.Ports[i].PopulateFrom(port.(*dynamic.Message))
+	}
+
+	flows := val.GetFieldByName("flows").(*dynamic.Message)
+	if flows == nil {
+		device.Flows = make([]Flow, 0)
+	} else {
+		items := flows.GetFieldByName("items").([]interface{})
+		device.Flows = make([]Flow, len(items))
+		for i, flow := range items {
+			device.Flows[i].PopulateFrom(flow.(*dynamic.Message))
+		}
+	}
+}
+
+func (port *LogicalPort) PopulateFrom(val *dynamic.Message) {
+	port.Id = val.GetFieldByName("id").(string)
+	port.DeviceId = val.GetFieldByName("device_id").(string)
+	port.DevicePortNo = val.GetFieldByName("device_port_no").(uint32)
+	port.RootPort = val.GetFieldByName("root_port").(bool)
+	ofp := val.GetFieldByName("ofp_port").(*dynamic.Message)
+	hw := strings.Builder{}
+	first := true
+	for _, b := range ofp.GetFieldByName("hw_addr").([]interface{}) {
+		if !first {
+			hw.WriteString(":")
+		}
+		first = false
+		hw.WriteString(fmt.Sprintf("%02x", b))
+	}
+	port.Openflow.HwAddr = hw.String()
+	port.Openflow.PortNo = ofp.GetFieldByName("port_no").(uint32)
+	port.Openflow.Name = ofp.GetFieldByName("name").(string)
+	port.Openflow.Config = fmt.Sprintf("0x%08x", ofp.GetFieldByName("config").(uint32))
+	port.Openflow.State = fmt.Sprintf("0x%08x", ofp.GetFieldByName("state").(uint32))
+	port.Openflow.Features.Current = fmt.Sprintf("0x%08x", ofp.GetFieldByName("curr").(uint32))
+	port.Openflow.Features.Advertised = fmt.Sprintf("0x%08x", ofp.GetFieldByName("advertised").(uint32))
+	port.Openflow.Features.Supported = fmt.Sprintf("0x%08x", ofp.GetFieldByName("supported").(uint32))
+	port.Openflow.Features.Peer = fmt.Sprintf("0x%08x", ofp.GetFieldByName("peer").(uint32))
+	port.Openflow.Bitrate.Current = ofp.GetFieldByName("curr_speed").(uint32)
+	port.Openflow.Bitrate.Max = ofp.GetFieldByName("max_speed").(uint32)
+}
diff --git a/pkg/model/utils.go b/pkg/model/utils.go
new file mode 100644
index 0000000..b885ae0
--- /dev/null
+++ b/pkg/model/utils.go
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package model
+
+import (
+	"github.com/jhump/protoreflect/dynamic"
+)
+
+func GetEnumValue(val *dynamic.Message, name string) string {
+	return val.FindFieldDescriptorByName(name).GetEnumType().
+		FindValueByNumber(val.GetFieldByName(name).(int32)).GetName()
+}
diff --git a/pkg/order/order.go b/pkg/order/order.go
new file mode 100644
index 0000000..715b302
--- /dev/null
+++ b/pkg/order/order.go
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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.
+ */
+package order
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+	"strings"
+)
+
+type Operation int
+
+const (
+	ASC Operation = iota
+	DSC
+)
+
+type SortTerm struct {
+	Op   Operation
+	Name string
+}
+
+func (o Operation) String() string {
+	switch o {
+	default:
+		fallthrough
+	case ASC:
+		return "ASC"
+	case DSC:
+		return "DSC"
+	}
+}
+
+type Sorter []SortTerm
+
+func split(term string) SortTerm {
+	st := SortTerm{}
+	if len(term) > 0 {
+		switch term[0] {
+		case '+':
+			fallthrough
+		case '>':
+			st.Op = ASC
+			st.Name = term[1:]
+		case '-':
+			fallthrough
+		case '<':
+			st.Op = DSC
+			st.Name = term[1:]
+		default:
+			st.Op = ASC
+			st.Name = term
+		}
+	} else {
+		st.Op = ASC
+		st.Name = term
+	}
+	return st
+}
+
+// Parse parses a comma separated list of filter terms
+func Parse(spec string) (Sorter, error) {
+	terms := strings.Split(spec, ",")
+	s := make([]SortTerm, 0)
+	for _, term := range terms {
+		s = append(s, split(term))
+	}
+
+	return s, nil
+}
+
+func (s Sorter) Process(data interface{}) (interface{}, error) {
+	slice := reflect.ValueOf(data)
+	if slice.Kind() != reflect.Slice {
+		return data, nil
+	}
+
+	sort.SliceStable(data, func(i, j int) bool {
+		left := reflect.ValueOf(slice.Index(i).Interface())
+		right := reflect.ValueOf(slice.Index(j).Interface())
+		for _, term := range s {
+			fleft := left.FieldByName(term.Name)
+			fright := right.FieldByName(term.Name)
+			switch fleft.Kind() {
+			case reflect.Uint:
+				fallthrough
+			case reflect.Uint8:
+				fallthrough
+			case reflect.Uint16:
+				fallthrough
+			case reflect.Uint32:
+				fallthrough
+			case reflect.Uint64:
+				ileft := fleft.Uint()
+				iright := fright.Uint()
+				switch term.Op {
+				case ASC:
+					if ileft < iright {
+						return true
+					} else if ileft > iright {
+						return false
+					}
+				case DSC:
+					if ileft > iright {
+						return true
+					} else if ileft < iright {
+						return false
+					}
+				}
+			case reflect.Int:
+				fallthrough
+			case reflect.Int8:
+				fallthrough
+			case reflect.Int16:
+				fallthrough
+			case reflect.Int32:
+				fallthrough
+			case reflect.Int64:
+				ileft := fleft.Int()
+				iright := fright.Int()
+				switch term.Op {
+				case ASC:
+					if ileft < iright {
+						return true
+					} else if ileft > iright {
+						return false
+					}
+				case DSC:
+					if ileft > iright {
+						return true
+					} else if ileft < iright {
+						return false
+					}
+				}
+			default:
+				sleft := fmt.Sprintf("%v", left.FieldByName(term.Name))
+				sright := fmt.Sprintf("%v", right.FieldByName(term.Name))
+				diff := strings.Compare(sleft, sright)
+				if term.Op != DSC {
+					if diff == -1 {
+						return true
+					} else if diff == 1 {
+						return false
+					}
+				} else {
+					if diff == 1 {
+						return true
+					} else if diff == -1 {
+						return false
+					}
+				}
+			}
+		}
+		return false
+	})
+
+	return data, nil
+}
diff --git a/pkg/order/order_test.go b/pkg/order/order_test.go
new file mode 100644
index 0000000..484d0a6
--- /dev/null
+++ b/pkg/order/order_test.go
@@ -0,0 +1,231 @@
+package order
+
+import (
+	"testing"
+)
+
+type SortTestStruct struct {
+	Id    int
+	One   string
+	Two   string
+	Three uint
+	Four  int
+}
+
+var testSetOne = []SortTestStruct{
+	{
+		Id:    0,
+		One:   "a",
+		Two:   "x",
+		Three: 10,
+		Four:  1,
+	},
+	{
+		Id:    1,
+		One:   "a",
+		Two:   "c",
+		Three: 1,
+		Four:  10,
+	},
+	{
+		Id:    2,
+		One:   "a",
+		Two:   "b",
+		Three: 2,
+		Four:  1000,
+	},
+	{
+		Id:    3,
+		One:   "a",
+		Two:   "a",
+		Three: 3,
+		Four:  100,
+	},
+	{
+		Id:    4,
+		One:   "b",
+		Two:   "a",
+		Three: 3,
+		Four:  0,
+	},
+}
+
+var testSetTwo = []SortTestStruct{
+	{
+		Id:    0,
+		One:   "a",
+		Two:   "x",
+		Three: 10,
+		Four:  10,
+	},
+	{
+		Id:    1,
+		One:   "a",
+		Two:   "y",
+		Three: 1,
+		Four:  1,
+	},
+}
+
+func Verify(v []SortTestStruct, order []int) bool {
+	for i, item := range v {
+		if item.Id != order[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func TestSort(t *testing.T) {
+	s, err := Parse("+One,-Two")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetOne)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	if !Verify(o.([]SortTestStruct), []int{0, 1, 2, 3, 4}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortASC(t *testing.T) {
+	s, err := Parse("+One,Two")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetTwo)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	if !Verify(o.([]SortTestStruct), []int{0, 1}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortUintASC(t *testing.T) {
+	s, err := Parse("Three,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetOne)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	if !Verify(o.([]SortTestStruct), []int{1, 2, 3, 4, 0}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortUintDSC(t *testing.T) {
+	s, err := Parse("-Three,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetOne)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	if !Verify(o.([]SortTestStruct), []int{0, 3, 4, 2, 1}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortUintDSC2(t *testing.T) {
+	s, err := Parse("-Three,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetTwo)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	if !Verify(o.([]SortTestStruct), []int{0, 1}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortIntASC(t *testing.T) {
+	s, err := Parse("Four,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetOne)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+	if !Verify(o.([]SortTestStruct), []int{4, 0, 1, 3, 2}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortIntDSC(t *testing.T) {
+	s, err := Parse("-Four,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetOne)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+	if !Verify(o.([]SortTestStruct), []int{2, 3, 1, 0, 4}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestSortIntDSC2(t *testing.T) {
+	s, err := Parse("-Four,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetTwo)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+	if !Verify(o.([]SortTestStruct), []int{0, 1}) {
+		t.Errorf("incorrect sort")
+	}
+}
+
+func TestOperString(t *testing.T) {
+	if ASC.String() != "ASC" {
+		t.Errorf("ASC to string failed")
+	}
+	if DSC.String() != "DSC" {
+		t.Errorf("DSC to string failed")
+	}
+	var o Operation = 5 // Invalid
+	if o.String() != "ASC" {
+		t.Errorf("to string default failed")
+	}
+}
+
+func TestSortSingle(t *testing.T) {
+	s, err := Parse("-Four,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	o, err := s.Process(testSetOne[0])
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	if o == nil {
+		t.Errorf("expected value, got nil")
+	}
+
+	r, ok := o.(SortTestStruct)
+	if !ok {
+		t.Errorf("Unexpected result type")
+	}
+
+	if r.Id != testSetOne[0].Id {
+		t.Errorf("results don't match input")
+	}
+}