Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Portions copyright 2019-present Open Networking Foundation |
| 3 | * Original copyright 2019-present Ciena Corporation |
| 4 | * |
| 5 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | * you may not use this file except in compliance with the License. |
| 7 | * You may obtain a copy of the License at |
| 8 | * |
| 9 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | * |
| 11 | * Unless required by applicable law or agreed to in writing, software |
| 12 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | * See the License for the specific language governing permissions and |
| 15 | * limitations under the License. |
| 16 | */ |
| 17 | package commands |
| 18 | |
| 19 | import ( |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 20 | "fmt" |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 21 | flags "github.com/jessevdk/go-flags" |
| 22 | "github.com/jhump/protoreflect/dynamic" |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 23 | "strings" |
| 24 | ) |
| 25 | |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 26 | const ( |
| 27 | DEFAULT_DELETE_FORMAT = "table{{ .Id }}\t{{ .Message }}" |
| 28 | DEFAULT_UPDATE_FORMAT = "table{{ .Id }}\t{{ .Message }}" |
| 29 | ) |
| 30 | |
Scott Baker | 63ce82e | 2019-05-15 09:01:42 -0700 | [diff] [blame] | 31 | type ModelNameString string |
| 32 | |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 33 | type ModelList struct { |
| 34 | OutputOptions |
Scott Baker | 5201c0b | 2019-05-15 15:35:56 -0700 | [diff] [blame] | 35 | ShowHidden bool `long:"showhidden" description:"Show hidden fields in default output"` |
| 36 | ShowFeedback bool `long:"showfeedback" description:"Show feedback fields in default output"` |
| 37 | ShowBookkeeping bool `long:"showbookkeeping" description:"Show bookkeeping fields in default output"` |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 38 | Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"` |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 39 | Args struct { |
Scott Baker | 63ce82e | 2019-05-15 09:01:42 -0700 | [diff] [blame] | 40 | ModelName ModelNameString |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 41 | } `positional-args:"yes" required:"yes"` |
| 42 | } |
| 43 | |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 44 | type ModelUpdate struct { |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 45 | OutputOptions |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 46 | Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"` |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 47 | SetFields string `long:"set-field" description:"Comma-separated list of field=value to set"` |
| 48 | SetJSON string `long:"set-json" description:"JSON dictionary to use for settings fields"` |
| 49 | Args struct { |
| 50 | ModelName ModelNameString |
| 51 | } `positional-args:"yes" required:"yes"` |
| 52 | IDArgs struct { |
| 53 | ID []int32 |
| 54 | } `positional-args:"yes" required:"no"` |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 55 | } |
| 56 | |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 57 | type ModelDelete struct { |
| 58 | OutputOptions |
| 59 | Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"` |
| 60 | Args struct { |
| 61 | ModelName ModelNameString |
| 62 | } `positional-args:"yes" required:"yes"` |
| 63 | IDArgs struct { |
| 64 | ID []int32 |
| 65 | } `positional-args:"yes" required:"no"` |
| 66 | } |
| 67 | |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 68 | type ModelOpts struct { |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 69 | List ModelList `command:"list"` |
| 70 | Update ModelUpdate `command:"update"` |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 71 | Delete ModelDelete `command:"delete"` |
| 72 | } |
| 73 | |
| 74 | type ModelStatusOutputRow struct { |
| 75 | Id int32 `json:"id"` |
| 76 | Message string `json:"message"` |
| 77 | } |
| 78 | |
| 79 | type ModelStatusOutput struct { |
| 80 | Rows []ModelStatusOutputRow |
| 81 | Quiet bool |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 82 | } |
| 83 | |
| 84 | var modelOpts = ModelOpts{} |
| 85 | |
| 86 | func RegisterModelCommands(parser *flags.Parser) { |
| 87 | parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts) |
| 88 | } |
| 89 | |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 90 | func InitModelStatusOutput(quiet bool, count int) ModelStatusOutput { |
| 91 | if quiet { |
| 92 | return ModelStatusOutput{Quiet: quiet} |
| 93 | } else { |
| 94 | return ModelStatusOutput{Rows: make([]ModelStatusOutputRow, count), Quiet: quiet} |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func UpdateModelStatusOutput(output *ModelStatusOutput, i int, id int32, status string, err error) { |
| 99 | if err != nil { |
| 100 | if output.Quiet { |
| 101 | fmt.Printf("%d: %s\n", id, HumanReadableError(err)) |
| 102 | } else { |
| 103 | output.Rows[i] = ModelStatusOutputRow{Id: id, Message: HumanReadableError(err)} |
| 104 | } |
| 105 | } else { |
| 106 | if output.Quiet { |
| 107 | fmt.Println(id) |
| 108 | } else { |
| 109 | output.Rows[i] = ModelStatusOutputRow{Id: id, Message: status} |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 114 | func (options *ModelList) Execute(args []string) error { |
| 115 | conn, descriptor, err := InitReflectionClient() |
| 116 | if err != nil { |
| 117 | return err |
| 118 | } |
| 119 | |
| 120 | defer conn.Close() |
| 121 | |
Scott Baker | 63ce82e | 2019-05-15 09:01:42 -0700 | [diff] [blame] | 122 | err = CheckModelName(descriptor, string(options.Args.ModelName)) |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 123 | if err != nil { |
| 124 | return err |
| 125 | } |
| 126 | |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 127 | queries, err := CommaSeparatedQueryToMap(options.Filter, true) |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 128 | if err != nil { |
| 129 | return err |
| 130 | } |
| 131 | |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 132 | models, err := ListOrFilterModels(conn, descriptor, string(options.Args.ModelName), queries) |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 133 | if err != nil { |
| 134 | return err |
| 135 | } |
| 136 | |
| 137 | field_names := make(map[string]bool) |
Scott Baker | 5201c0b | 2019-05-15 15:35:56 -0700 | [diff] [blame] | 138 | data := make([]map[string]interface{}, len(models)) |
| 139 | for i, val := range models { |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 140 | data[i] = make(map[string]interface{}) |
| 141 | for _, field_desc := range val.GetKnownFields() { |
| 142 | field_name := field_desc.GetName() |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 143 | |
| 144 | isGuiHidden := strings.Contains(field_desc.GetFieldOptions().String(), "1005:1") |
| 145 | isFeedback := strings.Contains(field_desc.GetFieldOptions().String(), "1006:1") |
| 146 | isBookkeeping := strings.Contains(field_desc.GetFieldOptions().String(), "1007:1") |
| 147 | |
| 148 | if isGuiHidden && (!options.ShowHidden) { |
| 149 | continue |
| 150 | } |
| 151 | |
| 152 | if isFeedback && (!options.ShowFeedback) { |
| 153 | continue |
| 154 | } |
| 155 | |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 156 | if isBookkeeping && (!options.ShowBookkeeping) { |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 157 | continue |
| 158 | } |
| 159 | |
| 160 | if field_desc.IsRepeated() { |
| 161 | continue |
| 162 | } |
| 163 | |
Scott Baker | 6cf525a | 2019-05-09 12:25:08 -0700 | [diff] [blame] | 164 | data[i][field_name] = val.GetFieldByName(field_name) |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 165 | |
| 166 | field_names[field_name] = true |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | var default_format strings.Builder |
| 171 | default_format.WriteString("table") |
| 172 | first := true |
| 173 | for field_name, _ := range field_names { |
| 174 | if first { |
| 175 | fmt.Fprintf(&default_format, "{{ .%s }}", field_name) |
| 176 | first = false |
| 177 | } else { |
| 178 | fmt.Fprintf(&default_format, "\t{{ .%s }}", field_name) |
| 179 | } |
| 180 | } |
| 181 | |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 182 | FormatAndGenerateOutput(&options.OutputOptions, default_format.String(), "{{.id}}", data) |
| 183 | |
| 184 | return nil |
| 185 | } |
| 186 | |
| 187 | func (options *ModelUpdate) Execute(args []string) error { |
| 188 | conn, descriptor, err := InitReflectionClient() |
| 189 | if err != nil { |
| 190 | return err |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 191 | } |
| 192 | |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 193 | defer conn.Close() |
| 194 | |
| 195 | err = CheckModelName(descriptor, string(options.Args.ModelName)) |
| 196 | if err != nil { |
| 197 | return err |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 198 | } |
| 199 | |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 200 | if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) || |
| 201 | (len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) { |
| 202 | return fmt.Errorf("Use either an ID or a --filter to specify which models to update") |
| 203 | } |
| 204 | |
| 205 | queries, err := CommaSeparatedQueryToMap(options.Filter, true) |
| 206 | if err != nil { |
| 207 | return err |
| 208 | } |
| 209 | |
| 210 | updates, err := CommaSeparatedQueryToMap(options.SetFields, true) |
| 211 | if err != nil { |
| 212 | return err |
| 213 | } |
| 214 | |
| 215 | modelName := string(options.Args.ModelName) |
| 216 | |
| 217 | var models []*dynamic.Message |
| 218 | |
| 219 | if len(options.IDArgs.ID) > 0 { |
| 220 | models = make([]*dynamic.Message, len(options.IDArgs.ID)) |
| 221 | for i, id := range options.IDArgs.ID { |
| 222 | models[i], err = GetModel(conn, descriptor, modelName, id) |
| 223 | if err != nil { |
| 224 | return err |
| 225 | } |
| 226 | } |
| 227 | } else { |
| 228 | models, err = ListOrFilterModels(conn, descriptor, modelName, queries) |
| 229 | if err != nil { |
| 230 | return err |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | if len(models) == 0 { |
| 235 | return fmt.Errorf("Filter matches no objects") |
| 236 | } else if len(models) > 1 { |
| 237 | if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) { |
| 238 | return fmt.Errorf("Aborted by user") |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | fields := make(map[string]interface{}) |
| 243 | |
| 244 | if len(options.SetJSON) > 0 { |
| 245 | fields["_json"] = []byte(options.SetJSON) |
| 246 | } |
| 247 | |
| 248 | for fieldName, value := range updates { |
| 249 | value = value[1:] |
| 250 | proto_value, err := TypeConvert(descriptor, modelName, fieldName, value) |
| 251 | if err != nil { |
| 252 | return err |
| 253 | } |
| 254 | fields[fieldName] = proto_value |
| 255 | } |
| 256 | |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 257 | modelStatusOutput := InitModelStatusOutput(options.Quiet, len(models)) |
| 258 | for i, model := range models { |
| 259 | id := model.GetFieldByName("id").(int32) |
| 260 | fields["id"] = id |
| 261 | err := UpdateModel(conn, descriptor, modelName, fields) |
| 262 | |
| 263 | UpdateModelStatusOutput(&modelStatusOutput, i, id, "Updated", err) |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 264 | } |
| 265 | |
Scott Baker | 175cb40 | 2019-05-17 16:13:06 -0700 | [diff] [blame^] | 266 | if !options.Quiet { |
| 267 | FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_UPDATE_FORMAT, "", modelStatusOutput.Rows) |
| 268 | } |
| 269 | |
| 270 | return nil |
| 271 | } |
| 272 | |
| 273 | func (options *ModelDelete) Execute(args []string) error { |
| 274 | conn, descriptor, err := InitReflectionClient() |
| 275 | if err != nil { |
| 276 | return err |
| 277 | } |
| 278 | |
| 279 | defer conn.Close() |
| 280 | |
| 281 | err = CheckModelName(descriptor, string(options.Args.ModelName)) |
| 282 | if err != nil { |
| 283 | return err |
| 284 | } |
| 285 | |
| 286 | if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) || |
| 287 | (len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) { |
| 288 | return fmt.Errorf("Use either an ID or a --filter to specify which models to update") |
| 289 | } |
| 290 | |
| 291 | queries, err := CommaSeparatedQueryToMap(options.Filter, true) |
| 292 | if err != nil { |
| 293 | return err |
| 294 | } |
| 295 | |
| 296 | modelName := string(options.Args.ModelName) |
| 297 | |
| 298 | var ids []int32 |
| 299 | |
| 300 | if len(options.IDArgs.ID) > 0 { |
| 301 | ids = options.IDArgs.ID |
| 302 | } else { |
| 303 | models, err := ListOrFilterModels(conn, descriptor, modelName, queries) |
| 304 | if err != nil { |
| 305 | return err |
| 306 | } |
| 307 | ids = make([]int32, len(models)) |
| 308 | for i, model := range models { |
| 309 | ids[i] = model.GetFieldByName("id").(int32) |
| 310 | } |
| 311 | if len(ids) == 0 { |
| 312 | return fmt.Errorf("Filter matches no objects") |
| 313 | } else if len(ids) > 1 { |
| 314 | if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) { |
| 315 | return fmt.Errorf("Aborted by user") |
| 316 | } |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | modelStatusOutput := InitModelStatusOutput(options.Quiet, len(ids)) |
| 321 | for i, id := range ids { |
| 322 | err = DeleteModel(conn, descriptor, modelName, id) |
| 323 | UpdateModelStatusOutput(&modelStatusOutput, i, id, "Deleted", err) |
| 324 | } |
| 325 | |
| 326 | if !options.Quiet { |
| 327 | FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_DELETE_FORMAT, "", modelStatusOutput.Rows) |
| 328 | } |
Scott Baker | 5281d00 | 2019-05-16 10:45:26 -0700 | [diff] [blame] | 329 | |
Scott Baker | 2c0ebda | 2019-05-06 16:55:47 -0700 | [diff] [blame] | 330 | return nil |
| 331 | } |
Scott Baker | 63ce82e | 2019-05-15 09:01:42 -0700 | [diff] [blame] | 332 | |
| 333 | func (modelName *ModelNameString) Complete(match string) []flags.Completion { |
| 334 | conn, descriptor, err := InitReflectionClient() |
| 335 | if err != nil { |
| 336 | return nil |
| 337 | } |
| 338 | |
| 339 | defer conn.Close() |
| 340 | |
| 341 | models, err := GetModelNames(descriptor) |
| 342 | if err != nil { |
| 343 | return nil |
| 344 | } |
| 345 | |
| 346 | list := make([]flags.Completion, 0) |
| 347 | for k := range models { |
| 348 | if strings.HasPrefix(k, match) { |
| 349 | list = append(list, flags.Completion{Item: k}) |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | return list |
| 354 | } |