SEBA-672 model update commands

Change-Id: I7fef3a4c1ee5ccb8c33a01f37a772142a82249c1
diff --git a/README.md b/README.md
index 22361e6..88d476a 100644
--- a/README.md
+++ b/README.md
@@ -23,13 +23,13 @@
 
 ```bash
 # Show help for global options
-./cordctl -h
+cordctl -h
 
 # Show help for model-related commands
-./cordctl model -h
+cordctl model -h
 
 # Show help for the model list command
-./cordctl model list -h
+cordctl model list -h
 ```
 
 ## Shell Completion
@@ -52,6 +52,57 @@
 cordctl completion bash >> $HOME/.bashrc
 ```
 
+## Interacting with models
+
+`cordctl` has several commands for interacting with models in XOS:
+
+* `cordctl modeltype list` ... list the types of models that XOS supports.
+* `cordctl model list <modelName>` ... list instances of the given model, with optional filtering.
+* `cordctl model update <modelName> <id> --set-json <json>` ... update models with new fields
+
+### Listing model types
+
+XOS supports a dynamic set of models that may be extended by services that are loaded into XOS. As such the set of models that XOS supports is not necessarily fixed at initial deployment time, but may evolve over the life cycle of an XOS deployment as services are added, removed, or upgraded. The `modeltype list` command allows you to query the set of model types that XOS supports. For example,
+
+```bash
+# Query available model types
+cordctl modeltype list
+```
+
+### Listing models
+
+The basic syntax for listing models (`cordctl model list <modelName>`) will list all objects of a particular model. Filtering options can be added by using the `--filter` argument and providing a comma-separated list of filters. If the filters contain characters that the shell would interpret, such as spaces or `>`, `<` or `!` then you'll need to escape your filter specifier. For example,
+
+```bash
+# List slices that have id > 10 and controller_kind = Deployment
+cordctl model list Slice --filter "id>10, controller_kind=Deployment"
+```
+
+Supported operators in the filters include `=`, `!=`, `>`, `<`, `>=`, `<=`.
+
+### Updating models
+
+The `model update` command is a flexible way to update one or more models. The most basic syntax uses one or more model IDs. For example,
+
+```bash
+# Update Site 1 and set its site_url to http://www.opencord.org/
+cordctl model update Site 1 --set-field site_url=http://www.opencord.org/
+```
+
+Alternatively you may specify a JSON-formatted dictionary. Make sure to properly quote your JSON dictionary when using it as a command line argument. For example,
+
+```bash
+# Update Site 1 and set its site_url to http://www.opencord.org/
+cordctl model update Site 1 --set-json '{"site_url": "http://www.opencord.org/"}'
+```
+
+If you don't know the ID of the object you wish to operate, or if you want to update several objects at the same time that have something in common, then you can use a `--filter` argument instead of an ID. For example,
+
+```bash
+# Update all sites named "mysite"  and set its site_url to http://www.opencord.org/
+cordctl model update Site --filter name=mysite --set-field site_url=http://www.opencord.org/
+```
+
 ## 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 53daf9b..11d221b 100644
--- a/cmd/cordctl.go
+++ b/cmd/cordctl.go
@@ -32,6 +32,7 @@
 	}
 	commands.RegisterBackupCommands(parser)
 	commands.RegisterModelCommands(parser)
+	commands.RegisterModelTypeCommands(parser)
 	commands.RegisterServiceCommands(parser)
 	commands.RegisterTransferCommands(parser)
 	commands.RegisterVersionCommands(parser)
diff --git a/commands/backup.go b/commands/backup.go
index db6e51a..9d11db9 100644
--- a/commands/backup.go
+++ b/commands/backup.go
@@ -20,7 +20,6 @@
 	"errors"
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
-	"github.com/opencord/cordctl/format"
 	"time"
 )
 
@@ -139,27 +138,13 @@
 
 	// STEP 6: 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 = h.GetChecksum()
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
 
 	return nil
 }
@@ -235,14 +220,6 @@
 
 	// 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))
@@ -254,13 +231,7 @@
 		data[0].Status = "FAILURE"
 	}
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
 
 	return nil
 }
diff --git a/commands/command.go b/commands/command.go
index 6a8d663..53f1d8a 100644
--- a/commands/command.go
+++ b/commands/command.go
@@ -80,6 +80,7 @@
 	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 {
@@ -143,6 +144,7 @@
 		GlobalConfig.Password = GlobalOptions.Password
 	}
 
+	// Generate error messages for required settings
 	if GlobalConfig.Server == "" {
 		log.Fatal("Server is not set. Please update config file or use the -s option")
 	}
@@ -179,3 +181,22 @@
 		}
 	}
 }
+
+// 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) && (quiet_format != "") {
+		outputFormat = quiet_format
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+	}
+
+	GenerateOutput(&result)
+}
diff --git a/commands/common.go b/commands/common.go
index 9461f99..93c1b5d 100644
--- a/commands/common.go
+++ b/commands/common.go
@@ -16,6 +16,7 @@
 package commands
 
 import (
+	"bufio"
 	b64 "encoding/base64"
 	"fmt"
 	"github.com/fullstorydev/grpcurl"
@@ -23,6 +24,9 @@
 	"golang.org/x/net/context"
 	"google.golang.org/grpc"
 	reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+	"log"
+	"os"
+	"strings"
 )
 
 func GenerateHeaders() []string {
@@ -62,3 +66,30 @@
 		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/commands/handler.go b/commands/handler.go
index 8aadb44..678883d 100644
--- a/commands/handler.go
+++ b/commands/handler.go
@@ -69,7 +69,15 @@
 	}
 
 	for k, v := range fields {
-		dmsg.TrySetFieldByName(k, v)
+		// _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())
 
diff --git a/commands/models.go b/commands/models.go
index 16e4dc4..26e9d9f 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -20,15 +20,9 @@
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
-	"github.com/opencord/cordctl/format"
-	"sort"
 	"strings"
 )
 
-const (
-	DEFAULT_MODEL_AVAILABLE_FORMAT = "{{ . }}"
-)
-
 type ModelNameString string
 
 type ModelList struct {
@@ -42,13 +36,22 @@
 	} `positional-args:"yes" required:"yes"`
 }
 
-type ModelAvailable struct {
+type ModelUpdate struct {
 	OutputOptions
+	Filter    string `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"`
+	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"`
-	Available ModelAvailable `command:"available"`
+	List   ModelList   `command:"list"`
+	Update ModelUpdate `command:"update"`
 }
 
 var modelOpts = ModelOpts{}
