SEBA-671 Add model create and model sync commands

Change-Id: Ic5f3f0eb8c5c4eb635fb1bdcb818fbafde2b9001
diff --git a/README.md b/README.md
index 1b7297a..4e2f8da 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,10 @@
 
 * `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
+* `cordctl model update <modelName> <id> --set-json <json>` ... update models with new fields.
+* `cordctl model delete <modelName> <id>` ... delete models.
+* `cordctl model create <modelName> --set-json <json>` ... create a new model.
+* `cordctl model sync <modelName> <id>` ... wait for a model to be synchronized.
 
 ### Listing model types
 
@@ -115,6 +118,41 @@
 cordctl model delete Slice --filter name=mylice
 ```
 
+### Creating Models
+
+The `model create` command allows you to create new instances of a model in XOS. To do this, specify the type of the model that you want to create and the set of fields that populate it. The set of fields can be set using a name=value syntax or by passing a JSON object. The following two examples are equivalent,
+
+```base
+# Create a Site by using set-field
+cordctl model create Site --set-field name=somesite,abbreviated_name=somesite,login_base=somesite
+
+# Create a Site by passing a json object
+cordctl model create Site --set-json '{"name": "somesite", "abbreviated_name": "somesite", "login_base": "somesite"}'
+```
+
+### Syncing Models
+
+All XOS operations are by nature asynchronous. When a model instance is created or updated, a synchronizer will typically run at a later time, enacting that model by applying its changes to some external component. After this is complete, the synchronizer updates the timestamps and other metadata to convey that the synchronization is complete. 
+
+Asynchronous operations are often inconvenient for test infrastructure, automation scripts, and even human operators. `cordctl` offers some features for synchronous behavior. 
+
+The first is the `model sync` command that can sync models based on ID or based on a filter. For example,
+
+```bash
+# Sync based on ID
+cordctl model sync ONOSApp 17
+
+# Sync based on a field filter
+cordctl model sync ONOSApp --filter name=olt
+```
+
+The second way to sync an object is to use the `--sync` command when doing a `model create` or a `model update`. For example,
+
+```bash
+cordctl model create ONOSApp --sync --set-field name=myapp,app_id=org.opencord.myapp
+```
+
+
 ## 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/VERSION b/VERSION
index 05639a5..3eefcb9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.0-dev
+1.0.0
diff --git a/commands/models.go b/commands/models.go
index bae5dbc..33c3793 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -24,8 +24,10 @@
 )
 
 const (
+	DEFAULT_CREATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
 	DEFAULT_DELETE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
 	DEFAULT_UPDATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
+	DEFAULT_SYNC_FORMAT   = "table{{ .Id }}\t{{ .Message }}"
 )
 
 type ModelNameString string
