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),