Initial commit
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
This commit is contained in:
6
5g-uulm-network-monitoring/.gitignore
vendored
Normal file
6
5g-uulm-network-monitoring/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/target
|
||||||
|
/output/*
|
||||||
|
*.pcap
|
||||||
|
*.log
|
||||||
|
/resources/video/*
|
||||||
|
!/resources/video/.gitignore
|
||||||
79
5g-uulm-network-monitoring/.gitlab-ci.yml
Normal file
79
5g-uulm-network-monitoring/.gitlab-ci.yml
Normal 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
2374
5g-uulm-network-monitoring/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
5g-uulm-network-monitoring/Cargo.toml
Normal file
30
5g-uulm-network-monitoring/Cargo.toml
Normal 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 = []
|
||||||
22
5g-uulm-network-monitoring/Dockerfile
Normal file
22
5g-uulm-network-monitoring/Dockerfile
Normal 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" ]
|
||||||
105
5g-uulm-network-monitoring/README.md
Normal file
105
5g-uulm-network-monitoring/README.md
Normal 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).
|
||||||
107
5g-uulm-network-monitoring/README.md.old
Normal file
107
5g-uulm-network-monitoring/README.md.old
Normal 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).
|
||||||
57
5g-uulm-network-monitoring/README.org
Normal file
57
5g-uulm-network-monitoring/README.org
Normal 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]].
|
||||||
|
|
||||||
3
5g-uulm-network-monitoring/Rocket.toml
Normal file
3
5g-uulm-network-monitoring/Rocket.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[default]
|
||||||
|
address = "0.0.0.0"
|
||||||
|
limits = { form = "64 kB", json = "1 MiB" }
|
||||||
7
5g-uulm-network-monitoring/buildx/buildkitd.toml
Normal file
7
5g-uulm-network-monitoring/buildx/buildkitd.toml
Normal 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"
|
||||||
3
5g-uulm-network-monitoring/buildx/create_builder.sh
Executable file
3
5g-uulm-network-monitoring/buildx/create_builder.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker buildx create --name iana --platform linux/amd64,linux/arm64 --bootstrap --config ./buildkitd.toml --use
|
||||||
18
5g-uulm-network-monitoring/buildx/setup.sh
Executable file
18
5g-uulm-network-monitoring/buildx/setup.sh
Executable 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"
|
||||||
|
)
|
||||||
39
5g-uulm-network-monitoring/docker-compose.yml
Normal file
39
5g-uulm-network-monitoring/docker-compose.yml
Normal 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
|
||||||
11
5g-uulm-network-monitoring/docker-push-rtt.sh
Executable file
11
5g-uulm-network-monitoring/docker-push-rtt.sh
Executable 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
|
||||||
11
5g-uulm-network-monitoring/docker-push-throughput.sh
Executable file
11
5g-uulm-network-monitoring/docker-push-throughput.sh
Executable 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
|
||||||
24
5g-uulm-network-monitoring/docker-push.sh
Executable file
24
5g-uulm-network-monitoring/docker-push.sh
Executable 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
|
||||||
7
5g-uulm-network-monitoring/docker/nginx.Dockerfile
Normal file
7
5g-uulm-network-monitoring/docker/nginx.Dockerfile
Normal 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"]
|
||||||
22
5g-uulm-network-monitoring/docker/nmcli_default.Dockerfile
Normal file
22
5g-uulm-network-monitoring/docker/nmcli_default.Dockerfile
Normal 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" ]
|
||||||
22
5g-uulm-network-monitoring/docker/nmcli_rtt.Dockerfile
Normal file
22
5g-uulm-network-monitoring/docker/nmcli_rtt.Dockerfile
Normal 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" ]
|
||||||
@@ -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" ]
|
||||||
95
5g-uulm-network-monitoring/docs/measurement_setup.org
Normal file
95
5g-uulm-network-monitoring/docs/measurement_setup.org
Normal 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
|
||||||
18
5g-uulm-network-monitoring/local-docker-push.sh
Executable file
18
5g-uulm-network-monitoring/local-docker-push.sh
Executable 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
|
||||||
16
5g-uulm-network-monitoring/local/app.py
Normal file
16
5g-uulm-network-monitoring/local/app.py
Normal 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]))
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
57
5g-uulm-network-monitoring/maestro-compose.yml
Normal file
57
5g-uulm-network-monitoring/maestro-compose.yml
Normal 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
|
||||||
28
5g-uulm-network-monitoring/run.sh
Executable file
28
5g-uulm-network-monitoring/run.sh
Executable 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
|
||||||
1
5g-uulm-network-monitoring/rustfmt.toml
Normal file
1
5g-uulm-network-monitoring/rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
edition = "2018"
|
||||||
139
5g-uulm-network-monitoring/src/bps.rs
Normal file
139
5g-uulm-network-monitoring/src/bps.rs
Normal 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(())
|
||||||
|
}
|
||||||
30
5g-uulm-network-monitoring/src/cli.rs
Normal file
30
5g-uulm-network-monitoring/src/cli.rs
Normal 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>,
|
||||||
|
}
|
||||||
107
5g-uulm-network-monitoring/src/endpoints.rs
Normal file
107
5g-uulm-network-monitoring/src/endpoints.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
17
5g-uulm-network-monitoring/src/ffmpeg.rs
Normal file
17
5g-uulm-network-monitoring/src/ffmpeg.rs
Normal 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(())
|
||||||
|
}
|
||||||
96
5g-uulm-network-monitoring/src/gps.rs
Normal file
96
5g-uulm-network-monitoring/src/gps.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
793
5g-uulm-network-monitoring/src/main.rs
Normal file
793
5g-uulm-network-monitoring/src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
80
5g-uulm-network-monitoring/src/rttps.rs
Normal file
80
5g-uulm-network-monitoring/src/rttps.rs
Normal 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
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Just run `docker compose up -d`
|
||||||
|
|
||||||
|
If the aggregator container fails, retry the command or remove the healthcheck.
|
||||||
17
aggregator-node/Dockerfile
Normal file
17
aggregator-node/Dockerfile
Normal 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
|
||||||
0
aggregator-node/Example files/.gitkeep
Normal file
0
aggregator-node/Example files/.gitkeep
Normal file
3
aggregator-node/Example files/clients_list.json
Normal file
3
aggregator-node/Example files/clients_list.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"eligible_clients_ids" : ["1", "2"]
|
||||||
|
}
|
||||||
10
aggregator-node/Example files/config_server.json
Normal file
10
aggregator-node/Example files/config_server.json
Normal 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
25
aggregator-node/README.md
Normal 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
12
aggregator-node/docker-push.sh
Executable 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
|
||||||
151
aggregator-node/logs/2024-07-15.log
Normal file
151
aggregator-node/logs/2024-07-15.log
Normal 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 -
|
||||||
62
aggregator-node/requirements.txt
Normal file
62
aggregator-node/requirements.txt
Normal 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
|
||||||
0
aggregator-node/resources/.gitkeep
Normal file
0
aggregator-node/resources/.gitkeep
Normal file
BIN
aggregator-node/resources/best_model_no_tuner_40.h5
Normal file
BIN
aggregator-node/resources/best_model_no_tuner_40.h5
Normal file
Binary file not shown.
24028
aggregator-node/resources/data.csv
Normal file
24028
aggregator-node/resources/data.csv
Normal file
File diff suppressed because it is too large
Load Diff
2404
aggregator-node/resources/test.csv
Normal file
2404
aggregator-node/resources/test.csv
Normal file
File diff suppressed because it is too large
Load Diff
0
aggregator-node/src/.gitkeep
Normal file
0
aggregator-node/src/.gitkeep
Normal file
506
aggregator-node/src/server.py
Normal file
506
aggregator-node/src/server.py
Normal 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
163
compose.yaml
Normal 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
|
||||||
9
config/client_config.Dockerfile
Normal file
9
config/client_config.Dockerfile
Normal 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
|
||||||
9
config/server_config.Dockerfile
Normal file
9
config/server_config.Dockerfile
Normal 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
947
notes.md
Normal 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
16
obu-node/Dockerfile
Normal 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
16
obu-node/README.md
Normal 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`.
|
||||||
7
obu-node/buildx/buildkitd.toml
Normal file
7
obu-node/buildx/buildkitd.toml
Normal 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"
|
||||||
3
obu-node/buildx/create_builder.sh
Executable file
3
obu-node/buildx/create_builder.sh
Executable 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
18
obu-node/buildx/setup.sh
Executable 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
12
obu-node/docker-push.sh
Executable 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
62
obu-node/requirements.txt
Normal 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
|
||||||
0
obu-node/resources/.gitkeep
Normal file
0
obu-node/resources/.gitkeep
Normal file
BIN
obu-node/resources/best_model_no_tuner_40.h5
Normal file
BIN
obu-node/resources/best_model_no_tuner_40.h5
Normal file
Binary file not shown.
10813
obu-node/resources/train_c1.csv
Normal file
10813
obu-node/resources/train_c1.csv
Normal file
File diff suppressed because it is too large
Load Diff
0
obu-node/src/.gitkeep
Normal file
0
obu-node/src/.gitkeep
Normal file
8
obu-node/src/changedb.py
Normal file
8
obu-node/src/changedb.py
Normal 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")
|
||||||
31
obu-node/src/check_conn.py
Normal file
31
obu-node/src/check_conn.py
Normal 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
356
obu-node/src/client.py
Normal 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
16
pqos/Dockerfile
Normal 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
|
||||||
0
pqos/Example files/.gitkeep
Normal file
0
pqos/Example files/.gitkeep
Normal file
29
pqos/Example files/pqos_curl.py
Normal file
29
pqos/Example files/pqos_curl.py
Normal 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
2404
pqos/Example files/test.csv
Normal file
File diff suppressed because it is too large
Load Diff
101
pqos/Example files/test100.csv
Normal file
101
pqos/Example files/test100.csv
Normal 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
|
||||||
|
22
pqos/README.md
Normal file
22
pqos/README.md
Normal 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
12
pqos/docker-push.sh
Executable 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
131
pqos/pqos.py
Normal 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
62
pqos/requirements.txt
Normal 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
BIN
pqos/trained_rtt.h5
Normal file
Binary file not shown.
Reference in New Issue
Block a user