SEBA-580 Add backup commands;
Retrieve server version;
Show available models

Change-Id: I3dc37d6f155661a2635fb4c95cf42b2aa81035e8
diff --git a/Makefile b/Makefile
index 8ed55fc..297c16b 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,7 @@
 	go build $(LDFLAGS) cmd/cordctl.go
 
 dependencies:
-	dep ensure
+	[ -d "vendor" ] || dep ensure
 
 lint: dependencies
 	find $(GOPATH)/src/github.com/opencord/cordctl -name "*.go" -not -path '$(GOPATH)/src/github.com/opencord/cordctl/vendor/*' | xargs gofmt -l
@@ -37,3 +37,8 @@
 	go-junit-report < ./tests/results/go-test-results.out > ./tests/results/go-test-results.xml ;\
 	gocover-cobertura < ./tests/results/go-test-coverage.out > ./tests/results/go-test-coverage.xml ;\
 	exit $$RETURN
+
+clean:
+	rm -f cordctl
+	rm -rf vendor
+	rm -f Gopkg.lock
diff --git a/README.md b/README.md
index 5a1b8e3..346a244 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,38 @@
-TODO: This readme will discuss cordctl
+# cordctl
+---
 
-# Development Environment
+`cordctl` is a command-line tool for interacting with XOS. XOS is part of the SEBA NEM and part of CORD, so by extension this tool is useful for interacting with SEBA and CORD deployments. `cordctl` makes use of gRPC to connect to XOS and may by used for administration of a remote pod, assuming the appropriate firewall rules are configured. Typically XOS exposes its gRPC API on port `30011`.
+
+## Configuration
+
+Typically a configuration file should be placed at `~/.cord/config` as `cordctl` will automatically look in that location. Alternatively, the `-c` command-line option may be used to specify a different config file location. Below is a sample config file:
+
+```yaml
+server: 10.201.101.33:30011
+username: admin@opencord.org
+password: letmein
+grpc:
+  timeout: 10s
+```
+
+The `server`, `username`, and `password` parameters are essential to configure access to the XOS container running on your pod. 
+
+## Getting Help
+
+The `-h` option can be used at multiple levels to get help, for example:
+
+```bash
+# Show help for global options
+./cordctl -h
+
+# Show help for model-related commands
+./cordctl model -h
+
+# Show help for the model list command
+./cordctl model list -h
+```
+
+## Development Environment
 
 To run unit tests, `go-junit-report` and `gocover-obertura` tools must be installed. One way to do this is to install them with `go get`, and then ensure your `GOPATH` is part of your `PATH` (editing your `~/.profile` as necessary). 
 
diff --git a/cmd/cordctl.go b/cmd/cordctl.go
index 7648182..b4e3c0d 100644
--- a/cmd/cordctl.go
+++ b/cmd/cordctl.go
@@ -30,6 +30,7 @@
 	if err != nil {
 		panic(err)
 	}
+	commands.RegisterBackupCommands(parser)
 	commands.RegisterModelCommands(parser)
 	commands.RegisterServiceCommands(parser)
 	commands.RegisterTransferCommands(parser)