@@ -57,42 +60,6 @@
 	parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
 }
 
-func (options *ModelAvailable) Execute(args []string) error {
-	conn, descriptor, err := InitReflectionClient()
-	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)
-
-	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 {
@@ -106,18 +73,12 @@
 		return err
 	}
 
-	var models []*dynamic.Message
-
-	queries, err := CommaSeparatedQueryToMap(options.Filter)
+	queries, err := CommaSeparatedQueryToMap(options.Filter, true)
 	if err != nil {
 		return err
 	}
 
-	if len(queries) == 0 {
-		models, err = ListModels(conn, descriptor, string(options.Args.ModelName))
-	} else {
-		models, err = FilterModels(conn, descriptor, string(options.Args.ModelName), queries)
-	}
+	models, err := ListOrFilterModels(conn, descriptor, string(options.Args.ModelName), queries)
 	if err != nil {
 		return err
 	}
@@ -167,21 +128,89 @@
 		}
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = default_format.String()
-	}
-	if options.Quiet {
-		outputFormat = "{{.Id}}"
+	FormatAndGenerateOutput(&options.OutputOptions, default_format.String(), "{{.id}}", data)
+
+	return nil
+}
+
+func (options *ModelUpdate) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
+	if err != nil {
+		return err
 	}
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, string(options.Args.ModelName))
+	if err != nil {
+		return err
 	}
 
