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

Change-Id: I3dc37d6f155661a2635fb4c95cf42b2aa81035e8
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),