diff --git a/commands/backup.go b/commands/backup.go
new file mode 100644
index 0000000..d5ae100
--- /dev/null
+++ b/commands/backup.go
@@ -0,0 +1,241 @@
+/*
+ * 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 (
+	"errors"
+	"fmt"
+	flags "github.com/jessevdk/go-flags"
+	"github.com/opencord/cordctl/format"
+	"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:"Host and port"`
+	Args      struct {
+		LocalFileName string
+	} `positional-args:"yes" required:"yes"`
+}
+
+type BackupRestore struct {
+	OutputOptions
+	Args struct {
+		LocalFileName string
+	} `positional-args:"yes" required:"yes"`
+}
+
+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 := InitReflectionClient()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	// 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(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 errors.New("BackupOp status is " + status)
+	}
+
+	// STEP 3: Retrieve URI
+	backupfile_id := completed_backupop.GetFieldByName("file_id").(int32)
+	if backupfile_id == 0 {
+		return errors.New("BackupOp.file_id is not set")
+	}
+
+	completed_backupfile, err := GetModel(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: Show results
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = DEFAULT_BACKUP_FORMAT
+	}
+	if options.Quiet {
+		outputFormat = "{{.Status}}"
+	}
+
+	data := make([]BackupOutput, 1)
+	data[0].Chunks = h.chunks
+	data[0].Bytes = h.bytes
+	data[0].Status = h.status
+	data[0].Checksum = fmt.Sprintf("sha1:%x", h.hash.Sum(nil))
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+	}
+
+	GenerateOutput(&result)
+
+	return nil
+}
+
+func (options *BackupRestore) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	local_name := options.Args.LocalFileName
+	remote_name := "cordctl-restore-" + time.Now().Format("20060102T150405Z")
+	uri := "file:///var/run/xos/backup/local/" + remote_name
+
+	// STEP 1: Upload the file
+
+	upload_result, err := UploadFile(conn, descriptor, local_name, uri, 65536)
+	if err != nil {
+		return err
+	}
+
+	upload_status := GetEnumValue(upload_result, "status")
+	if upload_status != "SUCCESS" {
+		return errors.New("Upload status was " + upload_status)
+	}
+
+	// STEP 2: Create a BackupFile object
+	backupfile := make(map[string]interface{})
+	backupfile["name"] = remote_name
+	backupfile["uri"] = uri
+	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)
+	conn, completed_backupop, err := FindModelWithRetry(conn, descriptor, "BackupOperation", "uuid", backupop["uuid"].(string), flags)
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	conditional_printf(!options.Quiet, "\n")
+
+	// STEP 5: Show results
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = DEFAULT_BACKUP_FORMAT
+	}
+	if options.Quiet {
+		outputFormat = "{{.Status}}"
+	}
+
+	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"
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+	}
+
+	GenerateOutput(&result)
+
+	return nil
+}
diff --git a/commands/command.go b/commands/command.go
index 329fee1..9741e51 100644
--- a/commands/command.go
+++ b/commands/command.go
@@ -52,17 +52,15 @@
 }
 
 type GlobalConfigSpec struct {
-	ApiVersion string        `yaml:"apiVersion"`
-	Server     string        `yaml:"server"`
-	Username   string        `yaml:"username"`
-	Password   string        `yaml:"password"`
-	Tls        TlsConfigSpec `yaml:"tls"`
-	Grpc       GrpcConfigSpec
+	Server   string        `yaml:"server"`
+	Username string        `yaml:"username"`
+	Password string        `yaml:"password"`
+	Tls      TlsConfigSpec `yaml:"tls"`
+	Grpc     GrpcConfigSpec
 }
 
 var GlobalConfig = GlobalConfigSpec{
-	ApiVersion: "v1",
-	Server:     "localhost",
+	Server: "localhost",
 	Tls: TlsConfigSpec{
 		UseTls: false,
 	},
@@ -72,17 +70,16 @@
 }
 
 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"`
-	ApiVersion string `short:"a" long:"apiversion" description:"API version" value-name:"VERSION" choice:"v1" choice:"v2"`
-	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"`
+	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"`
+	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"`
 }
 
 type OutputOptions struct {
@@ -111,8 +108,7 @@
 }
 
 type config struct {
-	ApiVersion string `yaml:"apiVersion"`
-	Server     string `yaml:"server"`
+	Server string `yaml:"server"`
 }
 
 func NewConnection() (*grpc.ClientConn, error) {
@@ -144,9 +140,6 @@
 	if GlobalOptions.Server != "" {
 		GlobalConfig.Server = GlobalOptions.Server
 	}
-	if GlobalOptions.ApiVersion != "" {
-		GlobalConfig.ApiVersion = GlobalOptions.ApiVersion
-	}
 	if GlobalOptions.Username != "" {
 		GlobalConfig.Username = GlobalOptions.Username
 	}
diff --git a/commands/common.go b/commands/common.go
index 493b302..9461f99 100644
--- a/commands/common.go
+++ b/commands/common.go
@@ -17,6 +17,12 @@
 
 import (
 	b64 "encoding/base64"
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	"github.com/jhump/protoreflect/grpcreflect"
+	"golang.org/x/net/context"
+	"google.golang.org/grpc"
+	reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
 )
 
 func GenerateHeaders() []string {
@@ -26,3 +32,33 @@
 	headers := []string{"authorization: basic " + sEnc}
 	return headers
 }
+
+func InitReflectionClient() (*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()
+
+	descriptor := grpcurl.DescriptorSourceFromServer(context.Background(), refClient)
+
+	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...)
+	}
+}
diff --git a/commands/handler.go b/commands/handler.go
index 01fb091..8aadb44 100644
--- a/commands/handler.go
+++ b/commands/handler.go
@@ -58,11 +58,13 @@
 	//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
 	}
 
diff --git a/commands/models.go b/commands/models.go
index 11067e7..92a7b2c 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -20,28 +20,34 @@
 	"context"
 	"fmt"
 	"github.com/fullstorydev/grpcurl"
-	pbdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
 	"github.com/opencord/cordctl/format"
+	"sort"
 	"strings"
 )
 
 const (
-	DEFAULT_MODEL_FORMAT = "table{{ .id }}\t{{ .name }}"
+	DEFAULT_MODEL_AVAILABLE_FORMAT = "{{ . }}"
 )
 
 type ModelList struct {
 	OutputOptions
-	ShowHidden   bool `long:"showhidden" description:"Show hidden fields in default output"`
-	ShowFeedback bool `long:"showfeedback" description:"Show feedback fields in default output"`
-	Args         struct {
+	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"`
+	Args            struct {
 		ModelName string
 	} `positional-args:"yes" required:"yes"`
 }
 
+type ModelAvailable struct {
+	OutputOptions
+}
+
 type ModelOpts struct {
-	List ModelList `command:"list"`
+	List      ModelList      `command:"list"`
+	Available ModelAvailable `command:"available"`
 }
 
 var modelOpts = ModelOpts{}
@@ -50,23 +56,57 @@
 	parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
 }
 
-func (options *ModelList) Execute(args []string) error {
-
-	conn, err := NewConnection()
+func (options *ModelAvailable) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
 	if err != nil {
 		return err
 	}
+
 	defer conn.Close()
 
-	// TODO: Validate ModelName
-
-	method_name := "xos.xos/List" + options.Args.ModelName
-
-	descriptor, method, err := GetReflectionMethod(conn, method_name)
+	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)
+
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = DEFAULT_MODEL_AVAILABLE_FORMAT
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     model_names,
+	}
+
+	GenerateOutput(&result)
+
+	return nil
+}
+
+func (options *ModelList) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, options.Args.ModelName)
+	if err != nil {
+		return err
+	}
+
+	method := "xos.xos/List" + options.Args.ModelName
+
 	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
 	defer cancel()
 
@@ -99,7 +139,6 @@
 		data[i] = make(map[string]interface{})
 		for _, field_desc := range val.GetKnownFields() {
 			field_name := field_desc.GetName()
-			field_type := field_desc.GetType()
 
 			isGuiHidden := strings.Contains(field_desc.GetFieldOptions().String(), "1005:1")
 			isFeedback := strings.Contains(field_desc.GetFieldOptions().String(), "1006:1")
@@ -113,7 +152,7 @@
 				continue
 			}
 
-			if isBookkeeping {
+			if isBookkeeping && (!options.ShowBookkeeping) {
 				continue
 			}
 
@@ -121,16 +160,7 @@
 				continue
 			}
 
-			switch field_type {
-			case pbdescriptor.FieldDescriptorProto_TYPE_STRING:
-				data[i][field_name] = val.GetFieldByName(field_name).(string)
-			case pbdescriptor.FieldDescriptorProto_TYPE_INT32:
-				data[i][field_name] = val.GetFieldByName(field_name).(int32)
-			case pbdescriptor.FieldDescriptorProto_TYPE_BOOL:
-				data[i][field_name] = val.GetFieldByName(field_name).(bool)
-				//				case pbdescriptor.FieldDescriptorProto_TYPE_DOUBLE:
-				//					data[i][field_name] = val.GetFieldByName(field_name).(double)
-			}
+			data[i][field_name] = val.GetFieldByName(field_name)
 
 			field_names[field_name] = true
 		}
diff --git a/commands/orm.go b/commands/orm.go
new file mode 100644
index 0000000..8788e8a
--- /dev/null
+++ b/commands/orm.go
@@ -0,0 +1,299 @@
+/*
+ * 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"
+	"errors"
+	"fmt"
+	"github.com/fullstorydev/grpcurl"
+	"github.com/jhump/protoreflect/dynamic"
+	"google.golang.org/grpc"
+	"strings"
+	"time"
+)
+
+const GM_QUIET = 1
+const GM_UNTIL_FOUND = 2
+const GM_UNTIL_ENACTED = 4
+const GM_UNTIL_STATUS = 8
+
+// 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 errors.New("Model " + name + " does not exist. Use `cordctl models available` to get a list of available models")
+	}
+	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 err
+	} else if h.Status != nil && h.Status.Err() != nil {
+		return h.Status.Err()
+	}
+
+	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
+}
+
+// Get a model from XOS given its ID
+func GetModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) (*dynamic.Message, 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.Get"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return nil, err
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, 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(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(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(".")
+				}
+				time.Sleep(100 * time.Millisecond)
+				conn.Close()
+				conn = nil
+				continue
+			}
+
+			if until_found && strings.Contains(err.Error(), "rpc error: code = NotFound") {
+				if !quiet {
+					fmt.Print("x")
+				}
+				time.Sleep(100 * time.Millisecond)
+				continue
+			}
+			return nil, nil, err
+		}
+
+		if until_enacted && !IsEnacted(model) {
+			if !quiet {
+				fmt.Print("o")
+			}
+			time.Sleep(100 * time.Millisecond)
+			continue
+		}
+
+		if until_status && model.GetFieldByName("status") == nil {
+			if !quiet {
+				fmt.Print("O")
+			}
+			time.Sleep(100 * time.Millisecond)
+			continue
+		}
+
+		return conn, model, nil
+	}
+}
+
+// Get a model from XOS given a fieldName/fieldValue
+func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fieldName string, fieldValue string) (*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	// TODO(smbaker): Implement filter the right way
+
+	h := &RpcEventHandler{}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.List"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return nil, err
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, h.Status.Err()
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, err
+	}
+
+	items, err := d.TryGetFieldByName("items")
+	if err != nil {
+		return nil, err
+	}
+
+	for _, item := range items.([]interface{}) {
+		val := item.(*dynamic.Message)
+
+		if val.GetFieldByName(fieldName).(string) == fieldValue {
+			return val, nil
+		}
+
+	}
+
+	return nil, errors.New("rpc error: code = NotFound")
+}
+
+// Find a model, but retry under a variety of circumstances
+func FindModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fieldName string, fieldValue 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(conn, descriptor, modelName, fieldName, fieldValue)
+		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(".")
+				}
+				time.Sleep(100 * time.Millisecond)
+				conn.Close()
+				conn = nil
+				continue
+			}
+
+			if until_found && strings.Contains(err.Error(), "rpc error: code = NotFound") {
+				if !quiet {
+					fmt.Print("x")
+				}
+				time.Sleep(100 * time.Millisecond)
+				continue
+			}
+			return nil, nil, err
+		}
+
+		if until_enacted && !IsEnacted(model) {
+			if !quiet {
+				fmt.Print("o")
+			}
+			time.Sleep(100 * time.Millisecond)
+			continue
+		}
+
+		if until_status && model.GetFieldByName("status") == nil {
+			if !quiet {
+				fmt.Print("O")
+			}
+			time.Sleep(100 * time.Millisecond)
+			continue
+		}
+
+		return conn, model, nil
+	}
+}
+
+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
+}
+
+func IsEnacted(d *dynamic.Message) bool {
+	enacted := d.GetFieldByName("enacted").(float64)
+	updated := d.GetFieldByName("updated").(float64)
+
+	return (enacted >= updated)
+}
diff --git a/commands/transfer.go b/commands/transfer.go
index 8a42af3..f41a8b8 100644
--- a/commands/transfer.go
+++ b/commands/transfer.go
@@ -17,16 +17,9 @@
 package commands
 
 import (
-	"context"
 	"errors"
-	"fmt"
-	"github.com/fullstorydev/grpcurl"
-	"github.com/golang/protobuf/proto"
 	flags "github.com/jessevdk/go-flags"
-	"github.com/jhump/protoreflect/dynamic"
 	"github.com/opencord/cordctl/format"
-	"io"
-	"os"
 	"strings"
 )
 
@@ -69,69 +62,11 @@
 	parser.AddCommand("transfer", "file transfer commands", "Commands to transfer files to and from XOS", &transferOpts)
 }
 
-/* Handlers for streaming upload and download */
-
-type DownloadHandler struct {
-	RpcEventHandler
-	f      *os.File
-	chunks int
-	bytes  int
-	status string
-}
-
-type UploadHandler struct {
-	RpcEventHandler
-	chunksize int
-	f         *os.File
-	uri       string
-}
-
-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)
-	h.f.Write([]byte(chunk))
-	h.chunks += 1
-	h.bytes += len(chunk)
-}
-
-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
-	}
-
-	dmsg.TrySetFieldByName("uri", h.uri)
-	dmsg.TrySetFieldByName("chunk", string(block[:bytes_read]))
-
-	return nil
-}
-
 /* Command processors */
 
 func (options *TransferUpload) Execute(args []string) error {
 
-	conn, err := NewConnection()
+	conn, descriptor, err := InitReflectionClient()
 	if err != nil {
 		return err
 	}
@@ -140,31 +75,15 @@
 	local_name := options.Args.LocalFileName
 	uri := options.Args.URI
 
-	descriptor, method, err := GetReflectionMethod(conn, "xos.filetransfer/Upload")
-	if err != nil {
-		return err
+	if IsFileUri(local_name) {
+		return errors.New("local_name argument should not be a uri")
 	}
 
-	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
-	defer cancel()
-
-	headers := GenerateHeaders()
-
-	f, err := os.Open(local_name)
-	if err != nil {
-		return err
+	if !IsFileUri(uri) {
+		return errors.New("uri argument should be a file:// uri")
 	}
 
-	h := &UploadHandler{uri: uri, f: f, chunksize: options.ChunkSize}
-
-	err = grpcurl.InvokeRPC(ctx, descriptor, conn, method, headers, h, h.GetParams)
-	if err != nil {
-		return err
-	}
-	d, err := dynamic.AsDynamicMessage(h.Response)
-	if err != nil {
-		return err
-	}
+	d, err := UploadFile(conn, descriptor, local_name, uri, options.ChunkSize)
 
 	outputFormat := CharReplacer.Replace(options.Format)
 	if outputFormat == "" {
@@ -196,8 +115,7 @@
 }
 
 func (options *TransferDownload) Execute(args []string) error {
-
-	conn, err := NewConnection()
+	conn, descriptor, err := InitReflectionClient()
 	if err != nil {
 		return err
 	}
@@ -214,34 +132,7 @@
 		return errors.New("uri argument should be a file:// uri")
 	}
 
-	descriptor, method, err := GetReflectionMethod(conn, "xos.filetransfer/Download")
-	if err != nil {
-		return err
-	}
-
-	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
-	defer cancel()
-
-	headers := GenerateHeaders()
-
-	f, err := os.Create(local_name)
-	if err != nil {
-		return err
-	}
-
-	dm := make(map[string]interface{})
-	dm["uri"] = uri
-
-	h := &DownloadHandler{
-		RpcEventHandler: RpcEventHandler{
-			Fields: map[string]map[string]interface{}{"xos.FileRequest": dm},
-		},
-		f:      f,
-		chunks: 0,
-		bytes:  0,
-		status: "SUCCESS"}
-
-	err = grpcurl.InvokeRPC(ctx, descriptor, conn, method, headers, h, h.GetParams)
+	h, err := DownloadFile(conn, descriptor, uri, local_name)
 	if err != nil {
 		return err
 	}
diff --git a/commands/transfer_handler.go b/commands/transfer_handler.go
new file mode 100644
index 0000000..0fce0f6
--- /dev/null
+++ b/commands/transfer_handler.go
@@ -0,0 +1,144 @@
+/*
+ * 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/sha1"
+	"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
+}
+
+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 *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
+	}
+
+	dmsg.TrySetFieldByName("uri", h.uri)
+	dmsg.TrySetFieldByName("chunk", string(block[:bytes_read]))
+
+	return nil
+}
+
+func UploadFile(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, local_name string, uri string, chunkSize int) (*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, err
+	}
+
+	h := &UploadHandler{uri: uri, f: f, chunksize: chunkSize}
+
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.filetransfer/Upload", headers, h, h.GetParams)
+	if err != nil {
+		return nil, err
+	}
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, err
+	}
+
+	return 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:   sha1.New(),
+		status: "SUCCESS"}
+
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.filetransfer/Download", headers, h, h.GetParams)
+	if err != nil {
+		return nil, err
+	}
+
+	return h, err
+}
diff --git a/commands/version.go b/commands/version.go
index 7f7daf4..9cc8b05 100644
--- a/commands/version.go
+++ b/commands/version.go
@@ -17,7 +17,10 @@
 package commands
 
 import (
+	"context"
+	"github.com/fullstorydev/grpcurl"
 	flags "github.com/jessevdk/go-flags"
+	"github.com/jhump/protoreflect/dynamic"
 	"github.com/opencord/cordctl/cli/version"
 	"github.com/opencord/cordctl/format"
 )
@@ -31,8 +34,18 @@
 	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"`
+}
+
 type VersionOutput struct {
-	Client VersionDetails `json:"client"`
+	Client VersionDetails     `json:"client"`
+	Server CoreVersionDetails `json:"server"`
 }
 
 type VersionOpts struct {
@@ -50,6 +63,14 @@
 		Arch:      version.Arch,
 		BuildTime: version.BuildTime,
 	},
+	Server: CoreVersionDetails{
+		Version:       "unknown",
+		PythonVersion: "unknown",
+		GitCommit:     "unknown",
+		Os:            "unknown",
+		Arch:          "unknown",
+		BuildTime:     "unknown",
+	},
 }
 
 func RegisterVersionCommands(parent *flags.Parser) {
@@ -57,14 +78,59 @@
 }
 
 const DefaultFormat = `Client:
- Version        {{.Client.Version}}
- Go version:    {{.Client.GoVersion}}
- Git commit:    {{.Client.GitCommit}}
- Built:         {{.Client.BuildTime}}
- OS/Arch:       {{.Client.Os}}/{{.Client.Arch}}
+ Version         {{.Client.Version}}
+ Go version:     {{.Client.GoVersion}}
+ Git commit:     {{.Client.GitCommit}}
+ Built:          {{.Client.BuildTime}}
+ OS/Arch:        {{.Client.Os}}/{{.Client.Arch}}
+
+Server:
+ Version         {{.Server.Version}}
+ Python version: {{.Server.PythonVersion}}
+ Git commit:     {{.Server.GitCommit}}
+ Built:          {{.Server.BuildTime}}
+ OS/Arch:        {{.Server.Os}}/{{.Server.Arch}}
 `
 
 func (options *VersionOpts) Execute(args []string) error {
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	descriptor, method, err := GetReflectionMethod(conn, "xos.utility.GetVersion")
+	if err != nil {
+		return err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{}
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, method, headers, h, h.GetParams)
+	if err != nil {
+		return err
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return h.Status.Err()
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	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)
+
 	result := CommandResult{
 		Format:   format.Format(DefaultFormat),
 		OutputAs: toOutputType(options.OutputAs),
diff --git a/cordctl.config b/cordctl.config
index e9ce437..f2549d6 100644
--- a/cordctl.config
+++ b/cordctl.config
@@ -1,4 +1,3 @@
-apiVersion: v1
 server: 10.201.101.33:30011
 username: admin@opencord.org
 password: letmein