SEBA-580 cordctl CLI for XOS
Change-Id: I807821a12d3d0fe4aee89791e2fe46e973da8eda
diff --git a/commands/command.go b/commands/command.go
new file mode 100644
index 0000000..329fee1
--- /dev/null
+++ b/commands/command.go
@@ -0,0 +1,179 @@
+/*
+ * 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 (
+ "encoding/json"
+ "fmt"
+ "github.com/opencord/cordctl/format"
+ "google.golang.org/grpc"
+ "gopkg.in/yaml.v2"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+ "time"
+)
+
+type OutputType uint8
+
+const (
+ OUTPUT_TABLE OutputType = iota
+ OUTPUT_JSON
+ OUTPUT_YAML
+)
+
+var CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
+
+type GrpcConfigSpec struct {
+ Timeout time.Duration `yaml:"timeout"`
+}
+
+type TlsConfigSpec struct {
+ UseTls bool `yaml:"useTls"`
+ CACert string `yaml:"caCert"`
+ Cert string `yaml:"cert"`
+ Key string `yaml:"key"`
+ Verify string `yaml:"verify"`
+}
+
+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
+}
+
+var GlobalConfig = GlobalConfigSpec{
+ ApiVersion: "v1",
+ Server: "localhost",
+ Tls: TlsConfigSpec{
+ UseTls: false,
+ },
+ Grpc: GrpcConfigSpec{
+ Timeout: time.Second * 10,
+ },
+}
+
+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"`
+}
+
+type OutputOptions struct {
+ Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
+ Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
+ OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
+}
+
+func toOutputType(in string) OutputType {
+ switch in {
+ case "table":
+ fallthrough
+ default:
+ return OUTPUT_TABLE
+ case "json":
+ return OUTPUT_JSON
+ case "yaml":
+ return OUTPUT_YAML
+ }
+}
+
+type CommandResult struct {
+ Format format.Format
+ OutputAs OutputType
+ Data interface{}
+}
+
+type config struct {
+ ApiVersion string `yaml:"apiVersion"`
+ Server string `yaml:"server"`
+}
+
+func NewConnection() (*grpc.ClientConn, error) {
+ if len(GlobalOptions.Config) == 0 {
+ home := os.Getenv("HOME")
+ // TODO: Replace after Jenkins updated to go 1.12
+ /*
+ home, err := os.UserHomeDir()
+ if err != nil {
+ log.Printf("Unable to discover they users home directory: %s\n", err)
+ }
+ */
+ GlobalOptions.Config = fmt.Sprintf("%s/.cord/config", home)
+ }
+
+ info, err := os.Stat(GlobalOptions.Config)
+ if err == nil && !info.IsDir() {
+ configFile, err := ioutil.ReadFile(GlobalOptions.Config)
+ if err != nil {
+ log.Printf("configFile.Get err #%v ", err)
+ }
+ err = yaml.Unmarshal(configFile, &GlobalConfig)
+ if err != nil {
+ log.Fatalf("Unmarshal: %v", err)
+ }
+ }
+
+ // Override from command line
+ if GlobalOptions.Server != "" {
+ GlobalConfig.Server = GlobalOptions.Server
+ }
+ if GlobalOptions.ApiVersion != "" {
+ GlobalConfig.ApiVersion = GlobalOptions.ApiVersion
+ }
+ if GlobalOptions.Username != "" {
+ GlobalConfig.Username = GlobalOptions.Username
+ }
+ if GlobalOptions.Password != "" {
+ GlobalConfig.Password = GlobalOptions.Password
+ }
+
+ return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure())
+}
+
+func GenerateOutput(result *CommandResult) {
+ if result != nil && result.Data != nil {
+ if result.OutputAs == OUTPUT_TABLE {
+ tableFormat := format.Format(result.Format)
+ tableFormat.Execute(os.Stdout, true, result.Data)
+ } else if result.OutputAs == OUTPUT_JSON {
+ asJson, err := json.Marshal(&result.Data)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%s", asJson)
+ } else if result.OutputAs == OUTPUT_YAML {
+ asYaml, err := yaml.Marshal(&result.Data)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%s", asYaml)
+ }
+ }
+}
diff --git a/commands/common.go b/commands/common.go
new file mode 100644
index 0000000..493b302
--- /dev/null
+++ b/commands/common.go
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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 (
+ b64 "encoding/base64"
+)
+
+func GenerateHeaders() []string {
+ username := GlobalConfig.Username
+ password := GlobalConfig.Password
+ sEnc := b64.StdEncoding.EncodeToString([]byte(username + ":" + password))
+ headers := []string{"authorization: basic " + sEnc}
+ return headers
+}
diff --git a/commands/funcmap.go b/commands/funcmap.go
new file mode 100644
index 0000000..0342653
--- /dev/null
+++ b/commands/funcmap.go
@@ -0,0 +1,75 @@
+/*
+ * 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 (
+ "fmt"
+ "github.com/fullstorydev/grpcurl"
+ "github.com/jhump/protoreflect/dynamic"
+ "github.com/jhump/protoreflect/grpcreflect"
+ "golang.org/x/net/context"
+ "google.golang.org/grpc"
+ reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+)
+
+type MethodNotFoundError struct {
+ Name string
+}
+
+func (e *MethodNotFoundError) Error() string {
+ return fmt.Sprintf("Method '%s' not found in function map", e.Name)
+}
+
+type MethodVersionNotFoundError struct {
+ Name string
+ Version string
+}
+
+func (e *MethodVersionNotFoundError) Error() string {
+ return fmt.Sprintf("Method '%s' does not have a verison for '%s' specfied in function map", e.Name, e.Version)
+}
+
+type DescriptorNotFoundError struct {
+ Version string
+}
+
+func (e *DescriptorNotFoundError) Error() string {
+ return fmt.Sprintf("Protocol buffer descriptor for API version '%s' not found", e.Version)
+}
+
+type UnableToParseDescriptorErrror struct {
+ err error
+ Version string
+}
+
+func (e *UnableToParseDescriptorErrror) Error() string {
+ return fmt.Sprintf("Unable to parse protocal buffer descriptor for version '%s': %s", e.Version, e.err)
+}
+
+func GetReflectionMethod(conn *grpc.ClientConn, name string) (grpcurl.DescriptorSource, string, error) {
+ refClient := grpcreflect.NewClient(context.Background(), reflectpb.NewServerReflectionClient(conn))
+ defer refClient.Reset()
+
+ desc := grpcurl.DescriptorSourceFromServer(context.Background(), refClient)
+
+ return desc, name, nil
+}
+
+func GetEnumValue(val *dynamic.Message, name string) string {
+ return val.FindFieldDescriptorByName(name).GetEnumType().
+ FindValueByNumber(val.GetFieldByName(name).(int32)).GetName()
+}
diff --git a/commands/handler.go b/commands/handler.go
new file mode 100644
index 0000000..01fb091
--- /dev/null
+++ b/commands/handler.go
@@ -0,0 +1,75 @@
+/*
+ * 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 (
+ "github.com/golang/protobuf/proto"
+ "github.com/jhump/protoreflect/desc"
+ "github.com/jhump/protoreflect/dynamic"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/status"
+ "io"
+)
+
+type RpcEventHandler struct {
+ Response proto.Message
+ Status *status.Status
+ Data []byte
+ Fields map[string]map[string]interface{}
+}
+
+func (h *RpcEventHandler) OnResolveMethod(*desc.MethodDescriptor) {
+}
+
+func (h *RpcEventHandler) OnSendHeaders(metadata.MD) {
+}
+
+func (h *RpcEventHandler) OnReceiveHeaders(metadata.MD) {
+}
+
+func (h *RpcEventHandler) OnReceiveResponse(m proto.Message) {
+ h.Response = m
+}
+
+func (h *RpcEventHandler) OnReceiveTrailers(s *status.Status, m metadata.MD) {
+ h.Status = s
+}
+
+func (h *RpcEventHandler) GetParams(msg proto.Message) error {
+ dmsg, err := dynamic.AsDynamicMessage(msg)
+ if err != nil {
+ return err
+ }
+
+ //fmt.Printf("MessageName: %s\n", dmsg.XXX_MessageName())
+
+ if h.Fields == nil || len(h.Fields) == 0 {
+ return io.EOF
+ }
+
+ fields, ok := h.Fields[dmsg.XXX_MessageName()]
+ if !ok {
+ return nil
+ }
+
+ for k, v := range fields {
+ dmsg.TrySetFieldByName(k, v)
+ }
+ delete(h.Fields, dmsg.XXX_MessageName())
+
+ return nil
+}
diff --git a/commands/models.go b/commands/models.go
new file mode 100644
index 0000000..11067e7
--- /dev/null
+++ b/commands/models.go
@@ -0,0 +1,167 @@
+/*
+ * 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"
+ "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"
+ "strings"
+)
+
+const (
+ DEFAULT_MODEL_FORMAT = "table{{ .id }}\t{{ .name }}"
+)
+
+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 {
+ ModelName string
+ } `positional-args:"yes" required:"yes"`
+}
+
+type ModelOpts struct {
+ List ModelList `command:"list"`
+}
+
+var modelOpts = ModelOpts{}
+
+func RegisterModelCommands(parser *flags.Parser) {
+ parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
+}
+
+func (options *ModelList) Execute(args []string) error {
+
+ conn, err := NewConnection()
+ 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)
+ 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
+ }
+
+ items, err := d.TryGetFieldByName("items")
+ if err != nil {
+ return err
+ }
+
+ field_names := make(map[string]bool)
+ data := make([]map[string]interface{}, len(items.([]interface{})))
+ for i, item := range items.([]interface{}) {
+ val := item.(*dynamic.Message)
+ 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")
+ isBookkeeping := strings.Contains(field_desc.GetFieldOptions().String(), "1007:1")
+
+ if isGuiHidden && (!options.ShowHidden) {
+ continue
+ }
+
+ if isFeedback && (!options.ShowFeedback) {
+ continue
+ }
+
+ if isBookkeeping {
+ continue
+ }
+
+ if field_desc.IsRepeated() {
+ 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)
+ }
+
+ field_names[field_name] = true
+ }
+ }
+
+ var default_format strings.Builder
+ default_format.WriteString("table")
+ first := true
+ for field_name, _ := range field_names {
+ if first {
+ fmt.Fprintf(&default_format, "{{ .%s }}", field_name)
+ first = false
+ } else {
+ fmt.Fprintf(&default_format, "\t{{ .%s }}", field_name)
+ }
+ }
+
+ outputFormat := CharReplacer.Replace(options.Format)
+ if outputFormat == "" {
+ outputFormat = default_format.String()
+ }
+ if options.Quiet {
+ outputFormat = "{{.Id}}"
+ }
+
+ result := CommandResult{
+ Format: format.Format(outputFormat),
+ OutputAs: toOutputType(options.OutputAs),
+ Data: data,
+ }
+
+ GenerateOutput(&result)
+ return nil
+}
diff --git a/commands/services.go b/commands/services.go
new file mode 100644
index 0000000..a38c469
--- /dev/null
+++ b/commands/services.go
@@ -0,0 +1,114 @@
+/*
+ * 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"
+ "github.com/fullstorydev/grpcurl"
+ flags "github.com/jessevdk/go-flags"
+ "github.com/jhump/protoreflect/dynamic"
+ "github.com/opencord/cordctl/format"
+)
+
+const (
+ DEFAULT_SERVICE_FORMAT = "table{{ .Name }}\t{{.Version}}\t{{.State}}"
+)
+
+type ServiceList struct {
+ OutputOptions
+}
+
+type ServiceListOutput struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ State string `json:"state"`
+}
+
+type ServiceOpts struct {
+ List ServiceList `command:"list"`
+}
+
+var serviceOpts = ServiceOpts{}
+
+func RegisterServiceCommands(parser *flags.Parser) {
+ parser.AddCommand("service", "service commands", "Commands to query and manipulate dynamically loaded XOS Services", &serviceOpts)
+}
+
+func (options *ServiceList) Execute(args []string) error {
+
+ conn, err := NewConnection()
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ descriptor, method, err := GetReflectionMethod(conn, "xos.dynamicload.GetLoadStatus")
+ 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
+ }
+
+ items, err := d.TryGetFieldByName("services")
+ if err != nil {
+ 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{}) {
+ val := item.(*dynamic.Message)
+ data[i].Name = val.GetFieldByName("name").(string)
+ data[i].Version = val.GetFieldByName("version").(string)
+ data[i].State = val.GetFieldByName("state").(string)
+ }
+
+ result := CommandResult{
+ Format: format.Format(outputFormat),
+ OutputAs: toOutputType(options.OutputAs),
+ Data: data,
+ }
+
+ GenerateOutput(&result)
+ return nil
+}
diff --git a/commands/transfer.go b/commands/transfer.go
new file mode 100644
index 0000000..8a42af3
--- /dev/null
+++ b/commands/transfer.go
@@ -0,0 +1,271 @@
+/*
+ * 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/golang/protobuf/proto"
+ flags "github.com/jessevdk/go-flags"
+ "github.com/jhump/protoreflect/dynamic"
+ "github.com/opencord/cordctl/format"
+ "io"
+ "os"
+ "strings"
+)
+
+const (
+ DEFAULT_TRANSFER_FORMAT = "table{{ .Status }}\t{{ .Checksum }}\t{{ .Chunks }}\t{{ .Bytes }}"
+)
+
+type TransferOutput struct {
+ Status string `json:"status"`
+ Checksum string `json:"checksum"`
+ Chunks int `json:"chunks"`
+ Bytes int `json:"bytes"`
+}
+
+type TransferUpload struct {
+ OutputOptions
+ ChunkSize int `short:"h" long:"chunksize" default:"65536" description:"Host and port"`
+ Args struct {
+ LocalFileName string
+ URI string
+ } `positional-args:"yes" required:"yes"`
+}
+
+type TransferDownload struct {
+ OutputOptions
+ Args struct {
+ URI string
+ LocalFileName string
+ } `positional-args:"yes" required:"yes"`
+}
+
+type TransferOpts struct {
+ Upload TransferUpload `command:"upload"`
+ Download TransferDownload `command:"download"`
+}
+
+var transferOpts = TransferOpts{}
+
+func RegisterTransferCommands(parser *flags.Parser) {
+ 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()
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ local_name := options.Args.LocalFileName
+ uri := options.Args.URI
+
+ descriptor, method, err := GetReflectionMethod(conn, "xos.filetransfer/Upload")
+ if err != nil {
+ return err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+ defer cancel()
+
+ headers := GenerateHeaders()
+
+ f, err := os.Open(local_name)
+ if err != nil {
+ return err
+ }
+
+ 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
+ }
+
+ outputFormat := CharReplacer.Replace(options.Format)
+ if outputFormat == "" {
+ outputFormat = DEFAULT_TRANSFER_FORMAT
+ }
+ if options.Quiet {
+ outputFormat = "{{.Status}}"
+ }
+
+ data := make([]TransferOutput, 1)
+ data[0].Checksum = d.GetFieldByName("checksum").(string)
+ data[0].Chunks = int(d.GetFieldByName("chunks_received").(int32))
+ data[0].Bytes = int(d.GetFieldByName("bytes_received").(int32))
+ data[0].Status = GetEnumValue(d, "status")
+
+ result := CommandResult{
+ Format: format.Format(outputFormat),
+ OutputAs: toOutputType(options.OutputAs),
+ Data: data,
+ }
+
+ GenerateOutput(&result)
+
+ return nil
+}
+
+func IsFileUri(s string) bool {
+ return strings.HasPrefix(s, "file://")
+}
+
+func (options *TransferDownload) Execute(args []string) error {
+
+ conn, err := NewConnection()
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ local_name := options.Args.LocalFileName
+ uri := options.Args.URI
+
+ if IsFileUri(local_name) {
+ return errors.New("local_name argument should not be a uri")
+ }
+
+ if !IsFileUri(uri) {
+ 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)
+ if err != nil {
+ 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
+
+ result := CommandResult{
+ Format: format.Format(outputFormat),
+ OutputAs: toOutputType(options.OutputAs),
+ Data: data,
+ }
+
+ GenerateOutput(&result)
+
+ return nil
+}
diff --git a/commands/version.go b/commands/version.go
new file mode 100644
index 0000000..7f7daf4
--- /dev/null
+++ b/commands/version.go
@@ -0,0 +1,76 @@
+/*
+ * 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"
+ "github.com/opencord/cordctl/cli/version"
+ "github.com/opencord/cordctl/format"
+)
+
+type VersionDetails struct {
+ Version string `json:"version"`
+ GoVersion 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"`
+}
+
+type VersionOpts struct {
+ OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
+}
+
+var versionOpts = VersionOpts{}
+
+var versionInfo = VersionOutput{
+ Client: VersionDetails{
+ Version: version.Version,
+ GoVersion: version.GoVersion,
+ GitCommit: version.GitCommit,
+ Os: version.Os,
+ Arch: version.Arch,
+ BuildTime: version.BuildTime,
+ },
+}
+
+func RegisterVersionCommands(parent *flags.Parser) {
+ parent.AddCommand("version", "display version", "Display client version", &versionOpts)
+}
+
+const DefaultFormat = `Client:
+ Version {{.Client.Version}}
+ Go version: {{.Client.GoVersion}}
+ Git commit: {{.Client.GitCommit}}
+ Built: {{.Client.BuildTime}}
+ OS/Arch: {{.Client.Os}}/{{.Client.Arch}}
+`
+
+func (options *VersionOpts) Execute(args []string) error {
+ result := CommandResult{
+ Format: format.Format(DefaultFormat),
+ OutputAs: toOutputType(options.OutputAs),
+ Data: versionInfo,
+ }
+
+ GenerateOutput(&result)
+ return nil
+}