Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add infrastructure for integration tests #409

Merged
merged 20 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2022-present Open Networking Foundation
name: E2E integration tests

on:
push:
branches:
- master
pull_request:

jobs:
test-integration-up4:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/setup-go@v2
with:
go-version: '1.13'
- name: Run integration tests for PFCP Agent & UP4
run: |
make test-integration
osinstom marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@ RUN mkdir /bess_pb && \
FROM golang AS pfcpiface-build
WORKDIR /pfcpiface

COPY pfcpiface/go.mod ./
COPY pfcpiface/go.sum ./
COPY go.mod /pfcpiface/go.mod
COPY go.sum /pfcpiface/go.sum

RUN go mod download

COPY pfcpiface .
RUN CGO_ENABLED=0 go build -o /bin/pfcpiface
COPY . /pfcpiface
RUN CGO_ENABLED=0 go build -o /bin/pfcpiface ./pfcpiface

# Stage pfcpiface: runtime image of pfcpiface toward SMF/SPGW-C
FROM alpine AS pfcpiface
Expand Down
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ DOCKER_LABEL_BUILD_DATE ?= $(shell date -u "+%Y-%m-%dT%H:%M:%SZ")

DOCKER_TARGETS ?= bess pfcpiface

# Golang grpc/protobuf generation
BESS_PB_DIR ?= pfcpiface
PTF_PB_DIR ?= ptf/lib

# https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target
docker-build:
for target in $(DOCKER_TARGETS); do \
Expand Down Expand Up @@ -60,9 +64,11 @@ output:
.;
rm -rf output && mkdir output && tar -xf output.tar -C output && rm -f output.tar

# Golang grpc/protobuf generation
BESS_PB_DIR ?= pfcpiface
PTF_PB_DIR ?= ptf/lib
test-integration:
docker-compose -f test/integration/infra/docker-compose.yml rm -fsv
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) docker-compose -f test/integration/infra/docker-compose.yml up --build -d
go test -count=1 ./test/integration/...
docker-compose -f test/integration/infra/docker-compose.yml rm -fsv

pb:
DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) docker build $(DOCKER_PULL) $(DOCKER_BUILD_ARGS) \
Expand All @@ -85,4 +91,4 @@ fmt:
golint:
@docker run --rm -v $(CURDIR):/app -w /app/pfcpiface golangci/golangci-lint:latest golangci-lint run -v --config /app/.golangci.yml

.PHONY: docker-build docker-push output pb fmt golint
.PHONY: docker-build docker-push output pb fmt golint test-integration
24 changes: 24 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module github.com/omec-project/upf-epc
osinstom marked this conversation as resolved.
Show resolved Hide resolved

go 1.13

require (
github.com/Showmax/go-fqdn v1.0.0
github.com/golang/protobuf v1.5.2
github.com/google/gopacket v1.1.19
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/kr/pretty v0.2.1 // indirect
github.com/libp2p/go-reuseport v0.1.0
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/p4lang/p4runtime v1.3.0
github.com/prometheus/client_golang v1.11.0
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/wmnsk/go-pfcp v0.0.14
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/text v0.3.4 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
google.golang.org/grpc v1.43.0
google.golang.org/protobuf v1.27.1
)
74 changes: 15 additions & 59 deletions pfcpiface/go.sum → go.sum

Large diffs are not rendered by default.

18 changes: 0 additions & 18 deletions pfcpiface/go.mod

This file was deleted.

229 changes: 229 additions & 0 deletions pkg/pfcpsim/pfcpsim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package pfcpsim

import (
"context"
"errors"
"fmt"
"github.com/wmnsk/go-pfcp/ie"
"github.com/wmnsk/go-pfcp/message"
"net"
"sync"
"time"
)

const (
PFCPStandardPort = 8805
)

// PFCPClient enables to simulate a client sending PFCP messages towards the UPF.
// It provides two usage modes:
// - 1st mode enables high-level PFCP operations (e.g., SetupAssociation())
// - 2nd mode gives a user more control over PFCP sequence flow
// and enables send and receive of individual messages (e.g., SendAssociationSetupRequest(), PeekNextResponse())
type PFCPClient struct {
aliveLock sync.Mutex
isAssociationAlive bool

ctx context.Context
cancelHeartbeats context.CancelFunc

heartbeatsChan chan *message.HeartbeatResponse
recvChan chan message.Message

sequenceNumber uint32
seqNumLock sync.Mutex

localAddr string
conn *net.UDPConn
}

func NewPFCPClient(localAddr string) *PFCPClient {
client := &PFCPClient{
sequenceNumber: 0,
localAddr: localAddr,
}
client.ctx = context.Background()
client.heartbeatsChan = make(chan *message.HeartbeatResponse)
client.recvChan = make(chan message.Message)
return client
}

func (c *PFCPClient) getNextSequenceNumber() uint32 {
c.seqNumLock.Lock()
defer c.seqNumLock.Unlock()

c.sequenceNumber++

return c.sequenceNumber
}

func (c *PFCPClient) resetSequenceNumber() {
c.seqNumLock.Lock()
defer c.seqNumLock.Unlock()

c.sequenceNumber = 0
}

