Initial commit

Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
This commit is contained in:
Tuan-Dat Tran
2024-12-31 13:36:22 +01:00
commit 931652c494
78 changed files with 46976 additions and 0 deletions

6
5g-uulm-network-monitoring/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/target
/output/*
*.pcap
*.log
/resources/video/*
!/resources/video/.gitignore

View File

@@ -0,0 +1,79 @@
variables:
DOCKER_TAG_PREFIX: "uulm"
KANIKO_IMAGE: "gcr.io/kaniko-project/executor:v1.9.0-debug"
CI_REGISTRY: 192.168.100.2:5000
CI_COMMIT_TAG: "develop"
DOCKER_CONFIG: "/kaniko/.docker/"
stages:
- build
- deploy
.use-kaniko:
image:
name: $KANIKO_IMAGE
entrypoint: [""]
.multi:
parallel:
matrix:
- COMPONENT_NAME: "videoprobe"
DOCKERFILE_PATH: "Dockerfile"
- COMPONENT_NAME: "ffmpeg"
DOCKERFILE_PATH: "ffmpeg.Dockerfile"
- COMPONENT_NAME: "nginx"
DOCKERFILE_PATH: "nginx.Dockerfile"
.branches:
only:
- master
- dev
build:
stage: build
extends:
- .multi
- .use-kaniko
- .branches
script:
- echo "Building $COMPONENT_NAME"
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${DOCKERFILE_PATH}"
--destination "${CI_REGISTRY}/${DOCKER_TAG_PREFIX}/${COMPONENT_NAME}:${CI_COMMIT_TAG}"
--no-push
deploy:
before_script:
- |
echo "-----BEGIN CERTIFICATE-----
MIIClDCCAf2gAwIBAgIUac+ko3JCbLKoWfsw4zZ7jmK2hWUwDQYJKoZIhvcNAQEF
BQAwfDELMAkGA1UEBhMCWFgxDDAKBgNVBAgMA04vQTEMMAoGA1UEBwwDTi9BMSAw
HgYDVQQKDBdTZWxmLXNpZ25lZCBjZXJ0aWZpY2F0ZTEvMC0GA1UEAwwmMTkyLjE2
OC4xMDAuMjogU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwHhcNMjMwNzI4MDcyOTAz
WhcNMjQwNzI3MDcyOTAzWjB8MQswCQYDVQQGEwJYWDEMMAoGA1UECAwDTi9BMQww
CgYDVQQHDANOL0ExIDAeBgNVBAoMF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRlMS8w
LQYDVQQDDCYxOTIuMTY4LjEwMC4yOiBTZWxmLXNpZ25lZCBjZXJ0aWZpY2F0ZTCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAniESM4TXYpLuqkkkXe6wdAlVo/In
iaPVIV6WH64dab8s5idpkl6ThvkpuON6czF8oQtEC5OCWvHUmPf8wu29kC7s2Gop
8yeWlu8BG0fD28qDxhURbDoxqlrbEVQN3r+ekYKlEm83yxM4Zay+r1+s1fzYkf5q
/O0n8WV74Sf4/tkCAwEAAaMTMBEwDwYDVR0RBAgwBocEwKhkAjANBgkqhkiG9w0B
AQUFAAOBgQCJ5618apVWYG2+mizc3HgDgOrY88wUdXOnpejj5r6YrhaQp/vUHGmY
Tv5E3G+lYtNJDzqfjMNgZXGzK6A7D66tU+MuO7yHX7a370JyBF/5rc0YQM+ygIlr
2WQ58cXzY9INB2l+JTbzDXA+gL7EvGzu/8CWoUd9RabSTRRz6hd2OQ==
-----END CERTIFICATE-----" >> /kaniko/ssl/certs/additional-ca-cert-bundle.crt
stage: deploy
extends:
- .multi
- .use-kaniko
- .branches
script:
- echo "Deploying $COMPONENT_NAME"
- echo {\"auths\":{\"192.168.100.2:5000/v2/\":{\"username\":\"5g-iana\",\"password\":\"5g-iana\"}}} > /kaniko/.docker/config.json
- /kaniko/executor
--skip-tls-verify
--context "${CI_PROJECT_DIR}"
--dockerfile "${DOCKERFILE_PATH}"
--destination "${CI_REGISTRY}/${DOCKER_TAG_PREFIX}/${COMPONENT_NAME}:${CI_COMMIT_TAG}"

2374
5g-uulm-network-monitoring/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
[package]
name = "videoprobe"
version = "0.2.0"
edition = "2021"
[profile.dev]
[profile.release]
lto = true
strip = true
panic = "abort"
[dependencies]
byte-unit = "4.0.18"
chrono = "0.4.24"
clap = { version = "4.1.6", features = ["derive", "string", "unicode"] }
gpsd_proto = "0.7.0"
rocket = { version = "0.5.0-rc.1", features = ["json"] }
serde_json = "1.0.94"
tokio = { version = "1.26.0", features = ["full"] }
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-subscriber = "0.3.16"
serde = { version = "1.0", features = ["derive"] }
reqwest = "0.11.22"
local-ip-address = "0.5.6"
[features]
rtt = []
throughput = []

View File

@@ -0,0 +1,22 @@
# Build Stage
FROM rust:1.74 as builder
WORKDIR /usr/src/5G_VideoProbe
COPY src src
COPY Cargo.* .
RUN cargo install -F rtt --path .
# Runtime Stage
FROM debian:stable-slim as runtime
RUN apt-get update && apt-get install -y \
tshark \
gpsd \
iputils-ping \
ffmpeg \
tcpdump \
&& rm -rf /var/lib/apt/lists/*
COPY Rocket.toml /etc/videoprobe/Rocket.toml
COPY run.sh /run.sh
COPY --from=builder /usr/local/cargo/bin/videoprobe /usr/local/bin/videoprobe
CMD [ "/run.sh" ]

View File

@@ -0,0 +1,105 @@
# 5G-IANA: UULM Network Monitoring
This repository contains the CI/CD and Dockerfiles necessary to build
the UULM Network Monitoring Tool.
This tool is used to deploy a RTMP stream behind a reverse proxy and a network
monitoring client which has a consumer for the stream.
The monitoring tool outputs the timestamp, lat, lon, byts per second and rtt.
## Feature Flags
There are currently two feature flags for this tool which enable us to record
and output data to the endpoint in multiple formats.
### RTT
With only the `rtt` flag enabled the tool records and emits the `rtt` towards
the `ping_ip`. One output would look like this:
```csv
# lat, lon, rtt
0.00000000,0.00000000,6480000 ns
```
```sh
curl -X GET -H "Content-Type: application/json" -d "{ \"endpoint_ip\": [\"http://172.17.0.1:41002/upload\"], \"ping_ip\": \"1.1\" }" http://172.17.0.1:8000/demo/start
```
```sh
cargo build -F rtt
```
### RTT/Throughput
```csv
# unix timestamp, lat, lon, bytes per second, rtt
1716480819,0.00000000,0.00000000,1.86 KB,6960000 ns
```
```sh
curl -X GET -H "Content-Type: application/json" -d "{ \"endpoint_ip\": [\"http://172.17.0.1:41002/upload\"], \"ping_ip\": \"1.1\" , \"stream_url\": \"rtmp://132.252.100.137:31000/live/test\" }" http://172.17.0.1:8000/demo/start
```
```sh
cargo build -F rtt -F throughput
```
### Throughput (not yet tested)
```csv
# lat, lon, throughput per second
0.00000000,0.00000000,1.86 KB
```
```sh
curl -X GET -H "Content-Type: application/json" -d "{ \"endpoint_ip\": [\"http://172.17.0.1:41002/upload\"], \"stream_url\": \"rtmp://132.252.100.137:31000/live/test\" }" http://172.17.0.1:8000/demo/start
```
```sh
cargo build -F throughput
```
## Local Deployment
### Example
```sh
# Server
docker build -f nginx.Dockerfile -t ffmpeg-nginx .; docker run -p 1935:1935 ffmpeg-nginx:latest
# Client
# Add features as needed
cargo run -F rtt -F throughput -- -p "/pcap/receiver.pcap" -o "/output/videoprobe_$(date '+%s').log"
# Configure Client
curl -X GET -H "Content-Type: application/json" -d "{ \"endpoint_ip\": [\"http://172.17.0.1:41002/upload\"], \"ping_ip\": \"1.1\" , \"stream_url\": \"rtmp://localhost:1935/live/test\" }" http://172.17.0.1:8000/demo/start
```
## Open internal Ports
- **1935**: RTMP of `web` providing `sender`-stream
- **8000**: Endpoint of `videoprobe`
## Configurations/Environment Variables
- STREAM<sub>URL</sub>: The URL of a rtmp based video stream. In this
environment it is to be `web`.
- RUST<sub>LOG</sub>: The logging level of the network monitoring tool
itself.
- ROCKET<sub>CONFIG</sub>: Might as well be constant, but specifies the
path for the configuration of the API endpoint of `videoprobe`.
- VP<sub>TARGET</sub>: The API endpoint to upload the collected data to
with with a `POST` request. This is variable should not be used during
the demonstration.
- CMD: Needed as an alternative to using the `command:` keyword, which
is usually used to overwrite a containers entrypoint.
- GNSS<sub>ENABLED</sub>: Used for choosing whether the videoprobe
should be running with "dry gps". Dry GPS means that the tool will be
running without GPS capabilities in case the user is sure that there
is no GNSS device present or satalite connectivity can't be ensured.
- GNSS<sub>DEV</sub>: The path of the mounted GNSS Device. Needed to
start gpsd inside of the container. Changes to it should also be
applied to the corresponding
[file:local-docker-compose.yml](local-docker-compose.yml) and
[file:docker-compose.yml](docker-compose.yml).

View File

@@ -0,0 +1,107 @@
# 5G-IANA: UULM Network Monitoring
This repository contains the CI/CD and Dockerfiles necessary to build
the UULM Network Monitoring Tool.
For demonstration purposes we need to send a command to `videoprobe`
before it starts running, so we can deploy it beforehand. To do this
simply run the following command:
``` bash
curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"<obu-node endpoint>\",\"<pqos endpoint>\"], \"stream_ip\": \"<ping target>\", \"stream_url\": \"<stream url>"}" http://<videoprobe ip/port>/demo/start
```
- node<sub>ip</sub>: A list of API endpoints `videorprobe` should send
the collected data to i.e. \_\[<http://192.168.0.149:8001/upload>,
<http://192.168.0.149:8002/upload%5D_>.
- stream<sub>ip</sub>: The IP `videoprobe` measures the latency to.
Usually this is the same as IP as the `stream_url` i.e.
<u>192.168.0.149</u>.
- stream<u>url: The full path to the nginx-proxy thats hosting a rtmp
stream i.e. <sub>rtmp</sub>://192.168.0.149/live/test</u>.
## Testing Locally
When testing locally we may host the videostream provider and the
consumer on the same device. This is not the case for the deployment on
the 5G-IANA platform, where we put them on different clusters (see
[file:maestro-compose.yml](maestro-compose.yml)). All files regarding
local testing can be found in [file:local/](local/).
1. Make sure to have the GNSS Dongle connected as a device at
`/dev/ttyACM0`. If it has another name, change the entry in
[local-docker-compose.yml](local-docker-compose.yml) accordingly.
2. Run `docker compose -f local-docker-compose.yml up --build` to
build/run all of the `*Dockerfiles`.
3. For the current version, which is built for the demonstration, we
need to run the `curl` command to provide `videoprobe` with the
endpoint to which it'll send the data.
Usually that would be the `obu-node` container. For local testing we are
using [file:app.py](app.py). Adjust the port accordingly in the curl
command so it looks roughly like this:
``` bash
# Another example: curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"https://webhook.site/30ffd7cd-0fa5-4391-8725-c05a1bf48a75/upload/\"], \"stream_ip\": \"192.168.30.248\", \"stream_url\": \"rtmp://192.168.30.248:32731/live/test\"}" http://192.168.30.248:31234/demo/start
curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"http://192.168.0.149:8001/upload\",\"http://192.168.0.149:8002/upload\"], \"stream_ip\": \"192.168.0.149\", \"stream_url\": \"rtmp://192.168.0.149/live/test\"}" http://192.168.0.149:8000/demo/start
```
Given your devices IP is `192.168.100.2`
1. Once running you can do either of the following:
1. Simulate DMLOs `get_data_stats` by running the following
command:
`curl -X GET -H "Content-Type: application/json" -d "{\"id\": 1}" http://<IP of videoprobe>:8000/data_collection/get_data_stats`
## Running on 5G-IANA
When testing locally we are hosting the videostream provider and the
consumer on the same device. This is not the case for the deployment on
the 5G-IANA platform, where we put them on different clusters (see
[file:maestro-compose.yml](maestro-compose.yml)).
1. Make sure OBUs are connected by running the following command on the
MEC: `kubectl get nodes # UULM-OBU1 and UULM-OBU2 should be present`
2. Make sure the OBUs each have a GNSS receiver connected to them. If
there are no devices called `/dev/ttyACM0` on each OBU, change the
entries in the
[docker-compose.yml](docker-compose.yml)/[maestro-compose.yml](maestro-compose.yml)
accordingly to the actual name of the GNSS receivers and redeploy
the images. A possibly easier alternative would be to unplug the
GNSS receiver, reboot the machine and plug it back in, if possible.
3. Find out the IPs for the OBUs and run
`curl -X GET -H "Content-Type: application/json" -d "{\"ip\": http://192.168.100.2:32123/upload}" http://192.168.100.2:8000/demo/start`
on each of them. `192.168.100.2` being a placeholder for their
respective IPs, 32123 being a placeholder for the port the
`obu-node` container is listening on for data-uploads and port 8000
being a placeholder for videoprobe listening on for the start
command.
## Open internal Ports
- **1935**: RTMP of `web` providing `sender`-stream
- **8000**: Endpoint of `videoprobe`
## Configurations/Environment Variables
- STREAM<sub>URL</sub>: The URL of a rtmp based video stream. In this
environment it is to be `web`.
- RUST<sub>LOG</sub>: The logging level of the network monitoring tool
itself.
- ROCKET<sub>CONFIG</sub>: Might as well be constant, but specifies the
path for the configuration of the API endpoint of `videoprobe`.
- VP<sub>TARGET</sub>: The API endpoint to upload the collected data to
with with a `POST` request. This is variable should not be used during
the demonstration.
- CMD: Needed as an alternative to using the `command:` keyword, which
is usually used to overwrite a containers entrypoint.
- GNSS<sub>ENABLED</sub>: Used for choosing whether the videoprobe
should be running with "dry gps". Dry GPS means that the tool will be
running without GPS capabilities in case the user is sure that there
is no GNSS device present or satalite connectivity can't be ensured.
- GNSS<sub>DEV</sub>: The path of the mounted GNSS Device. Needed to
start gpsd inside of the container. Changes to it should also be
applied to the corresponding
[file:local-docker-compose.yml](local-docker-compose.yml) and
[file:docker-compose.yml](docker-compose.yml).

View File

@@ -0,0 +1,57 @@
* 5G-IANA: UULM Network Monitoring
This repository contains the CI/CD and Dockerfiles necessary to build the UULM Network Monitoring Tool.
For demonstration purposes we need to send a command to =videoprobe= before it starts running, so we can deploy it beforehand.
To do this simply run the following command:
#+begin_src sh
curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"<obu-node endpoint>\",\"<pqos endpoint>\"], \"stream_ip\": \"<ping target>\", \"stream_url\": \"<stream url>"}" http://<videoprobe ip/port>/demo/start
#+end_src
- node_ip: A list of API endpoints =videorprobe= should send the collected data to i.e. _[http://192.168.0.149:8001/upload, http://192.168.0.149:8002/upload]_.
- stream_ip: The IP =videoprobe= measures the latency to. Usually this is the same as IP as the ~stream_url~ i.e. _192.168.0.149_.
- stream_url: The full path to the nginx-proxy thats hosting a rtmp stream i.e. _rtmp://192.168.0.149/live/test_.
** Testing Locally
When testing locally we may host the videostream provider and the consumer on the same device.
This is not the case for the deployment on the 5G-IANA platform, where we put them on different clusters (see [[file:maestro-compose.yml]]).
All files regarding local testing can be found in [[file:local/]].
1. Make sure to have the GNSS Dongle connected as a device at ~/dev/ttyACM0~.
If it has another name, change the entry in [[file:local-docker-compose.yml][local-docker-compose.yml]] accordingly.
2. Run ~docker compose -f local-docker-compose.yml up --build~ to build/run all of the =*Dockerfiles=.
3. For the current version, which is built for the demonstration, we need to run the ~curl~ command to provide =videoprobe= with the endpoint to which it'll send the data.
Usually that would be the =obu-node= container.
For local testing we are using [[file:app.py]].
Adjust the port accordingly in the curl command so it looks roughly like this:
#+BEGIN_SRC sh
# Another example: curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"https://webhook.site/30ffd7cd-0fa5-4391-8725-c05a1bf48a75/upload/\"], \"stream_ip\": \"192.168.30.248\", \"stream_url\": \"rtmp://192.168.30.248:32731/live/test\"}" http://192.168.30.248:31234/demo/start
curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"http://192.168.0.149:8001/upload\",\"http://192.168.0.149:8002/upload\"], \"stream_ip\": \"192.168.0.149\", \"stream_url\": \"rtmp://192.168.0.149/live/test\"}" http://192.168.0.149:8000/demo/start
#+END_SRC
Given your devices IP is =192.168.100.2=
4. Once running you can do either of the following:
1. Simulate DMLOs ~get_data_stats~ by running the following command:
~curl -X GET -H "Content-Type: application/json" -d "{\"id\": 1}" http://<IP of videoprobe>:8000/data_collection/get_data_stats~
** Running on 5G-IANA
When testing locally we are hosting the videostream provider and the consumer on the same device.
This is not the case for the deployment on the 5G-IANA platform, where we put them on different clusters (see [[file:maestro-compose.yml]]).
1. Make sure OBUs are connected by running the following command on the MEC:
~kubectl get nodes # UULM-OBU1 and UULM-OBU2 should be present~
2. Make sure the OBUs each have a GNSS receiver connected to them.
If there are no devices called ~/dev/ttyACM0~ on each OBU, change the entries in the [[file:docker-compose.yml][docker-compose.yml]]/[[file:maestro-compose.yml][maestro-compose.yml]] accordingly to the actual name of the GNSS receivers and redeploy the images.
A possibly easier alternative would be to unplug the GNSS receiver, reboot the machine and plug it back in, if possible.
3. Find out the IPs for the OBUs and run ~curl -X GET -H "Content-Type: application/json" -d "{\"ip\": http://192.168.100.2:32123/upload}" http://192.168.100.2:8000/demo/start~ on each of them. ~192.168.100.2~ being a placeholder for their respective IPs, 32123 being a placeholder for the port the =obu-node= container is listening on for data-uploads and port 8000 being a placeholder for videoprobe listening on for the start command.
** Open internal Ports
- *1935*: RTMP of =web= providing =sender=-stream
- *8000*: Endpoint of =videoprobe=
** Configurations/Environment Variables
- STREAM_URL: The URL of a rtmp based video stream. In this environment it is to be =web=.
- RUST_LOG: The logging level of the network monitoring tool itself.
- ROCKET_CONFIG: Might as well be constant, but specifies the path for the configuration of the API endpoint of =videoprobe=.
- VP_TARGET: The API endpoint to upload the collected data to with with a ~POST~ request. This is variable should not be used during the demonstration.
- CMD: Needed as an alternative to using the ~command:~ keyword, which is usually used to overwrite a containers entrypoint.
- GNSS_ENABLED: Used for choosing whether the videoprobe should be running with "dry gps". Dry GPS means that the tool will be running without GPS capabilities in case the user is sure that there is no GNSS device present or satalite connectivity can't be ensured.
- GNSS_DEV: The path of the mounted GNSS Device. Needed to start gpsd inside of the container. Changes to it should also be applied to the corresponding [[file:local-docker-compose.yml]] and [[file:docker-compose.yml]].

View File

@@ -0,0 +1,3 @@
[default]
address = "0.0.0.0"
limits = { form = "64 kB", json = "1 MiB" }

View File

@@ -0,0 +1,7 @@
[registry."192.168.100.2:5000"]
http = true
insecure = true
ca = ["certs/192.168.100.2:5000/ca.crt"]
[[registry."192.168.100.2:5000".keypair]]
key = "certs/192.168.100.2:5000/client.key"
cert = "certs/192.168.100.2:5000/client.cert"

View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker buildx create --name iana --platform linux/amd64,linux/arm64 --bootstrap --config ./buildkitd.toml --use

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Nokia
#IANA_REGISTRY=192.168.100.2:5000
# TS
IANA_REGISTRY=192.168.100.2:5000
mkdir -p certs/"$IANA_REGISTRY"
(
cd certs/"$IANA_REGISTRY" || exit 1
openssl s_client -showcerts -connect "$IANA_REGISTRY" </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >ca.crt
openssl genrsa -out client.key 4096
openssl req -new -x509 -text -key client.key -out client.cert \
-subj "/C=DE/ST=Northrhine Westphalia/L=Essen/O=University Duisburg-Essen/emailAddress=tuan-dat.tran@stud.uni-due.de"
)

View File

@@ -0,0 +1,39 @@
version: "3.9"
name: uulm_network_monitoring
services:
videoprobe:
image: 192.168.100.2:5000/uulm/passive_network_monitoring:latest
container_name: netmon_receiver_videoprobe
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
healthcheck:
test: curl http://localhost:8000
interval: 10s
environment:
- RUST_LOG=info
- ROCKET_CONFIG=/etc/videoprobe/Rocket.toml
- GNSS_DEV=/dev/ttyACM0
- GNSS_ENABLED=true # default
depends_on:
- web
ports:
- 8000:8000
devices:
- /dev/ttyACM0:/dev/ttyACM0
web:
image: 192.168.100.2:5000/uulm/nginx:latest
container_name: netmon_sender_web
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
healthcheck:
test: curl http://localhost:1935
interval: 10s
ports:
- 1935:1935

View File

@@ -0,0 +1,11 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
PNM_VERSION=v1.3.0
LOCAL_CL_IMAGE=videoprobe-rtt
REMOTE_CL_IMAGE=uc6nmclirtt
docker build -f ./docker/nmcli_rtt.Dockerfile -t $LOCAL_CL_IMAGE .
docker tag $LOCAL_CL_IMAGE:latest 192.168.100.2:5000/uulm/$REMOTE_CL_IMAGE:$PNM_VERSION
docker push 192.168.100.2:5000/uulm/$REMOTE_CL_IMAGE:$PNM_VERSION

View File

@@ -0,0 +1,11 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
PNM_VERSION=v1.0.0
LOCAL_CL_IMAGE=videoprobe-throughput
REMOTE_CL_IMAGE=uc6nmclithroughput
docker build -f ./docker/nmcli_throughput.Dockerfile -t $LOCAL_CL_IMAGE .
docker tag $LOCAL_CL_IMAGE:latest 192.168.100.2:5000/uulm/$REMOTE_CL_IMAGE:$PNM_VERSION
docker push 192.168.100.2:5000/uulm/$REMOTE_CL_IMAGE:$PNM_VERSION

View File

@@ -0,0 +1,24 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
REGISTRY=192.168.100.2:5000/uulm
TAG=v1.3.0
DOCKERFILE=./docker/nmcli_default.Dockerfile
REMOTE_IMAGE_X86=passive_network_monitoring
REMOTE_IMAGE_ARM=passive_network_monitoring_arm
docker buildx build --platform linux/amd64 -f $DOCKERFILE -t \
$REGISTRY/$REMOTE_IMAGE_X86:$TAG . --push
docker buildx build --platform linux/arm64 -f $DOCKERFILE -t \
$REGISTRY/$REMOTE_IMAGE_ARM:$TAG . --push
NGINX_VERSION=v1.2.2
LOCAL_NGINX_IMAGE=nginx-stream
REMOTE_NGINX_IMAGE=nginx
docker build -f ./docker/nginx.Dockerfile -t $LOCAL_NGINX_IMAGE .
docker tag $LOCAL_NGINX_IMAGE $REGISTRY/$REMOTE_NGINX_IMAGE:$NGINX_VERSION
docker push $REGISTRY/$REMOTE_NGINX_IMAGE:$NGINX_VERSION

View File

@@ -0,0 +1,7 @@
FROM tiangolo/nginx-rtmp:latest-2024-01-15
# Install dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends ffmpeg && \
rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["sh", "-c", "nginx && ffmpeg -f lavfi -i testsrc -vf scale=1920x1080 -r 30 -c:v libx264 -pix_fmt yuv420p -b:v 20M -f flv rtmp://localhost/live/test"]

View File

@@ -0,0 +1,22 @@
# Build Stage
FROM rust:1.74 AS builder
WORKDIR /usr/src/5G_VideoProbe
COPY ../src src
COPY ../Cargo.* .
RUN cargo install -F rtt -F throughput --path .
# Runtime Stage
FROM debian:stable-slim AS runtime
RUN apt-get update && apt-get install -y \
tshark \
gpsd \
iputils-ping \
ffmpeg \
tcpdump \
&& rm -rf /var/lib/apt/lists/*
COPY ../Rocket.toml /etc/videoprobe/Rocket.toml
COPY ../run.sh /run.sh
COPY --from=builder /usr/local/cargo/bin/videoprobe /usr/local/bin/videoprobe
CMD [ "/run.sh" ]

View File

@@ -0,0 +1,22 @@
# Build Stage
FROM rust:1.74 AS builder
WORKDIR /usr/src/5G_VideoProbe
COPY ../src src
COPY ../Cargo.* .
RUN cargo install -F rtt --path .
# Runtime Stage
FROM debian:stable-slim AS runtime
RUN apt-get update && apt-get install -y \
tshark \
gpsd \
iputils-ping \
ffmpeg \
tcpdump \
&& rm -rf /var/lib/apt/lists/*
COPY ../Rocket.toml /etc/videoprobe/Rocket.toml
COPY ../run.sh /run.sh
COPY --from=builder /usr/local/cargo/bin/videoprobe /usr/local/bin/videoprobe
CMD [ "/run.sh" ]

View File

@@ -0,0 +1,22 @@
# Build Stage
FROM rust:1.74 as builder
WORKDIR /usr/src/5G_VideoProbe
COPY ../src src
COPY ../Cargo.* .
RUN cargo install -F throughput --path .
# Runtime Stage
FROM debian:stable-slim AS runtime
RUN apt-get update && apt-get install -y \
tshark \
gpsd \
iputils-ping \
ffmpeg \
tcpdump \
&& rm -rf /var/lib/apt/lists/*
COPY ../Rocket.toml /etc/videoprobe/Rocket.toml
COPY ../run.sh /run.sh
COPY --from=builder /usr/local/cargo/bin/videoprobe /usr/local/bin/videoprobe
CMD [ "/run.sh" ]

View File

@@ -0,0 +1,95 @@
* Measurement Setup
During our testing in Ulm we had two machines.
The *5G-MEC*, which we used as the sender of a video stream.
The receiver of the stream was a laptop with a GNSS module and the 5G antenna.
The 5G-MEC was a VM running Ubuntu.
To access the sender from the receiver, the receiver had to be on a specific mobile network.
From there the receiver had access to a OpenVPN-Server, which granted us a connection to the 5G-MEC.
To limit traffic between the sender and receiver to only the 5G connection, Wi-Fi was turned off.
** Sender
The sender had a video file that was used by a [[https://hub.docker.com/r/jrottenberg/ffmpeg][ffmpeg]] container which then provided an rmtp stream.
The resulting rtmp stream was forwarded to [[https://hub.docker.com/r/tiangolo/nginx-rtmp/][nginx-rtmp]] so it is accessable from the outside.
*** Video File
The video file the sender used for streaming had the following format:
- Bitrate: 23780 kb/s
- Encoding: h264
- Color model: yuv420p
- Resolution: 1920x1080
- Frame Rate: 23.98 fps
*** Video Stream
To stream the video the following flags were used:
#+begin_src shell
-i /video/video.mkv -c:v libx264 -b:v 40M -movflags frag_keyframe+empty_moov -f flv ${STREAM_URL}
#+end_src
- ~-i /video/video.mkv~: This specifies the path to the video file
- ~-c:v libx264~: Specifies the video coded that should be used. H264, the original codec, in this case.
- ~-b:v 40M~: Specifies the target video bitrate, which is 40 Mb/s.
- ~-movflags frag_keyframe+empty_moov~: These were used to make the file compatible with the FLV format
- ~-f flv~: Specifies the target file format. FLV is necessary for the RTMP video stream.
- ~${STREAM_URL}~: The URL where the stream will be served.
** Receiver
The receiver had a GNSS module to record GPS data and a 5G mobile connection.
It was running the videoprobe tool in docker along with [[https://hub.docker.com/r/jrottenberg/ffmpeg][ffmpeg]] and [[https://hub.docker.com/r/nicolaka/netshoot][netshoot]].
ffmpeg was configured to use netshoot as its network gateway.
netshoot ran tshark on its passing traffic and created a pcap file.
That pcap file was written to a docker volume, which was also attached to our videoprobe tool.
The videoprobe tool used the pcap file to gauge the throughput of the video stream.
Along with videoprobe to generate logs we also ran ~signal.sh~ to gain mobile network signal information from the antenna, such as "Reference Signal Received Quality" (RSRQ) and "Reference Signal Received Power" (RSRP).
The logfiles of both have to be manually joined on their timestamps at a later time.
*** Receiver
The receiver ran with the following flags:
#+begin_src shell
-i ${STREAM_URL} -c copy -f null -
#+end_src
- ~-i ${STREAM_URL}~: The source of the video stream that should be read in.
- ~-c copy~: Makes the receiver take in the input stream as is.
- ~-f null -~: Discard the streamed video.
*** Netshoot
Netshoot is a network diagnostic tool for docker.
We use it to get the traffic to/from the ffmpeg container.
*** VideoProbe
VideoProbe uses the following metrics:
- Location Data (from GNSS module)
- Throughput (from netshoot-pcap file)
- Latency (ping)
to create a logfile.
Each entry of the logfile has the fields: (timestamp, latitude,longitude, throughput, latency (in ns)).
The resolution of the output was 1 entry/s.
*** signal.sh
We used ~qmicli~ to log signal information of the antenna.
An entry of the logfile has the fields: (timestamp, rsrq, rsrp)
The resolution of the output was 1 entry/s.
** Diagram
#+begin_src
┌───────────────────────────────┐
│ │
│ Client (Laptop) │
│ │
│ ┌──────┐ ┌────────────┐ │
┌───────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │
│ │ │ │ pcap │◄─────┤ videoprobe │ │
│ Sender (5G-MEC) ┌────────────────────────────────────────┐ │ │ │ │ │ │ │
│ │ │ │ │ └──────┘ └────────────┘ │
│ ┌──────────────┐ │ Docker │ │ │ ▲ │
│ │ │ │ ┌────────┐ │ │ │ │ │
│ │ Videofile │ │ │ │ ┌──────────────┐ │ │ │ ┌──┴───────┐ ┌────────┐ │
│ │ - h264 │ │ │ ffmpeg │ │ │ │ │ │ │ │ │ │ │
│ │ - 1920x1080 ├─────┼────►│ - h264 ├────►│ nginx-server ├────┼──►├─────────────────────────────────┼─►│ netshoot ├─────►│ ffmpeg │ │
│ │ - 23.98 fps │ │ │ - 40M │ │ │ │ │ │ │ │ │ - copy │ │
│ │ - 23780 kb/s │ │ │ - flv │ └──────────────┘ │ │ │ └──────────┘ │ │ │
│ │ - mkv │ │ │ │ │ │ │ └────────┘ │
│ └──────────────┘ │ └────────┘ │ │ │ │
│ │ │ │ └───────────────────────────────┘
│ └────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘
#+end_src

View File

@@ -0,0 +1,18 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
REGISTRY="mos4"
CL_TAG=v1.3.0
REMOTE_CL_IMAGE=passive_network_monitoring
docker buildx build --platform linux/amd64,linux/arm64 -f ./docker/nmcli_default.Dockerfile -t $REGISTRY/$REMOTE_CL_IMAGE:$CL_TAG . --push
NGINX_VERSION=v1.2.2
LOCAL_NGINX_IMAGE=nginx-stream
REMOTE_NGINX_IMAGE=nginx
# docker buildx build --platform linux/amd64 -f ./docker/nginx.Dockerfile -t $REGISTRY/$REMOTE_NGINX_IMAGE:$NGINX_VERSION --push .
docker build -f ./docker/nginx.Dockerfile -t $LOCAL_NGINX_IMAGE .
docker tag $LOCAL_NGINX_IMAGE $REGISTRY/$REMOTE_NGINX_IMAGE:$NGINX_VERSION
docker push $REGISTRY/$REMOTE_NGINX_IMAGE:$NGINX_VERSION

View File

@@ -0,0 +1,16 @@
from flask import Flask, request
import sys
app = Flask(__name__)
@app.route('/upload', methods=['POST'])
def handle_post_request():
# Print the received POST request data
print(request)
print(request.data)
# Respond with a 200 status code
return '', 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(sys.argv[1]))

View File

@@ -0,0 +1,24 @@
version: "3.9"
name: uulm_network_monitoring
services:
videoprobe:
build:
context: ../
dockerfile: Dockerfile
container_name: netmon_receiver_videoprobe
deploy:
resources:
limits:
cpus : "1.0"
memory: 512M
healthcheck:
test: curl http://localhost:8000
interval: 10s
environment:
- RUST_LOG=info
- ROCKET_CONFIG=/etc/videoprobe/Rocket.toml
- GNSS_DEV=/dev/ttyACM0
ports:
- 8000:8000
devices:
- /dev/ttyACM0:/dev/ttyACM0

View File

@@ -0,0 +1,38 @@
version: "3.9"
name: uulm_network_monitoring
services:
web:
build:
context: .
dockerfile: nginx.Dockerfile
container_name: netmon_sender_web
deploy:
resources:
limits:
cpus : "1.0"
memory: 512M
healthcheck:
test: curl http://localhost:1935
interval: 10s
ports:
- 1935:1935
sender:
build:
context: .
dockerfile: ffmpeg.Dockerfile
container_name: netmon_sender_ffmpeg
deploy:
resources:
limits:
cpus : "1.0"
memory: 2048M
healthcheck:
test: curl http://web:1935
interval: 10s
environment:
- CMD=/usr/local/bin/ffmpeg -f lavfi -i testsrc -vf scale=3840x2160 -r 60 -c:v libx264 -pix_fmt yuv420p -b:v 40M -f flv rtmp://web/live/test
depends_on:
- web
devices:
- /dev/dri:/dev/dri

View File

@@ -0,0 +1,57 @@
application_name: uulm_network_monitoring
infrastructure_manager: kubernetes # kubernetes (default) or openstack
default_image_registry:
username: my_username_for_image_repository
password: "R3p0$1t0rY_P@$$W0rD!"
components:
videoprobe:
artifacts:
image: 192.168.100.2:5000/uulm/passive_network_monitoring:latest
registry: default
aarch: amd64
replicas: 1
compute:
cpus: 1.0
ram: 512.0
storage: 512.0
location:
cluster: cluster-1
node: node-1
depends_on:
- component_name: web
component_port: 1935
healthcheck:
http: http://localhost:8000
interval: 10s
environment:
- RUST_LOG=info
- ROCKET_CONFIG=/etc/videoprobe/Rocket.toml
- GNSS_DEV=/dev/ttyACM0
- GNSS_ENABLED=true
container_interfaces:
- tcp: 8000
user_facing: true
devices:
- /dev/ttyACM0:/dev/ttyACM0
web:
artifacts:
image: 192.168.100.2:5000/uulm/nginx:latest
registry: default
aarch: amd64
replicas: 1
compute:
cpus: 1.0
ram: 512.0
storage: 1024.0
location:
cluster: cluster-2
node: node-1
healthcheck:
cmd: curl rtmp://localhost:1935/live/test
interval: 10s
container_interfaces:
- tcp: 1935
user_facing: true

View File

@@ -0,0 +1,28 @@
#!/bin/sh
#
# This script is needed for the Dockerfile since running gpsd with a RUN command doesn't seem to work
#
RED='\033[0;31m'
NC='\033[0m' # No Color
echo "Log Level: $RUST_LOG"
echo "Rocket Config: $ROCKET_CONFIG"
echo "GNSS ENABLED: $GNSS_ENABLED"
if [ "$GNSS_ENABLED" = true ]; then
echo "GNSS Device Path: $GNSS_DEV"
gpsd -n -G -S 2947 -F /var/run/gpsd.sock $GNSS_DEV
else
echo "${RED}GNSS is DISABLED${NC}"
fi
mkdir /pcap/
touch /pcap/receiver.pcap
tcpdump -i eth0 -w /pcap/receiver.pcap &
sleep 5
mkdir /output/
if [ "$GNSS_ENABLED" = true ]; then
videoprobe -p "/pcap/receiver.pcap" -o "/output/videoprobe_$(date '+%s').log"
else
videoprobe -p "/pcap/receiver.pcap" -o "/output/videoprobe_$(date '+%s').log" -d
fi

View File

@@ -0,0 +1 @@
edition = "2018"

View File

@@ -0,0 +1,139 @@
use std::{io::Error, path::Path, process::Stdio, time::Duration};
use byte_unit::{AdjustedByte, Byte};
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::Command,
sync::mpsc::UnboundedSender,
};
use tracing::{debug, trace};
struct Bandwidth {
time: Duration,
data: Byte,
}
#[derive(Default, Debug)]
struct Bps {
/// Last recorded time
last_time: Duration,
/// The current total time
total_time: Duration,
/// The current total length
total_len: Byte,
}
impl Bps {
fn new() -> Self {
Self::default()
}
fn update(&mut self, data: &str) -> Option<BpsData> {
let x: Vec<&str> = data.trim().split('\t').collect();
let epoch = Duration::from_secs_f64(x[0].parse::<f64>().unwrap_or_default());
let _src = x[1];
let _dst = x[2];
let len = Byte::from_bytes((x[3].parse::<usize>().unwrap_or_default() as u64).into());
let mut res = None;
if self.total_time > Duration::from_secs(1) {
// One second elapsed
let window_size = self.total_len; // Total amount of bytes
let window_time = epoch; // Duration of the window
let bandwidth = Bandwidth {
time: window_time,
data: window_size,
};
self.reset(epoch);
res = Some(BpsData {
timestamp: bandwidth.time.as_secs(),
data: bandwidth.data.get_appropriate_unit(false),
});
} else {
// We're still in the window
// One second hasn't elapsed yet
// Difference between current time and last time a pkt got recorded
let delta = if epoch > self.last_time {
epoch - self.last_time
} else {
self.last_time - epoch
};
self.last_time = epoch;
self.total_time += delta;
self.total_len = Byte::from_bytes(self.total_len.get_bytes() + len.get_bytes());
}
trace!("Bps: {:?}", self);
res
}
fn reset(&mut self, last_time: Duration) {
self.last_time = last_time;
self.total_time = Duration::default();
self.total_len = Byte::default();
}
}
#[derive(Debug)]
pub struct BpsData {
pub timestamp: u64,
pub data: AdjustedByte,
}
pub async fn run_bandwidth_eval(
pcap_file: &Path,
sender: UnboundedSender<Option<BpsData>>,
) -> Result<(), Error> {
debug!("Running tail...");
let mut tail_child = Command::new("tail")
.args(["-f", "-c", "+0", pcap_file.as_os_str().to_str().unwrap()])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let tail_stdout: Stdio = tail_child.stdout.take().unwrap().try_into().unwrap();
debug!("Running tshark...");
let mut tshark_child = Command::new("tshark")
.args([
"-T",
"fields",
"-e",
"frame.time_epoch",
"-e",
"ip.src_host",
"-e",
"ip.dst_host",
"-e",
"frame.len",
"-i",
"-",
])
.stdin(tail_stdout)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let mut bps = Bps::new();
let tshark_stdout = tshark_child.stdout.take().unwrap();
let tshark_handler = tokio::spawn(async move {
let mut reader = BufReader::new(tshark_stdout).lines();
let mut counter = 0;
while let Some(line) = reader.next_line().await.unwrap() {
trace!("Pkt {}: {}", counter, &line);
counter += 1;
let data = bps.update(&line);
sender.send(data).expect("Couldn't send BpsData");
}
});
tshark_handler.await.unwrap();
Ok(())
}

View File

@@ -0,0 +1,30 @@
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(author, version, about=None, long_about=None)]
pub struct Cli {
/// Path of the pcap file
#[arg(short, long)]
pub pcap: PathBuf,
/// Output file as csv
#[arg(short, long)]
pub out: PathBuf,
/// Endpoint to send data to
#[arg(short, long)]
pub endpoint: Option<String>,
/// Target for ping
#[arg(short, long)]
pub target: Option<String>,
/// Option purely for testing.
#[arg(short, long)]
pub dry_gps: bool,
/// STREAM_URL for ffmpeg
#[arg(short, long)]
pub stream_url: Option<String>,
}

View File

@@ -0,0 +1,107 @@
use local_ip_address::local_ip;
use rocket::{get, serde::json::Json};
use serde::{Deserialize, Serialize};
use std::{
ops::Deref,
time::{SystemTime, UNIX_EPOCH},
};
use crate::SharedCounter;
#[cfg(all(feature = "throughput", feature = "rtt"))]
#[derive(Debug, Deserialize)]
pub struct StartDemoRequest {
endpoint_ip: Vec<String>,
ping_ip: String,
stream_url: String,
}
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
#[derive(Debug, Deserialize)]
pub struct StartDemoRequest {
endpoint_ip: Vec<String>,
stream_url: String,
}
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
#[derive(Debug, Deserialize)]
pub struct StartDemoRequest {
endpoint_ip: Vec<String>,
ping_ip: String,
}
#[derive(Debug, Deserialize)]
pub struct DataCollectionStatsRequest {
id: i32,
}
#[derive(Debug, Serialize)]
pub struct DataNode {
id: i32,
ip: String,
dataset_size: f32,
time_since_last_record: u64,
}
#[get("/demo/start", format = "json", data = "<data>")]
pub fn start_demo(
state: &rocket::State<SharedCounter>,
data: Json<StartDemoRequest>,
) -> &'static str {
{
let (local_state, cvar) = state.inner().deref();
let mut local_state = local_state.lock().unwrap();
local_state.started = true;
local_state.endpoint_ip = Some(data.endpoint_ip.clone());
#[cfg(feature = "rtt")]
{
local_state.ping_ip = Some(data.ping_ip.clone());
}
#[cfg(feature = "throughput")]
{
local_state.stream_url = Some(data.stream_url.clone());
}
cvar.notify_one();
}
"Ok"
}
#[get("/data_collection/get_data_stats", format = "json", data = "<data>")]
pub fn get_counter(
state: &rocket::State<SharedCounter>,
data: Json<DataCollectionStatsRequest>,
) -> Json<DataNode> {
// Get counter value
let (counter_val, last_visited): (f32, u64) = {
let (local_state, _) = state.inner().deref();
let local_state = local_state.lock().unwrap();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
(local_state.counter as f32, now - local_state.last_check)
};
// Reset counter now that it has been seen
{
let (local_state, _) = state.inner().deref();
let mut local_state = local_state.lock().unwrap();
local_state.counter = 0;
local_state.last_check = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
// Response
Json(DataNode {
id: data.id,
ip: local_ip().unwrap().to_string(),
dataset_size: counter_val,
time_since_last_record: last_visited,
})
}

View File

@@ -0,0 +1,17 @@
use std::error::Error;
use tokio::process::Command;
pub async fn run_ffmpeg(stream_url: String) -> Result<(), Box<dyn Error>> {
let _ffmpeg_child = Command::new("ffmpeg")
.arg("-i")
.arg(stream_url)
.arg("-c")
.arg("copy")
.arg("-f")
.arg("null")
.arg("-")
// .stdout(Stdio::piped())
.spawn()?;
Ok(())
}

View File

@@ -0,0 +1,96 @@
use std::net::SocketAddr;
use std::time::UNIX_EPOCH;
use std::{error::Error, time::SystemTime};
use chrono::DateTime;
use gpsd_proto::{Tpv, UnifiedResponse};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
net::TcpStream,
sync::mpsc::UnboundedSender,
};
use tracing::{debug, info, trace};
#[derive(Debug)]
pub struct GpsData {
pub timestamp: u64,
pub lat: Option<f64>,
pub lon: Option<f64>,
}
pub async fn run_gpsd_eval(
sender: UnboundedSender<Option<GpsData>>,
testing: bool,
) -> Result<(), Box<dyn Error>> {
if !testing {
let addr: SocketAddr = "127.0.0.1:2947".parse().unwrap();
let mut stream = TcpStream::connect(addr).await?;
debug!("Connected to server: {}", stream.peer_addr()?);
stream
.write_all(gpsd_proto::ENABLE_WATCH_CMD.as_bytes())
.await?;
let mut stream_reader = BufReader::new(stream).lines();
while let Some(line) = stream_reader.next_line().await.unwrap() {
if let Ok(rd) = serde_json::from_str(&line) {
match rd {
UnifiedResponse::Version(v) => {
if v.proto_major < gpsd_proto::PROTO_MAJOR_MIN {
panic!("Gpsd major version mismatch");
}
info!("Gpsd version {} connected", v.rev);
}
UnifiedResponse::Device(d) => debug!("Device {:?}", d),
UnifiedResponse::Tpv(t) => {
let data = parse_tpv(t);
if let Some(data) = data {
sender
.send(Some(data))
.expect("Couldn't send GpsData to main thread")
}
}
_ => {}
}
};
}
Ok(())
} else {
loop {
sender
.send(Some(GpsData {
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards.")
.as_secs(),
lat: Some(0_f64),
lon: Some(0_f64),
}))
.expect("Couldn't send GpsData to main thread");
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
#[allow(unreachable_code)]
Ok(())
}
}
fn parse_tpv(t: Tpv) -> Option<GpsData> {
if let Some(time) = t.time {
let timestamp = DateTime::parse_from_rfc3339(&time)
.unwrap()
.timestamp()
.unsigned_abs();
let lat = t.lat;
let lon = t.lon;
trace!("TPV: t: {}, lat: {:?}, lon: {:?}", timestamp, lat, lon);
Some(GpsData {
timestamp,
lat,
lon,
})
} else {
None
}
}

View File

@@ -0,0 +1,793 @@
#[cfg(feature = "throughput")]
use byte_unit::AdjustedByte;
#[cfg(feature = "rtt")]
use chrono::Duration;
use clap::Parser;
use rocket::routes;
use std::{
collections::BTreeMap,
io::Error,
sync::{Arc, Condvar, Mutex},
time::{SystemTime, UNIX_EPOCH},
};
use tokio::{fs::File, io::AsyncWriteExt, sync::mpsc};
use tracing::{debug, error, info, trace};
use crate::cli::Cli;
use crate::endpoints::{get_counter, start_demo};
#[cfg(feature = "throughput")]
mod bps;
mod cli;
mod endpoints;
#[cfg(feature = "throughput")]
mod ffmpeg;
mod gps;
#[cfg(feature = "rtt")]
mod rttps;
/// The maximum length of a entry/line in the csv file
const MAX_CSV_ENTRY_LENGTH: usize = 55; // 55 is the realistic upper bound 100 to be safe
/// The buffer that stores the data entries before they are sent out to the http endpoint
const ENTRIES_BUFFER_LENGTH: usize = 100;
type CsvEntries = [CsvEntry; ENTRIES_BUFFER_LENGTH];
type CsvEntry = [char; MAX_CSV_ENTRY_LENGTH];
pub type SharedCounter = Arc<(Mutex<State>, Condvar)>;
/// The state of the app, specifically used for the API endpoint
#[cfg(all(feature = "throughput", feature = "rtt"))]
#[derive(Debug)]
pub struct State {
// Whether program should be started
started: bool,
// To configure IP of the endpoint that should receive the collected data
endpoint_ip: Option<Vec<String>>,
// To configure IP of the ping-target after starting
ping_ip: Option<String>,
// To configure IP of the stream url for ffmpeg
stream_url: Option<String>,
// Amount of counted data packages
counter: usize,
// Time of last check on endpoint
last_check: u64,
// Push Data
entries: CsvEntries,
// Amount of counted data packages
entries_counter: usize,
}
/// The state of the app, specifically used for the API endpoint
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
#[derive(Debug)]
pub struct State {
// Whether program should be started
started: bool,
// To configure IP of the endpoint that should receive the collected data
endpoint_ip: Option<Vec<String>>,
// To configure IP of the ping-target after starting
ping_ip: Option<String>,
// Amount of counted data packages
counter: usize,
// Time of last check on endpoint
last_check: u64,
// Push Data
entries: CsvEntries,
// Amount of counted data packages
entries_counter: usize,
}
/// The state of the app, specifically used for the API endpoint
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
#[derive(Debug)]
pub struct State {
// Whether program should be started
started: bool,
// To configure IP of the endpoint that should receive the collected data
endpoint_ip: Option<Vec<String>>,
// To configure IP of the stream url for ffmpeg
stream_url: Option<String>,
// Amount of counted data packages
counter: usize,
// Time of last check on endpoint
last_check: u64,
// Push Data
entries: CsvEntries,
// Amount of counted data packages
entries_counter: usize,
}
#[cfg(all(feature = "throughput", feature = "rtt"))]
impl Default for State {
fn default() -> Self {
State {
started: false,
endpoint_ip: None,
ping_ip: None,
stream_url: None,
counter: 0,
last_check: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
entries: [[' '; MAX_CSV_ENTRY_LENGTH]; ENTRIES_BUFFER_LENGTH],
entries_counter: 0,
}
}
}
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
impl Default for State {
fn default() -> Self {
State {
started: false,
endpoint_ip: None,
ping_ip: None,
counter: 0,
last_check: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
entries: [[' '; MAX_CSV_ENTRY_LENGTH]; ENTRIES_BUFFER_LENGTH],
entries_counter: 0,
}
}
}
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
impl Default for State {
fn default() -> Self {
State {
started: false,
endpoint_ip: None,
stream_url: None,
counter: 0,
last_check: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
entries: [[' '; MAX_CSV_ENTRY_LENGTH]; ENTRIES_BUFFER_LENGTH],
entries_counter: 0,
}
}
}
#[derive(Debug)]
struct DataMsg {
timestamp: u64,
entry: DataEntry,
}
#[derive(Debug)]
#[cfg(all(feature = "throughput", feature = "rtt"))]
struct DataEntry {
lat: Option<f64>,
lon: Option<f64>,
gps_count: u64,
byte: Option<AdjustedByte>,
rtt: Option<Duration>,
}
#[derive(Debug)]
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
struct DataEntry {
lat: Option<f64>,
lon: Option<f64>,
gps_count: u64,
rtt: Option<Duration>,
}
#[derive(Debug)]
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
struct DataEntry {
lat: Option<f64>,
lon: Option<f64>,
gps_count: u64,
byte: Option<AdjustedByte>,
}
impl DataEntry {
fn combine(&self, other: &DataEntry) -> DataEntry {
// trace!("Compare: Self: {:?}, Other: {:?}", self, other);
let lat = match (self.lat, other.lat) {
(Some(lat1), Some(lat2)) => Some(lat1 + lat2),
(None, Some(lat2)) => Some(lat2),
(Some(lat1), None) => Some(lat1),
(None, None) => None,
};
let lon = match (self.lon, other.lon) {
(Some(lon1), Some(lon2)) => Some(lon1 + lon2),
(None, Some(lon2)) => Some(lon2),
(Some(lon1), None) => Some(lon1),
(None, None) => None,
};
let gps_count = self.gps_count + other.gps_count;
#[cfg(all(feature = "throughput", feature = "rtt"))]
{
let byte = self.byte.or(other.byte);
let rtt = match (self.rtt, other.rtt) {
(Some(rtt1), Some(rtt2)) => Some((rtt1 + rtt2) / 2),
(Some(rtt1), _) => Some(rtt1),
(None, Some(rtt2)) => Some(rtt2),
(None, None) => None,
};
DataEntry {
lat,
lon,
gps_count,
byte,
rtt,
}
}
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
{
let rtt = match (self.rtt, other.rtt) {
(Some(rtt1), Some(rtt2)) => Some((rtt1 + rtt2) / 2),
(Some(rtt1), _) => Some(rtt1),
(None, Some(rtt2)) => Some(rtt2),
(None, None) => None,
};
DataEntry {
lat,
lon,
gps_count,
rtt,
}
}
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
{
let byte = self.byte.or(other.byte);
DataEntry {
lat,
lon,
gps_count,
byte,
}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt::init();
let args = Cli::parse();
#[cfg(feature = "throughput")]
let pcap_file = &args.pcap;
let state: SharedCounter = Arc::new((Mutex::new(State::default()), Condvar::new()));
debug!("Starting API...");
let state_api = state.clone();
let api_handler = rocket::build()
.mount("/", routes![get_counter])
.mount("/", routes![start_demo])
.manage(state_api)
.launch();
let _api_join_handle = tokio::spawn(api_handler);
{
info!("Waiting for GET to /demo/start...");
let state_started = state.clone();
let (lock, cvar) = &*state_started;
let mut started = lock.lock().unwrap();
while !started.started {
started = cvar.wait(started).unwrap()
}
}
let endpoint_ip: Vec<String> = {
let state_endpoint = state.clone();
let (lock, _) = &*state_endpoint;
let local_state = lock.lock().unwrap();
let e = if let Some(endpoint) = args.endpoint {
local_state.endpoint_ip.clone().unwrap_or(vec![endpoint])
} else {
local_state.endpoint_ip.clone().unwrap()
};
info!("Endpoint to upload data to is: {:?}", e);
e
};
debug!("Creating mpscs...");
let (gps_sender, mut gps_receiver) = mpsc::unbounded_channel();
#[cfg(feature = "rtt")]
let (rttps_sender, mut rttps_receiver) = mpsc::unbounded_channel();
#[cfg(feature = "rtt")]
let ping_ip: String = {
let state_endpoint = state.clone();
let (lock, _) = &*state_endpoint;
let local_state = lock.lock().unwrap();
let p = local_state.ping_ip.clone().or(args.target).unwrap();
info!("Endpoint to measure latency at: {}", p);
p
};
#[cfg(feature = "throughput")]
let (bps_sender, mut bps_receiver) = mpsc::unbounded_channel();
#[cfg(feature = "throughput")]
let stream_url: String = {
let state_endpoint = state.clone();
let (lock, _) = &*state_endpoint;
let local_state = lock.lock().unwrap();
let s = local_state.stream_url.clone().or(args.stream_url).unwrap();
info!("Endpoint to stream video from: {}", s);
s
};
#[cfg(feature = "throughput")]
debug!("Running ffmpeg...");
#[cfg(feature = "throughput")]
let ffmpeg_handler = ffmpeg::run_ffmpeg(stream_url.clone());
#[cfg(feature = "throughput")]
debug!("Running bps...");
#[cfg(feature = "throughput")]
let bps_handler = bps::run_bandwidth_eval(pcap_file, bps_sender);
// wait here until api request comes in.
debug!("Running gps...");
let gps_handler = gps::run_gpsd_eval(gps_sender, args.dry_gps);
#[cfg(feature = "rtt")]
debug!("Running rttps...");
#[cfg(feature = "rtt")]
let rttps_handler = rttps::run_rtt_eval(rttps_sender, ping_ip);
let (tx, mut rx) = mpsc::unbounded_channel();
let gps_tx = tx.clone();
#[cfg(all(feature = "throughput", feature = "rtt"))]
let gps_channel_handler = tokio::spawn(async move {
while let Some(msg) = gps_receiver.recv().await {
if let Some(data) = msg {
debug!("GpsData: {:?}", data);
gps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: data.lat,
lon: data.lon,
gps_count: 1,
byte: None,
rtt: None,
},
})
.map_err(|err| error!("Failed to send data via GPS channel: {}", err))
.ok();
}
}
});
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
let gps_channel_handler = tokio::spawn(async move {
while let Some(msg) = gps_receiver.recv().await {
if let Some(data) = msg {
debug!("GpsData: {:?}", data);
gps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: data.lat,
lon: data.lon,
gps_count: 1,
rtt: None,
},
})
.map_err(|err| error!("Failed to send data via GPS channel: {}", err))
.ok();
}
}
});
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
let gps_channel_handler = tokio::spawn(async move {
while let Some(msg) = gps_receiver.recv().await {
if let Some(data) = msg {
debug!("GpsData: {:?}", data);
gps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: data.lat,
lon: data.lon,
gps_count: 1,
byte: None,
},
})
.map_err(|err| error!("Failed to send data via GPS channel: {}", err))
.ok();
}
}
});
#[cfg(feature = "throughput")]
let bps_tx = tx.clone();
#[cfg(all(feature = "throughput", feature = "rtt"))]
let bps_channel_handler = tokio::spawn(async move {
while let Some(msg) = bps_receiver.recv().await {
if let Some(data) = msg {
debug!("BPSData: {:?}", data);
bps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: None,
lon: None,
gps_count: 0,
byte: Some(data.data),
rtt: None,
},
})
.map_err(|err| error!("Failed to send data via BPS channel: {}", err))
.ok();
}
}
});
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
let bps_channel_handler = tokio::spawn(async move {
while let Some(msg) = bps_receiver.recv().await {
if let Some(data) = msg {
debug!("BPSData: {:?}", data);
bps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: None,
lon: None,
gps_count: 0,
byte: Some(data.data),
},
})
.map_err(|err| error!("Failed to send data via BPS channel: {}", err))
.ok();
}
}
});
#[cfg(feature = "rtt")]
let rttps_tx = tx.clone();
#[cfg(all(feature = "throughput", feature = "rtt"))]
let rttps_channel_handler = tokio::spawn(async move {
while let Some(msg) = rttps_receiver.recv().await {
if let Some(data) = msg {
debug!("RTTps: {:?}", data);
rttps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: None,
lon: None,
gps_count: 0,
byte: None,
rtt: Some(data.rtt),
},
})
.map_err(|err| error!("Failed to send data via RTTps channel: {}", err))
.ok();
}
}
});
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
let rttps_channel_handler = tokio::spawn(async move {
while let Some(msg) = rttps_receiver.recv().await {
if let Some(data) = msg {
debug!("RTTps: {:?}", data);
rttps_tx
.send(DataMsg {
timestamp: data.timestamp,
entry: DataEntry {
lat: None,
lon: None,
gps_count: 0,
rtt: Some(data.rtt),
},
})
.map_err(|err| error!("Failed to send data via RTTps channel: {}", err))
.ok();
}
}
});
let mut output_file = File::create(&args.out).await?;
let mut entries: BTreeMap<u64, DataEntry> = BTreeMap::new();
let client = reqwest::Client::new();
let state_csv = state.clone();
let csv_handler = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
let key = msg.timestamp;
if entries.contains_key(&key) {
let former_entry = entries.get(&key).unwrap();
let new_entry = &msg.entry;
let combined_entry = former_entry.combine(new_entry);
entries.insert(key, combined_entry);
} else {
entries.insert(key, msg.entry);
}
// Write entry to csv if complete
let entry = entries.get(&key).unwrap();
#[cfg(all(feature = "throughput", feature = "rtt"))]
if let (Some(lat), Some(lon), Some(byte), Some(rtt)) =
(entry.lat, entry.lon, entry.byte, entry.rtt)
{
let rtt_ns = rtt.num_nanoseconds().unwrap();
let rtt_string = format!("{rtt_ns} ns");
let csv_entry = format!(
"{},{:2.8},{:2.8},{},{}\n",
&key,
lat / (entry.gps_count as f64),
lon / (entry.gps_count as f64),
byte,
rtt_string
);
info!("Writing data: {}", &csv_entry.trim());
output_file.write_all(csv_entry.as_bytes()).await.unwrap();
let mut char_array: [char; MAX_CSV_ENTRY_LENGTH] = [' '; MAX_CSV_ENTRY_LENGTH];
// Convert the String into a Vec<char>
let char_vec: Vec<char> = csv_entry.chars().collect();
let len = char_vec.len().min(MAX_CSV_ENTRY_LENGTH);
{
let (local_state, _) = &*state_csv;
let mut local_state = local_state.lock().unwrap();
let counter = local_state.entries_counter;
if counter < ENTRIES_BUFFER_LENGTH {
char_array[..len].copy_from_slice(&char_vec[..len]);
local_state.entries[counter] = char_array;
local_state.counter += 1;
local_state.entries_counter += 1;
}
}
let request_body: Option<String> = {
let (local_state, _) = &*state_csv;
let mut local_state = local_state.lock().unwrap();
if local_state.entries_counter >= ENTRIES_BUFFER_LENGTH {
let body = local_state
.entries
.iter()
.map(|r| r.iter().collect::<String>().trim().to_string())
.filter(|l| !l.is_empty())
.collect::<Vec<String>>()
.join("\n");
{
local_state.entries_counter = 0;
local_state.entries =
[[' '; MAX_CSV_ENTRY_LENGTH]; ENTRIES_BUFFER_LENGTH];
}
info!("Sending {} to {:?}", body.clone(), endpoint_ip);
Some(body)
} else {
info!("counter: {}", local_state.entries_counter);
None
}
};
if let Some(rb) = request_body {
info!("Trying to send data...");
for e in endpoint_ip.clone().iter() {
if let Ok(response) = client.post(e.clone()).body(rb.clone()).send().await {
info!(
"Sucessfully sent data to {}. Response: {:?}",
e.clone(),
response
);
} else {
error!("Couldn't send data to {}", e.clone());
}
}
}
} else {
trace!(
"Building data: {{{}: {:?}}} (unfinished)",
&key,
entries.get(&key)
);
}
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
if let (Some(lat), Some(lon), Some(rtt)) = (entry.lat, entry.lon, entry.rtt) {
let rtt_ns = rtt.num_nanoseconds().unwrap();
let rtt_string = format!("{rtt_ns} ns");
let csv_entry = format!(
"{},{:2.8},{:2.8},{}\n",
&key,
lat / (entry.gps_count as f64),
lon / (entry.gps_count as f64),
rtt_string
);
info!("Writing data: {}", &csv_entry.trim());
output_file.write_all(csv_entry.as_bytes()).await.unwrap();
let mut char_array: [char; MAX_CSV_ENTRY_LENGTH] = [' '; MAX_CSV_ENTRY_LENGTH];
// Convert the String into a Vec<char>
let char_vec: Vec<char> = csv_entry.chars().collect();
let len = char_vec.len().min(MAX_CSV_ENTRY_LENGTH);
{
let (local_state, _) = &*state_csv;
let mut local_state = local_state.lock().unwrap();
let counter = local_state.entries_counter;
if counter < ENTRIES_BUFFER_LENGTH {
char_array[..len].copy_from_slice(&char_vec[..len]);
local_state.entries[counter] = char_array;
local_state.counter += 1;
local_state.entries_counter += 1;
}
}
let request_body: Option<String> = {
let (local_state, _) = &*state_csv;
let mut local_state = local_state.lock().unwrap();
if local_state.entries_counter >= ENTRIES_BUFFER_LENGTH {
let body = local_state
.entries
.iter()
.map(|r| r.iter().collect::<String>().trim().to_string())
.filter(|l| !l.is_empty())
.collect::<Vec<String>>()
.join("\n");
let mut new_entries: [[char; MAX_CSV_ENTRY_LENGTH]; ENTRIES_BUFFER_LENGTH] =
local_state.entries;
new_entries.copy_within(1.., 0);
new_entries[ENTRIES_BUFFER_LENGTH - 1] = [' '; MAX_CSV_ENTRY_LENGTH];
{
local_state.entries_counter = ENTRIES_BUFFER_LENGTH - 1;
local_state.entries = new_entries;
}
info!("Sending {} to {:?}", body.clone(), endpoint_ip);
Some(body)
} else {
info!("counter: {}", local_state.entries_counter);
None
}
};
if let Some(rb) = request_body {
info!("Trying to send data...");
for e in endpoint_ip.clone().iter() {
if let Ok(response) = client.post(e.clone()).body(rb.clone()).send().await {
info!(
"Sucessfully sent data to {}. Response: {:?}",
e.clone(),
response
);
} else {
error!("Couldn't send data to {}", e.clone());
}
}
}
} else {
trace!(
"Building data: {{{}: {:?}}} (unfinished)",
&key,
entries.get(&key)
);
}
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
if let (Some(lat), Some(lon), Some(byte)) = (entry.lat, entry.lon, entry.byte) {
let csv_entry = format!(
"{:2.8},{:2.8},{}\n",
lat / (entry.gps_count as f64),
lon / (entry.gps_count as f64),
byte,
);
info!("Writing data: {}", &csv_entry.trim());
output_file.write_all(csv_entry.as_bytes()).await.unwrap();
let mut char_array: [char; MAX_CSV_ENTRY_LENGTH] = [' '; MAX_CSV_ENTRY_LENGTH];
// Convert the String into a Vec<char>
let char_vec: Vec<char> = csv_entry.chars().collect();
let len = char_vec.len().min(MAX_CSV_ENTRY_LENGTH);
{
let (local_state, _) = &*state_csv;
let mut local_state = local_state.lock().unwrap();
let counter = local_state.entries_counter;
if counter < ENTRIES_BUFFER_LENGTH {
char_array[..len].copy_from_slice(&char_vec[..len]);
local_state.entries[counter] = char_array;
local_state.counter += 1;
local_state.entries_counter += 1;
}
}
let request_body: Option<String> = {
let (local_state, _) = &*state_csv;
let mut local_state = local_state.lock().unwrap();
if local_state.entries_counter >= ENTRIES_BUFFER_LENGTH {
let body = local_state
.entries
.iter()
.map(|r| r.iter().collect::<String>().trim().to_string())
.filter(|l| !l.is_empty())
.collect::<Vec<String>>()
.join("\n");
{
local_state.entries_counter = 0;
local_state.entries =
[[' '; MAX_CSV_ENTRY_LENGTH]; ENTRIES_BUFFER_LENGTH];
}
info!("Sending {} to {:?}", body.clone(), endpoint_ip);
Some(body)
} else {
info!("counter: {}", local_state.entries_counter);
None
}
};
if let Some(rb) = request_body {
info!("Trying to send data...");
for e in endpoint_ip.clone().iter() {
if let Ok(response) = client.post(e.clone()).body(rb.clone()).send().await {
info!(
"Sucessfully sent data to {}. Response: {:?}",
e.clone(),
response
);
} else {
error!("Couldn't send data to {}", e.clone());
}
}
}
} else {
trace!(
"Building data: {{{}: {:?}}} (unfinished)",
&key,
entries.get(&key)
);
}
}
});
#[cfg(all(feature = "throughput", feature = "rtt"))]
let _handler = tokio::join!(
ffmpeg_handler,
bps_handler,
gps_handler,
rttps_handler,
gps_channel_handler,
bps_channel_handler,
rttps_channel_handler,
csv_handler,
);
#[cfg(all(feature = "throughput", not(feature = "rtt")))]
let _handler = tokio::join!(
ffmpeg_handler,
bps_handler,
gps_handler,
gps_channel_handler,
bps_channel_handler,
csv_handler,
);
#[cfg(all(not(feature = "throughput"), feature = "rtt"))]
let _handler = tokio::join!(
gps_handler,
rttps_handler,
gps_channel_handler,
rttps_channel_handler,
csv_handler,
);
Ok(())
}

View File

@@ -0,0 +1,80 @@
use std::{error::Error, process::Stdio};
use chrono::Duration;
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::Command,
sync::mpsc::UnboundedSender,
};
use tracing::{debug, trace};
#[derive(Debug)]
pub struct RTTps {
pub timestamp: u64,
pub rtt: Duration,
}
impl RTTps {
fn new() -> Self {
RTTps {
timestamp: 0,
rtt: Duration::min_value(),
}
}
fn parse(&mut self, line: &str) -> Option<RTTData> {
debug!(?line);
if line.contains("time=") {
let start = line.find('[')?;
let end = line.find(']')?;
let timestamp_str = &line[start + 1..end];
let start = line.find("time=")?;
let end = line.find(" ms")?;
let rtt_str = &line[start + 5..end];
let timestamp = timestamp_str.split('.').next()?.parse::<u64>().ok()?;
let rtt_mus = rtt_str.parse::<f64>().ok()? * 1000f64;
let rtt = Duration::microseconds(rtt_mus.round() as i64);
Some(RTTData { timestamp, rtt })
} else {
None
}
}
}
#[derive(Debug)]
pub struct RTTData {
pub timestamp: u64,
pub rtt: Duration,
}
pub async fn run_rtt_eval(
sender: UnboundedSender<Option<RTTData>>,
ping_target: String,
) -> Result<(), Box<dyn Error>> {
let mut ping_child = Command::new("ping")
.arg("-D")
.arg("-i")
.arg(".2")
.arg(ping_target.trim())
.stdout(Stdio::piped())
.spawn()?;
let mut rttps = RTTps::new();
let ping_stdout = ping_child.stdout.take().unwrap();
let ping_handler = tokio::spawn(async move {
let mut reader = BufReader::new(ping_stdout).lines();
while let Some(line) = reader.next_line().await.unwrap() {
let data = rttps.parse(&line);
trace! {"{:?}", data}
sender.send(data).expect("Couldn't send RTTData");
}
});
ping_handler.await.unwrap();
Ok(())
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
Just run `docker compose up -d`
If the aggregator container fails, retry the command or remove the healthcheck.

View File

@@ -0,0 +1,17 @@
FROM python:3.11 AS compile-image
WORKDIR /federated-example
COPY requirements.txt .
RUN python3 -m pip install --upgrade pip
RUN python3 -m venv /venv
RUN . /venv/bin/activate && \
python3 -m ensurepip --upgrade && \
python3 -m pip install -r /federated-example/requirements.txt
FROM python:3.11 AS run-image
COPY --from=compile-image /venv /venv
WORKDIR /federated-example/src
COPY . /federated-example/
# RUN apt-get update && apt-get install -y tshark && rm -rf /var/lib/apt/lists/*
CMD . /venv/bin/activate && python server.py $FLWR_PORT $DMLO_PORT

View File

View File

@@ -0,0 +1,3 @@
{
"eligible_clients_ids" : ["1", "2"]
}

View File

@@ -0,0 +1,10 @@
{
"ml_model": "../resources/best_model_no_tuner_40.h5",
"num_epochs": 20,
"min_working_nodes": 2,
"hyperparam_epochs": 10,
"hyperparam_batch_size": 2048,
"hyperparam_learning_rate": 0.001,
"avg_algorithm": "FedAvg",
"training_clients_per_round": 2
}

25
aggregator-node/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Aggregator node
This is the version matching the final requirements where the client are started from the policy execution
## Running the code using Docker
1. To create the Docker Image, run "Dockerfile" using this command: `docker build -f Dockerfile -t server-image .`
2. Create a container from the above image using this command: `docker run -p 8080:8080 -p 5000:5000 -e FLWR_PORT={flwr_port} -e DMLO_PORT={dmlo_port} --name server --rm server-image`
3. The script for the Agg.Node will run automatically, the other nodes will await the Agg.Node if they are started first.
* **Notes**:
- `flwr_port` is the port number that will be used to communicate with the clients on the flower level (8080 for tests).
- `dmlo_port` is teh port number that will be used to communicate with the dmlo (5000 for tests).
- The `-p` flag is used to map the docker ports to the devices ports and should be changed according to the ports used in the simulation (currently set to ports 8080 and 5000).
- The execution can be stopped by opening another terminal and using this command `docker kill server`.
- The "Example files" directory contains examples for json files to be sent to the server. (The list of client IDs sent to the server should be a list of strings and not integers, see the example json file)
* **Below are helper shell commands to simulate server functions triggered by the DMLO (if needed):**
- To send the param file to the server:
`curl -X POST -H "Content-Type: application/json" -d @{file_name}.json {server_ip}:5000/config_server`
- To send the list of eligible clients to the server:
`curl -X POST -H "Content-Type: application/json" -d @{file_name}.json {server_ip}:5000/select_clients`
- To terminate the training:
`curl -X POST -d "" {server_ip}:5000/terminate_app`

12
aggregator-node/docker-push.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
TA_VERSION=v1.2.0
LOCAL_IMAGE="aggregator"
REMOTE_IMAGE="uc6aggnode"
docker build -t $LOCAL_IMAGE .
docker tag $LOCAL_IMAGE:latest 192.168.100.2:5000/uulm/$REMOTE_IMAGE:$TA_VERSION
docker push 192.168.100.2:5000/uulm/$REMOTE_IMAGE:$TA_VERSION

View File

@@ -0,0 +1,151 @@
nxw@5g-iana-manager:~$ kc logs $(kc get pods --all-namespaces | grep agg | awk '{ print $2 } ') -n $(kc get pods --all-namespaces | grep agg | awk '{ print $1 } ') -f
2024-07-15 15:48:16.807096: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2024-07-15 15:48:16.828642: E tensorflow/compiler/xla/stream_executor/cuda/cuda_dnn.cc:9342] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-07-15 15:48:16.828672: E tensorflow/compiler/xla/stream_executor/cuda/cuda_fft.cc:609] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-07-15 15:48:16.828715: E tensorflow/compiler/xla/stream_executor/cuda/cuda_blas.cc:1518] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registe
2024-07-15 15:48:16.833761: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2024-07-15 15:48:16.833925: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2024-07-15 15:48:17.538321: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
/federated-example/src/server.py:169: DeprecationWarning: setDaemon() is deprecated, set the daemon attribute instead
flask_thread.setDaemon(True)
* Serving Flask app 'server'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://10.1.68.68:5000
Press CTRL+C to quit
10.1.3.0 - - [15/Jul/2024 15:49:00] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:01] "POST /check_connection HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:07] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:08] "POST /check_connection HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:12] "POST /config_server HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:12] "GET /select_clients HTTP/1.1" 200 -
INFO flwr 2024-07-15 15:49:12,445 | app.py:162 | Starting Flower server, config: ServerConfig(num_rounds=5, round_timeout=None)
INFO flwr 2024-07-15 15:49:12,449 | app.py:175 | Flower ECE: gRPC server running (5 rounds), SSL is disabled
INFO flwr 2024-07-15 15:49:12,449 | server.py:89 | Initializing global parameters
INFO flwr 2024-07-15 15:49:12,450 | server.py:272 | Using initial parameters provided by strategy
INFO flwr 2024-07-15 15:49:12,450 | server.py:91 | Evaluating initial parameters
Parameters loaded
Inializing Model
Model loaded
Model Compiled
(2003, 400, 3)
(2003, 1, 3)
63/63 [==============================] - 2s 23ms/step - loss: 0.0739 - quantile_metric: 0.1243 - mean_absolute_error: 0.5655
63/63 [==============================] - 2s 22ms/step
INFO flwr 2024-07-15 15:49:16,180 | server.py:94 | initial parameters (loss, other metrics): 0.07388024777173996, {'accuracy': 0.5655196309089661}
INFO flwr 2024-07-15 15:49:16,180 | server.py:104 | FL starting
10.1.3.0 - - [15/Jul/2024 15:49:35] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:36] "POST /check_connection HTTP/1.1" 200 -
DEBUG flwr 2024-07-15 15:49:41,146 | server.py:222 | fit_round 1: strategy sampled 2 clients (out of 2)
10.1.3.0 - - [15/Jul/2024 15:49:41] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:49:41] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:50:21] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:50:21] "POST /check_connection HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:50:42] "GET /select_clients HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:51:06] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:51:12] "POST /upload_kpi05 HTTP/1.1" 200 -
DEBUG flwr 2024-07-15 15:51:12,130 | server.py:236 | fit_round 1 received 2 results and 0 failures
WARNING flwr 2024-07-15 15:51:12,131 | fedavg.py:242 | No fit_metrics_aggregation_fn provided
2 clients connected.
WARNING: 2 clients are needed but only 3 client IDs are received. The training will wait for another list with enough eligible clients.
(2003, 400, 3)
(2003, 1, 3)
63/63 [==============================] - 1s 23ms/step - loss: 0.1734 - quantile_metric: 0.1908 - mean_absolute_error: 2.4910
63/63 [==============================] - 1s 22ms/step
INFO flwr 2024-07-15 15:51:15,075 | server.py:125 | fit progress: (1, 0.1733752340078354, {'accuracy': 2.490957498550415}, 118.89502924995031)
DEBUG flwr 2024-07-15 15:51:15,149 | server.py:173 | evaluate_round 1: strategy sampled 3 clients (out of 3)
DEBUG flwr 2024-07-15 15:51:26,920 | server.py:187 | evaluate_round 1 received 3 results and 0 failures
WARNING flwr 2024-07-15 15:51:26,920 | fedavg.py:273 | No evaluate_metrics_aggregation_fn provided
DEBUG flwr 2024-07-15 15:51:26,974 | server.py:222 | fit_round 2: strategy sampled 3 clients (out of 3)
10.1.3.0 - - [15/Jul/2024 15:51:27] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:51:27] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:51:27] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:51:27] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:51:27] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:05] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:42] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:47] "POST /upload_kpi05 HTTP/1.1" 200 -
DEBUG flwr 2024-07-15 15:52:47,347 | server.py:236 | fit_round 2 received 3 results and 0 failures
2 clients connected.
2 clients connected.
(2003, 400, 3)
(2003, 1, 3)
63/63 [==============================] - 1s 21ms/step - loss: 0.0874 - quantile_metric: 0.2492 - mean_absolute_error: 0.2591
63/63 [==============================] - 1s 21ms/step
INFO flwr 2024-07-15 15:52:50,161 | server.py:125 | fit progress: (2, 0.08735799789428711, {'accuracy': 0.2590666115283966}, 213.98151048796717)
DEBUG flwr 2024-07-15 15:52:50,221 | server.py:173 | evaluate_round 2: strategy sampled 3 clients (out of 3)
DEBUG flwr 2024-07-15 15:52:59,542 | server.py:187 | evaluate_round 2 received 3 results and 0 failures
DEBUG flwr 2024-07-15 15:52:59,589 | server.py:222 | fit_round 3: strategy sampled 3 clients (out of 3)
10.1.3.0 - - [15/Jul/2024 15:52:59] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:59] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:59] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:59] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:59] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:52:59] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:53:34] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:53:36] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:53:36] "POST /check_connection HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:54:12] "POST /upload_kpi05 HTTP/1.1" 200 -
DEBUG flwr 2024-07-15 15:54:13,045 | server.py:236 | fit_round 3 received 2 results and 1 failures
2 clients connected.
2 clients connected.
(2003, 400, 3)
(2003, 1, 3)
63/63 [==============================] - 1s 22ms/step - loss: 0.0654 - quantile_metric: 0.1364 - mean_absolute_error: 0.9301
63/63 [==============================] - 1s 22ms/step
INFO flwr 2024-07-15 15:54:15,922 | server.py:125 | fit progress: (3, 0.06537292897701263, {'accuracy': 0.9301236867904663}, 299.7421916149906)
DEBUG flwr 2024-07-15 15:54:15,981 | server.py:173 | evaluate_round 3: strategy sampled 3 clients (out of 3)
DEBUG flwr 2024-07-15 15:54:28,262 | server.py:187 | evaluate_round 3 received 3 results and 0 failures
DEBUG flwr 2024-07-15 15:54:28,314 | server.py:222 | fit_round 4: strategy sampled 3 clients (out of 3)
10.1.3.0 - - [15/Jul/2024 15:54:28] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:54:28] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:54:28] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:54:28] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:54:28] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:55:03] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:55:40] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:55:53] "POST /upload_kpi05 HTTP/1.1" 200 -
DEBUG flwr 2024-07-15 15:55:53,632 | server.py:236 | fit_round 4 received 3 results and 0 failures
2 clients connected.
2 clients connected.
(2003, 400, 3)
(2003, 1, 3)
63/63 [==============================] - 1s 22ms/step - loss: 0.1268 - quantile_metric: 0.3151 - mean_absolute_error: 0.3247
63/63 [==============================] - 1s 22ms/step
INFO flwr 2024-07-15 15:55:56,563 | server.py:125 | fit progress: (4, 0.12679509818553925, {'accuracy': 0.3247184455394745}, 400.3833388419589)
DEBUG flwr 2024-07-15 15:55:56,646 | server.py:173 | evaluate_round 4: strategy sampled 3 clients (out of 3)
DEBUG flwr 2024-07-15 15:56:06,016 | server.py:187 | evaluate_round 4 received 3 results and 0 failures
DEBUG flwr 2024-07-15 15:56:06,066 | server.py:222 | fit_round 5: strategy sampled 3 clients (out of 3)
10.1.3.0 - - [15/Jul/2024 15:56:06] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:56:06] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:56:06] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:56:06] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:56:06] "POST /upload_kpi02 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:56:06] "POST /upload_kpi01 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:56:41] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:57:17] "POST /upload_kpi05 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:57:25] "POST /upload_kpi05 HTTP/1.1" 200 -
DEBUG flwr 2024-07-15 15:57:25,615 | server.py:236 | fit_round 5 received 3 results and 0 failures
2 clients connected.
2 clients connected.
(2003, 400, 3)
(2003, 1, 3)
63/63 [==============================] - 1s 22ms/step - loss: 0.0718 - quantile_metric: 0.1710 - mean_absolute_error: 0.3574
63/63 [==============================] - 1s 22ms/step
INFO flwr 2024-07-15 15:57:28,518 | server.py:125 | fit progress: (5, 0.0717623308300972, {'accuracy': 0.35737916827201843}, 492.3376815340016)
DEBUG flwr 2024-07-15 15:57:28,599 | server.py:173 | evaluate_round 5: strategy sampled 3 clients (out of 3)
DEBUG flwr 2024-07-15 15:57:37,732 | server.py:187 | evaluate_round 5 received 3 results and 0 failures
INFO flwr 2024-07-15 15:57:37,732 | server.py:153 | FL finished in 501.5518533719587
INFO flwr 2024-07-15 15:57:37,732 | app.py:225 | app_fit: losses_distributed [(1, 0.22432586054007211), (2, 0.05442244683702787), (3, 0.06365528702735901), (4, 0.05708811432123184), (5, 0.04476702958345413)]
INFO flwr 2024-07-15 15:57:37,732 | app.py:226 | app_fit: metrics_distributed_fit {}
INFO flwr 2024-07-15 15:57:37,732 | app.py:227 | app_fit: metrics_distributed {}
INFO flwr 2024-07-15 15:57:37,732 | app.py:228 | app_fit: losses_centralized [(0, 0.07388024777173996), (1, 0.1733752340078354), (2, 0.08735799789428711), (3, 0.06537292897701263), (4, 0.12679509818553925), (5, 0.0717623308300972)]
INFO flwr 2024-07-15 15:57:37,732 | app.py:229 | app_fit: metrics_centralized {'accuracy': [(0, 0.5655196309089661), (1, 2.490957498550415), (2, 0.2590666115283966), (3, 0.9301236867904663), (4, 0.3247184455394745), (5, 0.35737916827201843)]}
2 clients connected.
10.1.3.0 - - [15/Jul/2024 15:58:02] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:58:02] "POST /check_connection HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:58:13] "POST /upload_kpi04 HTTP/1.1" 200 -
10.1.3.0 - - [15/Jul/2024 15:58:14] "POST /check_connection HTTP/1.1" 200 -

View File

@@ -0,0 +1,62 @@
absl-py==2.0.0
astunparse==1.6.3
blinker==1.7.0
cachetools==5.3.2
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
cryptography==41.0.5
Flask==3.0.0
flatbuffers==23.5.26
flwr==1.5.0
gast==0.5.4
google-auth==2.23.4
google-auth-oauthlib==1.0.0
google-pasta==0.2.0
grpcio==1.59.2
h5py==3.10.0
idna==3.4
iterators==0.0.2
itsdangerous==2.1.2
Jinja2==3.1.2
joblib==1.3.2
keras==2.14.0
libclang==16.0.6
Markdown==3.5.1
MarkupSafe==2.1.3
ml-dtypes==0.2.0
netifaces==0.11.0
numpy==1.26.1
oauthlib==3.2.2
opt-einsum==3.3.0
packaging==23.2
pandas==2.1.2
protobuf==3.20.3
psutil==5.9.6
pyasn1==0.5.0
pyasn1-modules==0.3.0
pycparser==2.21
pycryptodome==3.19.0
Pympler==1.0.1
python-dateutil==2.8.2
pytz==2023.3.post1
requests==2.31.0
requests-oauthlib==1.3.1
rsa==4.9
scikit-learn==1.3.2
scipy==1.11.3
six==1.16.0
tensorboard==2.14.1
tensorboard-data-server==0.7.2
tensorflow==2.14.0
tensorflow-estimator==2.14.0
tensorflow-io-gcs-filesystem==0.34.0
termcolor==2.3.0
threadpoolctl==3.2.0
typing_extensions==4.8.0
tzdata==2023.3
urllib3==2.0.7
watchdog==3.0.0
Werkzeug==3.0.1
wrapt==1.14.1

View File

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

View File

@@ -0,0 +1,506 @@
import flwr as fl
import tensorflow as tf
from tensorflow import keras
from typing import Dict, Optional, Tuple, List, Union
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import sys
import json
from flwr.server.client_manager import SimpleClientManager
from flwr.server.client_proxy import ClientProxy
from abc import ABC
from logging import INFO
from flwr.common.logger import log
from time import sleep
from time import time_ns
from flask import Flask, request
import threading
import os
Scalar = Union[bool, bytes, float, int, str]
Config = Dict[str, Scalar]
param_file = None
global best_model, list_kpi_11
selected_clients_ids = [] # This is the list of client IDs the Agg.Node receives from the DMLO and will use for training.
all_round_reports = {} # The dictionary containing all the round reports
flwr_port = sys.argv[1]
dmlo_port = sys.argv[2]
# server_ip = ip
l_kpi1, l_kpi2, l_kpi4, l_kpi5, list_kpi_11 = [], [], [], [], []
app = Flask(__name__)
@app.route("/config_server", methods=["POST"])
def config_server():
global param_file
param_file = request.json
param_received.set()
# print("_____Received a config file", flush=True)
try:
global req_clients
# highest_req_clients = max(req_clients, highest_req_clients)
req_clients = request["training_clients_per_round"]
# print(f"_____The new number of clients (req_clients) is: {req_clients} and the highest was had so far is {highest_req_clients}", flush=True)
# if req_clients > highest_req_clients:
# print(f"_____Rescaled the last dimension to {req_clients}", flush=True)
# kpis = np.resize(kpis, (epochs+1, 12, req_clients))
except:
# print("_____Except path triggered", flush=True)
pass
return "Parameters received successfully.", 200
@app.route(
"/select_clients", methods=["GET"]
) # The method that will receive the list of client IDs the server will use for training.
def select_clients():
global selected_clients_ids
selected_clients_ids = request.json["eligible_clients_ids"]
if len(selected_clients_ids) != req_clients:
print(
f"WARNING: {req_clients} clients are needed but only {len(selected_clients_ids)} client IDs are received. The training will wait for another list with enough eligible clients."
)
# A selection logic can be added here to modify the "selected_clients_id" variable. Do not forget to modify the next line (return) if this logic is added
return request.json, 200
@app.route("/check_connection", methods=["POST"])
def check_connection():
"""A function part of the older system to synchronize the processes.
It does not hurt to keep for the final version to check server availability.
"""
return "Agg.Node is online", 200
@app.route("/terminate_app", methods=["POST"])
def terminate_app():
try:
save_kpis()
except:
print("No KPIs saved.")
try:
global best_model
tf.keras.models.save_model(
model=best_model,
filepath="../resources/last_model.h5",
overwrite=True,
save_format="h5",
)
except:
print("No model has been saved")
print("Agg.Node shutting down...")
end_thread = threading.Thread(target=__terminate__)
end_thread.start()
# myserver.disconnect_all_clients(timeout=None)
return "Agg.Node successfully received shutdown command.", 200
@app.route("/upload_kpi01", methods=["POST"])
def upload_kpi01():
"""for automatic averaging if needed again
received01 += 1
if received01 != 1:
kpi01_value = (kpi01_value*((received01-1)/received01)) + (((request.json["kpi01"] - uc6_01_start)/1000000000)/received01)
print(f"KPI01 average so far: {kpi01_value}")
else: kpi01_value = (request.json["kpi01"] - uc6_01_start)/1000000000
return "", 200
"""
l_kpi1.append((request.json["kpi01"] - uc6_01_start) / 1000000000)
if (
current_training_round != 1
): # Skipping the measurement for the first round as it is inaccurate because of the starting process
kpis[current_training_round, 1, len(l_kpi1) - 1] = (
request.json["kpi01"] - uc6_01_start
) / 1000000000
return "", 200
@app.route("/upload_kpi02", methods=["POST"])
def upload_kpi02():
tmp = (request.json["kpi02"] - (uc6_02_help_end - uc6_02_help_start)) / 1000000000
l_kpi2.append(tmp)
kpis[current_training_round, 2, len(l_kpi2) - 1] = tmp
return "", 200
@app.route("/upload_kpi04", methods=["POST"])
def upload_kpi04():
try:
l_kpi4.append(request.json["kpi04"])
kpis[current_training_round, 4, len(l_kpi4) - 1] = request.json["kpi04"]
except:
pass
return "", 200
@app.route("/upload_kpi05", methods=["POST"])
def upload_kpi05():
l_kpi5.append(request.json["kpi05"])
kpis[current_training_round, 5, len(l_kpi5) - 1] = request.json["kpi05"]
return "", 200
@app.route("/get_status", methods=["GET"])
def get_status():
try:
with open("Round_report.txt", "r") as file:
report = file.read()
return report
except FileNotFoundError:
return "No report available", 200
except Exception as e:
return f"An error occurred: {e}", 500
def __terminate__():
sleep(2)
os._exit(0)
def run_flask():
app.run(host="0.0.0.0", port=dmlo_port)
param_received = threading.Event()
flask_thread = threading.Thread(target=run_flask)
flask_thread.setDaemon(True)
flask_thread.start()
param_received.wait()
local_training = param_file["hyperparam_epochs"]
epochs = param_file["num_epochs"]
req_clients = param_file["training_clients_per_round"] # Number of clients to train
# highest_req_clients = req_clients # the highest number of clinets a round has had so far (to resize the KPI matrix if needed)
hyperparam_learning_rate = param_file["hyperparam_learning_rate"]
hyperparam_batch_size = param_file["hyperparam_batch_size"]
ml_model = param_file["ml_model"]
kpis = np.empty((epochs + 1, 12, 8), dtype=object)
print("Parameters loaded")
q_alpha = 0.95
n_features = 3
n_future = 1
n_past = 400
def save_kpis():
try:
np.save("kpis.npy", kpis)
except:
print("No KPIs recorded so far.")
def save_round_report(round_status):
all_round_reports[f"Round {current_training_round}"] = round_status
try:
with open("Round_report.txt", "w") as file:
json.dump(all_round_reports, file, indent=4)
except Exception as e:
print(f"An error occurred: {e}")
class QuantileMetric(tf.keras.metrics.Metric):
def __init__(self, name="quantile_metric", **kwargs):
super(QuantileMetric, self).__init__(name=name, **kwargs)
self.quantile_metric = self.add_weight(
name="quantile_metric", initializer="zeros"
)
self.quantile_metric_count = self.add_weight(
name="quantile_metric_count", initializer="zeros"
)
def update_state(self, y_true, y_pred, sample_weight=None):
quantileCondition = tf.math.greater(y_true, tf.squeeze(y_pred))
qc = tf.math.reduce_sum(tf.cast(quantileCondition, tf.float32))
self.quantile_metric.assign_add(qc)
self.quantile_metric_count.assign_add(
tf.cast(tf.size(quantileCondition), tf.float32)
)
def result(self):
return self.quantile_metric / self.quantile_metric_count
def reset_state(self):
self.quantile_metric.assign(0.0)
self.quantile_metric_count.assign(0)
def tilted_loss(y_true, y_pred):
q = q_alpha
e = y_true - y_pred
tl = tf.stack([q * e, (q - 1) * e])
e_max = tf.math.reduce_max(tl, axis=0, keepdims=True)
return tf.reduce_mean(e_max)
""" Choosing GPU
gpu_id = 0 # Index of the GPU you want to use
physical_devices = tf.config.list_physical_devices('GPU')
print(physical_devices)
tf.config.set_visible_devices(physical_devices[gpu_id], 'GPU')
tf.config.experimental.set_memory_growth(physical_devices[gpu_id], True)
"""
def main() -> None:
global best_model
print("Inializing Model")
best_model = tf.keras.models.load_model(ml_model, compile=False)
print("Model loaded")
opt = tf.keras.optimizers.Adam(learning_rate=hyperparam_learning_rate)
best_model.compile(
optimizer=opt,
loss=[tilted_loss],
metrics=[QuantileMetric(), keras.metrics.MeanAbsoluteError()],
)
print("Model Compiled")
class CustomStrategy(fl.server.strategy.FedAdagrad):
def aggregate_fit(self, rnd, results, failures):
uc6_03_start = time_ns()
aggregated_parameters = super().aggregate_fit(rnd, results, failures)
uc6_03_end = time_ns()
global kpi_uc6_03
kpi_uc6_03 = (
(uc6_03_end - uc6_03_start) / 1000000000
) # Time required to aggregate all locally trained models sent by the OBUs in sec (Target <5s)
kpis[current_training_round, 3, 0] = kpi_uc6_03
per_client_accuracy = []
per_client_loss = []
clients_order = [] # To map the accuracy and loss to a client ID (n'th ID to the n'th accuracy/loss)
for result in results:
client_info = result[1].metrics
clients_order.append(client_info["id"])
per_client_accuracy.append(client_info["accuracy"])
per_client_loss.append(client_info["loss"])
round_status = {
"is_completed": "True",
"current_accuracy": accuracy_perc,
"current_loss": loss_perc,
"lost_clients": len(failures),
"clients_order": clients_order,
"per_client_accuracy": per_client_accuracy,
"per_client_loss": per_client_loss,
}
save_round_report(round_status)
kpi_uc6_11 = round(
100 - ((len(failures) / (len(results) + len(failures))) * 100), 1
) # The % of successfully uploaded trained models for a certain round (Target >90%)
kpis[current_training_round, 11, 0] = kpi_uc6_11
list_kpi_11.append(kpi_uc6_11)
kpi_uc6_10 = sum(list_kpi_11) / len(
list_kpi_11
) # The % of successfully uploaded trained models in total (Target >90%)
kpis[current_training_round, 10, 0] = kpi_uc6_10
return aggregated_parameters
strategy = CustomStrategy(
evaluate_fn=get_evaluate_fn(best_model),
on_fit_config_fn=fit_config,
initial_parameters=fl.common.ndarrays_to_parameters(best_model.get_weights()),
)
class GetPropertiesIns:
"""Properties request for a client."""
def __init__(self, config: Config):
self.config = config
test: GetPropertiesIns = GetPropertiesIns(config={"server_round": 1})
class Criterion(ABC):
"""Abstract class which allows subclasses to implement criterion
sampling."""
def select(self, client: ClientProxy) -> bool:
"""Decide whether a client should be eligible for sampling or not."""
# if client.get_properties(ins=test, timeout = None).properties["client_id"] in eligible_clients_ids: #This line makes the selection logic on the server side but needs clients to be connected first. In the final test version, the logic is elsewhere. This function just uses the previous selection
if (
client.get_properties(ins=test, timeout=None).properties["client_id"]
in selected_clients_ids
):
return True
else:
# # Code to debug clients not being selected for training despite selecting their ID (first thought: ID as str compared to ID as int will always return false)
# print(f"Rejected: _{client.get_properties(ins=test, timeout = None).properties['client_id']}_ with the list being:")
# for i in selected_clients_ids:
# print(f"_{i}_")
return False
c = Criterion()
class CustomClientManager(SimpleClientManager):
def sample(
self,
num_clients: int = 2, # Number of clients currently connected to the server
rq_clients: int = req_clients, # Number of clients to train (added)
min_num_clients: int = 3,
min_wait: int = req_clients, # Number of clients to have before beginning the selection (added)
criterion: [Criterion] = c,
) -> List[ClientProxy]:
"""Sample a number of Flower ClientProxy instances."""
# Block until at least num_clients are connected.
if min_wait is None:
min_wait = num_clients
self.wait_for(min_wait)
print(f"{min_wait} clients connected.")
connection_attempts = 40 # Helper variable to give the OBUs more time to start and connect to the agg.node
while connection_attempts != 0:
# Sample clients which meet the criterion
available_cids = list(self.clients)
if criterion is not None:
available_cids = [
cid
for cid in available_cids
if criterion.select(self.clients[cid])
]
if rq_clients > len(available_cids):
log(
INFO,
"Sampling failed: number of available clients"
" (%s) is less than number of requested clients (%s).",
len(available_cids),
rq_clients,
)
connection_attempts -= 1
print(
f"Retrying in 5 seconds. Attempts left: {connection_attempts}"
)
sleep(5)
else:
break
if rq_clients > len(available_cids):
return []
sampled_cids = available_cids
return [self.clients[cid] for cid in sampled_cids]
fl.server.start_server(
server_address=f"0.0.0.0:{flwr_port}",
config=fl.server.ServerConfig(num_rounds=epochs),
strategy=strategy,
client_manager=CustomClientManager(),
)
def get_evaluate_fn(best_model):
"""Return an evaluation function for server-side evaluation."""
# The `evaluate` function will be called after every round
def evaluate(
server_round: int,
parameters: fl.common.NDArrays,
config: Dict[str, fl.common.Scalar],
) -> Optional[Tuple[float, Dict[str, fl.common.Scalar]]]:
global uc6_02_help_start
uc6_02_help_start = (
time_ns()
) # Time to be substracted as processing time to know the model upload time
best_model.set_weights(parameters) # Update model with the latest parameters
df_final = pd.read_csv("../resources/test.csv")
df_train = pd.read_csv("../resources/data.csv")
# train test validation split
test_df = df_final
# Scaling the dataframe
test = test_df
scalers = {}
# Scaling train data
for i in test_df.columns:
scaler = MinMaxScaler(feature_range=(-1, 1))
s_s = scaler.fit_transform(test[i].values.reshape(-1, 1))
s_s = np.reshape(s_s, len(s_s))
scalers["scaler_" + i] = scaler
test[i] = s_s
def split_series(series, n_past, n_future):
X, y = list(), list()
# Loop to create array of every observations (past) and predictions (future) for every datapoint
for window_start in range(len(series)):
# Calculating boundaries for each datapoint
past_end = window_start + n_past
future_end = past_end + n_future
# Loop will end if the number of datapoints is less than observations (past)
if future_end > len(series):
break
past, future = (
series[window_start:past_end, :],
series[past_end:future_end, :],
)
X.append(past)
y.append(future)
return np.array(X), np.array(y)
X_test, y_test = split_series(test.values, n_past, n_future)
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], n_features))
y_test = y_test.reshape((y_test.shape[0], y_test.shape[1], n_features))
print(X_test.shape)
print(y_test.shape)
y_test_sliced = y_test[:, :, 2]
np.save("X_test_server.npy", X_test)
np.save("y_test_server.npy", y_test_sliced)
loss, metric, error = best_model.evaluate(X_test, y_test_sliced)
pred = best_model.predict(X_test)
pred_copies = np.repeat(pred, 3, axis=-1)
pred_copies = np.expand_dims(pred_copies, axis=1)
for index, i in enumerate(test_df.columns):
scaler = scalers["scaler_" + i]
pred_copies[:, :, index] = scaler.inverse_transform(
pred_copies[:, :, index]
)
y_test[:, :, index] = scaler.inverse_transform(y_test[:, :, index])
np.save("prediction_server.npy", pred_copies)
np.save("test_server.npy", y_test)
global loss_perc, accuracy_perc
loss_perc = loss
accuracy_perc = error
save_kpis()
return loss, {"accuracy": error}
return evaluate
def fit_config(server_round: int):
"""Return training configuration dict for each round.
Keep batch size fixed at 2048, perform two rounds of training with one
local epoch, increase to two local epochs afterwards.
"""
global \
current_training_round, \
uc6_02_help_end, \
uc6_01_start, \
l_kpi1, \
l_kpi2, \
l_kpi4, \
l_kpi5
current_training_round = server_round
l_kpi1, l_kpi2, l_kpi4, l_kpi5 = [], [], [], []
uc6_02_help_end = time_ns()
uc6_01_start = time_ns()
config = {
"batch_size": hyperparam_batch_size,
"local_epochs": local_training,
}
return config
if __name__ == "__main__":
main()

163
compose.yaml Normal file
View File

@@ -0,0 +1,163 @@
services:
aggregator:
build:
context: ./aggregator-node
container_name: aggregator
ports:
- "34000:8080"
- "34001:5000"
environment:
- FLWR_PORT=8080
- DMLO_PORT=5000
healthcheck:
test: >
curl -f -s http://localhost:5000 ||
[ "$(curl -o /dev/null -s -w '%{http_code}' http://localhost:5000)" -eq 404 ]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
pqos:
build:
context: ./pqos
container_name: pqos
ports:
- "32000:5000"
environment:
- ENDPOINT=https://webhook.site/9ebcf608-2c9a-4302-87e5-5b477831b6b
healthcheck:
test: >
curl -f -s http://localhost:5000 ||
[ "$(curl -o /dev/null -s -w '%{http_code}' http://localhost:5000)" -eq 404 ]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
nmsender:
build:
context: ./5g-uulm-network-monitoring/
dockerfile: ./docker/nginx.Dockerfile
container_name: nginx
ports:
- "31000:1935"
deploy:
resources:
limits:
cpus: "1.0"
restart: unless-stopped
# healthcheck:
# test: >
# ffprobe rtmp://localhost/live/test
# interval: 30s
# timeout: 10s
# retries: 5
server_config:
depends_on:
aggregator:
condition: service_healthy
build:
context: ./config
dockerfile: server_config.Dockerfile
container_name: server_config
nmcli1:
depends_on:
server_config:
condition: service_completed_successfully
nmsender:
condition: service_started
build:
context: ./5g-uulm-network-monitoring/
dockerfile: ./docker/nmcli_default.Dockerfile
container_name: nmcli1
ports:
- "45000:8000"
environment:
- RUST_LOG=info
- ROCKET_CONFIG=/etc/videoprobe/Rocket.toml
- GNSS_DEV=/dev/ttyACM0
- GNSS_ENABLED=false
restart: unless-stopped
nmcli2:
depends_on:
server_config:
condition: service_completed_successfully
nmsender:
condition: service_started
build:
context: ./5g-uulm-network-monitoring/
dockerfile: ./docker/nmcli_default.Dockerfile
container_name: nmcli2
ports:
- "55000:8000"
environment:
- RUST_LOG=info
- ROCKET_CONFIG=/etc/videoprobe/Rocket.toml
- GNSS_DEV=/dev/ttyACM0
- GNSS_ENABLED=false
restart: unless-stopped
client1_config:
depends_on:
- nmcli1
- nmcli2
build:
context: ./config
dockerfile: client_config.Dockerfile
environment:
- ENDPOINT=http://172.17.0.1:45000
container_name: client1_config
# client2_config:
# depends_on:
# - nmcli2
# build:
# context: ./configs
# dockerfile: client_config.Dockerfile
# environment:
# - ENDPOINT: http://172.17.0.1:55000
# container_name: client2_config
client1:
depends_on:
- client1_config
build:
context: ./obu-node
container_name: client1
ports:
- "41000:8080"
- "41001:5000"
- "41002:80"
environment:
- SERVER_IP_FLWR=172.17.0.1:34000
- SERVER_IP_AGG=172.17.0.1:34001
- CLIENT_ID=1
deploy:
resources:
limits:
cpus: "1.0"
restart: unless-stopped
client2:
depends_on:
- client1_config
build:
context: ./obu-node
container_name: client2
ports:
- "51000:8080"
- "51001:5000"
- "51002:80"
environment:
- SERVER_IP_FLWR=172.17.0.1:34000
- SERVER_IP_AGG=172.17.0.1:34001
- CLIENT_ID=2
deploy:
resources:
limits:
cpus: "1.0"
restart: unless-stopped

View File

@@ -0,0 +1,9 @@
FROM curlimages/curl:latest
# Use curl to send POST and GET requests
CMD curl -X GET -H "Content-Type: application/json" \
-d '{"node_ip": ["http://172.17.0.1:41002/upload"], "stream_ip": "172.17.0.1", "stream_url": "rtmp://172.17.0.1:31000/live/test"}' \
http://172.17.0.1:45000/demo/start && \
curl -X GET -H "Content-Type: application/json" \
-d '{"node_ip": ["http://172.17.0.1:51002/upload"], "stream_ip": "172.17.0.1", "stream_url": "rtmp://172.17.0.1:31000/live/test"}' \
http://172.17.0.1:55000/demo/start

View File

@@ -0,0 +1,9 @@
FROM curlimages/curl:latest
# Use curl to send POST and GET requests
CMD curl -X POST -H "Content-Type: application/json" \
-d '{ "ml_model": "../resources/best_model_no_tuner_40.h5", "num_epochs": 10, "min_working_nodes": 2, "hyperparam_epochs": 5, "hyperparam_batch_size": 2048, "hyperparam_learning_rate": 0.001, "avg_algorithm": "FedAvg", "training_clients_per_round": 2 }' \
http://172.17.0.1:34001/config_server && \
curl -X GET -H "Content-Type: application/json" \
-d '{"eligible_clients_ids" : ["1", "2"]}' \
http://172.17.0.1:34001/select_clients

947
notes.md Normal file
View File

@@ -0,0 +1,947 @@
<!-- markdownlint-disable MD013 MD024 -->
# Notes
## Running Network Monitoring and Training
### mec
- To start aggregator: `docker run -p 34000:8080 -p 34001:5000 -e FLWR_PORT=8080 -e DMLO_PORT=5000 --name aggregator --rm aggregator:latest`
- To start pqos: `docker run --rm --name pqos -e ENDPOINT=https://webhook.site/9ebcf608-2c9a-4302-87e5-5b477831b6b -p 32000:5000 pqos:latest`
- To start nmsender: `docker run --rm --cpus=1 -p 31000:1935 --name nginx nginx-stream`
### Send server_config to aggregator and do client_select
- config: `curl -X POST -H "Content-Type: application/json" -d '{ "ml_model": "../resources/best_model_no_tuner_40.h5", "num_epochs": 10, "min_working_nodes": 2, "hyperparam_epochs": 5, "hyperparam_batch_size": 2048, "hyperparam_learning_rate": 0.001, "avg_algorithm": "FedAvg", "training_clients_per_round": 2}' http://172.17.0.1:34001/config_server`
- client_select: `curl -X GET -H "Content-Type: application/json" -d '{"eligible_clients_ids" : ["1", "2"]}' http://172.17.0.1:34001/select_clients`
### Run nmclient on obus
`docker run -p 45000:8000 -e RUST_LOG=info -e ROCKET_CONFIG=/etc/videoprobe/Rocket.toml -e GNSS_DEV=/dev/ttyACM0 -e GNSS_ENABLED=false --name nmcli1 nmcli:latest`
`docker run -p 55000:8000 -e RUST_LOG=info -e ROCKET_CONFIG=/etc/videoprobe/Rocket.toml -e GNSS_DEV=/dev/ttyACM0 -e GNSS_ENABLED=false --name nmcli2 nmcli:latest`
### Send config to nmclients on obus
`curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"http://172.17.0.1:41002/upload\"], \"stream_ip\": \"172.17.0.1\", \"stream_url\": \"rtmp://172.17.0.1:31000/live/test\"}" http://172.17.0.1:45000/demo/start`
`curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"http://172.17.0.1:51002/upload\"], \"stream_ip\": \"172.17.0.1\", \"stream_url\": \"rtmp://172.17.0.1:31000/live/test\"}" http://172.17.0.1:55000/demo/start`
### Start training on Clients
`docker run --cpus=1 -p 41000:8080 -p 41001:5000 -p 41002:80 -e SERVER_IP_FLWR=172.17.0.1:34000 -e SERVER_IP_AGG=172.17.0.1:34001 -e CLIENT_ID=1 --name client1 --rm obu:latest`
`docker run --cpus=1 -p 51000:8080 -p 51001:5000 -p 51002:80 -e SERVER_IP_FLWR=172.17.0.1:34000 -e SERVER_IP_AGG=172.17.0.1:34001 -e CLIENT_ID=2 --name client2 --rm obu:latest`
## 5G IANA
Here I'll describe how we deploy this setup onto the 5G IANA platform.
### Setup Components
We'll need the following 5 components:
- nmsender
- nmclient
- aggregator
- obu-node
- pqos
All images are on the custom docker registry: [192.168.100.2:5000/uulm](192.168.100.2:5000/uulm) with
the 5g-iana user.
#### uc6nmsen2 / nmsender
nmsender is pushed as nginx:v1.2.2 onto the registry. The components name is: uc6nmsen2
##### General
Name: uc6nmsen2
Architecture: amd64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: nginx:v1.2.2
##### Minimum Execution Requirements
vCPUs: 1
RAM: 2048
Storage: 10
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: ffprobe -v quiet -print_format json -show_streams rtmp://localhost/live/test
Time Interval: 10
##### ~~Container Execution~~
##### ~~Environment Variables~~
##### Exposed Interfaces
uc6nmsen21935: 1935 / Access / TCP/UDP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6nmclient / nmclient
nmclient is pushed as passive_network_monitoring:v1.2.1. The components name is:
uc6nmclient
##### General
Name: uc6nmclient
Architecture: amd64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: passive_network_monitoring:v1.2.1
##### Minimum Execution Requirements
vCPUs: 1
RAM: 512
Storage: 10
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: <http://localhost:8000>
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
ROCKET_CONFIG: /etc/videoprobe/Rocket.toml
GNSS_DEV: /dev/ttyACM0
RUST_LOG: info
GNSS_ENABLED: true
##### Exposed Interfaces
uc6nmclientstatus: 8000 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6aggnode / aggregator
aggregator is pushed as uc6aggnode:v1.1.0. The component is: uc6aggnode
##### General
Name: uc6aggnode
Architecture: amd64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: uc6aggnode:v0.9.0
##### Minimum Execution Requirements
vCPUs: 1
RAM: 1024
Storage: 10
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: <http://localhost:8080>
Time Interval: 10
##### ~~Container Execution~~
##### ~~Environment Variables~~
##### Exposed Interfaces
uc6aggnode8080: 8080 / Access / TCP
uc6aggnode5000: 5000 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6dmltrain0 / obu-node
We'll need this twice, with different environment variables
obu-node is pushed as training_agent:v1.2.0. The component name is: uc6dmltrain0
##### General
Name: uc6dmltrain0
Architecture: amd64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: training_agent:v1.2.0
##### Minimum Execution Requirements
vCPUs: 1
RAM: 4096
Storage: 4
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: [ http://localhost:80 ]
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
- SERVER_IP_FLWR=192.168.100.4:30765
- SERVER_IP_AGG=192.168.100.4:31810
- CLIENT_ID=0
##### Exposed Interfaces
uc6dmltrain08080: 8080 / Access / TCP
uc6dmltrain080: 80 / Access / TCP
obustart0: 5001 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6dmltrain1 / obu-node
We'll need this twice, with different environment variables
obu-node is pushed as training_agent:v1.2.0. The component name is: uc6dmltrain1
##### General
Name: uc6dmltrain1
Architecture: amd64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: training_agent:v1.2.0
##### Minimum Execution Requirements
vCPUs: 1
RAM: 4096
Storage: 4
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: [ http://localhost:80 ]
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
- SERVER_IP_FLWR=192.168.100.4:30765
- SERVER_IP_AGG=192.168.100.4:31810
- CLIENT_ID=1
##### Exposed Interfaces
uc6dmltrain18080: 8080 / Access / TCP
uc6dmltrain180: 80 / Access / TCP
obustart1: 5001 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6pqos / pqos
pqos is pushed as uc6pqos:v1.2.0. The component is uc6pqos
##### General
Name: uc6pqos
Architecture: amd64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: uc6pqos:v1.2.0
##### Minimum Execution Requirements
vCPUs: 1
RAM: 512
Storage: 10
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: [http://localhost:5000]
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
ENDPOINT: [ https://webhook.site/9ebcf608-2c9a-4302-87e5-5b477831b6b ]
##### Exposed Interfaces
uc6pqos5000: 5000 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6dmlarm0 / obu-node
We'll need this twice, with different environment variables
obu-node is pushed as training_agent:v1.2.0. The component name is: uc6dmlarm0
##### General
Name: us6dmlarm0
Architecture: arm64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: training_agent:v1.2.0
Docker Username: 5g-iana
Docker Password: 5g-iana
Custom Docker Registry: [ 192.168.100.2:5000/uulm ]
##### Minimum Execution Requirements
vCPUs: 1
RAM: 4096
Storage: 4
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: [ http://localhost:80 ]
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
- SERVER_IP_FLWR=192.168.100.4:30765
- SERVER_IP_AGG=192.168.100.4:31810
- CLIENT_ID=3
##### Exposed Interfaces
uc6dmlarm08080: 8080 / Access / TCP
uc6dmlarm080: 80 / Access / TCP
obustart3: 5001 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6dmlarm1 / obu-node
We'll need this twice, with different environment variables
obu-node is pushed as training_agent:v1.2.0. The component name is: uc6dmlarm1
##### General
Name: uc6dmlarm1
Architecture: arm64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: training_agent:v1.2.0
Docker Username: 5g-iana
Docker Password: 5g-iana
Custom Docker Registry: [ 192.168.100.2:5000/uulm ]
##### Minimum Execution Requirements
vCPUs: 1
RAM: 4096
Storage: 4
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: [ http://localhost:80 ]
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
- SERVER_IP_FLWR=192.168.100.4:31805
- SERVER_IP_AGG=192.168.100.4:30760
- CLIENT_ID=4
##### Exposed Interfaces
uc6dmlarm18080: 8080 / Access / TCP
uc6dmlarm180: 80 / Access / TCP
obustart4: 5001 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
#### uc6dmlarm2 / obu-node
We'll need this twice, with different environment variables
obu-node is pushed as training_agent:v1.2.0. The component name is: uc6dmlarm2
##### General
Name: uc6dmlarm2
Architecture: arm64
Elasticity Controller: HORIZONTAL
##### Distribution Parameters
Docker Image: training_agent:v1.2.0
Docker Username: 5g-iana
Docker Password: 5g-iana
Custom Docker Registry: [ 192.168.100.2:5000/uulm ]
##### Minimum Execution Requirements
vCPUs: 1
RAM: 4096
Storage: 4
Hypervisor Type: ESXI
##### Health Check
HTTP/Command: [ http://localhost:80 ]
Time Interval: 10
##### ~~Container Execution~~
##### Environment Variables
- SERVER_IP_FLWR=192.168.100.4:30765
- SERVER_IP_AGG=192.168.100.4:31810
- CLIENT_ID=2
##### Exposed Interfaces
uc6dmlarm28080: 8080 / Access / TCP
uc6dmlarm280: 80 / Access / TCP
obustart2: 5001 / Access / TCP
##### ~~Required Interfaces~~
##### ~~Plugins~~
##### ~~Volumes~~
##### ~~Devices~~
##### ~~Labels~~
##### ~~Advanced Options~~
### Setup Applications
#### uc6nmsen2 / uc6nmsen2
Has uc6nmsen2 with the name uc6nmsen21831.
#### uc6nmcli2 / uc6nmclient
Has uc6nmclient with the name us6nmclient1771.
#### uc6aggnode / uc6aggnode
Has uc6aggnode with the name uc6aggnode1781.
#### uc6dmltrain0 / uc6dmltrain0
Has uc6dmltrain0 with the name uc6dmltrain02031.
#### uc6dmltrain1 / uc6dmltrain1
Has uc6dmltrain1 with the name uc6dmltrain1541.
#### uc6pqos / uc6pqos
Has uc6pqos with the name uc6pqos1841.
#### uc6dmlarm0 / uc6dmlarm0
Has uc6dmlarm0 with the name uc6dmlarm02041.
#### uc6dmlarm1 / uc6dmlarm1
Has uc6dmlarm1 with the name uc6dmlarm12051.
#### uc6dmlarm2 / uc6dmlarm2
Has uc6dmlarm2 with the name uc6dmlarm22061.
### Setup Deployment
#### uc6aggnode6 / uc6aggnode
##### Configure "uc6aggnode1781" Component
Select node: 5g-iana-mec
##### Set the Constraints of "uc6aggnode1781" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6nmsender / uc6nmsen2
##### Configure "uc6nmsen21831" Component
Select node: 5g-iana-mec
##### Set the Constraints of "uc6nmsen21831" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6pqos0 / uc6pqos
##### Configure "uc6pqos1841" Component
Select node: 5g-iana-mec
##### Set the Constraints of "uc6pqos1841" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6nmcli1 / uc6nmcli2
Select node: uulm-obu1
##### Set the Constraints of "" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6nmcli0 / uc6nmcli2
##### Configure "" Component
Select node: uulm-obu0
##### Set the Constraints of "uc6nmclient1771" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6dml90 / uc6dmltrain0
##### Configure "uc6dmltrain02031" Component
Select node: uulm-obu0
##### Set the Constraints of "uc6dmltrain02031" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6dml91 / uc6dmltrain1
##### Configure "uc6dmltrain1541" Component
Select node: uulm-obu1
##### Set the Constraints of "uc6dmltrain1541" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6dmlarm0 / uc6dmlarm0
##### Configure "uc6dmlarm02041" Component
Select node: orin
##### Set the Constraints of "uc6dmlarm02041" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6dmlarm1 / uc6dmlarm1
##### Configure "uc6dmlarm12051" Component
Select node: ubuntu
##### Set the Constraints of "uc6dmlarm12051" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
#### uc6dmlarm2 / uc6dmlarm2
##### Configure "uc6dmlarm22061" Component
Select node: links-vobu-1
##### Set the Constraints of "uc6dmlarm22061" Access Interface
Radio Service Types: eMBB
Uplink Bandwidth: 1
Downlink Bandwidth: 10
### Curl commands
```sh
# towards uc6aggnode
curl -X POST -H "Content-Type: application/json" -d '{ "ml_model": "../resources/best_model_no_tuner_40.h5", "num_epochs": 10, "min_working_nodes": 1, "hyperparam_epochs": 5, "hyperparam_batch_size": 2048, "hyperparam_learning_rate": 0.001, "avg_algorithm": "FedAvg", "training_clients_per_round": 1}' http://172.17.0.1:34001/config_server
curl -X POST -H "Content-Type: application/json" -d '{"eligible_clients_ids" : ["1"]}' http://172.17.0.1:34001/select_clients
# towards nmcli2
curl -X GET -H "Content-Type: application/json" -d "{\"node_ip\": [\"http://172.17.0.1:41002/upload\"], \"stream_ip\": \"172.17.0.1\", \"stream_url\": \"rtmp://172.17.0.1:31000/live/test\"}" http://172.17.0.1:45000/demo/start
```
### Possible Tags
```sh
curl -u 5g-iana:5g-iana -k https://192.168.100.2:5000/v2/uulm/nginx/tags/list
{"name":"uulm/nginx","tags":["v1.2.1","v1.1.0","v1.2.2","v1.1.1"]}
```
```sh
curl -u 5g-iana:5g-iana -k https://192.168.100.2:5000/v2/uulm/passive_network_monitoring/tags/list
{"name":"uulm/passive_network_monitoring","tags":["v1.2.1","v1.1.0","v1.2.0","v1.1.1"]}
```
```sh
curl -u 5g-iana:5g-iana -k https://192.168.100.2:5000/v2/uulm/uc6aggnode/tags/list
{"name":"uulm/uc6aggnode","tags":["v0.9.0"]}
```
```sh
curl -u 5g-iana:5g-iana -k https://192.168.100.2:5000/v2/uulm/training_agent/tags/list
{"name":"uulm/training_agent","tags":["v1.1.0","v1.2.0","v1.1.1"]}
```
### Kubernetes Stats
#### Aggregator node
```sh
nxw@5g-iana-manager:~$ kc get all -n 31ff3ac6-c9c9-454c-8131-f0be06dfd711
NAME READY STATUS RESTARTS AGE
pod/uc6agg-uc6aggnode1781-eeqsuyvfcx-deployment-767949685b-tlrn8 1/1 Running 4 (44h ago) 45h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6agg-uc6aggnode1781-eeqsuyvfcx-service NodePort 10.152.183.223 <none> 8080:30765/TCP,5000:31810/TCP 45h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6agg-uc6aggnode1781-eeqsuyvfcx-deployment 1/1 1 1 45h
```
Curl only from obu or mec
Curl to start this: `curl -X POST -H "Content-Type: application/json" -d '{ "ml_model": "../resources/best_model_no_tuner_40.h5", "num_epochs": 10, "min_working_nodes": 2, "hyperparam_epochs": 5, "hyperparam_batch_size": 2048,"hyperparam_learning_rate": 0.001, "avg_algorithm": "FedAvg", "training_clients_per_round": 2}' http://192.168.100.4:31808/config_server`
Client Selection: `curl -X GET -H "Content-Type: application/json" -d '{"eligible_clients_ids" : ["0", "1"]}' http://192.168.100.4:31808/select_clients`
#### uc6dml on uulm-obu0 / uulm-obu1
##### uulm-obu0
```sh
nxw@5g-iana-manager:~$ kc get all -n 5dcb12b1-a7a9-4b73-b290-a30f1d02b7db
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6dmltrain0-uc6dmltrain02031-jekf9nmeui-service NodePort 10.152.183.71 <none> 80:31418/TCP,8080:30402/TCP,5001:32126/TCP 45h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6dmltrain0-uc6dmltrain02031-jekf9nmeui-deployment 0/0 0 0 45h
NAME DESIRED CURRENT READY AGE
replicaset.apps/uc6dmltrain0-uc6dmltrain02031-jekf9nmeui-deployment-7b779477b4 0 0 0 45h
```
##### uulm-obu1
```sh
nxw@5g-iana-manager:~$ kc get all -n 7cc5f73c-e349-493c-a592-9e0e685a0a65
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6dmltrain1-uc6dmltrain1541-xtyata0ycb-service NodePort 10.152.183.89 <none> 8080:30022/TCP,80:32174/TCP,5001:31066/TCP 45h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6dmltrain1-uc6dmltrain1541-xtyata0ycb-deployment 0/0 0 0 45h
NAME DESIRED CURRENT READY AGE
replicaset.apps/uc6dmltrain1-uc6dmltrain1541-xtyata0ycb-deployment-bd7d5964b 0 0 0 45h
```
#### PQoS (TODO)
```sh
nxw@5g-iana-manager:~$ kc get all -n e2c74d7f-5de3-47ae-a450-5af3745ba0dc
NAME READY STATUS RESTARTS AGE
pod/uc6pqos0-uc6pqos1841-chdrqcj0rm-deployment-7546f9455f-qmfx6 0/1 ContainerCreating 0 21s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6pqos0-uc6pqos1841-chdrqcj0rm-service NodePort 10.152.183.18 <none> 5000:32119/TCP 21s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6pqos0-uc6pqos1841-chdrqcj0rm-deployment 0/1 1 0 21s
NAME DESIRED CURRENT READY AGE
replicaset.apps/uc6pqos0-uc6pqos1841-chdrqcj0rm-deployment-7546f9455f 1 1 0 21s
```
#### NmSender
```sh
nxw@5g-iana-manager:~$ kc get all -n 1918b480-5b0c-4cf5-aff6-f60d06622251
NAME READY STATUS RESTARTS AGE
pod/uc6nmsender-uc6nmsen21831-3ebfpbnq83-deployment-64884c7f769pc8s 1/1 Running 0 56m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6nmsender-uc6nmsen21831-3ebfpbnq83-service NodePort 10.152.183.30 <none> 1935:31023/TCP 56m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6nmsender-uc6nmsen21831-3ebfpbnq83-deployment 1/1 1 1 56m
NAME DESIRED CURRENT READY AGE
replicaset.apps/uc6nmsender-uc6nmsen21831-3ebfpbnq83-deployment-64884c7f76 1 1 1 56m
```
#### NmCli on uulm-obu0 / uulm-obu1
##### uulm-obu0
```sh
nxw@5g-iana-manager:~$ kc get all -n a0633f4a-98bb-4dc3-9aff-c0c8b75ccf33
NAME READY STATUS RESTARTS AGE
pod/uc6nmcli0-uc6nmclient1771-1gyqqtau43-deployment-6d6fc795dfsbvgj 1/1 Running 0 16m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6nmcli0-uc6nmclient1771-1gyqqtau43-service NodePort 10.152.183.164 <none> 8000:30637/TCP 16m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6nmcli0-uc6nmclient1771-1gyqqtau43-deployment 1/1 1 1 16m
NAME DESIRED CURRENT READY AGE
replicaset.apps/uc6nmcli0-uc6nmclient1771-1gyqqtau43-deployment-6d6fc795df 1 1 1 16m
```
Curl to start this: `curl -X GET -H "Content-Type: application/json" -d "{\"endpoint_ip\": [\"http://192.168.100.4:31418/upload\", \"https://webhook.site/fbf62890-8c93-426c-bb19-461d8e11ff8c\"], \"ping_ip\": \"192.168.100.4\", \"stream_url\": \"rtmp://192.168.100.4:31023/live/test\"}" http://192.168.100.4:30637/demo/start`
##### uulm-obu1
```sh
nxw@5g-iana-manager:~$ kc get all -n fa862b87-1d25-4357-bc29-ffcc4aa67907
NAME READY STATUS RESTARTS AGE
pod/uc6nmcli1-uc6nmclient1771-k9rifya6n3-deployment-84f59b85ccvrn8q 1/1 Running 0 14m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/uc6nmcli1-uc6nmclient1771-k9rifya6n3-service NodePort 10.152.183.237 <none> 8000:32334/TCP 14m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/uc6nmcli1-uc6nmclient1771-k9rifya6n3-deployment 1/1 1 1 14m
NAME DESIRED CURRENT READY AGE
replicaset.apps/uc6nmcli1-uc6nmclient1771-k9rifya6n3-deployment-84f59b85cc 1 1 1 14m
```
Curl only from obu or mec
Curl to start this: `curl -X GET -H "Content-Type: application/json" -d "{\"endpoint_ip\": [\"http://192.168.100.4:32174/upload\", \"https://webhook.site/fbf62890-8c93-426c-bb19-461d8e11ff8c\"], \"ping_ip\": \"192.168.100.4\", \"stream_url\": \"rtmp://192.168.100.4:31023/live/test\"}" http://192.168.100.4:32334/demo/start`
```json
{
"endpoint_ip": [
"http://192.168.100.4:30956/upload",
"http://192.168.100.4:32119/accept_data"
],
"ping_ip": "192.168.100.4",
"stream_url": "rtmp://192.168.100.4:30888/live/test"
}
```
## Scripts
From uulm-obu0, uulm-obu1 or 5g-iana-mec.
Get list of repos:
`curl -u 5g-iana:5g-iana -k https://192.168.100.2:5000/v2/_catalog`
Get tags for a repo:
`curl -u 5g-iana:5g-iana -k https://192.168.100.2:5000/v2/uulm/training_agent/tags/list`
## Issues
### uulm-obu0 offline
uulm-obu0 is offline and has been offline for a while.
### Logs
Can't access logs from kubernetes on the uulm-obu1
```sh
Error from server: Get "https://172.16.1.11:10250/containerLogs/26f69d2b-bf62-44a6-9346-421d34d376d8/uc6dml9-01-uc6dmltrain541-ccdgt3sqq8-deployment-6896f68c87cgq4r/uc6dml9-01-uc6dmltrain541-ccdgt3sqq8": dial tcp 172.16.1.11:10250: i/o timeout
```
Troubleshooting steps would be
> I get "i/o timeouts" when calling "microk8s kubectl logs"
>
> Make sure your hostname resolves correctly to the IP address of your host or localhost. The following error may indicate this misconfiguration:
>
> microk8s kubectl logs
> Error from server: Get "<https://hostname:10250/containerLogs/default/>...": dial tcp host-IP:10250: i/o timeout
>
> One way to address this issue is to add the hostname and IP details of the host in /etc/hosts. In the case of a multi-node cluster, the /etc/hosts on each machine has to be updated with the details of all cluster nodes.
But doesn't work
## todo
- Agg node testing - Giorgos will test on platform
- network monitoring figure out why not our obus are working
- check arm deployment of nmclient
- deploy nmclient tool on uc1 obus
- confirm pqos deployment on obu on our obus
## Data Collection for NEtwork Monitoring
```sh
curl -X GET -d "{\"id\": 1}" 192.168.200.11:32684/data_collection/get_data_stats
curl -X GET -H "Content-Type: application/json" -d "{\"endpoint_ip\": [\"https://webhook.site/85e1c94b-6642-45e2-abd4-59ef11450c2b\"], \"ping_ip\": \"192.168.100.4\", \"stream_url\": \"rtmp://192.168.100.4:31023/live/test\"}" http://192.168.200.11:32684/demo/start
```
## PQoS Final Event
### Component
#### General
- Name: uc6pqos-fe
- Architecture: x86
- Elasticity controller: HORIZONTAL
#### Distribution Parameters
- Docker Image: uc6pqos:v1.3.0
- Docker Credentials: <STANDARD>
#### Minimum Execution Requirements
- vCPUs: 1
- RAM (MB): 512
- Storage (GB): 10
- Hypervisor Type: ESXI
#### Health Check
- HTTP: [http://localhost:5000](http://localhost:5000)
- Time Interval (in seconds): 10
#### Environment variables
# For testing
- ENDPOINT: [https://webhook.site/fc612cc3-48a1-418f-8e97-e6b691285892](https://webhook.site/fc612cc3-48a1-418f-8e97-e6b691285892)
#### Exposed Interfaces
- uc6pqosfe5000: 5000
### Application
- Name: PQoS-finalevent
- Component: uc6pqos-fe
### Instance
- Name: PQoS-finalevent
- Selector Provider: NEXTWORKS-OSS
- Application: PQoS-finalevent
- Selector node: uulm-obu0 # For Testing
- Ingress:
- Minimum Bandwidth: eMBB
- Radio Service Types: 1
- Maximum Bandwidth: 10

16
obu-node/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM python:3.11 AS compile-image
WORKDIR /federated-example
COPY requirements.txt .
RUN python3 -m pip install --upgrade pip
RUN python3 -m venv /venv
RUN . /venv/bin/activate && \
python3 -m ensurepip --upgrade && \
python3 -m pip install -r /federated-example/requirements.txt
FROM python:3.11 AS run-image
COPY --from=compile-image /venv /venv
WORKDIR /federated-example/src
COPY . /federated-example/
CMD . /venv/bin/activate && python3 client.py $SERVER_IP_FLWR $PARAMETER_IP:5000 $SERVER_IP_AGG $CLIENT_ID

16
obu-node/README.md Normal file
View File

@@ -0,0 +1,16 @@
# OBU node
This is the version matching the final requirements where the client are started from the policy executor
## Running the code using Docker
1. To create the Docker Image, run "Dockerfile" using this command: `docker build -f Dockerfile -t client-image .`
2. Create a container from the above image using this command: `docker run -p 8080:8080 -p 5000:5000 -p 80:80 -e SERVER_IP_FLWR={server_ip_port_flwr} -e PARAMETER_IP=1 -e SERVER_IP_AGG={server_ip_port_agg} -e CLIENT_ID={client_id} --name client --rm client-image` (More notes below)
3. The script for the clients will run automatically. The clients assume the server is ready to accept the connection (which is the scenario to expect given no error happens on the server side), otherwise the clients will fail to establish the connection and stop the execution.
* **Notes**:
- `{server_ip_port_flwr}`is the IP address and port number used for the flower framework (port 8080 in tests) and `{server_ip_port_agg}` are the ip address and port used to communicated with the DMLO (port 5000 in tests), they should both be of the form `192.168.0.1:5000`.
- `{client_id}` is the ID to assign the specific client (each client should have a unique ID)
- The `-p` flag is used to map the docker ports to the devices ports and should be changed according to the ports used in the simulation (currently set to ports 8080 and 5000).
- The `-e` flag is used to set the variables used to run the script automatically.
- The execution can be stopped by opening another terminal and using this command `docker kill client`.

View File

@@ -0,0 +1,7 @@
[registry."192.168.100.2:5000"]
http = true
insecure = true
ca = ["certs/192.168.100.2:5000/ca.crt"]
[[registry."192.168.100.2:5000".keypair]]
key = "certs/192.168.100.2:5000/client.key"
cert = "certs/192.168.100.2:5000/client.cert"

View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker buildx create --name iana --platform linux/amd64,linux/arm64 --bootstrap --config ./buildkitd.toml --use

18
obu-node/buildx/setup.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Nokia
#IANA_REGISTRY=192.168.100.2:5000
# TS
IANA_REGISTRY=192.168.100.2:5000
mkdir -p certs/"$IANA_REGISTRY"
(
cd certs/"$IANA_REGISTRY" || exit 1
openssl s_client -showcerts -connect "$IANA_REGISTRY" </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >ca.crt
openssl genrsa -out client.key 4096
openssl req -new -x509 -text -key client.key -out client.cert \
-subj "/C=DE/ST=Northrhine Westphalia/L=Essen/O=University Duisburg-Essen/emailAddress=tuan-dat.tran@stud.uni-due.de"
)

12
obu-node/docker-push.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
DOCKERFILE="./Dockerfile"
REGISTRY=192.168.100.2:5000/uulm
REMOTE_IMAGE="training_agent"
TAG=v1.3.0
docker buildx build --platform linux/amd64,linux/arm64 -f $DOCKERFILE -t \
$REGISTRY/$REMOTE_IMAGE:$TAG . --push

62
obu-node/requirements.txt Normal file
View File

@@ -0,0 +1,62 @@
absl-py==2.0.0
astunparse==1.6.3
blinker==1.7.0
cachetools==5.3.2
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
cryptography==41.0.5
Flask==3.0.0
flatbuffers==23.5.26
flwr==1.5.0
gast==0.5.4
google-auth==2.23.4
google-auth-oauthlib==1.0.0
google-pasta==0.2.0
grpcio==1.59.2
h5py==3.10.0
idna==3.4
iterators==0.0.2
itsdangerous==2.1.2
Jinja2==3.1.2
joblib==1.3.2
keras==2.14.0
libclang==16.0.6
Markdown==3.5.1
MarkupSafe==2.1.3
ml-dtypes==0.2.0
netifaces==0.11.0
numpy==1.26.1
oauthlib==3.2.2
opt-einsum==3.3.0
packaging==23.2
pandas==2.1.2
protobuf==3.20.3
psutil==5.9.6
pyasn1==0.5.0
pyasn1-modules==0.3.0
pycparser==2.21
pycryptodome==3.19.0
Pympler==1.0.1
python-dateutil==2.8.2
pytz==2023.3.post1
requests==2.31.0
requests-oauthlib==1.3.1
rsa==4.9
scikit-learn==1.3.2
scipy==1.11.3
six==1.16.0
tensorboard==2.14.1
tensorboard-data-server==0.7.2
tensorflow==2.14.0
tensorflow-estimator==2.14.0
tensorflow-io-gcs-filesystem==0.34.0
termcolor==2.3.0
threadpoolctl==3.2.0
typing_extensions==4.8.0
tzdata==2023.3
urllib3==2.0.7
watchdog==3.0.0
Werkzeug==3.0.1
wrapt==1.14.1

View File

Binary file not shown.

File diff suppressed because it is too large Load Diff

0
obu-node/src/.gitkeep Normal file
View File

8
obu-node/src/changedb.py Normal file
View File

@@ -0,0 +1,8 @@
import pandas as pd
#Script to change the used database to simulate having a new database in the final version. The new database is the old one minus 50 elements
df = pd.read_csv('C:/Users/Firas/Desktop/docker/data/train_c1.csv')
r=len(df)-50
sampled = df.sample(n=r)
sampled.to_csv('C:/Users/Firas/Desktop/docker/data/train_c1.csv', index=False)
print(f"Sampled {r} lines and updated it as a new database")

View File

@@ -0,0 +1,31 @@
import requests
import sys
from time import sleep
import subprocess
def check_connection(ip):
try:
response = requests.post(f"http://{ip}/check_connection")
if response.status_code == 200:
print(f"Connetion established with {ip}. The script will run in 15 seconds.")
sleep(15)
execute_python_file(main_script, *new_args)
except:
sleep(5)
check_connection(ip)
def execute_python_file(main_script, *args):
cmd = ['python', main_script] + list(args)
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
print(f"Error running the script: {e}")
if __name__ == "__main__":
ip = sys.argv[1] #ip with port to check, for the clients, check the DMLO
main_script = sys.argv[2]
new_args = sys.argv[3:]
check_connection(ip)

356
obu-node/src/client.py Normal file
View File

@@ -0,0 +1,356 @@
import argparse
import os
from pathlib import Path
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
import numpy as np
import tensorflow as tf
from tensorflow import keras
import sys
import flwr as fl
import json
import requests
from flwr.common import Scalar, Config
from time import sleep
from typing import Dict, Union
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from flask import Flask, request
import threading
from time import time_ns
# Make TensorFlow logs less verbose
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
app = Flask(__name__)
@app.route("/upload", methods=["POST"])
def upload():
global new_data, database_changed
data = request.data
data = data.decode("utf-8")
formatted_lines = []
for line in data.strip().split("\n"):
elements = line.split(",")
formatted_line = f"{elements[1]}, {elements[2]}, {elements[4].split()[0]}"
formatted_lines.append(formatted_line)
new_data = "\n".join(formatted_lines)
new_data = pd.DataFrame(
[line.split(",") for line in new_data.strip().split("\n")],
columns=["lat", "lon", "rtt"],
)
database_changed = True
return "Received new datapoints from the network monitoring tool", 200
def run_flask():
app.run(host="0.0.0.0", port=80)
flask_thread = threading.Thread(target=run_flask)
flask_thread.setDaemon(True)
flask_thread.start()
"""
gpu_id = 0 # Index of the GPU you want to use
physical_devices = tf.config.list_physical_devices('GPU')
print(physical_devices)
tf.config.set_visible_devices(physical_devices[gpu_id], 'GPU')
tf.config.experimental.set_memory_growth(physical_devices[gpu_id], True)
"""
client_id = sys.argv[4]
server_ip = sys.argv[1]
dmlo_ip = sys.argv[2]
server_ip_kpi = sys.argv[3]
q_alpha = 0.95
n_features = 3
n_future = 1
n_past = 400
learning_rate_argv = 0.001
database_changed = False
rounds_involved, uc6_02_start_obu = (
0,
0,
) # Simple workaround to help measure the model upload time
data_df = pd.read_csv("../resources/train_c1.csv")
datapoints = len(data_df)
def reload_data(data_df): # untested change (db01)
"""Reloading the dataset after detecting a change"""
print("Database is being processed")
# data_df = pd.read_csv("data/train_c1.csv") #db01
train_df, test_df = np.split(data_df, [int(0.70 * len(data_df))])
# Scaling the dataframe
train = train_df
scalers = {}
# Scaling train data
for i in train_df.columns:
scaler = MinMaxScaler(feature_range=(-1, 1))
s_s = scaler.fit_transform(train[i].values.reshape(-1, 1))
s_s = np.reshape(s_s, len(s_s))
scalers["scaler_" + i] = scaler
train[i] = s_s
# Scaling test data
test = test_df
for i in train_df.columns:
scaler = scalers["scaler_" + i]
s_s = scaler.transform(test[i].values.reshape(-1, 1))
s_s = np.reshape(s_s, len(s_s))
scalers["scaler_" + i] = scaler
test[i] = s_s
def split_series(series, n_past, n_future):
X, y = list(), list()
# Loop to create array of every observations (past) and predictions (future) for every datapoint
for window_start in range(len(series)):
# Calculating boundaries for each datapoint
past_end = window_start + n_past
future_end = past_end + n_future
# Loop will end if the number of datapoints is less than observations (past)
if future_end > len(series):
break
past, future = (
series[window_start:past_end, :],
series[past_end:future_end, :],
)
X.append(past)
y.append(future)
return np.array(X), np.array(y)
# Creating X_train, y_train, X_test, y_test
X_train, y_train = split_series(train.values, n_past, n_future)
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], n_features))
y_train = y_train.reshape((y_train.shape[0], y_train.shape[1], n_features))
X_test, y_test = split_series(test.values, n_past, n_future)
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], n_features))
y_test = y_test.reshape((y_test.shape[0], y_test.shape[1], n_features))
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)
y_train = y_train[:, :, 2]
y_test = y_test[:, :, 2]
global database_changed
database_changed = False
return X_train, y_train, X_test, y_test, train_df, scalers
class QuantileMetric(tf.keras.metrics.Metric):
def __init__(self, name="quantile_metric", **kwargs):
super(QuantileMetric, self).__init__(name=name, **kwargs)
self.quantile_metric = self.add_weight(
name="quantile_metric", initializer="zeros"
)
self.quantile_metric_count = self.add_weight(
name="quantile_metric_count", initializer="zeros"
)
def update_state(self, y_true, y_pred, sample_weight=None):
quantileCondition = tf.math.greater(y_true, tf.squeeze(y_pred))
qc = tf.math.reduce_sum(tf.cast(quantileCondition, tf.float32))
self.quantile_metric.assign_add(qc)
self.quantile_metric_count.assign_add(
tf.cast(tf.size(quantileCondition), tf.float32)
)
def result(self):
return self.quantile_metric / self.quantile_metric_count
def reset_state(self):
self.quantile_metric.assign(0.0)
self.quantile_metric_count.assign(0)
def tilted_loss(y_true, y_pred):
q = q_alpha
e = y_true - y_pred
tl = tf.stack([q * e, (q - 1) * e])
e_max = tf.math.reduce_max(tl, axis=0, keepdims=True)
return tf.reduce_mean(e_max)
class LSTMClient(fl.client.NumPyClient):
def __init__(self, best_model, X_train, y_train, X_test, y_test, train_df, scalers):
self.best_model = best_model
self.X_train, self.y_train = X_train, y_train
self.X_test, self.y_test = X_test, y_test
self.train_df = train_df
self.scalers = scalers
self.properties = {"client_id": client_id}
def get_properties(self, config: Config) -> Dict[str, Scalar]:
return self.properties
def get_parameters(self, config):
"""Get parameters of the local model."""
return self.best_model.get_weights()
def fit(self, parameters, config):
"""Train parameters on the locally held training set."""
uc6_01_end = time_ns() # Time required to download the global model from the agg.node in secs (Target <2s) has another part on the agg.node side
global uc6_02_start_obu, rounds_involved
rounds_involved += 1
uc6_02_end = time_ns() # Time required to upload the model (has another part on the agg.node side, in sec * 1000000000) (Target < 2s)
if rounds_involved > 1:
kpi_uc6_02 = uc6_02_end - uc6_02_start_obu
try:
response = requests.post(
f"http://{server_ip_kpi}/upload_kpi02", json={f"kpi02": kpi_uc6_02}
)
if response.status_code != 200:
print(f"Failed to send KPI_02. Status code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Error while sending KPI_02: {e}")
try:
response = requests.post(
f"http://{server_ip_kpi}/upload_kpi01", json={f"kpi01": uc6_01_end}
)
if response.status_code != 200:
print(f"Failed to send KPI_01. Status code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Error while sending KPI_01: {e}")
if database_changed == True:
try:
(
client.X_train,
client.y_train,
client.X_test,
client.y_test,
client.train_df,
client.scalers,
) = reload_data(new_data)
except Exception as e:
print(f"Error with the new data: {e}")
uc6_05_start = time_ns()
# Update local model parameters
self.best_model.set_weights(parameters)
# Get hyperparameters for this round
batch_size: int = config["batch_size"]
epochs: int = config["local_epochs"]
# Train the model using hyperparameters from config
history = self.best_model.fit(
self.X_train, self.y_train, batch_size, epochs, validation_split=0.1
)
# Return updated model parameters and results
parameters_prime = self.best_model.get_weights()
num_examples_train = len(self.X_train)
results = {
"id": client_id,
"loss": history.history["loss"][0],
"accuracy": history.history["mean_absolute_error"][0],
"val_loss": history.history["val_loss"][0],
"val_accuracy": history.history["val_mean_absolute_error"][0],
}
uc6_05_end = time_ns()
global kpi_uc6_05
kpi_uc6_05 = (
(uc6_05_end - uc6_05_start) / 1000000000
) # Time required to finish a training round (inkl. all local epochs) on the OBU side in sec (target <240s)
try:
response = requests.post(
f"http://{server_ip_kpi}/upload_kpi05", json={f"kpi05": kpi_uc6_05}
)
if response.status_code != 200:
print(f"Failed to send KPI_05. Status code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Error while sending KPI_05: {e}")
uc6_02_start_obu = time_ns()
return parameters_prime, num_examples_train, results
def evaluate(self, parameters, config):
"""Evaluate parameters on the locally held test set."""
# Update local model with global parameters
self.best_model.set_weights(parameters)
# Evaluate global model parameters on the local test data and return results
loss, metric, error = self.best_model.evaluate(self.X_test, self.y_test, 32)
num_examples_test = len(self.X_test)
pred = self.best_model.predict(self.X_test)
pred_copies = np.repeat(pred, 3, axis=-1)
pred_copies = np.expand_dims(pred_copies, axis=1)
for index, i in enumerate(self.train_df.columns):
scaler = self.scalers["scaler_" + i]
pred_copies[:, :, index] = scaler.inverse_transform(
pred_copies[:, :, index]
)
np.save("prediction_client1.npy", pred_copies[:, :, 2])
return loss, num_examples_test, {"accuracy": error}
def main() -> None:
uc6_04_start = time_ns()
X_train, y_train, X_test, y_test, train_df, scalers = reload_data(data_df)
uc6_04_end = time_ns()
global kpi_uc6_04
kpi_uc6_04 = (
uc6_04_end - uc6_04_start
) / 1000000000 # Time required to process training data by OBU in sec (Target <60s)
try:
response = requests.post(
f"http://{server_ip_kpi}/upload_kpi04", json={f"kpi04": kpi_uc6_04}
)
if response.status_code != 200:
print(f"Failed to send KPI_04. Status code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Error while sending KPI_04: {e}")
best_model = tf.keras.models.load_model(
"../resources/best_model_no_tuner_40.h5", compile=False
)
opt = tf.keras.optimizers.Adam(learning_rate=learning_rate_argv)
best_model.compile(
optimizer=opt,
loss=[tilted_loss],
metrics=[QuantileMetric(), keras.metrics.MeanAbsoluteError()],
)
global client
client = LSTMClient(best_model, X_train, y_train, X_test, y_test, train_df, scalers)
for i in range(40):
try:
response = requests.post(f"http://{server_ip_kpi}/check_connection")
if response.status_code == 200:
sleep(5)
break
except:
print(
"\n\n\n\nConnection to the Agg.Node could not be established, trying again in 5 seconds...\n",
flush=True,
)
sleep(5)
fl.client.start_numpy_client(
server_address=server_ip,
client=client,
)
if __name__ == "__main__":
main()

16
pqos/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM python:3.11 AS compile-image
WORKDIR /federated-example
COPY requirements.txt .
RUN python3 -m pip install --upgrade pip
RUN python3 -m venv /venv
RUN . /venv/bin/activate && \
python3 -m ensurepip --upgrade && \
python3 -m pip install -r /federated-example/requirements.txt
FROM python:3.11 AS run-image
COPY --from=compile-image /venv /venv
WORKDIR /federated-example
COPY . /federated-example/
CMD . /venv/bin/activate && python pqos.py $ENDPOINT

View File

View File

@@ -0,0 +1,29 @@
import requests
import pandas as pd
import pickle
from time import sleep
# This part of the code is to be manually edited:
#
# url='http://IP_OF_THE_PQoS:PORT_5000_INTERNALLY/accept_data'
url = 'http://192.168.2.213:5000/accept_data' # url to send the request to
total_sets = 10 # The total number of sets of 100s to send to the PQoS
#
# End of the part to manually edit
def send_dataset(start, end, sets):
try:
dataset = pd.read_csv("test.csv")
elements = dataset[start-1:end]
to_send = pickle.dumps(elements)
requests.post(url, data= to_send)
sets += 1
print("Dataset sent to PQoS")
if (end < len(dataset)) and (sets != total_sets):
sleep(5)
send_dataset(start + 100, end + 100, sets)
except requests.exceptions.RequestException as e:
print(f"Error while sending data to PQoS: {e}")
sets = 0
send_dataset(1, 100, sets)

2404
pqos/Example files/test.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
lat,long,rtt
48.4339443,9.967161733333334,78900000
48.43394475,9.9671683,78900000
48.43393966666667,9.9671245,51800000
48.4339408,9.96712915,51800000
48.43393145,9.96710145,77200000
48.43391836666667,9.967084033333334,82100000
48.4339061,9.9670742,17200000
48.43389823333333,9.9670709,17200000
48.4339024,9.96707235,17200000
48.4338805,9.9670667,21400000
48.4338755,9.96706675,21400000
48.43385146666666,9.9670702,41500000
48.433824900000005,9.96708105,92800000
48.43379503333333,9.967094366666666,75400000
48.43376073333334,9.967111333333332,172000000
48.4337206,9.9671297,119000000
48.43372695,9.9671269,119000000
48.43368280000001,9.967145766666668,28000000
48.43364845,9.9671582,18000000
48.43364193333334,9.967160833333333,18000000
48.4336012,9.967175066666666,50600000
48.433549825,9.96718925,94500000
48.43355746666666,9.967187366666668,94500000
48.43349716666666,9.9672049,38000000
48.43345243333332,9.967218666666668,21900000
48.4334024,9.96723545,26100000
48.433353966666665,9.967254733333334,34100000
48.43331236666666,9.967272533333334,18400000
48.43327283333334,9.967288466666666,22700000
48.433266875,9.967290825,22700000
48.433233400000006,9.96730325,28600000
48.433228533333335,9.967305133333332,28600000
48.4332024,9.96731445,26800000
48.43319576666666,9.967316466666666,26800000
48.4331661,9.96732555,20200000
48.433161000000005,9.967327233333334,20200000
48.43313016666667,9.967335133333334,22100000
48.4331365,9.96733405,22100000
48.43310035,9.967341,34500000
48.4330944,9.9673422,34500000
48.433064,9.9673496,28400000
48.433057600000005,9.967351266666666,28400000
48.4330266,9.9673621,26500000
48.43301893333333,9.967365466666664,26500000
48.43298155,9.9673846,30600000
48.43297520000001,9.967387166666668,30600000
48.4329455,9.9674008,30500000
48.43293645,9.96740435,30500000
48.43290655,9.96741765,16700000
48.432897966666665,9.967420833333334,16700000
48.4328529,9.967438200000002,27200000
48.432813566666674,9.967453433333334,37000000
48.4327773,9.967465,31800000
48.4327722,9.967466675,31800000
48.43274195,9.9674775,37400000
48.43273713333334,9.967479633333332,37400000
48.43270859999999,9.9674912,33800000
48.43267659999999,9.967503575,52400000
48.4326811,9.967501733333334,52400000
48.4326455,9.967515566666666,42300000
48.43264985,9.96751385,42300000
48.432623750000005,9.96752325,82800000
48.43261966666668,9.967524666666666,82800000
48.43259766666666,9.967532233333332,75400000
48.432594775,9.967533275,75400000
48.43257795,9.9675397,37800000
48.43257533333334,9.967540666666666,37800000
48.43255946666667,9.967545433333337,36000000
48.432543900000006,9.967550766666667,32400000
48.4325268,9.96755595,20900000
48.4325292,9.967555333333332,20900000
48.43251245,9.96755925,29300000
48.43250983333333,9.967559766666668,29300000
48.4324989,9.96756175,25500000
48.432496833333325,9.9675621,25500000
48.4324861,9.9675637,39500000
48.43248413333333,9.967563833333331,39500000
48.4324748,9.9675651,27200000
48.43247515,9.9675647,27200000
48.4324734,9.967566266666667,33600000
48.4324737,9.9675665,19700000
48.4324737,9.9675665,22500000
48.4324737,9.9675665,22500000
48.43247363333334,9.9675666,37100000
48.43247362500001,9.96756665,37100000
48.4324736,9.9675668,35500000
48.4324736,9.9675668,26700000
48.4324736,9.9675668,26700000
48.4324736,9.9675668,28400000
48.4324736,9.9675668,43600000
48.432473,9.967566,26200000
48.432471825,9.9675648,26200000
48.43247246666667,9.967565366666667,26200000
48.43246805,9.96756225,32000000
48.43246783333333,9.967562166666667,32000000
48.4324674,9.967562,28000000
48.4324674,9.967562,28000000
48.4324674,9.967562,19000000
48.4324674,9.967562,19000000
48.4324674,9.967562,26000000
1 lat long rtt
2 48.4339443 9.967161733333334 78900000
3 48.43394475 9.9671683 78900000
4 48.43393966666667 9.9671245 51800000
5 48.4339408 9.96712915 51800000
6 48.43393145 9.96710145 77200000
7 48.43391836666667 9.967084033333334 82100000
8 48.4339061 9.9670742 17200000
9 48.43389823333333 9.9670709 17200000
10 48.4339024 9.96707235 17200000
11 48.4338805 9.9670667 21400000
12 48.4338755 9.96706675 21400000
13 48.43385146666666 9.9670702 41500000
14 48.433824900000005 9.96708105 92800000
15 48.43379503333333 9.967094366666666 75400000
16 48.43376073333334 9.967111333333332 172000000
17 48.4337206 9.9671297 119000000
18 48.43372695 9.9671269 119000000
19 48.43368280000001 9.967145766666668 28000000
20 48.43364845 9.9671582 18000000
21 48.43364193333334 9.967160833333333 18000000
22 48.4336012 9.967175066666666 50600000
23 48.433549825 9.96718925 94500000
24 48.43355746666666 9.967187366666668 94500000
25 48.43349716666666 9.9672049 38000000
26 48.43345243333332 9.967218666666668 21900000
27 48.4334024 9.96723545 26100000
28 48.433353966666665 9.967254733333334 34100000
29 48.43331236666666 9.967272533333334 18400000
30 48.43327283333334 9.967288466666666 22700000
31 48.433266875 9.967290825 22700000
32 48.433233400000006 9.96730325 28600000
33 48.433228533333335 9.967305133333332 28600000
34 48.4332024 9.96731445 26800000
35 48.43319576666666 9.967316466666666 26800000
36 48.4331661 9.96732555 20200000
37 48.433161000000005 9.967327233333334 20200000
38 48.43313016666667 9.967335133333334 22100000
39 48.4331365 9.96733405 22100000
40 48.43310035 9.967341 34500000
41 48.4330944 9.9673422 34500000
42 48.433064 9.9673496 28400000
43 48.433057600000005 9.967351266666666 28400000
44 48.4330266 9.9673621 26500000
45 48.43301893333333 9.967365466666664 26500000
46 48.43298155 9.9673846 30600000
47 48.43297520000001 9.967387166666668 30600000
48 48.4329455 9.9674008 30500000
49 48.43293645 9.96740435 30500000
50 48.43290655 9.96741765 16700000
51 48.432897966666665 9.967420833333334 16700000
52 48.4328529 9.967438200000002 27200000
53 48.432813566666674 9.967453433333334 37000000
54 48.4327773 9.967465 31800000
55 48.4327722 9.967466675 31800000
56 48.43274195 9.9674775 37400000
57 48.43273713333334 9.967479633333332 37400000
58 48.43270859999999 9.9674912 33800000
59 48.43267659999999 9.967503575 52400000
60 48.4326811 9.967501733333334 52400000
61 48.4326455 9.967515566666666 42300000
62 48.43264985 9.96751385 42300000
63 48.432623750000005 9.96752325 82800000
64 48.43261966666668 9.967524666666666 82800000
65 48.43259766666666 9.967532233333332 75400000
66 48.432594775 9.967533275 75400000
67 48.43257795 9.9675397 37800000
68 48.43257533333334 9.967540666666666 37800000
69 48.43255946666667 9.967545433333337 36000000
70 48.432543900000006 9.967550766666667 32400000
71 48.4325268 9.96755595 20900000
72 48.4325292 9.967555333333332 20900000
73 48.43251245 9.96755925 29300000
74 48.43250983333333 9.967559766666668 29300000
75 48.4324989 9.96756175 25500000
76 48.432496833333325 9.9675621 25500000
77 48.4324861 9.9675637 39500000
78 48.43248413333333 9.967563833333331 39500000
79 48.4324748 9.9675651 27200000
80 48.43247515 9.9675647 27200000
81 48.4324734 9.967566266666667 33600000
82 48.4324737 9.9675665 19700000
83 48.4324737 9.9675665 22500000
84 48.4324737 9.9675665 22500000
85 48.43247363333334 9.9675666 37100000
86 48.43247362500001 9.96756665 37100000
87 48.4324736 9.9675668 35500000
88 48.4324736 9.9675668 26700000
89 48.4324736 9.9675668 26700000
90 48.4324736 9.9675668 28400000
91 48.4324736 9.9675668 43600000
92 48.432473 9.967566 26200000
93 48.432471825 9.9675648 26200000
94 48.43247246666667 9.967565366666667 26200000
95 48.43246805 9.96756225 32000000
96 48.43246783333333 9.967562166666667 32000000
97 48.4324674 9.967562 28000000
98 48.4324674 9.967562 28000000
99 48.4324674 9.967562 19000000
100 48.4324674 9.967562 19000000
101 48.4324674 9.967562 26000000

22
pqos/README.md Normal file
View File

@@ -0,0 +1,22 @@
# PQOS
`This branch uses the updated model from Oct 2024 (created from the 'deploy' branch)`
The PQOS expects 100 datapoints as input and returns 5 predicted values as a json file:
{
"Predicitons": str_of_5_predictions,
"Response time": int (in Seconds)
}
## Running the code using Docker
1. To create the Docker Image, run "Dockerfile" using this command: `docker build -f Dockerfile -t pqos-deploy-image .`
2. Create a container from the above image using this command: `docker run -p 5000:5000 --name pqos -e ENDPOINT=IP:PORT --rm pqos-deploy-image` where ENDPOINT is the ip address and the port (e.g. 192.168.0.1:5000) to which the results will be sent as a json file expecting the function "/upload_predictions"
3. The script for the PQOS will run automatically, and will await the receival of a dataset from a sender. (See additional notes below)
4. (For testing purposes without an endpoint) In another terminal, enter the command `python3 -m http.server {port_number}` to simulate an endpoint receiving the predictions. This will show a 501 server error given it does not have a backend implementation of an endpoint.
* **Notes**:
- The `-p` flag is used to map the docker ports to the devices ports.
- The `-e` flag is used to enter command line variables.
- The execution can be stopped by opening another terminal and using this command `docker kill pqos`.
- The "Example files" directory contains a dummy dataset to send to the PQOS for testing purposes. In this case run the pqos_curl.py python script in that same directory. (The IP address to which the dataset should be sent is hardcoded there as it is for testing. The part to edit manually is marked there)

12
pqos/docker-push.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# docker tag SOURCE_IMAGE[:TAG] 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
# docker push 192.168.100.2:5000/uulm/<COMPONENT_NAME>:<VERSION>
TA_VERSION=v1.3.0
LOCAL_IMAGE="pqos"
REMOTE_IMAGE="uc6pqos"
docker build -t $LOCAL_IMAGE .
docker tag $LOCAL_IMAGE:latest 192.168.100.2:5000/uulm/$REMOTE_IMAGE:$TA_VERSION
docker push 192.168.100.2:5000/uulm/$REMOTE_IMAGE:$TA_VERSION

131
pqos/pqos.py Normal file
View File

@@ -0,0 +1,131 @@
from flask import Flask, request
import threading
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import tensorflow as tf
from time import time_ns
import pickle
import pandas as pd
import requests
import sys
# Part to be hardcoded for now, expected to be "ip:port"
destination = sys.argv[1]
app = Flask(__name__)
@app.route('/accept_data', methods=['POST'])
def accept_data():
data = request.data
data = data.decode("utf-8")
formatted_lines = []
for line in data.strip().split("\n"):
elements = line.split(",")
formatted_line = f"{elements[0]}, {elements[1]}, {elements[2].split()[0]}"
formatted_lines.append(formatted_line)
new_data = "\n".join(formatted_lines)
new_data = pd.DataFrame(
[line.split(",") for line in new_data.strip().split("\n")],
columns=["lat", "long", "rtt"],
)
new_data["lat"] = new_data["lat"].astype(float)
new_data["long"] = new_data["long"].astype(float)
new_data["rtt"] = new_data["rtt"].astype(int)
global df_final
df_final = new_data
dataset_received.set()
return "Received new datapoints from the network monitoring tool", 200
def run_flask():
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
def scale(data, min_val, max_val):
# lat_min_val = 0
# lat_max_val = 50
# lon_min_val = 0
# lon_max_val = 10
# rtt_min_val = 0
# rtt_max_val = 1000
range_max = 1
range_min = -1
return ((data - min_val) / (max_val - min_val)) * (range_max - range_min) + range_min
def reverse_scale(data, min_val, max_val):
range_min = -1
range_max = 1
return ((data - range_min) / (range_max - range_min)) * (max_val - min_val) + min_val
def main():
flask_thread = threading.Thread(target=run_flask)
flask_thread.setDaemon(True)
flask_thread.start()
pd.set_option('mode.chained_assignment', None)
tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
best_model = tf.keras.models.load_model("trained_rtt.h5", compile=False)
global q_alpha, n_future, n_past, dataset_received
dataset_received = threading.Event()
q_alpha = 0.95
n_features = 3
n_future= 5
n_past = 100
while True:
dataset_received.wait()
uc6_06_start = time_ns()
dataset_100 = df_final[0:100]
# dataset_compare_5 = df_final[100:105, 2]
scalers={}
dataset_100.loc[:,"lat"] = scale(dataset_100["lat"],0,50)
dataset_100.loc[:,"long"] = scale(dataset_100["long"],0,10)
dataset_100.loc[:,"rtt"] = scale(dataset_100["rtt"],0,1000)
# Scaling train data
for i in dataset_100.columns:
scaler = MinMaxScaler(feature_range=(-1,1))
s_s = scaler.fit_transform(dataset_100[i].values.reshape(-1,1))
s_s=np.reshape(s_s,len(s_s))
scalers['scaler_'+ i] = scaler
dataset_100[i]=s_s.copy()
X_test = np.array(dataset_100)
X_test = X_test.reshape((1, X_test.shape[0], n_features))
pred = best_model.predict(X_test)
pred = reverse_scale(pred,0,1000)
pred = np.ceil(pred)
dataset_compare_5 = df_final.iloc[100:105, 2]
# df_final['column'] = df_final['column'].astype(str)
# print(df_final)
# dataset_compare_5 = df_final["column"].iloc[100:106].str().split(',')[2].astype(float)
numpy_actual_values = (np.array(dataset_compare_5)/100000).astype(int)
# errors = np.sum(pred < numpy_actual_values)
uc6_06_end = time_ns()
kpi_uc6_06 = (uc6_06_end-uc6_06_start)/1000000000 # Time required by the PQoS to provide a response in sec (Target <0.2)
try:
response = requests.post(f"http://{destination}/upload_predictions", json={f"Predicitons": np.array2string(pred), "Response time": kpi_uc6_06})
except requests.exceptions.RequestException as e:
print(f"Error while sending the prediction results: {e}")
# Time required by the PQoS to provide a response in sec (Target <0.2)
#print(f"Predictions: \n{pred}")
#print (f"Time required to process the request: {kpi_uc6_06}s (Target <0.2s)\n\n")
dataset_received.clear()
if __name__ == "__main__":
main()

62
pqos/requirements.txt Normal file
View File

@@ -0,0 +1,62 @@
absl-py==2.0.0
astunparse==1.6.3
blinker==1.7.0
cachetools==5.3.2
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
cryptography==41.0.5
Flask==3.0.0
flatbuffers==23.5.26
flwr==1.5.0
gast==0.5.4
google-auth==2.23.4
google-auth-oauthlib==1.0.0
google-pasta==0.2.0
grpcio==1.59.2
h5py==3.10.0
idna==3.4
iterators==0.0.2
itsdangerous==2.1.2
Jinja2==3.1.2
joblib==1.3.2
keras==2.14.0
libclang==16.0.6
Markdown==3.5.1
MarkupSafe==2.1.3
ml-dtypes==0.2.0
netifaces==0.11.0
numpy==1.26.1
oauthlib==3.2.2
opt-einsum==3.3.0
packaging==23.2
pandas==2.1.2
protobuf==3.20.3
psutil==5.9.6
pyasn1==0.5.0
pyasn1-modules==0.3.0
pycparser==2.21
pycryptodome==3.19.0
Pympler==1.0.1
python-dateutil==2.8.2
pytz==2023.3.post1
requests==2.31.0
requests-oauthlib==1.3.1
rsa==4.9
scikit-learn==1.3.2
scipy==1.11.3
six==1.16.0
tensorboard==2.14.1
tensorboard-data-server==0.7.2
tensorflow==2.14.0
tensorflow-estimator==2.14.0
tensorflow-io-gcs-filesystem==0.34.0
termcolor==2.3.0
threadpoolctl==3.2.0
typing_extensions==4.8.0
tzdata==2023.3
urllib3==2.0.7
watchdog==3.0.0
Werkzeug==3.0.1
wrapt==1.14.1

BIN
pqos/trained_rtt.h5 Normal file

Binary file not shown.