go.ligato.io/vpp-agent/v3@v3.5.0/tests/e2e/e2etest/vppagent.go (about) 1 // Copyright (c) 2020 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 e2etest 16 17 import ( 18 "context" 19 "fmt" 20 "path/filepath" 21 "regexp" 22 "strings" 23 "testing" 24 25 docker "github.com/fsouza/go-dockerclient" 26 "github.com/go-errors/errors" 27 "github.com/onsi/gomega" 28 "github.com/vishvananda/netns" 29 "go.ligato.io/cn-infra/v2/health/statuscheck/model/status" 30 "go.ligato.io/cn-infra/v2/logging" 31 "google.golang.org/grpc" 32 "google.golang.org/protobuf/proto" 33 34 "go.ligato.io/vpp-agent/v3/client" 35 "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" 36 ctl "go.ligato.io/vpp-agent/v3/cmd/agentctl/client" 37 "go.ligato.io/vpp-agent/v3/pkg/models" 38 kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" 39 "go.ligato.io/vpp-agent/v3/plugins/linux/ifplugin/linuxcalls" 40 "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" 41 ) 42 43 const ( 44 agentImage = "ligato/vpp-agent:latest" 45 agentLabelKey = "e2e.test.vppagent" 46 agentNamePrefix = "e2e-test-vppagent-" 47 agentInitTimeout = 15 // seconds 48 agentStopTimeout = 3 // seconds 49 ) 50 51 var vppPingRegexp = regexp.MustCompile("Statistics: ([0-9]+) sent, ([0-9]+) received, ([0-9]+)% packet loss") 52 53 // Agent represents running VPP-Agent test component 54 type Agent struct { 55 ComponentRuntime 56 client ctl.APIClient 57 ctx *TestCtx 58 name string 59 } 60 61 // NewAgent creates and starts new VPP-Agent container 62 func NewAgent(ctx *TestCtx, name string, optMods ...AgentOptModifier) (*Agent, error) { 63 // compute options 64 opts := DefaultAgentOpt(ctx, name) 65 for _, mod := range optMods { 66 mod(opts) 67 } 68 69 // create struct for Agent 70 agent := &Agent{ 71 ComponentRuntime: opts.Runtime, 72 ctx: ctx, 73 name: name, 74 } 75 76 // get runtime specific options and start agent in runtime environment 77 startOpts, err := opts.RuntimeStartOptions(ctx, opts) 78 if err != nil { 79 return nil, errors.Errorf("can't get agent %s start option for runtime due to: %v", name, err) 80 } 81 err = agent.Start(startOpts) 82 if err != nil { 83 return nil, errors.Errorf("can't start agent %s due to: %v", name, err) 84 } 85 agent.client, err = ctl.NewClient(agent.IPAddress()) 86 if err != nil { 87 return nil, errors.Errorf("can't create client for %s due to: %v", name, err) 88 } 89 90 agent.ctx.Eventually(agent.checkReady, agentInitTimeout, checkPollingInterval).Should(gomega.Succeed()) 91 if opts.InitialResync { 92 agent.Sync() 93 } 94 return agent, nil 95 } 96 97 func (agent *Agent) Stop(options ...interface{}) error { 98 if err := agent.ComponentRuntime.Stop(options); err != nil { 99 // not additionally cleaning up after attempting to stop test topology component because 100 // it would lock access to further inspection of this component (i.e. why it won't stop) 101 return err 102 } 103 // cleanup 104 if err := agent.client.Close(); err != nil { 105 return err 106 } 107 delete(agent.ctx.agents, agent.name) 108 return nil 109 } 110 111 // AgentStartOptionsForContainerRuntime translates AgentOpt to options for ComponentRuntime.Start(option) 112 // method implemented by ContainerRuntime 113 func AgentStartOptionsForContainerRuntime(ctx *TestCtx, options interface{}) (interface{}, error) { 114 opts, ok := options.(*AgentOpt) 115 if !ok { 116 return nil, errors.Errorf("expected AgentOpt but got %+v", options) 117 } 118 119 // construct vpp-agent container creation options 120 agentLabel := agentNamePrefix + opts.Name 121 createOpts := &docker.CreateContainerOptions{ 122 Context: ctx.ctx, 123 Name: agentLabel, 124 Config: &docker.Config{ 125 Image: opts.Image, 126 Labels: map[string]string{ 127 agentLabelKey: opts.Name, 128 }, 129 Env: opts.Env, 130 AttachStderr: true, 131 AttachStdout: true, 132 }, 133 HostConfig: &docker.HostConfig{ 134 PublishAllPorts: true, 135 Privileged: true, 136 PidMode: "host", 137 Binds: []string{ 138 "/var/run/docker.sock:/var/run/docker.sock", 139 ctx.DataDir + ":/testdata:ro", 140 filepath.Join(ctx.DataDir, "certs") + ":/etc/certs:ro", 141 shareVolumeName + ":" + ctx.ShareDir, 142 }, 143 }, 144 } 145 if opts.ContainerOptsHook != nil { 146 opts.ContainerOptsHook(createOpts) 147 } 148 return &ContainerStartOptions{ 149 ContainerOptions: createOpts, 150 Pull: false, 151 AttachLogs: true, 152 }, nil 153 } 154 155 // TODO this is runtime specific -> integrate it into runtime concept 156 func removeDanglingAgents(t *testing.T, dockerClient *docker.Client) { 157 // remove any running vpp-agents prior to starting a new test 158 containers, err := dockerClient.ListContainers(docker.ListContainersOptions{ 159 All: true, 160 Filters: map[string][]string{ 161 "label": {agentLabelKey}, 162 }, 163 }) 164 if err != nil { 165 t.Fatalf("failed to list existing vpp-agents: %v", err) 166 } 167 for _, container := range containers { 168 err = dockerClient.RemoveContainer(docker.RemoveContainerOptions{ 169 ID: container.ID, 170 Force: true, 171 }) 172 if err != nil { 173 t.Fatalf("failed to remove existing vpp-agents: %v", err) 174 } else { 175 t.Logf("removed existing vpp-agent: %s", container.Labels[agentLabelKey]) 176 } 177 } 178 } 179 180 func (agent *Agent) LinuxInterfaceHandler() linuxcalls.NetlinkAPI { 181 agent.ctx.t.Helper() 182 ns, err := netns.GetFromPid(agent.PID()) 183 if err != nil { 184 agent.ctx.t.Fatalf("unable to get netns (PID %v)", agent.PID()) 185 } 186 ifHandler := linuxcalls.NewNetLinkHandlerNs(ns, logging.DefaultLogger) 187 return ifHandler 188 } 189 190 func (agent *Agent) Client() ctl.APIClient { 191 return agent.client 192 } 193 194 // GenericClient provides generic client for communication with default VPP-Agent test component 195 func (agent *Agent) GenericClient() client.GenericClient { 196 agent.ctx.t.Helper() 197 c, err := agent.client.GenericClient() 198 if err != nil { 199 agent.ctx.t.Fatalf("Failed to get generic VPP-agent client: %v", err) 200 } 201 return c 202 } 203 204 // GRPCConn provides GRPC client connection for communication with default VPP-Agent test component 205 func (agent *Agent) GRPCConn() *grpc.ClientConn { 206 agent.ctx.t.Helper() 207 conn, err := agent.client.GRPCConn() 208 if err != nil { 209 agent.ctx.t.Fatalf("Failed to get gRPC connection: %v", err) 210 } 211 return conn 212 } 213 214 // Sync runs downstream resync and returns the list of executed operations. 215 func (agent *Agent) Sync() kvs.RecordedTxnOps { 216 agent.ctx.t.Helper() 217 txn, err := agent.client.SchedulerResync(context.Background(), types.SchedulerResyncOptions{ 218 Retry: true, 219 }) 220 if err != nil { 221 agent.ctx.t.Fatalf("Downstream resync request has failed: %v", err) 222 } 223 if txn.Start.IsZero() { 224 agent.ctx.t.Fatalf("Downstream resync returned empty transaction record: %v", txn) 225 } 226 return txn.Executed 227 } 228 229 // IsInSync checks if the agent NB config and the SB state (VPP+Linux) are in-sync. 230 func (agent *Agent) IsInSync() bool { 231 agent.ctx.t.Helper() 232 ops := agent.Sync() 233 for _, op := range ops { 234 if !op.NOOP { 235 return false 236 } 237 } 238 return true 239 } 240 241 func (agent *Agent) checkReady() error { 242 agentStatus, err := agent.client.Status(agent.ctx.ctx) 243 if err != nil { 244 return fmt.Errorf("query to get %s status failed: %v", agent.name, err) 245 } 246 agentPlugin, ok := agentStatus.PluginStatus["VPPAgent"] 247 if !ok { 248 return fmt.Errorf("%s plugin status missing", agent.name) 249 } 250 if agentPlugin.State != status.OperationalState_OK { 251 return fmt.Errorf("%s status: %v", agent.name, agentPlugin.State.String()) 252 } 253 return nil 254 } 255 256 // ExecVppctl returns output from vppctl for given action and arguments. 257 func (agent *Agent) ExecVppctl(action string, args ...string) (string, error) { 258 cmd := append([]string{"-s", "/run/vpp/cli.sock", action}, args...) 259 stdout, _, err := agent.ExecCmd("vppctl", cmd...) 260 if err != nil { 261 return "", fmt.Errorf("execute `vppctl %s` error: %v", strings.Join(cmd, " "), err) 262 } 263 if Debug { 264 agent.ctx.t.Logf("executed (vppctl %v): %v", strings.Join(cmd, " "), stdout) 265 } 266 267 return stdout, nil 268 } 269 270 // PingFromVPPAsCallback can be used to ping repeatedly inside the assertions "Eventually" 271 // and "Consistently" from Omega. 272 func (agent *Agent) PingFromVPPAsCallback(destAddress string, args ...string) func() error { 273 return func() error { 274 return agent.PingFromVPP(destAddress, args...) 275 } 276 } 277 278 // PingFromVPP pings <dstAddress> from inside the VPP. 279 func (agent *Agent) PingFromVPP(destAddress string, args ...string) error { 280 // run ping on VPP using vppctl 281 stdout, err := agent.ExecVppctl("ping", append([]string{destAddress}, args...)...) 282 if err != nil { 283 return err 284 } 285 286 // parse output 287 matches := vppPingRegexp.FindStringSubmatch(stdout) 288 sent, recv, loss, err := parsePingOutput(stdout, matches) 289 if err != nil { 290 return err 291 } 292 agent.ctx.Logger.Printf("VPP ping %s: sent=%d, received=%d, loss=%d%%", 293 destAddress, sent, recv, loss) 294 295 if sent == 0 || loss >= 50 { 296 return fmt.Errorf("failed to ping '%s': %s", destAddress, matches[0]) 297 } 298 return nil 299 } 300 301 func (agent *Agent) getKVDump(value proto.Message, view kvs.View) []kvs.RecordedKVWithMetadata { 302 agent.ctx.t.Helper() 303 model, err := models.GetModelFor(value) 304 if err != nil { 305 agent.ctx.t.Fatalf("Failed to get model for value %v: %v", value, err) 306 } 307 kvDump, err := agent.client.SchedulerDump(context.Background(), types.SchedulerDumpOptions{ 308 KeyPrefix: model.KeyPrefix(), 309 View: view.String(), 310 }) 311 if err != nil { 312 agent.ctx.t.Fatalf("Request to dump values failed: %v", err) 313 } 314 return kvDump 315 } 316 317 // GetValue retrieves value(s) as seen by the given view 318 func (agent *Agent) GetValue(value proto.Message, view kvs.View) proto.Message { 319 agent.ctx.t.Helper() 320 key, err := models.GetKey(value) 321 if err != nil { 322 agent.ctx.t.Fatalf("Failed to get key for value %v: %v", value, err) 323 } 324 kvDump := agent.getKVDump(value, view) 325 for _, kv := range kvDump { 326 if kv.Key == key { 327 return kv.Value.Message 328 } 329 } 330 return nil 331 } 332 333 // GetValueMetadata retrieves metadata associated with the given value. 334 func (agent *Agent) GetValueMetadata(value proto.Message, view kvs.View) (metadata interface{}) { 335 agent.ctx.t.Helper() 336 key, err := models.GetKey(value) 337 if err != nil { 338 agent.ctx.t.Fatalf("Failed to get key for value %v: %v", value, err) 339 } 340 kvDump := agent.getKVDump(value, view) 341 for _, kv := range kvDump { 342 if kv.Key == key { 343 return kv.Metadata 344 } 345 } 346 return nil 347 } 348 349 // NumValues returns number of values found under the given model 350 func (agent *Agent) NumValues(value proto.Message, view kvs.View) int { 351 agent.ctx.t.Helper() 352 return len(agent.getKVDump(value, view)) 353 } 354 355 func (agent *Agent) getValueStateByKey(key, derivedKey string) kvscheduler.ValueState { 356 agent.ctx.t.Helper() 357 values, err := agent.client.SchedulerValues(context.Background(), types.SchedulerValuesOptions{ 358 Key: key, 359 }) 360 if err != nil { 361 agent.ctx.t.Fatalf("Request to obtain value status has failed: %v", err) 362 } 363 if len(values) != 1 { 364 agent.ctx.t.Fatalf("Expected single value status, got status for %d values", len(values)) 365 } 366 st := values[0] 367 if st.GetValue().GetKey() != key { 368 agent.ctx.t.Fatalf("Received value status for unexpected key: %v", st) 369 } 370 if derivedKey != "" { 371 for _, derVal := range st.DerivedValues { 372 if derVal.Key == derivedKey { 373 return derVal.State 374 } 375 } 376 return kvscheduler.ValueState_NONEXISTENT 377 } 378 return st.GetValue().GetState() 379 } 380 381 func (agent *Agent) GetValueState(value proto.Message) kvscheduler.ValueState { 382 agent.ctx.t.Helper() 383 key := models.Key(value) 384 return agent.getValueStateByKey(key, "") 385 } 386 387 func (agent *Agent) GetValueStateClb(value proto.Message) func() kvscheduler.ValueState { 388 return func() kvscheduler.ValueState { 389 return agent.GetValueState(value) 390 } 391 } 392 393 func (agent *Agent) GetValueStateByKey(key string) kvscheduler.ValueState { 394 agent.ctx.t.Helper() 395 return agent.getValueStateByKey(key, "") 396 } 397 398 func (agent *Agent) GetValueStateByKeyClb(key string) func() kvscheduler.ValueState { 399 return func() kvscheduler.ValueState { 400 return agent.GetValueStateByKey(key) 401 } 402 } 403 404 func (agent *Agent) GetDerivedValueState(baseValue proto.Message, derivedKey string) kvscheduler.ValueState { 405 agent.ctx.t.Helper() 406 key := models.Key(baseValue) 407 return agent.getValueStateByKey(key, derivedKey) 408 } 409 410 func (agent *Agent) GetDerivedValueStateClb(baseValue proto.Message, derivedKey string) func() kvscheduler.ValueState { 411 return func() kvscheduler.ValueState { 412 return agent.GetDerivedValueState(baseValue, derivedKey) 413 } 414 }