func (c *PFCPClient) setAssociationAlive(status bool) {
c.aliveLock.Lock()
defer c.aliveLock.Unlock()

c.isAssociationAlive = status
}

func (c *PFCPClient) sendMsg(msg message.Message) error {
b := make([]byte, msg.MarshalLen())
if err := msg.MarshalTo(b); err != nil {
return err
}

if _, err := c.conn.Write(b); err != nil {
return err
}

return nil
}

func (c *PFCPClient) receiveFromN4() {
buf := make([]byte, 1500)
for {
n, _, err := c.conn.ReadFrom(buf)
if err != nil {
continue
}

msg, err := message.Parse(buf[:n])
if err != nil {
continue
}

if hbResp, ok := msg.(*message.HeartbeatResponse); ok {
c.heartbeatsChan <- hbResp
} else {
c.recvChan <- msg
}
}
}

func (c *PFCPClient) ConnectN4(remoteAddr string) error {
raddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", remoteAddr, PFCPStandardPort))
if err != nil {
return err
}

conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
return err
}

c.conn = conn

go c.receiveFromN4()

return nil
}

func (c *PFCPClient) DisconnectN4() {
c.cancelHeartbeats()
c.conn.Close()
}

func (c *PFCPClient) PeekNextHeartbeatResponse(timeout time.Duration) (*message.HeartbeatResponse, error) {
select {
case msg := <-c.heartbeatsChan:
return msg, nil
case <-time.After(timeout * time.Second):
return nil, errors.New("timeout waiting for response")
}
}

func (c *PFCPClient) PeekNextResponse(timeout time.Duration) (message.Message, error) {
select {
case msg := <-c.recvChan:
return msg, nil
case <-time.After(timeout * time.Second):
return nil, errors.New("timeout waiting for response")
}
}

// TODO: enable passing custom IEs
func (c *PFCPClient) SendAssociationSetupRequest() error {
c.resetSequenceNumber()

assocReq := message.NewAssociationSetupRequest(
c.getNextSequenceNumber(),
ie.NewRecoveryTimeStamp(time.Now()),
ie.NewNodeID(c.localAddr, "", ""),
)

return c.sendMsg(assocReq)
}

func (c *PFCPClient) SendHeartbeatRequest() error {
hbReq := message.NewHeartbeatRequest(
c.getNextSequenceNumber(),
ie.NewRecoveryTimeStamp(time.Now()),
ie.NewSourceIPAddress(net.ParseIP(c.localAddr), nil, 0),
)

return c.sendMsg(hbReq)
}

func (c *PFCPClient) StartHeartbeats(stopCtx context.Context) {
ticker := time.NewTicker(5 * time.Second)
for {
select {
case <-stopCtx.Done():
return
case <-ticker.C:
c.SendAndRecvHeartbeat()
}
}
}

func (c *PFCPClient) SendAndRecvHeartbeat() error {
err := c.SendHeartbeatRequest()
if err != nil {
return err
}

_, err = c.PeekNextHeartbeatResponse(5)
if err != nil {
c.setAssociationAlive(false)
return err
}

c.setAssociationAlive(true)

return nil
}

func (c *PFCPClient) SetupAssociation() error {
err := c.SendAssociationSetupRequest()
if err != nil {
return err
}

resp, err := c.PeekNextResponse(5)
if err != nil {
return err
}

if _, ok := resp.(*message.AssociationSetupResponse); !ok {
return fmt.Errorf("invalid message received, expected association setup response")
}

ctx, cancelFunc := context.WithCancel(c.ctx)
c.cancelHeartbeats = cancelFunc

go c.StartHeartbeats(ctx)

return nil
}

func (c *PFCPClient) IsAssociationAlive() bool {
c.aliveLock.Lock()
defer c.aliveLock.Unlock()

return c.isAssociationAlive
}
30 changes: 30 additions & 0 deletions test/integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# E2E integration tests

The tests defined in this directory implement the so-called "broad integration tests"
(they are sometimes called system tests or E2E tests, see [Martin Fowler's blog](https://martinfowler.com/bliki/IntegrationTest.html)).

The purpose of E2E integration tests is to verify the behavior of the PFCP Agent with different flavors of PFCP messages,
as well as to check PFCP Agent's integration with data plane components (UP4, BESS-UPF). In detail, these tests verify if
PFCP messages are handled as expected by the PFCP Agent, and if the PFCP Agent installs correct packet forwarding rules onto the fast-path target (UP4/BESS).

## Structure

- `infra/`: contains build and deployment files.
- `config/`: contains app-specific config (e.g., `upf.json`).
- the current directory contains `*_test.go` files defining test scenarios.

## Overview

The E2E integration tests are integrated within the Go test framework and can be run by `go test`.

The tests use `docker-compose` to set up `pfcpiface` and `mock-up4` (the BMv2 container running the UP4 pipeline) images.
Then, a given test case generates PFCP messages towards `pfcpiface` and fetches the runtime forwarding configuration from the
data plane component (e.g., via P4Runtime for UP4) to verify forwarding state configuration.

## Run tests

To run all E2E integration tests invoke the command below from the root directory:

```bash
make test-integration
```