@@ -43,10 +45,12 @@
 
 type ModelUpdate struct {
 	OutputOptions
-	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 {
+	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	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"`
+	Sync       bool   `long:"sync" description:"Synchronize before returning"`
+	Args       struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
 	IDArgs struct {
@@ -56,8 +60,32 @@
 
 type ModelDelete struct {
 	OutputOptions
-	Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
-	Args   struct {
+	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	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 ModelCreate struct {
+	OutputOptions
+	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output"`
+	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"`
+	Sync       bool   `long:"sync" description:"Synchronize before returning"`
+	Args       struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+}
+
+type ModelSync struct {
+	OutputOptions
+	Unbuffered bool   `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
+	Filter     string `short:"f" long:"filter" description:"Comma-separated list of filters"`
+	Args       struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
 	IDArgs struct {
@@ -69,16 +97,18 @@
 	List   ModelList   `command:"list"`
 	Update ModelUpdate `command:"update"`
 	Delete ModelDelete `command:"delete"`
+	Create ModelCreate `command:"create"`
+	Sync   ModelSync   `command:"sync"`
 }
 
 type ModelStatusOutputRow struct {
-	Id      int32  `json:"id"`
-	Message string `json:"message"`
+	Id      interface{} `json:"id"`
+	Message string      `json:"message"`
 }
 
 type ModelStatusOutput struct {
-	Rows  []ModelStatusOutputRow
-	Quiet bool
+	Rows       []ModelStatusOutputRow
+	Unbuffered bool
 }
 
 var modelOpts = ModelOpts{}
@@ -87,27 +117,33 @@
 	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}
-	}
+// Initialize ModelStatusOutput structure, creating a row for each model that will be output
+func InitModelStatusOutput(unbuffered bool, count int) ModelStatusOutput {
+	return ModelStatusOutput{Rows: make([]ModelStatusOutputRow, count), Unbuffered: unbuffered}
 }
 
-func UpdateModelStatusOutput(output *ModelStatusOutput, i int, id int32, status string, err error) {
+// Update model status output row for the model
+//    If unbuffered is set then we will output directly to the console. Regardless of the unbuffered
+//    setting, we always update the row, as callers may check that row for status.
+// Args:
+//    output - ModelStatusOutput struct to update
+//    i - index of row to update
+//    id - id of model, <nil> if no model exists
+//    status - status text to set if there is no error
+//    errror - if non-nil, then apply error text instead of status text
+//    final - true if successful status should be reported, false if successful status is yet to come
+
+func UpdateModelStatusOutput(output *ModelStatusOutput, i int, id interface{}, status string, err error, final bool) {
 	if err != nil {
-		if output.Quiet {
-			fmt.Printf("%d: %s\n", id, HumanReadableError(err))
-		} else {
-			output.Rows[i] = ModelStatusOutputRow{Id: id, Message: HumanReadableError(err)}
+		if output.Unbuffered {
+			fmt.Printf("%v: %s\n", id, HumanReadableError(err))
 		}
+		output.Rows[i] = ModelStatusOutputRow{Id: id, Message: HumanReadableError(err)}
 	} else {
-		if output.Quiet {
+		if output.Unbuffered && final {
 			fmt.Println(id)
-		} else {
-			output.Rows[i] = ModelStatusOutputRow{Id: id, Message: status}
 		}
+		output.Rows[i] = ModelStatusOutputRow{Id: id, Message: status}
 	}
 }
 
@@ -254,17 +290,29 @@
 		fields[fieldName] = proto_value
 	}
 
-	modelStatusOutput := InitModelStatusOutput(options.Quiet, len(models))
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, 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)
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Updated", err, !options.Sync)
 	}
 
-	if !options.Quiet {
-		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_UPDATE_FORMAT, "", modelStatusOutput.Rows)
+	if options.Sync {
+		for i, model := range models {
+			id := model.GetFieldByName("id").(int32)
+			if modelStatusOutput.Rows[i].Message == "Updated" {
+				conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
+				conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+				conditional_printf(!options.Quiet, "\n")
+				UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
+			}
+		}
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_UPDATE_FORMAT, DEFAULT_UPDATE_FORMAT, modelStatusOutput.Rows)
 	}
 
 	return nil
@@ -285,7 +333,7 @@
 
 	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")
+		return fmt.Errorf("Use either an ID or a --filter to specify which models to delete")
 	}
 
 	queries, err := CommaSeparatedQueryToMap(options.Filter, true)
@@ -317,14 +365,133 @@
 		}
 	}
 
-	modelStatusOutput := InitModelStatusOutput(options.Quiet, len(ids))
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
 	for i, id := range ids {
 		err = DeleteModel(conn, descriptor, modelName, id)
-		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Deleted", err)
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Deleted", err, true)
 	}
 
-	if !options.Quiet {
-		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_DELETE_FORMAT, "", modelStatusOutput.Rows)
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_DELETE_FORMAT, DEFAULT_DELETE_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelCreate) 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
+	}
+
+	updates, err := CommaSeparatedQueryToMap(options.SetFields, true)
+	if err != nil {
+		return err
+	}
+
+	modelName := string(options.Args.ModelName)
+
+	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
+	}
+
+	modelStatusOutput := InitModelStatusOutput(options.Unbuffered, 1)
+
+	err = CreateModel(conn, descriptor, modelName, fields)
+	UpdateModelStatusOutput(&modelStatusOutput, 0, fields["id"], "Created", err, !options.Sync)
+
+	if options.Sync {
+		if modelStatusOutput.Rows[0].Message == "Created" {
+			id := fields["id"].(int32)
+			conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
+			conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+			conditional_printf(!options.Quiet, "\n")
+			UpdateModelStatusOutput(&modelStatusOutput, 0, id, "Enacted", err, true)
+		}
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_CREATE_FORMAT, DEFAULT_CREATE_FORMAT, modelStatusOutput.Rows)
+	}
+
+	return nil
+}
+
+func (options *ModelSync) 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 sync")
+	}
+
+	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.Unbuffered, len(ids))
+	for i, id := range ids {
+		conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
+		conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
+		conditional_printf(!options.Quiet, "\n")
+		UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
+	}
+
+	if !options.Unbuffered {
+		FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SYNC_FORMAT, DEFAULT_SYNC_FORMAT, modelStatusOutput.Rows)
 	}
 
 	return nil