github.phpd.cn/cilium/cilium@v1.6.12/test/helpers/cilium.go (about) 1 // Copyright 2017-2019 Authors of Cilium 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 helpers 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "net" 23 "os" 24 "path/filepath" 25 "strings" 26 "time" 27 28 "github.com/cilium/cilium/api/v1/models" 29 "github.com/cilium/cilium/pkg/logging" 30 "github.com/cilium/cilium/pkg/logging/logfields" 31 "github.com/cilium/cilium/test/config" 32 "github.com/cilium/cilium/test/ginkgo-ext" 33 "github.com/cilium/cilium/test/helpers/logutils" 34 35 "github.com/sirupsen/logrus" 36 ) 37 38 var log = logging.DefaultLogger 39 40 const ( 41 // MaxRetries is the number of times that a loop should iterate until a 42 // specified condition is not met 43 MaxRetries = 30 44 ) 45 46 // BpfLBList returns the output of `cilium bpf lb list -o json` as a map 47 // Key will be the frontend address and the value is an array with all backend 48 // addresses 49 func (s *SSHMeta) BpfLBList(noDuplicates bool) (map[string][]string, error) { 50 var ( 51 result map[string][]string 52 res *CmdRes 53 ) 54 55 res = s.ExecCilium("bpf lb list -o json") 56 57 if !res.WasSuccessful() { 58 return nil, fmt.Errorf("cannot get bpf lb list: %s", res.CombineOutput()) 59 } 60 err := res.Unmarshal(&result) 61 if err != nil { 62 return nil, err 63 } 64 65 if noDuplicates { 66 for svc, entries := range result { 67 unique := make(map[string]struct{}) 68 for _, e := range entries { 69 unique[e] = struct{}{} 70 } 71 result[svc] = make([]string, 0, len(unique)) 72 for e := range unique { 73 result[svc] = append(result[svc], e) 74 } 75 } 76 } 77 78 return result, nil 79 } 80 81 // ExecCilium runs a Cilium CLI command and returns the resultant cmdRes. 82 func (s *SSHMeta) ExecCilium(cmd string) *CmdRes { 83 command := fmt.Sprintf("cilium %s", cmd) 84 return s.ExecWithSudo(command) 85 } 86 87 // EndpointGet returns the output of `cilium endpoint get` for the provided 88 // endpoint ID. 89 func (s *SSHMeta) EndpointGet(id string) *models.Endpoint { 90 if id == "" { 91 return nil 92 } 93 var data []models.Endpoint 94 endpointGetCmd := fmt.Sprintf("endpoint get %s -o json", id) 95 res := s.ExecCilium(endpointGetCmd) 96 err := res.Unmarshal(&data) 97 if err != nil { 98 s.logger.WithError(err).Errorf("EndpointGet fail %s", id) 99 return nil 100 } 101 if len(data) > 0 { 102 return &data[0] 103 } 104 return nil 105 } 106 107 // GetEndpointMutableConfigurationOption returns the value of the mutable 108 // configuration option optionName for the endpoint with ID endpointID, or an 109 // error if optionName's corresponding value cannot be retrieved for the 110 // endpoint. 111 func (s *SSHMeta) GetEndpointMutableConfigurationOption(endpointID, optionName string) (string, error) { 112 cmd := fmt.Sprintf("endpoint config %s -o json | jq -r '.realized.options.%s'", endpointID, optionName) 113 res := s.ExecCilium(cmd) 114 if !res.WasSuccessful() { 115 return "", fmt.Errorf("Unable to execute %q: %s", cmd, res.CombineOutput()) 116 } 117 118 return res.SingleOut(), nil 119 } 120 121 // SetAndWaitForEndpointConfiguration waits for the endpoint configuration to become a certain value 122 func (s *SSHMeta) SetAndWaitForEndpointConfiguration(endpointID, optionName, expectedValue string) error { 123 logger := s.logger.WithFields(logrus.Fields{ 124 logfields.EndpointID: endpointID, 125 "option": optionName, 126 "value": expectedValue}) 127 body := func() bool { 128 logger.Infof("Setting endpoint configuration") 129 status := s.EndpointSetConfig(endpointID, optionName, expectedValue) 130 if !status { 131 logger.Error("Cannot set endpoint configuration") 132 return status 133 } 134 135 value, err := s.GetEndpointMutableConfigurationOption(endpointID, optionName) 136 if err != nil { 137 log.WithError(err).Error("cannot get endpoint configuration") 138 return false 139 } 140 141 if value == expectedValue { 142 return true 143 } 144 logger.Debugf("Expected configuration option to have value %s, but got %s", 145 expectedValue, value) 146 return false 147 } 148 149 err := WithTimeout( 150 body, 151 fmt.Sprintf("cannot set endpoint config for endpoint %q", endpointID), 152 &TimeoutConfig{Timeout: HelperTimeout}) 153 return err 154 } 155 156 // WaitEndpointsDeleted waits up until timeout reached for all endpoints to be 157 // deleted. Returns true if all endpoints have been deleted before HelperTimeout 158 // is exceeded, false otherwise. 159 func (s *SSHMeta) WaitEndpointsDeleted() bool { 160 logger := s.logger.WithFields(logrus.Fields{"functionName": "WaitEndpointsDeleted"}) 161 // cilium-health endpoint is always running. 162 desiredState := "1" 163 body := func() bool { 164 cmd := fmt.Sprintf(`cilium endpoint list -o json | jq '. | length'`) 165 res := s.Exec(cmd) 166 numEndpointsRunning := strings.TrimSpace(res.GetStdOut()) 167 if numEndpointsRunning == desiredState { 168 return true 169 } 170 171 logger.Infof("%s endpoints are still running, want %s", numEndpointsRunning, desiredState) 172 return false 173 } 174 err := WithTimeout(body, "Endpoints are not deleted after timeout", &TimeoutConfig{Timeout: HelperTimeout}) 175 if err != nil { 176 logger.WithError(err).Warn("Endpoints are not deleted after timeout") 177 s.Exec("cilium endpoint list") // This function is only for debugging. 178 return false 179 } 180 return true 181 182 } 183 184 // WaitEndpointsReady waits up until timeout reached for all endpoints to not be 185 // in any regenerating or waiting-for-identity state. Returns true if all 186 // endpoints regenerate before HelperTimeout is exceeded, false otherwise. 187 func (s *SSHMeta) WaitEndpointsReady() bool { 188 logger := s.logger.WithFields(logrus.Fields{"functionName": "WaitEndpointsReady"}) 189 desiredState := string(models.EndpointStateReady) 190 body := func() bool { 191 filter := `{range [*]}{@.status.external-identifiers.container-name}{"="}{@.status.state},{@.status.identity.id}{"\n"}{end}` 192 cmd := fmt.Sprintf(`cilium endpoint list -o jsonpath='%s'`, filter) 193 194 res := s.Exec(cmd) 195 if !res.WasSuccessful() { 196 logger.Infof("Cannot get endpoint list: %s", res.CombineOutput()) 197 return false 198 } 199 values := res.KVOutput() 200 total := len(values) 201 202 result := map[string]int{} 203 for _, status := range values { 204 fields := strings.Split(status, ",") 205 state := fields[0] 206 secID := fields[1] 207 // Consider an endpoint with reserved identity 5 (reserved:init) as not ready. 208 if secID == "5" { 209 state = state + "+init" 210 } 211 result[state]++ 212 } 213 214 logger.WithField("status", result).Infof( 215 "'%d' containers are in a '%s' state of a total of '%d' containers.", 216 result[desiredState], desiredState, total) 217 218 if result[desiredState] == total { 219 return true 220 } 221 222 return false 223 } 224 225 err := WithTimeout(body, "Endpoints are not ready after timeout", &TimeoutConfig{Timeout: HelperTimeout}) 226 if err != nil { 227 logger.WithError(err).Warn("Endpoints are not ready after timeout") 228 s.Exec("cilium endpoint list") // This function is only for debugging into log. 229 return false 230 } 231 return true 232 } 233 234 // EndpointSetConfig sets the provided configuration option to the provided 235 // value for the endpoint with the endpoint ID id. It returns true if the 236 // configuration update command returned successfully. 237 func (s *SSHMeta) EndpointSetConfig(id, option, value string) bool { 238 logger := s.logger.WithFields(logrus.Fields{"endpointID": id}) 239 res := s.ExecCilium(fmt.Sprintf( 240 "endpoint config %s -o json | jq -r '.realized.options.%s'", id, option)) 241 242 if res.SingleOut() == value { 243 logger.Debugf("no need to update %s=%s; value already set", option, value) 244 return res.WasSuccessful() 245 } 246 247 before := s.EndpointGet(id) 248 if before == nil { 249 return false 250 } 251 252 configCmd := fmt.Sprintf("endpoint config %s %s=%s", id, option, value) 253 data := s.ExecCilium(configCmd) 254 if !data.WasSuccessful() { 255 logger.Errorf("cannot set endpoint configuration %s=%s", option, value) 256 return false 257 } 258 259 return true 260 } 261 262 // ListEndpoints returns the CmdRes resulting from executing 263 // `cilium endpoint list -o json`. 264 func (s *SSHMeta) ListEndpoints() *CmdRes { 265 return s.ExecCilium("endpoint list -o json") 266 } 267 268 // GetEndpointsIDMap returns a mapping of an endpoint ID to Docker container 269 // name, and an error if the list of endpoints cannot be retrieved via the 270 // Cilium CLI. 271 func (s *SSHMeta) GetEndpointsIDMap() (map[string]string, error) { 272 filter := `{range [*]}{@.id}{"="}{@.status.external-identifiers.container-name}{"\n"}{end}` 273 cmd := fmt.Sprintf("endpoint list -o jsonpath='%s'", filter) 274 endpoints := s.ExecCilium(cmd) 275 if !endpoints.WasSuccessful() { 276 return nil, fmt.Errorf("%q failed: %s", cmd, endpoints.CombineOutput()) 277 } 278 return endpoints.KVOutput(), nil 279 } 280 281 // GetAllEndpointsIds returns a mapping of all Docker container name to to its 282 // corresponding endpoint ID, and an error if the list of endpoints cannot be 283 // retrieved via the Cilium CLI. 284 func (s *SSHMeta) GetAllEndpointsIds() (map[string]string, error) { 285 filter := `{range [*]}{@.status.external-identifiers.container-name}{"="}{@.id}{"\n"}{end}` 286 cmd := fmt.Sprintf("endpoint list -o jsonpath='%s'", filter) 287 endpoints := s.ExecCilium(cmd) 288 if !endpoints.WasSuccessful() { 289 return nil, fmt.Errorf("%q failed: %s", cmd, endpoints.CombineOutput()) 290 } 291 return endpoints.KVOutput(), nil 292 } 293 294 // GetEndpointsIds returns a mapping of a Docker container name to to its 295 // corresponding endpoint ID, and an error if the list of endpoints cannot be 296 // retrieved via the Cilium CLI. 297 func (s *SSHMeta) GetEndpointsIds() (map[string]string, error) { 298 // cilium endpoint list -o jsonpath='{range [?(@.status.labels.security-relevant[0]!='reserved:health')]}{@.status.external-identifiers.container-name}{"="}{@.id}{"\n"}{end}' 299 filter := `{range [?(@.status.labels.security-relevant[0]!="reserved:health")]}{@.status.external-identifiers.container-name}{"="}{@.id}{"\n"}{end}` 300 cmd := fmt.Sprintf("endpoint list -o jsonpath='%s'", filter) 301 endpoints := s.ExecCilium(cmd) 302 if !endpoints.WasSuccessful() { 303 return nil, fmt.Errorf("%q failed: %s", cmd, endpoints.CombineOutput()) 304 } 305 return endpoints.KVOutput(), nil 306 } 307 308 // GetEndpointsIdentityIds returns a mapping of a Docker container name to it's 309 // corresponding endpoint's security identity, it will return an error if the list 310 // of endpoints cannot be retrieved via the Cilium CLI. 311 func (s *SSHMeta) GetEndpointsIdentityIds() (map[string]string, error) { 312 filter := `{range [*]}{@.status.external-identifiers.container-name}{"="}{@.status.identity.id}{"\n"}{end}` 313 endpoints := s.ExecCilium(fmt.Sprintf("endpoint list -o jsonpath='%s'", filter)) 314 if !endpoints.WasSuccessful() { 315 return nil, fmt.Errorf("cannot get endpoint list: %s", endpoints.CombineOutput()) 316 } 317 return endpoints.KVOutput(), nil 318 } 319 320 // GetEndpointsNames returns the container-name field of each Cilium endpoint. 321 func (s *SSHMeta) GetEndpointsNames() ([]string, error) { 322 data := s.ListEndpoints() 323 if data.WasSuccessful() == false { 324 return nil, fmt.Errorf("`cilium endpoint list` was not successful") 325 } 326 327 result, err := data.Filter("{ [?(@.status.labels.security-relevant[0]!='reserved:health')].status.external-identifiers.container-name }") 328 if err != nil { 329 return nil, err 330 } 331 332 return strings.Split(result.String(), " "), nil 333 } 334 335 // ManifestsPath returns the path of the directory where manifests (YAMLs 336 // containing policies, DaemonSets, etc.) are stored for the runtime tests. 337 // TODO: this can just be a constant; there's no need to have a function. 338 func (s *SSHMeta) ManifestsPath() string { 339 return fmt.Sprintf("%s/runtime/manifests/", BasePath) 340 } 341 342 // MonitorStart starts the monitor command in background and returns a callback 343 // function wich stops the monitor when the user needs. When the callback is 344 // called the command will stop and monitor's output is saved on 345 // `monitorLogFileName` file. 346 func (s *SSHMeta) MonitorStart() func() error { 347 cmd := "cilium monitor -v | ts '[%Y-%m-%d %H:%M:%S]'" 348 ctx, cancel := context.WithCancel(context.Background()) 349 res := s.ExecInBackground(ctx, cmd, ExecOptions{SkipLog: true}) 350 351 cb := func() error { 352 cancel() 353 testPath, err := CreateReportDirectory() 354 if err != nil { 355 s.logger.WithError(err).Errorf( 356 "cannot create test results path '%s'", testPath) 357 return err 358 } 359 360 err = ioutil.WriteFile( 361 filepath.Join(testPath, MonitorLogFileName), 362 res.CombineOutput().Bytes(), 363 LogPerm) 364 if err != nil { 365 log.WithError(err).Errorf("cannot create monitor log file") 366 } 367 return nil 368 } 369 return cb 370 } 371 372 // GetFullPath returns the path of file name prepended with the absolute path 373 // where manifests (YAMLs containing policies, DaemonSets, etc.) are stored. 374 func (s *SSHMeta) GetFullPath(name string) string { 375 return fmt.Sprintf("%s%s", s.ManifestsPath(), name) 376 } 377 378 // PolicyEndpointsSummary returns the count of whether policy enforcement is 379 // enabled, disabled, and the total number of endpoints, and an error if the 380 // Cilium endpoint metadata cannot be retrieved via the API. 381 func (s *SSHMeta) PolicyEndpointsSummary() (map[string]int, error) { 382 result := map[string]int{ 383 Enabled: 0, 384 Disabled: 0, 385 Total: 0, 386 } 387 388 res := s.ListEndpoints() 389 if !res.WasSuccessful() { 390 return nil, fmt.Errorf("was not able to list endpoints: %s", res.CombineOutput().String()) 391 } 392 393 endpoints, err := res.Filter("{ [?(@.status.labels.security-relevant[0]!='reserved:health')].status.policy.realized.policy-enabled }") 394 395 if err != nil { 396 return result, fmt.Errorf(`cannot filter for "policy-enabled" from output of "cilium endpoint list"`) 397 } 398 status := strings.Split(endpoints.String(), " ") 399 for _, kind := range status { 400 switch models.EndpointPolicyEnabled(kind) { 401 case models.EndpointPolicyEnabledBoth, models.EndpointPolicyEnabledEgress, 402 models.EndpointPolicyEnabledIngress: 403 result[Enabled]++ 404 case OptionNone: 405 result[Disabled]++ 406 } 407 result[Total]++ 408 } 409 return result, nil 410 } 411 412 // SetPolicyEnforcement sets the PolicyEnforcement configuration value for the 413 // Cilium agent to the provided status. 414 func (s *SSHMeta) SetPolicyEnforcement(status string) *CmdRes { 415 // We check before setting PolicyEnforcement; if we do not, EndpointWait 416 // will fail due to the status of the endpoints not changing. 417 log.Infof("setting %s=%s", PolicyEnforcement, status) 418 res := s.ExecCilium(fmt.Sprintf("config -o json | jq -r '.status.realized[\"policy-enforcement\"]'")) 419 if res.SingleOut() == status { 420 return res 421 } 422 return s.ExecCilium(fmt.Sprintf("config %s=%s", PolicyEnforcement, status)) 423 } 424 425 // SetPolicyEnforcementAndWait and wait sets the PolicyEnforcement configuration 426 // value for the Cilium agent to the provided status, and then waits for all endpoints 427 // running in s to be ready. Returns whether setting of the configuration value 428 // was unsuccessful / if the endpoints go into ready state. 429 func (s *SSHMeta) SetPolicyEnforcementAndWait(status string) bool { 430 res := s.SetPolicyEnforcement(status) 431 if !res.WasSuccessful() { 432 return false 433 } 434 435 return s.WaitEndpointsReady() 436 } 437 438 // PolicyDelAll deletes all policy rules currently imported into Cilium. 439 func (s *SSHMeta) PolicyDelAll() *CmdRes { 440 log.Info("Deleting all policy in agent") 441 return s.PolicyDel("--all") 442 } 443 444 // PolicyDel deletes the policy with the given ID from Cilium. 445 func (s *SSHMeta) PolicyDel(id string) *CmdRes { 446 res := s.ExecCilium(fmt.Sprintf( 447 "policy delete %s -o json | jq '.revision'", id)) 448 if !res.WasSuccessful() { 449 return res 450 } 451 policyID, _ := res.IntOutput() 452 return s.PolicyWait(policyID) 453 } 454 455 // PolicyGet runs `cilium policy get <id>`, where id is the name of a specific 456 // policy imported into Cilium. It returns the resultant CmdRes from running 457 // the aforementioned command. 458 func (s *SSHMeta) PolicyGet(id string) *CmdRes { 459 return s.ExecCilium(fmt.Sprintf("policy get %s", id)) 460 } 461 462 // PolicyGetAll gets all policies that are imported in the Cilium agent. 463 func (s *SSHMeta) PolicyGetAll() *CmdRes { 464 return s.ExecCilium("policy get") 465 466 } 467 468 // PolicyGetRevision retrieves the current policy revision number in the Cilium 469 // agent. 470 func (s *SSHMeta) PolicyGetRevision() (int, error) { 471 rev := s.ExecCilium("policy get -o json | jq '.revision'") 472 return rev.IntOutput() 473 } 474 475 // PolicyImportAndWait validates and imports a new policy into Cilium and waits 476 // until the policy revision number increments. Returns an error if the policy 477 // is invalid or could not be imported. 478 func (s *SSHMeta) PolicyImportAndWait(path string, timeout time.Duration) (int, error) { 479 ginkgoext.By(fmt.Sprintf("Setting up policy: %s", path)) 480 481 revision, err := s.PolicyGetRevision() 482 if err != nil { 483 return -1, fmt.Errorf("cannot get policy revision: %s", err) 484 } 485 s.logger.WithFields(logrus.Fields{ 486 logfields.Path: path, 487 logfields.PolicyRevision: revision}).Info("before importing policy") 488 489 s.logger.WithFields(logrus.Fields{ 490 logfields.Path: path}).Info("validating policy before importing") 491 492 res := s.ExecCilium(fmt.Sprintf("policy validate %s", path)) 493 if res.WasSuccessful() == false { 494 s.logger.WithFields(logrus.Fields{ 495 logfields.Path: path, 496 }).Errorf("could not validate policy %s: %s", path, res.CombineOutput()) 497 return -1, fmt.Errorf("could not validate policy %s: %s", path, res.CombineOutput()) 498 } 499 500 res = s.ExecCilium(fmt.Sprintf("policy import %s", path)) 501 if res.WasSuccessful() == false { 502 s.logger.WithFields(logrus.Fields{ 503 logfields.Path: path, 504 }).Errorf("could not import policy: %s", res.CombineOutput()) 505 return -1, fmt.Errorf("could not import policy %s", path) 506 } 507 body := func() bool { 508 currentRev, _ := s.PolicyGetRevision() 509 if currentRev > revision { 510 res := s.PolicyWait(currentRev) 511 if !res.WasSuccessful() { 512 log.Errorf("policy wait failed: %s", res.CombineOutput()) 513 } 514 return res.WasSuccessful() 515 } 516 s.logger.WithFields(logrus.Fields{ 517 logfields.PolicyRevision: currentRev, 518 "policyRevisionAfterImport": revision, 519 }).Infof("policy revisions are the same") 520 return false 521 } 522 err = WithTimeout(body, "could not import policy", &TimeoutConfig{Timeout: timeout}) 523 if err != nil { 524 return -1, err 525 } 526 revision, err = s.PolicyGetRevision() 527 s.logger.WithFields(logrus.Fields{ 528 logfields.Path: path, 529 logfields.PolicyRevision: revision, 530 }).Infof("policy import finished and revision increased") 531 return revision, err 532 } 533 534 // PolicyImport imports a new policy into Cilium. 535 func (s *SSHMeta) PolicyImport(path string) error { 536 res := s.ExecCilium(fmt.Sprintf("policy import %s", path)) 537 if !res.WasSuccessful() { 538 s.logger.Errorf("could not import policy: %s", res.CombineOutput()) 539 return fmt.Errorf("could not import policy %s", path) 540 } 541 return nil 542 } 543 544 // PolicyRenderAndImport receives an string with a policy, renders it in the 545 // test root directory and imports the policy to cilium. It returns the new 546 // policy id. Returns an error if the file cannot be created or if the policy 547 // cannot be imported 548 func (s *SSHMeta) PolicyRenderAndImport(policy string) (int, error) { 549 filename := fmt.Sprintf("policy_%s.json", MakeUID()) 550 s.logger.Debugf("PolicyRenderAndImport: render policy to '%s'", filename) 551 err := RenderTemplateToFile(filename, policy, os.ModePerm) 552 if err != nil { 553 s.logger.Errorf("PolicyRenderAndImport: cannot create policy file on '%s'", filename) 554 return 0, fmt.Errorf("cannot render the policy: %s", err) 555 } 556 path := GetFilePath(filename) 557 s.logger.Debugf("PolicyRenderAndImport: import policy from '%s'", path) 558 defer os.Remove(filename) 559 return s.PolicyImportAndWait(path, HelperTimeout) 560 } 561 562 // PolicyWait executes `cilium policy wait`, which waits until all endpoints are 563 // updated to the given policy revision. 564 func (s *SSHMeta) PolicyWait(revisionNum int) *CmdRes { 565 return s.ExecCilium(fmt.Sprintf("policy wait %d", revisionNum)) 566 } 567 568 // ReportFailed gathers relevant Cilium runtime data and logs for debugging 569 // purposes. 570 func (s *SSHMeta) ReportFailed(commands ...string) { 571 if config.CiliumTestConfig.SkipLogGathering { 572 ginkgoext.GinkgoPrint("Skipped gathering logs (-cilium.skipLogs=true)\n") 573 return 574 } 575 576 // Log the following line to both the log file, and to console to delineate 577 // when log gathering begins. 578 res := s.ExecCilium("endpoint list") // save the output in the logs 579 ginkgoext.GinkgoPrint(res.GetDebugMessage()) 580 581 for _, cmd := range commands { 582 res = s.ExecWithSudo(fmt.Sprintf("%s", cmd), ExecOptions{SkipLog: true}) 583 ginkgoext.GinkgoPrint(res.GetDebugMessage()) 584 } 585 586 s.DumpCiliumCommandOutput() 587 s.GatherLogs() 588 s.GatherDockerLogs() 589 } 590 591 // ValidateEndpointsAreCorrect is a function that validates that all Docker 592 // container that are in the given docker network are correct as cilium 593 // endpoints. 594 func (s *SSHMeta) ValidateEndpointsAreCorrect(dockerNetwork string) error { 595 endpointsFilter := `{range[*]}{.status.external-identifiers.container-id}{"="}{.id}{"\n"}{end}` 596 jqFilter := `.[].Containers|keys |.[]` 597 598 res := s.Exec(fmt.Sprintf("docker network inspect %s | jq -r '%s'", dockerNetwork, jqFilter)) 599 if !res.WasSuccessful() { 600 return errors.New("Cannot get Docker containers in the given network") 601 } 602 603 epRes := s.ExecCilium(fmt.Sprintf("endpoint list -o jsonpath='%s'", endpointsFilter)) 604 if !epRes.WasSuccessful() { 605 return errors.New("Cannot get cilium endpoint list") 606 } 607 608 endpoints := epRes.KVOutput() 609 for _, containerID := range res.ByLines() { 610 _, exists := endpoints[containerID] 611 if !exists { 612 613 return fmt.Errorf("ContainerID %s is not present in the endpoint list", containerID) 614 } 615 } 616 return nil 617 } 618 619 // ValidateNoErrorsInLogs checks in cilium logs since the given duration (By 620 // default `CurrentGinkgoTestDescription().Duration`) do not contain `panic`, 621 // `deadlocks` or `segmentation faults` messages . In case of any of these 622 // messages, it'll mark the test as failed. 623 func (s *SSHMeta) ValidateNoErrorsInLogs(duration time.Duration) { 624 logsCmd := fmt.Sprintf(`sudo journalctl -au %s --since '%v seconds ago'`, 625 DaemonName, duration.Seconds()) 626 logs := s.Exec(logsCmd, ExecOptions{SkipLog: true}).Output().String() 627 628 defer func() { 629 // Keep the cilium logs for the given test in a separate file. 630 testPath, err := CreateReportDirectory() 631 if err != nil { 632 s.logger.WithError(err).Error("Cannot create report directory") 633 return 634 } 635 err = ioutil.WriteFile( 636 fmt.Sprintf("%s/%s", testPath, CiliumTestLog), 637 []byte(logs), LogPerm) 638 639 if err != nil { 640 s.logger.WithError(err).Errorf("Cannot create %s", CiliumTestLog) 641 } 642 }() 643 644 failIfContainsBadLogMsg(logs) 645 646 fmt.Fprintf(CheckLogs, logutils.LogErrorsSummary(logs)) 647 } 648 649 // PprofReport runs pprof each 5 minutes and saves the data into the test 650 // folder saved with pprof suffix. 651 func (s *SSHMeta) PprofReport() { 652 PProfCadence := 5 * time.Minute 653 ticker := time.NewTicker(PProfCadence) 654 log := s.logger.WithField("subsys", "pprofReport") 655 656 for { 657 select { 658 case <-ticker.C: 659 660 testPath, err := CreateReportDirectory() 661 if err != nil { 662 log.WithError(err).Errorf("cannot create test result path '%s'", testPath) 663 return 664 } 665 d := time.Now().Add(50 * time.Second) 666 ctx, cancel := context.WithDeadline(context.Background(), d) 667 668 res := s.ExecInBackground(ctx, `sudo gops pprof-cpu $(pgrep cilium-agent)`) 669 670 err = res.WaitUntilMatch("Profiling dump saved to") 671 if err != nil { 672 log.WithError(err).Error("Cannot get pprof report") 673 } 674 675 files := s.Exec("ls -1 /tmp/") 676 for _, file := range files.ByLines() { 677 if !strings.Contains(file, "profile") { 678 continue 679 } 680 681 dest := filepath.Join( 682 BasePath, testPath, 683 fmt.Sprintf("%s.pprof", file)) 684 _ = s.ExecWithSudo(fmt.Sprintf("mv /tmp/%s %s", file, dest)) 685 } 686 cancel() 687 } 688 } 689 } 690 691 // DumpCiliumCommandOutput runs a variety of Cilium CLI commands and dumps their 692 // output to files. These files are gathered as part of each Jenkins job for 693 // postmortem debugging of build failures. 694 func (s *SSHMeta) DumpCiliumCommandOutput() { 695 696 testPath, err := CreateReportDirectory() 697 if err != nil { 698 s.logger.WithError(err).Errorf( 699 "cannot create test results path '%s'", testPath) 700 return 701 } 702 reportMap(testPath, ciliumCLICommands, s) 703 704 // No need to create file for bugtool because it creates an archive of files 705 // for us. 706 res := s.ExecWithSudo( 707 fmt.Sprintf("%s -t %s", CiliumBugtool, filepath.Join(BasePath, testPath)), 708 ExecOptions{SkipLog: true}) 709 if !res.WasSuccessful() { 710 s.logger.Errorf("Error running bugtool: %s", res.CombineOutput()) 711 } 712 713 } 714 715 // GatherLogs dumps Cilium, Cilium Docker, key-value store logs, and gops output 716 // to the directory testResultsPath 717 func (s *SSHMeta) GatherLogs() { 718 ciliumLogCommands := map[string]string{ 719 fmt.Sprintf("sudo journalctl -au %s --no-pager", DaemonName): "cilium.log", 720 fmt.Sprintf("sudo journalctl -au %s --no-pager", CiliumDockerDaemonName): "cilium-docker.log", 721 "sudo docker logs cilium-consul": "consul.log", 722 } 723 724 testPath, err := CreateReportDirectory() 725 if err != nil { 726 s.logger.WithError(err).Errorf( 727 "cannot create test results path '%s'", testPath) 728 return 729 } 730 reportMap(testPath, ciliumLogCommands, s) 731 732 ciliumStateCommands := []string{ 733 fmt.Sprintf("sudo rsync -rv --exclude=*.sock %s %s", RunDir, filepath.Join(BasePath, testPath, "lib")), 734 fmt.Sprintf("sudo rsync -rv --exclude=*.sock %s %s", LibDir, filepath.Join(BasePath, testPath, "run")), 735 fmt.Sprintf("sudo mv /tmp/core* %s", filepath.Join(BasePath, testPath)), 736 } 737 738 for _, cmd := range ciliumStateCommands { 739 res := s.Exec(cmd, ExecOptions{SkipLog: true}) 740 if !res.WasSuccessful() { 741 s.logger.Errorf("cannot gather files for cmd '%s': %s", cmd, res.CombineOutput()) 742 } 743 } 744 } 745 746 // ServiceAdd creates a new Cilium service with the provided ID, frontend, 747 // backends. Returns the result of creating said service. 748 func (s *SSHMeta) ServiceAdd(id int, frontend string, backends []string) *CmdRes { 749 cmd := fmt.Sprintf( 750 "service update --frontend '%s' --backends '%s' --id '%d' --rev", 751 frontend, strings.Join(backends, ","), id) 752 return s.ExecCilium(cmd) 753 } 754 755 // ServiceIsSynced checks that the Cilium service with the specified id has its 756 // metadata match that of the load balancer BPF maps 757 func (s *SSHMeta) ServiceIsSynced(id int) (bool, error) { 758 var svc *models.Service 759 svcRes := s.ServiceGet(id) 760 if !svcRes.WasSuccessful() { 761 return false, fmt.Errorf("cannot get service id %d: %s", id, svcRes.CombineOutput()) 762 } 763 err := svcRes.Unmarshal(&svc) 764 if err != nil { 765 return false, err 766 } 767 768 bpfLB, err := s.BpfLBList(false) 769 if err != nil { 770 return false, err 771 } 772 773 frontendAddr := net.JoinHostPort( 774 svc.Status.Realized.FrontendAddress.IP, 775 fmt.Sprintf("%d", svc.Status.Realized.FrontendAddress.Port)) 776 lb, ok := bpfLB[frontendAddr] 777 if ok == false { 778 return false, fmt.Errorf( 779 "frontend address from the service %d does not have it's corresponding frontend address(%s) on bpf maps", 780 id, frontendAddr) 781 } 782 783 for _, backendAddr := range svc.Status.Realized.BackendAddresses { 784 result := false 785 backendSVC := net.JoinHostPort( 786 *backendAddr.IP, 787 fmt.Sprintf("%d", backendAddr.Port)) 788 target := fmt.Sprintf("%s (%d)", backendSVC, id) 789 790 for _, addr := range lb { 791 if addr == target { 792 result = true 793 } 794 } 795 if result == false { 796 return false, fmt.Errorf( 797 "backend address %s does not exists on BPF load balancer metadata id=%d", target, id) 798 } 799 } 800 return true, nil 801 } 802 803 // ServiceList returns the output of `cilium service list` 804 func (s *SSHMeta) ServiceList() *CmdRes { 805 return s.ExecCilium("service list -o json") 806 } 807 808 // ServiceGet is a wrapper around `cilium service get <id>`. It returns the 809 // result of retrieving said service. 810 func (s *SSHMeta) ServiceGet(id int) *CmdRes { 811 return s.ExecCilium(fmt.Sprintf("service get '%d' -o json", id)) 812 } 813 814 // ServiceGetFrontendAddress returns a string with the frontend address and 815 // port. It returns an error if the ID cannot be retrieved. 816 func (s *SSHMeta) ServiceGetFrontendAddress(id int) (string, error) { 817 818 var svc *models.Service 819 res := s.ServiceGet(id) 820 if !res.WasSuccessful() { 821 return "", fmt.Errorf("Cannot get service id %d: %s", id, res.CombineOutput()) 822 } 823 824 err := res.Unmarshal(&svc) 825 if err != nil { 826 return "", err 827 } 828 829 frontendAddress := net.JoinHostPort( 830 svc.Status.Realized.FrontendAddress.IP, 831 fmt.Sprintf("%d", svc.Status.Realized.FrontendAddress.Port)) 832 return frontendAddress, nil 833 } 834 835 // ServiceGetIds returns an array with the IDs of all Cilium services. Returns 836 // an error if the IDs cannot be retrieved 837 func (s *SSHMeta) ServiceGetIds() ([]string, error) { 838 filter := `{range [*]}{@.status.realized.id}{"\n"}{end}` 839 res, err := s.ServiceList().Filter(filter) 840 if err != nil { 841 return nil, err 842 } 843 // trim the trailing \n 844 trimmed := strings.Trim(res.String(), "\n") 845 return strings.Split(trimmed, "\n"), nil 846 } 847 848 // ServiceDel is a wrapper around `cilium service delete <id>`. It returns the 849 // result of deleting said service. 850 func (s *SSHMeta) ServiceDel(id int) *CmdRes { 851 return s.ExecCilium(fmt.Sprintf("service delete '%d'", id)) 852 } 853 854 // ServiceDelAll is a wrapper around `cilium service delete --all`. It returns the 855 // result of the command. 856 func (s *SSHMeta) ServiceDelAll() *CmdRes { 857 return s.ExecCilium("service delete --all") 858 } 859 860 // SetUpCilium sets up Cilium as a systemd service with a hardcoded set of options. It 861 // returns an error if any of the operations needed to start Cilium fails. 862 func (s *SSHMeta) SetUpCilium() error { 863 return s.SetUpCiliumWithOptions("--tofqdns-enable-poller=true") 864 } 865 866 // SetUpCiliumWithOptions sets up Cilium as a systemd service with a given set of options. It 867 // returns an error if any of the operations needed to start Cilium fail. 868 func (s *SSHMeta) SetUpCiliumWithOptions(ciliumOpts string) error { 869 ciliumOpts += " --exclude-local-address=" + DockerBridgeIP + "/32" 870 ciliumOpts += " --exclude-local-address=" + FakeIPv4WorldAddress + "/32" 871 ciliumOpts += " --exclude-local-address=" + FakeIPv6WorldAddress + "/128" 872 873 systemdTemplate := ` 874 PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 875 CILIUM_OPTS=--kvstore consul --kvstore-opt consul.address=127.0.0.1:8500 --debug --pprof=true --log-system-load %s 876 INITSYSTEM=SYSTEMD` 877 878 err := RenderTemplateToFile("cilium", fmt.Sprintf(systemdTemplate, ciliumOpts), os.ModePerm) 879 if err != nil { 880 return err 881 } 882 defer os.Remove("cilium") 883 884 res := s.Exec("sudo cp /vagrant/cilium /etc/sysconfig/cilium") 885 if !res.WasSuccessful() { 886 return fmt.Errorf("%s", res.CombineOutput()) 887 } 888 res = s.Exec("sudo systemctl restart cilium") 889 if !res.WasSuccessful() { 890 return fmt.Errorf("%s", res.CombineOutput()) 891 } 892 return nil 893 } 894 895 func (s *SSHMeta) SetUpCiliumWithSockops() error { 896 return s.SetUpCiliumWithOptions("--sockops-enable --tofqdns-enable-poller=true") 897 } 898 899 // SetUpCiliumInIpvlanMode starts cilium-agent in the ipvlan mode 900 func (s *SSHMeta) SetUpCiliumInIpvlanMode(ipvlanMasterDevice string) error { 901 return s.SetUpCiliumWithOptions("--tunnel=disabled --datapath-mode=ipvlan --ipvlan-master-device=" + ipvlanMasterDevice) 902 } 903 904 // WaitUntilReady waits until the output of `cilium status` returns with code 905 // zero. Returns an error if the output of `cilium status` returns a nonzero 906 // return code after the specified timeout duration has elapsed. 907 func (s *SSHMeta) WaitUntilReady(timeout time.Duration) error { 908 909 body := func() bool { 910 res := s.ExecCilium("status") 911 s.logger.Infof("Cilium status is %t", res.WasSuccessful()) 912 return res.WasSuccessful() 913 } 914 err := WithTimeout(body, "Cilium is not ready", &TimeoutConfig{Timeout: timeout}) 915 return err 916 } 917 918 // RestartCilium reloads cilium on this host, then waits for it to become 919 // ready again. 920 func (s *SSHMeta) RestartCilium() error { 921 ginkgoext.By("Restarting Cilium") 922 923 res := s.ExecWithSudo("systemctl restart cilium") 924 if !res.WasSuccessful() { 925 return fmt.Errorf("%s", res.CombineOutput()) 926 } 927 if err := s.WaitUntilReady(CiliumStartTimeout); err != nil { 928 return err 929 } 930 if !s.WaitEndpointsReady() { 931 return fmt.Errorf("Endpoints are not ready after timeout") 932 } 933 return nil 934 } 935 936 // AddIPToLoopbackDevice adds the specified IP (assumed to be in form <ip>/<mask>) 937 // to the loopback device on s. 938 func (s *SSHMeta) AddIPToLoopbackDevice(ip string) *CmdRes { 939 return s.ExecWithSudo(fmt.Sprintf("ip addr add dev lo %s", ip)) 940 } 941 942 // RemoveIPFromLoopbackDevice removes the specified IP (assumed to be in form <ip>/<mask>) 943 // from the loopback device on s. 944 func (s *SSHMeta) RemoveIPFromLoopbackDevice(ip string) *CmdRes { 945 return s.ExecWithSudo(fmt.Sprintf("ip addr del dev lo %s", ip)) 946 } 947 948 // FlushGlobalConntrackTable flushes the global connection tracking table. 949 func (s *SSHMeta) FlushGlobalConntrackTable() *CmdRes { 950 return s.ExecCilium("bpf ct flush global") 951 }