go.ligato.io/vpp-agent/v3@v3.5.0/plugins/kvscheduler/rest.go (about) 1 // Copyright (c) 2018 Cisco and/or its affiliates. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at: 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package kvscheduler 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "net/http" 22 "net/url" 23 "sort" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/unrolled/render" 29 30 "go.ligato.io/cn-infra/v2/rpc/rest" 31 32 kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" 33 "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/internal/graph" 34 "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/internal/utils" 35 "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" 36 ) 37 38 const ( 39 // prefix used for REST urls of the KVScheduler. 40 urlPrefix = "/scheduler/" 41 42 // txnHistoryURL is URL used to obtain the transaction history. 43 txnHistoryURL = urlPrefix + "txn-history" 44 45 // sinceArg is the name of the argument used to define the start of the time 46 // window for the transaction history to display. 47 sinceArg = "since" 48 49 // untilArg is the name of the argument used to define the end of the time 50 // window for the transaction history to display. 51 untilArg = "until" 52 53 // seqNumArg is the name of the argument used to define the sequence number 54 // of the transaction to display (txnHistoryURL). 55 seqNumArg = "seq-num" 56 57 // formatArg is the name of the argument used to set the output format 58 // for the transaction history API. 59 formatArg = "format" 60 61 // recognized formats: 62 formatJSON = "json" 63 formatText = "text" 64 65 // keyTimelineURL is URL used to obtain timeline of value changes for a given key. 66 keyTimelineURL = urlPrefix + "key-timeline" 67 68 // keyArg is the name of the argument used to define key for "key-timeline" and "status" API. 69 keyArg = "key" 70 71 // graphSnapshotURL is URL used to obtain graph snapshot from a given point in time. 72 graphSnapshotURL = urlPrefix + "graph-snapshot" 73 74 // flagStatsURL is URL used to obtain flag statistics. 75 flagStatsURL = urlPrefix + "flag-stats" 76 77 // flagArg is the name of the argument used to define flag for "flag-stats" API. 78 flagArg = "flag" 79 80 // prefixArg is the name of the argument used to define prefix to filter keys 81 // for "flag-stats" API. 82 prefixArg = "prefix" 83 84 // time is the name of the argument used to define point in time for a graph snapshot 85 // to retrieve. Value = number of nanoseconds since the start of the epoch. 86 timeArg = "time" 87 88 // downstreamResyncURL is URL used to trigger downstream-resync. 89 downstreamResyncURL = urlPrefix + "downstream-resync" 90 91 // retryArg is the name of the argument used for "downstream-resync" API to tell whether 92 // to retry failed operations or not. 93 retryArg = "retry" 94 95 // verboseArg is the name of the argument used for "downstream-resync" API 96 // to tell whether the refreshed graph should be printed to stdout or not. 97 verboseArg = "verbose" 98 99 // dumpURL is URL used to dump either SB or scheduler's internal state of kv-pairs 100 // under the given descriptor / key-prefix. 101 dumpURL = urlPrefix + "dump" 102 103 // descriptorArg is the name of the argument used to define descriptor for "dump" API. 104 descriptorArg = "descriptor" 105 106 // keyPrefixArg is the name of the argument used to define key prefix for "dump" API. 107 keyPrefixArg = "key-prefix" 108 109 // viewArg is the name of the argument used for "dump" API to chooses from 110 // which point of view to look at the key-value space when dumping values. 111 // See type View from kvscheduler's API to learn the set of possible values. 112 viewArg = "view" 113 114 // txnArg allows to display graph at the time when the referenced transaction 115 // has just finalized 116 txnArg = "txn" // value = txn sequence number 117 118 // statusURL is URL used to print the state of values under the given 119 // descriptor / key-prefix or all of them. 120 statusURL = urlPrefix + "status" 121 ) 122 123 // errorString wraps string representation of an error that, unlike the original 124 // error, can be marshalled. 125 type errorString struct { 126 Error string 127 } 128 129 // dumpIndex defines "index" page for the Dump REST API. 130 type dumpIndex struct { 131 Descriptors []string 132 KeyPrefixes []string 133 Views []string 134 } 135 136 // recordKVsWithMetadata converts a list of key-value pairs with metadata 137 // into an equivalent list with proto.Message recorded for proper marshalling. 138 func recordKVsWithMetadata(in []kvs.KVWithMetadata) (out []kvs.RecordedKVWithMetadata) { 139 for _, kv := range in { 140 out = append(out, kvs.RecordedKVWithMetadata{ 141 RecordedKVPair: kvs.RecordedKVPair{ 142 Key: kv.Key, 143 Value: utils.RecordProtoMessage(kv.Value), 144 Origin: kv.Origin, 145 }, 146 Metadata: kv.Metadata, 147 }) 148 } 149 return out 150 } 151 152 // registerHandlers registers all supported REST APIs. 153 func (s *Scheduler) registerHandlers(http rest.HTTPHandlers) { 154 if http == nil { 155 s.Log.Debug("No http handler provided, skipping registration of KVScheduler REST handlers") 156 return 157 } 158 http.RegisterHTTPHandler(txnHistoryURL, s.txnHistoryGetHandler, "GET") 159 http.RegisterHTTPHandler(keyTimelineURL, s.keyTimelineGetHandler, "GET") 160 http.RegisterHTTPHandler(graphSnapshotURL, s.graphSnapshotGetHandler, "GET") 161 http.RegisterHTTPHandler(flagStatsURL, s.flagStatsGetHandler, "GET") 162 http.RegisterHTTPHandler(downstreamResyncURL, s.downstreamResyncPostHandler, "POST") 163 http.RegisterHTTPHandler(dumpURL, s.dumpGetHandler, "GET") 164 http.RegisterHTTPHandler(statusURL, s.statusGetHandler, "GET") 165 http.RegisterHTTPHandler(urlPrefix+"graph", s.graphHandler, "GET") 166 http.RegisterHTTPHandler(urlPrefix+"stats", s.statsHandler, "GET") 167 } 168 169 // txnHistoryGetHandler is the GET handler for "txn-history" API. 170 func (s *Scheduler) txnHistoryGetHandler(formatter *render.Render) http.HandlerFunc { 171 return func(w http.ResponseWriter, req *http.Request) { 172 var since, until time.Time 173 var seqNum uint64 174 args := req.URL.Query() 175 176 // parse optional *format* argument (default = JSON) 177 format := formatJSON 178 if formatStr, withFormat := args[formatArg]; withFormat && len(formatStr) == 1 { 179 format = formatStr[0] 180 if format != formatJSON && format != formatText { 181 err := errors.New("unrecognized output format") 182 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 183 return 184 } 185 } 186 187 // parse optional *seq-num* argument 188 if seqNumStr, withSeqNum := args[seqNumArg]; withSeqNum && len(seqNumStr) == 1 { 189 var err error 190 seqNum, err = strconv.ParseUint(seqNumStr[0], 10, 64) 191 if err != nil { 192 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 193 return 194 } 195 196 // sequence number takes precedence over the since-until time window 197 txn := s.GetRecordedTransaction(seqNum) 198 if txn == nil { 199 err := errors.New("transaction with such sequence number is not recorded") 200 s.logError(formatter.JSON(w, http.StatusNotFound, errorString{err.Error()})) 201 return 202 } 203 204 if format == formatJSON { 205 s.logError(formatter.JSON(w, http.StatusOK, txn)) 206 } else { 207 s.logError(formatter.Text(w, http.StatusOK, txn.StringWithOpts(false, true, 0))) 208 } 209 return 210 } 211 212 // parse optional *until* argument 213 if untilStr, withUntil := args[untilArg]; withUntil && len(untilStr) == 1 { 214 var err error 215 until, err = stringToTime(untilStr[0]) 216 if err != nil { 217 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 218 return 219 } 220 } 221 222 // parse optional *since* argument 223 if sinceStr, withSince := args[sinceArg]; withSince && len(sinceStr) == 1 { 224 var err error 225 since, err = stringToTime(sinceStr[0]) 226 if err != nil { 227 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 228 return 229 } 230 } 231 232 txnHistory := s.GetTransactionHistory(since, until) 233 if format == formatJSON { 234 s.logError(formatter.JSON(w, http.StatusOK, txnHistory)) 235 } else { 236 s.logError(formatter.Text(w, http.StatusOK, txnHistory.StringWithOpts(false, false, 0))) 237 } 238 } 239 } 240 241 // keyTimelineGetHandler is the GET handler for "key-timeline" API. 242 func (s *Scheduler) keyTimelineGetHandler(formatter *render.Render) http.HandlerFunc { 243 return func(w http.ResponseWriter, req *http.Request) { 244 args := req.URL.Query() 245 246 // parse optional *time* argument 247 var timeVal time.Time 248 if timeStr, withTime := args[timeArg]; withTime && len(timeStr) == 1 { 249 var err error 250 timeVal, err = stringToTime(timeStr[0]) 251 if err != nil { 252 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 253 return 254 } 255 } 256 257 // parse mandatory *key* argument 258 if keys, withKey := args[keyArg]; withKey && len(keys) == 1 { 259 graphR := s.graph.Read() 260 defer graphR.Release() 261 262 timeline := graphR.GetNodeTimeline(keys[0]) 263 if !timeVal.IsZero() { 264 var nodeRecord *graph.RecordedNode 265 for _, record := range timeline { 266 if record.Since.Before(timeVal) && 267 (record.Until.IsZero() || record.Until.After(timeVal)) { 268 nodeRecord = record 269 break 270 } 271 } 272 s.logError(formatter.JSON(w, http.StatusOK, nodeRecord)) 273 return 274 } 275 s.logError(formatter.JSON(w, http.StatusOK, timeline)) 276 return 277 } 278 279 err := errors.New("missing key argument") 280 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 281 } 282 } 283 284 // graphSnapshotGetHandler is the GET handler for "graph-snapshot" API. 285 func (s *Scheduler) graphSnapshotGetHandler(formatter *render.Render) http.HandlerFunc { 286 return func(w http.ResponseWriter, req *http.Request) { 287 timeVal := time.Now() 288 args := req.URL.Query() 289 290 // parse optional *time* argument 291 if timeStr, withTime := args[timeArg]; withTime && len(timeStr) == 1 { 292 var err error 293 timeVal, err = stringToTime(timeStr[0]) 294 if err != nil { 295 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 296 return 297 } 298 } 299 300 graphR := s.graph.Read() 301 defer graphR.Release() 302 303 snapshot := graphR.GetSnapshot(timeVal) 304 s.logError(formatter.JSON(w, http.StatusOK, snapshot)) 305 } 306 } 307 308 // flagStatsGetHandler is the GET handler for "flag-stats" API. 309 func (s *Scheduler) flagStatsGetHandler(formatter *render.Render) http.HandlerFunc { 310 return func(w http.ResponseWriter, req *http.Request) { 311 args := req.URL.Query() 312 313 // parse repeated *prefix* argument 314 prefixes := args[prefixArg] 315 316 if flags, withFlag := args[flagArg]; withFlag && len(flags) == 1 { 317 graphR := s.graph.Read() 318 defer graphR.Release() 319 320 stats := graphR.GetFlagStats(flagNameToIndex(flags[0]), func(key string) bool { 321 if len(prefixes) == 0 { 322 return true 323 } 324 for _, prefix := range prefixes { 325 if strings.HasPrefix(key, prefix) { 326 return true 327 } 328 } 329 return false 330 }) 331 s.logError(formatter.JSON(w, http.StatusOK, stats)) 332 return 333 } 334 335 err := errors.New("missing flag argument") 336 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 337 } 338 } 339 340 // downstreamResyncPostHandler is the POST handler for "downstream-resync" API. 341 func (s *Scheduler) downstreamResyncPostHandler(formatter *render.Render) http.HandlerFunc { 342 return func(w http.ResponseWriter, req *http.Request) { 343 // parse optional *retry* argument 344 args := req.URL.Query() 345 retry := false 346 if retryStr, withRetry := args[retryArg]; withRetry && len(retryStr) == 1 { 347 retryVal := retryStr[0] 348 if retryVal == "true" || retryVal == "1" { 349 retry = true 350 } 351 } 352 353 // parse optional *verbose* argument 354 verbose := false 355 if verboseStr, withVerbose := args[verboseArg]; withVerbose && len(verboseStr) == 1 { 356 verboseVal := verboseStr[0] 357 if verboseVal == "true" || verboseVal == "1" { 358 verbose = true 359 } 360 } 361 362 ctx := context.Background() 363 ctx = kvs.WithResync(ctx, kvs.DownstreamResync, verbose) 364 if retry { 365 ctx = kvs.WithRetryDefault(ctx) 366 } 367 seqNum, err := s.StartNBTransaction().Commit(ctx) 368 if err != nil { 369 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 370 return 371 } 372 txn := s.GetRecordedTransaction(seqNum) 373 s.logError(formatter.JSON(w, http.StatusOK, txn)) 374 } 375 } 376 377 func parseDumpAndStatusCommonArgs(args url.Values) (descriptor, keyPrefix, key string, err error) { 378 // parse optional *descriptor* argument 379 descriptors, withDescriptor := args[descriptorArg] 380 if withDescriptor && len(descriptors) != 1 { 381 err = errors.New("descriptor argument listed more than once") 382 return 383 } 384 if withDescriptor { 385 descriptor = descriptors[0] 386 } 387 388 // parse optional *key-prefix* argument 389 keyPrefixes, withKeyPrefix := args[keyPrefixArg] 390 if withKeyPrefix && len(keyPrefixes) != 1 { 391 err = errors.New("key-prefix argument listed more than once") 392 return 393 } 394 if withKeyPrefix { 395 keyPrefix = keyPrefixes[0] 396 } 397 398 // parse optional *key* argument 399 keys, withKey := args[keyArg] 400 if withKey && len(keys) != 1 { 401 err = errors.New("key argument listed more than once") 402 return 403 } 404 if withKey { 405 key = keys[0] 406 } 407 return 408 } 409 410 // dumpGetHandler is the GET handler for "dump" API. 411 func (s *Scheduler) dumpGetHandler(formatter *render.Render) http.HandlerFunc { 412 return func(w http.ResponseWriter, req *http.Request) { 413 args := req.URL.Query() 414 415 descriptor, keyPrefix, _, err := parseDumpAndStatusCommonArgs(args) 416 if err != nil { 417 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 418 return 419 } 420 421 // without descriptor and key prefix return "index" page 422 if descriptor == "" && keyPrefix == "" { 423 s.txnLock.Lock() 424 defer s.txnLock.Unlock() 425 index := dumpIndex{Views: []string{ 426 kvs.SBView.String(), kvs.NBView.String(), kvs.CachedView.String()}} 427 for _, descriptor := range s.registry.GetAllDescriptors() { 428 index.Descriptors = append(index.Descriptors, descriptor.Name) 429 index.KeyPrefixes = append(index.KeyPrefixes, descriptor.NBKeyPrefix) 430 } 431 s.logError(formatter.JSON(w, http.StatusOK, index)) 432 return 433 } 434 435 // parse optional *view* argument (default = SBView) 436 var view kvs.View 437 if viewStr, withState := args[viewArg]; withState && len(viewStr) == 1 { 438 switch viewStr[0] { 439 case kvs.SBView.String(): 440 view = kvs.SBView 441 case kvs.NBView.String(): 442 view = kvs.NBView 443 case kvs.CachedView.String(): 444 view = kvs.CachedView 445 default: 446 err := errors.New("unrecognized system view") 447 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 448 return 449 } 450 } 451 452 var dump []kvs.KVWithMetadata 453 if descriptor != "" { 454 dump, err = s.DumpValuesByDescriptor(descriptor, view) 455 456 if err != nil { 457 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 458 return 459 } 460 } else { 461 dump, err = s.DumpValuesByKeyPrefix(keyPrefix, view) 462 463 if err != nil { 464 s.logError(formatter.JSON(w, http.StatusNotFound, errorString{err.Error()})) 465 return 466 } 467 } 468 s.logError(formatter.JSON(w, http.StatusOK, recordKVsWithMetadata(dump))) 469 } 470 } 471 472 // statusGetHandler is the GET handler for "status" API. 473 func (s *Scheduler) statusGetHandler(formatter *render.Render) http.HandlerFunc { 474 return func(w http.ResponseWriter, req *http.Request) { 475 args := req.URL.Query() 476 477 descriptor, keyPrefix, key, err := parseDumpAndStatusCommonArgs(args) 478 if err != nil { 479 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 480 return 481 } 482 483 graphR := s.graph.Read() 484 defer graphR.Release() 485 486 if key != "" { 487 singleStatus := getValueStatus(graphR.GetNode(key), key) 488 s.logError(formatter.JSON(w, http.StatusOK, singleStatus)) 489 return 490 } 491 492 if descriptor == "" && keyPrefix != "" { 493 descriptor = s.getDescriptorForKeyPrefix(keyPrefix) 494 if descriptor == "" { 495 err = errors.New("unknown key prefix") 496 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 497 return 498 } 499 } 500 501 var nodes []graph.Node 502 if descriptor == "" { 503 // get all nodes with base values 504 nodes = graphR.GetNodes(nil, graph.WithoutFlags(&DerivedFlag{})) 505 } else { 506 // get nodes with base values under the given descriptor 507 nodes = graphR.GetNodes(nil, 508 graph.WithFlags(&DescriptorFlag{descriptor}), 509 graph.WithoutFlags(&DerivedFlag{})) 510 } 511 512 var status []*kvscheduler.BaseValueStatus 513 for _, node := range nodes { 514 status = append(status, getValueStatus(node, node.GetKey())) 515 } 516 // sort by keys 517 sort.Slice(status, func(i, j int) bool { 518 return status[i].Value.Key < status[j].Value.Key 519 }) 520 s.logError(formatter.JSON(w, http.StatusOK, status)) 521 } 522 } 523 524 func (s *Scheduler) graphHandler(formatter *render.Render) http.HandlerFunc { 525 return func(w http.ResponseWriter, req *http.Request) { 526 args := req.URL.Query() 527 s.txnLock.Lock() 528 defer s.txnLock.Unlock() 529 graphRead := s.graph.Read() 530 defer graphRead.Release() 531 532 var txn *kvs.RecordedTxn 533 timestamp := time.Now() 534 535 // parse optional *txn* argument 536 if txnStr, withTxn := args[txnArg]; withTxn && len(txnStr) == 1 { 537 txnSeqNum, err := strconv.ParseUint(txnStr[0], 10, 64) 538 if err != nil { 539 s.logError(formatter.JSON(w, http.StatusInternalServerError, errorString{err.Error()})) 540 return 541 } 542 543 txn = s.GetRecordedTransaction(txnSeqNum) 544 if txn == nil { 545 err := errors.New("transaction with such sequence number is not recorded") 546 s.logError(formatter.JSON(w, http.StatusNotFound, errorString{err.Error()})) 547 return 548 } 549 timestamp = txn.Stop 550 } 551 552 graphSnapshot := graphRead.GetSnapshot(timestamp) 553 output, err := s.renderDotOutput(graphSnapshot, txn) 554 if err != nil { 555 http.Error(w, err.Error(), http.StatusInternalServerError) 556 return 557 } 558 559 format := req.FormValue("format") 560 switch format { 561 case "raw": 562 _, err = w.Write(output) 563 if err != nil { 564 http.Error(w, err.Error(), http.StatusInternalServerError) 565 } 566 return 567 case "dot": 568 dot, err := validateDot(output) 569 if err != nil { 570 http.Error(w, err.Error(), http.StatusInternalServerError) 571 return 572 } 573 _, err = w.Write(dot) 574 if err != nil { 575 http.Error(w, err.Error(), http.StatusInternalServerError) 576 } 577 return 578 default: 579 format = "svg" 580 } 581 582 img, err := dotToImage("", format, output) 583 if err != nil { 584 http.Error(w, fmt.Sprintf("rendering image %v failed: %v\n%s", img, err, output), http.StatusInternalServerError) 585 return 586 } 587 588 s.Log.Debug("serving graph image from:", img) 589 http.ServeFile(w, req, img) 590 } 591 } 592 593 func (s *Scheduler) statsHandler(formatter *render.Render) http.HandlerFunc { 594 return func(w http.ResponseWriter, req *http.Request) { 595 s.logError(formatter.JSON(w, http.StatusOK, GetStats())) 596 } 597 } 598 599 // logError logs non-nil errors from JSON formatter 600 func (s *Scheduler) logError(err error) { 601 if err != nil { 602 s.Log.Error(err) 603 } 604 } 605 606 // stringToTime converts Unix timestamp from string to time.Time. 607 func stringToTime(s string) (time.Time, error) { 608 nsec, err := strconv.ParseInt(s, 10, 64) 609 if err != nil { 610 return time.Time{}, err 611 } 612 return time.Unix(0, nsec), nil 613 }