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{} {