SEBA-678 Add model delete command

Change-Id: I3f1cbf516b481059937a33d113032def1da2c48d
diff --git a/README.md b/README.md
index 88d476a..1b7297a 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,18 @@
 cordctl model update Site --filter name=mysite --set-field site_url=http://www.opencord.org/
 ```
 
+### Deleting Models
+
+The syntax for deleting models is similar to that for updating models. You may delete by specifying one of more IDs, or you may delete by using a filter. For example,
+
+```bash
+# Delete Slice 1
+cordctl model delete Slice 1
+
+# Delete the Slice named myslice
+cordctl model delete Slice --filter name=mylice
+```
+
 ## 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/commands/command.go b/commands/command.go
index 53f1d8a..6d975cf 100644
--- a/commands/command.go
+++ b/commands/command.go
@@ -188,7 +188,7 @@
 	if outputFormat == "" {
 		outputFormat = default_format
 	}
-	if (options.Quiet) && (quiet_format != "") {
+	if options.Quiet {
 		outputFormat = quiet_format
 	}
 
diff --git a/commands/common.go b/commands/common.go
index 93c1b5d..9810300 100644
--- a/commands/common.go
+++ b/commands/common.go
@@ -24,6 +24,7 @@
 	"golang.org/x/net/context"
 	"google.golang.org/grpc"
 	reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+	"google.golang.org/grpc/status"
 	"log"
 	"os"
 	"strings"
@@ -93,3 +94,16 @@
 		}
 	}
 }
+
+func HumanReadableError(err error) string {
+	st, ok := status.FromError(err)
+	if ok {
+		grpc_message := st.Message()
+		if strings.HasPrefix(grpc_message, "Exception calling application: ") {
+			return st.Message()[31:]
+		} else {
+			return st.Message()
+		}
+	}
+	return err.Error()
+}
diff --git a/commands/models.go b/commands/models.go
index 26e9d9f..bae5dbc 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -23,6 +23,11 @@
 	"strings"
 )
 
+const (
+	DEFAULT_DELETE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
+	DEFAULT_UPDATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
+)
+
 type ModelNameString string
 
 type ModelList struct {
@@ -30,7 +35,7 @@
 	ShowHidden      bool   `long:"showhidden" description:"Show hidden fields in default output"`
 	ShowFeedback    bool   `long:"showfeedback" description:"Show feedback fields in default output"`
 	ShowBookkeeping bool   `long:"showbookkeeping" description:"Show bookkeeping fields in default output"`
-	Filter          string `long:"filter" description:"Comma-separated list of filters"`
+	Filter          string `short:"f" long:"filter" description:"Comma-separated list of filters"`
 	Args            struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
@@ -38,7 +43,7 @@
 
 type ModelUpdate struct {
 	OutputOptions
-	Filter    string `long:"filter" description:"Comma-separated list of filters"`
+	Filter    string `short:"f" long:"filter" description:"Comma-separated list of filters"`
 	SetFields string `long:"set-field" description:"Comma-separated list of field=value to set"`
 	SetJSON   string `long:"set-json" description:"JSON dictionary to use for settings fields"`
 	Args      struct {
@@ -49,9 +54,31 @@
 	} `positional-args:"yes" required:"no"`
 }
 
+type ModelDelete struct {
+	OutputOptions
+	Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	Args   struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+	IDArgs struct {
+		ID []int32
+	} `positional-args:"yes" required:"no"`
+}
+
 type ModelOpts struct {
 	List   ModelList   `command:"list"`
 	Update ModelUpdate `command:"update"`
+	Delete ModelDelete `command:"delete"`
+}
+
+type ModelStatusOutputRow struct {
+	Id      int32  `json:"id"`
+	Message string `json:"message"`
+}
+
+type ModelStatusOutput struct {
+	Rows  []ModelStatusOutputRow
+	Quiet bool
 }
 
 var modelOpts = ModelOpts{}
@@ -60,6 +87,30 @@
 	parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
 }
 
+func InitModelStatusOutput(quiet bool, count int) ModelStatusOutput {
+	if quiet {
+		return ModelStatusOutput{Quiet: quiet}
+	} else {
+		return ModelStatusOutput{Rows: make([]ModelStatusOutputRow, count), Quiet: quiet}
+	}
+}
+
+func UpdateModelStatusOutput(output *ModelStatusOutput, i int, id int32, status string, err error) {
+	if err != nil {
+		if output.Quiet {
+			fmt.Printf("%d: %s\n", id, HumanReadableError(err))
+		} else {
+			output.Rows[i] = ModelStatusOutputRow{Id: id, Message: HumanReadableError(err)}
+		}
+	} else {
+		if output.Quiet {
+			fmt.Println(id)
+		} else {
+			output.Rows[i] = ModelStatusOutputRow{Id: id, Message: status}
+		}
+	}
+}
+
 func (options *ModelList) Execute(args []string) error {
 	conn, descriptor, err := InitReflectionClient()
 	if err != nil {
@@ -203,13 +254,78 @@
 		fields[fieldName] = proto_value
 	}
 
-	for _, model := range models {
-		fields["id"] = model.GetFieldByName("id").(int32)
-		UpdateModel(conn, descriptor, modelName, fields)
+	modelStatusOutput := InitModelStatusOutput(options.Quiet, len(models))
+	for i, model := range models {
+		id := model.GetFieldByName("id").(int32)
+		fields["id"] = id
+		err := UpdateModel(conn, descriptor, modelName, fields)
+
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Updated", err)
 	}
 
-	count := len(models)
-	FormatAndGenerateOutput(&options.OutputOptions, "{{.}} models updated.", "{{.}}", count)
+	if !options.Quiet {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_UPDATE_FORMAT, "", modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelDelete) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, string(options.Args.ModelName))
+	if err != nil {
+		return err
+	}
+
+	if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
+		(len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
+		return 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
+	}
+
+	modelName := string(options.Args.ModelName)
+
+	var ids []int32
+
+	if len(options.IDArgs.ID) > 0 {
+		ids = options.IDArgs.ID
+	} else {
+		models, err := ListOrFilterModels(conn, descriptor, modelName, queries)
+		if err != nil {
+			return err
+		}
+		ids = make([]int32, len(models))
+		for i, model := range models {
+			ids[i] = model.GetFieldByName("id").(int32)
+		}
+		if len(ids) == 0 {
+			return fmt.Errorf("Filter matches no objects")
+		} else if len(ids) > 1 {
+			if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
+				return fmt.Errorf("Aborted by user")
+			}
+		}
+	}
+
+	modelStatusOutput := InitModelStatusOutput(options.Quiet, len(ids))
+	for i, id := range ids {
+		err = DeleteModel(conn, descriptor, modelName, id)
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Deleted", err)
+	}
+
+	if !options.Quiet {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_DELETE_FORMAT, "", modelStatusOutput.Rows)
+	}
 
 	return nil
 }
diff --git a/commands/modeltypes.go b/commands/modeltypes.go
index be2294c..4d2153a 100644
--- a/commands/modeltypes.go
+++ b/commands/modeltypes.go
@@ -59,7 +59,11 @@
 
 	sort.Strings(model_names)
 
-	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_MODELTYPE_LIST_FORMAT, "", model_names)
+	FormatAndGenerateOutput(
+		&options.OutputOptions,
+		DEFAULT_MODELTYPE_LIST_FORMAT,
+		DEFAULT_MODELTYPE_LIST_FORMAT,
+		model_names)
 
 	return nil
 }
diff --git a/commands/orm.go b/commands/orm.go
index 65304a2..87dee55 100644
--- a/commands/orm.go
+++ b/commands/orm.go
@@ -563,6 +563,33 @@
 	}
 }
 
+// Get a model from XOS given its ID
+func DeleteModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) error {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{
+		Fields: map[string]map[string]interface{}{"xos.ID": map[string]interface{}{"id": id}},
+	}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Delete"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return err
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return h.Status.Err()
+	}
+
+	_, err = dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // Takes a *dynamic.Message and turns it into a map of fields to interfaces
 //    TODO: Might be more useful to convert the values to strings and ints
 func MessageToMap(d *dynamic.Message) map[string]interface{} {