-	GenerateOutput(&result)
+	if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
+		(len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
+		return fmt.Errorf("Use either an ID or a --filter to specify which models to update")
+	}
+
+	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(conn, descriptor, modelName, id)
+			if err != nil {
+				return err
+			}
+		}
+	} else {
+		models, err = ListOrFilterModels(conn, descriptor, modelName, queries)
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(models) == 0 {
+		return fmt.Errorf("Filter matches no objects")
+	} else if len(models) > 1 {
+		if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
+			return fmt.Errorf("Aborted by user")
+		}
+	}
+
+	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
+	}
+
+	for _, model := range models {
+		fields["id"] = model.GetFieldByName("id").(int32)
+		UpdateModel(conn, descriptor, modelName, fields)
+	}
+
+	count := len(models)
+	FormatAndGenerateOutput(&options.OutputOptions, "{{.}} models updated.", "{{.}}", count)
+
 	return nil
 }
 
diff --git a/commands/modeltypes.go b/commands/modeltypes.go
new file mode 100644
index 0000000..be2294c
--- /dev/null
+++ b/commands/modeltypes.go
@@ -0,0 +1,65 @@
+/*
+ * 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 := InitReflectionClient()
+	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, "", model_names)
+
+	return nil
+}
diff --git a/commands/orm.go b/commands/orm.go
index 8a4108e..65304a2 100644
--- a/commands/orm.go
+++ b/commands/orm.go
@@ -51,21 +51,21 @@
 //    "==foo"  --> "EQUAL", "foo"
 func DecodeOperator(query string) (string, string, bool, error) {
 	if strings.HasPrefix(query, "!=") {
-		return query[2:], "EQUAL", true, nil
+		return strings.TrimSpace(query[2:]), "EQUAL", true, nil
 	} else if strings.HasPrefix(query, "==") {
 		return "", "", false, errors.New("Operator == is now allowed. Suggest using = instead.")
 	} else if strings.HasPrefix(query, "=") {
-		return query[1:], "EQUAL", false, nil
+		return strings.TrimSpace(query[1:]), "EQUAL", false, nil
 	} else if strings.HasPrefix(query, ">=") {
-		return query[2:], "GREATER_THAN_OR_EQUAL", false, nil
+		return strings.TrimSpace(query[2:]), "GREATER_THAN_OR_EQUAL", false, nil
 	} else if strings.HasPrefix(query, ">") {
-		return query[1:], "GREATER_THAN", false, nil
+		return strings.TrimSpace(query[1:]), "GREATER_THAN", false, nil
 	} else if strings.HasPrefix(query, "<=") {
-		return query[2:], "LESS_THAN_OR_EQUAL", false, nil
+		return strings.TrimSpace(query[2:]), "LESS_THAN_OR_EQUAL", false, nil
 	} else if strings.HasPrefix(query, "<") {
-		return query[1:], "LESS_THAN", false, nil
+		return strings.TrimSpace(query[1:]), "LESS_THAN", false, nil
 	} else {
-		return query, "EQUAL", false, nil
+		return strings.TrimSpace(query), "EQUAL", false, nil
 	}
 }
 
@@ -95,15 +95,32 @@
 
 		nm := dynamic.NewMessage(elements_mt)
 
-		field_type := h.Model.FindFieldByName(field_name).GetType()
-		if field_type == descriptor.FieldDescriptorProto_TYPE_INT32 {
-			i, _ := strconv.ParseInt(value, 10, 32)
+		field_descriptor := h.Model.FindFieldByName(field_name)
+		if field_descriptor == nil {
+			return fmt.Errorf("Field %s does not exist", 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))
-		} else if field_type == descriptor.FieldDescriptorProto_TYPE_UINT32 {
-			i, _ := strconv.ParseInt(value, 10, 32)
+		case descriptor.FieldDescriptorProto_TYPE_UINT32:
+			var i int64
+			i, err = strconv.ParseInt(value, 10, 32)
 			nm.SetFieldByName("iValue", uint32(i))
-		} else {
+		case descriptor.FieldDescriptorProto_TYPE_FLOAT:
+			err = errors.New("Floating point filters are unsupported")
+		case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
+			err = errors.New("Floating point filters are unsupported")
+		default:
 			nm.SetFieldByName("sValue", value)
+			err = nil
+		}
+
+		if err != nil {
+			return err
 		}
 
 		nm.SetFieldByName("name", field_name)
@@ -120,33 +137,83 @@
 }
 
 // Take a string list of queries and turns it into a map of queries
-func QueryStringsToMap(query_args []string) (map[string]string, error) {
+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.Trim(query_str, " ")
+		query_str := strings.TrimSpace(query_str)
 		operator_pos := -1
 		for i, ch := range query_str {
-			if (ch == '!') || (ch == '=') || (ch == '>') || (ch == '<') {
-				operator_pos = i
-				break
+			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, fmt.Errorf("Illegal query string %s", query_str)
+			return nil, fmt.Errorf("Illegal operator/value string %s", query_str)
 		}
-		queries[query_str[:operator_pos]] = query_str[operator_pos:]
+		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) (map[string]string, error) {
+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)
+	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, fmt.Errorf("Failed to convert model %s to a messagedescriptor", modelName)
+	}
+	field_descriptor := model_md.FindFieldByName(field_name)
+	if field_descriptor == nil {
+		return nil, fmt.Errorf("Field %s does not exist in model %s", field_name, modelName)
+	}
+	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
@@ -211,6 +278,34 @@
 	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 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
+	}
+
+	// TODO: Do we need to do anything with the response?
+	_ = resp
+
+	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)
@@ -385,6 +480,15 @@
 	return ItemsToDynamicMessageList(items), nil
 }
 
+// Call ListModels or FilterModels as appropriate
+func ListOrFilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
+	if len(queries) == 0 {
+		return ListModels(conn, descriptor, modelName)
+	} else {
+		return FilterModels(conn, descriptor, modelName, queries)
+	}
+}
+
 // Get a model from XOS given a fieldName/fieldValue
 func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
 	models, err := FilterModels(conn, descriptor, modelName, queries)
diff --git a/commands/services.go b/commands/services.go
index a38c469..99d39c0 100644
--- a/commands/services.go
+++ b/commands/services.go
@@ -21,7 +21,7 @@
 	"github.com/fullstorydev/grpcurl"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
-	"github.com/opencord/cordctl/format"
+	//"github.com/opencord/cordctl/format"
 )
 
 const (
@@ -86,14 +86,6 @@
 		return err
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_SERVICE_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Id}}"
-	}
-
 	data := make([]ServiceListOutput, len(items.([]interface{})))
 
 	for i, item := range items.([]interface{}) {
@@ -103,12 +95,7 @@
 		data[i].State = val.GetFieldByName("state").(string)
 	}
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SERVICE_FORMAT, "{{.Name}}", data)
 
-	GenerateOutput(&result)
 	return nil
 }
diff --git a/commands/transfer.go b/commands/transfer.go
index 819e82f..699a4f7 100644
--- a/commands/transfer.go
+++ b/commands/transfer.go
@@ -20,7 +20,6 @@
 	"errors"
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
-	"github.com/opencord/cordctl/format"
 	"strings"
 )
 
@@ -92,27 +91,13 @@
 			upload_result.GetFieldByName("checksum").(string))
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_TRANSFER_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Status}}"
-	}
-
 	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")
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_TRANSFER_FORMAT, "{{.Status}}", data)
 
 	return nil
 }
@@ -144,26 +129,13 @@
 		return err
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_TRANSFER_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Status}}"
-	}
-
 	data := make([]TransferOutput, 1)
 	data[0].Chunks = h.chunks
 	data[0].Bytes = h.bytes
 	data[0].Status = h.status
+	data[0].Checksum = h.GetChecksum()
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_TRANSFER_FORMAT, "{{.Status}}", data)
 
 	return nil
 }