SEBA-767 Directory restructuring in accordance with best practices

Change-Id: Id651366a3545ad0141a7854e99fa46867e543295
diff --git a/internal/pkg/cli/version/version.go b/internal/pkg/cli/version/version.go
new file mode 100644
index 0000000..21880bb
--- /dev/null
+++ b/internal/pkg/cli/version/version.go
@@ -0,0 +1,30 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 version
+
+// Default build-time variable.
+// These values can (should) be overridden via ldflags when built with
+// `make`
+var (
+	Version   = "unknown-version"
+	GoVersion = "unknown-goversion"
+	GitCommit = "unknown-gitcommit"
+	GitDirty  = "unknown-gitdirty"
+	BuildTime = "unknown-buildtime"
+	Os        = "unknown-os"
+	Arch      = "unknown-arch"
+)
diff --git a/internal/pkg/commands/backup.go b/internal/pkg/commands/backup.go
new file mode 100644
index 0000000..84df5a9
--- /dev/null
+++ b/internal/pkg/commands/backup.go
@@ -0,0 +1,255 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	flags "github.com/jessevdk/go-flags"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"time"
+)
+
+const (
+	DEFAULT_BACKUP_FORMAT = "table{{ .Status }}\t{{ .Checksum }}\t{{ .Chunks }}\t{{ .Bytes }}"
+)
+
+type BackupOutput struct {
+	Status   string `json:"status"`
+	Checksum string `json:"checksum"`
+	Chunks   int    `json:"chunks"`
+	Bytes    int    `json:"bytes"`
+}
+
+type BackupCreate struct {
+	OutputOptions
+	ChunkSize int `short:"h" long:"chunksize" default:"65536" description:"Chunk size for streaming transfer"`
+	Args      struct {
+		LocalFileName string
+	} `positional-args:"yes" required:"yes"`
+}
+
+type BackupRestore struct {
+	OutputOptions
+	ChunkSize int `short:"h" long:"chunksize" default:"65536" description:"Chunk size for streaming transfer"`
+	Args      struct {
+		LocalFileName string
+	} `positional-args:"yes" required:"yes"`
+	CreateURIFunc func() (string, string) // allow override of CreateURIFunc for easy unit testing
+}
+
+type BackupOpts struct {
+	Create  BackupCreate  `command:"create"`
+	Restore BackupRestore `command:"restore"`
+}
+
+var backupOpts = BackupOpts{}
+
+func RegisterBackupCommands(parser *flags.Parser) {
+	parser.AddCommand("backup", "backup management commands", "Commands to create backups and restore backups", &backupOpts)
+}
+
+func (options *BackupCreate) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	ctx := context.Background() // TODO: Implement a sync timeout
+
+	// We might close and reopen the connection befor we do the DownloadFile,
+	// so make sure we've downloaded the service descriptor.
+	_, err = descriptor.FindSymbol("xos.filetransfer")
+	if err != nil {
+		return err
+	}
+
+	local_name := options.Args.LocalFileName
+
+	// STEP 1: Create backup operation
+
+	backupop := make(map[string]interface{})
+	backupop["operation"] = "create"
+	err = CreateModel(conn, descriptor, "BackupOperation", backupop)
+	if err != nil {
+		return err
+	}
+	conditional_printf(!options.Quiet, "Created backup-create operation id=%d uuid=%s\n", backupop["id"], backupop["uuid"])
+	conditional_printf(!options.Quiet, "Waiting for sync ")
+
+	// STEP 2: Wait for the operation to complete
+
+	flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | Ternary_uint32(options.Quiet, GM_QUIET, 0)
+	conn, completed_backupop, err := GetModelWithRetry(ctx, conn, descriptor, "BackupOperation", backupop["id"].(int32), flags)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	status := completed_backupop.GetFieldByName("status").(string)
+	conditional_printf(!options.Quiet, "\nStatus: %s\n", status)
+
+	// we've failed. leave.
+	if status != "created" {
+		return corderrors.NewInternalError("BackupOp status is %s", status)
+	}
+
+	// STEP 3: Retrieve URI
+	backupfile_id := completed_backupop.GetFieldByName("file_id").(int32)
+	if backupfile_id == 0 {
+		return corderrors.NewInternalError("BackupOp.file_id is not set")
+	}
+
+	completed_backupfile, err := GetModel(ctx, conn, descriptor, "BackupFile", backupfile_id)
+	if err != nil {
+		return err
+	}
+
+	uri := completed_backupfile.GetFieldByName("uri").(string)
+	conditional_printf(!options.Quiet, "URI %s\n", uri)
+
+	// STEP 4: Download the file
+
+	conditional_printf(!options.Quiet, "Downloading %s\n", local_name)
+
+	h, err := DownloadFile(conn, descriptor, uri, local_name)
+	if err != nil {
+		return err
+	}
+
+	// STEP 5: Verify checksum
+
+	if completed_backupfile.GetFieldByName("checksum").(string) != h.GetChecksum() {
+		return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
+			Actual:   h.GetChecksum(),
+			Expected: completed_backupfile.GetFieldByName("checksum").(string)})
+	}
+
+	// STEP 6: Show results
+
+	data := make([]BackupOutput, 1)
+	data[0].Chunks = h.chunks
+	data[0].Bytes = h.bytes
+	data[0].Status = h.status
+	data[0].Checksum = h.GetChecksum()
+
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
+
+	return nil
+}
+
+// Create a file:/// URI to use for storing the file in the core
+func CreateDynamicURI() (string, string) {
+	remote_name := "cordctl-restore-" + time.Now().Format("20060102T150405Z")
+	uri := "file:///var/run/xos/backup/local/" + remote_name
+	return remote_name, uri
+}
+
+func (options *BackupRestore) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	ctx := context.Background() // TODO: Implement a sync timeout
+
+	local_name := options.Args.LocalFileName
+
+	var remote_name, uri string
+	if options.CreateURIFunc != nil {
+		remote_name, uri = options.CreateURIFunc()
+	} else {
+		remote_name, uri = CreateDynamicURI()
+	}
+
+	// STEP 1: Upload the file
+
+	h, upload_result, err := UploadFile(conn, descriptor, local_name, uri, options.ChunkSize)
+	if err != nil {
+		return err
+	}
+
+	upload_status := GetEnumValue(upload_result, "status")
+	if upload_status != "SUCCESS" {
+		return corderrors.NewInternalError("Upload status was %s", upload_status)
+	}
+
+	// STEP 2: Verify checksum
+
+	if upload_result.GetFieldByName("checksum").(string) != h.GetChecksum() {
+		return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
+			Expected: h.GetChecksum(),
+			Actual:   upload_result.GetFieldByName("checksum").(string)})
+	}
+
+	// STEP 2: Create a BackupFile object
+
+	backupfile := make(map[string]interface{})
+	backupfile["name"] = remote_name
+	backupfile["uri"] = uri
+	backupfile["checksum"] = h.GetChecksum()
+	err = CreateModel(conn, descriptor, "BackupFile", backupfile)
+	if err != nil {
+		return err
+	}
+	conditional_printf(!options.Quiet, "Created backup file %d\n", backupfile["id"])
+
+	// STEP 3: Create a BackupOperation object
+
+	backupop := make(map[string]interface{})
+	backupop["operation"] = "restore"
+	backupop["file_id"] = backupfile["id"]
+	err = CreateModel(conn, descriptor, "BackupOperation", backupop)
+	if err != nil {
+		return err
+	}
+	conditional_printf(!options.Quiet, "Created backup-restore operation id=%d uuid=%s\n", backupop["id"], backupop["uuid"])
+
+	conditional_printf(!options.Quiet, "Waiting for completion ")
+
+	// STEP 4: Wait for completion
+
+	flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | GM_UNTIL_STATUS | Ternary_uint32(options.Quiet, GM_QUIET, 0)
+	queries := map[string]string{"uuid": backupop["uuid"].(string)}
+	conn, completed_backupop, err := FindModelWithRetry(ctx, conn, descriptor, "BackupOperation", queries, flags)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	conditional_printf(!options.Quiet, "\n")
+
+	// STEP 5: Show results
+
+	data := make([]BackupOutput, 1)
+	data[0].Checksum = upload_result.GetFieldByName("checksum").(string)
+	data[0].Chunks = int(upload_result.GetFieldByName("chunks_received").(int32))
+	data[0].Bytes = int(upload_result.GetFieldByName("bytes_received").(int32))
+
+	if completed_backupop.GetFieldByName("status") == "restored" {
+		data[0].Status = "SUCCESS"
+	} else {
+		data[0].Status = "FAILURE"
+	}
+
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
+
+	return nil
+}
diff --git a/internal/pkg/commands/backup_test.go b/internal/pkg/commands/backup_test.go
new file mode 100644
index 0000000..4d78a11
--- /dev/null
+++ b/internal/pkg/commands/backup_test.go
@@ -0,0 +1,90 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"io/ioutil"
+	"testing"
+)
+
+func TestBackupCreate(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"bytes": 6,
+			"checksum": "sha256:e9c0f8b575cbfcb42ab3b78ecc87efa3b011d9a5d10b09fa4e96f240bf6a82f5",
+			"chunks": 2,
+			"status": "SUCCESS"
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options BackupOpts
+	options.Create.OutputAs = "json"
+	options.Create.Args.LocalFileName = "/tmp/transfer.down"
+	options.Create.ChunkSize = 3
+	err := options.Create.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+// Mock the CreateURI function in the Restore code to use file:///tmp/transfer.up
+func CreateURI() (string, string) {
+	remote_name := "transfer.up"
+	uri := "file:///tmp/" + remote_name
+	return remote_name, uri
+}
+
+func TestBackupRestore(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"bytes": 6,
+			"checksum": "sha256:e9c0f8b575cbfcb42ab3b78ecc87efa3b011d9a5d10b09fa4e96f240bf6a82f5",
+			"chunks": 2,
+			"status": "SUCCESS"
+		}
+	]`
+
+	err := ioutil.WriteFile("/tmp/transfer.up", []byte("ABCDEF"), 0644)
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options BackupRestore
+	options.OutputAs = "json"
+	options.Args.LocalFileName = "/tmp/transfer.up"
+	options.ChunkSize = 3
+	options.CreateURIFunc = CreateURI
+	err = options.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
diff --git a/internal/pkg/commands/command.go b/internal/pkg/commands/command.go
new file mode 100644
index 0000000..45ccd5d
--- /dev/null
+++ b/internal/pkg/commands/command.go
@@ -0,0 +1,277 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/opencord/cordctl/pkg/format"
+	"github.com/opencord/cordctl/pkg/order"
+	"google.golang.org/grpc"
+	"gopkg.in/yaml.v2"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+	"time"
+	"net"
+)
+
+type OutputType uint8
+
+const (
+	OUTPUT_TABLE OutputType = iota
+	OUTPUT_JSON
+	OUTPUT_YAML
+
+	CORE_VERSION_CONSTRAINT = ">= 3, < 4" // Support XOS major version 3
+)
+
+// Make it easy to override output stream for testing
+var OutputStream io.Writer = os.Stdout
+
+var CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
+
+type GrpcConfigSpec struct {
+	Timeout time.Duration `yaml:"timeout"`
+}
+
+type TlsConfigSpec struct {
+	UseTls bool   `yaml:"useTls"`
+	CACert string `yaml:"caCert"`
+	Cert   string `yaml:"cert"`
+	Key    string `yaml:"key"`
+	Verify string `yaml:"verify"`
+}
+
+type GlobalConfigSpec struct {
+	Server   string        `yaml:"server"`
+	Username string        `yaml:"username"`
+	Password string        `yaml:"password"`
+	Protoset string        `yaml:"protoset"`
+	Tls      TlsConfigSpec `yaml:"tls"`
+	Grpc     GrpcConfigSpec
+}
+
+var GlobalConfig = GlobalConfigSpec{
+	Server: "localhost",
+	Tls: TlsConfigSpec{
+		UseTls: false,
+	},
+	Grpc: GrpcConfigSpec{
+		Timeout: time.Second * 10,
+	},
+}
+
+var GlobalOptions struct {
+	Config   string `short:"c" long:"config" env:"CORDCONFIG" value-name:"FILE" default:"" description:"Location of client config file"`
+	Server   string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of XOS"`
+	Username string `short:"u" long:"username" value-name:"USERNAME" default:"" description:"Username to authenticate with XOS"`
+	Password string `short:"p" long:"password" value-name:"PASSWORD" default:"" description:"Password to authenticate with XOS"`
+	Protoset string `long:"protoset" value-name:"FILENAME" description:"Load protobuf definitions from protoset instead of reflection api"`
+	Debug    bool   `short:"d" long:"debug" description:"Enable debug mode"`
+	UseTLS   bool   `long:"tls" description:"Use TLS"`
+	CACert   string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
+	Cert     string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
+	Key      string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
+	Verify   bool   `long:"tlsverify" description:"Use TLS and verify the remote"`
+	Yes      bool   `short:"y" long:"yes" description:"answer yes to any confirmation prompts"`
+}
+
+type OutputOptions struct {
+	Format   string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
+	Quiet    bool   `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
+	OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
+}
+
+type ListOutputOptions struct {
+	OutputOptions
+	OrderBy string `short:"r" long:"orderby" default:"" description:"Specify the sort order of the results"`
+}
+
+func toOutputType(in string) OutputType {
+	switch in {
+	case "table":
+		fallthrough
+	default:
+		return OUTPUT_TABLE
+	case "json":
+		return OUTPUT_JSON
+	case "yaml":
+		return OUTPUT_YAML
+	}
+}
+
+type CommandResult struct {
+	Format   format.Format
+	OrderBy  string
+	OutputAs OutputType
+	Data     interface{}
+}
+
+type config struct {
+	Server string `yaml:"server"`
+}
+
+func ProcessGlobalOptions() {
+	if len(GlobalOptions.Config) == 0 {
+		home, err := os.UserHomeDir()
+		if err != nil {
+			log.Printf("Unable to discover the users home directory: %s\n", err)
+		}
+		GlobalOptions.Config = fmt.Sprintf("%s/.cord/config", home)
+	}
+
+	info, err := os.Stat(GlobalOptions.Config)
+	if err == nil && !info.IsDir() {
+		configFile, err := ioutil.ReadFile(GlobalOptions.Config)
+		if err != nil {
+			log.Printf("configFile.Get err   #%v ", err)
+		}
+		err = yaml.Unmarshal(configFile, &GlobalConfig)
+		if err != nil {
+			log.Fatalf("Unmarshal: %v", err)
+		}
+	}
+
+	// Override from environment
+	//    in particualr, for passing env vars via `go test`
+	env_server, present := os.LookupEnv("CORDCTL_SERVER")
+	if present {
+		GlobalConfig.Server = env_server
+	}
+	env_username, present := os.LookupEnv("CORDCTL_USERNAME")
+	if present {
+		GlobalConfig.Username = env_username
+	}
+	env_password, present := os.LookupEnv("CORDCTL_PASSWORD")
+	if present {
+		GlobalConfig.Password = env_password
+	}
+	env_protoset, present := os.LookupEnv("CORDCTL_PROTOSET")
+	if present {
+		GlobalConfig.Protoset = env_protoset
+	}
+
+	// Override from command line
+	if GlobalOptions.Server != "" {
+		GlobalConfig.Server = GlobalOptions.Server
+	}
+	if GlobalOptions.Username != "" {
+		GlobalConfig.Username = GlobalOptions.Username
+	}
+	if GlobalOptions.Password != "" {
+		GlobalConfig.Password = GlobalOptions.Password
+	}
+	if GlobalOptions.Protoset != "" {
+		GlobalConfig.Protoset = GlobalOptions.Protoset
+	}
+
+	// Generate error messages for required settings
+	if GlobalConfig.Server == "" {
+		log.Fatal("Server is not set. Please update config file or use the -s option")
+	}
+	if GlobalConfig.Username == "" {
+		log.Fatal("Username is not set. Please update config file or use the -u option")
+	}
+	if GlobalConfig.Password == "" {
+		log.Fatal("Password is not set. Please update config file or use the -p option")
+	}
+	//Try to resolve hostname if provided for the server
+	if host, port, err := net.SplitHostPort(GlobalConfig.Server); err == nil {
+		if addrs, err := net.LookupHost(host); err == nil {
+			GlobalConfig.Server = net.JoinHostPort(addrs[0], port)
+		}
+	}
+}
+
+func NewConnection() (*grpc.ClientConn, error) {
+	ProcessGlobalOptions()
+	return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure())
+}
+
+func GenerateOutput(result *CommandResult) {
+	if result != nil && result.Data != nil {
+		data := result.Data
+		if result.OrderBy != "" {
+			s, err := order.Parse(result.OrderBy)
+			if err != nil {
+				panic(err)
+			}
+			data, err = s.Process(data)
+			if err != nil {
+				panic(err)
+			}
+		}
+		if result.OutputAs == OUTPUT_TABLE {
+			tableFormat := format.Format(result.Format)
+			tableFormat.Execute(OutputStream, true, data)
+		} else if result.OutputAs == OUTPUT_JSON {
+			asJson, err := json.Marshal(&data)
+			if err != nil {
+				panic(err)
+			}
+			fmt.Fprintf(OutputStream, "%s", asJson)
+		} else if result.OutputAs == OUTPUT_YAML {
+			asYaml, err := yaml.Marshal(&data)
+			if err != nil {
+				panic(err)
+			}
+			fmt.Fprintf(OutputStream, "%s", asYaml)
+		}
+	}
+}
+
+// Applies common output options to format and generate output
+func FormatAndGenerateOutput(options *OutputOptions, default_format string, quiet_format string, data interface{}) {
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = default_format
+	}
+	if options.Quiet {
+		outputFormat = quiet_format
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+	}
+
+	GenerateOutput(&result)
+}
+
+// Applies common output options to format and generate output
+func FormatAndGenerateListOutput(options *ListOutputOptions, default_format string, quiet_format string, data interface{}) {
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = default_format
+	}
+	if options.Quiet {
+		outputFormat = quiet_format
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+		OrderBy:  options.OrderBy,
+	}
+
+	GenerateOutput(&result)
+}
diff --git a/internal/pkg/commands/common.go b/internal/pkg/commands/common.go
new file mode 100644
index 0000000..6f795e4
--- /dev/null
+++ b/internal/pkg/commands/common.go
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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 commands
+
+import (
+	"bufio"
+	b64 "encoding/base64"
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	versionUtils "github.com/hashicorp/go-version"
+	"github.com/jhump/protoreflect/dynamic"
+	"github.com/jhump/protoreflect/grpcreflect"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"golang.org/x/net/context"
+	"google.golang.org/grpc"
+	reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+	"log"
+	"os"
+	"strings"
+)
+
+// Flags for calling the InitReflectionClient Method
+const (
+	INIT_DEFAULT          = 0
+	INIT_NO_VERSION_CHECK = 1 // Do not check whether server is allowed version
+)
+
+func GenerateHeaders() []string {
+	username := GlobalConfig.Username
+	password := GlobalConfig.Password
+	sEnc := b64.StdEncoding.EncodeToString([]byte(username + ":" + password))
+	headers := []string{"authorization: basic " + sEnc}
+	return headers
+}
+
+// Perform the GetVersion API call on the core to get the version
+func GetVersion(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource) (*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.utility.GetVersion", headers, h, h.GetParams)
+	if err != nil {
+		return nil, corderrors.RpcErrorToCordError(err)
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, corderrors.RpcErrorToCordError(h.Status.Err())
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+
+	return d, err
+}
+
+// Initialize client connection
+//    flags is a set of optional flags that may influence how the connection is setup
+//        INIT_DEFAULT - default behavior (0)
+//        INIT_NO_VERSION_CHECK - do not perform core version check
+
+func InitClient(flags uint32) (*grpc.ClientConn, grpcurl.DescriptorSource, error) {
+	conn, err := NewConnection()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	refClient := grpcreflect.NewClient(context.Background(), reflectpb.NewServerReflectionClient(conn))
+	defer refClient.Reset()
+
+	// Intended method of use is to download the protos via reflection API. Loading the
+	// protos from a file is supported for unit testing, as the mock server does not
+	// support the reflection API.
+
+	var descriptor grpcurl.DescriptorSource
+	if GlobalConfig.Protoset != "" {
+		descriptor, err = grpcurl.DescriptorSourceFromProtoSets(GlobalConfig.Protoset)
+		if err != nil {
+			return nil, nil, err
+		}
+	} else {
+		descriptor = grpcurl.DescriptorSourceFromServer(context.Background(), refClient)
+	}
+
+	if flags&INIT_NO_VERSION_CHECK == 0 {
+		d, err := GetVersion(conn, descriptor)
+		if err != nil {
+			return nil, nil, err
+		}
+		// Note: NewVersion doesn't like the `-dev` suffix, so strip it off.
+		serverVersion, err := versionUtils.NewVersion(strings.Split(d.GetFieldByName("version").(string), "-")[0])
+		if err != nil {
+			return nil, nil, err
+		}
+
+		constraint, err := versionUtils.NewConstraint(CORE_VERSION_CONSTRAINT)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		if !constraint.Check(serverVersion) {
+			return nil, nil, corderrors.WithStackTrace(&corderrors.VersionConstraintError{
+				Name:       "xos-core",
+				Version:    serverVersion.String(),
+				Constraint: CORE_VERSION_CONSTRAINT})
+		}
+
+	}
+
+	return conn, descriptor, nil
+}
+
+// A makeshift substitute for C's Ternary operator
+func Ternary_uint32(condition bool, value_true uint32, value_false uint32) uint32 {
+	if condition {
+		return value_true
+	} else {
+		return value_false
+	}
+}
+
+// call printf only if visible is True
+func conditional_printf(visible bool, format string, args ...interface{}) {
+	if visible {
+		fmt.Printf(format, args...)
+	}
+}
+
+// Print a confirmation prompt and get a response from the user
+func Confirmf(format string, args ...interface{}) bool {
+	if GlobalOptions.Yes {
+		return true
+	}
+
+	reader := bufio.NewReader(os.Stdin)
+
+	for {
+		msg := fmt.Sprintf(format, args...)
+		fmt.Print(msg)
+
+		response, err := reader.ReadString('\n')
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		response = strings.ToLower(strings.TrimSpace(response))
+
+		if response == "y" || response == "yes" {
+			return true
+		} else if response == "n" || response == "no" {
+			return false
+		}
+	}
+}
diff --git a/internal/pkg/commands/completion.go b/internal/pkg/commands/completion.go
new file mode 100644
index 0000000..74e7cfa
--- /dev/null
+++ b/internal/pkg/commands/completion.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 commands
+
+import (
+	"fmt"
+	flags "github.com/jessevdk/go-flags"
+	"github.com/opencord/cordctl/internal/pkg/completion"
+)
+
+type BashOptions struct{}
+
+type CompletionOptions struct {
+	BashOptions `command:"bash"`
+}
+
+func RegisterCompletionCommands(parent *flags.Parser) {
+	parent.AddCommand("completion", "generate shell compleition", "Commands to generate shell completion information", &CompletionOptions{})
+}
+
+func (options *BashOptions) Execute(args []string) error {
+	fmt.Fprintln(OutputStream, completion.Bash)
+	return nil
+}
diff --git a/internal/pkg/commands/completion_test.go b/internal/pkg/commands/completion_test.go
new file mode 100644
index 0000000..b7b25c8
--- /dev/null
+++ b/internal/pkg/commands/completion_test.go
@@ -0,0 +1,39 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"github.com/opencord/cordctl/internal/pkg/completion"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"testing"
+)
+
+func TestCompletion(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options CompletionOptions
+	err := options.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertStringEqual(t, got.String(), completion.Bash+"\n")
+}
diff --git a/internal/pkg/commands/config.go b/internal/pkg/commands/config.go
new file mode 100644
index 0000000..81875a5
--- /dev/null
+++ b/internal/pkg/commands/config.go
@@ -0,0 +1,60 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"fmt"
+	flags "github.com/jessevdk/go-flags"
+	"gopkg.in/yaml.v2"
+)
+
+const copyrightNotice = `
+# Portions copyright 2019-present Open Networking Foundation
+# Original 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.
+#
+`
+
+type ConfigOptions struct {
+}
+
+func RegisterConfigCommands(parent *flags.Parser) {
+	parent.AddCommand("config", "generate cordctl configuration", "Commands to generate cordctl configuration", &ConfigOptions{})
+}
+
+func (options *ConfigOptions) Execute(args []string) error {
+	//GlobalConfig
+	ProcessGlobalOptions()
+	b, err := yaml.Marshal(GlobalConfig)
+	if err != nil {
+		return err
+	}
+	fmt.Println(copyrightNotice)
+	fmt.Println(string(b))
+	return nil
+}
diff --git a/internal/pkg/commands/funcmap.go b/internal/pkg/commands/funcmap.go
new file mode 100644
index 0000000..7a2bdf2
--- /dev/null
+++ b/internal/pkg/commands/funcmap.go
@@ -0,0 +1,80 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	"github.com/jhump/protoreflect/dynamic"
+	"github.com/jhump/protoreflect/grpcreflect"
+	"golang.org/x/net/context"
+	"google.golang.org/grpc"
+	reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+)
+
+type MethodNotFoundError struct {
+	Name string
+}
+
+func (e *MethodNotFoundError) Error() string {
+	return fmt.Sprintf("Method '%s' not found in function map", e.Name)
+}
+
+type MethodVersionNotFoundError struct {
+	Name    string
+	Version string
+}
+
+func (e *MethodVersionNotFoundError) Error() string {
+	return fmt.Sprintf("Method '%s' does not have a verison for '%s' specfied in function map", e.Name, e.Version)
+}
+
+type DescriptorNotFoundError struct {
+	Version string
+}
+
+func (e *DescriptorNotFoundError) Error() string {
+	return fmt.Sprintf("Protocol buffer descriptor for API version '%s' not found", e.Version)
+}
+
+type UnableToParseDescriptorErrror struct {
+	err     error
+	Version string
+}
+
+func (e *UnableToParseDescriptorErrror) Error() string {
+	return fmt.Sprintf("Unable to parse protocal buffer descriptor for version '%s': %s", e.Version, e.err)
+}
+
+func GetReflectionMethod(conn *grpc.ClientConn, name string) (grpcurl.DescriptorSource, string, error) {
+	refClient := grpcreflect.NewClient(context.Background(), reflectpb.NewServerReflectionClient(conn))
+	defer refClient.Reset()
+
+	desc := grpcurl.DescriptorSourceFromServer(context.Background(), refClient)
+
+	return desc, name, nil
+}
+
+func GetEnumValue(msg *dynamic.Message, name string) string {
+	return msg.FindFieldDescriptorByName(name).GetEnumType().
+		FindValueByNumber(msg.GetFieldByName(name).(int32)).GetName()
+}
+
+func SetEnumValue(msg *dynamic.Message, name string, value string) {
+	eValue := msg.FindFieldDescriptorByName(name).GetEnumType().FindValueByName(value)
+	msg.SetFieldByName(name, eValue.GetNumber())
+}
diff --git a/internal/pkg/commands/handler.go b/internal/pkg/commands/handler.go
new file mode 100644
index 0000000..678883d
--- /dev/null
+++ b/internal/pkg/commands/handler.go
@@ -0,0 +1,85 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"github.com/golang/protobuf/proto"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/dynamic"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/status"
+	"io"
+)
+
+type RpcEventHandler struct {
+	Response proto.Message
+	Status   *status.Status
+	Data     []byte
+	Fields   map[string]map[string]interface{}
+}
+
+func (h *RpcEventHandler) OnResolveMethod(*desc.MethodDescriptor) {
+}
+
+func (h *RpcEventHandler) OnSendHeaders(metadata.MD) {
+}
+
+func (h *RpcEventHandler) OnReceiveHeaders(metadata.MD) {
+}
+
+func (h *RpcEventHandler) OnReceiveResponse(m proto.Message) {
+	h.Response = m
+}
+
+func (h *RpcEventHandler) OnReceiveTrailers(s *status.Status, m metadata.MD) {
+	h.Status = s
+}
+
+func (h *RpcEventHandler) GetParams(msg proto.Message) error {
+	dmsg, err := dynamic.AsDynamicMessage(msg)
+	if err != nil {
+		return err
+	}
+
+	//fmt.Printf("MessageName: %s\n", dmsg.XXX_MessageName())
+
+	if h.Fields == nil || len(h.Fields) == 0 {
+		//fmt.Println("EOF")
+		return io.EOF
+	}
+
+	fields, ok := h.Fields[dmsg.XXX_MessageName()]
+	if !ok {
+		//fmt.Println("nil")
+		return nil
+	}
+
+	for k, v := range fields {
+		// _json is a special field name that indicates we should unmarshal json data
+		if k == "_json" {
+			err = dmsg.UnmarshalMergeJSON(v.([]byte))
+			if err != nil {
+				return err
+			}
+		} else {
+			dmsg.SetFieldByName(k, v)
+		}
+	}
+	delete(h.Fields, dmsg.XXX_MessageName())
+
+	return nil
+}
diff --git a/internal/pkg/commands/models.go b/internal/pkg/commands/models.go
new file mode 100644
index 0000000..c39d1df
--- /dev/null
+++ b/internal/pkg/commands/models.go
@@ -0,0 +1,609 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	flags "github.com/jessevdk/go-flags"
+	"github.com/jhump/protoreflect/dynamic"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"google.golang.org/grpc"
+	"sort"
+	"strings"
+	"time"
+)
+
+const (
+	DEFAULT_CREATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
+	DEFAULT_DELETE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
+	DEFAULT_UPDATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
+	DEFAULT_SYNC_FORMAT   = "table{{ .Id }}\t{{ .Message }}"
+)
+
+type ModelNameString string
+
+type ModelList struct {
+	ListOutputOptions
+	ShowHidden      bool   `long:"showhidden" description:"Show hidden fields in default output"`
+	ShowFeedback    bool   `long:"showfeedback" description:"Show feedback fields in default output"`
+	ShowBookkeeping bool   `long:"showbookkeeping" description:"Show bookkeeping fields in default output"`
+	Filter          string `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	State           string `short:"s" long:"state" description:"Filter model state [DEFAULT | ALL | DIRTY | DELETED | DIRTYPOL | DELETEDPOL]"`
+	Args            struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+}
+
+type ModelUpdate struct {
+	OutputOptions
+	Unbuffered  bool          `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter      string        `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	SetFields   string        `long:"set-field" description:"Comma-separated list of field=value to set"`
+	SetJSON     string        `long:"set-json" description:"JSON dictionary to use for settings fields"`
+	Sync        bool          `long:"sync" description:"Synchronize before returning"`
+	SyncTimeout time.Duration `long:"synctimeout" default:"600s" description:"Timeout for --sync option"`
+	Args        struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+	IDArgs struct {
+		ID []int32
+	} `positional-args:"yes" required:"no"`
+}
+
+type ModelDelete struct {
+	OutputOptions
+	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter     string `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	All        bool   `short:"a" long:"all" description:"Operate on all models"`
+	Args       struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+	IDArgs struct {
+		ID []int32
+	} `positional-args:"yes" required:"no"`
+}
+
+type ModelCreate struct {
+	OutputOptions
+	Unbuffered  bool          `short:"u" long:"unbuffered" description:"Do not buffer console output"`
+	SetFields   string        `long:"set-field" description:"Comma-separated list of field=value to set"`
+	SetJSON     string        `long:"set-json" description:"JSON dictionary to use for settings fields"`
+	Sync        bool          `long:"sync" description:"Synchronize before returning"`
+	SyncTimeout time.Duration `long:"synctimeout" default:"600s" description:"Timeout for --sync option"`
+	Args        struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+}
+
+type ModelSync struct {
+	OutputOptions
+	Unbuffered  bool          `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter      string        `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	SyncTimeout time.Duration `long:"synctimeout" default:"600s" description:"Timeout for synchronization"`
+	All         bool          `short:"a" long:"all" description:"Operate on all models"`
+	Args        struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+	IDArgs struct {
+		ID []int32
+	} `positional-args:"yes" required:"no"`
+}
+
+type ModelSetDirty struct {
+	OutputOptions
+	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter     string `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	All        bool   `short:"a" long:"all" description:"Operate on all models"`
+	Args       struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+	IDArgs struct {
+		ID []int32
+	} `positional-args:"yes" required:"no"`
+}
+
+type ModelOpts struct {
+	List     ModelList     `command:"list"`
+	Update   ModelUpdate   `command:"update"`
+	Delete   ModelDelete   `command:"delete"`
+	Create   ModelCreate   `command:"create"`
+	Sync     ModelSync     `command:"sync"`
+	SetDirty ModelSetDirty `command:"setdirty"`
+}
+
+type ModelStatusOutputRow struct {
+	Id      interface{} `json:"id"`
+	Message string      `json:"message"`
+}
+
+type ModelStatusOutput struct {
+	Rows       []ModelStatusOutputRow
+	Unbuffered bool
+}
+
+var modelOpts = ModelOpts{}
+
+func RegisterModelCommands(parser *flags.Parser) {
+	parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
+}
+
+// Initialize ModelStatusOutput structure, creating a row for each model that will be output
+func InitModelStatusOutput(unbuffered bool, count int) ModelStatusOutput {
+	return ModelStatusOutput{Rows: make([]ModelStatusOutputRow, count), Unbuffered: unbuffered}
+}
+
+// Update model status output row for the model
+//    If unbuffered is set then we will output directly to the console. Regardless of the unbuffered
+//    setting, we always update the row, as callers may check that row for status.
+// Args:
+//    output - ModelStatusOutput struct to update
+//    i - index of row to update
+//    id - id of model, <nil> if no model exists
+//    status - status text to set if there is no error
+//    errror - if non-nil, then apply error text instead of status text
+//    final - true if successful status should be reported, false if successful status is yet to come
+
+func UpdateModelStatusOutput(output *ModelStatusOutput, i int, id interface{}, status string, err error, final bool) {
+	if err != nil {
+		if output.Unbuffered {
+			fmt.Printf("%v: %s\n", id, err)
+		}
+		output.Rows[i] = ModelStatusOutputRow{Id: id, Message: err.Error()}
+	} else {
+		if output.Unbuffered && final {
+			fmt.Println(id)
+		}
+		output.Rows[i] = ModelStatusOutputRow{Id: id, Message: status}
+	}
+}
+
+// Convert a user-supplied state filter argument to the appropriate enum name
+func GetFilterKind(kindArg string) (string, error) {
+	kindMap := map[string]string{
+		"default":   FILTER_DEFAULT,
+		"all":       FILTER_ALL,
+		"dirty":     FILTER_DIRTY,
+		"deleted":   FILTER_DELETED,
+		"dirtypol":  FILTER_DIRTYPOL,
+		"deletedpo": FILTER_DELETEDPOL,
+	}
+
+	// If no arg then use default
+	if kindArg == "" {
+		return kindMap["default"], nil
+	}
+
+	val, ok := kindMap[strings.ToLower(kindArg)]
+	if !ok {
+		return "", corderrors.WithStackTrace(&corderrors.UnknownModelStateError{Name: kindArg})
+	}
+
+	return val, nil
+}
+
+// Common processing for commands that take a modelname and a list of ids or a filter
+func GetIDList(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, ids []int32, filter string, all bool) ([]int32, error) {
+	err := CheckModelName(descriptor, modelName)
+	if err != nil {
+		return nil, err
+	}
+
+	// we require exactly one of ID, --filter, or --all
+	exclusiveCount := 0
+	if len(ids) > 0 {
+		exclusiveCount++
+	}
+	if filter != "" {
+		exclusiveCount++
+	}
+	if all {
+		exclusiveCount++
+	}
+
+	if (exclusiveCount == 0) || (exclusiveCount > 1) {
+		return nil, corderrors.WithStackTrace(&corderrors.FilterRequiredError{})
+	}
+
+	queries, err := CommaSeparatedQueryToMap(filter, true)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(ids) > 0 {
+		// do nothing
+	} else {
+		models, err := ListOrFilterModels(context.Background(), conn, descriptor, modelName, FILTER_DEFAULT, queries)
+		if err != nil {
+			return nil, err
+		}
+		ids = make([]int32, len(models))
+		for i, model := range models {
+			ids[i] = model.GetFieldByName("id").(int32)
+		}
+		if len(ids) == 0 {
+			return nil, corderrors.WithStackTrace(&corderrors.NoMatchError{})
+		} else if len(ids) > 1 {
+			if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
+				return nil, corderrors.WithStackTrace(&corderrors.AbortedError{})
+			}
+		}
+	}
+
+	return ids, nil
+}
+
+func (options *ModelList) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, string(options.Args.ModelName))
+	if err != nil {
+		return err
+	}
+
+	filterKind, err := GetFilterKind(options.State)
+	if err != nil {
+		return err
+	}
+
+	queries, err := CommaSeparatedQueryToMap(options.Filter, true)
+	if err != nil {
+		return err
+	}
+
+	models, err := ListOrFilterModels(context.Background(), conn, descriptor, string(options.Args.ModelName), filterKind, queries)
+	if err != nil {
+		return err
+	}
+
+	var field_names []string
+	data := make([]map[string]interface{}, len(models))
+	for i, val := range models {
+		data[i] = make(map[string]interface{})
+		for _, field_desc := range val.GetKnownFields() {
+			field_name := field_desc.GetName()
+
+			isGuiHidden := strings.Contains(field_desc.GetFieldOptions().String(), "1005:1")
+			isFeedback := strings.Contains(field_desc.GetFieldOptions().String(), "1006:1")
+			isBookkeeping := strings.Contains(field_desc.GetFieldOptions().String(), "1007:1")
+
+			if isGuiHidden && (!options.ShowHidden) {
+				continue
+			}
+
+			if isFeedback && (!options.ShowFeedback) {
+				continue
+			}
+
+			if isBookkeeping && (!options.ShowBookkeeping) {
+				continue
+			}
+
+			if field_desc.IsRepeated() {
+				continue
+			}
+
+			data[i][field_name] = val.GetFieldByName(field_name)
+
+			// Every row has the same set of known field names, so it suffices to use the names
+			// from the first row.
+			if i == 0 {
+				field_names = append(field_names, field_name)
+			}
+		}
+	}
+
+	// Sort field names, making sure "id" appears first
+	sort.SliceStable(field_names, func(i, j int) bool {
+		if field_names[i] == "id" {
+			return true
+		} else if field_names[j] == "id" {
+			return false
+		} else {
+			return (field_names[i] < field_names[j])
+		}
+	})
+
+	var default_format strings.Builder
+	default_format.WriteString("table")
+	for i, field_name := range field_names {
+		if i == 0 {
+			fmt.Fprintf(&default_format, "{{ .%s }}", field_name)
+		} else {
+			fmt.Fprintf(&default_format, "\t{{ .%s }}", field_name)
+		}
+	}
+
+	FormatAndGenerateListOutput(&options.ListOutputOptions, default_format.String(), "{{.id}}", data)
+
+	return nil
+}
+
+func (options *ModelUpdate) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, string(options.Args.ModelName))
+	if err != nil {
+		return err
+	}
+
+	if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
+		(len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
+		return corderrors.WithStackTrace(&corderrors.FilterRequiredError{})
+	}
+
+	queries, err := CommaSeparatedQueryToMap(options.Filter, true)
+	if err != nil {
+		return err
+	}
+
+	updates, err := CommaSeparatedQueryToMap(options.SetFields, true)
+	if err != nil {
+		return err
+	}
+
+	modelName := string(options.Args.ModelName)
+
+	var models []*dynamic.Message
+
+	if len(options.IDArgs.ID) > 0 {
+		models = make([]*dynamic.Message, len(options.IDArgs.ID))
+		for i, id := range options.IDArgs.ID {
+			models[i], err = GetModel(context.Background(), conn, descriptor, modelName, id)
+			if err != nil {
+				return err
+			}
+		}
+	} else {
+		models, err = ListOrFilterModels(context.Background(), conn, descriptor, modelName, FILTER_DEFAULT, queries)
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(models) == 0 {
+		return corderrors.WithStackTrace(&corderrors.NoMatchError{})
+	} else if len(models) > 1 {
+		if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
+			return corderrors.WithStackTrace(&corderrors.AbortedError{})
+		}
+	}
+
+	fields := make(map[string]interface{})
+
+	if len(options.SetJSON) > 0 {
+		fields["_json"] = []byte(options.SetJSON)
+	}
+
+	for fieldName, value := range updates {
+		value = value[1:]
+		proto_value, err := TypeConvert(descriptor, modelName, fieldName, value)
+		if err != nil {
+			return err
+		}
+		fields[fieldName] = proto_value
+	}
+
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(models))
+	for i, model := range models {
+		id := model.GetFieldByName("id").(int32)
+		fields["id"] = id
+		err := UpdateModel(conn, descriptor, modelName, fields)
+
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Updated", err, !options.Sync)
+	}
+
+	if options.Sync {
+		ctx, cancel := context.WithTimeout(context.Background(), options.SyncTimeout)
+		defer cancel()
+		for i, model := range models {
+			id := model.GetFieldByName("id").(int32)
+			if modelStatusOutput.Rows[i].Message == "Updated" {
+				conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
+				conn, _, err = GetModelWithRetry(ctx, conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+				conditional_printf(!options.Quiet, "\n")
+				UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
+			}
+		}
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_UPDATE_FORMAT, DEFAULT_UPDATE_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelDelete) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	modelName := string(options.Args.ModelName)
+	ids, err := GetIDList(conn, descriptor, modelName, options.IDArgs.ID, options.Filter, options.All)
+	if err != nil {
+		return err
+	}
+
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
+	for i, id := range ids {
+		err = DeleteModel(conn, descriptor, modelName, id)
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Deleted", err, true)
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_DELETE_FORMAT, DEFAULT_DELETE_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelCreate) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, string(options.Args.ModelName))
+	if err != nil {
+		return err
+	}
+
+	updates, err := CommaSeparatedQueryToMap(options.SetFields, true)
+	if err != nil {
+		return err
+	}
+
+	modelName := string(options.Args.ModelName)
+
+	fields := make(map[string]interface{})
+
+	if len(options.SetJSON) > 0 {
+		fields["_json"] = []byte(options.SetJSON)
+	}
+
+	for fieldName, value := range updates {
+		value = value[1:]
+		proto_value, err := TypeConvert(descriptor, modelName, fieldName, value)
+		if err != nil {
+			return err
+		}
+		fields[fieldName] = proto_value
+	}
+
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, 1)
+
+	err = CreateModel(conn, descriptor, modelName, fields)
+	UpdateModelStatusOutput(&modelStatusOutput, 0, fields["id"], "Created", err, !options.Sync)
+
+	if options.Sync {
+		ctx, cancel := context.WithTimeout(context.Background(), options.SyncTimeout)
+		defer cancel()
+		if modelStatusOutput.Rows[0].Message == "Created" {
+			id := fields["id"].(int32)
+			conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
+			conn, _, err = GetModelWithRetry(ctx, conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+			conditional_printf(!options.Quiet, "\n")
+			UpdateModelStatusOutput(&modelStatusOutput, 0, id, "Enacted", err, true)
+		}
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_CREATE_FORMAT, DEFAULT_CREATE_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelSync) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	modelName := string(options.Args.ModelName)
+	ids, err := GetIDList(conn, descriptor, modelName, options.IDArgs.ID, options.Filter, options.All)
+	if err != nil {
+		return err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), options.SyncTimeout)
+	defer cancel()
+
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
+	for i, id := range ids {
+		conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
+		conn, _, err = GetModelWithRetry(ctx, conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+		conditional_printf(!options.Quiet, "\n")
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SYNC_FORMAT, DEFAULT_SYNC_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelSetDirty) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	modelName := string(options.Args.ModelName)
+	ids, err := GetIDList(conn, descriptor, modelName, options.IDArgs.ID, options.Filter, options.All)
+	if err != nil {
+		return err
+	}
+
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
+	for i, id := range ids {
+		updateMap := map[string]interface{}{"id": id}
+		err := UpdateModel(conn, descriptor, modelName, updateMap)
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Dirtied", err, true)
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SYNC_FORMAT, DEFAULT_SYNC_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (modelName *ModelNameString) Complete(match string) []flags.Completion {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return nil
+	}
+
+	defer conn.Close()
+
+	models, err := GetModelNames(descriptor)
+	if err != nil {
+		return nil
+	}
+
+	list := make([]flags.Completion, 0)
+	for k := range models {
+		if strings.HasPrefix(k, match) {
+			list = append(list, flags.Completion{Item: k})
+		}
+	}
+
+	return list
+}
diff --git a/internal/pkg/commands/models_test.go b/internal/pkg/commands/models_test.go
new file mode 100644
index 0000000..40e25ea
--- /dev/null
+++ b/internal/pkg/commands/models_test.go
@@ -0,0 +1,446 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original copyright 2019-present Ciena Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the"github.com/stretchr/testify/assert" "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 commands
+
+import (
+	"bytes"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"github.com/stretchr/testify/assert"
+	"testing"
+	"time"
+)
+
+func TestModelList(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 1,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice1",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		},
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 2,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice2",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelListFilterID(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 1,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice1",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	options.List.Filter = "id=1"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelListFilterName(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 2,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice2",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	options.List.Filter = "name=mockslice2"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelListDirty(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"controller_kind": "",
+			"controller_replica_count": 0,
+			"creator_id": 0,
+			"default_flavor_id": 0,
+			"default_image_id": 0,
+			"default_isolation": "",
+			"default_node_id": 0,
+			"description": "",
+			"enabled": false,
+			"exposed_ports": "",
+			"id": 2,
+			"max_instances": 0,
+			"mount_data_sets": "",
+			"name": "mockslice2",
+			"network": "",
+			"principal_id": 0,
+			"service_id": 0,
+			"site_id": 1,
+			"trust_domain_id": 0
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.List.Args.ModelName = "Slice"
+	options.List.OutputAs = "json"
+	options.List.State = "dirty"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelUpdate(t *testing.T) {
+	expected := `[{"id":1, "message":"Updated"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.IDArgs.ID = []int32{1}
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelUpdateUsingFilter(t *testing.T) {
+	expected := `[{"id":1, "message":"Updated"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.Filter = "id=1"
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelUpdateNoExist(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.IDArgs.ID = []int32{77}
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	_, matched := err.(*corderrors.ModelNotFoundError)
+	assert.True(t, matched)
+}
+
+func TestModelUpdateUsingFilterNoExist(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Update.Args.ModelName = "Slice"
+	options.Update.OutputAs = "json"
+	options.Update.Filter = "id=77"
+	options.Update.SetFields = "name=mockslice1_newname"
+	err := options.Update.Execute([]string{})
+
+	_, matched := err.(*corderrors.NoMatchError)
+	assert.True(t, matched)
+}
+
+func TestModelCreate(t *testing.T) {
+	expected := `[{"id":3, "message":"Created"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Create.Args.ModelName = "Slice"
+	options.Create.OutputAs = "json"
+	options.Create.SetFields = "name=mockslice3,site_id=1"
+	err := options.Create.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDelete(t *testing.T) {
+	expected := `[{"id":1, "message":"Deleted"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.IDArgs.ID = []int32{1}
+	err := options.Delete.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDeleteUsingFilter(t *testing.T) {
+	expected := `[{"id":1, "message":"Deleted"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.Filter = "id=1"
+	err := options.Delete.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDeleteNoExist(t *testing.T) {
+	expected := `[{"id":77, "message":"Not Found [on model Slice <id=77>]"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.IDArgs.ID = []int32{77}
+	err := options.Delete.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelDeleteFilterNoExist(t *testing.T) {
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Delete.Args.ModelName = "Slice"
+	options.Delete.OutputAs = "json"
+	options.Delete.Filter = "id=77"
+	err := options.Delete.Execute([]string{})
+
+	_, matched := err.(*corderrors.NoMatchError)
+	assert.True(t, matched)
+}
+
+func TestModelSync(t *testing.T) {
+	expected := `[{"id":1, "message":"Enacted"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Sync.Args.ModelName = "Slice"
+	options.Sync.OutputAs = "json"
+	options.Sync.IDArgs.ID = []int32{1}
+	options.Sync.SyncTimeout = 5 * time.Second
+	err := options.Sync.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelSyncTimeout(t *testing.T) {
+	expected := `[{"id":2, "message":"context deadline exceeded"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.Sync.Args.ModelName = "Slice"
+	options.Sync.OutputAs = "json"
+	options.Sync.IDArgs.ID = []int32{2}
+	options.Sync.SyncTimeout = 5 * time.Second
+	err := options.Sync.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestModelSetDirty(t *testing.T) {
+	expected := `[{"id":1, "message":"Dirtied"}]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelOpts
+	options.SetDirty.Args.ModelName = "Slice"
+	options.SetDirty.OutputAs = "json"
+	options.SetDirty.IDArgs.ID = []int32{1}
+	err := options.SetDirty.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
diff --git a/internal/pkg/commands/modeltypes.go b/internal/pkg/commands/modeltypes.go
new file mode 100644
index 0000000..e24a57a
--- /dev/null
+++ b/internal/pkg/commands/modeltypes.go
@@ -0,0 +1,69 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	flags "github.com/jessevdk/go-flags"
+	"sort"
+)
+
+const (
+	DEFAULT_MODELTYPE_LIST_FORMAT = "{{ . }}"
+)
+
+type ModelTypeList struct {
+	OutputOptions
+}
+
+type ModelTypeOpts struct {
+	List ModelTypeList `command:"list"`
+}
+
+var modelTypeOpts = ModelTypeOpts{}
+
+func RegisterModelTypeCommands(parser *flags.Parser) {
+	parser.AddCommand("modeltype", "model type commands", "Commands to query the types of models", &modelTypeOpts)
+}
+
+func (options *ModelTypeList) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	models, err := GetModelNames(descriptor)
+	if err != nil {
+		return err
+	}
+
+	model_names := []string{}
+	for k := range models {
+		model_names = append(model_names, k)
+	}
+
+	sort.Strings(model_names)
+
+	FormatAndGenerateOutput(
+		&options.OutputOptions,
+		DEFAULT_MODELTYPE_LIST_FORMAT,
+		DEFAULT_MODELTYPE_LIST_FORMAT,
+		model_names)
+
+	return nil
+}
diff --git a/internal/pkg/commands/modeltypes_test.go b/internal/pkg/commands/modeltypes_test.go
new file mode 100644
index 0000000..e18c2db
--- /dev/null
+++ b/internal/pkg/commands/modeltypes_test.go
@@ -0,0 +1,111 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"testing"
+)
+
+func TestModelTypeList(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `ANIPort
+AddressPool
+AttWorkflowDriverService
+AttWorkflowDriverServiceInstance
+AttWorkflowDriverWhiteListEntry
+BNGPortMapping
+BackupFile
+BackupOperation
+BandwidthProfile
+ComputeServiceInstance
+FabricCrossconnectService
+FabricCrossconnectServiceInstance
+FabricIpAddress
+FabricService
+Flavor
+Image
+InterfaceType
+KubernetesConfigMap
+KubernetesConfigVolumeMount
+KubernetesData
+KubernetesResourceInstance
+KubernetesSecret
+KubernetesSecretVolumeMount
+KubernetesService
+KubernetesServiceInstance
+NNIPort
+Network
+NetworkParameter
+NetworkParameterType
+NetworkSlice
+NetworkTemplate
+Node
+NodeLabel
+NodeToSwitchPort
+OLTDevice
+ONOSApp
+ONOSService
+ONUDevice
+PONPort
+Port
+PortBase
+PortInterface
+Principal
+Privilege
+RCORDIpAddress
+RCORDService
+RCORDSubscriber
+Role
+Service
+ServiceAttribute
+ServiceDependency
+ServiceGraphConstraint
+ServiceInstance
+ServiceInstanceAttribute
+ServiceInstanceLink
+ServiceInterface
+ServicePort
+Site
+Slice
+Switch
+SwitchPort
+Tag
+TechnologyProfile
+TrustDomain
+UNIPort
+User
+VOLTService
+VOLTServiceInstance
+XOSCore
+XOSGuiExtension
+`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ModelTypeOpts
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertStringEqual(t, got.String(), expected)
+}
diff --git a/internal/pkg/commands/orm.go b/internal/pkg/commands/orm.go
new file mode 100644
index 0000000..0ac846a
--- /dev/null
+++ b/internal/pkg/commands/orm.go
@@ -0,0 +1,658 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/dynamic"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"google.golang.org/grpc"
+	"io"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// Flags for calling the *WithRetry methods
+const (
+	GM_QUIET         = 1
+	GM_UNTIL_FOUND   = 2
+	GM_UNTIL_ENACTED = 4
+	GM_UNTIL_STATUS  = 8
+)
+
+// Valid choices for FilterModels `Kind` argument
+const (
+	FILTER_DEFAULT    = "DEFAULT"
+	FILTER_ALL        = "ALL"
+	FILTER_DIRTY      = "SYNCHRONIZER_DIRTY_OBJECTS"
+	FILTER_DELETED    = "SYNCHRONIZER_DELETED_OBJECTS"
+	FILTER_DIRTYPOL   = "SYNCHRONIZER_DIRTY_POLICIES"
+	FILTER_DELETEDPOL = "SYNCHRONIZER_DELETED_POLICIES"
+)
+
+type QueryEventHandler struct {
+	RpcEventHandler
+	Elements map[string]string
+	Model    *desc.MessageDescriptor
+	Kind     string
+	EOF      bool
+}
+
+// Separate the operator from the query value.
+// For example,
+//    "==foo"  --> "EQUAL", "foo"
+func DecodeOperator(query string) (string, string, bool, error) {
+	if strings.HasPrefix(query, "!=") {
+		return strings.TrimSpace(query[2:]), "EQUAL", true, nil
+	} else if strings.HasPrefix(query, "==") {
+		return "", "", false, corderrors.NewInvalidInputError("Operator == is now allowed. Suggest using = instead.")
+	} else if strings.HasPrefix(query, "=") {
+		return strings.TrimSpace(query[1:]), "EQUAL", false, nil
+	} else if strings.HasPrefix(query, ">=") {
+		return strings.TrimSpace(query[2:]), "GREATER_THAN_OR_EQUAL", false, nil
+	} else if strings.HasPrefix(query, ">") {
+		return strings.TrimSpace(query[1:]), "GREATER_THAN", false, nil
+	} else if strings.HasPrefix(query, "<=") {
+		return strings.TrimSpace(query[2:]), "LESS_THAN_OR_EQUAL", false, nil
+	} else if strings.HasPrefix(query, "<") {
+		return strings.TrimSpace(query[1:]), "LESS_THAN", false, nil
+	} else {
+		return strings.TrimSpace(query), "EQUAL", false, nil
+	}
+}
+
+// Generate the parameters for Query messages.
+func (h *QueryEventHandler) GetParams(msg proto.Message) error {
+	dmsg, err := dynamic.AsDynamicMessage(msg)
+	if err != nil {
+		return err
+	}
+
+	//fmt.Printf("MessageName: %s\n", dmsg.XXX_MessageName())
+
+	if h.EOF {
+		return io.EOF
+	}
+
+	// Get the MessageType for the `elements` field
+	md := dmsg.GetMessageDescriptor()
+	elements_fld := md.FindFieldByName("elements")
+	elements_mt := elements_fld.GetMessageType()
+
+	for field_name, element := range h.Elements {
+		value, operator, invert, err := DecodeOperator(element)
+		if err != nil {
+			return err
+		}
+
+		nm := dynamic.NewMessage(elements_mt)
+
+		field_descriptor := h.Model.FindFieldByName(field_name)
+		if field_descriptor == nil {
+			return corderrors.WithStackTrace(&corderrors.FieldDoesNotExistError{ModelName: h.Model.GetName(), FieldName: field_name})
+		}
+
+		field_type := field_descriptor.GetType()
+		switch field_type {
+		case descriptor.FieldDescriptorProto_TYPE_INT32:
+			var i int64
+			i, err = strconv.ParseInt(value, 10, 32)
+			nm.SetFieldByName("iValue", int32(i))
+		case descriptor.FieldDescriptorProto_TYPE_UINT32:
+			var i int64
+			i, err = strconv.ParseInt(value, 10, 32)
+			nm.SetFieldByName("iValue", uint32(i))
+		case descriptor.FieldDescriptorProto_TYPE_FLOAT:
+			err = corderrors.NewInvalidInputError("Floating point filters are unsupported")
+		case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
+			err = corderrors.NewInvalidInputError("Floating point filters are unsupported")
+		default:
+			nm.SetFieldByName("sValue", value)
+			err = nil
+		}
+
+		if err != nil {
+			return err
+		}
+
+		nm.SetFieldByName("name", field_name)
+		nm.SetFieldByName("invert", invert)
+		SetEnumValue(nm, "operator", operator)
+		dmsg.AddRepeatedFieldByName("elements", nm)
+	}
+
+	SetEnumValue(dmsg, "kind", h.Kind)
+
+	h.EOF = true
+
+	return nil
+}
+
+// Take a string list of queries and turns it into a map of queries
+func QueryStringsToMap(query_args []string, allow_inequality bool) (map[string]string, error) {
+	queries := make(map[string]string)
+	for _, query_str := range query_args {
+		query_str := strings.TrimSpace(query_str)
+		operator_pos := -1
+		for i, ch := range query_str {
+			if allow_inequality {
+				if (ch == '!') || (ch == '=') || (ch == '>') || (ch == '<') {
+					operator_pos = i
+					break
+				}
+			} else {
+				if ch == '=' {
+					operator_pos = i
+					break
+				}
+			}
+		}
+		if operator_pos == -1 {
+			return nil, corderrors.WithStackTrace(&corderrors.IllegalQueryError{Query: query_str})
+		}
+		queries[strings.TrimSpace(query_str[:operator_pos])] = query_str[operator_pos:]
+	}
+	return queries, nil
+}
+
+// Take a string of comma-separated queries and turn it into a map of queries
+func CommaSeparatedQueryToMap(query_str string, allow_inequality bool) (map[string]string, error) {
+	if query_str == "" {
+		return nil, nil
+	}
+
+	query_strings := strings.Split(query_str, ",")
+	return QueryStringsToMap(query_strings, allow_inequality)
+}
+
+// Convert a string into the appropriate gRPC type for a given field
+func TypeConvert(source grpcurl.DescriptorSource, modelName string, field_name string, v string) (interface{}, error) {
+	model_descriptor, err := source.FindSymbol("xos." + modelName)
+	if err != nil {
+		return nil, err
+	}
+	model_md, ok := model_descriptor.(*desc.MessageDescriptor)
+	if !ok {
+		return nil, corderrors.WithStackTrace(&corderrors.TypeConversionError{Source: modelName, Destination: "messageDescriptor"})
+	}
+	field_descriptor := model_md.FindFieldByName(field_name)
+	if field_descriptor == nil {
+		return nil, corderrors.WithStackTrace(&corderrors.FieldDoesNotExistError{ModelName: modelName, FieldName: field_name})
+	}
+	field_type := field_descriptor.GetType()
+
+	var result interface{}
+
+	switch field_type {
+	case descriptor.FieldDescriptorProto_TYPE_INT32:
+		var i int64
+		i, err = strconv.ParseInt(v, 10, 32)
+		result = int32(i)
+	case descriptor.FieldDescriptorProto_TYPE_UINT32:
+		var i int64
+		i, err = strconv.ParseInt(v, 10, 32)
+		result = uint32(i)
+	case descriptor.FieldDescriptorProto_TYPE_FLOAT:
+		var f float64
+		f, err = strconv.ParseFloat(v, 32)
+		result = float32(f)
+	case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
+		var f float64
+		f, err = strconv.ParseFloat(v, 64)
+		result = f
+	default:
+		result = v
+		err = nil
+	}
+
+	return result, err
+}
+
+// Return a list of all available model names
+func GetModelNames(source grpcurl.DescriptorSource) (map[string]bool, error) {
+	models := make(map[string]bool)
+	methods, err := grpcurl.ListMethods(source, "xos.xos")
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, method := range methods {
+		if strings.HasPrefix(method, "xos.xos.Get") {
+			models[method[11:]] = true
+		}
+	}
+
+	return models, nil
+}
+
+// Check to see if a model name is valid
+func CheckModelName(source grpcurl.DescriptorSource, name string) error {
+	models, err := GetModelNames(source)
+	if err != nil {
+		return err
+	}
+	_, present := models[name]
+	if !present {
+		return corderrors.WithStackTrace(&corderrors.UnknownModelTypeError{Name: name})
+	}
+	return nil
+}
+
+// Create a model in XOS given a map of fields
+func CreateModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fields map[string]interface{}) error {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{
+		Fields: map[string]map[string]interface{}{"xos." + modelName: fields},
+	}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Create"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return corderrors.RpcErrorWithModelNameToCordError(err, modelName)
+	} else if h.Status != nil && h.Status.Err() != nil {
+		return corderrors.RpcErrorWithModelNameToCordError(h.Status.Err(), modelName)
+	}
+
+	resp, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return err
+	}
+
+	fields["id"] = resp.GetFieldByName("id").(int32)
+
+	if resp.HasFieldName("uuid") {
+		fields["uuid"] = resp.GetFieldByName("uuid").(string)
+	}
+
+	return nil
+}
+
+// Update a model in XOS given a map of fields
+func UpdateModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fields map[string]interface{}) error {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{
+		Fields: map[string]map[string]interface{}{"xos." + modelName: fields},
+	}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Update"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return corderrors.RpcErrorWithModelNameToCordError(err, modelName)
+	} else if h.Status != nil && h.Status.Err() != nil {
+		return corderrors.RpcErrorWithModelNameToCordError(h.Status.Err(), modelName)
+	}
+
+	resp, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return err
+	}
+
+	// TODO: Do we need to do anything with the response?
+	_ = resp
+
+	return nil
+}
+
+// Get a model from XOS given its ID
+func GetModel(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) (*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(ctx, GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{
+		Fields: map[string]map[string]interface{}{"xos.ID": map[string]interface{}{"id": id}},
+	}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Get"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return nil, corderrors.RpcErrorWithIdToCordError(err, modelName, id)
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, corderrors.RpcErrorWithIdToCordError(h.Status.Err(), modelName, id) //h.Status.Err()
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, err
+	}
+
+	return d, nil
+}
+
+// Get a model, but retry under a variety of circumstances
+func GetModelWithRetry(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
+	quiet := (flags & GM_QUIET) != 0
+	until_found := (flags & GM_UNTIL_FOUND) != 0
+	until_enacted := (flags & GM_UNTIL_ENACTED) != 0
+	until_status := (flags & GM_UNTIL_STATUS) != 0
+
+	for {
+		var err error
+
+		if conn == nil {
+			conn, err = NewConnection()
+			if err != nil {
+				return nil, nil, err
+			}
+		}
+
+		model, err := GetModel(ctx, conn, descriptor, modelName, id)
+		if err != nil {
+			if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
+				strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
+				if !quiet {
+					fmt.Print(".")
+				}
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
+				conn.Close()
+				conn = nil
+				continue
+			}
+
+			_, is_not_found_error := err.(*corderrors.ModelNotFoundError)
+			if until_found && is_not_found_error {
+				if !quiet {
+					fmt.Print("x")
+				}
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
+				continue
+			}
+			return nil, nil, err
+		}
+
+		if until_enacted && !IsEnacted(model) {
+			if !quiet {
+				fmt.Print("o")
+			}
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
+			continue
+		}
+
+		if until_status && model.GetFieldByName("status") == nil {
+			if !quiet {
+				fmt.Print("O")
+			}
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
+			continue
+		}
+
+		return conn, model, nil
+	}
+}
+
+func ItemsToDynamicMessageList(items interface{}) []*dynamic.Message {
+	result := make([]*dynamic.Message, len(items.([]interface{})))
+	for i, item := range items.([]interface{}) {
+		result[i] = item.(*dynamic.Message)
+	}
+	return result
+}
+
+// List all objects of a given model
+func ListModels(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string) ([]*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(ctx, GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.List"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return nil, corderrors.RpcErrorWithModelNameToCordError(err, modelName)
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, corderrors.RpcErrorWithModelNameToCordError(h.Status.Err(), modelName)
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, err
+	}
+
+	items, err := d.TryGetFieldByName("items")
+	if err != nil {
+		return nil, err
+	}
+
+	return ItemsToDynamicMessageList(items), nil
+}
+
+// Filter models based on field values
+//   queries is a map of <field_name> to <operator><query>
+//   For example,
+//     map[string]string{"name": "==mysite"}
+func FilterModels(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, kind string, queries map[string]string) ([]*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(ctx, GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	model_descriptor, err := descriptor.FindSymbol("xos." + modelName)
+	if err != nil {
+		return nil, err
+	}
+	model_md, ok := model_descriptor.(*desc.MessageDescriptor)
+	if !ok {
+		return nil, corderrors.WithStackTrace(&corderrors.TypeConversionError{Source: modelName, Destination: "messageDescriptor"})
+	}
+
+	h := &QueryEventHandler{
+		RpcEventHandler: RpcEventHandler{
+			Fields: map[string]map[string]interface{}{"xos.Query": map[string]interface{}{"kind": 0}},
+		},
+		Elements: queries,
+		Model:    model_md,
+		Kind:     kind,
+	}
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Filter"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return nil, corderrors.RpcErrorWithQueriesToCordError(err, modelName, queries)
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, corderrors.RpcErrorWithQueriesToCordError(h.Status.Err(), modelName, queries)
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, err
+	}
+
+	items, err := d.TryGetFieldByName("items")
+	if err != nil {
+		return nil, err
+	}
+
+	return ItemsToDynamicMessageList(items), nil
+}
+
+// Call ListModels or FilterModels as appropriate
+func ListOrFilterModels(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, kind string, queries map[string]string) ([]*dynamic.Message, error) {
+	if (len(queries) == 0) && (kind == FILTER_DEFAULT) {
+		return ListModels(ctx, conn, descriptor, modelName)
+	} else {
+		return FilterModels(ctx, conn, descriptor, modelName, kind, queries)
+	}
+}
+
+// Get a model from XOS given a fieldName/fieldValue
+func FindModel(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
+	models, err := FilterModels(ctx, conn, descriptor, modelName, FILTER_DEFAULT, queries)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(models) == 0 {
+		cordError := &corderrors.ModelNotFoundError{}
+		cordError.Obj = corderrors.ObjectReference{ModelName: modelName, Queries: queries}
+		return nil, corderrors.WithStackTrace(cordError)
+	}
+
+	return models[0], nil
+}
+
+// Find a model, but retry under a variety of circumstances
+func FindModelWithRetry(ctx context.Context, conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
+	quiet := (flags & GM_QUIET) != 0
+	until_found := (flags & GM_UNTIL_FOUND) != 0
+	until_enacted := (flags & GM_UNTIL_ENACTED) != 0
+	until_status := (flags & GM_UNTIL_STATUS) != 0
+
+	for {
+		var err error
+
+		if conn == nil {
+			conn, err = NewConnection()
+			if err != nil {
+				return nil, nil, err
+			}
+		}
+
+		model, err := FindModel(ctx, conn, descriptor, modelName, queries)
+		if err != nil {
+			if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
+				strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
+				if !quiet {
+					fmt.Print(".")
+				}
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
+				conn.Close()
+				conn = nil
+				continue
+			}
+
+			_, is_not_found_error := err.(*corderrors.ModelNotFoundError)
+			if until_found && is_not_found_error {
+				if !quiet {
+					fmt.Print("x")
+				}
+				select {
+				case <-time.After(100 * time.Millisecond):
+				case <-ctx.Done():
+					return nil, nil, ctx.Err()
+				}
+				continue
+			}
+			return nil, nil, err
+		}
+
+		if until_enacted && !IsEnacted(model) {
+			if !quiet {
+				fmt.Print("o")
+			}
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
+			continue
+		}
+
+		if until_status && model.GetFieldByName("status") == nil {
+			if !quiet {
+				fmt.Print("O")
+			}
+			select {
+			case <-time.After(100 * time.Millisecond):
+			case <-ctx.Done():
+				return nil, nil, ctx.Err()
+			}
+			continue
+		}
+
+		return conn, model, nil
+	}
+}
+
+// Get a model from XOS given its ID
+func DeleteModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) error {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{
+		Fields: map[string]map[string]interface{}{"xos.ID": map[string]interface{}{"id": id}},
+	}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Delete"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return corderrors.RpcErrorWithIdToCordError(err, modelName, id)
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return corderrors.RpcErrorWithIdToCordError(h.Status.Err(), modelName, id)
+	}
+
+	_, err = dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Takes a *dynamic.Message and turns it into a map of fields to interfaces
+//    TODO: Might be more useful to convert the values to strings and ints
+func MessageToMap(d *dynamic.Message) map[string]interface{} {
+	fields := make(map[string]interface{})
+	for _, field_desc := range d.GetKnownFields() {
+		field_name := field_desc.GetName()
+		fields[field_name] = d.GetFieldByName(field_name)
+	}
+	return fields
+}
+
+// Returns True if a message has been enacted
+func IsEnacted(d *dynamic.Message) bool {
+	enacted := d.GetFieldByName("enacted").(float64)
+	updated := d.GetFieldByName("updated").(float64)
+
+	return (enacted >= updated)
+}
diff --git a/internal/pkg/commands/orm_test.go b/internal/pkg/commands/orm_test.go
new file mode 100644
index 0000000..2d8db62
--- /dev/null
+++ b/internal/pkg/commands/orm_test.go
@@ -0,0 +1,243 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestDecodeOperator(t *testing.T) {
+	value, operator, invert, err := DecodeOperator("=something")
+	assert.Equal(t, value, "something")
+	assert.Equal(t, operator, "EQUAL")
+	assert.Equal(t, invert, false)
+	assert.Equal(t, err, nil)
+
+	value, operator, invert, err = DecodeOperator("!=something")
+	assert.Equal(t, value, "something")
+	assert.Equal(t, operator, "EQUAL")
+	assert.Equal(t, invert, true)
+	assert.Equal(t, err, nil)
+
+	value, operator, invert, err = DecodeOperator(">3")
+	assert.Equal(t, value, "3")
+	assert.Equal(t, operator, "GREATER_THAN")
+	assert.Equal(t, invert, false)
+	assert.Equal(t, err, nil)
+
+	value, operator, invert, err = DecodeOperator(">=3")
+	assert.Equal(t, value, "3")
+	assert.Equal(t, operator, "GREATER_THAN_OR_EQUAL")
+	assert.Equal(t, invert, false)
+	assert.Equal(t, err, nil)
+
+	value, operator, invert, err = DecodeOperator("<3")
+	assert.Equal(t, value, "3")
+	assert.Equal(t, operator, "LESS_THAN")
+	assert.Equal(t, invert, false)
+	assert.Equal(t, err, nil)
+
+	value, operator, invert, err = DecodeOperator("<=3")
+	assert.Equal(t, value, "3")
+	assert.Equal(t, operator, "LESS_THAN_OR_EQUAL")
+	assert.Equal(t, invert, false)
+	assert.Equal(t, err, nil)
+}
+
+func TestCommaSeparatedQueryToMap(t *testing.T) {
+	m, err := CommaSeparatedQueryToMap("foo=7,bar!=stuff, x = 5, y= 27", true)
+	assert.Equal(t, err, nil)
+	assert.Equal(t, m["foo"], "=7")
+	assert.Equal(t, m["bar"], "!=stuff")
+	assert.Equal(t, m["x"], "= 5")
+	assert.Equal(t, m["y"], "= 27")
+}
+
+func TestCommaSeparatedQueryToMapIllegal(t *testing.T) {
+	// Query string missing operator
+	_, err := CommaSeparatedQueryToMap("foo", true)
+
+	_, matched := err.(*corderrors.IllegalQueryError)
+	assert.True(t, matched)
+
+	// Query string is contains an empty element
+	_, err = CommaSeparatedQueryToMap(",foo=bar", true)
+
+	_, matched = err.(*corderrors.IllegalQueryError)
+	assert.True(t, matched)
+}
+
+func TestCommaSeparatedQueryToMapEmpty(t *testing.T) {
+	// Query string missing operator
+	m, err := CommaSeparatedQueryToMap("", true)
+
+	assert.Equal(t, err, nil)
+	assert.Equal(t, len(m), 0)
+}
+
+func TestTypeConvert(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	v, err := TypeConvert(descriptor, "Site", "id", "7")
+	assert.Equal(t, err, nil)
+	assert.Equal(t, v, int32(7))
+
+	v, err = TypeConvert(descriptor, "Site", "name", "foo")
+	assert.Equal(t, err, nil)
+	assert.Equal(t, v, "foo")
+
+	v, err = TypeConvert(descriptor, "Site", "enacted", "123.4")
+	assert.Equal(t, err, nil)
+	assert.Equal(t, v, 123.4)
+}
+
+func TestCheckModelName(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, "Slice")
+	assert.Equal(t, err, nil)
+
+	err = CheckModelName(descriptor, "DoesNotExist")
+	_, matched := err.(*corderrors.UnknownModelTypeError)
+	assert.True(t, matched)
+}
+
+func TestCreateModel(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	m := make(map[string]interface{})
+	m["name"] = "mockslice3"
+	m["site_id"] = int32(1)
+
+	err = CreateModel(conn, descriptor, "Slice", m)
+	assert.Equal(t, err, nil)
+
+	assert.Equal(t, m["id"], int32(3))
+}
+
+func TestUpdateModel(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	m := make(map[string]interface{})
+	m["id"] = int32(1)
+	m["name"] = "mockslice1_newname"
+
+	err = UpdateModel(conn, descriptor, "Slice", m)
+	assert.Equal(t, err, nil)
+}
+
+func TestGetModel(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	m, err := GetModel(context.Background(), conn, descriptor, "Slice", int32(1))
+	assert.Equal(t, err, nil)
+
+	assert.Equal(t, m.GetFieldByName("id").(int32), int32(1))
+	assert.Equal(t, m.GetFieldByName("name").(string), "mockslice1")
+}
+
+func TestGetModelNoExist(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	_, err = GetModel(context.Background(), conn, descriptor, "Slice", int32(77))
+	assert.NotEqual(t, err, nil)
+
+	_, matched := err.(*corderrors.ModelNotFoundError)
+	assert.True(t, matched)
+}
+
+func TestListModels(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	m, err := ListModels(context.Background(), conn, descriptor, "Slice")
+	assert.Equal(t, err, nil)
+
+	assert.Equal(t, len(m), 2)
+	assert.Equal(t, m[0].GetFieldByName("id").(int32), int32(1))
+	assert.Equal(t, m[0].GetFieldByName("name").(string), "mockslice1")
+	assert.Equal(t, m[1].GetFieldByName("id").(int32), int32(2))
+	assert.Equal(t, m[1].GetFieldByName("name").(string), "mockslice2")
+}
+
+func TestFilterModels(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	qm := map[string]string{"id": "=1"}
+
+	m, err := FilterModels(context.Background(), conn, descriptor, "Slice", FILTER_DEFAULT, qm)
+	assert.Equal(t, err, nil)
+
+	assert.Equal(t, len(m), 1)
+	assert.Equal(t, m[0].GetFieldByName("id").(int32), int32(1))
+	assert.Equal(t, m[0].GetFieldByName("name").(string), "mockslice1")
+}
+
+func TestFindModel(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	qm := map[string]string{"id": "=1"}
+
+	m, err := FindModel(context.Background(), conn, descriptor, "Slice", qm)
+	assert.Equal(t, err, nil)
+
+	assert.Equal(t, m.GetFieldByName("id").(int32), int32(1))
+	assert.Equal(t, m.GetFieldByName("name").(string), "mockslice1")
+}
+
+func TestFindModelNoExist(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	qm := map[string]string{"id": "=77"}
+
+	_, err = FindModel(context.Background(), conn, descriptor, "Slice", qm)
+	assert.NotEqual(t, err, nil)
+
+	_, matched := err.(*corderrors.ModelNotFoundError)
+	assert.True(t, matched)
+}
+
+func TestDeleteModel(t *testing.T) {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	assert.Equal(t, err, nil)
+	defer conn.Close()
+
+	err = DeleteModel(conn, descriptor, "Slice", int32(1))
+	assert.Equal(t, err, nil)
+}
diff --git a/internal/pkg/commands/services.go b/internal/pkg/commands/services.go
new file mode 100644
index 0000000..d199ee9
--- /dev/null
+++ b/internal/pkg/commands/services.go
@@ -0,0 +1,95 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	"github.com/fullstorydev/grpcurl"
+	flags "github.com/jessevdk/go-flags"
+	"github.com/jhump/protoreflect/dynamic"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+)
+
+const (
+	DEFAULT_SERVICE_FORMAT = "table{{ .Name }}\t{{.Version}}\t{{.State}}"
+)
+
+type ServiceList struct {
+	ListOutputOptions
+}
+
+type ServiceListOutput struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+	State   string `json:"state"`
+}
+
+type ServiceOpts struct {
+	List ServiceList `command:"list"`
+}
+
+var serviceOpts = ServiceOpts{}
+
+func RegisterServiceCommands(parser *flags.Parser) {
+	parser.AddCommand("service", "service commands", "Commands to query and manipulate dynamically loaded XOS Services", &serviceOpts)
+}
+
+func (options *ServiceList) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{}
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.dynamicload.GetLoadStatus", headers, h, h.GetParams)
+	if err != nil {
+		return corderrors.RpcErrorToCordError(err)
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return corderrors.RpcErrorToCordError(h.Status.Err())
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return err
+	}
+
+	items, err := d.TryGetFieldByName("services")
+	if err != nil {
+		return err
+	}
+
+	data := make([]ServiceListOutput, len(items.([]interface{})))
+
+	for i, item := range items.([]interface{}) {
+		val := item.(*dynamic.Message)
+		data[i].Name = val.GetFieldByName("name").(string)
+		data[i].Version = val.GetFieldByName("version").(string)
+		data[i].State = val.GetFieldByName("state").(string)
+	}
+
+	FormatAndGenerateListOutput(&options.ListOutputOptions, DEFAULT_SERVICE_FORMAT, "{{.Name}}", data)
+
+	return nil
+}
diff --git a/internal/pkg/commands/services_test.go b/internal/pkg/commands/services_test.go
new file mode 100644
index 0000000..721b6dc
--- /dev/null
+++ b/internal/pkg/commands/services_test.go
@@ -0,0 +1,101 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"testing"
+)
+
+func TestServicesList(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"name": "onos",
+			"state": "present",
+			"version": "2.1.1-dev"
+		},
+		{
+			"name": "kubernetes",
+			"state": "present",
+			"version": "1.2.1"
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ServiceOpts
+	options.List.OutputAs = "json"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestServicesListTable(t *testing.T) {
+	// We'll use the ServicesList command to be our end-to-end test for
+	// table formatted commands, as the output is relatively simple.
+	expected := `NAME          VERSION      STATE
+onos          2.1.1-dev    present
+kubernetes    1.2.1        present
+`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ServiceOpts
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertStringEqual(t, got.String(), expected)
+}
+
+func TestServicesListYaml(t *testing.T) {
+	// We'll use the ServicesList command to be our end-to-end test for
+	// yaml formatted commands, as the output is relatively simple.
+	expected := `- name: onos
+  version: 2.1.1-dev
+  state: present
+- name: kubernetes
+  version: 1.2.1
+  state: present
+`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options ServiceOpts
+	options.List.OutputAs = "yaml"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertStringEqual(t, got.String(), expected)
+}
diff --git a/internal/pkg/commands/setup_test.go b/internal/pkg/commands/setup_test.go
new file mode 100644
index 0000000..3b1dc7b
--- /dev/null
+++ b/internal/pkg/commands/setup_test.go
@@ -0,0 +1,35 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"fmt"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"os"
+	"testing"
+)
+
+// This TestMain is global to all tests in the `commands` package
+
+func TestMain(m *testing.M) {
+	err := testutils.StartMockServer("data.json")
+	if err != nil {
+		fmt.Printf("Error when initializing mock server %v", err)
+		os.Exit(-1)
+	}
+	os.Exit(m.Run())
+}
diff --git a/internal/pkg/commands/status.go b/internal/pkg/commands/status.go
new file mode 100644
index 0000000..802720a
--- /dev/null
+++ b/internal/pkg/commands/status.go
@@ -0,0 +1,91 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	"github.com/fullstorydev/grpcurl"
+	flags "github.com/jessevdk/go-flags"
+	"github.com/jhump/protoreflect/dynamic"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"strings"
+)
+
+const StatusListFormat = "table{{ .Component }}\t{{ .Name }}\t{{ .Version }}\t{{ .Connection }}\t{{ .Status }}"
+
+type StatusListOpts struct {
+	ListOutputOptions
+	Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
+}
+
+type StatusOpts struct {
+	List StatusListOpts `command:"list"`
+}
+
+var statusOpts = StatusOpts{}
+
+func RegisterStatusCommands(parser *flags.Parser) {
+	parser.AddCommand("status", "status commands", "Commands to query status of various subsystems", &statusOpts)
+}
+
+func (options *StatusListOpts) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	var components []map[string]string
+
+	// TODO(smbaker): Consider using David's client-side filtering can be used so we get filtering
+	// by other fields (status, etc) for free.
+
+	if options.Filter == "" || strings.Contains(strings.ToLower(options.Filter), "database") {
+		h := &RpcEventHandler{}
+		err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.utility.GetDatabaseInfo", headers, h, h.GetParams)
+		if err != nil {
+			return corderrors.RpcErrorToCordError(err)
+		}
+
+		if h.Status != nil && h.Status.Err() != nil {
+			return corderrors.RpcErrorToCordError(h.Status.Err())
+		}
+
+		d, err := dynamic.AsDynamicMessage(h.Response)
+		if err != nil {
+			return err
+		}
+
+		db_map := make(map[string]string)
+		db_map["Component"] = "Database"
+		db_map["Name"] = d.GetFieldByName("name").(string)
+		db_map["Version"] = d.GetFieldByName("version").(string)
+		db_map["Connection"] = d.GetFieldByName("connection").(string)
+		db_map["Status"] = GetEnumValue(d, "status")
+
+		components = append(components, db_map)
+	}
+
+	FormatAndGenerateListOutput(&options.ListOutputOptions, StatusListFormat, StatusListFormat, components)
+
+	return nil
+}
diff --git a/internal/pkg/commands/status_test.go b/internal/pkg/commands/status_test.go
new file mode 100644
index 0000000..a7662fb
--- /dev/null
+++ b/internal/pkg/commands/status_test.go
@@ -0,0 +1,50 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"testing"
+)
+
+func TestStatusList(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"Component": "Database",
+			"Connection": "xos-db:5432",
+			"Name": "xos",
+			"Status": "OPERATIONAL",
+			"Version": "10.3"
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options StatusOpts
+	options.List.OutputAs = "json"
+	err := options.List.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
diff --git a/internal/pkg/commands/transfer.go b/internal/pkg/commands/transfer.go
new file mode 100644
index 0000000..c8fa89d
--- /dev/null
+++ b/internal/pkg/commands/transfer.go
@@ -0,0 +1,143 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	flags "github.com/jessevdk/go-flags"
+	corderrors "github.com/opencord/cordctl/internal/pkg/error"
+	"strings"
+)
+
+const (
+	DEFAULT_TRANSFER_FORMAT = "table{{ .Status }}\t{{ .Checksum }}\t{{ .Chunks }}\t{{ .Bytes }}"
+)
+
+type TransferOutput struct {
+	Status   string `json:"status"`
+	Checksum string `json:"checksum"`
+	Chunks   int    `json:"chunks"`
+	Bytes    int    `json:"bytes"`
+}
+
+type TransferUpload struct {
+	OutputOptions
+	ChunkSize int `short:"h" long:"chunksize" default:"65536" description:"Host and port"`
+	Args      struct {
+		LocalFileName string
+		URI           string
+	} `positional-args:"yes" required:"yes"`
+}
+
+type TransferDownload struct {
+	OutputOptions
+	Args struct {
+		URI           string
+		LocalFileName string
+	} `positional-args:"yes" required:"yes"`
+}
+
+type TransferOpts struct {
+	Upload   TransferUpload   `command:"upload"`
+	Download TransferDownload `command:"download"`
+}
+
+var transferOpts = TransferOpts{}
+
+func RegisterTransferCommands(parser *flags.Parser) {
+	parser.AddCommand("transfer", "file transfer commands", "Commands to transfer files to and from XOS", &transferOpts)
+}
+
+/* Command processors */
+
+func (options *TransferUpload) Execute(args []string) error {
+
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	local_name := options.Args.LocalFileName
+	uri := options.Args.URI
+
+	if IsFileUri(local_name) {
+		return corderrors.NewInvalidInputError("local_name argument should not be a uri")
+	}
+
+	if !IsFileUri(uri) {
+		return corderrors.NewInvalidInputError("uri argument should be a file:// uri")
+	}
+
+	h, upload_result, err := UploadFile(conn, descriptor, local_name, uri, options.ChunkSize)
+	if err != nil {
+		return err
+	}
+
+	if upload_result.GetFieldByName("checksum").(string) != h.GetChecksum() {
+		return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
+			Expected: h.GetChecksum(),
+			Actual:   upload_result.GetFieldByName("checksum").(string)})
+	}
+
+	data := make([]TransferOutput, 1)
+	data[0].Checksum = upload_result.GetFieldByName("checksum").(string)
+	data[0].Chunks = int(upload_result.GetFieldByName("chunks_received").(int32))
+	data[0].Bytes = int(upload_result.GetFieldByName("bytes_received").(int32))
+	data[0].Status = GetEnumValue(upload_result, "status")
+
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_TRANSFER_FORMAT, "{{.Status}}", data)
+
+	return nil
+}
+
+func IsFileUri(s string) bool {
+	return strings.HasPrefix(s, "file://")
+}
+
+func (options *TransferDownload) Execute(args []string) error {
+	conn, descriptor, err := InitClient(INIT_DEFAULT)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	local_name := options.Args.LocalFileName
+	uri := options.Args.URI
+
+	if IsFileUri(local_name) {
+		return corderrors.NewInvalidInputError("local_name argument should not be a uri")
+	}
+
+	if !IsFileUri(uri) {
+		return corderrors.NewInvalidInputError("uri argument should be a file:// uri")
+	}
+
+	h, err := DownloadFile(conn, descriptor, uri, local_name)
+	if err != nil {
+		return err
+	}
+
+	data := make([]TransferOutput, 1)
+	data[0].Chunks = h.chunks
+	data[0].Bytes = h.bytes
+	data[0].Status = h.status
+	data[0].Checksum = h.GetChecksum()
+
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_TRANSFER_FORMAT, "{{.Status}}", data)
+
+	return nil
+}
diff --git a/internal/pkg/commands/transfer_handler.go b/internal/pkg/commands/transfer_handler.go
new file mode 100644
index 0000000..cc18f1b
--- /dev/null
+++ b/internal/pkg/commands/transfer_handler.go
@@ -0,0 +1,167 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"context"
+	"crypto/sha256"
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	"github.com/golang/protobuf/proto"
+	"github.com/jhump/protoreflect/dynamic"
+	"google.golang.org/grpc"
+	"hash"
+	"io"
+	"os"
+)
+
+/* Handlers for streaming upload and download */
+
+type DownloadHandler struct {
+	RpcEventHandler
+	f      *os.File
+	chunks int
+	bytes  int
+	status string
+	hash   hash.Hash
+}
+
+type UploadHandler struct {
+	RpcEventHandler
+	chunksize int
+	f         *os.File
+	uri       string
+	hash      hash.Hash
+}
+
+func (h *DownloadHandler) OnReceiveResponse(m proto.Message) {
+	d, err := dynamic.AsDynamicMessage(m)
+	if err != nil {
+		h.status = "ERROR"
+		// TODO(smbaker): How to raise an exception?
+		return
+	}
+	chunk := d.GetFieldByName("chunk").(string)
+	io.WriteString(h.hash, chunk)
+	h.f.Write([]byte(chunk))
+	h.chunks += 1
+	h.bytes += len(chunk)
+}
+
+func (h *DownloadHandler) GetChecksum() string {
+	return fmt.Sprintf("sha256:%x", h.hash.Sum(nil))
+}
+
+func (h *UploadHandler) GetParams(msg proto.Message) error {
+	dmsg, err := dynamic.AsDynamicMessage(msg)
+	if err != nil {
+		return err
+	}
+
+	//fmt.Printf("streamer, MessageName: %s\n", dmsg.XXX_MessageName())
+
+	block := make([]byte, h.chunksize)
+	bytes_read, err := h.f.Read(block)
+
+	if err == io.EOF {
+		h.f.Close()
+		//fmt.Print("EOF\n")
+		return err
+	}
+
+	if err != nil {
+		//fmt.Print("ERROR!\n")
+		return err
+	}
+
+	chunk := string(block[:bytes_read])
+	io.WriteString(h.hash, chunk)
+
+	dmsg.TrySetFieldByName("uri", h.uri)
+	dmsg.TrySetFieldByName("chunk", chunk)
+
+	return nil
+}
+
+func (h *UploadHandler) GetChecksum() string {
+	return fmt.Sprintf("sha256:%x", h.hash.Sum(nil))
+}
+
+func UploadFile(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, local_name string, uri string, chunkSize int) (*UploadHandler, *dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	f, err := os.Open(local_name)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	h := &UploadHandler{uri: uri,
+		f:         f,
+		chunksize: chunkSize,
+		hash:      sha256.New()}
+
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.filetransfer/Upload", headers, h, h.GetParams)
+	if err != nil {
+		return nil, nil, err
+	}
+	if h.Status.Err() != nil {
+		return nil, nil, h.Status.Err()
+	}
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return h, d, err
+}
+
+func DownloadFile(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, uri string, local_name string) (*DownloadHandler, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	f, err := os.Create(local_name)
+	if err != nil {
+		return nil, err
+	}
+
+	dm := make(map[string]interface{})
+	dm["uri"] = uri
+
+	h := &DownloadHandler{
+		RpcEventHandler: RpcEventHandler{
+			Fields: map[string]map[string]interface{}{"xos.FileRequest": dm},
+		},
+		f:      f,
+		hash:   sha256.New(),
+		status: "SUCCESS"}
+
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.filetransfer/Download", headers, h, h.GetParams)
+	if err != nil {
+		return nil, err
+	}
+
+	if h.Status.Err() != nil {
+		return nil, h.Status.Err()
+	}
+
+	return h, err
+}
diff --git a/internal/pkg/commands/transfer_test.go b/internal/pkg/commands/transfer_test.go
new file mode 100644
index 0000000..b6a4dd2
--- /dev/null
+++ b/internal/pkg/commands/transfer_test.go
@@ -0,0 +1,83 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"github.com/opencord/cordctl/pkg/testutils"
+	"io/ioutil"
+	"testing"
+)
+
+func TestDownload(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"bytes": 6,
+			"checksum": "sha256:e9c0f8b575cbfcb42ab3b78ecc87efa3b011d9a5d10b09fa4e96f240bf6a82f5",
+			"chunks": 2,
+			"status": "SUCCESS"
+		}
+	]`
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options TransferOpts
+	options.Download.OutputAs = "json"
+	options.Download.Args.LocalFileName = "/tmp/transfer.down"
+	options.Download.Args.URI = "file:///tmp/transfer.down"
+	err := options.Download.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
+
+func TestUpload(t *testing.T) {
+	// use `python -m json.tool` to pretty-print json
+	expected := `[
+		{
+			"bytes": 6,
+			"checksum": "sha256:e9c0f8b575cbfcb42ab3b78ecc87efa3b011d9a5d10b09fa4e96f240bf6a82f5",
+			"chunks": 2,
+			"status": "SUCCESS"
+		}
+	]`
+
+	err := ioutil.WriteFile("/tmp/transfer.up", []byte("ABCDEF"), 0644)
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options TransferOpts
+	options.Upload.OutputAs = "json"
+	options.Upload.Args.LocalFileName = "/tmp/transfer.up"
+	options.Upload.Args.URI = "file:///tmp/transfer.up"
+	options.Upload.ChunkSize = 3
+	err = options.Upload.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	testutils.AssertJSONEqual(t, got.String(), expected)
+}
diff --git a/internal/pkg/commands/version.go b/internal/pkg/commands/version.go
new file mode 100644
index 0000000..c207d55
--- /dev/null
+++ b/internal/pkg/commands/version.go
@@ -0,0 +1,143 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	flags "github.com/jessevdk/go-flags"
+	"github.com/opencord/cordctl/internal/pkg/cli/version"
+	"github.com/opencord/cordctl/pkg/format"
+)
+
+type VersionDetails struct {
+	Version   string `json:"version"`
+	GoVersion string `json:"goversion"`
+	GitCommit string `json:"gitcommit"`
+	GitDirty  string `json:"gitdirty"`
+	BuildTime string `json:"buildtime"`
+	Os        string `json:"os"`
+	Arch      string `json:"arch"`
+}
+
+type CoreVersionDetails struct {
+	Version       string `json:"version"`
+	PythonVersion string `json:"goversion"`
+	GitCommit     string `json:"gitcommit"`
+	BuildTime     string `json:"buildtime"`
+	Os            string `json:"os"`
+	Arch          string `json:"arch"`
+	DjangoVersion string `json:"djangoversion"`
+}
+
+type VersionOutput struct {
+	Client VersionDetails     `json:"client"`
+	Server CoreVersionDetails `json:"server"`
+}
+
+type VersionOpts struct {
+	OutputAs   string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
+	ClientOnly bool   `short:"c" long:"client-only" description:"Print only client version"`
+}
+
+var versionOpts = VersionOpts{}
+
+var versionInfo = VersionOutput{
+	Client: VersionDetails{
+		Version:   version.Version,
+		GoVersion: version.GoVersion,
+		GitCommit: version.GitCommit,
+		GitDirty:  version.GitDirty,
+		Os:        version.Os,
+		Arch:      version.Arch,
+		BuildTime: version.BuildTime,
+	},
+	Server: CoreVersionDetails{
+		Version:       "unknown",
+		PythonVersion: "unknown",
+		GitCommit:     "unknown",
+		Os:            "unknown",
+		Arch:          "unknown",
+		BuildTime:     "unknown",
+		DjangoVersion: "unknown",
+	},
+}
+
+func RegisterVersionCommands(parent *flags.Parser) {
+	parent.AddCommand("version", "display version", "Display client version", &versionOpts)
+}
+
+const ClientFormat = `Client:
+ Version         {{.Client.Version}}
+ Go version:     {{.Client.GoVersion}}
+ Git commit:     {{.Client.GitCommit}}
+ Git dirty:      {{.Client.GitDirty}}
+ Built:          {{.Client.BuildTime}}
+ OS/Arch:        {{.Client.Os}}/{{.Client.Arch}}
+`
+const ServerFormat = `
+Server:
+ Version         {{.Server.Version}}
+ Python version: {{.Server.PythonVersion}}
+ Django version: {{.Server.DjangoVersion}}
+ Git commit:     {{.Server.GitCommit}}
+ Built:          {{.Server.BuildTime}}
+ OS/Arch:        {{.Server.Os}}/{{.Server.Arch}}
+`
+
+const DefaultFormat = ClientFormat + ServerFormat
+
+func (options *VersionOpts) Execute(args []string) error {
+	if !options.ClientOnly {
+		conn, descriptor, err := InitClient(INIT_NO_VERSION_CHECK)
+		if err != nil {
+			return err
+		}
+		defer conn.Close()
+
+		d, err := GetVersion(conn, descriptor)
+		if err != nil {
+			return err
+		}
+
+		versionInfo.Server.Version = d.GetFieldByName("version").(string)
+		versionInfo.Server.PythonVersion = d.GetFieldByName("pythonVersion").(string)
+		versionInfo.Server.GitCommit = d.GetFieldByName("gitCommit").(string)
+		versionInfo.Server.BuildTime = d.GetFieldByName("buildTime").(string)
+		versionInfo.Server.Os = d.GetFieldByName("os").(string)
+		versionInfo.Server.Arch = d.GetFieldByName("arch").(string)
+
+		// djangoVersion was added to GetVersion() in xos-core 3.3.1-dev
+		djangoVersion, err := d.TryGetFieldByName("djangoVersion")
+		if err == nil {
+			versionInfo.Server.DjangoVersion = djangoVersion.(string)
+		}
+	}
+
+	result := CommandResult{
+		// Format:   format.Format(DefaultFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     versionInfo,
+	}
+
+	if options.ClientOnly {
+		result.Format = format.Format(ClientFormat)
+	} else {
+		result.Format = format.Format(DefaultFormat)
+	}
+
+	GenerateOutput(&result)
+	return nil
+}
diff --git a/internal/pkg/commands/version_test.go b/internal/pkg/commands/version_test.go
new file mode 100644
index 0000000..e37b6d1
--- /dev/null
+++ b/internal/pkg/commands/version_test.go
@@ -0,0 +1,89 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 commands
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestVersionClientOnly(t *testing.T) {
+	expected := "" +
+		"Client:\n" +
+		" Version         unknown-version\n" +
+		" Go version:     unknown-goversion\n" +
+		" Git commit:     unknown-gitcommit\n" +
+		" Git dirty:      unknown-gitdirty\n" +
+		" Built:          unknown-buildtime\n" +
+		" OS/Arch:        unknown-os/unknown-arch\n" +
+		"\n"
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options VersionOpts
+	options.ClientOnly = true
+	err := options.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	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 TestVersionClientAndServer(t *testing.T) {
+	expected := "" +
+		"Client:\n" +
+		" Version         unknown-version\n" +
+		" Go version:     unknown-goversion\n" +
+		" Git commit:     unknown-gitcommit\n" +
+		" Git dirty:      unknown-gitdirty\n" +
+		" Built:          unknown-buildtime\n" +
+		" OS/Arch:        unknown-os/unknown-arch\n" +
+		"\n" +
+		"Server:\n" +
+		" Version         3.2.6\n" +
+		" Python version: 2.7.16 (default, May  6 2019, 19:35:26)\n" +
+		" Django version: unknown\n" +
+		" Git commit:     b0df1bf6ed1698285eda6a6725c5da0c80aa4aee\n" +
+		" Built:          2019-05-20T17:04:14Z\n" +
+		" OS/Arch:        linux/x86_64\n" +
+		"\n"
+
+	got := new(bytes.Buffer)
+	OutputStream = got
+
+	var options VersionOpts
+	err := options.Execute([]string{})
+
+	if err != nil {
+		t.Errorf("%s: Received error %v", t.Name(), err)
+		return
+	}
+
+	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())
+	}
+}
diff --git a/internal/pkg/completion/bash.go b/internal/pkg/completion/bash.go
new file mode 100644
index 0000000..4feb2a2
--- /dev/null
+++ b/internal/pkg/completion/bash.go
@@ -0,0 +1,45 @@
+/*
+ * 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 completion
+
+const Bash = `
+# Portions copyright 2019-present Open Networking Foundation
+# Original 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.
+#
+_cordctl() {
+    # All arguments except the first one
+    args=("${COMP_WORDS[@]:1:$COMP_CWORD}")
+    # Only split on newlines
+    local IFS=$'\n'
+    # Call completion (note that the first element of COMP_WORDS is
+    # the executable itself)
+    COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}"))
+    return 0
+}
+complete -F _cordctl cordctl
+`
diff --git a/internal/pkg/error/error.go b/internal/pkg/error/error.go
new file mode 100644
index 0000000..e35a14a
--- /dev/null
+++ b/internal/pkg/error/error.go
@@ -0,0 +1,435 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 error
+
+/*  Cordctl error classes
+
+	The basic idea is to throw specific error classes, so it's easier to test for them rather than doing string
+	comparisons or other ad hoc mechanisms for determining the type of error. This decouples the human
+	readable text of an error from programmatic testing of error type.
+
+	We differentiate between errors that we want to generate brief output, such as for example a
+	user mistyping a model name, versus errors that we want to generate additional context. This prevents
+	overwhelming a user with voluminous output for a simple mistake. A command-line option may be provided
+	to force full error output should it be desired.
+
+	Additionally, an added benefit is ease of maintenance and localisation, by locating all error text
+	in one place.
+
+	To return an error, for example:
+
+		return WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
+
+	To check to see if a specific error was returned, either of the following are acceptable:
+
+		_, ok := err.(*ChecksumMismatchError)
+		...
+
+		switch err.(type) {
+		case *ChecksumMismatchError:
+        ...
+*/
+
+import (
+	"bytes"
+	"fmt"
+	go_errors "github.com/go-errors/errors"
+	"google.golang.org/grpc/status"
+	"runtime"
+	"strings"
+)
+
+const (
+	MaxStackDepth = 50
+)
+
+/* CordCtlError is the interface for errors created by cordctl.
+ *    ShouldDumpStack()
+ *        Returns false for well-understood problems such as invalid user input where a brief error message is sufficient
+ *		  Returns true for poorly-understood / unexpected problems where a full dump context may be useful
+ *    Stack()
+ *        Returns a string containing the stack trace where the error occurred
+ *        Only useful if WithStackTrace() was called on the error
+ */
+
+type CordCtlError interface {
+	error
+	ShouldDumpStack() bool
+	Stack() string
+	AddStackTrace(skip int)
+}
+
+/* ObjectReference contains information about the object that the error applies to.
+   This may be empty (ModelName="") or it may contain a ModelName together with
+   option Id or Queries.
+*/
+
+type ObjectReference struct {
+	ModelName string
+	Id        int32
+	Queries   map[string]string
+}
+
+// Returns true if the reference is populated
+func (f *ObjectReference) IsValid() bool {
+	return (f.ModelName != "")
+}
+
+func (f *ObjectReference) String() string {
+	if !f.IsValid() {
+		// The reference is empty
+		return ""
+	}
+
+	if f.Queries != nil {
+		kv := make([]string, 0, len(f.Queries))
+		for k, v := range f.Queries {
+			kv = append(kv, fmt.Sprintf("%s%s", k, v))
+		}
+		return fmt.Sprintf("%s <%v>", f.ModelName, strings.Join(kv, ", "))
+	}
+
+	if f.Id > 0 {
+		return fmt.Sprintf("%s <id=%d>", f.ModelName, f.Id)
+	}
+
+	return fmt.Sprintf("%s", f.ModelName)
+}
+
+// Returns " on model ModelName [id]" if the reference is populated, or "" otherwise.
+func (f *ObjectReference) Clause() string {
+	if !f.IsValid() {
+		// The reference is empty
+		return ""
+	}
+
+	return fmt.Sprintf(" [on model %s]", f.String())
+}
+
+/* BaseError
+ *
+ * Supports attaching stack traces to errors
+ *    Borrowed the technique from github.com/go-errors. Decided against using go-errors directly since it requires
+ *    wrapping our error classes. Instead, incorporated the stack trace directly into our error class.
+ *
+ * Also supports encapsulating error messages, so that a CordError can encapsulate the error message from a
+ * function that was called.
+ */
+
+type BaseError struct {
+	Obj          ObjectReference
+	Encapsulated error                  // in case this error encapsulates an error from a lower level
+	stack        []uintptr              // for stack trace
+	frames       []go_errors.StackFrame // for stack trace
+}
+
+func (f *BaseError) AddStackTrace(skip int) {
+	stack := make([]uintptr, MaxStackDepth)
+	length := runtime.Callers(2+skip, stack[:])
+	f.stack = stack[:length]
+}
+
+func (f *BaseError) Stack() string {
+	buf := bytes.Buffer{}
+
+	for _, frame := range f.StackFrames() {
+		buf.WriteString(frame.String())
+	}
+
+	return string(buf.Bytes())
+}
+
+func (f *BaseError) StackFrames() []go_errors.StackFrame {
+	if f.frames == nil {
+		f.frames = make([]go_errors.StackFrame, len(f.stack))
+
+		for i, pc := range f.stack {
+			f.frames[i] = go_errors.NewStackFrame(pc)
+		}
+	}
+
+	return f.frames
+}
+
+// ***************************************************************************
+// UserError is composed into Errors that are due to user input
+
+type UserError struct {
+	BaseError
+}
+
+func (f UserError) ShouldDumpStack() bool {
+	return false
+}
+
+// **************************************************************************
+// TransferError is composed into Errors that are due to failures in transfers
+
+type TransferError struct {
+	BaseError
+}
+
+func (f TransferError) ShouldDumpStack() bool {
+	return false
+}
+
+// ***************************************************************************
+// UnexpectedError is things that we don't expect to happen. They should
+// generate maximum error context, to provide useful information for developer
+// diagnosis.
+
+type UnexpectedError struct {
+	BaseError
+}
+
+func (f UnexpectedError) ShouldDumpStack() bool {
+	return true
+}
+
+// ***************************************************************************
+// Specific error classes follow
+
+// Checksum mismatch when downloading or uploading a file
+type ChecksumMismatchError struct {
+	TransferError
+	Name     string // (optional) Name of file
+	Expected string
+	Actual   string
+}
+
+func (f ChecksumMismatchError) Error() string {
+	if f.Name != "" {
+		return fmt.Sprintf("%s: checksum mismatch (actual=%s, expected=%s)", f.Name, f.Expected, f.Actual)
+	} else {
+		return fmt.Sprintf("checksum mismatch (actual=%s, expected=%s)", f.Expected, f.Actual)
+	}
+}
+
+// User specified a model type that is not valid
+type UnknownModelTypeError struct {
+	UserError
+	Name string // Name of model
+}
+
+func (f UnknownModelTypeError) Error() string {
+	return fmt.Sprintf("Model %s does not exist. Use `cordctl modeltype list` to get a list of available models", f.Name)
+}
+
+// User specified a model state that is not valid
+type UnknownModelStateError struct {
+	UserError
+	Name string // Name of state
+}
+
+func (f UnknownModelStateError) Error() string {
+	return fmt.Sprintf("Model state %s does not exist", f.Name)
+}
+
+// Command requires a filter be specified
+type FilterRequiredError struct {
+	UserError
+}
+
+func (f FilterRequiredError) Error() string {
+	return "Filter required. Use either an ID, --filter, or --all to specify which models to operate on"
+}
+
+// Command was aborted by the user
+type AbortedError struct {
+	UserError
+}
+
+func (f AbortedError) Error() string {
+	return "Aborted"
+}
+
+// Command was aborted by the user
+type NoMatchError struct {
+	UserError
+}
+
+func (f NoMatchError) Error() string {
+	return "No Match"
+}
+
+// User specified a field name that is not valid
+type FieldDoesNotExistError struct {
+	UserError
+	ModelName string
+	FieldName string
+}
+
+func (f FieldDoesNotExistError) Error() string {
+	return fmt.Sprintf("Model %s does not have field %s", f.ModelName, f.FieldName)
+}
+
+// User specified a query string that is not properly formatted
+type IllegalQueryError struct {
+	UserError
+	Query string
+}
+
+func (f IllegalQueryError) Error() string {
+	return fmt.Sprintf("Illegal query string %s", f.Query)
+}
+
+// We failed to type convert something that we thought should have converted
+type TypeConversionError struct {
+	UnexpectedError
+	Source      string
+	Destination string
+}
+
+func (f TypeConversionError) Error() string {
+	return fmt.Sprintf("Failed to type convert from %s to %s", f.Source, f.Destination)
+}
+
+// Version did not match a constraint
+type VersionConstraintError struct {
+	UserError
+	Name       string
+	Version    string
+	Constraint string
+}
+
+func (f VersionConstraintError) Error() string {
+	return fmt.Sprintf("%s version %s did not match constraint '%s'", f.Name, f.Version, f.Constraint)
+}
+
+// A model was not found
+type ModelNotFoundError struct {
+	UserError
+}
+
+func (f ModelNotFoundError) Error() string {
+	return fmt.Sprintf("Not Found%s", f.Obj.Clause())
+}
+
+// Permission Denied
+type PermissionDeniedError struct {
+	UserError
+}
+
+func (f PermissionDeniedError) Error() string {
+	return fmt.Sprintf("Permission Denied%s. Please verify username and password are correct", f.Obj.Clause())
+}
+
+// InvalidInputError is a catch-all for user mistakes that aren't covered elsewhere
+type InvalidInputError struct {
+	UserError
+	Message string
+}
+
+func (f InvalidInputError) Error() string {
+	return fmt.Sprintf("%s", f.Message)
+}
+
+func NewInvalidInputError(format string, params ...interface{}) *InvalidInputError {
+	msg := fmt.Sprintf(format, params...)
+	err := &InvalidInputError{Message: msg}
+	err.AddStackTrace(2)
+	return err
+}
+
+// InternalError is a catch-all for errors that don't fit somewhere else
+type InternalError struct {
+	UnexpectedError
+	Message string
+}
+
+func (f InternalError) Error() string {
+	return fmt.Sprintf("Internal Error%s: %s", f.Obj.Clause(), f.Message)
+}
+
+func NewInternalError(format string, params ...interface{}) *InternalError {
+	msg := fmt.Sprintf(format, params...)
+	err := &InternalError{Message: msg}
+	err.AddStackTrace(2)
+	return err
+}
+
+// ***************************************************************************
+// Global exported function declarations
+
+// Attach a stack trace to an error. The error passed in must be a pointer to an error structure for the
+// CordCtlError interface to match.
+func WithStackTrace(err CordCtlError) error {
+	err.AddStackTrace(2)
+	return err
+}
+
+/* RpcErrorWithObjToCordError
+ *
+ * Convert an RPC error into a Cord Error. The ObjectReference allows methods to attach
+ * object-related information to the error, and this varies by method. For example the Delete()
+ * method comes with an ModelName and an Id. The List() method has only a ModelName.
+ *
+ * Stubs (RpcErrorWithModelNameToCordError) are provided below to make common usage more convenient.
+ */
+
+func RpcErrorWithObjToCordError(err error, obj ObjectReference) error {
+	if err == nil {
+		return err
+	}
+
+	st, ok := status.FromError(err)
+	if ok {
+		switch st.Code().String() {
+		case "PermissionDenied":
+			cordErr := &PermissionDeniedError{}
+			cordErr.Obj = obj
+			cordErr.Encapsulated = err
+			cordErr.AddStackTrace(2)
+			return cordErr
+		case "NotFound":
+			cordErr := &ModelNotFoundError{}
+			cordErr.Obj = obj
+			cordErr.Encapsulated = err
+			cordErr.AddStackTrace(2)
+			return cordErr
+		case "Unknown":
+			msg := st.Message()
+			if strings.HasPrefix(msg, "Exception calling application: ") {
+				msg = msg[31:]
+			}
+			cordErr := &InternalError{Message: msg}
+			cordErr.Obj = obj
+			cordErr.Encapsulated = err
+			cordErr.AddStackTrace(2)
+			return cordErr
+		}
+	}
+
+	return err
+}
+
+func RpcErrorToCordError(err error) error {
+	return RpcErrorWithObjToCordError(err, ObjectReference{})
+}
+
+func RpcErrorWithModelNameToCordError(err error, modelName string) error {
+	return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName})
+}
+
+func RpcErrorWithIdToCordError(err error, modelName string, id int32) error {
+	return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName, Id: id})
+}
+
+func RpcErrorWithQueriesToCordError(err error, modelName string, queries map[string]string) error {
+	return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName, Queries: queries})
+}
diff --git a/internal/pkg/error/error_test.go b/internal/pkg/error/error_test.go
new file mode 100644
index 0000000..3030e0d
--- /dev/null
+++ b/internal/pkg/error/error_test.go
@@ -0,0 +1,149 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 error
+
+import (
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	"testing"
+)
+
+func TestGenericError(t *testing.T) {
+	var err error
+
+	err = fmt.Errorf("Some error")
+
+	// Type conversion from `error` to ChecksumMismatchError should fail
+	_, ok := err.(ChecksumMismatchError)
+	assert.False(t, ok)
+
+	// Type conversion from `error` to CordCtlError should fail
+	_, ok = err.(CordCtlError)
+	assert.False(t, ok)
+}
+
+func TestChecksumMismatchError(t *testing.T) {
+	var err error
+
+	err = WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
+
+	// Check that the Error() function returns the right text
+	assert.Equal(t, err.Error(), "checksum mismatch (actual=456, expected=123)")
+
+	// Type conversion from `error` to ChecksumMismatchError should succeed
+	_, ok := err.(*ChecksumMismatchError)
+	assert.True(t, ok)
+
+	// Type switch is another way of doing the same
+	switch err.(type) {
+	case *ChecksumMismatchError:
+		// do nothing
+	case CordCtlError:
+		assert.Fail(t, "Should have used the ChecksumMismatchError case instead")
+	default:
+		assert.Fail(t, "Wrong part of switch statement was called")
+	}
+
+	// Type conversion from `error` to CordCtlError should succeed
+	cce, ok := err.(CordCtlError)
+	assert.True(t, ok)
+
+	// ShouldDumpStack() returned from a ChecksumMismatchError should be false
+	assert.False(t, cce.ShouldDumpStack())
+}
+
+func TestUnknownModelTypeError(t *testing.T) {
+	var err error
+
+	err = WithStackTrace(&UnknownModelTypeError{Name: "foo"})
+
+	// Check that the Error() function returns the right text
+	assert.Equal(t, err.Error(), "Model foo does not exist. Use `cordctl modeltype list` to get a list of available models")
+}
+
+func TestRpcErrorToCordError(t *testing.T) {
+	// InternalError
+	err := status.Error(codes.Unknown, "A fake Unknown error")
+
+	cordErr := RpcErrorToCordError(err)
+
+	_, ok := cordErr.(*InternalError)
+	assert.True(t, ok)
+	assert.Equal(t, cordErr.Error(), "Internal Error: A fake Unknown error")
+
+	// NotFound
+	err = status.Error(codes.NotFound, "A fake not found error")
+
+	cordErr = RpcErrorToCordError(err)
+
+	_, ok = cordErr.(*ModelNotFoundError)
+	assert.True(t, ok)
+	assert.Equal(t, cordErr.Error(), "Not Found")
+
+	// PermissionDeniedError
+	err = status.Error(codes.PermissionDenied, "A fake Permission error")
+
+	cordErr = RpcErrorToCordError(err)
+
+	_, ok = cordErr.(*PermissionDeniedError)
+	assert.True(t, ok)
+	assert.Equal(t, cordErr.Error(), "Permission Denied. Please verify username and password are correct")
+}
+
+func TestRpcErrorWithModelNameToCordError(t *testing.T) {
+	// InternalError
+	err := status.Error(codes.Unknown, "A fake Unknown error")
+
+	cordErr := RpcErrorWithModelNameToCordError(err, "Foo")
+
+	_, ok := cordErr.(*InternalError)
+	assert.True(t, ok)
+	assert.Equal(t, cordErr.Error(), "Internal Error [on model Foo]: A fake Unknown error")
+}
+
+func TestRpcErrorWithIdToCordError(t *testing.T) {
+	// InternalError
+	err := status.Error(codes.Unknown, "A fake Unknown error")
+
+	cordErr := RpcErrorWithIdToCordError(err, "Foo", 7)
+
+	_, ok := cordErr.(*InternalError)
+	assert.True(t, ok)
+	assert.Equal(t, cordErr.Error(), "Internal Error [on model Foo <id=7>]: A fake Unknown error")
+}
+
+func TestRpcErrorWithQueriesToCordError(t *testing.T) {
+	// InternalError
+	err := status.Error(codes.Unknown, "A fake Unknown error")
+
+	cordErr := RpcErrorWithQueriesToCordError(err, "Foo", map[string]string{"id": "=3"})
+
+	_, ok := cordErr.(*InternalError)
+	assert.True(t, ok)
+	assert.Equal(t, cordErr.Error(), "Internal Error [on model Foo <id=3>]: A fake Unknown error")
+}
+
+func TestStackTrace(t *testing.T) {
+	var err error
+
+	err = WithStackTrace(&UnknownModelTypeError{Name: "foo"})
+
+	// goexit occurs near the end of the stack trace
+	assert.Contains(t, err.(CordCtlError).Stack(), "goexit")
+}