From b970953c6c0545919e5b360643e78e5af6c49831 Mon Sep 17 00:00:00 2001 From: Xiex <562837197@qq.com> Date: Wed, 15 Nov 2023 21:39:30 +0800 Subject: [PATCH 01/49] Perf: optimize memory and CPU consumption 1. For memory usage, deprecate Docker(runc) container runtime. Instead use our own native implementation, in C module "pyctr", containing single source file pyctr.c, only support init and exec. Docker's implementation require a shim for each container, which consumes considerable program memory in user space. Besides, docker need complex ipc chain like docker<->dockerd<->containerd<->shim<->runc, slowing container init. Even some trials on lower level api (runc/crun), there's still unaffordable memory consumption in kernel. Finally, I reinventing the container management wheel specific to our environment, comprising some native linux syscall, just for less memory and a little improvement in container initialization. 2. For network, use veth in place of Docker's veth-bridge-veth mode. Docker's default network scheme occupy more CPU on softirq and we don't need it in satellite point-to-point link. --- setup.py | 4 +- starrynet/pyctr.c | 323 ++++++++++++++ starrynet/sn_orchestrater.py | 813 +++++++++++++---------------------- 3 files changed, 631 insertions(+), 509 deletions(-) create mode 100644 starrynet/pyctr.c diff --git a/setup.py b/setup.py index 77e031e..7780b16 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python "Setuptools params" -from setuptools import setup, find_packages +from setuptools import setup, find_packages, Extension from os.path import join # Get version number from source tree @@ -10,6 +10,7 @@ sys.path.append('.') scripts = [join('bin', filename) for filename in ['sn']] +ext_modules = [Extension('pyctr', [join('starrynet', 'pyctr.c')])] modname = distname = 'starrynet' @@ -35,4 +36,5 @@ license='BSD', install_requires=['setuptools'], scripts=scripts, + ext_modules=ext_modules, ) diff --git a/starrynet/pyctr.c b/starrynet/pyctr.c new file mode 100644 index 0000000..375a899 --- /dev/null +++ b/starrynet/pyctr.c @@ -0,0 +1,323 @@ +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +const int NS = CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWNET|CLONE_NEWIPC|CLONE_NEWUTS; + +static int child_err(const char *prefix, int write_fd) { + int err = errno; + const char *error_msg = strerror(err); + write(write_fd, prefix, strlen(prefix)); + write(write_fd, error_msg, strlen(error_msg)); + close(write_fd); + return err; +} + +// in child process with new namespace +static int container_init( + const char* newroot, + const char* overlay_opt, + const char* hostname, + int err_fd + ) { + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + int flags = fcntl(err_fd, F_GETFD); + flags |= FD_CLOEXEC; + fcntl(err_fd, F_SETFD, flags); + + if(mount("none", "/", NULL, MS_PRIVATE|MS_REC, NULL) != 0) { + return child_err("mount rprivate / failed: ", err_fd); + } + // mount overlay + if(mount("overlay", newroot, "overlay", 0, overlay_opt) != 0) { + return child_err("mount overlay failed: ", err_fd); + } + if(mount("none", newroot, NULL, MS_PRIVATE|MS_REC, NULL) != 0) { + return child_err("mount rprivate newroot failed: ", err_fd); + } + + if(chdir(newroot) != 0) { + return child_err("chdir failed: ", err_fd); + } + // pivot root + // https://unix.stackexchange.com/questions/456620/how-to-perform-chroot-with-linux-namespaces + if(syscall(SYS_pivot_root, ".", ".") != 0) { + return child_err("pivot_root failed: ", err_fd); + } + if(chroot(".") != 0) { + return child_err("chroot failed: ", err_fd); + } + if(umount2 (".", MNT_DETACH) != 0) { + return child_err("umount2 failed: ", err_fd); + } + // mount proc + if(mount("proc", "/proc", "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL) != 0) { + return child_err("mount /proc failed: ", err_fd); + } + // new session, detach to become a daemon process + if(setsid() < 0) { + return child_err("setsid failed: ", err_fd); + } + + // other miscellaneous configuration, maybe warning is better choice + if(signal(SIGCLD, SIG_IGN) < 0) { + return child_err("ignore SIGCLD failed: ", err_fd); + } + if(sethostname(hostname, strlen(hostname))) { + return child_err("sethostname failed: ", err_fd); + } + if(clearenv() != 0) { + return child_err("clearenv failed: ", err_fd); + } + if(putenv("HOME=/root") + || putenv("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")) { + return child_err("putenv failed: ", err_fd); + } + // sleep infinity, need a process with low resource requirement + execlp("sleep", "sleep", "inf", NULL); + // should not be executed here + return child_err("execlp failed: ", err_fd); +} + +// in child process +int container_enter(pid_t ctr_pid, char *const* argv, int err_fd) { + int flags = fcntl(err_fd, F_GETFD); + if(flags < 0) + return child_err("failed to fcntl F_GETFD: ", err_fd); + flags |= FD_CLOEXEC; + if(fcntl(err_fd, F_SETFD, flags) < 0) + return child_err("failed to fcntl F_SETFD: ", err_fd); + + int pid_fd = syscall(SYS_pidfd_open, ctr_pid, 0); + if(pid_fd < 0) + return child_err("failed to pidfd_open: ", err_fd); + + int ret = setns(pid_fd, NS); + close(pid_fd); + if(ret != 0) + return child_err("failed to setns: ", err_fd); + + execvp(argv[0], &argv[0]); + return child_err("failed to execvp: ", err_fd); +} + +// in parent process +// on success, ret > 0 means child pid. +// ret < 0 for parent err, ret == 0 for child err +static int container_run_inner( + const char *base_dir, const char *hostname, char *chd_err, size_t max_len) { + // 0755 + const mode_t MODE = S_IRWXU | (S_IRGRP|S_IXGRP) | (S_IROTH|S_IXOTH); + const char* UPPER_DIR = "upper"; + const char* WORK_DIR = "work"; + const char* NEWROOT = "rootfs"; + + if(access(base_dir, F_OK) && mkdir(base_dir, MODE)) return -1; + + int dir_fd = open(base_dir, O_RDONLY); + if(dir_fd < 0) return -1; + if((faccessat(dir_fd, UPPER_DIR, F_OK, 0) && mkdirat(dir_fd, UPPER_DIR, MODE)) + || (faccessat(dir_fd, WORK_DIR, F_OK, 0) && mkdirat(dir_fd, WORK_DIR, MODE)) + || (faccessat(dir_fd, NEWROOT, F_OK, 0) && mkdirat(dir_fd, NEWROOT, MODE))) { + close(dir_fd); + return -1; + } + if(close(dir_fd) != 0) return -1; + + char overlay_opt[PATH_MAX * 3]; + char new_root[PATH_MAX]; + snprintf(overlay_opt, sizeof(overlay_opt), + "lowerdir=/,upperdir=%s/%s,workdir=%s/%s", + base_dir, UPPER_DIR, base_dir, WORK_DIR); + snprintf(new_root, sizeof(new_root), "%s/%s", base_dir, NEWROOT); + + int err_fds[2], event_fd; + if(pipe(err_fds) != 0 || (event_fd = eventfd(0, 0)) < 0) return -1; + + pid_t pid = fork(); + if(pid < 0) { + close(err_fds[0]), close(err_fds[1]), close(event_fd); + return -1; + } else if(pid == 0) { + close(err_fds[0]); + if(unshare(NS) != 0) { + close(event_fd); + exit(child_err("unshare failed: ", err_fds[1])); + } + pid = fork(); + if(pid < 0) { + close(event_fd); + exit(child_err("second fork failed: ", err_fds[1])); + } else if(pid == 0) { + close(event_fd); + exit(container_init(new_root, overlay_opt, hostname, err_fds[1])); + // should not execute here + } + close(err_fds[1]); + uint64_t pid_u64 = pid; + write(event_fd, &pid_u64, sizeof(pid_u64)); + close(event_fd); + exit(0); + // should not execute here + } + + close(err_fds[1]); + ssize_t len = read(err_fds[0], chd_err, max_len); + // anyway, child should exit immediately + waitpid(pid, NULL, 0); + if(len > 0) { + chd_err[len] = '\0'; + close(err_fds[0]), close(event_fd); + return 0; + } + + // receive grandchild pid from child + uint64_t pid_u64; + if(read(event_fd, &pid_u64, sizeof(pid_u64)) == sizeof(pid_u64)) + pid = pid_u64; + else + pid = -1; + + close(err_fds[0]), close(event_fd); + return pid; +} + +// in parent process +// on success, ret > 0 means child pid, need to be waited and recycled +// ret < 0 for parent err, ret == 0 for child err +static int container_exec_inner( + pid_t ctr_pid, char *const* argv, char *chd_err, size_t max_len) { + int err_fds[2]; + pid_t ret; + ssize_t err_len; + + if(pipe(err_fds) != 0) return -1; + + ret = fork(); + if(ret < 0) { + close(err_fds[0]), close(err_fds[1]); + return -1; + } else if(ret == 0) { + close(err_fds[0]); + exit(container_enter(ctr_pid, argv, err_fds[1])); + // should not be executed + } + close(err_fds[1]); + + err_len = read(err_fds[0], chd_err, max_len); + if(err_len > 0) { + chd_err[err_len] = '\0'; + waitpid(ret, NULL, 0); + ret = 0; + } + close(err_fds[0]); + return ret; +} + +// ========================Python wrapper======================== + +static PyObject *container_run(PyObject *self, PyObject *args) { + const char *base_dir = NULL; + const char *hostname = NULL; + char chd_err[256]; + int pid; + + if (!PyArg_ParseTuple(args, + "ss:container_run(base_dir, hostname)", &base_dir, &hostname)) + return NULL; + + pid = container_run_inner(base_dir, hostname, chd_err, sizeof(chd_err) - 1); + if(pid < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } else if (pid == 0) { + PyErr_SetString(PyExc_ChildProcessError, chd_err); + return NULL; + } else { // normal case + return PyLong_FromLong(pid); + } +} + +static PyObject *container_exec(PyObject *self, PyObject *args) { + int pid; + PyObject *cmdline; + Py_ssize_t argc; + char **argv; + char chd_err[256]; + int ret; + + if(!PyArg_ParseTuple(args, + "iO:container_exec(container_pid, cmdline)", &pid, &cmdline)) + return NULL; + if(!PySequence_Check(cmdline) || (argc = PySequence_Size(cmdline)) <= 0) { + PyErr_SetString(PyExc_TypeError, + "argument 2 \"cmdline\" must be sequence with length >= 1"); + return NULL; + } + + argv = malloc((argc + 1) * sizeof(*argv)); + for(Py_ssize_t i = 0; i < argc; i++) { + argv[i] = PyBytes_AsString(PySequence_ITEM(cmdline, i)); + if(argv[i] == NULL) { + free(argv); + return NULL; + } + } + argv[argc] = NULL; + + ret = container_exec_inner(pid, argv, chd_err, sizeof(chd_err) - 1); + if(ret < 0) { + PyErr_SetFromErrno(PyExc_OSError); + } else if (ret == 0) { + PyErr_SetString(PyExc_ChildProcessError, chd_err); + } else { + int status; + waitpid(ret, &status, 0); + if(!WIFEXITED(status)) { + PyErr_SetString(PyExc_ChildProcessError, "child did not exit normally"); + } else { + free(argv); + return PyLong_FromLong(WEXITSTATUS(status)); + } + } + free(argv); + return NULL; +} + +static PyMethodDef methods[] = { + {"container_run", container_run, METH_VARARGS, "run a simplified container"}, + {"container_exec", container_exec, METH_VARARGS, "exec command in container"}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, + "pyctr", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + methods +}; + +PyMODINIT_FUNC PyInit_pyctr(void) { + return PyModule_Create(&module); +} diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index d677469..1ed22bf 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -1,427 +1,273 @@ import os -import threading +import subprocess import sys -from time import sleep -import numpy +import gzip +from concurrent.futures import ThreadPoolExecutor + +# C module +import pyctr + +# from time import sleep """ Used in the remote machine for link updating, initializing links, damaging and recovering links and other functionalities。 author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) and Zeqi Lai (zeqilai@tsinghua.edu.cn) """ +PID_FILENAME = 'container_pid.txt' +ASSIGN_FILENAME = 'assign.txt' +NOT_ASSIGNED = 'NA' -def sn_get_right_satellite(current_sat_id, current_orbit_id, orbit_num): - if current_orbit_id == orbit_num - 1: - return [current_sat_id, 0] - else: - return [current_sat_id, current_orbit_id + 1] +machine_id = None +workdir = None +init_pid_mat = None +def _sat_name(orbit_id, sat_id): + return f'O{orbit_id+1}S{sat_id+1}' -def sn_get_down_satellite(current_sat_id, current_orbit_id, sat_num): - if current_sat_id == sat_num - 1: - return [0, current_orbit_id] - else: - return [current_sat_id + 1, current_orbit_id] - - -def sn_ISL_establish(current_sat_id, current_orbit_id, container_id_list, - orbit_num, sat_num, constellation_size, matrix, bw, loss): - current_id = current_orbit_id * sat_num + current_sat_id - isl_idx = current_id * 2 + 1 - # Establish intra-orbit ISLs - # (Down): - [down_sat_id, - down_orbit_id] = sn_get_down_satellite(current_sat_id, current_orbit_id, - sat_num) - print("[" + str(isl_idx) + "/" + str(constellation_size * 2) + - "] Establish intra-orbit ISL from: (" + str(current_sat_id) + "," + - str(current_orbit_id) + ") to (" + str(down_sat_id) + "," + - str(down_orbit_id) + ")") - ISL_name = "Le_" + str(current_sat_id) + "-" + str(current_orbit_id) + \ - "_" + str(down_sat_id) + "-" + str(down_orbit_id) - address_16_23 = isl_idx >> 8 - address_8_15 = isl_idx & 0xff - # Create internal network in docker. - os.system('docker network create ' + ISL_name + " --subnet 10." + - str(address_16_23) + "." + str(address_8_15) + ".0/24") - print('[Create ISL:]' + 'docker network create ' + ISL_name + - " --subnet 10." + str(address_16_23) + "." + str(address_8_15) + - ".0/24") - os.system('docker network connect ' + ISL_name + " " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + " --ip 10." + - str(address_16_23) + "." + str(address_8_15) + ".40") - delay = matrix[current_orbit_id * sat_num + - current_sat_id][down_orbit_id * sat_num + down_sat_id] - with os.popen( - "docker exec -it " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip addr | grep -B 2 10." + str(address_16_23) + "." + - str(address_8_15) + - ".40 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip link set dev " + target_interface + " down") - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip link set dev " + target_interface + " name " + "B" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - "-eth" + str(down_orbit_id * sat_num + down_sat_id + 1)) - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip link set dev B" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - "-eth" + str(down_orbit_id * sat_num + down_sat_id + 1) + - " up") - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " tc qdisc add dev B" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - "-eth" + str(down_orbit_id * sat_num + down_sat_id + 1) + - " root netem delay " + str(delay) + "ms loss " + str(loss) + "% rate " + str(bw) + "Gbps") - print('[Add current node:]' + 'docker network connect ' + ISL_name + " " + - str(container_id_list[current_orbit_id * sat_num + current_sat_id]) + - " --ip 10." + str(address_16_23) + "." + str(address_8_15) + ".40") - os.system('docker network connect ' + ISL_name + " " + - str(container_id_list[down_orbit_id * sat_num + down_sat_id]) + - " --ip 10." + str(address_16_23) + "." + str(address_8_15) + - ".10") - with os.popen( - "docker exec -it " + - str(container_id_list[down_orbit_id * sat_num + down_sat_id]) + - " ip addr | grep -B 2 10." + str(address_16_23) + "." + - str(address_8_15) + - ".10 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + - str(container_id_list[down_orbit_id * sat_num + - down_sat_id]) + " ip link set dev " + - target_interface + " down") - os.system("docker exec -d " + - str(container_id_list[down_orbit_id * sat_num + - down_sat_id]) + " ip link set dev " + - target_interface + " name " + "B" + - str(down_orbit_id * sat_num + down_sat_id + 1) + "-eth" + - str(current_orbit_id * sat_num + current_sat_id + 1)) - os.system("docker exec -d " + - str(container_id_list[down_orbit_id * sat_num + - down_sat_id]) + " ip link set dev B" + - str(down_orbit_id * sat_num + down_sat_id + 1) + "-eth" + - str(current_orbit_id * sat_num + current_sat_id + 1) + " up") - os.system("docker exec -d " + - str(container_id_list[down_orbit_id * sat_num + - down_sat_id]) + " tc qdisc add dev B" + - str(down_orbit_id * sat_num + down_sat_id + 1) + "-eth" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - " root netem delay " + str(delay) + "ms loss " + str(loss) + "% rate " + str(bw) + "Gbps") - print('[Add down node:]' + 'docker network connect ' + ISL_name + " " + - str(container_id_list[down_orbit_id * sat_num + down_sat_id]) + - " --ip 10." + str(address_16_23) + "." + str(address_8_15) + ".10") - - print("Add 10." + str(address_16_23) + "." + str(address_8_15) + - ".40/24 and 10." + str(address_16_23) + "." + str(address_8_15) + - ".10/24 to (" + str(current_sat_id) + "," + str(current_orbit_id) + - ") to (" + str(down_sat_id) + "," + str(down_orbit_id) + ")") - isl_idx = isl_idx + 1 - - # Establish inter-orbit ISLs - # (Right): - [right_sat_id, - right_orbit_id] = sn_get_right_satellite(current_sat_id, current_orbit_id, - orbit_num) - print("[" + str(isl_idx) + "/" + str(constellation_size * 2) + - "] Establish inter-orbit ISL from: (" + str(current_sat_id) + "," + - str(current_orbit_id) + ") to (" + str(right_sat_id) + "," + - str(right_orbit_id) + ")") - ISL_name = "La_" + str(current_sat_id) + "-" + str(current_orbit_id) + \ - "_" + str(right_sat_id) + "-" + str(right_orbit_id) - address_16_23 = isl_idx >> 8 - address_8_15 = isl_idx & 0xff - # Create internal network in docker. - os.system('docker network create ' + ISL_name + " --subnet 10." + - str(address_16_23) + "." + str(address_8_15) + ".0/24") - print('[Create ISL:]' + 'docker network create ' + ISL_name + - " --subnet 10." + str(address_16_23) + "." + str(address_8_15) + - ".0/24") - os.system('docker network connect ' + ISL_name + " " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + " --ip 10." + - str(address_16_23) + "." + str(address_8_15) + ".30") - delay = matrix[current_orbit_id * sat_num + - current_sat_id][right_orbit_id * sat_num + right_sat_id] - with os.popen( - "docker exec -it " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip addr | grep -B 2 10." + str(address_16_23) + "." + - str(address_8_15) + - ".30 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip link set dev " + target_interface + " down") - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip link set dev " + target_interface + " name " + "B" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - "-eth" + str(right_orbit_id * sat_num + right_sat_id + 1)) - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " ip link set dev B" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - "-eth" + str(right_orbit_id * sat_num + right_sat_id + 1) + - " up") - os.system("docker exec -d " + - str(container_id_list[current_orbit_id * sat_num + - current_sat_id]) + - " tc qdisc add dev B" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - "-eth" + str(right_orbit_id * sat_num + right_sat_id + 1) + - " root netem delay " + str(delay) + "ms loss " + str(loss) + "% rate " + str(bw) + "Gbps") - print('[Add current node:]' + 'docker network connect ' + ISL_name + " " + - str(container_id_list[current_orbit_id * sat_num + current_sat_id]) + - " --ip 10." + str(address_16_23) + "." + str(address_8_15) + ".30") - os.system('docker network connect ' + ISL_name + " " + - str(container_id_list[right_orbit_id * sat_num + right_sat_id]) + - " --ip 10." + str(address_16_23) + "." + str(address_8_15) + - ".20") - - with os.popen( - "docker exec -it " + - str(container_id_list[right_orbit_id * sat_num + right_sat_id]) + - " ip addr | grep -B 2 10." + str(address_16_23) + "." + - str(address_8_15) + - ".20 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + - str(container_id_list[right_orbit_id * sat_num + - right_sat_id]) + " ip link set dev " + - target_interface + " down") - os.system("docker exec -d " + - str(container_id_list[right_orbit_id * sat_num + - right_sat_id]) + " ip link set dev " + - target_interface + " name " + "B" + - str(right_orbit_id * sat_num + right_sat_id + 1) + "-eth" + - str(current_orbit_id * sat_num + current_sat_id + 1)) - os.system("docker exec -d " + - str(container_id_list[right_orbit_id * sat_num + - right_sat_id]) + " ip link set dev B" + - str(right_orbit_id * sat_num + right_sat_id + 1) + "-eth" + - str(current_orbit_id * sat_num + current_sat_id + 1) + " up") - os.system("docker exec -d " + - str(container_id_list[right_orbit_id * sat_num + - right_sat_id]) + - " tc qdisc add dev B" + - str(right_orbit_id * sat_num + right_sat_id + 1) + "-eth" + - str(current_orbit_id * sat_num + current_sat_id + 1) + - " root netem delay " + str(delay) + "ms loss " + str(loss) + "% rate " + str(bw) + "Gbps") - print('[Add right node:]' + 'docker network connect ' + ISL_name + " " + - str(container_id_list[right_orbit_id * sat_num + right_sat_id]) + - " --ip 10." + str(address_16_23) + "." + str(address_8_15) + ".20") - - print("Add 10." + str(address_16_23) + "." + str(address_8_15) + - ".30/24 and 10." + str(address_16_23) + "." + str(address_8_15) + - ".20/24 to (" + str(current_sat_id) + "," + str(current_orbit_id) + - ") to (" + str(right_sat_id) + "," + str(right_orbit_id) + ")") - - -def sn_establish_ISLs(container_id_list, matrix, orbit_num, sat_num, - constellation_size, bw, loss): - ISL_threads = [] - for current_orbit_id in range(0, orbit_num): - for current_sat_id in range(0, sat_num): - ISL_thread = threading.Thread( - target=sn_ISL_establish, - args=(current_sat_id, current_orbit_id, container_id_list, - orbit_num, sat_num, constellation_size, matrix, bw, - loss)) - ISL_threads.append(ISL_thread) - for ISL_thread in ISL_threads: - ISL_thread.start() - for ISL_thread in ISL_threads: - ISL_thread.join() - - -def sn_get_param(file_): - f = open(file_) - ADJ = f.readlines() - for i in range(len(ADJ)): - ADJ[i] = ADJ[i].strip('\n') - ADJ = [x.split(',') for x in ADJ] +def _get_delay(path): + f = gzip.open(path, 'rt') + ADJ = [ line.strip().split(',') for line in f ] f.close() return ADJ +def _get_params(path): + with open(path, 'r') as f: + orbit_num = int(f.readline()) + sat_assign_lst = [int(tok) for tok in f.readline().split(' ')] + gs_assign_lst = [int(tok) for tok in f.readline().split(' ')] + return orbit_num, sat_assign_lst, gs_assign_lst + +def _get_init_pid(orbit_id, sat_id): + global init_pid_mat + if init_pid_mat is None: + with open(workdir + '/' + PID_FILENAME, 'r') as f: + init_pid_mat = [[pid for pid in line.strip().split(' ')] for line in f] + return init_pid_mat[sat_id][orbit_id] + +def sn_right_satellite(current_sat_id, current_orbit_id, orbit_num): + if current_orbit_id == orbit_num - 1: + return current_sat_id, 0 + else: + return current_sat_id, current_orbit_id + 1 -def sn_get_container_info(): - # Read all container information in all_container_info - with os.popen("docker ps") as f: - all_container_info = f.readlines() - n_container = len(all_container_info) - 1 - - container_id_list = [] - for container_idx in range(1, n_container + 1): - container_id_list.append(all_container_info[container_idx].split()[0]) - - return container_id_list - - -def sn_establish_GSL(container_id_list, matrix, GS_num, constellation_size, bw, - loss): +def sn_down_satellite(current_sat_id, current_orbit_id, sat_num): + if current_sat_id == sat_num - 1: + return 0, current_orbit_id + else: + return current_sat_id + 1, current_orbit_id + +def sn_init_nodes(dir, orbit_num, sat_assign_lst, gs_assign_lst): + sn_clear(dir) + overlay_dir = dir + '/overlay' + os.makedirs(overlay_dir, exist_ok=True) + pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') + for sat_id, assign in enumerate(sat_assign_lst): + if assign != machine_id: + pid_file.write(' '.join(NOT_ASSIGNED for _ in range(orbit_num)) + '\n') + continue + print('sat:', sat_id) + for orbit_id in range(orbit_num): + sat_name = _sat_name(orbit_id, sat_id) + sat_dir = f'{overlay_dir}/{sat_name}' + pid_file.write(str(pyctr.container_run(sat_dir, sat_name)) + ' ') + pid_file.write('\n') + + for gs_id, assign in enumerate(gs_assign_lst): + if assign != machine_id: + pid_file.write(NOT_ASSIGNED + '\n') + continue + gs_name = f'GS{gs_id+1}' + gs_dir = f'{overlay_dir}/{gs_name}' + pid_file.write(str(pyctr.container_run(gs_dir, gs_name)) + '\n') + pid_file.close() + +def sn_ISL_intra_machine(isl_idx, orbit, sat, peer_orbit, peer_sat, delay, bw, loss): + cur_name, peer_name = _sat_name(orbit, sat), _sat_name(peer_orbit, peer_sat) + for o, s, netns in ((orbit, sat, cur_name), (peer_orbit, peer_sat, peer_name)): + pid = _get_init_pid(o, s) + netns_link = f'/run/netns/{netns}' + if os.path.exists(netns_link) or os.path.islink(netns_link): + print('[Warning]netns exist:', netns_link) + subprocess.check_call(('rm', netns_link)) + subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) + + prefix = f'10.{isl_idx >> 8}.{isl_idx & 0xFF}' + cur_peer = f'{cur_name}-{peer_name}' + peer_cur = f'{peer_name}-{cur_name}' + if subprocess.call(f"ip link show | grep '{cur_peer}'", shell=True) == 0: + print('[Warning]veth exist:', cur_peer) + subprocess.check_call(('ip', 'link', 'del', cur_peer)) + if subprocess.call(f"ip link show | grep '{peer_cur}'", shell=True) == 0: + print('[Warning]veth exist:', peer_cur) + subprocess.check_call(('ip', 'link', 'del', peer_cur)) + subprocess.check_call( + ('ip', 'link', 'add', cur_peer, 'type', 'veth', 'peer', 'name', peer_cur)) + subprocess.check_call(('ip', 'link', 'set', cur_peer, 'netns', cur_name)) + subprocess.check_call(('ip', 'link', 'set', peer_cur, 'netns', peer_name)) + subprocess.check_call( + ('ip', 'netns', 'exec', cur_name, + 'ip', 'addr', 'add', prefix+'.40/24', 'dev', cur_peer)) + subprocess.check_call( + ('ip', 'netns', 'exec', cur_name, + 'tc', 'qdisc', 'add', 'dev', cur_peer, 'root', + # 'netem', 'loss', loss+'%', 'rate', bw+'Gbps')) + 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbps')) + subprocess.check_call( + ('ip', 'netns', 'exec', cur_name, 'ip', 'link', 'set', cur_peer, 'up')) + + subprocess.check_call( + ('ip', 'netns', 'exec', peer_name, + 'ip', 'addr', 'add', prefix+'.10/24', 'dev', peer_cur)) + subprocess.check_call( + ('ip', 'netns', 'exec', peer_name, + 'tc', 'qdisc', 'add', 'dev', peer_cur, 'root', + # 'netem', 'loss', loss+'%', 'rate', bw+'Gbps')) + 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbps')) + subprocess.check_call( + ('ip', 'netns', 'exec', peer_name, 'ip', 'link', 'set', peer_cur, 'up')) + for netns in (cur_name, peer_name): + os.remove(f'/run/netns/{netns}') + +def sn_ISL_inter_machine(isl_idx, orbit, sat, peer_orbit, peer_sat, delay, bw, loss): + raise NotImplementedError + +def sn_GSL_intra_machine(gsl_idx, orbit, sat, gs, delay, bw, loss): + raise NotImplementedError + +def sn_init_networks(matrix, bw, loss, orbit_num, sat_assign_lst, gs_assign_lst): + sat_num = len(sat_assign_lst) + for sat_id, sat_assign in enumerate(sat_assign_lst): + if sat_assign != machine_id: + continue + print('sat:', sat_id) + for orbit_id in range(orbit_num): + cur_idx = orbit_id * sat_num + sat_id + right_sat, right_orbit = sn_right_satellite(sat_id, orbit_id, orbit_num) + down_sat, down_orbit = sn_down_satellite(sat_id, orbit_id, sat_num) + + isl_indices = (cur_idx << 1, (cur_idx <<1) + 1) + isl_orbits = (right_orbit, down_orbit) + isl_sats = (right_sat, down_sat) + for isl_idx, isl_orbit, isl_sat in zip(isl_indices, isl_orbits, isl_sats): + if sat_assign_lst[isl_sat] == sat_assign: + sn_ISL_intra_machine( + isl_idx, orbit_id, sat_id, isl_orbit, isl_sat, + matrix[cur_idx][isl_orbit * sat_num + isl_sat], bw, loss + ) + else: + sn_ISL_inter_machine( + isl_idx, orbit_id, sat_id, isl_orbit, isl_sat, + matrix[cur_idx][isl_orbit * sat_num + isl_sat], bw, loss + ) + + # for gs_id, gs_assign in enumerate(gs_assign_lst): + # if gs_assign != machine_id: + # continue + # print('gs:', gs_id) + # for sat_id, sat_assign in enumerate(sat_assign_lst): + +def sn_establish_GSL(netns_list, matrix, GS_num, bw, loss): # starting links among satellites and ground stations - for i in range(1, constellation_size + 1): - for j in range(constellation_size + 1, - constellation_size + GS_num + 1): - # matrix[i-1][j-1])==1 means a link between node i and node j - if ((float(matrix[i - 1][j - 1])) <= 0.01): + constellation_size = orbit_num * sat_num + for sat_id in range(constellation_size): + for gs_id in range(constellation_size, constellation_size + GS_num): + # matrix[i1][j1])==1 means a link between node i and node j + delay = matrix[sat_id][gs_id] + if float(delay) <= 0.01: continue # IP address (there is a link between i and j) - delay = str(matrix[i - 1][j - 1]) - address_16_23 = (j - constellation_size) & 0xff - address_8_15 = i & 0xff - GSL_name = "GSL_" + str(i) + "-" + str(j) + address_16_23 = (gs_id - constellation_size) & 0xff + address_8_15 = sat_id & 0xff + prefix = f'9.{address_16_23}.{address_8_15}' + GSL_name = f'GSL_{sat_id + 1}-{gs_id + 1}' + sat2gs = f'B{sat_id + 1}-eth{gs_id + 1}' + gs2sat = f'B{gs_id + 1}-eth{sat_id + 1}' # Create internal network in docker. - os.system('docker network create ' + GSL_name + " --subnet 9." + - str(address_16_23) + "." + str(address_8_15) + ".0/24") - print('[Create GSL:]' + 'docker network create ' + GSL_name + - " --subnet 9." + str(address_16_23) + "." + - str(address_8_15) + ".0/24") - os.system('docker network connect ' + GSL_name + " " + - str(container_id_list[i - 1]) + " --ip 9." + - str(address_16_23) + "." + str(address_8_15) + ".50") - with os.popen( - "docker exec -it " + str(container_id_list[i - 1]) + - " ip addr | grep -B 2 9." + str(address_16_23) + "." + - str(address_8_15) + - ".50 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]" - ) as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev " + target_interface + " down") - os.system("docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev " + target_interface + " name " + - "B" + str(i - 1 + 1) + "-eth" + str(j)) - os.system("docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev B" + str(i - 1 + 1) + "-eth" + - str(j) + " up") - os.system("docker exec -d " + str(container_id_list[i - 1]) + - " tc qdisc add dev B" + str(i - 1 + 1) + "-eth" + - str(j) + " root netem delay " + str(delay) + "ms loss " + str(loss) + "% rate " + str(bw) + "Gbps") - print('[Add current node:]' + 'docker network connect ' + - GSL_name + " " + str(container_id_list[i - 1]) + " --ip 9." + - str(address_16_23) + "." + str(address_8_15) + ".50") - - os.system('docker network connect ' + GSL_name + " " + - str(container_id_list[j - 1]) + " --ip 9." + - str(address_16_23) + "." + str(address_8_15) + ".60") - with os.popen( - "docker exec -it " + str(container_id_list[j - 1]) + - " ip addr | grep -B 2 9." + str(address_16_23) + "." + - str(address_8_15) + - ".60 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]" - ) as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev " + target_interface + " down") - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev " + target_interface + " name " + - "B" + str(j) + "-eth" + str(i - 1 + 1)) - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev B" + str(j) + "-eth" + - str(i - 1 + 1) + " up") - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " tc qdisc add dev B" + str(j) + "-eth" + - str(i - 1 + 1) + " root netem delay " + str(delay) + - "ms loss " + str(loss) + "% rate " + str(bw) + - "Gbps") - print('[Add right node:]' + 'docker network connect ' + GSL_name + - " " + str(container_id_list[j - 1]) + " --ip 9." + - str(address_16_23) + "." + str(address_8_15) + ".60") - for j in range(constellation_size + 1, constellation_size + GS_num + 1): - GS_name = "GS_" + str(j) + print(f"[Create {GSL_name}] {prefix}.0/24") + subprocess.check_call( + f"ip link add {sat2gs} type veth peer name {gs2sat} ", shell=True) + subprocess.check_call( + f"ip link set {sat2gs} netns {netns_list[sat_id]}", shell=True) + subprocess.check_call( + f"ip link set {gs2sat} netns {netns_list[gs_id]}", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[sat_id]} " + f"ip addr add {prefix}.50/24 dev {sat2gs}", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[sat_id]} " + f"tc qdisc add dev {sat2gs} root " + f"netem delay {delay}ms loss {loss}% rate {bw}Gbps", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[sat_id]} " + f"ip link set dev {sat2gs} up", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[gs_id]} " + f"ip addr add {prefix}.60/24 dev {gs2sat}", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[gs_id]} " + f"tc qdisc add dev {gs2sat} root " + f"netem delay {delay}ms loss {loss}% rate {bw}Gbps", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[gs_id]} " + f"ip link set dev {gs2sat} up", shell=True) + for gs_id in range(constellation_size, constellation_size + GS_num): + GS_name = f"GS_{gs_id + 1}" # Create default network and interface for GS. - os.system('docker network create ' + GS_name + " --subnet 9." + - str(j) + "." + str(j) + ".0/24") - print('[Create GS network:]' + 'docker network create ' + GS_name + - " --subnet 9." + str(j) + "." + str(j) + ".10/24") - os.system('docker network connect ' + GS_name + " " + - str(container_id_list[j - 1]) + " --ip 9." + str(j) + "." + - str(j) + ".10") - with os.popen( - "docker exec -it " + str(container_id_list[j - 1]) + - " ip addr | grep -B 2 9." + str(j) + "." + str(j) + - ".10 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]" - ) as f: - ifconfig_output = f.readline() - target_interface = str(ifconfig_output).split("@")[0] - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev " + target_interface + " down") - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev " + target_interface + " name " + "B" + - str(j - 1 + 1) + "-default") - os.system("docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev B" + str(j - 1 + 1) + "-default" + - " up") - print('[Add current node:]' + 'docker network connect ' + GS_name + - " " + str(container_id_list[j - 1]) + " --ip 9." + str(j) + "." + - str(j) + ".10") - - -def sn_copy_run_conf(container_idx, Path, current, total): - os.system("docker cp " + Path + "/B" + str(current + 1) + ".conf " + - str(container_idx) + ":/B" + str(current + 1) + ".conf") - print("[" + str(current + 1) + "/" + str(total) + "]" + - " docker cp bird.conf " + str(container_idx) + ":/bird.conf") - os.system("docker exec -it " + str(container_idx) + " bird -c B" + - str(current + 1) + ".conf") - print("[" + str(current + 1) + "/" + str(total) + - "] Bird routing process for container: " + str(container_idx) + - " has started. ") - - -def sn_copy_run_conf_to_each_container(container_id_list, sat_node_number, - fac_node_number, path): - print( - "Copy bird configuration file to each container and run routing process." - ) - total = len(container_id_list) - copy_threads = [] - for current in range(0, total): - copy_thread = threading.Thread( - target=sn_copy_run_conf, - args=(container_id_list[current], path + "/conf/bird-" + - str(sat_node_number) + "-" + str(fac_node_number), current, - total)) - copy_threads.append(copy_thread) - for copy_thread in copy_threads: - copy_thread.start() - for copy_thread in copy_threads: - copy_thread.join() - print("Initializing routing...") - sleep(120) - print("Routing initialized!") - - -def sn_damage_link(sat_index, container_id_list): + # TODO limit GS size to 256 + prefix = f'9.{gs_id - constellation_size}.{gs_id - constellation_size}' + gs_int = f'B{gs_id + 1}-default' + print(f"[Create {GS_name}] {prefix}.0/24") + subprocess.check_call( + f"ip netns exec {netns_list[gs_id]} " + f"ip link add {gs_int} type dummy", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[gs_id]} " + f"ip addr add {prefix}.10/24 dev {gs_int}", shell=True) + subprocess.check_call( + f"ip netns exec {netns_list[gs_id]} " + f"ip link set dev {gs_int} up", shell=True) + +def sn_container_exec(pid, cmd): + return pyctr.container_exec(pid, tuple(arg.encode() for arg in cmd)) + +def sn_init_route_daemon(sat_assign_lst, bird_conf): + for sat_id, sat_assign in enumerate(sat_assign_lst): + if sat_assign != machine_id: + continue + print('sat:', sat_id) + for orbit_id in range(orbit_num): + sn_container_exec( + int(_get_init_pid(orbit_id, sat_id)), + ('bird', '-c', bird_conf) + ) + +def sn_clear(dir): + pid_path = dir + '/' + PID_FILENAME + if os.path.exists(pid_path): + pid_file = open(pid_path, 'r', encoding='utf-8') + for line in pid_file: + for pid in line.strip().split(' '): + if pid == NOT_ASSIGNED: + continue + os.kill(int(pid), 9) + pid_file.close() + os.remove(pid_path) + overlay_dir = dir + '/overlay' + if not os.path.exists(overlay_dir): + return + for entr in os.listdir(overlay_dir): + merge_dir = f'{overlay_dir}/{entr}/rootfs' + if os.system(f"mountpoint -q {merge_dir}") == 0: + subprocess.check_call(('umount', merge_dir)) + +def sn_damage_link(orbit_id, sat_id): + with os.popen( "docker exec -it " + str(container_id_list[sat_index]) + " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") as f: ifconfig_output = f.readlines() for intreface in range(0, len(ifconfig_output), 2): - os.system("docker exec -d " + str(container_id_list[sat_index]) + + subprocess.check_call("docker exec -d " + str(container_id_list[sat_index], shell=True) + " tc qdisc change dev " + ifconfig_output[intreface][:-1] + " root netem loss 100%") @@ -429,18 +275,11 @@ def sn_damage_link(sat_index, container_id_list): " tc qdisc change dev " + ifconfig_output[intreface][:-1] + " root netem loss 100%") - def sn_damage(random_list, container_id_list): - damage_threads = [] + pool = ThreadPoolExecutor() for random_satellite in random_list: - damage_thread = threading.Thread(target=sn_damage_link, - args=(int(random_satellite), - container_id_list)) - damage_threads.append(damage_thread) - for damage_thread in damage_threads: - damage_thread.start() - for damage_thread in damage_threads: - damage_thread.join() + pool.submit(sn_damage_link, int(random_satellite), container_id_list) + pool.shutdown(wait=True) def sn_recover_link( @@ -453,8 +292,8 @@ def sn_recover_link( " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") as f: ifconfig_output = f.readlines() for i in range(0, len(ifconfig_output), 2): - os.system("docker exec -d " + - str(container_id_list[damaged_satellite]) + + subprocess.check_call("docker exec -d " + + str(container_id_list[damaged_satellite], shell=True) + " tc qdisc change dev " + ifconfig_output[i][:-1] + " root netem loss " + str(sat_loss) + "%") print("docker exec -d " + @@ -464,126 +303,84 @@ def sn_recover_link( def sn_del_network(network_name): - os.system('docker network rm ' + network_name) + subprocess.check_call('docker network rm ' + network_name, shell=True) def sn_stop_emulation(): - os.system("docker service rm constellation-test") - with os.popen("docker rm -f $(docker ps -a -q)") as f: - f.readlines() - with os.popen("docker network ls") as f: - all_br_info = f.readlines() - del_threads = [] - for line in all_br_info: - if "La" in line or "Le" or "GS" in line: - network_name = line.split()[1] - del_thread = threading.Thread(target=sn_del_network, - args=(network_name, )) - del_threads.append(del_thread) - for del_thread in del_threads: - del_thread.start() - for del_thread in del_threads: - del_thread.join() + # subprocess.check_call("docker service rm constellation-test", shell=True) + # with os.popen("docker rm -f $(docker ps -a -q)") as f: + # f.readlines() + subprocess.check_call("for COMPOSE in $(docker compose ls | grep grid | awk '{print $3}', shell=True);" + "do docker compose -f $COMPOSE down 2>/dev/null;" + "done") + subprocess.check_call( + "for NETWORK in $(docker network ls | grep -o '\(La\|Le\|GS\, shell=True)\S*');" + "do docker network rm $NETWORK;" + "done" + ) def sn_recover(damage_list, container_id_list, sat_loss): - recover_threads = [] + pool = ThreadPoolExecutor() for damaged_satellite in damage_list: - recover_thread = threading.Thread(target=sn_recover_link, - args=(int(damaged_satellite), - container_id_list, sat_loss)) - recover_threads.append(recover_thread) - for recover_thread in recover_threads: - recover_thread.start() - for recover_thread in recover_threads: - recover_thread.join() - - -def sn_update_delay(matrix, container_id_list, - constellation_size): # updating delays - delay_threads = [] + pool.submit(sn_recover_link, + int(damaged_satellite), container_id_list, sat_loss) + pool.shutdown(wait=True) + + +# updating delays +def sn_update_delay(matrix, container_id_list, constellation_size): + pool = ThreadPoolExecutor() for row in range(len(matrix)): for col in range(row, len(matrix[row])): - if float(matrix[row][col]) > 0: - if row < col: - delay_thread = threading.Thread( - target=sn_delay_change, - args=(row, col, matrix[row][col], container_id_list, - constellation_size)) - delay_threads.append(delay_thread) - else: - delay_thread = threading.Thread( - target=sn_delay_change, - args=(col, row, matrix[col][row], container_id_list, - constellation_size)) - delay_threads.append(delay_thread) - for delay_thread in delay_threads: - delay_thread.start() - for delay_thread in delay_threads: - delay_thread.join() + if float(matrix[row][col]) <= 0: + continue + + if row < col: + pool.submit(sn_delay_change, + row, col, matrix[row][col], + container_id_list, constellation_size) + else: + pool.submit(sn_delay_change, + col, row, matrix[col][row], + container_id_list, constellation_size) + pool.shutdown(wait=True) print("Delay updating done.\n") def sn_delay_change(link_x, link_y, delay, container_id_list, constellation_size): # multi-thread updating delays if link_y <= constellation_size: - os.system("docker exec -d " + str(container_id_list[link_x]) + + subprocess.check_call("docker exec -d " + str(container_id_list[link_x], shell=True) + " tc qdisc change dev B" + str(link_x + 1) + "-eth" + str(link_y + 1) + " root netem delay " + str(delay) + "ms") - os.system("docker exec -d " + str(container_id_list[link_y]) + + subprocess.check_call("docker exec -d " + str(container_id_list[link_y], shell=True) + " tc qdisc change dev B" + str(link_y + 1) + "-eth" + str(link_x + 1) + " root netem delay " + str(delay) + "ms") else: - os.system("docker exec -d " + str(container_id_list[link_x]) + + subprocess.check_call("docker exec -d " + str(container_id_list[link_x], shell=True) + " tc qdisc change dev B" + str(link_x + 1) + "-eth" + str(link_y + 1) + " root netem delay " + str(delay) + "ms") - os.system("docker exec -d " + str(container_id_list[link_y]) + + subprocess.check_call("docker exec -d " + str(container_id_list[link_y], shell=True) + " tc qdisc change dev B" + str(link_y + 1) + "-eth" + str(link_x + 1) + " root netem delay " + str(delay) + "ms") if __name__ == '__main__': - if len(sys.argv) == 10: - orbit_num = int(sys.argv[1]) - sat_num = int(sys.argv[2]) - constellation_size = int(sys.argv[3]) - GS_num = int(sys.argv[4]) - sat_bandwidth = float(sys.argv[5]) - sat_loss = float(sys.argv[6]) - sat_ground_bandwidth = float(sys.argv[7]) - sat_ground_loss = float(sys.argv[8]) - current_topo_path = sys.argv[9] - matrix = sn_get_param(current_topo_path) - container_id_list = sn_get_container_info() - sn_establish_ISLs(container_id_list, matrix, orbit_num, sat_num, - constellation_size, sat_bandwidth, sat_loss) - sn_establish_GSL(container_id_list, matrix, GS_num, constellation_size, - sat_ground_bandwidth, sat_ground_loss) - elif len(sys.argv) == 4: - if sys.argv[3] == "update": - current_delay_path = sys.argv[1] - constellation_size = int(sys.argv[2]) - matrix = sn_get_param(current_delay_path) - container_id_list = sn_get_container_info() - sn_update_delay(matrix, container_id_list, constellation_size) - else: - constellation_size = int(sys.argv[1]) - GS_num = int(sys.argv[2]) - path = sys.argv[3] - container_id_list = sn_get_container_info() - sn_copy_run_conf_to_each_container(container_id_list, - constellation_size, GS_num, - path) - elif len(sys.argv) == 2: - path = sys.argv[1] - random_list = numpy.loadtxt(path + "/damage_list.txt") - container_id_list = sn_get_container_info() - sn_damage(random_list, container_id_list) - elif len(sys.argv) == 3: - path = sys.argv[1] - sat_loss = float(sys.argv[2]) - damage_list = numpy.loadtxt(path + "/damage_list.txt") - container_id_list = sn_get_container_info() - sn_recover(damage_list, container_id_list, sat_loss) - elif len(sys.argv) == 1: - sn_stop_emulation() + machine_id = 0 + workdir = sys.argv[2] + orbit_num, sat_assign_lst, gs_assign_lst = _get_params( + workdir + '/' + ASSIGN_FILENAME) + if sys.argv[1] == 'nodes': + sn_init_nodes(workdir, orbit_num, sat_assign_lst, gs_assign_lst) + elif sys.argv[1] == 'networks': + sn_init_networks( + _get_delay(workdir + '/1.txt.gz'), sys.argv[3], sys.argv[4], + orbit_num, sat_assign_lst, gs_assign_lst + ) + elif sys.argv[1] == 'routed': + sn_init_route_daemon(sat_assign_lst, workdir + '/bird.conf') + elif sys.argv[1] == 'clean': + sn_clear(workdir) + else: + print('Unknown command') From de25fa6f6bc48031a8f214ed630c2b3b21a58766 Mon Sep 17 00:00:00 2001 From: Xiex <562837197@qq.com> Date: Tue, 21 Nov 2023 01:52:25 +0800 Subject: [PATCH 02/49] refactor: observer and topo calculation. DONT USE! NEED SOME EXTRA WORK change: 1. refactor unnecessary Observer class since we only use it once. Functions with no state may be better. 2. change link representation from matrix to human-readable list. Such sparse structure can also save much memory and disk. 3. use more numpy ndarray operation for better performance(maybe). 4. abstract interfaces for more flexible link style 5. use individual bird.conf instead --- bird.conf | 33 ++ starrynet/sn_observer.py | 694 ++++++++++++--------------------------- 2 files changed, 235 insertions(+), 492 deletions(-) create mode 100644 bird.conf diff --git a/bird.conf b/bird.conf new file mode 100644 index 0000000..3c367ca --- /dev/null +++ b/bird.conf @@ -0,0 +1,33 @@ +log "/var/log/bird.log" { debug, trace, info, remote, warning, error, auth, fatal, bug }; +protocol device { +} +protocol direct { + disabled; # Disable by default + ipv4; # Connect to default IPv4 table + ipv6; # ... and to default IPv6 table +} +protocol kernel { + ipv4 { # Connect protocol to IPv4 table by channel + export all; # Export to protocol. default is export none + }; +} +# protocol static { +# ipv4; # Again, IPv6 channel with default options +# } +protocol ospf{ + ipv4 { + import all; + }; + area 0 { + interface "O*S*-O*S*" { + type broadcast; # Detected by default + cost 256; + hello 10; # Default hello perid 10 is too long + }; + interface "B*-default" { + type broadcast; # Detected by default + cost 256; + hello 10; # Default hello perid 10 is too long + }; + }; +} diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index 3209114..5cbd4c9 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -1,265 +1,174 @@ #encoding: utf-8 -import math +import os +import datetime +import glob +import numpy as np from sgp4.api import Satrec, WGS84 from skyfield.api import load, wgs84, EarthSatellite -from datetime import datetime -import numpy as np -import os - -from starrynet.sn_utils import * - -_ = inf = 999999 # inf - -# To calculate the connection between satellites and GSes in time_in -# fac_num: number of GSes - -class Observer(): - def __init__(self, file_path, configuration_file_path, inclination, - satellite_altitude, orbit_number, sat_number, duration, - antenna_number, GS_lat_long, antenna_inclination, - intra_routing, hello_interval, AS): - self.file_path = file_path - self.configuration_file_path = configuration_file_path - self.inclination = inclination - self.satellite_altitude = satellite_altitude - self.orbit_number = orbit_number - self.sat_number = sat_number - self.duration = duration - self.antenna_number = antenna_number - self.GS_lat_long = GS_lat_long - self.antenna_inclination = antenna_inclination - self.intra_routing = intra_routing - self.hello_interval = hello_interval - self.AS = AS - - def access_P_L_shortest(self, sat_cbf, fac_cbf, fac_num, sat_num, - num_orbits, num_sats_per_orbit, duration, fac_ll, - sat_lla, bound_dis, alpha, antenna_num, path): - delay_matrix = np.zeros((fac_num + sat_num, fac_num + sat_num)) - for cur_time in range(duration): - for i in range(0, fac_num): - access_list = {} - fac_lat = float(fac_ll[i][0]) # latitude - up_lat = fac_lat + alpha # bound - down_lat = fac_lat - alpha - x2 = fac_cbf[i][0] - y2 = fac_cbf[i][1] - z2 = fac_cbf[i][2] - for j in range(0, sat_num): - if sat_lla[cur_time][j][0] >= down_lat and sat_lla[ - cur_time][j][0] <= up_lat: - x1 = sat_cbf[cur_time][j][0] # in km - y1 = sat_cbf[cur_time][j][1] - z1 = sat_cbf[cur_time][j][2] - dist = math.sqrt( - np.square(x1 - x2) + np.square(y1 - y2) + - np.square(z1 - z2)) - if dist < bound_dis: - # [satellite index,distance] - access_list.update({j: dist}) - if len(access_list) > antenna_num: - sorted_access_list = dict( - sorted(access_list.items(), key=lambda item: item[1])) - cnt = 0 - for key, value in sorted_access_list.items(): - cnt = cnt + 1 - if cnt > antenna_num: - break - delay_time = value / (17.31 / 29.5 * - 299792.458) * 1000 # ms - delay_matrix[sat_num + i][key] = delay_time - delay_matrix[key][sat_num + i] = delay_time - elif len(access_list) != 0: - for key, value in access_list.items(): - delay_time = value / (17.31 / 29.5 * - 299792.458) * 1000 # ms - delay_matrix[sat_num + i][key] = delay_time - delay_matrix[key][sat_num + i] = delay_time - for i in range(num_orbits): - for j in range(num_sats_per_orbit): - num_sat1 = i * num_sats_per_orbit + j - x1 = sat_cbf[cur_time][num_sat1][0] # km - y1 = sat_cbf[cur_time][num_sat1][1] - z1 = sat_cbf[cur_time][num_sat1][2] - num_sat2 = i * num_sats_per_orbit + ( - j + 1) % num_sats_per_orbit - x2 = sat_cbf[cur_time][num_sat2][0] # km - y2 = sat_cbf[cur_time][num_sat2][1] - z2 = sat_cbf[cur_time][num_sat2][2] - num_sat3 = ((i + 1) % num_orbits) * num_sats_per_orbit + j - x3 = sat_cbf[cur_time][num_sat3][0] # km - y3 = sat_cbf[cur_time][num_sat3][1] - z3 = sat_cbf[cur_time][num_sat3][2] - delay1 = math.sqrt( - np.square(x1 - x2) + np.square(y1 - y2) + - np.square(z1 - z2)) / (17.31 / 29.5 * - 299792.458) * 1000 # ms - delay2 = math.sqrt( - np.square(x1 - x3) + np.square(y1 - y3) + - np.square(z1 - z3)) / (17.31 / 29.5 * - 299792.458) * 1000 # ms - delay_matrix[num_sat1][num_sat2] = delay1 - delay_matrix[num_sat2][num_sat1] = delay1 - delay_matrix[num_sat1][num_sat3] = delay2 - delay_matrix[num_sat3][num_sat1] = delay2 - np.savetxt(path + "/delay/" + str(cur_time + 1) + ".txt", - delay_matrix, - fmt='%.2f', - delimiter=',') - for i in range(len(delay_matrix)): - delay_matrix[i, ...] = 0 - - def to_cbf(self, lat_long, - length): # the xyz coordinate system. length: number of nodes - cbf = [] - radius = 6371 - for num in range(0, length): - cbf_in = [] - z = radius * math.sin(math.radians(float(lat_long[num][0]))) - x = radius * math.cos(math.radians(float( - lat_long[num][0]))) * math.cos( - math.radians(float(lat_long[num][1]))) - y = radius * math.cos(math.radians(float( - lat_long[num][0]))) * math.sin( - math.radians(float(lat_long[num][1]))) - cbf_in.append(x) - cbf_in.append(y) - cbf_in.append(z) - cbf.append(cbf_in) - return cbf # xyz coordinates of all the satellites - - def calculate_bound(self, inclination_angle, height): - bound_distance = 6371 * math.cos( - (90 + inclination_angle) / 180 * math.pi) + math.sqrt( - math.pow( - 6371 * math.cos( - (90 + inclination_angle) / 180 * math.pi), 2) + - math.pow(height, 2) + 2 * height * 6371) - return bound_distance - - def matrix_to_change(self, duration, orbit_number, sat_number, path, - GS_lat_long): - no_fac = len(GS_lat_long) - no_geo = 0 - duration = duration - 1 - no_leo = orbit_number * sat_number - - topo_duration = [[[0 for i in range(no_leo + no_geo + no_fac)] - for i in range(no_leo + no_geo + no_fac)] - for k in range(duration)] - for time in range(1, duration + 1): - topo_path = path + '/delay/' + str(time) + ".txt" - adjacency_matrix = sn_get_param(topo_path) - for i in range(len(adjacency_matrix)): - for j in range(len(adjacency_matrix[i])): - if float(adjacency_matrix[i][j]) > 0: - adjacency_matrix[i][j] = 1 +def _isl_grid(sat_cbf_t_shell): + # [[[[ [isl] for every satellite] for every orbit] for every t] for every shell] + isls_t_shell = [] + isl_offset = 0 + for sat_cbf_t in sat_cbf_t_shell: + orbit_num, sat_num = sat_cbf_t.shape[1], sat_cbf_t.shape[2] + down_cbf_t = np.roll(sat_cbf_t, -1, 2) + right_cbf_t = np.roll(sat_cbf_t, -1, 1) + delay_down_t = np.sqrt(np.sum(np.square(sat_cbf_t - down_cbf_t), -1)) / ( + 17.31 / 29.5 * 299792.458) * 1000 # ms + delay_right_t = np.sqrt(np.sum(np.square(sat_cbf_t - right_cbf_t), -1)) / ( + 17.31 / 29.5 * 299792.458) * 1000 # ms + isls_t = [] + for delay_down, delay_right in zip(delay_down_t, delay_right_t): + orbit_lst = [] + idx = isl_offset + for oid in range(orbit_num): + sat_lst = [] + for sid in range(sat_num): + sat_lst.append([ + # (isl_idx, orbit_id, sat_id, delay in ms) + # down isl + (idx, oid, sid + 1 if sid + 1 < sat_num else 0, + delay_down[oid, sid]), + # right isl + (idx + 1, oid + 1 if oid + 1 < orbit_num else 0, sid, + delay_right[oid, sid]), + ]) + idx += 2 + orbit_lst.append(sat_lst) + isls_t.append(orbit_lst) + isl_offset += orbit_num * sat_num * 2 + isls_t_shell.append(isls_t) + return isls_t_shell + +def _gsl_least_delay(sat_cbf_t_shell, gs_cbf, antenna_num, bound_dis): + gsls_t_shell = [] # [[[ [gsl] for every gs] for every ts] for every shell] + for sat_cbf_t in sat_cbf_t_shell: + gsls_t = [] + for sat_cbf in sat_cbf_t: + orbit_num, sat_num = sat_cbf.shape[0], sat_cbf.shape[1] + # (gs_num) op (orbit_num, sat_num) -> (gs_num, orbit_num, sat_num) + dx = np.subtract.outer(gs_cbf[..., 0], sat_cbf[..., 0]) + dy = np.subtract.outer(gs_cbf[..., 1], sat_cbf[..., 1]) + dz = np.subtract.outer(gs_cbf[..., 2], sat_cbf[..., 2]) + dist = np.sqrt(np.square(dx) + np.square(dy) + np.square(dz)) + gsls = [] + for gs_dist in dist: + gs_dist = gs_dist.flatten() + bound_mask = gs_dist < bound_dis + sat_indices = np.arange(len(gs_dist))[bound_mask] + gs_dist = gs_dist[bound_mask] + sorted_sat = gs_dist.argsort() + gsls.append([ + # (orbit_id, sat_id, delay in ms) + (sat_indices[sat] // sat_num, sat_indices[sat] % sat_num, + gs_dist[sat] / (17.31 / 29.5 * 299792.458) * 1000) + for sat in sorted_sat[:antenna_num] + ]) + gsls_t.append(gsls) + gsls_t_shell.append(gsls_t) + + # [[gsl for every shell] for every gs] for every t] TODO: better method + gsl_shell_gs_t = [ + [[] for gid in range(len(gs_cbf))] for t in range(len(gsls_t_shell[0])) + ] + gsl_idx_dict = {} + for t, gsl_shell_gs in enumerate(gsl_shell_gs_t): + for gid, gsl_lst in enumerate(gsl_shell_gs): + for shell_id in range(len(gsls_t_shell)): + for oid, sid, delay in gsls_t_shell[shell_id][t][gid]: + if (gid, shell_id, oid, sid) in gsl_idx_dict: + idx = gsl_idx_dict[(gid, shell_id, oid, sid)] else: - adjacency_matrix[i][j] = 0 - topo_duration[time - 1] = adjacency_matrix - - changetime = [] - Duration = [] - for i in range(duration - 1): - l1 = topo_duration[i] - l2 = topo_duration[i + 1] - if l1 == l2: - continue - else: - changetime.append(i) - pretime = 0 - for item in changetime: - Duration.append(item - pretime) - pretime = item - Duration.append(60) - - topo_leo_change_path = path + "/Topo_leo_change.txt" - f = open(topo_leo_change_path, "w") - cnt = 1 - for i in range(duration - 1): - pre_lines = topo_duration[i] - now_lines = topo_duration[i + 1] - if pre_lines == now_lines: - continue - else: - f.write("time " + str(i + 2) + ":\n") # time started from 1 - f.write('duration ' + str(Duration[cnt]) + ":\n") - cnt += 1 - f.write("add:\n") - for j in range(no_fac): - prelines = pre_lines[no_geo + no_leo + j] - nowlines = now_lines[no_geo + no_leo + j] - for k in range(no_geo + no_leo + no_fac): - if prelines[k] == 0 and nowlines[k] == 1: - f.write( - str(k + 1) + "-" + str(no_leo + j + 1) + - "\n") # index - f.write("del:\n") - for j in range(no_fac): - prelines = pre_lines[no_geo + no_leo + j] - nowlines = now_lines[no_geo + no_leo + j] - for k in range(no_geo + no_leo + no_fac): - if prelines[k] == 1 and nowlines[k] == 0: - f.write( - str(k + 1) + "-" + str(no_leo + j + 1) + - "\n") # index - f.write("time " + str(self.duration) + ":\n") # - f.write("end of the emulation! \n") # - f.close() - cnt = 1 - - def calculate_delay(self): - path = self.configuration_file_path + "/" + self.file_path - sat_cbf = [ - ] # first dimension: time. second dimension: node. third dimension: xyz - sat_lla = [ - ] # first dimension: time. second dimension: node. third dimension: lla - fac_cbf = [] # first dimension: node. second dimension: xyz - - if os.path.exists(path + '/delay') == True: - osstr = "rm -f " + path + "/delay/*" - os.system(osstr) - else: - os.system("mkdir " + path) - os.system("mkdir " + path + "/delay") - if os.path.exists(path + '/position') == True: - osstr = "rm -f " + path + "/position/*" - os.system(osstr) - else: - os.system("mkdir " + path + "/position") - - ts = load.timescale() - since = datetime(1949, 12, 31, 0, 0, 0) - start = datetime(2020, 1, 1, 0, 0, 0) - epoch = (start - since).days - inclination = self.inclination * 2 * np.pi / 360 - GM = 3.9860044e14 - R = 6371393 - altitude = self.satellite_altitude * 1000 + idx = len(gsl_idx_dict) + gsl_idx_dict[(gid, shell_id, oid, sid)] = idx + gsl_lst.append((idx, shell_id, oid, sid, delay)) + return gsl_shell_gs_t + +#TODO: More ISL styles +isl_styles = { + 'Grid': _isl_grid, +} +#TODO: More GSL styles +gsl_styles = { + 'LeastDelay':_gsl_least_delay, +} + +def _to_cbf(lat_long):# the xyz coordinate system. + radius = 6371 + if lat_long.shape[-1] > 2: + radius += lat_long[..., 2] + theta_mat = np.radians(lat_long[..., 0]) + phi_mat = np.radians(lat_long[..., 1]) + z_mat = radius * np.sin(theta_mat) + rho_mat = radius * np.cos(theta_mat) + x_mat = rho_mat * np.cos(phi_mat) + y_mat = rho_mat * np.sin(phi_mat) + return np.stack((x_mat, y_mat, z_mat), -1) + +def _bound_gsl(antenna_elevation, altitude): + a = 6371 * np.cos(np.radians(90 + antenna_elevation)) + return a + np.sqrt(np.square(a) + np.square(altitude) + 2 * altitude * 6371) + +def calculate_delay( + dir, duration, shell_lst, isl_style, + GS_lat_long, antenna_number, antenna_elevation, gsl_style + ): + cached = True + for shell in shell_lst: + pos_dir = os.path.join(dir, shell['name'], 'position') + isl_dir = os.path.join(dir, shell['name'], 'isl') + os.makedirs(pos_dir, exist_ok=True) + os.makedirs(isl_dir, exist_ok=True) + pos_files = glob.glob(os.path.join(pos_dir, '*.txt')) + isl_files = glob.glob(os.path.join(isl_dir, '*.txt')) + if len(pos_files) != duration or len(isl_files) != duration: + cached = False + gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') + os.makedirs(gsl_dir, exist_ok=True) + gsl_files = glob.glob(os.path.join(gsl_dir, '*.txt')) + if len(gsl_files) != duration: + cached = False + if cached and input(f"Use cached local files [y/n]?").strip().lower()[:1] == 'y': + return + + for shell in shell_lst: + pos_files = glob.glob(os.path.join(dir, shell['name'], 'position', '*.txt')) + isl_files = glob.glob(os.path.join(dir, shell['name'], 'isl', '*.txt')) + for file in pos_files: + os.remove(file) + for file in isl_files: + os.remove(file) + for file in gsl_files: + os.remove(file) + + ts = load.timescale() + since = datetime.datetime(1949, 12, 31, 0, 0, 0) + start = datetime.datetime(2020, 1, 1, 0, 0, 0) + epoch = (start - since).days + GM = 3.9860044e14 + R = 6371393 + F = 18 + sat_cbf_t_shell = [] + for i, shell in enumerate(shell_lst): + inclination = shell['inclination'] * 2 * np.pi / 360 + altitude = shell['altitude'] * 1000 mean_motion = np.sqrt(GM / (R + altitude)**3) * 60 - num_of_orbit = self.orbit_number - sat_per_orbit = self.sat_number - num_of_sat = num_of_orbit * sat_per_orbit - F = 18 - bound_dis = self.calculate_bound( - self.antenna_inclination, self.satellite_altitude) * 29.5 / 17.31 - - duration = self.duration # second - result = [[] for i in range(duration)] # LLA result - lla_per_sec = [[] for i in range(duration)] # LLA result - - for i in range(num_of_orbit): # range(num_of_orbit) - raan = i / num_of_orbit * 2 * np.pi - for j in range(sat_per_orbit): # range(sat_per_orbit) - mean_anomaly = (j * 360 / sat_per_orbit + i * 360 * F / + orbit_number, sat_number = shell['orbit'], shell['sat'] + num_of_sat = orbit_number * sat_number + + sat_lla_t = np.zeros((duration, orbit_number, sat_number, 3)) + for oid in range(orbit_number): + raan = oid / orbit_number * 2 * np.pi + for sid in range(sat_number): + mean_anomaly = (sid * 360 / sat_number + oid * 360 * F / num_of_sat) % 360 * 2 * np.pi / 360 satrec = Satrec() satrec.sgp4init( WGS84, # gravity model 'i', # 'a' = old AFSPC mode, 'i' = improved mode - i * sat_per_orbit + j, # satnum: Satellite number + oid * sat_number + sid, # satnum: Satellite number epoch, # epoch: days since 1949 December 31 00:00 UT 2.8098e-05, # bstar: drag coefficient (/earth radii) 6.969196665e-13, # ndot: ballistic coefficient (revs/day) @@ -272,251 +181,52 @@ def calculate_delay(self): raan, # nodeo: right ascension of ascending node (radians) ) sat = EarthSatellite.from_satrec(satrec, ts) - cur = datetime(2022, 1, 1, 1, 0, 0) + cur = datetime.datetime(2022, 1, 1, 1, 0, 0) t_ts = ts.utc(*cur.timetuple()[:5], - range(duration)) # [:4]:minute,[:5]:second + range(duration)) # [:4]:minute,[:5]:second geocentric = sat.at(t_ts) subpoint = wgs84.subpoint(geocentric) # list: [subpoint.latitude.degrees] [subpoint.longitude.degrees] [subpoint.elevation.km] for t in range(duration): - lla = '%f,%f,%f\n' % (subpoint.latitude.degrees[t], - subpoint.longitude.degrees[t], - subpoint.elevation.km[t]) - result[t].append(lla) - lla = [] - lla.append(subpoint.latitude.degrees[t]) - lla.append(subpoint.longitude.degrees[t]) - lla.append(subpoint.elevation.km[t]) - lla_per_sec[t].append(lla) - + sat_lla_t[t, oid, sid] = (subpoint.latitude.degrees[t], + subpoint.longitude.degrees[t], + subpoint.elevation.km[t]) + pos_dir = os.path.join(dir, shell['name'], 'position') + for t, sat_lla in enumerate(sat_lla_t): + f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') + for sat_lst in sat_lla: + for sat in sat_lst: + f.write('%f,%f,%f\n' % (sat[0], sat[1], sat[2])) + f.write('\n') + f.close() + sat_cbf_t_shell.append(_to_cbf(sat_lla_t)) + + isls_t_shell = isl_styles[isl_style](sat_cbf_t_shell) + for i, isls_t in enumerate(isls_t_shell): + isl_dir = os.path.join(dir, shell_lst[i]['name'], 'isl') for t in range(duration): - file = path + '/position/' + '%d.txt' % t - with open(file, 'w') as fw: - fw.writelines(result[t]) - cbf_per_sec = self.to_cbf(lla_per_sec[t], num_of_sat) - sat_cbf.append(cbf_per_sec) - sat_lla.append(lla_per_sec[t]) - - if len(self.GS_lat_long) != 0: - fac_cbf = self.to_cbf(self.GS_lat_long, len(self.GS_lat_long)) - - alpha = np.degrees( - np.arccos(6371 / (6371 + self.satellite_altitude) * - np.cos(np.radians(inclination)))) - inclination - self.access_P_L_shortest(sat_cbf, fac_cbf, len(self.GS_lat_long), - self.sat_number * self.orbit_number, - self.orbit_number, self.sat_number, - self.duration, self.GS_lat_long, sat_lla, - bound_dis, alpha, self.antenna_number, path) - self.matrix_to_change(self.duration, self.orbit_number, - self.sat_number, path, self.GS_lat_long) - - def compute_conf(self, sat_node_number, interval, num1, num2, ID, Q, - num_backbone, matrix): - Q.append( - "log \"/var/log/bird.log\" { debug, trace, info, remote, warning, error, auth, fatal, bug };" - ) - Q.append("debug protocols all;") - Q.append("protocol device {") - Q.append("}") - Q.append(" protocol direct {") - Q.append(" disabled; # Disable by default") - Q.append(" ipv4; # Connect to default IPv4 table") - Q.append(" ipv6; # ... and to default IPv6 table") - Q.append("}") - Q.append("protocol kernel {") - Q.append(" ipv4 { # Connect protocol to IPv4 table by channel") - Q.append( - " export all; # Export to protocol. default is export none") - Q.append(" };") - Q.append("}") - # Q.append("protocol kernel {") - # Q.append(" ipv6 { export all; ") - # Q.append(" };") - # Q.append("}") - Q.append("protocol static {") - Q.append(" ipv4; # Again, IPv6 channel with default options") - Q.append("}") - Q.append("protocol ospf{") - Q.append(" ipv4 {") - Q.append(" import all;") - Q.append(" };") - Q.append(" area 0 {") - Q.append(" interface \"B%d-eth0\" {" % ID) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - Q.append(" interface \"inter_machine\" {") - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - if num1 <= sat_node_number and num2 <= num_backbone and ID <= sat_node_number: # satellite - for peer in range(num1, num2 + 1): - if (peer == ID) or (int(float(matrix[ID - 1][peer - 1])) == 0): - continue - Q.append(" interface \"B%d-eth%d\" {" % (ID, peer)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - if num2 > sat_node_number: - for i in range(sat_node_number + 1, - num_backbone + 1): # each ground station - Q.append(" interface \"B%d-eth%d\" {" % (ID, i)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - elif num1 <= sat_node_number and num2 <= num_backbone and ID > sat_node_number: # ground station - for peer in range(1, - 1 + sat_node_number): # fac and each satellite - Q.append(" interface \"B%d-eth%d\" {" % (ID, peer)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - Q.append(" interface \"B%d-default\" {" % (ID)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - elif num1 > num_backbone and num2 > num_backbone: # ground users - if ID != num1 and ID != num2: - Q.append(" interface \"B%d-eth%d\" {" % (ID, ID - 1)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - Q.append(" interface \"B%d-eth%d\" {" % (ID, ID + 1)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - elif ID == num1: - Q.append(" interface \"B%d-eth%d\" {" % (ID, ID + 1)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - elif ID == num2: - Q.append(" interface \"B%d-eth%d\" {" % (ID, ID - 1)) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - else: - return False - Q.append(" };") - Q.append(" }") - return True - - def print_conf(self, sat_node_number, fac_node_number, ID, Q, remote_ftp): - filename = self.file_path + "/conf/bird-" + \ - str(sat_node_number) + "-" + str(fac_node_number) + "/B%d.conf" % ID - fout = open(self.configuration_file_path + "/" + filename, 'w+') - for item in Q: - fout.write(str(item) + "\n") - fout.close() - remote_ftp.put(self.configuration_file_path + "/" + filename, filename) - - def generate_conf(self, remote_ssh, remote_ftp): - if self.intra_routing != "OSPF" and self.intra_routing != "ospf": - return False - if os.path.exists(self.configuration_file_path + "/" + self.file_path + - "/conf/bird-" + - str(self.orbit_number * self.sat_number) + "-" + - str(len(self.GS_lat_long))) == True: - osstr = "rm -f " + self.configuration_file_path+"/"+self.file_path+"/conf/bird-" + \ - str(self.orbit_number*self.sat_number) + "-" + str(len(self.GS_lat_long)) + "/*" - os.system(osstr) - sn_remote_cmd(remote_ssh, "mkdir ~/" + self.file_path + "/conf") - sn_remote_cmd( - remote_ssh, "mkdir ~/" + self.file_path + "/conf/bird-" + - str(self.orbit_number * self.sat_number) + "-" + - str(len(self.GS_lat_long))) - else: - os.makedirs(self.configuration_file_path + "/" + self.file_path + - "/conf/bird-" + - str(self.orbit_number * self.sat_number) + "-" + - str(len(self.GS_lat_long))) - sn_remote_cmd(remote_ssh, "mkdir ~/" + self.file_path + "/conf") - sn_remote_cmd( - remote_ssh, "mkdir ~/" + self.file_path + "/conf/bird-" + - str(self.orbit_number * self.sat_number) + "-" + - str(len(self.GS_lat_long))) - path = self.configuration_file_path + "/" + self.file_path + "/delay/1.txt" - matrix = sn_get_param(path) - num_backbone = self.orbit_number * self.sat_number + len( - self.GS_lat_long) - error = True - for i in range(len(self.AS)): - if len(self.AS[i]) != 1: - for ID in range(self.AS[i][0], self.AS[i][1] + 1): - Q = [] - error = self.compute_conf( - self.orbit_number * self.sat_number, - self.hello_interval, self.AS[i][0], self.AS[i][1], ID, - Q, num_backbone, matrix) - self.print_conf(self.orbit_number * self.sat_number, - len(self.GS_lat_long), ID, Q, remote_ftp) - else: # one node in one AS - ID = self.AS[i][0] - Q = [] - Q.append( - "log \"/var/log/bird.log\" { debug, trace, info, remote, warning, error, auth, fatal, bug };" - ) - Q.append("debug protocols all;") - Q.append("protocol device {") - Q.append("}") - Q.append(" protocol direct {") - Q.append(" disabled; # Disable by default") - Q.append(" ipv4; # Connect to default IPv4 table") - Q.append(" ipv6; # ... and to default IPv6 table") - Q.append("}") - Q.append("protocol kernel {") - Q.append( - " ipv4 { # Connect protocol to IPv4 table by channel") - Q.append( - " export all; # Export to protocol. default is export none" - ) - Q.append(" };") - Q.append("}") - Q.append("protocol static {") - Q.append( - " ipv4; # Again, IPv6 channel with default options") - Q.append("}") - Q.append("protocol ospf {") - Q.append(" ipv4 {") - Q.append(" import all;") - Q.append(" };") - Q.append(" area 0 {") - Q.append(" interface \"B%d-eth0\" {" % ID) - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(self.hello_interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - Q.append(" interface \"inter_machine\" {") - Q.append(" type broadcast; # Detected by default") - Q.append(" cost 256;") - Q.append(" hello " + str(interval) + - "; # Default hello perid 10 is too long") - Q.append(" };") - Q.append(" };") - Q.append(" }") - self.print_conf(self.orbit_number * self.sat_number, - len(self.GS_lat_long), ID, Q, remote_ftp) - - return error + f = open(f"{isl_dir}/{t + 1}.txt", 'w') + for oid, sat_lst in enumerate(isls_t[t]): + for sid, isl_lst in enumerate(sat_lst): + # one line for each satellite + f.write(f"{oid},{sid}: ") + f.write(' '.join( + f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}"for isl in isl_lst + )) + f.write('\n') + f.write('\n') + f.close() + + gs_cbf = _to_cbf(np.array(GS_lat_long)) + bound_dis = _bound_gsl(antenna_elevation, shell['altitude']) + gsls_t = gsl_styles[gsl_style](sat_cbf_t_shell, gs_cbf, antenna_number, bound_dis) + for t, gsls in enumerate(gsls_t): + f = open(f"{gsl_dir}/{t + 1}.txt", 'w') + for gid, gsl_lst in enumerate(gsls): + # one line for each ground station + f.write(f"{gid}: ") + f.write(' '.join( + f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" for gsl in gsl_lst + )) + f.write('\n') + f.close() From 8e29deefc7e2cc6e83c4f8c93d377b61140867e5 Mon Sep 17 00:00:00 2001 From: Xiex <562837197@qq.com> Date: Tue, 21 Nov 2023 02:08:58 +0800 Subject: [PATCH 03/49] feat: multishell and multimachine DONT USE! NEED SOME EXTRA WORK --- config.json | 61 ++- starrynet/sn_synchronizer.py | 453 ++++++++++++------- starrynet/sn_utils.py | 816 +++++------------------------------ 3 files changed, 462 insertions(+), 868 deletions(-) diff --git a/config.json b/config.json index 43374a6..34e8750 100644 --- a/config.json +++ b/config.json @@ -1,28 +1,57 @@ { "Name": "starlink", - "Altitude (km)": 550, - "Cycle (s)": 5731, - "Inclination": 53, - "Phase shift": 1, - "# of orbit": 5, - "# of satellites": 5, + "Shells": [ + { + "Altitude (km)": 550, + "Inclination": 53, + "Orbits": 72, + "Satellites per orbit": 22, + "Phase shift": 1 + }, + { + "Altitude (km)": 540, + "Inclination": 53.2, + "Orbits": 72, + "Satellites per orbit": 22, + "Phase shift": 1 + }, + { + "Altitude (km)": 550, + "Inclination": 70, + "Orbits": 36, + "Satellites per orbit": 20, + "Phase shift": 1 + }, + { + "Altitude (km)": 550, + "Inclination": 97.6, + "Orbits": 6, + "Satellites per orbit": 58, + "Phase shift": 1 + }, + { + "Altitude (km)": 550, + "Inclination": 97.6, + "Orbits": 4, + "Satellites per orbit": 43, + "Phase shift": 1 + } + ], "Duration (s)": 100, "update_time (s)": 10, "satellite link bandwidth (\"X\" Gbps)": 5, "sat-ground bandwidth (\"X\" Gbps)": 5, - "satellite link loss (\"X\"% )": 1, + "satellite link loss (\"X\"% )": 0, "sat-ground loss (\"X\"% )": 1, - "GS number": 2, "antenna number": 1, - "antenna_inclination_angle": 25, - "remote_machine_IP": "101.6.21.2", - "remote_machine_username": "root", - "remote_machine_password": "123456", - "Satellite link": "grid", + "antenna elevation angle": 25, + "Satellite link": "Grid", "IP version": "IPv4", - "Intra-AS routing": "OSPF", - "Inter-AS routing": "BGP", "Link policy": "LeastDelay", "Handover policy": "instant handover", - "multi-machine (\"0\" for no, \"1\" for yes)": 0 + "Machines": [ + {"IP":"101.6.21.1", "port":22, "username":"root", "password":"123456"}, + {"IP":"101.6.21.2", "port":22, "username":"root", "password":"123456"}, + {"IP":"101.6.21.3", "port":22, "username":"root", "password":"123456"} + ] } \ No newline at end of file diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 0a8a711..d536017 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -4,80 +4,111 @@ StarryNet: empowering researchers to evaluate futuristic integrated space and terrestrial networks. author: Zeqi Lai (zeqilai@tsinghua.edu.cn) and Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) """ +import time +import threading from starrynet.sn_observer import * from starrynet.sn_utils import * +ASSIGN_FILENAME = 'assign.txt' + +class RemoteMachine: + + def __init__(self, id, host, port, username, password, + shell_lst, experiment_name, local_dir, gs_dirname): + self.id = id + self.shell_lst = shell_lst + self.local_dir = local_dir + self.gs_dirname = gs_dirname + self.ssh, self.sftp = sn_connect_remote( + host = host, + port = port, + username = username, + password = password, + ) + sn_remote_cmd(self.ssh, f'export MACHINE_ID={self.id}') + sn_remote_cmd(self.ssh, 'mkdir ~/' + experiment_name) + self.dir = sn_remote_cmd(self.ssh, 'echo ~/' + experiment_name) + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'orchestrater.py'), + self.dir + '/orchestrater.py' + ) + + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'pyctr.so'), + self.dir + '/pyctr.so' + ) + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'setup.py'), + self.dir + '/setup.py' + ) + self.sftp.put( + os.path.join(self.local_dir, ASSIGN_FILENAME), + self.dir + '/' + ASSIGN_FILENAME + ) + + def init_nodes(self): + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/orchestrater.py {self.id} nodes {self.dir}" + ) + + def create_network(self, bw, loss): + for shell in self.shell_lst: + self.sftp.put( + os.path.join(self.local_dir, shell['name'], 'isl', '1.txt'), + f"{self.dir}/{shell['name']}/1.txt" + ) + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/orchestrater.py isls {self.dir}/{shell['name']} " + f"1.txt {bw} {loss}" + ) + self.sftp.put( + os.path.join(self.local_dir, self.gs_dirname, 'GS', '1.txt'), + f"{self.dir}/{shell['name']}/1.txt" + ) + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/orchestrater.py gsls {self.dir}/{self.gs_dirname} " + ) class StarryNet(): - def __init__(self, - configuration_file_path, - GS_lat_long, - hello_interval=10, - AS=[]): + def __init__(self, configuration_file_path, GS_lat_long): # Initialize constellation information. sn_args = sn_load_file(configuration_file_path, GS_lat_long) - self.name = sn_args.cons_name - self.satellite_altitude = sn_args.satellite_altitude - self.inclination = sn_args.inclination - self.orbit_number = sn_args.orbit_number - self.sat_number = sn_args.sat_number - self.fac_num = sn_args.fac_num - self.constellation_size = self.orbit_number * self.sat_number - self.node_size = self.orbit_number * self.sat_number + sn_args.fac_num + self.shell_lst = sn_args.shell_lst + self.gs_lat_long = GS_lat_long self.link_style = sn_args.link_style - self.IP_version = sn_args.IP_version self.link_policy = sn_args.link_policy + self.IP_version = sn_args.IP_version self.update_interval = sn_args.update_interval self.duration = sn_args.duration - self.inter_routing = sn_args.inter_routing - self.intra_routing = sn_args.intra_routing - self.cycle = sn_args.cycle - self.time_slot = sn_args.time_slot self.sat_bandwidth = sn_args.sat_bandwidth self.sat_ground_bandwidth = sn_args.sat_ground_bandwidth self.sat_loss = sn_args.sat_loss self.sat_ground_loss = sn_args.sat_ground_loss - self.ground_num = sn_args.ground_num - self.multi_machine = sn_args.multi_machine self.antenna_number = sn_args.antenna_number - self.antenna_inclination = sn_args.antenna_inclination - self.container_global_idx = 1 - self.hello_interval = hello_interval - self.AS = AS - self.configuration_file_path = os.path.dirname( + self.elevation = sn_args.antenna_elevation + self.configuration_dir = os.path.dirname( os.path.abspath(configuration_file_path)) - self.file_path = './' + sn_args.cons_name + '-' + str( - sn_args.orbit_number) + '-' + str(sn_args.sat_number) + '-' + str( - sn_args.satellite_altitude) + '-' + str( - sn_args.inclination - ) + '-' + sn_args.link_style + '-' + sn_args.link_policy - self.observer = Observer(self.file_path, self.configuration_file_path, - self.inclination, self.satellite_altitude, - self.orbit_number, self.sat_number, - self.duration, self.antenna_number, - GS_lat_long, self.antenna_inclination, - self.intra_routing, self.hello_interval, - self.AS) - self.docker_service_name = 'constellation-test' - self.isl_idx = 0 - self.ISL_hub = 'ISL_hub' - self.container_id_list = [] - self.n_container = 0 - # Get ssh handler. - self.remote_ssh, self.transport = sn_init_remote_machine( - sn_args.remote_machine_IP, sn_args.remote_machine_username, - sn_args.remote_machine_password) - if self.remote_ssh is None: - print('Remote SSH login failure.') - return - if self.transport is None: - print('Remote transport login failure.') - return - self.remote_ftp = sn_init_remote_ftp(self.transport) - if self.remote_ftp is None: - print('Remote ftp login failure.') - return + self.experiment_name = sn_args.cons_name\ + +'-'+ sn_args.link_style +'-'+ sn_args.link_policy + self.gs_dirname = 'GS-' + str(len(self.gs_lat_long)) + for shell_id, shell in enumerate(self.shell_lst): + shell['name'] = f"{shell_id}_{shell['altitude']}-{shell['inclination']}"\ + f"-{shell['orbit']}-{shell['sat']}"\ + f"-{shell['phase_shift']}" + + self.local_dir = os.path.join(self.configuration_dir, self.experiment_name) + self._init_local() + # Initiate a necessary delay and position data for emulation + calculate_delay( + self.local_dir, self.duration, self.shell_lst, self.link_style, + self.gs_lat_long, self.antenna_number, self.elevation, self.link_policy + ) + self.remote_lst = self._assign_remote(sn_args.machine_lst) + self.utility_checking_time = [] self.ping_src = [] self.ping_des = [] @@ -95,99 +126,108 @@ def __init__(self, self.recovery_time = [] self.route_src = [] self.route_time = [] + + def _init_local(self): + for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): + os.remove(txt_file) + for shell in self.shell_lst: + os.makedirs(os.path.join(self.local_dir, shell['name']), exist_ok=True) + os.makedirs(os.path.join(self.local_dir, self.gs_dirname), exist_ok=True) - # Initiate a working directory - sn_thread = sn_init_directory_thread(self.file_path, - self.configuration_file_path, - self.remote_ssh) - sn_thread.start() - sn_thread.join() - # Initiate a necessary delay and position data for emulation - self.observer.calculate_delay() - # Generate configuration file for routing - self.observer.generate_conf(self.remote_ssh, self.remote_ftp) + def _assign_remote(self, machine_lst): + # TODO: better partition + remote_lst = [] + if len(self.shell_lst) * 2 <= len(machine_lst): + # need intra-shell partition + machine_per_shell = len(machine_lst) // len(self.shell_lst) + raise NotImplementedError + else: + # only divide shell + shell_per_machine = len(self.shell_lst) // len(machine_lst) + remainder = len(self.shell_lst) % len(machine_lst) + shell_idx = 0 + sat_mask_lst = [] + for i, remote in enumerate(machine_lst): + shell_num = shell_per_machine + if i < remainder: + shell_num += 1 + assigned_shells = [ + self.shell_lst[j] for j in range(shell_idx, shell_idx + shell_num) + ] + # all satellites of a shell assigned to a single machine + sat_mask_lst.extend([ + (i,) * shell['sat'] for shell in assigned_shells + ]) + shell_idx += shell_num + with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: + # TODO: ground station + f.write(' '.join(str(mid) for mid in []) + '\n') + # every shell + for sat_mask, shell in zip(sat_mask_lst, self.shell_lst): + f.write( + str(shell['orbit']) + ' ' + shell['name'] + '\n' + + ' '.join(str(mid) for mid in sat_mask) + '\n' + ) + for i, remote in enumerate(machine_lst): + remote_lst.append(RemoteMachine( + i, + remote['IP'], + remote['port'], + remote['username'], + remote['password'], + self.shell_lst, + self.experiment_name, + self.local_dir, + self.gs_dirname + ) + ) + return remote_lst def create_nodes(self): - # Initialize each machine in multiple threads. - sn_thread = sn_Node_Init_Thread(self.remote_ssh, - self.docker_service_name, - self.node_size, self.container_id_list, - self.container_global_idx) - sn_thread.start() - sn_thread.join() - self.container_id_list = sn_get_container_info(self.remote_ssh) - print("Constellation initialization done. " + - str(len(self.container_id_list)) + " have been created.") + print('Initializing nodes ...') + begin = time.time() + for remote in self.remote_lst: + remote.init_nodes() + print("Node initialization:", time.time() - begin, "s consumed.") def create_links(self): - print("Create Links.") - isl_thread = sn_Link_Init_Thread( - self.remote_ssh, self.remote_ftp, self.orbit_number, - self.sat_number, self.constellation_size, self.fac_num, - self.file_path, self.configuration_file_path, self.sat_bandwidth, - self.sat_ground_bandwidth, self.sat_loss, self.sat_ground_loss) - isl_thread.start() - isl_thread.join() - print("Link initialization done.") + print('Initializing links ...') + begin = time.time() + for remote in self.remote_lst: + remote.create_network() + print("Link initialization:", time.time() - begin, 's consumed.') def run_routing_deamon(self): - routing_thread = sn_Routing_Init_Thread( - self.remote_ssh, self.remote_ftp, self.orbit_number, - self.sat_number, self.constellation_size, self.fac_num, - self.file_path, self.sat_bandwidth, self.sat_ground_bandwidth, - self.sat_loss, self.sat_ground_loss) - routing_thread.start() - routing_thread.join() - print("Bird routing in all containers are running.") + print('Initializing routing ...') + sn_remote_wait_output(self.remote_ssh, + f"python3 {self.remote_dir}/orchestrater.py routed {self.remote_dir} ") + print("Routing daemon initialized. Wait 30s for route converged") + for i in range(30): + print(f'\r{i} / 30', end=' ') + time.sleep(1) + print("Routing started!") def get_distance(self, sat1_index, sat2_index, time_index): - delaypath = self.configuration_file_path + "/" + self.file_path + '/delay/' + str( - time_index) + '.txt' - adjacency_matrix = sn_get_param(delaypath) - delay = float(adjacency_matrix[sat1_index - 1][sat2_index - 1]) - dis = delay * (17.31 / 29.5 * 299792.458) / 1000 # km - return dis + raise NotImplementedError def get_neighbors(self, sat_index, time_index): - neighbors = [] - delaypath = self.configuration_file_path + "/" + self.file_path + '/delay/' + str( - time_index) + '.txt' - adjacency_matrix = sn_get_param(delaypath) - sats = self.orbit_number * self.sat_number - for i in range(sats): - if (float(adjacency_matrix[i][sat_index - 1]) > 0.01): - neighbors.append(i + 1) - return neighbors + raise NotImplementedError def get_GSes(self, sat_index, time_index): - GSes = [] - delaypath = self.configuration_file_path + "/" + self.file_path + '/delay/' + str( - time_index) + '.txt' - adjacency_matrix = sn_get_param(delaypath) - sats = self.orbit_number * self.sat_number - for i in range(sats, len(adjacency_matrix)): - if (float(adjacency_matrix[i][sat_index - 1]) > 0.01): - GSes.append(i + 1) - return GSes + raise NotImplementedError def get_utility(self, time_index): self.utility_checking_time.append(time_index) def get_position(self, sat_index, time_index): - path = self.configuration_file_path + "/" + self.file_path + '/position/' + '/%d.txt' % time_index + path = self.local_dir + '/position/' + '/%d.txt' % time_index f = open(path) ADJ = f.readlines() + f.close() return ADJ[sat_index - 1] def get_IP(self, sat_index): - IP_info = sn_remote_cmd( - self.remote_ssh, "docker inspect" + - " --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}\n{{end}}'" - + " ovs_container_" + str(sat_index)) - ip_list = [] - for i in range(len(IP_info) - 2): - ip_list.append(IP_info[i].split()[0]) - return ip_list + raise NotImplementedError def set_damage(self, damaging_ratio, time_index): self.damage_ratio.append(damaging_ratio) @@ -215,26 +255,149 @@ def set_perf(self, sat1_index, sat2_index, time_index): self.perf_src.append(sat1_index) self.perf_des.append(sat2_index) self.perf_time.append(time_index) + + def event(self, timeptr): + if timeptr in self.utility_checking_time: + sn_check_utility(timeptr, self.remote_ssh, self.local_dir) + if timeptr % self.update_interval == 0: + # updating link delays after link changes + sn_update_delay(self.file_path, + self.configuration_dir, timeptr, + self.constellation_size, + self.remote_ssh, self.remote_ftp) + if timeptr in self.damage_time: + sn_damage( + self.damage_ratio[self.damage_time.index(timeptr)], + self.damage_list, self.constellation_size, + self.remote_ssh, self.remote_ftp, self.file_path, + self.configuration_dir) + if timeptr in self.recovery_time: + sn_recover(self.damage_list, self.sat_loss, + self.remote_ssh, self.remote_ftp, + self.file_path, + self.configuration_dir) + for i, val in enumerate(self.sr_time): + if val != timeptr: + continue + sn_sr(self.sr_src[i], + self.sr_des[i], + self.sr_target[i], + self.container_id_list, self.remote_ssh) + for i, val in enumerate(self.ping_time): + if val != timeptr: + continue + ping_thread = threading.Thread( + target=sn_ping, + args=(self.ping_src[i], + self.ping_des[i], + self.ping_time[i], + self.constellation_size, + self.container_id_list, + self.file_path, + self.configuration_dir, + self.remote_ssh)) + ping_thread.start() + self.ping_threads.append(ping_thread) + for i, val in enumerate(self.perf_time): + if val != timeptr: + continue + perf_thread = threading.Thread( + target=sn_perf, + args=(self.perf_src[i], + self.perf_des[i], + self.perf_time[i], + self.constellation_size, + self.container_id_list, + self.file_path, + self.configuration_dir, + self.remote_ssh)) + perf_thread.start() + self.perf_threads.append(perf_thread) + for i, val in enumerate(self.route_time): + if val != timeptr: + continue + sn_route(self.route_src[i], + self.route_time[i], + self.file_path, + self.configuration_dir, + self.container_id_list, self.remote_ssh) def start_emulation(self): - # Start emulation in a new thread. - sn_thread = sn_Emulation_Start_Thread( - self.remote_ssh, self.remote_ftp, self.sat_loss, - self.sat_ground_bandwidth, self.sat_ground_loss, - self.container_id_list, self.file_path, - self.configuration_file_path, self.update_interval, - self.constellation_size, self.ping_src, self.ping_des, - self.ping_time, self.sr_src, self.sr_des, self.sr_target, - self.sr_time, self.damage_ratio, self.damage_time, - self.damage_list, self.recovery_time, self.route_src, - self.route_time, self.duration, self.utility_checking_time, - self.perf_src, self.perf_des, self.perf_time) - sn_thread.start() - sn_thread.join() + self.ping_threads = [] + self.perf_threads = [] + timeptr = 2 # current emulating time + topo_change_file_path = os.path.join(self.local_dir, 'Topo_leo_change.txt') + fi = open(topo_change_file_path, 'r') + line = fi.readline() + while line: # starting reading change information and emulating + words = line.split() + if words[0] == 'time': + print('Emulation in No.' + str(timeptr) + ' second.') + # the time when the new change occurrs + current_time = str(int(words[1][:-1])) + while int(current_time) > timeptr: + start_time = time.time() + self.event(timeptr) + timeptr += 1 + end_time = time.time() + passed_time = ( + end_time - + start_time) if (end_time - start_time) < 1 else 1 + time.sleep(1 - passed_time) + if timeptr >= self.duration: + return + print('Emulation in No.' + str(timeptr) + ' second.') + print("A change in time " + current_time + ':') + line = fi.readline() + words = line.split() + line = fi.readline() + line = fi.readline() + words = line.split() + while words[0] != 'del:': # addlink + word = words[0].split('-') + s = int(word[0]) + f = int(word[1]) + if s > f: + s, f = f, s + print("add link", s, f) + current_topo_path = self.configuration_dir + "/" + self.file_path + '/delay/' + str( + current_time) + '.txt.gz' + matrix = sn_get_param(current_topo_path) + sn_establish_new_GSL(self.container_id_list, matrix, + self.constellation_size, + self.sat_ground_bw, + self.sat_ground_loss, s, f, + self.remote_ssh) + line = fi.readline() + words = line.split() + line = fi.readline() + words = line.split() + if len(words) == 0: + return + while words[0] != 'time': # delete link + word = words[0].split('-') + s = int(word[0]) + f = int(word[1]) + if s > f: + s, f = f, s + print("del link " + str(s) + "-" + str(f) + "\n") + sn_del_link(s, f, self.container_id_list, self.remote_ssh) + line = fi.readline() + words = line.split() + if len(words) == 0: + return + self.event(timeptr) + timeptr += 1 # current emulating time + if timeptr >= self.duration: + return + fi.close() + for ping_thread in self.ping_threads: + ping_thread.join() + for perf_thread in self.perf_threads: + perf_thread.join() def stop_emulation(self): - # Stop emulation in a new thread. - sn_thread = sn_Emulation_Stop_Thread(self.remote_ssh, self.remote_ftp, - self.file_path) - sn_thread.start() - sn_thread.join() + print("Removing containers...") + sn_remote_cmd(self.remote_ssh, + f"python3 {self.remote_dir}/orchestrater.py clean {self.remote_dir}" + ) diff --git a/starrynet/sn_utils.py b/starrynet/sn_utils.py index a0b9ae3..94d1b15 100644 --- a/starrynet/sn_utils.py +++ b/starrynet/sn_utils.py @@ -1,723 +1,140 @@ import os -import threading +import glob import json -import copy import argparse -import os -from time import sleep -import time +import paramiko import numpy import random -""" -Starrynet utils that are used in sn_synchronizer -author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) and Zeqi Lai (zeqilai@tsinghua.edu.cn) -""" -try: - import threading -except ImportError: - os.system("pip3 install threading") - import threading - -try: - import paramiko -except ImportError: - os.system("pip3 install paramiko") - import paramiko - -try: - import requests -except ImportError: - os.system("pip3 install requests") - import requests - - -def get_right_satellite(current_sat_id, current_orbit_id, orbit_num): - if current_orbit_id == orbit_num - 1: - return [current_sat_id, 0] - else: - return [current_sat_id, current_orbit_id + 1] - - -def get_down_satellite(current_sat_id, current_orbit_id, sat_num): - if current_sat_id == sat_num - 1: - return [0, current_orbit_id] - else: - return [current_sat_id + 1, current_orbit_id] - def sn_load_file(path, GS_lat_long): - f = open("./config.json", "r", encoding='utf8') + f = open(path, 'r', encoding='utf8') table = json.load(f) - data = {} - data['cons_name'] = table["Name"] - data['altitude'] = table["Altitude (km)"] - data['cycle'] = table["Cycle (s)"] - data['inclination'] = table["Inclination"] - data['phase_shift'] = table["Phase shift"] - data['orbit'] = table["# of orbit"] - data['sat'] = table["# of satellites"] - data['link'] = table["Satellite link"] - data['duration'] = table["Duration (s)"] - data['ip'] = table["IP version"] - data['intra_as_routing'] = table["Intra-AS routing"] - data['inter_as_routing'] = table["Inter-AS routing"] - data['link_policy'] = table["Link policy"] - data['handover_policy'] = table["Handover policy"] - data['update_time'] = table["update_time (s)"] - data['sat_bw'] = table["satellite link bandwidth (\"X\" Gbps)"] - data['sat_ground_bw'] = table["sat-ground bandwidth (\"X\" Gbps)"] - data['sat_loss'] = table["satellite link loss (\"X\"% )"] - data['sat_ground_loss'] = table["sat-ground loss (\"X\"% )"] - data['ground_num'] = table["GS number"] - data['multi_machine'] = table[ - "multi-machine (\"0\" for no, \"1\" for yes)"] - data['antenna_number'] = table["antenna number"] - data['antenna_inclination'] = table["antenna_inclination_angle"] - data['remote_machine_IP'] = table["remote_machine_IP"] - data['remote_machine_username'] = table["remote_machine_username"] - data['remote_machine_password'] = table["remote_machine_password"] - parser = argparse.ArgumentParser(description='manual to this script') - parser.add_argument('--cons_name', type=str, default=data['cons_name']) - parser.add_argument('--satellite_altitude', - type=int, - default=data['altitude']) - parser.add_argument('--inclination', type=int, default=data['inclination']) - parser.add_argument('--orbit_number', type=int, default=data['orbit']) - parser.add_argument('--sat_number', type=int, default=data['sat']) - parser.add_argument('--fac_num', type=int, default=len(GS_lat_long)) - parser.add_argument('--link_style', type=str, default=data['link']) - parser.add_argument('--IP_version', type=str, default=data['ip']) - parser.add_argument('--link_policy', type=str, default=data['link_policy']) + parser.add_argument('--cons_name', type=str, default=table['Name']) + parser.add_argument('--link_style', type=str, default=table['Satellite link']) + parser.add_argument('--IP_version', type=str, default=table['IP version']) + parser.add_argument('--link_policy', type=str, default=table['Link policy']) # link delay updating granularity parser.add_argument('--update_interval', type=int, - default=data['update_time']) - parser.add_argument('--duration', type=int, default=data['duration']) - parser.add_argument('--inter_routing', - type=str, - default=data['inter_as_routing']) - parser.add_argument('--intra_routing', - type=str, - default=data['intra_as_routing']) - parser.add_argument('--cycle', type=int, default=data['cycle']) - parser.add_argument('--time_slot', type=int, default=100) - parser.add_argument('--sat_bandwidth', type=int, default=data['sat_bw']) + default=table['update_time (s)']) + parser.add_argument('--duration', type=int, default=table['Duration (s)']) + parser.add_argument('--sat_bandwidth', + type=int, + default=table['satellite link bandwidth ("X" Gbps)']) parser.add_argument('--sat_ground_bandwidth', type=int, - default=data['sat_ground_bw']) - parser.add_argument('--sat_loss', type=int, default=data['sat_loss']) - parser.add_argument('--sat_ground_loss', + default=table['sat-ground bandwidth ("X" Gbps)']) + parser.add_argument('--sat_loss', type=int, - default=data['sat_ground_loss']) - parser.add_argument('--ground_num', type=int, default=data['ground_num']) - parser.add_argument('--multi_machine', + default=table['satellite link loss ("X"% )']) + parser.add_argument('--sat_ground_loss', type=int, - default=data['multi_machine']) + default=table['sat-ground loss ("X"% )']) parser.add_argument('--antenna_number', type=int, - default=data['antenna_number']) - parser.add_argument('--antenna_inclination', + default=table['antenna number']) + parser.add_argument('--antenna_elevation', type=int, - default=data['antenna_inclination']) - parser.add_argument('--user_num', type=int, default=0) - parser.add_argument('--remote_machine_IP', - type=str, - default=data['remote_machine_IP']) - parser.add_argument('--remote_machine_username', - type=str, - default=data['remote_machine_username']) - parser.add_argument('--remote_machine_password', - type=str, - default=data['remote_machine_password']) - - parser.add_argument('--path', - '-p', - type=str, - default="starrynet/config.xls") - parser.add_argument('--hello_interval', '-i', type=int, default=10) - parser.add_argument('--node_number', '-n', type=int, default=27) - parser.add_argument('--GS', - '-g', - type=str, - default="50.110924/8.682127/46.635700/14.311817") - + default=table['antenna elevation angle']) + # TODO: parser.add_argument('--handover', default=table["Handover policy"]) + # TODO: parser.add_argument('--time_slot', type=int, default=100) + # TODO: parser.add_argument('--user_num', type=int, default=0) sn_args = parser.parse_args() + sn_args.__setattr__('machine_lst', table['Machines']) + shell_lst = [{ + 'altitude': shell['Altitude (km)'], + 'inclination': shell['Inclination'], + 'phase_shift': shell['Phase shift'], + 'orbit': shell['Orbits'], + 'sat': shell['Satellites per orbit'], + } for shell in table["Shells"]] + sn_args.__setattr__('shell_lst', shell_lst) return sn_args - -def sn_get_param(file_): - f = open(file_) - ADJ = f.readlines() - for i in range(len(ADJ)): - ADJ[i] = ADJ[i].strip('\n') - ADJ = [x.split(',') for x in ADJ] - f.close() - return ADJ - - -def sn_init_remote_machine(host, username, password): - # transport = paramiko.Transport((host, 22)) - # transport.connect(username=username, password=password) - remote_machine_ssh = paramiko.SSHClient() - # remote_machine_ssh._transport = transport - remote_machine_ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - remote_machine_ssh.connect(hostname=host, - port=22, - username=username, - password=password) - transport = paramiko.Transport((host, 22)) - transport.connect(username=username, password=password) - return remote_machine_ssh, transport - # transport.close() - - -def sn_init_remote_ftp(transport): - ftp_client = paramiko.SFTPClient.from_transport(transport) ## ftp client - return ftp_client - +def sn_connect_remote(host, port, username, password): + remote_ssh = paramiko.SSHClient() + remote_ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + remote_ssh.connect(hostname=host, port=port, username=username, password=password) + return remote_ssh, remote_ssh.open_sftp() def sn_remote_cmd(remote_ssh, cmd): - stdin, stdout, stderr = remote_ssh.exec_command(cmd, get_pty=True) - lines = stdout.readlines() - return lines - - -# A thread designed for initializing working directory. -class sn_init_directory_thread(threading.Thread): - - def __init__(self, file_path, configuration_file_path, remote_ssh): - threading.Thread.__init__(self) - self.file_path = file_path - self.remote_ssh = remote_ssh - self.configuration_file_path = configuration_file_path - - def run(self): - # Reset docker environment. - os.system("rm " + self.configuration_file_path + "/" + self.file_path + - "/*.txt") - if os.path.exists(self.file_path + "/mid_files") == False: - os.system("mkdir " + self.configuration_file_path + "/" + - self.file_path) - os.system("mkdir " + self.configuration_file_path + "/" + - self.file_path + "/delay") - os.system("mkdir " + self.configuration_file_path + "/" + - self.file_path + "/mid_files") - sn_remote_cmd(self.remote_ssh, "mkdir ~/" + self.file_path) - sn_remote_cmd(self.remote_ssh, "mkdir ~/" + self.file_path + "/delay") - - -# A thread designed for initializing constellation nodes. -class sn_Node_Init_Thread(threading.Thread): - - def __init__(self, remote_ssh, docker_service_name, node_size, - container_id_list, container_global_idx): - threading.Thread.__init__(self) - self.remote_ssh = remote_ssh - self.docker_service_name = docker_service_name - self.node_size = node_size - self.container_global_idx = container_global_idx - self.container_id_list = copy.deepcopy(container_id_list) - - def run(self): - - # Reset docker environment. - sn_reset_docker_env(self.remote_ssh, self.docker_service_name, - self.node_size) - # Get container list in each machine. - self.container_id_list = sn_get_container_info(self.remote_ssh) - # Rename all containers with the global idx - sn_rename_all_container(self.remote_ssh, self.container_id_list, - self.container_global_idx) - - -def sn_get_container_info(remote_machine_ssh): - # Read all container information in all_container_info - all_container_info = sn_remote_cmd(remote_machine_ssh, "docker ps") - n_container = len(all_container_info) - 1 - container_id_list = [] - for container_idx in range(1, n_container + 1): - container_id_list.append(all_container_info[container_idx].split()[0]) - - return container_id_list - - -def sn_delete_remote_network_bridge(remote_ssh): - all_br_info = sn_remote_cmd(remote_ssh, "docker network ls") - for line in all_br_info: - if "La" in line or "Le" in line or "GS" in line: - network_name = line.split()[1] - print('docker network rm ' + network_name) - sn_remote_cmd(remote_ssh, 'docker network rm ' + network_name) - - -def sn_reset_docker_env(remote_ssh, docker_service_name, node_size): - print("Reset docker environment for constellation emulation ...") - print("Remove legacy containers.") - print(sn_remote_cmd(remote_ssh, - "docker service rm " + docker_service_name)) - print(sn_remote_cmd(remote_ssh, "docker rm -f $(docker ps -a -q)")) - print("Remove legacy emulated ISLs.") - sn_delete_remote_network_bridge(remote_ssh) - print("Creating new containers...") - sn_remote_cmd( - remote_ssh, "docker service create --replicas " + str(node_size) + - " --name " + str(docker_service_name) + - " --cap-add ALL lwsen/starlab_node:1.0 ping www.baidu.com") - - -def sn_rename_all_container(remote_ssh, container_id_list, new_idx): - print("Rename all containers ...") - new_idx = 1 - for container_id in container_id_list: - sn_remote_cmd( - remote_ssh, "docker rename " + str(container_id) + - " ovs_container_" + str(new_idx)) - new_idx = new_idx + 1 - - -# A thread designed for initializing constellation links. -class sn_Link_Init_Thread(threading.Thread): - - def __init__(self, remote_ssh, remote_ftp, orbit_num, sat_num, - constellation_size, fac_num, file_path, - configuration_file_path, sat_bandwidth, sat_ground_bandwidth, - sat_loss, sat_ground_loss): - threading.Thread.__init__(self) - self.remote_ssh = remote_ssh - self.constellation_size = constellation_size - self.fac_num = fac_num - self.orbit_num = orbit_num - self.sat_num = sat_num - self.file_path = file_path - self.configuration_file_path = configuration_file_path - self.sat_bandwidth = sat_bandwidth - self.sat_ground_bandwidth = sat_ground_bandwidth - self.sat_loss = sat_loss - self.sat_ground_loss = sat_ground_loss - self.remote_ftp = remote_ftp - - def run(self): - print('Run in link init thread.') - self.remote_ftp.put( - os.path.join(os.getcwd(), "starrynet/sn_orchestrater.py"), - self.file_path + "/sn_orchestrater.py") - self.remote_ftp.put( - self.configuration_file_path + "/" + self.file_path + - '/delay/1.txt', self.file_path + "/1.txt") - print('Initializing links ...') - sn_remote_cmd( - self.remote_ssh, "python3 " + self.file_path + - "/sn_orchestrater.py" + " " + str(self.orbit_num) + " " + - str(self.sat_num) + " " + str(self.constellation_size) + " " + - str(self.fac_num) + " " + str(self.sat_bandwidth) + " " + - str(self.sat_loss) + " " + str(self.sat_ground_bandwidth) + " " + - str(self.sat_ground_loss) + " " + self.file_path + "/1.txt") + return remote_ssh.exec_command(cmd)[1].read().decode().strip() +def sn_remote_wait_output(remote_ssh, cmd): + for line in remote_ssh.exec_command(cmd, get_pty=True)[1]: + print(line, end='') -# A thread designed for initializing bird routing. -class sn_Routing_Init_Thread(threading.Thread): - - def __init__(self, remote_ssh, remote_ftp, orbit_num, sat_num, - constellation_size, fac_num, file_path, sat_bandwidth, - sat_ground_bandwidth, sat_loss, sat_ground_loss): - threading.Thread.__init__(self) - self.remote_ssh = remote_ssh - self.constellation_size = constellation_size - self.fac_num = fac_num - self.orbit_num = orbit_num - self.sat_num = sat_num - self.file_path = file_path - self.sat_bandwidth = sat_bandwidth - self.sat_ground_bandwidth = sat_ground_bandwidth - self.sat_loss = sat_loss - self.sat_ground_loss = sat_ground_loss - self.remote_ftp = remote_ftp - - def run(self): - print( - "Copy bird configuration file to each container and run routing process." - ) - self.remote_ftp.put( - os.path.join(os.getcwd(), "starrynet/sn_orchestrater.py"), - self.file_path + "/sn_orchestrater.py") - print('Initializing routing ...') - sn_remote_cmd( - self.remote_ssh, "python3 " + self.file_path + - "/sn_orchestrater.py" + " " + str(self.constellation_size) + " " + - str(self.fac_num) + " " + self.file_path) - print("Routing initialized!") - - -# A thread designed for emulation. -class sn_Emulation_Start_Thread(threading.Thread): - - def __init__(self, remote_ssh, remote_ftp, sat_loss, sat_ground_bw, - sat_ground_loss, container_id_list, file_path, - configuration_file_path, update_interval, constellation_size, - ping_src, ping_des, ping_time, sr_src, sr_des, sr_target, - sr_time, damage_ratio, damage_time, damage_list, - recovery_time, route_src, route_time, duration, - utility_checking_time, perf_src, perf_des, perf_time): - threading.Thread.__init__(self) - self.remote_ssh = remote_ssh - self.remote_ftp = remote_ftp - self.sat_loss = sat_loss - self.sat_ground_bw = sat_ground_bw - self.sat_ground_loss = sat_ground_loss - self.container_id_list = copy.deepcopy(container_id_list) - self.file_path = file_path - self.configuration_file_path = configuration_file_path - self.update_interval = update_interval - self.constellation_size = constellation_size - self.ping_src = ping_src - self.ping_des = ping_des - self.ping_time = ping_time - self.perf_src = perf_src - self.perf_des = perf_des - self.perf_time = perf_time - self.sr_src = sr_src - self.sr_des = sr_des - self.sr_target = sr_target - self.sr_time = sr_time - self.damage_ratio = damage_ratio - self.damage_time = damage_time - self.damage_list = damage_list - self.recovery_time = recovery_time - self.route_src = route_src - self.route_time = route_time - self.duration = duration - self.utility_checking_time = utility_checking_time - if self.container_id_list == []: - self.container_id_list = sn_get_container_info(self.remote_ssh) - - def run(self): - ping_threads = [] - perf_threads = [] - timeptr = 2 # current emulating time - topo_change_file_path = self.configuration_file_path + "/" + self.file_path + '/Topo_leo_change.txt' - fi = open(topo_change_file_path, 'r') - line = fi.readline() - while line: # starting reading change information and emulating - words = line.split() - if words[0] == 'time': - print('Emulation in No.' + str(timeptr) + ' second.') - # the time when the new change occurrs - current_time = str(int(words[1][:-1])) - while int(current_time) > timeptr: - start_time = time.time() - if timeptr in self.utility_checking_time: - sn_check_utility( - timeptr, self.remote_ssh, - self.configuration_file_path + "/" + - self.file_path) - if timeptr % self.update_interval == 0: - # updating link delays after link changes - sn_update_delay(self.file_path, - self.configuration_file_path, timeptr, - self.constellation_size, - self.remote_ssh, self.remote_ftp) - if timeptr in self.damage_time: - sn_damage( - self.damage_ratio[self.damage_time.index(timeptr)], - self.damage_list, self.constellation_size, - self.remote_ssh, self.remote_ftp, self.file_path, - self.configuration_file_path) - if timeptr in self.recovery_time: - sn_recover(self.damage_list, self.sat_loss, - self.remote_ssh, self.remote_ftp, - self.file_path, - self.configuration_file_path) - if timeptr in self.sr_time: - index = [ - i for i, val in enumerate(self.sr_time) - if val == timeptr - ] - for index_num in index: - sn_sr(self.sr_src[index_num], - self.sr_des[index_num], - self.sr_target[index_num], - self.container_id_list, self.remote_ssh) - if timeptr in self.ping_time: - if timeptr in self.ping_time: - index = [ - i for i, val in enumerate(self.ping_time) - if val == timeptr - ] - for index_num in index: - ping_thread = threading.Thread( - target=sn_ping, - args=(self.ping_src[index_num], - self.ping_des[index_num], - self.ping_time[index_num], - self.constellation_size, - self.container_id_list, - self.file_path, - self.configuration_file_path, - self.remote_ssh)) - ping_thread.start() - ping_threads.append(ping_thread) - if timeptr in self.perf_time: - if timeptr in self.perf_time: - index = [ - i for i, val in enumerate(self.perf_time) - if val == timeptr - ] - for index_num in index: - perf_thread = threading.Thread( - target=sn_perf, - args=(self.perf_src[index_num], - self.perf_des[index_num], - self.perf_time[index_num], - self.constellation_size, - self.container_id_list, - self.file_path, - self.configuration_file_path, - self.remote_ssh)) - perf_thread.start() - perf_threads.append(perf_thread) - if timeptr in self.route_time: - index = [ - i for i, val in enumerate(self.route_time) - if val == timeptr - ] - for index_num in index: - sn_route(self.route_src[index_num], - self.route_time[index_num], - self.file_path, - self.configuration_file_path, - self.container_id_list, self.remote_ssh) - timeptr += 1 - end_time = time.time() - passed_time = ( - end_time - - start_time) if (end_time - start_time) < 1 else 1 - sleep(1 - passed_time) - if timeptr >= self.duration: - return - print('Emulation in No.' + str(timeptr) + ' second.') - print("A change in time " + current_time + ':') - line = fi.readline() - words = line.split() - line = fi.readline() - line = fi.readline() - words = line.split() - while words[0] != 'del:': # addlink - word = words[0].split('-') - s = int(word[0]) - f = int(word[1]) - if s > f: - tmp = s - s = f - f = tmp - print("add link", s, f) - current_topo_path = self.configuration_file_path + "/" + self.file_path + '/delay/' + str( - current_time) + '.txt' - matrix = sn_get_param(current_topo_path) - sn_establish_new_GSL(self.container_id_list, matrix, - self.constellation_size, - self.sat_ground_bw, - self.sat_ground_loss, s, f, - self.remote_ssh) - line = fi.readline() - words = line.split() - line = fi.readline() - words = line.split() - if len(words) == 0: - return - while words[0] != 'time': # delete link - word = words[0].split('-') - s = int(word[0]) - f = int(word[1]) - if s > f: - tmp = s - s = f - f = tmp - print("del link " + str(s) + "-" + str(f) + "\n") - sn_del_link(s, f, self.container_id_list, self.remote_ssh) - line = fi.readline() - words = line.split() - if len(words) == 0: - return - if timeptr in self.utility_checking_time: - sn_check_utility( - timeptr, self.remote_ssh, - self.configuration_file_path + "/" + self.file_path) - if timeptr % self.update_interval == 0: - # updating link delays after link changes - sn_update_delay(self.file_path, - self.configuration_file_path, timeptr, - self.constellation_size, self.remote_ssh, - self.remote_ftp) - if timeptr in self.damage_time: - sn_damage( - self.damage_ratio[self.damage_time.index(timeptr)], - self.damage_list, self.constellation_size, - self.remote_ssh, self.remote_ftp, self.file_path, - self.configuration_file_path) - if timeptr in self.recovery_time: - sn_recover(self.damage_list, self.sat_loss, - self.remote_ssh, self.remote_ftp, - self.file_path, self.configuration_file_path) - if timeptr in self.sr_time: - index = [ - i for i, val in enumerate(self.sr_time) - if val == timeptr - ] - for index_num in index: - sn_sr(self.sr_src[index_num], self.sr_des[index_num], - self.sr_target[index_num], - self.container_id_list, self.remote_ssh) - if timeptr in self.ping_time: - if timeptr in self.ping_time: - index = [ - i for i, val in enumerate(self.ping_time) - if val == timeptr - ] - for index_num in index: - ping_thread = threading.Thread( - target=sn_ping, - args=(self.ping_src[index_num], - self.ping_des[index_num], - self.ping_time[index_num], - self.constellation_size, - self.container_id_list, self.file_path, - self.configuration_file_path, - self.remote_ssh)) - ping_thread.start() - ping_threads.append(ping_thread) - if timeptr in self.perf_time: - if timeptr in self.perf_time: - index = [ - i for i, val in enumerate(self.perf_time) - if val == timeptr - ] - for index_num in index: - perf_thread = threading.Thread( - target=sn_perf, - args=(self.perf_src[index_num], - self.perf_des[index_num], - self.perf_time[index_num], - self.constellation_size, - self.container_id_list, self.file_path, - self.configuration_file_path, - self.remote_ssh)) - perf_thread.start() - perf_threads.append(perf_thread) - if timeptr in self.route_time: - index = [ - i for i, val in enumerate(self.route_time) - if val == timeptr - ] - for index_num in index: - sn_route(self.route_src[index_num], - self.route_time[index_num], self.file_path, - self.configuration_file_path, - self.container_id_list, self.remote_ssh) - timeptr += 1 # current emulating time - if timeptr >= self.duration: - return - fi.close() - for ping_thread in ping_threads: - ping_thread.join() - for perf_thread in perf_threads: - perf_thread.join() - - -def sn_check_utility(time_index, remote_ssh, file_path): +def sn_check_utility(time_index, remote_ssh, local_dir): result = sn_remote_cmd(remote_ssh, "vmstat") - f = open(file_path + "/utility-info" + "_" + str(time_index) + ".txt", "w") - f.writelines(result) + f = open(os.path.join(local_dir, f"utility-info_{time_index}.txt"), "w") + f.write(result) f.close() - -def sn_update_delay(file_path, configuration_file_path, timeptr, - constellation_size, remote_ssh, - remote_ftp): # updating delays - remote_ftp.put(os.path.join(os.getcwd(), "starrynet/sn_orchestrater.py"), - file_path + "/sn_orchestrater.py") +def sn_update_delay(remote_ssh, remote_ftp, remote_dir, local_dir, + timeptr, constellation_size): remote_ftp.put( - configuration_file_path + "/" + file_path + '/delay/' + str(timeptr) + - '.txt', file_path + '/' + str(timeptr) + '.txt') - sn_remote_cmd( - remote_ssh, - "python3 " + file_path + "/sn_orchestrater.py " + file_path + '/' + - str(timeptr) + '.txt ' + str(constellation_size) + " update") - print("Delay updating done.\n") - + os.path.join(local_dir, 'delay', f'{timeptr}.txt.gz'), + f'{remote_dir}/{timeptr}.txt.gz') + sn_remote_cmd(remote_ssh, + f"python3 {remote_dir}/orchestrater.py " + f"{remote_dir}/{timeptr}.txt.gz {constellation_size} update") + print("Delay updating done.") -def sn_damage(ratio, damage_list, constellation_size, remote_ssh, remote_ftp, - file_path, configuration_file_path): +def sn_damage(remote_ssh, remote_ftp, remote_dir, local_dir, + ratio, damage_list, constellation_size): print("Randomly setting damaged links...\n") - random_list = [] cumulated_damage_list = damage_list - while len(random_list) < (int(constellation_size * ratio)): - target = int(random.uniform(0, constellation_size - 1)) - random_list.append(target) - cumulated_damage_list.append(target) - numpy.savetxt( - configuration_file_path + "/" + file_path + - '/mid_files/damage_list.txt', random_list) - remote_ftp.put(os.path.join(os.getcwd(), "starrynet/sn_orchestrater.py"), - file_path + "/sn_orchestrater.py") - remote_ftp.put( - configuration_file_path + "/" + file_path + - '/mid_files/damage_list.txt', file_path + "/damage_list.txt") - sn_remote_cmd(remote_ssh, - "python3 " + file_path + "/sn_orchestrater.py " + file_path) + random_list = [ + random.randint(0, constellation_size - 1) + for _ in range(int(constellation_size * ratio)) + ] + cumulated_damage_list.extend(random_list) + list_file = os.path.join(local_dir, 'mid_files', 'damage_list.txt') + numpy.savetxt(list_file, random_list) + remote_ftp.put(list_file, f'{remote_dir}/damage_list.txt') + sn_remote_cmd(remote_ssh, f"python3 {remote_dir}/orchestrater.py {remote_dir}") print("Damage done.\n") -def sn_recover(damage_list, sat_loss, remote_ssh, remote_ftp, file_path, - configuration_file_path): +def sn_recover(remote_ssh, remote_ftp, remote_dir, local_dir, damage_list, sat_loss): print("Recovering damaged links...\n") - cumulated_damage_list = damage_list - numpy.savetxt( - configuration_file_path + "/" + file_path + - '/mid_files/damage_list.txt', cumulated_damage_list) - remote_ftp.put(os.path.join(os.getcwd(), "starrynet/sn_orchestrater.py"), - file_path + "/sn_orchestrater.py") - remote_ftp.put( - configuration_file_path + "/" + file_path + - '/mid_files/damage_list.txt', file_path + "/damage_list.txt") - sn_remote_cmd( - remote_ssh, "python3 " + file_path + "/sn_orchestrater.py " + - file_path + " " + str(sat_loss)) - cumulated_damage_list.clear() + list_file = os.path.join(local_dir, 'mid_files', 'damage_list.txt') + numpy.savetxt(list_file, damage_list) + remote_ftp.put(list_file, f'{remote_dir}/damage_list.txt') + sn_remote_cmd(remote_ssh, + f"python3 {remote_dir}/orchestrater.py {remote_dir} {sat_loss}" + ) + damage_list.clear() print("Link recover done.\n") - -def sn_sr(src, des, target, container_id_list, remote_ssh): - ifconfig_output = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") - des_IP = sn_remote_cmd( - remote_ssh, - "docker exec -it " + str(container_id_list[des - 1]) + " ifconfig " + - ifconfig_output[0][:-1] + "|awk -F '[ :]+' 'NR==2{print $4}'") - target_IP = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[target - 1]) + - " ifconfig B" + str(target) + "-eth" + str(src) + - "|awk -F '[ :]+' 'NR==2{print $4}'") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[src - 1]) + - " ip route del " + str(des_IP[0][:-3]) + "0/24") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[src - 1]) + - " ip route add " + str(des_IP[0][:-3]) + "0/24 dev B%d-eth%d via " % - (src, target) + target_IP[0]) - print("docker exec -d " + str(container_id_list[src - 1]) + - " ip route add " + str(des_IP[0][:-3]) + "0/24 dev B%d-eth%d via " % - (src, target) + target_IP[0]) - +def sn_sr(src, des, target, netns_list, remote_ssh): + ifconfig_output = sn_remote_cmd(remote_ssh, + f"ip netns exec {netns_list[des - 1]} " + r"ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'").splitlines() + des_IP = sn_remote_cmd(remote_ssh, + f"ip netns exec {netns_list[des - 1]} ifconfig {ifconfig_output[0][:-1]} " + "| awk -F '[ :]+' 'NR==2{print $4}'").splitlines() + target_IP = sn_remote_cmd(remote_ssh, + f"ip netns exec {netns_list[target - 1]} ifconfig B{target}-eth{src} " + "| awk -F '[ :]+' 'NR==2{print $4}'").splitlines() + sn_remote_cmd(remote_ssh, + f"ip netns exec {netns_list[src - 1]} " + f" ip route del {des_IP[0][:-3]}0/24") + sn_remote_cmd(remote_ssh, + f"ip netns exec {netns_list[src - 1]} " + f"ip route add {des_IP[0][:-3]}0/24 dev B{src}-eth{target} via {target_IP[0]}") + print( + f"ip netns exec {netns_list[src - 1]} " + f"ip route add {des_IP[0][:-3]}0/24 dev B{src}-eth{target} via {target_IP[0]}" + ) def sn_ping(src, des, time_index, constellation_size, container_id_list, file_path, configuration_file_path, remote_ssh): if des <= constellation_size: - ifconfig_output = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") + ifconfig_output = sn_remote_cmd(remote_ssh, + f"ip netns exec {container_id_list[des - 1]} " + r"ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") des_IP = sn_remote_cmd( remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + " ifconfig " + ifconfig_output[0][:-1] + @@ -752,7 +169,7 @@ def sn_perf(src, des, time_index, constellation_size, container_id_list, remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + " ifconfig B" + str(des) + "-default |awk -F '[ :]+' 'NR==2{print $4}'") - + print('<>', src, des, des_IP) sn_remote_cmd( remote_ssh, "docker exec -id " + str(container_id_list[des - 1]) + " iperf3 -s ") @@ -767,7 +184,6 @@ def sn_perf(src, des, time_index, constellation_size, container_id_list, f.writelines(perf_result) f.close() - def sn_route(src, time_index, file_path, configuration_file_path, container_id_list, remote_ssh): route_result = sn_remote_cmd( @@ -813,9 +229,7 @@ def sn_establish_new_GSL(container_id_list, matrix, constellation_size, bw, remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + " ip link set dev " + target_interface + " name " + "B" + str(i - 1 + 1) + "-eth" + str(j)) - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev B" + str(i - 1 + 1) + "-eth" + str(j) + " up") + sn_remote_cmd( remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + " tc qdisc add dev B" + str(i - 1 + 1) + "-eth" + str(j) + @@ -829,17 +243,18 @@ def sn_establish_new_GSL(container_id_list, matrix, constellation_size, bw, " tc qdisc add dev B" + str(i - 1 + 1) + "-eth" + str(j) + " root netem rate " + str(bw) + "Gbps") print('[Add current node:]' + 'docker network connect ' + GSL_name + " " + - str(container_id_list[i - 1]) + " --ip 10." + str(address_16_23) + + str(container_id_list[i - 1]) + " --ip 9." + str(address_16_23) + "." + str(address_8_15) + ".50") - sn_remote_cmd( + print(sn_remote_cmd( remote_ssh, 'docker network connect ' + GSL_name + " " + str(container_id_list[j - 1]) + " --ip 9." + str(address_16_23) + "." + - str(address_8_15) + ".60") + str(address_8_15) + ".60")) ifconfig_output = sn_remote_cmd( remote_ssh, "docker exec -it " + str(container_id_list[j - 1]) + " ip addr | grep -B 2 9." + str(address_16_23) + "." + str(address_8_15) + ".60 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") + print(ifconfig_output) target_interface = str(ifconfig_output[0]).split("@")[0] sn_remote_cmd( remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + @@ -848,9 +263,7 @@ def sn_establish_new_GSL(container_id_list, matrix, constellation_size, bw, remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + " ip link set dev " + target_interface + " name " + "B" + str(j) + "-eth" + str(i - 1 + 1)) - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev B" + str(j) + "-eth" + str(i - 1 + 1) + " up") + sn_remote_cmd( remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + " tc qdisc add dev B" + str(j) + "-eth" + str(i - 1 + 1) + @@ -866,6 +279,13 @@ def sn_establish_new_GSL(container_id_list, matrix, constellation_size, bw, print('[Add right node:]' + 'docker network connect ' + GSL_name + " " + str(container_id_list[j - 1]) + " --ip 10." + str(address_16_23) + "." + str(address_8_15) + ".60") + + sn_remote_cmd( + remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + + " ip link set dev B" + str(i - 1 + 1) + "-eth" + str(j) + " up") + sn_remote_cmd( + remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + + " ip link set dev B" + str(j) + "-eth" + str(i - 1 + 1) + " up") def sn_del_link(first_index, second_index, container_id_list, remote_ssh): @@ -884,22 +304,4 @@ def sn_del_link(first_index, second_index, container_id_list, remote_ssh): sn_remote_cmd( remote_ssh, 'docker network disconnect ' + GSL_name + " " + str(container_id_list[second_index - 1])) - sn_remote_cmd(remote_ssh, 'docker network rm ' + GSL_name) - - -# A thread designed for stopping the emulation. -class sn_Emulation_Stop_Thread(threading.Thread): - - def __init__(self, remote_ssh, remote_ftp, file_path): - threading.Thread.__init__(self) - self.remote_ssh = remote_ssh - self.remote_ftp = remote_ftp - self.file_path = file_path - - def run(self): - print("Deleting all native bridges and containers...") - self.remote_ftp.put( - os.path.join(os.getcwd(), "starrynet/sn_orchestrater.py"), - self.file_path + "/sn_orchestrater.py") - sn_remote_cmd(self.remote_ssh, - "python3 " + self.file_path + "/sn_orchestrater.py") + sn_remote_cmd(remote_ssh, 'docker network rm ' + GSL_name) \ No newline at end of file From 1b43d5decef2ab4fc60e1b108d1e0d800ce76a73 Mon Sep 17 00:00:00 2001 From: Xiex <562837197@qq.com> Date: Sun, 3 Dec 2023 23:24:04 +0800 Subject: [PATCH 04/49] feat: basic support multi-shell and multi-machine --- config.json | 2 +- example.py | 16 +- starrynet/sn_observer.py | 99 ++++- starrynet/sn_orchestrater.py | 802 ++++++++++++++++++++--------------- starrynet/sn_synchronizer.py | 501 ++++++++++++---------- starrynet/sn_utils.py | 164 +------ 6 files changed, 826 insertions(+), 758 deletions(-) diff --git a/config.json b/config.json index 34e8750..631fc60 100644 --- a/config.json +++ b/config.json @@ -38,7 +38,7 @@ } ], "Duration (s)": 100, - "update_time (s)": 10, + "step (s)": 1, "satellite link bandwidth (\"X\" Gbps)": 5, "sat-ground bandwidth (\"X\" Gbps)": 5, "satellite link loss (\"X\"% )": 0, diff --git a/example.py b/example.py index ef41ccb..8d3bdc4 100644 --- a/example.py +++ b/example.py @@ -6,7 +6,6 @@ """ from starrynet.sn_observer import * -from starrynet.sn_orchestrater import * from starrynet.sn_synchronizer import * if __name__ == "__main__": @@ -14,17 +13,26 @@ # The node index sequence is: 25 sattelites, 2 ground stations. # In this example, 25 satellites and 2 ground stations are one AS. - AS = [[1, 27]] # Node #1 to Node #27 are within the same AS. GS_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817] ] # latitude and longitude of frankfurt and Austria configuration_file_path = "./config.json" hello_interval = 1 # hello_interval(s) in OSPF. 1-200 are supported. print('Start StarryNet.') - sn = StarryNet(configuration_file_path, GS_lat_long, hello_interval, AS) + sn = StarryNet(configuration_file_path, GS_lat_long) sn.create_nodes() sn.create_links() - sn.run_routing_deamon() + # sn.run_routing_deamon() + sn.set_ping(1, 1, 1, 1, 1, 2, 0) + sn.set_ping(1, 1, 1, 1, 1, 2, 0.5) + sn.set_ping(1, 1, 1, 1, 1, 2, 1) + sn.set_ping(1, 1, 1, 1, 1, 2, 1.5) + sn.set_ping(1, 1, 1, 1, 1, 2, 13.5) + sn.set_ping(1, 1, 1, 1, 1, 2, 20) + sn.start_emulation() + if input('clear environment?[y/n]').strip().lower()[:1] == 'y': + sn.clean() + exit(0) node_index1 = 1 node_index2 = 2 diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index 5cbd4c9..c5c465c 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -69,15 +69,18 @@ def _gsl_least_delay(sat_cbf_t_shell, gs_cbf, antenna_num, bound_dis): gsls_t.append(gsls) gsls_t_shell.append(gsls_t) - # [[gsl for every shell] for every gs] for every t] TODO: better method + # merge different shell + # [[gsls for every shell] for every gs] for every t] gsl_shell_gs_t = [ - [[] for gid in range(len(gs_cbf))] for t in range(len(gsls_t_shell[0])) + [list() for gid in range(len(gs_cbf))] for t in range(len(gsls_t_shell[0])) ] gsl_idx_dict = {} for t, gsl_shell_gs in enumerate(gsl_shell_gs_t): for gid, gsl_lst in enumerate(gsl_shell_gs): for shell_id in range(len(gsls_t_shell)): for oid, sid, delay in gsls_t_shell[shell_id][t][gid]: + if len(gsl_lst) >= antenna_num: + break if (gid, shell_id, oid, sid) in gsl_idx_dict: idx = gsl_idx_dict[(gid, shell_id, oid, sid)] else: @@ -112,9 +115,10 @@ def _bound_gsl(antenna_elevation, altitude): return a + np.sqrt(np.square(a) + np.square(altitude) + 2 * altitude * 6371) def calculate_delay( - dir, duration, shell_lst, isl_style, + dir, duration, step, shell_lst, isl_style, GS_lat_long, antenna_number, antenna_elevation, gsl_style ): + ts_total = int(duration / step) cached = True for shell in shell_lst: pos_dir = os.path.join(dir, shell['name'], 'position') @@ -123,12 +127,12 @@ def calculate_delay( os.makedirs(isl_dir, exist_ok=True) pos_files = glob.glob(os.path.join(pos_dir, '*.txt')) isl_files = glob.glob(os.path.join(isl_dir, '*.txt')) - if len(pos_files) != duration or len(isl_files) != duration: + if len(pos_files) != ts_total or len(isl_files) != ts_total: cached = False gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') os.makedirs(gsl_dir, exist_ok=True) gsl_files = glob.glob(os.path.join(gsl_dir, '*.txt')) - if len(gsl_files) != duration: + if len(gsl_files) != ts_total: cached = False if cached and input(f"Use cached local files [y/n]?").strip().lower()[:1] == 'y': return @@ -151,6 +155,7 @@ def calculate_delay( R = 6371393 F = 18 sat_cbf_t_shell = [] + ts_lst = [i * step for i in range(ts_total)] for i, shell in enumerate(shell_lst): inclination = shell['inclination'] * 2 * np.pi / 360 altitude = shell['altitude'] * 1000 @@ -158,7 +163,7 @@ def calculate_delay( orbit_number, sat_number = shell['orbit'], shell['sat'] num_of_sat = orbit_number * sat_number - sat_lla_t = np.zeros((duration, orbit_number, sat_number, 3)) + sat_lla_t = np.zeros((ts_total, orbit_number, sat_number, 3)) for oid in range(orbit_number): raan = oid / orbit_number * 2 * np.pi for sid in range(sat_number): @@ -182,12 +187,11 @@ def calculate_delay( ) sat = EarthSatellite.from_satrec(satrec, ts) cur = datetime.datetime(2022, 1, 1, 1, 0, 0) - t_ts = ts.utc(*cur.timetuple()[:5], - range(duration)) # [:4]:minute,[:5]:second + t_ts = ts.utc(*cur.timetuple()[:5], ts_lst) # [:4]:minute,[:5]:second geocentric = sat.at(t_ts) subpoint = wgs84.subpoint(geocentric) # list: [subpoint.latitude.degrees] [subpoint.longitude.degrees] [subpoint.elevation.km] - for t in range(duration): + for t in range(ts_total): sat_lla_t[t, oid, sid] = (subpoint.latitude.degrees[t], subpoint.longitude.degrees[t], subpoint.elevation.km[t]) @@ -204,29 +208,84 @@ def calculate_delay( isls_t_shell = isl_styles[isl_style](sat_cbf_t_shell) for i, isls_t in enumerate(isls_t_shell): isl_dir = os.path.join(dir, shell_lst[i]['name'], 'isl') - for t in range(duration): - f = open(f"{isl_dir}/{t + 1}.txt", 'w') + + isl_state = [ [list() for _ in range(shell_lst[i]['sat'])] + for _ in range(shell_lst[i]['orbit'])] + for t in range(ts_total): + f = open(f"{isl_dir}/{t}.txt", 'w') for oid, sat_lst in enumerate(isls_t[t]): for sid, isl_lst in enumerate(sat_lst): # one line for each satellite - f.write(f"{oid},{sid}: ") + f.write(f"{oid},{sid}|") + old_lst = isl_state[oid][sid] + old_del = [True] * len(old_lst) + new_add = [True] * len(isl_lst) + update = [] + for i, old in enumerate(old_lst): + for j, new in enumerate(isl_lst): + if old[0] != new[0]: + continue + old_del[i] = False + new_add[j] = False + if abs(new[3] - old[3]) > 1e-2: + update.append(new) + else: + isl_lst[j] = old + # del some isls + f.write(' '.join( + f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" + for isl, de in zip(old_lst, old_del) if de + ) + '|') + # update some isls + f.write(' '.join( + f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" + for isl in update + ) + '|') + # add some isls f.write(' '.join( - f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}"for isl in isl_lst - )) - f.write('\n') + f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" + for isl, add in zip(isl_lst, new_add) if add + ) + '\n') + isl_state[oid][sid] = isl_lst f.write('\n') f.close() gs_cbf = _to_cbf(np.array(GS_lat_long)) bound_dis = _bound_gsl(antenna_elevation, shell['altitude']) gsls_t = gsl_styles[gsl_style](sat_cbf_t_shell, gs_cbf, antenna_number, bound_dis) + gsl_state = [list() for _ in range(len(GS_lat_long))] for t, gsls in enumerate(gsls_t): - f = open(f"{gsl_dir}/{t + 1}.txt", 'w') + f = open(f"{gsl_dir}/{t}.txt", 'w') for gid, gsl_lst in enumerate(gsls): # one line for each ground station - f.write(f"{gid}: ") + f.write(f"{gid}|") + # add some isls + old_lst = gsl_state[gid] + old_del = [True] * len(old_lst) + new_add = [True] * len(gsl_lst) + update = [] + for i, old in enumerate(old_lst): + for j, new in enumerate(gsl_lst): + if old[0] == new[0]: + old_del[i] = False + new_add[j] = False + if abs(new[4] - old[4]) > 1e-2: + update.append(new) + else: + gsl_lst[j] = old # if not update, remain old delay + f.write(' '.join( + f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + for gsl, de in zip(old_lst, old_del) if de + ) + '|') + # update some gsls + f.write(' '.join( + f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + for gsl in update + ) + '|') + # del some gsls f.write(' '.join( - f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" for gsl in gsl_lst - )) - f.write('\n') + f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + for gsl, add in zip(gsl_lst, new_add) if add + ) + '\n') + gsl_state[gid] = gsl_lst f.close() diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 1ed22bf..f97ced8 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -1,13 +1,11 @@ import os import subprocess import sys -import gzip -from concurrent.futures import ThreadPoolExecutor +import glob +import ctypes +# from line_profiler import LineProfiler -# C module -import pyctr -# from time import sleep """ Used in the remote machine for link updating, initializing links, damaging and recovering links and other functionalities。 author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) and Zeqi Lai (zeqilai@tsinghua.edu.cn) @@ -16,371 +14,491 @@ PID_FILENAME = 'container_pid.txt' ASSIGN_FILENAME = 'assign.txt' NOT_ASSIGNED = 'NA' - -machine_id = None -workdir = None -init_pid_mat = None - -def _sat_name(orbit_id, sat_id): - return f'O{orbit_id+1}S{sat_id+1}' - -def _get_delay(path): - f = gzip.open(path, 'rt') - ADJ = [ line.strip().split(',') for line in f ] - f.close() - return ADJ +VXLAN_PORT = 4789 +# FIXME +CLONE_NEWNET = 0x40000000 +libc = ctypes.CDLL(None) + +def _sat_name(shell_id, orbit_id, sat_id): + return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' + +def _gs_name(gid): + return f'GS{gid+1}' + +def _pid_matrix(path, pop = False): + global _mat_cache + if path not in _mat_cache: + with open(path, 'r') as f: + _mat_cache[path] = [ + [pid for pid in line.strip().split(' ')] + for line in f if len(line) > 0 and not line.isspace() + ] + if pop: + return _mat_cache.pop(path) + return _mat_cache[path] def _get_params(path): with open(path, 'r') as f: - orbit_num = int(f.readline()) - sat_assign_lst = [int(tok) for tok in f.readline().split(' ')] - gs_assign_lst = [int(tok) for tok in f.readline().split(' ')] - return orbit_num, sat_assign_lst, gs_assign_lst - -def _get_init_pid(orbit_id, sat_id): - global init_pid_mat - if init_pid_mat is None: - with open(workdir + '/' + PID_FILENAME, 'r') as f: - init_pid_mat = [[pid for pid in line.strip().split(' ')] for line in f] - return init_pid_mat[sat_id][orbit_id] - -def sn_right_satellite(current_sat_id, current_orbit_id, orbit_num): - if current_orbit_id == orbit_num - 1: - return current_sat_id, 0 - else: - return current_sat_id, current_orbit_id + 1 - -def sn_down_satellite(current_sat_id, current_orbit_id, sat_num): - if current_sat_id == sat_num - 1: - return 0, current_orbit_id - else: - return current_sat_id + 1, current_orbit_id - -def sn_init_nodes(dir, orbit_num, sat_assign_lst, gs_assign_lst): - sn_clear(dir) - overlay_dir = dir + '/overlay' - os.makedirs(overlay_dir, exist_ok=True) - pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') - for sat_id, assign in enumerate(sat_assign_lst): - if assign != machine_id: - pid_file.write(' '.join(NOT_ASSIGNED for _ in range(orbit_num)) + '\n') + gs_mid = [int(mid) for mid in f.readline().split()] + sat_mid_lst, ip_lst = [], [] + for line in f: + if len(line) == 0 or line.isspace(): + break + toks = line.strip().split(' ') + orbit_num, shell_name = int(toks[0]), toks[1] + sat_mid = [int(mid) for mid in f.readline().split(' ')] + sat_mid_lst.append((orbit_num, shell_name, sat_mid)) + for line in f: + ip_lst.append(line.strip()) + return gs_mid, sat_mid_lst, ip_lst + +def _parse_isls(path): + del_lst, update_lst, add_lst = [], [], [] + f = open(path, 'r') + oid, sid = 0, 0 + for line in f: + if len(line) == 0 or line.isspace(): + oid += 1 + sid = 0 continue - print('sat:', sat_id) - for orbit_id in range(orbit_num): - sat_name = _sat_name(orbit_id, sat_id) - sat_dir = f'{overlay_dir}/{sat_name}' - pid_file.write(str(pyctr.container_run(sat_dir, sat_name)) + ' ') - pid_file.write('\n') - - for gs_id, assign in enumerate(gs_assign_lst): - if assign != machine_id: - pid_file.write(NOT_ASSIGNED + '\n') - continue - gs_name = f'GS{gs_id+1}' - gs_dir = f'{overlay_dir}/{gs_name}' - pid_file.write(str(pyctr.container_run(gs_dir, gs_name)) + '\n') - pid_file.close() - -def sn_ISL_intra_machine(isl_idx, orbit, sat, peer_orbit, peer_sat, delay, bw, loss): - cur_name, peer_name = _sat_name(orbit, sat), _sat_name(peer_orbit, peer_sat) - for o, s, netns in ((orbit, sat, cur_name), (peer_orbit, peer_sat, peer_name)): - pid = _get_init_pid(o, s) - netns_link = f'/run/netns/{netns}' - if os.path.exists(netns_link) or os.path.islink(netns_link): - print('[Warning]netns exist:', netns_link) - subprocess.check_call(('rm', netns_link)) - subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) + toks = line.strip().split('|') + if len(toks[1]) > 0: + for isl in toks[1].split(' '): + i_o_s_d = isl.split(',') + idx, isl_oid, isl_sid = int(i_o_s_d[0]),int(i_o_s_d[1]),int(i_o_s_d[2]) + del_lst.append((idx, oid, sid, isl_oid, isl_sid, i_o_s_d[3])) + if len(toks[2]) > 0: + for isl in toks[2].split(' '): + i_o_s_d = isl.split(',') + idx, isl_oid, isl_sid = int(i_o_s_d[0]),int(i_o_s_d[1]),int(i_o_s_d[2]) + update_lst.append((idx, oid, sid, isl_oid, isl_sid, i_o_s_d[3])) + if len(toks[3]) > 0: + for isl in toks[3].split(' '): + i_o_s_d = isl.split(',') + idx, isl_oid, isl_sid = int(i_o_s_d[0]),int(i_o_s_d[1]),int(i_o_s_d[2]) + add_lst.append((idx, oid, sid, isl_oid, isl_sid, i_o_s_d[3])) + sid += 1 + f.close() + return del_lst, update_lst, add_lst - prefix = f'10.{isl_idx >> 8}.{isl_idx & 0xFF}' - cur_peer = f'{cur_name}-{peer_name}' - peer_cur = f'{peer_name}-{cur_name}' - if subprocess.call(f"ip link show | grep '{cur_peer}'", shell=True) == 0: - print('[Warning]veth exist:', cur_peer) - subprocess.check_call(('ip', 'link', 'del', cur_peer)) - if subprocess.call(f"ip link show | grep '{peer_cur}'", shell=True) == 0: - print('[Warning]veth exist:', peer_cur) - subprocess.check_call(('ip', 'link', 'del', peer_cur)) - subprocess.check_call( - ('ip', 'link', 'add', cur_peer, 'type', 'veth', 'peer', 'name', peer_cur)) - subprocess.check_call(('ip', 'link', 'set', cur_peer, 'netns', cur_name)) - subprocess.check_call(('ip', 'link', 'set', peer_cur, 'netns', peer_name)) - subprocess.check_call( - ('ip', 'netns', 'exec', cur_name, - 'ip', 'addr', 'add', prefix+'.40/24', 'dev', cur_peer)) - subprocess.check_call( - ('ip', 'netns', 'exec', cur_name, - 'tc', 'qdisc', 'add', 'dev', cur_peer, 'root', - # 'netem', 'loss', loss+'%', 'rate', bw+'Gbps')) - 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbps')) +def _parse_gsls(path): + del_lst, update_lst, add_lst = [], [], [] + f = open(path, 'r') + for gid, line in enumerate(f): + if len(line) == 0 or line.isspace(): + continue + toks = line.strip().split('|') + if len(toks[1]) > 0: + for isl in toks[1].split(' '): + i_s_o_s_d = isl.split(',') + idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) + oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) + del_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + if len(toks[2]) > 0: + for isl in toks[2].split(' '): + i_s_o_s_d = isl.split(',') + idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) + oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) + update_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + if len(toks[3]) > 0: + for isl in toks[3].split(' '): + i_s_o_s_d = isl.split(',') + idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) + oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) + add_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + f.close() + return del_lst, update_lst, add_lst + +# name1 in local machine +def _del_link(idx, name1, name2): + n1_n2 = f"{idx}-{name2}" + fd = os.open('/run/netns/' + name1, os.O_RDONLY) + libc.setns(fd, CLONE_NEWNET) + os.close(fd) + subprocess.check_call(('ip', 'link', 'del', n1_n2)) + +def _init_if(name, if_name, addr, delay, bw, loss): + fd = os.open('/run/netns/' + name, os.O_RDONLY) + libc.setns(fd, CLONE_NEWNET) + os.close(fd) + subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) subprocess.check_call( - ('ip', 'netns', 'exec', cur_name, 'ip', 'link', 'set', cur_peer, 'up')) + ('tc', 'qdisc', 'add', 'dev', if_name, 'root', + 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbit') + ) + subprocess.check_call(('ip', 'link', 'set', if_name, 'up')) +def _update_if(name, if_name, delay, bw, loss): + fd = os.open('/run/netns/' + name, os.O_RDONLY) + libc.setns(fd, CLONE_NEWNET) + os.close(fd) subprocess.check_call( - ('ip', 'netns', 'exec', peer_name, - 'ip', 'addr', 'add', prefix+'.10/24', 'dev', peer_cur)) - subprocess.check_call( - ('ip', 'netns', 'exec', peer_name, - 'tc', 'qdisc', 'add', 'dev', peer_cur, 'root', - # 'netem', 'loss', loss+'%', 'rate', bw+'Gbps')) - 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbps')) + ('tc', 'qdisc', 'change', 'dev', if_name, 'root', + 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', loss + '%') + ) + +def _update_link_intra_machine(idx, name1, name2, delay, bw, loss): + n1_n2 = f"{idx}-{name2}" + n2_n1 = f"{idx}-{name1}" + _update_if(name1, n1_n2, delay, bw, loss) + _update_if(name2, n2_n1, delay, bw, loss) + +# name1 in local machine +def _update_link_local(idx, name1, name2, delay, bw, loss): + n1_n2 = f"{idx}-{name2}" + _update_if(name1, n1_n2, delay, bw, loss) + +def _add_link_intra_machine(idx, name1, name2, prefix, delay, bw, loss): + n1_n2 = f"{idx}-{name2}" + n2_n1 = f"{idx}-{name1}" subprocess.check_call( - ('ip', 'netns', 'exec', peer_name, 'ip', 'link', 'set', peer_cur, 'up')) - for netns in (cur_name, peer_name): - os.remove(f'/run/netns/{netns}') + ('ip', 'link', 'add', n1_n2, 'netns', name1, + 'type', 'veth', 'peer', n2_n1, 'netns', name2) + ) + _init_if(name1, n1_n2, prefix+'.10/24', delay, bw, loss) + _init_if(name2, n2_n1, prefix+'.40/24', delay, bw, loss) -def sn_ISL_inter_machine(isl_idx, orbit, sat, peer_orbit, peer_sat, delay, bw, loss): - raise NotImplementedError +def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix, delay, bw, loss): + n1_n2 = f"{idx}-{name2}" + n2_n1 = f"{idx}-{name1}" + subprocess.check_call( + ('ip', 'link', 'add', n1_n2, 'type', 'vxlan', + 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) + ) + _init_if(name1, n1_n2, prefix+'.10/24', delay, bw, loss) + _init_if(name2, n2_n1, prefix+'.40/24', delay, bw, loss) -def sn_GSL_intra_machine(gsl_idx, orbit, sat, gs, delay, bw, loss): - raise NotImplementedError +def sn_init_nodes(dir, gs_mid, sat_mid_lst): + def _load_netns(pid, name): + netns_link = f'/run/netns/{name}' + if not os.path.exists(netns_link): + subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) -def sn_init_networks(matrix, bw, loss, orbit_num, sat_assign_lst, gs_assign_lst): - sat_num = len(sat_assign_lst) - for sat_id, sat_assign in enumerate(sat_assign_lst): - if sat_assign != machine_id: + for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): + if all(assign != machine_id for assign in sat_mid): continue - print('sat:', sat_id) - for orbit_id in range(orbit_num): - cur_idx = orbit_id * sat_num + sat_id - right_sat, right_orbit = sn_right_satellite(sat_id, orbit_id, orbit_num) - down_sat, down_orbit = sn_down_satellite(sat_id, orbit_id, sat_num) - - isl_indices = (cur_idx << 1, (cur_idx <<1) + 1) - isl_orbits = (right_orbit, down_orbit) - isl_sats = (right_sat, down_sat) - for isl_idx, isl_orbit, isl_sat in zip(isl_indices, isl_orbits, isl_sats): - if sat_assign_lst[isl_sat] == sat_assign: - sn_ISL_intra_machine( - isl_idx, orbit_id, sat_id, isl_orbit, isl_sat, - matrix[cur_idx][isl_orbit * sat_num + isl_sat], bw, loss + shell_dir = f"{dir}/{shell_name}" + overlay_dir = shell_dir + '/overlay' + os.makedirs(overlay_dir, exist_ok=True) + pid_file = open(shell_dir + '/' + PID_FILENAME, 'w', encoding='utf-8') + for sid, assign in enumerate(sat_mid): + if assign != machine_id: + pid_file.write(' '.join(NOT_ASSIGNED for _ in range(orbit_num)) + '\n') + continue + print(f'[{machine_id}] Satellite: {shell_id},{sid},(0-{orbit_num})') + for oid in range(orbit_num): + name = _sat_name(shell_id, oid, sid) + node_dir = f'{overlay_dir}/{name}' + pid_file.write(str(pyctr.container_run(node_dir, name)) + ' ') + pid_file.write('\n') + pid_file.close() + if len(gs_mid) > 0 and any(assign == machine_id for assign in gs_mid): + gs_dir = f"{dir}/GS-{len(gs_mid)}" + overlay_dir = gs_dir + '/overlay' + os.makedirs(overlay_dir, exist_ok=True) + pid_file = open(gs_dir + '/' + PID_FILENAME, 'w', encoding='utf-8') + for gid, assign in enumerate(gs_mid): + if assign != machine_id: + pid_file.write(NOT_ASSIGNED + ' ') + continue + print(f'[{machine_id}] GS: {gid}/{len(gs_mid)}') + name = _gs_name(gid) + node_dir = f'{overlay_dir}/{name}' + pid_file.write(str(pyctr.container_run(node_dir, name)) + ' ') + pid_file.write('\n') + pid_file.close() + sn_operate_every_node(dir, sat_mid_lst, gs_mid, _load_netns) + +def sn_update_network( + dir, ts, sat_mid_lst, gs_mid, ip_lst, + isl_bw, isl_loss, gsl_bw, gsl_loss + ): + for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): + shell_dir = f"{dir}/{shell_name}" + if not os.path.exists(shell_dir): + continue + del_cnt, update_cnt, add_cnt = 0, 0, 0 + del_lst, update_lst, add_lst = _parse_isls(f'{shell_dir}/{ts}.txt') + for idx, oid, sid, isl_oid, isl_sid, delay in del_lst: + if sat_mid[sid] == machine_id: + del_cnt += 1 + _del_link( + idx, + _sat_name(shell_id, oid, sid), + _sat_name(shell_id, isl_oid, isl_sid) + ) + elif sat_mid[isl_sid] == machine_id: + del_cnt += 1 + _del_link( + idx, + _sat_name(shell_id, isl_oid, isl_sid), + _sat_name(shell_id, oid, sid) + ) + for idx, oid, sid, isl_oid, isl_sid, delay in update_lst: + if sat_mid[sid] == machine_id: + update_cnt += 1 + if sat_mid[isl_sid] == machine_id: + _update_link_intra_machine( + idx, + _sat_name(shell_id, oid, sid), + _sat_name(shell_id, isl_oid, isl_sid), + delay, isl_bw, isl_loss ) else: - sn_ISL_inter_machine( - isl_idx, orbit_id, sat_id, isl_orbit, isl_sat, - matrix[cur_idx][isl_orbit * sat_num + isl_sat], bw, loss + _update_link_local( + idx, + _sat_name(shell_id, oid, sid), + _sat_name(shell_id, isl_oid, isl_sid), + delay, isl_bw, isl_loss ) - - # for gs_id, gs_assign in enumerate(gs_assign_lst): - # if gs_assign != machine_id: - # continue - # print('gs:', gs_id) - # for sat_id, sat_assign in enumerate(sat_assign_lst): + elif sat_mid[isl_sid] == machine_id: + update_cnt += 1 + _update_link_local( + idx, + _sat_name(shell_id, isl_oid, isl_sid), + _sat_name(shell_id, oid, sid), + delay, isl_bw, isl_loss + ) + for idx, oid, sid, isl_oid, isl_sid, delay in add_lst: + if sat_mid[sid] == machine_id: + add_cnt += 1 + if sat_mid[isl_sid] == machine_id: + _add_link_intra_machine( + idx, + _sat_name(shell_id, oid, sid), + _sat_name(shell_id, isl_oid, isl_sid), + f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss + ) + else: + _add_link_inter_machine( + idx, + _sat_name(shell_id, oid, sid), + _sat_name(shell_id, isl_oid, isl_sid), + ip_lst[sat_mid[isl_sid]], + f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss + ) + elif sat_mid[isl_sid] == machine_id: + add_cnt += 1 + _add_link_inter_machine( + idx, + _sat_name(shell_id, isl_oid, isl_sid), + _sat_name(shell_id, oid, sid), + ip_lst[sat_mid[sid]], + f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss + ) + print(f"[{machine_id}] Shell {shell_id}:", + f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") + + gs_dir = f"{dir}/GS-{len(gs_mid)}" + # return + if not os.path.exists(gs_dir): + return + del_cnt, update_cnt, add_cnt = 0, 0, 0 + del_lst, update_lst, add_lst = _parse_gsls(f'{gs_dir}/{ts}.txt') + for idx, gid, shell_id, oid, sid, delay in del_lst: + orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] + if gs_mid[gid] == machine_id: + del_cnt += 1 + _del_link(idx, _gs_name(gid), _sat_name(shell_id, oid, sid)) + elif sat_mid[sid] == machine_id: + del_cnt += 1 + _del_link(idx, _sat_name(shell_id, isl_oid, isl_sid), _gs_name(gid)) + for idx, gid, shell_id, oid, sid, delay in update_lst: + orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] + if gs_mid[gid] == machine_id: + update_cnt += 1 + if sat_mid[sid] == machine_id: + _update_link_intra_machine( + idx, + _gs_name(gid), _sat_name(shell_id, oid, sid), + delay, gsl_bw, gsl_loss + ) + else: + _update_link_local( + idx, + _gs_name(gid), _sat_name(shell_id, oid, sid), + delay, gsl_bw, gsl_loss + ) + elif sat_mid[sid] == machine_id: + update_cnt += 1 + _update_link_local( + idx, + _sat_name(shell_id, oid, sid), _gs_name(gid), + delay, gsl_bw, gsl_loss + ) + for idx, gid, shell_id, oid, sid, delay in add_lst: + orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] + if gs_mid[gid] == machine_id: + add_cnt += 1 + if sat_mid[sid] == machine_id: + _add_link_intra_machine( + idx, + _gs_name(gid), _sat_name(shell_id, oid, sid), + f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss + ) + else: + _add_link_inter_machine( + idx, + _gs_name(gid), _sat_name(shell_id, oid, sid), ip_lst[sat_mid[sid]], + f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss + ) + elif sat_mid[sid] == machine_id: + add_cnt += 1 + _add_link_inter_machine( + idx, + _sat_name(shell_id, oid, sid), _gs_name(gid), ip_lst[gs_mid[gid]], + f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss + ) + print(f"[{machine_id}] GSL:", + f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") -def sn_establish_GSL(netns_list, matrix, GS_num, bw, loss): - # starting links among satellites and ground stations - constellation_size = orbit_num * sat_num - for sat_id in range(constellation_size): - for gs_id in range(constellation_size, constellation_size + GS_num): - # matrix[i1][j1])==1 means a link between node i and node j - delay = matrix[sat_id][gs_id] - if float(delay) <= 0.01: - continue - # IP address (there is a link between i and j) - address_16_23 = (gs_id - constellation_size) & 0xff - address_8_15 = sat_id & 0xff - prefix = f'9.{address_16_23}.{address_8_15}' - GSL_name = f'GSL_{sat_id + 1}-{gs_id + 1}' - sat2gs = f'B{sat_id + 1}-eth{gs_id + 1}' - gs2sat = f'B{gs_id + 1}-eth{sat_id + 1}' - # Create internal network in docker. - print(f"[Create {GSL_name}] {prefix}.0/24") - subprocess.check_call( - f"ip link add {sat2gs} type veth peer name {gs2sat} ", shell=True) - subprocess.check_call( - f"ip link set {sat2gs} netns {netns_list[sat_id]}", shell=True) - subprocess.check_call( - f"ip link set {gs2sat} netns {netns_list[gs_id]}", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[sat_id]} " - f"ip addr add {prefix}.50/24 dev {sat2gs}", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[sat_id]} " - f"tc qdisc add dev {sat2gs} root " - f"netem delay {delay}ms loss {loss}% rate {bw}Gbps", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[sat_id]} " - f"ip link set dev {sat2gs} up", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[gs_id]} " - f"ip addr add {prefix}.60/24 dev {gs2sat}", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[gs_id]} " - f"tc qdisc add dev {gs2sat} root " - f"netem delay {delay}ms loss {loss}% rate {bw}Gbps", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[gs_id]} " - f"ip link set dev {gs2sat} up", shell=True) - for gs_id in range(constellation_size, constellation_size + GS_num): - GS_name = f"GS_{gs_id + 1}" - # Create default network and interface for GS. - # TODO limit GS size to 256 - prefix = f'9.{gs_id - constellation_size}.{gs_id - constellation_size}' - gs_int = f'B{gs_id + 1}-default' - print(f"[Create {GS_name}] {prefix}.0/24") - subprocess.check_call( - f"ip netns exec {netns_list[gs_id]} " - f"ip link add {gs_int} type dummy", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[gs_id]} " - f"ip addr add {prefix}.10/24 dev {gs_int}", shell=True) - subprocess.check_call( - f"ip netns exec {netns_list[gs_id]} " - f"ip link set dev {gs_int} up", shell=True) +def sn_container_check_call(pid, cmd): + subprocess.check_call( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd) + ) -def sn_container_exec(pid, cmd): - return pyctr.container_exec(pid, tuple(arg.encode() for arg in cmd)) +def sn_container_check_output(pid, cmd): + return subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd) + ) -def sn_init_route_daemon(sat_assign_lst, bird_conf): - for sat_id, sat_assign in enumerate(sat_assign_lst): - if sat_assign != machine_id: +def sn_operate_every_node(dir, sat_mid_lst, gs_mid, func, *args): + for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): + pid_file = f"{dir}/{shell_name}/{PID_FILENAME}" + if not os.path.exists(pid_file): continue - print('sat:', sat_id) - for orbit_id in range(orbit_num): - sn_container_exec( - int(_get_init_pid(orbit_id, sat_id)), - ('bird', '-c', bird_conf) - ) - -def sn_clear(dir): - pid_path = dir + '/' + PID_FILENAME - if os.path.exists(pid_path): - pid_file = open(pid_path, 'r', encoding='utf-8') - for line in pid_file: - for pid in line.strip().split(' '): + pid_mat = _pid_matrix(pid_file) + for sid, pid_lst in enumerate(pid_mat): + for oid, pid in enumerate(pid_lst): if pid == NOT_ASSIGNED: continue - os.kill(int(pid), 9) - pid_file.close() - os.remove(pid_path) - overlay_dir = dir + '/overlay' - if not os.path.exists(overlay_dir): + func(pid, _sat_name(shell_id, oid, sid), *args) + gs_dir = f"{dir}/GS-{len(gs_mid)}" + if not os.path.exists(gs_dir): return - for entr in os.listdir(overlay_dir): - merge_dir = f'{overlay_dir}/{entr}/rootfs' - if os.system(f"mountpoint -q {merge_dir}") == 0: - subprocess.check_call(('umount', merge_dir)) - -def sn_damage_link(orbit_id, sat_id): - - with os.popen( - "docker exec -it " + str(container_id_list[sat_index]) + - " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") as f: - ifconfig_output = f.readlines() - for intreface in range(0, len(ifconfig_output), 2): - subprocess.check_call("docker exec -d " + str(container_id_list[sat_index], shell=True) + - " tc qdisc change dev " + - ifconfig_output[intreface][:-1] + - " root netem loss 100%") - print("docker exec -d " + str(container_id_list[sat_index]) + - " tc qdisc change dev " + ifconfig_output[intreface][:-1] + - " root netem loss 100%") - -def sn_damage(random_list, container_id_list): - pool = ThreadPoolExecutor() - for random_satellite in random_list: - pool.submit(sn_damage_link, int(random_satellite), container_id_list) - pool.shutdown(wait=True) - - -def sn_recover_link( - damaged_satellite, - container_id_list, - sat_loss, -): - with os.popen( - "docker exec -it " + str(container_id_list[damaged_satellite]) + - " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") as f: - ifconfig_output = f.readlines() - for i in range(0, len(ifconfig_output), 2): - subprocess.check_call("docker exec -d " + - str(container_id_list[damaged_satellite], shell=True) + - " tc qdisc change dev " + ifconfig_output[i][:-1] + - " root netem loss " + str(sat_loss) + "%") - print("docker exec -d " + - str(container_id_list[damaged_satellite]) + - " tc qdisc change dev " + ifconfig_output[i][:-1] + - " root netem loss " + str(sat_loss) + "%") - - -def sn_del_network(network_name): - subprocess.check_call('docker network rm ' + network_name, shell=True) - - -def sn_stop_emulation(): - # subprocess.check_call("docker service rm constellation-test", shell=True) - # with os.popen("docker rm -f $(docker ps -a -q)") as f: - # f.readlines() - subprocess.check_call("for COMPOSE in $(docker compose ls | grep grid | awk '{print $3}', shell=True);" - "do docker compose -f $COMPOSE down 2>/dev/null;" - "done") - subprocess.check_call( - "for NETWORK in $(docker network ls | grep -o '\(La\|Le\|GS\, shell=True)\S*');" - "do docker network rm $NETWORK;" - "done" + pid_mat = _pid_matrix(gs_dir + '/' + PID_FILENAME) + assert len(pid_mat) == 1 + for gid, pid in enumerate(pid_mat[0]): + if pid == NOT_ASSIGNED: + continue + func(int(pid), _gs_name(gid), *args) + +def sn_init_route_daemons(dir, sat_mid_lst, gs_mid, conf_path): + def _init_route_daemon(pid, name): + bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' + sn_container_check_call(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) + sn_operate_every_node(dir, sat_mid_lst, gs_mid, _init_route_daemon) + +def sn_ping(dir,sat_mid_lst, src_shell, src_oid, src_sid, dst_shell, dst_oid, dst_sid): + # suppose src in this machine + mat1 = _pid_matrix(f"{dir}/{sat_mid_lst[src_shell][1]}/{PID_FILENAME}") + # TODO: dst in other machine + mat2 = _pid_matrix(f"{dir}/{sat_mid_lst[dst_shell][1]}/{PID_FILENAME}") + dst_addr = subprocess.check_output( + "nsenter -m -u -i -n -p -t " + mat2[dst_sid][dst_oid] + " ip -br addr " + "| awk '$1!=\"lo\"{print $3}'", shell=True + ).decode().splitlines()[0] + dst_addr = dst_addr[:dst_addr.rfind('/')] + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', mat2[src_sid][src_oid], + 'ping', '-c', '4', '-i', '0.01', dst_addr), + stdout=sys.stdout, stderr=subprocess.STDOUT ) +def sn_clean(dir): + for ns_link in glob.glob(f"/run/netns/SH*O*S*"): + if os.path.islink(ns_link): + os.remove(ns_link) + for ns_link in glob.glob(f"/run/netns/G*"): + if os.path.islink(ns_link): + os.remove(ns_link) + for pid_file in glob.glob(f"{dir}/[0-9]*/{PID_FILENAME}"): + pid_mat = _pid_matrix(pid_file, True) + for sid, pid_lst in enumerate(pid_mat): + for oid, pid in enumerate(pid_lst): + if pid == NOT_ASSIGNED: + continue + try: + os.kill(int(pid), 9) + except ProcessLookupError: + pass + os.remove(pid_file) + for pid_file in glob.glob(f"{dir}/GS-*/{PID_FILENAME}"): + pid_mat = _pid_matrix(pid_file, True) + for pid_lst in pid_mat: + for gid, pid in enumerate(pid_lst): + if pid == NOT_ASSIGNED: + continue + try: + os.kill(int(pid), 9) + except ProcessLookupError: + pass + os.remove(pid_file) + +def _change_sat_link_loss(pid, loss): + out = sn_container_check_output(pid, ('ip', '-br', 'link', 'show')).decode() + for line in out.splitlines(): + line = line.strip() + if len(line) == 0 or line.startswith('lo'): + continue + dev_name = line.split('@')[0] + sn_container_check_call( + pid, + ('tc', 'qdisc', 'change', 'dev', dev_name, 'root', 'netem', 'loss', loss) + ) -def sn_recover(damage_list, container_id_list, sat_loss): - pool = ThreadPoolExecutor() - for damaged_satellite in damage_list: - pool.submit(sn_recover_link, - int(damaged_satellite), container_id_list, sat_loss) - pool.shutdown(wait=True) - - -# updating delays -def sn_update_delay(matrix, container_id_list, constellation_size): - pool = ThreadPoolExecutor() - for row in range(len(matrix)): - for col in range(row, len(matrix[row])): - if float(matrix[row][col]) <= 0: - continue - - if row < col: - pool.submit(sn_delay_change, - row, col, matrix[row][col], - container_id_list, constellation_size) - else: - pool.submit(sn_delay_change, - col, row, matrix[col][row], - container_id_list, constellation_size) - pool.shutdown(wait=True) - print("Delay updating done.\n") - - -def sn_delay_change(link_x, link_y, delay, container_id_list, - constellation_size): # multi-thread updating delays - if link_y <= constellation_size: - subprocess.check_call("docker exec -d " + str(container_id_list[link_x], shell=True) + - " tc qdisc change dev B" + str(link_x + 1) + "-eth" + - str(link_y + 1) + " root netem delay " + str(delay) + "ms") - subprocess.check_call("docker exec -d " + str(container_id_list[link_y], shell=True) + - " tc qdisc change dev B" + str(link_y + 1) + "-eth" + - str(link_x + 1) + " root netem delay " + str(delay) + "ms") - else: - subprocess.check_call("docker exec -d " + str(container_id_list[link_x], shell=True) + - " tc qdisc change dev B" + str(link_x + 1) + "-eth" + - str(link_y + 1) + " root netem delay " + str(delay) + "ms") - subprocess.check_call("docker exec -d " + str(container_id_list[link_y], shell=True) + - " tc qdisc change dev B" + str(link_y + 1) + "-eth" + - str(link_x + 1) + " root netem delay " + str(delay) + "ms") - +def sn_damage(random_list, sat_mid_lst): + for shell_id, oid, sid in random_list: + pid_file = f"{dir}/{sat_mid_lst[shell_id][1]}/{PID_FILENAME}" + if not os.path.exists(pid_file): + continue + pid_mat = _pid_matrix(pid_file) + pid = pid_mat[sid][oid] + _change_sat_link_loss(pid, '100%') + print(f'[{machine_id}] damage sat: {shell_id},{oid},{sid}') + +def sn_recover(damage_list, sat_mid_lst, sat_loss): + for shell_id, oid, sid in damage_list: + pid_file = f"{dir}/{sat_mid_lst[shell_id][1]}/{PID_FILENAME}" + if not os.path.exists(pid_file): + continue + pid_mat = _pid_matrix(pid_file) + pid = pid_mat[sid][oid] + _change_sat_link_loss(pid, sat_loss) + print(f'[{machine_id}] recover sat: {shell_id},{oid},{sid}') if __name__ == '__main__': - machine_id = 0 - workdir = sys.argv[2] - orbit_num, sat_assign_lst, gs_assign_lst = _get_params( - workdir + '/' + ASSIGN_FILENAME) - if sys.argv[1] == 'nodes': - sn_init_nodes(workdir, orbit_num, sat_assign_lst, gs_assign_lst) - elif sys.argv[1] == 'networks': - sn_init_networks( - _get_delay(workdir + '/1.txt.gz'), sys.argv[3], sys.argv[4], - orbit_num, sat_assign_lst, gs_assign_lst + # C module + import pyctr + machine_id = int(sys.argv[1]) + _mat_cache = {} + cmd = sys.argv[2] + workdir = sys.argv[3] + gs_mid, sat_mid_lst, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) + if cmd == 'nodes': + sn_clean(workdir) + sn_init_nodes(workdir, gs_mid, sat_mid_lst) + elif cmd == 'networks': + # lp = LineProfiler() + # sn_update_network = lp(sn_update_network) + # lp.add_function(_update_link_intra_machine) + sn_update_network( + workdir, sys.argv[4], sat_mid_lst, gs_mid, ip_lst, + sys.argv[5], sys.argv[6], sys.argv[7], sys.argv[8] + ) + # with open('report.txt', 'w') as f: + # lp.print_stats(f) + elif cmd == 'routed': + sn_init_route_daemons(workdir, sat_mid_lst, gs_mid, workdir + '/bird.conf') + elif cmd == 'clean': + sn_clean(workdir) + elif cmd == 'ping': + sn_ping( + workdir, sat_mid_lst, + int(sys.argv[4]), int(sys.argv[5]), int(sys.argv[6]), + int(sys.argv[7]), int(sys.argv[8]), int(sys.argv[9]), ) - elif sys.argv[1] == 'routed': - sn_init_route_daemon(sat_assign_lst, workdir + '/bird.conf') - elif sys.argv[1] == 'clean': - sn_clear(workdir) + elif cmd == 'perf': + pass else: print('Unknown command') diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index d536017..1d63405 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -6,14 +6,56 @@ """ import time import threading +import zipfile from starrynet.sn_observer import * from starrynet.sn_utils import * ASSIGN_FILENAME = 'assign.txt' +BIRD_CONF_TEXT = """\ +log "/var/log/bird.log" { warning, error, auth, fatal, bug }; +protocol device { +} +protocol direct { + disabled; # Disable by default + ipv4; # Connect to default IPv4 table + ipv6; # ... and to default IPv6 table +} +protocol kernel { + ipv4 { # Connect protocol to IPv4 table by channel + export all; # Export to protocol. default is export none + }; +} +# protocol static { +# ipv4; # Again, IPv6 channel with default options +# } +protocol ospf{ + ipv4 { + import all; + }; + area 0 { + interface "*-SH*O*S*" { + type broadcast; # Detected by default + cost 256; + hello 5; # Default hello perid 10 is too long + }; + interface "*-GS*" { + type broadcast; # Detected by default + cost 256; + hello 5; # Default hello perid 10 is too long + }; + interface "B*-default" { + type broadcast; # Detected by default + cost 256; + hello 10; # Default hello perid 10 is too long + }; + }; +} +""" + class RemoteMachine: - def __init__(self, id, host, port, username, password, + def __init__(self, id, host, port, username, password, shell_lst, experiment_name, local_dir, gs_dirname): self.id = id self.shell_lst = shell_lst @@ -25,22 +67,20 @@ def __init__(self, id, host, port, username, password, username = username, password = password, ) - sn_remote_cmd(self.ssh, f'export MACHINE_ID={self.id}') sn_remote_cmd(self.ssh, 'mkdir ~/' + experiment_name) self.dir = sn_remote_cmd(self.ssh, 'echo ~/' + experiment_name) self.sftp.put( - os.path.join(os.path.dirname(__file__), 'orchestrater.py'), - self.dir + '/orchestrater.py' - ) - - self.sftp.put( - os.path.join(os.path.dirname(__file__), 'pyctr.so'), - self.dir + '/pyctr.so' + os.path.join(os.path.dirname(__file__), 'sn_orchestrater.py'), + self.dir + '/sn_orchestrater.py' ) self.sftp.put( - os.path.join(os.path.dirname(__file__), 'setup.py'), - self.dir + '/setup.py' + os.path.join(self.local_dir, 'bird.conf'), + self.dir + '/bird.conf' ) + # self.sftp.put( + # os.path.join(self.local_dir, 'pyctr.so'), + # self.dir + '/pyctr.so' + # ) self.sftp.put( os.path.join(self.local_dir, ASSIGN_FILENAME), self.dir + '/' + ASSIGN_FILENAME @@ -49,40 +89,91 @@ def __init__(self, id, host, port, username, password, def init_nodes(self): sn_remote_wait_output( self.ssh, - f"python3 {self.dir}/orchestrater.py {self.id} nodes {self.dir}" + f"python3 {self.dir}/sn_orchestrater.py {self.id} nodes {self.dir}" ) - def create_network(self, bw, loss): + def init_network(self, isl_bw, isl_loss, gsl_bw, gsl_loss): for shell in self.shell_lst: - self.sftp.put( - os.path.join(self.local_dir, shell['name'], 'isl', '1.txt'), - f"{self.dir}/{shell['name']}/1.txt" - ) - sn_remote_wait_output( - self.ssh, - f"python3 {self.dir}/orchestrater.py isls {self.dir}/{shell['name']} " - f"1.txt {bw} {loss}" + rmt_path = f"{self.dir}/{shell['name']}.zip" + rmt_f = self.sftp.open(rmt_path, "wb") + zip_f = zipfile.ZipFile(rmt_f, mode='w') + pattern = os.path.join(self.local_dir, shell['name'], 'isl', '*.txt') + for isl_txt in glob.glob(pattern): + zip_f.write(isl_txt, f"{shell['name']}/{os.path.basename(isl_txt)}") + zip_f.close() + rmt_f.close() + sn_remote_cmd(self.ssh, f"python3 -m zipfile -e {rmt_path} {self.dir}") + if self.gs_dirname: + rmt_path = f"{self.dir}/{self.gs_dirname}.zip" + rmt_f = self.sftp.open(rmt_path, "wb") + zip_f = zipfile.ZipFile(rmt_f, mode='w') + pattern = os.path.join(self.local_dir, self.gs_dirname, 'gsl', '*.txt') + for gsl_txt in glob.glob(pattern): + zip_f.write( + gsl_txt, + f"{self.gs_dirname}/{os.path.basename(gsl_txt)}" + ) + zip_f.close() + rmt_f.close() + sn_remote_cmd(self.ssh, f"python3 -m zipfile -e {rmt_path} {self.dir}") + self.update_network(0, isl_bw, isl_loss, gsl_bw, gsl_loss) + + def update_network(self, t, isl_bw, isl_loss, gsl_bw, gsl_loss): + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} networks {self.dir} " + f"{t} {isl_bw} {isl_loss} {gsl_bw} {gsl_loss}" + ) + + def init_routed(self): + print(sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} routed {self.dir}" + )) + + def ping_async(self, res_path, src, dst): + def _ping_inner(ssh, dir, res_path, src, dst): + output = sn_remote_cmd( + ssh, + f"python3 {dir}/sn_orchestrater.py {self.id} ping {dir} " + f"{src[0]} {src[1]} {src[2]} {dst[0]} {dst[1]} {dst[2]}" ) - self.sftp.put( - os.path.join(self.local_dir, self.gs_dirname, 'GS', '1.txt'), - f"{self.dir}/{shell['name']}/1.txt" + with open(res_path, 'w') as f: + f.write(output) + thread = threading.Thread( + target=_ping_inner, + args=(self.ssh, self.dir, res_path, src, dst) ) - sn_remote_wait_output( + thread.start() + return thread + + def perf_async(self, src, dst): + thread = threading.Thread( + target=sn_remote_cmd, + args=(self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} perf {self.dir} " + f"{src[0]} {src[1]} {src[2]} {dst[0]} {dst[1]} {dst[2]}"), + ) + thread.start() + return thread + + def clean(self): + sn_remote_cmd( self.ssh, - f"python3 {self.dir}/orchestrater.py gsls {self.dir}/{self.gs_dirname} " + f"python3 {self.dir}/sn_orchestrater.py {self.id} clean {self.dir}" ) class StarryNet(): def __init__(self, configuration_file_path, GS_lat_long): # Initialize constellation information. - sn_args = sn_load_file(configuration_file_path, GS_lat_long) + sn_args = sn_load_file(configuration_file_path) self.shell_lst = sn_args.shell_lst self.gs_lat_long = GS_lat_long self.link_style = sn_args.link_style self.link_policy = sn_args.link_policy self.IP_version = sn_args.IP_version - self.update_interval = sn_args.update_interval + self.step = sn_args.step self.duration = sn_args.duration self.sat_bandwidth = sn_args.sat_bandwidth self.sat_ground_bandwidth = sn_args.sat_ground_bandwidth @@ -104,28 +195,20 @@ def __init__(self, configuration_file_path, GS_lat_long): self._init_local() # Initiate a necessary delay and position data for emulation calculate_delay( - self.local_dir, self.duration, self.shell_lst, self.link_style, + self.local_dir, self.duration, self.step, self.shell_lst, self.link_style, self.gs_lat_long, self.antenna_number, self.elevation, self.link_policy ) - self.remote_lst = self._assign_remote(sn_args.machine_lst) + (self.remote_lst, + self.sat_mid_lst, self.gs_mid) = self._assign_remote(sn_args.machine_lst) self.utility_checking_time = [] - self.ping_src = [] - self.ping_des = [] - self.ping_time = [] - self.perf_src = [] - self.perf_des = [] - self.perf_time = [] - self.sr_src = [] - self.sr_des = [] - self.sr_target = [] - self.sr_time = [] - self.damage_ratio = [] - self.damage_time = [] + self.route_checking_events = [] + self.ping_events = [] + self.perf_events = [] + self.sr_events = [] + self.damage_events = [] self.damage_list = [] - self.recovery_time = [] - self.route_src = [] - self.route_time = [] + self.recovery_events = [] def _init_local(self): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): @@ -133,6 +216,8 @@ def _init_local(self): for shell in self.shell_lst: os.makedirs(os.path.join(self.local_dir, shell['name']), exist_ok=True) os.makedirs(os.path.join(self.local_dir, self.gs_dirname), exist_ok=True) + with open(os.path.join(self.local_dir, 'bird.conf'), 'w') as f: + f.write(BIRD_CONF_TEXT) def _assign_remote(self, machine_lst): # TODO: better partition @@ -145,43 +230,64 @@ def _assign_remote(self, machine_lst): # only divide shell shell_per_machine = len(self.shell_lst) // len(machine_lst) remainder = len(self.shell_lst) % len(machine_lst) - shell_idx = 0 - sat_mask_lst = [] + + shell_id = 0 + sat_mid_lst = [] + assigned_shell_lst = [] for i, remote in enumerate(machine_lst): shell_num = shell_per_machine if i < remainder: shell_num += 1 assigned_shells = [ - self.shell_lst[j] for j in range(shell_idx, shell_idx + shell_num) + self.shell_lst[j] for j in range(shell_id, shell_id + shell_num) ] # all satellites of a shell assigned to a single machine - sat_mask_lst.extend([ + sat_mid_lst.extend([ (i,) * shell['sat'] for shell in assigned_shells ]) - shell_idx += shell_num + assigned_shell_lst.append(assigned_shells) + shell_id += shell_num + gs_mid = [] + # TODO: better ground station assign + with open(os.path.join(self.local_dir, self.gs_dirname,'gsl','0.txt'))as f: + for line in f: + line = line.strip() + if len(line) == 0: + continue + init = line.split('|')[3] + if len(init) == 0: + gs_mid.append(0) + continue + gsl = init.split(' ')[0].split(',') + shell_id, sid = int(gsl[1]), int(gsl[3]) + mid = sat_mid_lst[shell_id][sid] + gs_mid.append(mid) with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: - # TODO: ground station - f.write(' '.join(str(mid) for mid in []) + '\n') + f.write(' '.join(str(mid) for mid in gs_mid) + '\n') # every shell - for sat_mask, shell in zip(sat_mask_lst, self.shell_lst): + for sat_mid, shell in zip(sat_mid_lst, self.shell_lst): f.write( str(shell['orbit']) + ' ' + shell['name'] + '\n' - + ' '.join(str(mid) for mid in sat_mask) + '\n' - ) - for i, remote in enumerate(machine_lst): - remote_lst.append(RemoteMachine( - i, - remote['IP'], - remote['port'], - remote['username'], - remote['password'], - self.shell_lst, - self.experiment_name, - self.local_dir, - self.gs_dirname + + ' '.join(str(mid) for mid in sat_mid) + '\n' ) + f.write('\n') + for remote in machine_lst: + f.write(remote['IP'] + '\n') + + for i, remote in enumerate(machine_lst): + remote_lst.append(RemoteMachine( + i, + remote['IP'], + remote['port'], + remote['username'], + remote['password'], + assigned_shell_lst[i], + self.experiment_name, + self.local_dir, + self.gs_dirname if i in gs_mid else None ) - return remote_lst + ) + return remote_lst, sat_mid_lst, gs_mid def create_nodes(self): print('Initializing nodes ...') @@ -192,22 +298,33 @@ def create_nodes(self): def create_links(self): print('Initializing links ...') + thread_lst = [] begin = time.time() for remote in self.remote_lst: - remote.create_network() + thread = threading.Thread( + target=remote.init_network, + args=(self.sat_bandwidth, + self.sat_loss, + self.sat_ground_bandwidth, + self.sat_ground_loss), + ) + thread.start() + thread_lst.append(thread) + for thread in thread_lst: + thread.join() print("Link initialization:", time.time() - begin, 's consumed.') def run_routing_deamon(self): print('Initializing routing ...') - sn_remote_wait_output(self.remote_ssh, - f"python3 {self.remote_dir}/orchestrater.py routed {self.remote_dir} ") + for remote in self.remote_lst: + remote.init_routed() print("Routing daemon initialized. Wait 30s for route converged") for i in range(30): print(f'\r{i} / 30', end=' ') time.sleep(1) print("Routing started!") - def get_distance(self, sat1_index, sat2_index, time_index): + def get_distance(self, shell1, orbit1, sat1, shell2, orbit2, sat2, t): raise NotImplementedError def get_neighbors(self, sat_index, time_index): @@ -219,8 +336,8 @@ def get_GSes(self, sat_index, time_index): def get_utility(self, time_index): self.utility_checking_time.append(time_index) - def get_position(self, sat_index, time_index): - path = self.local_dir + '/position/' + '/%d.txt' % time_index + def get_position(self, shell1, orbit1, sat1, time_index): + path = self.local_dir + '/position/%d.txt' % time_index f = open(path) ADJ = f.readlines() f.close() @@ -229,175 +346,99 @@ def get_position(self, sat_index, time_index): def get_IP(self, sat_index): raise NotImplementedError - def set_damage(self, damaging_ratio, time_index): - self.damage_ratio.append(damaging_ratio) - self.damage_time.append(time_index) + def set_damage(self, damaging_ratio, t): + self.damage_events.append((t, damaging_ratio)) - def set_recovery(self, time_index): - self.recovery_time.append(time_index) + def set_recovery(self, t): + self.recovery_events.append(t) - def check_routing_table(self, sat_index, time_index): - self.route_src.append(sat_index) - self.route_time.append(time_index) + def check_routing_table(self, sat_index, t): + self.route_checking_events.append((t, sat_index)) - def set_next_hop(self, sat_index, des, next_hop_sat, time_index): - self.sr_src.append(sat_index) - self.sr_des.append(des) - self.sr_target.append(next_hop_sat) - self.sr_time.append(time_index) + def set_next_hop(self, + src_shell, src_orbit, src_sat, + des_shell, des_orbit, des_sat, + nxt_shell, nxt_orbit, nxt_sat, t + ): + self.sr_events.append( + (t, + (src_shell, src_orbit, src_sat), + (des_shell, des_orbit, des_sat), + (nxt_shell, nxt_orbit, nxt_sat)) + ) - def set_ping(self, sat1_index, sat2_index, time_index): - self.ping_src.append(sat1_index) - self.ping_des.append(sat2_index) - self.ping_time.append(time_index) + def set_ping(self, shell1, orbit1, sat1, shell2, orbit2, sat2, t): + self.ping_events.append( + (t, (shell1-1, orbit1-1, sat1-1), (shell2-1, orbit2-1, sat2-1)) + ) - def set_perf(self, sat1_index, sat2_index, time_index): - self.perf_src.append(sat1_index) - self.perf_des.append(sat2_index) - self.perf_time.append(time_index) + def set_perf(self, shell1, orbit1, sat1, shell2, orbit2, sat2, t): + self.perf_events.append( + (t, (shell1-1, orbit1-1, sat1-1), (shell2-1, orbit2-1, sat2-1)) + ) - def event(self, timeptr): - if timeptr in self.utility_checking_time: - sn_check_utility(timeptr, self.remote_ssh, self.local_dir) - if timeptr % self.update_interval == 0: - # updating link delays after link changes - sn_update_delay(self.file_path, - self.configuration_dir, timeptr, - self.constellation_size, - self.remote_ssh, self.remote_ftp) - if timeptr in self.damage_time: - sn_damage( - self.damage_ratio[self.damage_time.index(timeptr)], - self.damage_list, self.constellation_size, - self.remote_ssh, self.remote_ftp, self.file_path, - self.configuration_dir) - if timeptr in self.recovery_time: - sn_recover(self.damage_list, self.sat_loss, - self.remote_ssh, self.remote_ftp, - self.file_path, - self.configuration_dir) - for i, val in enumerate(self.sr_time): - if val != timeptr: - continue - sn_sr(self.sr_src[i], - self.sr_des[i], - self.sr_target[i], - self.container_id_list, self.remote_ssh) - for i, val in enumerate(self.ping_time): - if val != timeptr: - continue - ping_thread = threading.Thread( - target=sn_ping, - args=(self.ping_src[i], - self.ping_des[i], - self.ping_time[i], - self.constellation_size, - self.container_id_list, - self.file_path, - self.configuration_dir, - self.remote_ssh)) - ping_thread.start() - self.ping_threads.append(ping_thread) - for i, val in enumerate(self.perf_time): - if val != timeptr: - continue - perf_thread = threading.Thread( - target=sn_perf, - args=(self.perf_src[i], - self.perf_des[i], - self.perf_time[i], - self.constellation_size, - self.container_id_list, - self.file_path, - self.configuration_dir, - self.remote_ssh)) - perf_thread.start() - self.perf_threads.append(perf_thread) - for i, val in enumerate(self.route_time): - if val != timeptr: - continue - sn_route(self.route_src[i], - self.route_time[i], - self.file_path, - self.configuration_dir, - self.container_id_list, self.remote_ssh) + def event(self, t): + while len(self.ping_events) > 0 and self.ping_events[-1][0] <= t: + ping_event = self.ping_events.pop(-1) + src_shell, src_sid = ping_event[1][0], ping_event[1][2] + machine = self.remote_lst[self.sat_mid_lst[src_shell][src_sid]] + self.ping_threads.append(machine.ping_async( + os.path.join( + self.local_dir, f'{t}-ping-{ping_event[1]}-{ping_event[2]}.txt'), + ping_event[1], + ping_event[2] + )) + while len(self.perf_events) > 0 and self.perf_events[-1][0] <= t: + perf_event = self.perf_events.pop(-1) + machine = self.remote_lst[self.sat_mid_lst[perf_event[1][0]]] + self.perf_threads.append(machine.perf_async( + perf_event[1], + perf_event[2] + )) + # TODO: other events def start_emulation(self): + self.ping_events.sort(key=lambda x:x[0], reverse=True) + self.perf_events.sort(key=lambda x:x[0], reverse=True) self.ping_threads = [] self.perf_threads = [] - timeptr = 2 # current emulating time - topo_change_file_path = os.path.join(self.local_dir, 'Topo_leo_change.txt') - fi = open(topo_change_file_path, 'r') - line = fi.readline() - while line: # starting reading change information and emulating - words = line.split() - if words[0] == 'time': - print('Emulation in No.' + str(timeptr) + ' second.') - # the time when the new change occurrs - current_time = str(int(words[1][:-1])) - while int(current_time) > timeptr: - start_time = time.time() - self.event(timeptr) - timeptr += 1 - end_time = time.time() - passed_time = ( - end_time - - start_time) if (end_time - start_time) < 1 else 1 - time.sleep(1 - passed_time) - if timeptr >= self.duration: - return - print('Emulation in No.' + str(timeptr) + ' second.') - print("A change in time " + current_time + ':') - line = fi.readline() - words = line.split() - line = fi.readline() - line = fi.readline() - words = line.split() - while words[0] != 'del:': # addlink - word = words[0].split('-') - s = int(word[0]) - f = int(word[1]) - if s > f: - s, f = f, s - print("add link", s, f) - current_topo_path = self.configuration_dir + "/" + self.file_path + '/delay/' + str( - current_time) + '.txt.gz' - matrix = sn_get_param(current_topo_path) - sn_establish_new_GSL(self.container_id_list, matrix, - self.constellation_size, - self.sat_ground_bw, - self.sat_ground_loss, s, f, - self.remote_ssh) - line = fi.readline() - words = line.split() - line = fi.readline() - words = line.split() - if len(words) == 0: - return - while words[0] != 'time': # delete link - word = words[0].split('-') - s = int(word[0]) - f = int(word[1]) - if s > f: - s, f = f, s - print("del link " + str(s) + "-" + str(f) + "\n") - sn_del_link(s, f, self.container_id_list, self.remote_ssh) - line = fi.readline() - words = line.split() - if len(words) == 0: - return - self.event(timeptr) - timeptr += 1 # current emulating time - if timeptr >= self.duration: - return - fi.close() + t = 0.0 + tid = 1 + while t < self.duration: + start = time.time() + print("Trigger events at", t, "s ...") + self.event(t) + print("Update networks ...") + update_start = time.time() + if tid < self.duration: + conn_threads = [] + for remote in self.remote_lst: + thread = threading.Thread( + target=remote.update_network, + args=(tid, + self.sat_bandwidth, + self.sat_loss, + self.sat_ground_bandwidth, + self.sat_ground_loss + )) + thread.start() + conn_threads.append(thread) + for thread in conn_threads: + thread.join() + end = time.time() + print(end-start, "s elapsed,", end-update_start, "s for network update") + if end - start < 1: + print('Sleep', 1 + start - end, 's') + time.sleep(1 + start - end) + t += self.step + tid += 1 for ping_thread in self.ping_threads: ping_thread.join() for perf_thread in self.perf_threads: perf_thread.join() - def stop_emulation(self): - print("Removing containers...") - sn_remote_cmd(self.remote_ssh, - f"python3 {self.remote_dir}/orchestrater.py clean {self.remote_dir}" - ) + def clean(self): + print("Removing containers and links...") + for remote in self.remote_lst: + remote.clean() + print("All containers and links remoted.") diff --git a/starrynet/sn_utils.py b/starrynet/sn_utils.py index 94d1b15..12ec1ad 100644 --- a/starrynet/sn_utils.py +++ b/starrynet/sn_utils.py @@ -1,12 +1,11 @@ import os -import glob import json import argparse import paramiko import numpy import random -def sn_load_file(path, GS_lat_long): +def sn_load_file(path): f = open(path, 'r', encoding='utf8') table = json.load(f) parser = argparse.ArgumentParser(description='manual to this script') @@ -15,9 +14,9 @@ def sn_load_file(path, GS_lat_long): parser.add_argument('--IP_version', type=str, default=table['IP version']) parser.add_argument('--link_policy', type=str, default=table['Link policy']) # link delay updating granularity - parser.add_argument('--update_interval', + parser.add_argument('--step', type=int, - default=table['update_time (s)']) + default=table['step (s)']) parser.add_argument('--duration', type=int, default=table['Duration (s)']) parser.add_argument('--sat_bandwidth', type=int, @@ -71,16 +70,6 @@ def sn_check_utility(time_index, remote_ssh, local_dir): f.write(result) f.close() -def sn_update_delay(remote_ssh, remote_ftp, remote_dir, local_dir, - timeptr, constellation_size): - remote_ftp.put( - os.path.join(local_dir, 'delay', f'{timeptr}.txt.gz'), - f'{remote_dir}/{timeptr}.txt.gz') - sn_remote_cmd(remote_ssh, - f"python3 {remote_dir}/orchestrater.py " - f"{remote_dir}/{timeptr}.txt.gz {constellation_size} update") - print("Delay updating done.") - def sn_damage(remote_ssh, remote_ftp, remote_dir, local_dir, ratio, damage_list, constellation_size): print("Randomly setting damaged links...\n") @@ -129,31 +118,6 @@ def sn_sr(src, des, target, netns_list, remote_ssh): f"ip route add {des_IP[0][:-3]}0/24 dev B{src}-eth{target} via {target_IP[0]}" ) -def sn_ping(src, des, time_index, constellation_size, container_id_list, - file_path, configuration_file_path, remote_ssh): - if des <= constellation_size: - ifconfig_output = sn_remote_cmd(remote_ssh, - f"ip netns exec {container_id_list[des - 1]} " - r"ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") - des_IP = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig " + ifconfig_output[0][:-1] + - "|awk -F '[ :]+' 'NR==2{print $4}'") - else: - des_IP = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig B" + str(des) + - "-default |awk -F '[ :]+' 'NR==2{print $4}'") - ping_result = sn_remote_cmd( - remote_ssh, "docker exec -i " + str(container_id_list[src - 1]) + - " ping " + str(des_IP[0][:-1]) + " -c 4 -i 0.01 ") - f = open( - configuration_file_path + "/" + file_path + "/ping-" + str(src) + "-" + - str(des) + "_" + str(time_index) + ".txt", "w") - f.writelines(ping_result) - f.close() - - def sn_perf(src, des, time_index, constellation_size, container_id_list, file_path, configuration_file_path, remote_ssh): if des <= constellation_size: @@ -183,125 +147,3 @@ def sn_perf(src, des, time_index, constellation_size, container_id_list, str(des) + "_" + str(time_index) + ".txt", "w") f.writelines(perf_result) f.close() - -def sn_route(src, time_index, file_path, configuration_file_path, - container_id_list, remote_ssh): - route_result = sn_remote_cmd( - remote_ssh, - "docker exec -it " + str(container_id_list[src - 1]) + " route ") - f = open( - configuration_file_path + "/" + file_path + "/route-" + str(src) + - "_" + str(time_index) + ".txt", "w") - f.writelines(route_result) - f.close() - - -def sn_establish_new_GSL(container_id_list, matrix, constellation_size, bw, - loss, sat_index, GS_index, remote_ssh): - i = sat_index - j = GS_index - # IP address (there is a link between i and j) - delay = str(matrix[i - 1][j - 1]) - address_16_23 = (j - constellation_size) & 0xff - address_8_15 = i & 0xff - GSL_name = "GSL_" + str(i) + "-" + str(j) - # Create internal network in docker. - sn_remote_cmd( - remote_ssh, 'docker network create ' + GSL_name + " --subnet 9." + - str(address_16_23) + "." + str(address_8_15) + ".0/24") - print('[Create GSL:]' + 'docker network create ' + GSL_name + - " --subnet 9." + str(address_16_23) + "." + str(address_8_15) + - ".0/24") - sn_remote_cmd( - remote_ssh, 'docker network connect ' + GSL_name + " " + - str(container_id_list[i - 1]) + " --ip 9." + str(address_16_23) + "." + - str(address_8_15) + ".50") - ifconfig_output = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[i - 1]) + - " ip addr | grep -B 2 9." + str(address_16_23) + "." + - str(address_8_15) + - ".50 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") - target_interface = str(ifconfig_output[0]).split("@")[0] - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev " + target_interface + " down") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev " + target_interface + " name " + "B" + - str(i - 1 + 1) + "-eth" + str(j)) - - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " tc qdisc add dev B" + str(i - 1 + 1) + "-eth" + str(j) + - " root netem delay " + str(delay) + "ms") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " tc qdisc add dev B" + str(i - 1 + 1) + "-eth" + str(j) + - " root netem loss " + str(loss) + "%") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " tc qdisc add dev B" + str(i - 1 + 1) + "-eth" + str(j) + - " root netem rate " + str(bw) + "Gbps") - print('[Add current node:]' + 'docker network connect ' + GSL_name + " " + - str(container_id_list[i - 1]) + " --ip 9." + str(address_16_23) + - "." + str(address_8_15) + ".50") - print(sn_remote_cmd( - remote_ssh, 'docker network connect ' + GSL_name + " " + - str(container_id_list[j - 1]) + " --ip 9." + str(address_16_23) + "." + - str(address_8_15) + ".60")) - ifconfig_output = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[j - 1]) + - " ip addr | grep -B 2 9." + str(address_16_23) + "." + - str(address_8_15) + - ".60 | head -n 1 | awk -F: '{ print $2 }' | tr -d [:blank:]") - print(ifconfig_output) - target_interface = str(ifconfig_output[0]).split("@")[0] - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev " + target_interface + " down") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev " + target_interface + " name " + "B" + str(j) + - "-eth" + str(i - 1 + 1)) - - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " tc qdisc add dev B" + str(j) + "-eth" + str(i - 1 + 1) + - " root netem delay " + str(delay) + "ms") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " tc qdisc add dev B" + str(j) + "-eth" + str(i - 1 + 1) + - " root netem loss " + str(loss) + "%") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " tc qdisc add dev B" + str(j) + "-eth" + str(i - 1 + 1) + - " root netem rate " + str(bw) + "Gbps") - print('[Add right node:]' + 'docker network connect ' + GSL_name + " " + - str(container_id_list[j - 1]) + " --ip 10." + str(address_16_23) + - "." + str(address_8_15) + ".60") - - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[i - 1]) + - " ip link set dev B" + str(i - 1 + 1) + "-eth" + str(j) + " up") - sn_remote_cmd( - remote_ssh, "docker exec -d " + str(container_id_list[j - 1]) + - " ip link set dev B" + str(j) + "-eth" + str(i - 1 + 1) + " up") - - -def sn_del_link(first_index, second_index, container_id_list, remote_ssh): - sn_remote_cmd( - remote_ssh, "docker exec -d " + - str(container_id_list[second_index - 1]) + " ip link set dev B" + - str(second_index) + "-eth" + str(first_index) + " down") - sn_remote_cmd( - remote_ssh, "docker exec -d " + - str(container_id_list[first_index - 1]) + " ip link set dev B" + - str(first_index) + "-eth" + str(second_index) + " down") - GSL_name = "GSL_" + str(first_index) + "-" + str(second_index) - sn_remote_cmd( - remote_ssh, 'docker network disconnect ' + GSL_name + " " + - str(container_id_list[first_index - 1])) - sn_remote_cmd( - remote_ssh, 'docker network disconnect ' + GSL_name + " " + - str(container_id_list[second_index - 1])) - sn_remote_cmd(remote_ssh, 'docker network rm ' + GSL_name) \ No newline at end of file From d026aeebb21e41d6d8d771296491c1ed88325010 Mon Sep 17 00:00:00 2001 From: Xiex <562837197@qq.com> Date: Thu, 11 Apr 2024 21:51:14 +0800 Subject: [PATCH 05/49] feat: sync to main distance, check route, iperf, sr, damage & recovery update node representation to support GS operation --- example.py | 68 +++---- starrynet/sn_observer.py | 97 +++++++-- starrynet/sn_orchestrater.py | 339 +++++++++++++++++++++---------- starrynet/sn_synchronizer.py | 381 ++++++++++++++++++++++++++--------- 4 files changed, 618 insertions(+), 267 deletions(-) diff --git a/example.py b/example.py index 8d3bdc4..47fb05a 100644 --- a/example.py +++ b/example.py @@ -19,47 +19,38 @@ hello_interval = 1 # hello_interval(s) in OSPF. 1-200 are supported. print('Start StarryNet.') - sn = StarryNet(configuration_file_path, GS_lat_long) + sn = StarryNet(configuration_file_path, GS_lat_long, hello_interval) sn.create_nodes() sn.create_links() - # sn.run_routing_deamon() - sn.set_ping(1, 1, 1, 1, 1, 2, 0) - sn.set_ping(1, 1, 1, 1, 1, 2, 0.5) - sn.set_ping(1, 1, 1, 1, 1, 2, 1) - sn.set_ping(1, 1, 1, 1, 1, 2, 1.5) - sn.set_ping(1, 1, 1, 1, 1, 2, 13.5) - sn.set_ping(1, 1, 1, 1, 1, 2, 20) - sn.start_emulation() - if input('clear environment?[y/n]').strip().lower()[:1] == 'y': - sn.clean() - exit(0) + # sn.run_routing_deamon(node_lst='all') + sn.run_routing_deamon(node_lst=['SH1O1S1', 'SH1O1S2', 'SH1O1S3', 'SH1O1S4',]) - node_index1 = 1 - node_index2 = 2 + node1 = 'SH1O1S1' + node2 = 'SH1O1S2' time_index = 2 # distance between nodes at a certain time - node_distance = sn.get_distance(node_index1, node_index2, time_index) + node_distance = sn.get_distance(node1, node2, time_index) print("node_distance (km): " + str(node_distance)) # neighbor node indexes of node at a certain time - neighbors_index = sn.get_neighbors(node_index1, time_index) + neighbors_index = sn.get_neighbors(node1, time_index) print("neighbors_index: " + str(neighbors_index)) # GS connected to the node at a certain time - node_index1 = 7 - GSes = sn.get_GSes(node_index1, time_index) + node1 = 'SH1O25S14' + GSes = sn.get_GSes(node1, time_index) print("GSes are: " + str(GSes)) # LLA of a node at a certain time - LLA = sn.get_position(node_index1, time_index) + LLA = sn.get_position(node1, time_index) print("LLA: " + str(LLA)) - sn.get_utility(time_index) # CPU and memory useage + # IP dict of a node + IP_dict = sn.get_IP(node1) + print("IP: " + str(IP_dict)) - # IPList of a node - IP_list = sn.get_IP(node_index1) - print("IP: " + str(IP_list)) + sn.get_utility(time_index) # CPU and memory useage ratio = 0.3 time_index = 5 @@ -69,35 +60,36 @@ time_index = 10 sn.set_recovery(time_index) # recover the damages at a certain time - node_index1 = 27 + node1 = 'GS1' time_index = 15 # routing table of a node at a certain time. The output file will be written at the working directory. - sn.check_routing_table(node_index1, time_index) + sn.check_routing_table(node1, time_index) - sat = 1 - des = 27 - next_hop_sat = 2 + sat = 'SH1O1S1' + des = 'GS2' + next_hop_sat = 'SH1O1S2' time_index = 20 # set the next hop at a certain time. Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors. sn.set_next_hop(sat, des, next_hop_sat, time_index) - node_index1 = 13 - node_index2 = 14 + node1 = 'SH1O5S6' + node2 = 'SH1O6S6' time_index = 3 # ping msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_ping(node_index1, node_index2, time_index) + sn.set_ping(node1, node2, time_index) for i in range(35, 80): - node_index1 = 26 - node_index2 = 27 + node1 = 'SH1O9S10' + node2 = 'SH1O10S10' time_index = i # ping msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_ping(node_index1, node_index2, time_index) + sn.set_ping(node1, node2, time_index) - node_index1 = 13 - node_index2 = 14 + node1 = 'SH1O5S6' + node2 = 'SH1O6S6' time_index = 4 # perf msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_perf(node_index1, node_index2, time_index) + sn.set_iperf(node1, node2, time_index) sn.start_emulation() - sn.stop_emulation() + if input('clear environment?[y/n]').strip().lower()[:1] == 'y': + sn.clean() diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index c5c465c..742c2b5 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -98,7 +98,8 @@ def _gsl_least_delay(sat_cbf_t_shell, gs_cbf, antenna_num, bound_dis): 'LeastDelay':_gsl_least_delay, } -def _to_cbf(lat_long):# the xyz coordinate system. +def to_cbf(lat_long):# the xyz coordinate system. + lat_long = np.array(lat_long) radius = 6371 if lat_long.shape[-1] > 2: radius += lat_long[..., 2] @@ -127,12 +128,12 @@ def calculate_delay( os.makedirs(isl_dir, exist_ok=True) pos_files = glob.glob(os.path.join(pos_dir, '*.txt')) isl_files = glob.glob(os.path.join(isl_dir, '*.txt')) - if len(pos_files) != ts_total or len(isl_files) != ts_total: + if len(pos_files) != ts_total or len(isl_files) != 2 * ts_total: cached = False gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') os.makedirs(gsl_dir, exist_ok=True) gsl_files = glob.glob(os.path.join(gsl_dir, '*.txt')) - if len(gsl_files) != ts_total: + if len(gsl_files) != 2 * ts_total: cached = False if cached and input(f"Use cached local files [y/n]?").strip().lower()[:1] == 'y': return @@ -197,13 +198,13 @@ def calculate_delay( subpoint.elevation.km[t]) pos_dir = os.path.join(dir, shell['name'], 'position') for t, sat_lla in enumerate(sat_lla_t): - f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') + f = open(os.path.join(pos_dir, '%d.txt' % t), 'w') for sat_lst in sat_lla: for sat in sat_lst: f.write('%f,%f,%f\n' % (sat[0], sat[1], sat[2])) f.write('\n') f.close() - sat_cbf_t_shell.append(_to_cbf(sat_lla_t)) + sat_cbf_t_shell.append(to_cbf(sat_lla_t)) isls_t_shell = isl_styles[isl_style](sat_cbf_t_shell) for i, isls_t in enumerate(isls_t_shell): @@ -212,11 +213,17 @@ def calculate_delay( isl_state = [ [list() for _ in range(shell_lst[i]['sat'])] for _ in range(shell_lst[i]['orbit'])] for t in range(ts_total): - f = open(f"{isl_dir}/{t}.txt", 'w') + f1 = open(f"{isl_dir}/{t}-state.txt", 'w') + f2 = open(f"{isl_dir}/{t}.txt", 'w') for oid, sat_lst in enumerate(isls_t[t]): for sid, isl_lst in enumerate(sat_lst): # one line for each satellite - f.write(f"{oid},{sid}|") + f1.write(f"{oid},{sid}:") + f1.write(' '.join(f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" + for isl in isl_lst)) + f1.write('\n') + + f2.write(f"{oid},{sid}|") old_lst = isl_state[oid][sid] old_del = [True] * len(old_lst) new_add = [True] * len(isl_lst) @@ -232,33 +239,41 @@ def calculate_delay( else: isl_lst[j] = old # del some isls - f.write(' '.join( + f2.write(' '.join( f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" for isl, de in zip(old_lst, old_del) if de ) + '|') # update some isls - f.write(' '.join( + f2.write(' '.join( f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" for isl in update ) + '|') # add some isls - f.write(' '.join( + f2.write(' '.join( f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" for isl, add in zip(isl_lst, new_add) if add ) + '\n') isl_state[oid][sid] = isl_lst - f.write('\n') - f.close() + f1.write('\n') + f2.write('\n') + f1.close() + f2.close() - gs_cbf = _to_cbf(np.array(GS_lat_long)) + gs_cbf = to_cbf(GS_lat_long) bound_dis = _bound_gsl(antenna_elevation, shell['altitude']) gsls_t = gsl_styles[gsl_style](sat_cbf_t_shell, gs_cbf, antenna_number, bound_dis) gsl_state = [list() for _ in range(len(GS_lat_long))] for t, gsls in enumerate(gsls_t): - f = open(f"{gsl_dir}/{t}.txt", 'w') + f1 = open(f"{gsl_dir}/{t}-state.txt", 'w') + f2 = open(f"{gsl_dir}/{t}.txt", 'w') for gid, gsl_lst in enumerate(gsls): # one line for each ground station - f.write(f"{gid}|") + f1.write(f"{gid}:") + f1.write(' '.join(f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + for gsl in gsl_lst)) + f1.write('\n') + + f2.write(f"{gid}|") # add some isls old_lst = gsl_state[gid] old_del = [True] * len(old_lst) @@ -273,19 +288,63 @@ def calculate_delay( update.append(new) else: gsl_lst[j] = old # if not update, remain old delay - f.write(' '.join( + f2.write(' '.join( f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" for gsl, de in zip(old_lst, old_del) if de ) + '|') # update some gsls - f.write(' '.join( + f2.write(' '.join( f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" for gsl in update ) + '|') # del some gsls - f.write(' '.join( + f2.write(' '.join( f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" for gsl, add in zip(gsl_lst, new_add) if add ) + '\n') gsl_state[gid] = gsl_lst - f.close() + f1.close() + f2.close() + +def load_pos(path): + f = open(path, 'r') + lla_mat = [] + lla_lst = [] + oid, sid = 0, 0 + for line in f: + if len(line) == 0 or line.isspace(): + lla_mat.append(lla_lst) + lla_lst = [] + sid = 0 + oid += 1 + continue + lla_lst.append(list(map(float, line.strip().split(',')))) + f.close() + return lla_mat + +def load_isl_state(path): + f = open(path, 'r') + isl_mat = [] + isl_lst = [] + oid, sid = 0, 0 + for line in f: + if len(line) == 0 or line.isspace(): + isl_mat.append(isl_lst) + isl_lst = [] + sid = 0 + oid += 1 + continue + line = line[line.find(':')+1:].strip() + isl_lst.append(line.split()) + f.close() + return isl_mat + +def load_gsl_state(path): + f = open(path, 'r') + gsl_lst = [] + gid = 0 + for line in f: + line = line[line.find(':')+1:].strip() + gsl_lst.append(line.split()) + f.close() + return gsl_lst diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index f97ced8..51cb47f 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -11,8 +11,10 @@ author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) and Zeqi Lai (zeqilai@tsinghua.edu.cn) """ -PID_FILENAME = 'container_pid.txt' ASSIGN_FILENAME = 'assign.txt' +PID_FILENAME = 'container_pid.txt' +DAMAGE_FILENAME = 'damage_list.txt' + NOT_ASSIGNED = 'NA' VXLAN_PORT = 4789 # FIXME @@ -25,17 +27,27 @@ def _sat_name(shell_id, orbit_id, sat_id): def _gs_name(gid): return f'GS{gid+1}' -def _pid_matrix(path, pop = False): - global _mat_cache - if path not in _mat_cache: - with open(path, 'r') as f: - _mat_cache[path] = [ - [pid for pid in line.strip().split(' ')] - for line in f if len(line) > 0 and not line.isspace() - ] +def _pid_map(pid_path, pop = False): + global _pid_map_cache + if _pid_map_cache is None: + _pid_map_cache = {} + if not os.path.exists(pid_path): + print('Error: container index file not found, please create nodes') + exit(1) + with open(pid_path, 'r') as f: + for line in f: + if len(line) == 0 or line.isspace(): + continue + for name_pid in line.strip().split(): + if name_pid == NOT_ASSIGNED: + continue + name_pid = name_pid.split(':') + _pid_map_cache[name_pid[0]] = name_pid[1] if pop: - return _mat_cache.pop(path) - return _mat_cache[path] + ret = _pid_map_cache + _pid_map_cache = None + return ret + return _pid_map_cache def _get_params(path): with open(path, 'r') as f: @@ -111,7 +123,7 @@ def _parse_gsls(path): # name1 in local machine def _del_link(idx, name1, name2): - n1_n2 = f"{idx}-{name2}" + n1_n2 = f"{name2}" fd = os.open('/run/netns/' + name1, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) @@ -138,19 +150,19 @@ def _update_if(name, if_name, delay, bw, loss): ) def _update_link_intra_machine(idx, name1, name2, delay, bw, loss): - n1_n2 = f"{idx}-{name2}" - n2_n1 = f"{idx}-{name1}" + n1_n2 = f"{name2}" + n2_n1 = f"{name1}" _update_if(name1, n1_n2, delay, bw, loss) _update_if(name2, n2_n1, delay, bw, loss) # name1 in local machine def _update_link_local(idx, name1, name2, delay, bw, loss): - n1_n2 = f"{idx}-{name2}" + n1_n2 = f"{name2}" _update_if(name1, n1_n2, delay, bw, loss) def _add_link_intra_machine(idx, name1, name2, prefix, delay, bw, loss): - n1_n2 = f"{idx}-{name2}" - n2_n1 = f"{idx}-{name1}" + n1_n2 = f"{name2}" + n2_n1 = f"{name1}" subprocess.check_call( ('ip', 'link', 'add', n1_n2, 'netns', name1, 'type', 'veth', 'peer', n2_n1, 'netns', name2) @@ -159,8 +171,8 @@ def _add_link_intra_machine(idx, name1, name2, prefix, delay, bw, loss): _init_if(name2, n2_n1, prefix+'.40/24', delay, bw, loss) def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix, delay, bw, loss): - n1_n2 = f"{idx}-{name2}" - n2_n1 = f"{idx}-{name1}" + n1_n2 = f"{name2}" + n2_n1 = f"{name1}" subprocess.check_call( ('ip', 'link', 'add', n1_n2, 'type', 'vxlan', 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) @@ -173,14 +185,24 @@ def _load_netns(pid, name): netns_link = f'/run/netns/{name}' if not os.path.exists(netns_link): subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) + sn_container_check_call( + pid, + ('sysctl', 'net.ipv6.conf.all.forwarding=1'), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + sn_container_check_call( + pid, + ('sysctl', 'net.ipv4.conf.all.forwarding=1'), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): if all(assign != machine_id for assign in sat_mid): continue shell_dir = f"{dir}/{shell_name}" overlay_dir = shell_dir + '/overlay' os.makedirs(overlay_dir, exist_ok=True) - pid_file = open(shell_dir + '/' + PID_FILENAME, 'w', encoding='utf-8') for sid, assign in enumerate(sat_mid): if assign != machine_id: pid_file.write(' '.join(NOT_ASSIGNED for _ in range(orbit_num)) + '\n') @@ -189,25 +211,32 @@ def _load_netns(pid, name): for oid in range(orbit_num): name = _sat_name(shell_id, oid, sid) node_dir = f'{overlay_dir}/{name}' - pid_file.write(str(pyctr.container_run(node_dir, name)) + ' ') + pid_file.write(name+':'+str(pyctr.container_run(node_dir, name))+' ') pid_file.write('\n') - pid_file.close() if len(gs_mid) > 0 and any(assign == machine_id for assign in gs_mid): gs_dir = f"{dir}/GS-{len(gs_mid)}" overlay_dir = gs_dir + '/overlay' os.makedirs(overlay_dir, exist_ok=True) - pid_file = open(gs_dir + '/' + PID_FILENAME, 'w', encoding='utf-8') + gs_lst = [] for gid, assign in enumerate(gs_mid): if assign != machine_id: pid_file.write(NOT_ASSIGNED + ' ') continue - print(f'[{machine_id}] GS: {gid}/{len(gs_mid)}') + gs_lst.append(str(gid)) name = _gs_name(gid) node_dir = f'{overlay_dir}/{name}' - pid_file.write(str(pyctr.container_run(node_dir, name)) + ' ') + pid_file.write(name+':'+str(pyctr.container_run(node_dir, name))+' ') pid_file.write('\n') - pid_file.close() - sn_operate_every_node(dir, sat_mid_lst, gs_mid, _load_netns) + print(f'[{machine_id}] GS:', ','.join(gs_lst)) + + pid_file.close() + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) + sn_operate_every_node(dir, _load_netns) def sn_update_network( dir, ts, sat_mid_lst, gs_mid, ip_lst, @@ -352,59 +381,138 @@ def sn_update_network( print(f"[{machine_id}] GSL:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") -def sn_container_check_call(pid, cmd): +def sn_container_check_call(pid, cmd, *args, **kwargs): subprocess.check_call( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd) + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + *args, **kwargs ) -def sn_container_check_output(pid, cmd): +def sn_container_check_output(pid, cmd, *args, **kwargs): return subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd) + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + *args, **kwargs ) -def sn_operate_every_node(dir, sat_mid_lst, gs_mid, func, *args): +def sn_operate_every_node(dir, func, *args): for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): - pid_file = f"{dir}/{shell_name}/{PID_FILENAME}" - if not os.path.exists(pid_file): - continue - pid_mat = _pid_matrix(pid_file) - for sid, pid_lst in enumerate(pid_mat): - for oid, pid in enumerate(pid_lst): - if pid == NOT_ASSIGNED: - continue - func(pid, _sat_name(shell_id, oid, sid), *args) - gs_dir = f"{dir}/GS-{len(gs_mid)}" - if not os.path.exists(gs_dir): - return - pid_mat = _pid_matrix(gs_dir + '/' + PID_FILENAME) - assert len(pid_mat) == 1 - for gid, pid in enumerate(pid_mat[0]): - if pid == NOT_ASSIGNED: + pid_map = _pid_map(dir + '/' + PID_FILENAME) + for name, pid in pid_map.items(): + func(pid, name, *args) + +def get_IP(dir, node): + pid = _pid_map(f"{dir}/{PID_FILENAME}")[node] + addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in addr_lst: + dev_state_addrs = dev_state_addrs.split() + if len(dev_state_addrs) < 3: continue - func(int(pid), _gs_name(gid), *args) + print(dev_state_addrs[0].split('@')[0], dev_state_addrs[2]) -def sn_init_route_daemons(dir, sat_mid_lst, gs_mid, conf_path): +def sn_init_route_daemons(dir, conf_path, nodes): def _init_route_daemon(pid, name): bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' sn_container_check_call(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) - sn_operate_every_node(dir, sat_mid_lst, gs_mid, _init_route_daemon) + if nodes == 'all': + sn_operate_every_node(dir, _init_route_daemon) + else: + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + nodes_lst = nodes.split(',') + for node in nodes_lst: + _init_route_daemon(pid_map[node], node) -def sn_ping(dir,sat_mid_lst, src_shell, src_oid, src_sid, dst_shell, dst_oid, dst_sid): +def sn_ping(dir, src, dst): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") # suppose src in this machine - mat1 = _pid_matrix(f"{dir}/{sat_mid_lst[src_shell][1]}/{PID_FILENAME}") + src_pid = pid_map[src] # TODO: dst in other machine - mat2 = _pid_matrix(f"{dir}/{sat_mid_lst[dst_shell][1]}/{PID_FILENAME}") - dst_addr = subprocess.check_output( - "nsenter -m -u -i -n -p -t " + mat2[dst_sid][dst_oid] + " ip -br addr " - "| awk '$1!=\"lo\"{print $3}'", shell=True - ).decode().splitlines()[0] + dst_pid = pid_map[dst] + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + if dev_state_addrs[0].split('@')[0] == src: + break dst_addr = dst_addr[:dst_addr.rfind('/')] + print('ping', src, dst_addr) + subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', mat2[src_sid][src_oid], + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, 'ping', '-c', '4', '-i', '0.01', dst_addr), stdout=sys.stdout, stderr=subprocess.STDOUT ) +def sn_iperf(dir, src, dst): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + # suppose src in this machine + src_pid = pid_map[src] + # TODO: dst in other machine + dst_pid = pid_map[dst] + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + if dev_state_addrs[0].split('@')[0] == src: + break + dst_addr = dst_addr[:dst_addr.rfind('/')] + + server = subprocess.Popen( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'iperf3', '-s'), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, + 'iperf3', '-c', dst_addr, '-t5'), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + server.terminate() + +def sn_sr(dir, src, dst, nxt): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + # suppose src in this machine + src_pid = pid_map[src] + # TODO: dst in other machine + dst_pid = pid_map[dst] + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + dst_prefix = dst_addr[:dst_addr.rfind('.')] + '.0/24' + subprocess.run( + ('nsenter', '-n', '-t', src_pid, + 'ip', 'route', 'add', dst_prefix, 'dev', nxt), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + +def sn_check_route(dir, node): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + subprocess.run( + ('nsenter', '-n', '-t', pid_map[node], + 'route'), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + def sn_clean(dir): for ns_link in glob.glob(f"/run/netns/SH*O*S*"): if os.path.islink(ns_link): @@ -412,28 +520,18 @@ def sn_clean(dir): for ns_link in glob.glob(f"/run/netns/G*"): if os.path.islink(ns_link): os.remove(ns_link) - for pid_file in glob.glob(f"{dir}/[0-9]*/{PID_FILENAME}"): - pid_mat = _pid_matrix(pid_file, True) - for sid, pid_lst in enumerate(pid_mat): - for oid, pid in enumerate(pid_lst): - if pid == NOT_ASSIGNED: - continue - try: - os.kill(int(pid), 9) - except ProcessLookupError: - pass - os.remove(pid_file) - for pid_file in glob.glob(f"{dir}/GS-*/{PID_FILENAME}"): - pid_mat = _pid_matrix(pid_file, True) - for pid_lst in pid_mat: - for gid, pid in enumerate(pid_lst): - if pid == NOT_ASSIGNED: - continue - try: - os.kill(int(pid), 9) - except ProcessLookupError: - pass - os.remove(pid_file) + pid_file = f"{dir}/{PID_FILENAME}" + if not os.path.exists(pid_file): + return + pid_map = _pid_map(pid_file, True) + for pid in pid_map.values(): + if pid == NOT_ASSIGNED: + continue + try: + os.kill(int(pid), 9) + except ProcessLookupError: + pass + os.remove(pid_file) def _change_sat_link_loss(pid, loss): out = sn_container_check_output(pid, ('ip', '-br', 'link', 'show')).decode() @@ -444,40 +542,53 @@ def _change_sat_link_loss(pid, loss): dev_name = line.split('@')[0] sn_container_check_call( pid, - ('tc', 'qdisc', 'change', 'dev', dev_name, 'root', 'netem', 'loss', loss) + ('tc', 'qdisc', 'change', 'dev', dev_name, 'root', + 'netem', 'loss', loss+'%') ) -def sn_damage(random_list, sat_mid_lst): - for shell_id, oid, sid in random_list: - pid_file = f"{dir}/{sat_mid_lst[shell_id][1]}/{PID_FILENAME}" - if not os.path.exists(pid_file): - continue - pid_mat = _pid_matrix(pid_file) - pid = pid_mat[sid][oid] - _change_sat_link_loss(pid, '100%') - print(f'[{machine_id}] damage sat: {shell_id},{oid},{sid}') - -def sn_recover(damage_list, sat_mid_lst, sat_loss): - for shell_id, oid, sid in damage_list: - pid_file = f"{dir}/{sat_mid_lst[shell_id][1]}/{PID_FILENAME}" - if not os.path.exists(pid_file): - continue - pid_mat = _pid_matrix(pid_file) - pid = pid_mat[sid][oid] - _change_sat_link_loss(pid, sat_loss) - print(f'[{machine_id}] recover sat: {shell_id},{oid},{sid}') +def sn_damage(dir, random_list): + with open(f"{dir}/{DAMAGE_FILENAME}", 'a') as f: + for node in random_list: + pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") + pid = pid_mat[node] + _change_sat_link_loss(pid, '100') + f.write(node + '\n') + print(f'[{machine_id}] damage node: {node}') + +def sn_recover(dir, sat_loss): + damage_file = f"{dir}/{DAMAGE_FILENAME}" + if not os.path.exists(damage_file): + return + with open(f"{dir}/{DAMAGE_FILENAME}", 'r') as f: + for node in f: + pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") + pid = pid_mat[node.strip()] + _change_sat_link_loss(pid, sat_loss) + print(f'[{machine_id}] recover sat: {node}') + os.remove(damage_file) if __name__ == '__main__': # C module - import pyctr + try: + import pyctr + except ModuleNotFoundError: + subprocess.check_call( + "gcc $(python3-config --cflags --ldflags)" + "-shared -fPIC -O2 pyctr.c -o pyctr.so'", + shell=True + ) + import pyctr machine_id = int(sys.argv[1]) - _mat_cache = {} + _pid_map_cache = None cmd = sys.argv[2] workdir = sys.argv[3] gs_mid, sat_mid_lst, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) if cmd == 'nodes': sn_clean(workdir) sn_init_nodes(workdir, gs_mid, sat_mid_lst) + elif cmd == 'list': + for name in _pid_map(workdir + '/' + PID_FILENAME): + print(name) elif cmd == 'networks': # lp = LineProfiler() # sn_update_network = lp(sn_update_network) @@ -489,16 +600,22 @@ def sn_recover(damage_list, sat_mid_lst, sat_loss): # with open('report.txt', 'w') as f: # lp.print_stats(f) elif cmd == 'routed': - sn_init_route_daemons(workdir, sat_mid_lst, gs_mid, workdir + '/bird.conf') + sn_init_route_daemons(workdir, workdir + '/bird.conf', sys.argv[4]) + elif cmd == 'IP': + get_IP(workdir, sys.argv[4]) + elif cmd == 'damage': + sn_damage(workdir, sys.argv[4].split(',')) + elif cmd == 'recovery': + sn_recover(workdir, sys.argv[4]) elif cmd == 'clean': sn_clean(workdir) elif cmd == 'ping': - sn_ping( - workdir, sat_mid_lst, - int(sys.argv[4]), int(sys.argv[5]), int(sys.argv[6]), - int(sys.argv[7]), int(sys.argv[8]), int(sys.argv[9]), - ) - elif cmd == 'perf': - pass + sn_ping(workdir, sys.argv[4], sys.argv[5]) + elif cmd == 'iperf': + sn_iperf(workdir, sys.argv[4], sys.argv[5]) + elif cmd == 'sr': + sn_sr(workdir, sys.argv[4], sys.argv[5], sys.argv[6]) + elif cmd == 'rtable': + sn_check_route(workdir, sys.argv[4]) else: print('Unknown command') diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 1d63405..9874c55 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -7,6 +7,7 @@ import time import threading import zipfile +import math from starrynet.sn_observer import * from starrynet.sn_utils import * @@ -34,25 +35,41 @@ import all; }; area 0 { - interface "*-SH*O*S*" { + interface "SH*O*S*" { type broadcast; # Detected by default cost 256; - hello 5; # Default hello perid 10 is too long + hello %d; }; - interface "*-GS*" { + interface "GS*" { type broadcast; # Detected by default cost 256; - hello 5; # Default hello perid 10 is too long + hello %d; }; - interface "B*-default" { + interface "POP" { type broadcast; # Detected by default cost 256; - hello 10; # Default hello perid 10 is too long + hello %d; }; }; } """ +def _sat_name(shell_id, orbit_id, sat_id): + return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' + +def _sat2idx(sat_name): + idx1 = sat_name.find('O') + idx2 = sat_name.find('S', idx1) + shell_id = int(sat_name[2:idx1])-1 + oid, sid = int(sat_name[idx1+1:idx2])-1, int(sat_name[idx2+1:])-1 + return shell_id, oid, sid + +def _gs2idx(gs_name): + return int(node[2:])-1 + +def _gs_name(gid): + return f'GS{gid+1}' + class RemoteMachine: def __init__(self, id, host, port, username, password, @@ -73,14 +90,14 @@ def __init__(self, id, host, port, username, password, os.path.join(os.path.dirname(__file__), 'sn_orchestrater.py'), self.dir + '/sn_orchestrater.py' ) + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'pyctr.c'), + self.dir + '/pyctr.c' + ) self.sftp.put( os.path.join(self.local_dir, 'bird.conf'), self.dir + '/bird.conf' ) - # self.sftp.put( - # os.path.join(self.local_dir, 'pyctr.so'), - # self.dir + '/pyctr.so' - # ) self.sftp.put( os.path.join(self.local_dir, ASSIGN_FILENAME), self.dir + '/' + ASSIGN_FILENAME @@ -92,6 +109,12 @@ def init_nodes(self): f"python3 {self.dir}/sn_orchestrater.py {self.id} nodes {self.dir}" ) + def get_nodes(self): + return sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} list {self.dir}" + ).splitlines() + def init_network(self, isl_bw, isl_loss, gsl_bw, gsl_loss): for shell in self.shell_lst: rmt_path = f"{self.dir}/{shell['name']}.zip" @@ -125,18 +148,30 @@ def update_network(self, t, isl_bw, isl_loss, gsl_bw, gsl_loss): f"{t} {isl_bw} {isl_loss} {gsl_bw} {gsl_loss}" ) - def init_routed(self): + def init_routed(self, nodes): print(sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} routed {self.dir}" + f"python3 {self.dir}/sn_orchestrater.py {self.id} routed {self.dir} " + +','.join(nodes) )) + def get_IP(self, node): + lines = sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} IP {self.dir} {node}" + ).splitlines() + IP_dict = {} + for line in lines: + dev_IP = line.strip().split() + IP_dict[dev_IP[0]] = dev_IP[1] + return IP_dict + def ping_async(self, res_path, src, dst): def _ping_inner(ssh, dir, res_path, src, dst): output = sn_remote_cmd( ssh, f"python3 {dir}/sn_orchestrater.py {self.id} ping {dir} " - f"{src[0]} {src[1]} {src[2]} {dst[0]} {dst[1]} {dst[2]}" + f"{src} {dst} 2>&1" ) with open(res_path, 'w') as f: f.write(output) @@ -147,15 +182,57 @@ def _ping_inner(ssh, dir, res_path, src, dst): thread.start() return thread - def perf_async(self, src, dst): + def iperf_async(self, res_path, src, dst): + def _iperf_inner(ssh, dir, res_path, src, dst): + output = sn_remote_cmd( + ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} iperf {self.dir} " + f"{src} {dst} 2>&1" + ) + with open(res_path, 'w') as f: + f.write(output) + thread = threading.Thread( - target=sn_remote_cmd, - args=(self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} perf {self.dir} " - f"{src[0]} {src[1]} {src[2]} {dst[0]} {dst[1]} {dst[2]}"), + target=_iperf_inner, + args=(self.ssh, self.dir, res_path, src, dst) ) thread.start() return thread + + def sr(self, src, dst, next_hop): + sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} sr {self.dir} " + f"{src} {dst} {next_hop} 2>&1" + ) + + def check_route(self, res_path, sat): + output = sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} rtable {self.dir} " + f"{sat} 2>&1" + ) + with open(res_path, 'w') as f: + f.write(output) + + def check_utility(self, res_path): + output = sn_remote_cmd(self.ssh, "vmstat 2>&1") + with open(res_path, 'w') as f: + f.write(output) + + def damage(self, random_lst): + sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} damage {self.dir} " + + ','.join(random_lst) + ) + + def recovery(self, sat_loss): + sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py {self.id} recovery {self.dir} " + f"{sat_loss}" + ) def clean(self): sn_remote_cmd( @@ -165,7 +242,7 @@ def clean(self): class StarryNet(): - def __init__(self, configuration_file_path, GS_lat_long): + def __init__(self, configuration_file_path, GS_lat_long, hello_interval): # Initialize constellation information. sn_args = sn_load_file(configuration_file_path) self.shell_lst = sn_args.shell_lst @@ -192,7 +269,7 @@ def __init__(self, configuration_file_path, GS_lat_long): f"-{shell['phase_shift']}" self.local_dir = os.path.join(self.configuration_dir, self.experiment_name) - self._init_local() + self._init_local(hello_interval) # Initiate a necessary delay and position data for emulation calculate_delay( self.local_dir, self.duration, self.step, self.shell_lst, self.link_style, @@ -201,23 +278,16 @@ def __init__(self, configuration_file_path, GS_lat_long): (self.remote_lst, self.sat_mid_lst, self.gs_mid) = self._assign_remote(sn_args.machine_lst) - self.utility_checking_time = [] - self.route_checking_events = [] - self.ping_events = [] - self.perf_events = [] - self.sr_events = [] - self.damage_events = [] - self.damage_list = [] - self.recovery_events = [] + self.events = [] - def _init_local(self): + def _init_local(self, hello_interval): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): os.remove(txt_file) for shell in self.shell_lst: os.makedirs(os.path.join(self.local_dir, shell['name']), exist_ok=True) os.makedirs(os.path.join(self.local_dir, self.gs_dirname), exist_ok=True) with open(os.path.join(self.local_dir, 'bird.conf'), 'w') as f: - f.write(BIRD_CONF_TEXT) + f.write(BIRD_CONF_TEXT % (hello_interval, hello_interval, hello_interval)) def _assign_remote(self, machine_lst): # TODO: better partition @@ -295,6 +365,21 @@ def create_nodes(self): for remote in self.remote_lst: remote.init_nodes() print("Node initialization:", time.time() - begin, "s consumed.") + self._load_node_map() + + def _load_node_map(self): + self.node_map = {} + self.undamaged_lst = list() + self.total_sat_lst = list() + for remote in self.remote_lst: + for node in remote.get_nodes(): + if node.startswith('Error'): + print(node) + exit(1) + if node.startswith('SH'): + self.undamaged_lst.append(node) + self.total_sat_lst.append(node) + self.node_map[node.strip()] = remote def create_links(self): print('Initializing links ...') @@ -314,100 +399,198 @@ def create_links(self): thread.join() print("Link initialization:", time.time() - begin, 's consumed.') - def run_routing_deamon(self): + def run_routing_deamon(self, node_lst='all'): print('Initializing routing ...') - for remote in self.remote_lst: - remote.init_routed() - print("Routing daemon initialized. Wait 30s for route converged") + if node_lst == 'all': + for remote in self.remote_lst: + remote.init_routed(['all']) + print("Routing daemon initialized. Wait 30s for route converged") + else: + rtd_lsts = {machine:[] for machine in self.remote_lst} + for node in node_lst: + rtd_lsts[self.node_map[node]].append(node) + for remote, nodes in rtd_lsts.items(): + if len(nodes) > 0: + remote.init_routed(nodes) + for i in range(30): print(f'\r{i} / 30', end=' ') time.sleep(1) print("Routing started!") - def get_distance(self, shell1, orbit1, sat1, shell2, orbit2, sat2, t): - raise NotImplementedError + # static information + def get_distance(self, node1, node2, time_index): + def _get_xyz(node): + if node.startswith('SH'): + shell_id, oid, sid = _sat2idx(node) + shell = self.shell_lst[shell_id] + lla_mat = load_pos(os.path.join( + self.local_dir, + shell['name'], + 'position', + f'{time_index}.txt' + )) + return to_cbf(lla_mat[oid][sid]) + elif node.startswith('GS'): + return to_cbf(self.gs_lat_long[_gs2idx(node)]) + else: + raise NotImplementedError + + xyz1, xyz2 = _get_xyz(node1), _get_xyz(node2) + dx, dy, dz = xyz1[0] - xyz2[0], xyz1[1] - xyz2[1], xyz1[2] - xyz2[2] + return math.sqrt(dx * dx + dy * dy + dz * dz) - def get_neighbors(self, sat_index, time_index): - raise NotImplementedError + def get_neighbors(self, sat, time_index): + if not sat.startswith('SH'): + raise RuntimeError('Not a satellite') + shell_id, oid, sid = _sat2idx(sat) + shell = self.shell_lst[shell_id] - def get_GSes(self, sat_index, time_index): - raise NotImplementedError + isl_mat = load_isl_state(os.path.join( + self.local_dir, + shell['name'], + 'isl', + f'{time_index}-state.txt' + )) + neighbors = [] + for isl in isl_mat[oid][sid]: + isl = isl.split(',') + neighbors.append(_sat_name(shell_id, int(isl[1]), int(isl[2]))) + for orbit, isls_lst in enumerate(isl_mat): + for sat, isls in enumerate(isls_lst): + for isl in isls: + isl = isl.split(',') + if int(isl[1]) == oid and int(isl[2]) == sid: + neighbors.append(_sat_name(shell_id, orbit, sat)) + return neighbors + + def get_GSes(self, sat, time_index): + if not sat.startswith('SH'): + raise RuntimeError('Not a Satellite') + shell_id, oid, sid = _sat2idx(sat) + shell = self.shell_lst[shell_id] + + gsl_lst = load_gsl_state(os.path.join( + self.local_dir, + self.gs_dirname, + 'gsl', + f'{time_index}-state.txt' + )) + GSes = [] + for gid, gsls in enumerate(gsl_lst): + for gsl in gsls: + gsl = gsl.split(',') + if int(gsl[1]) == shell_id \ + and int(gsl[2]) == oid \ + and int(gsl[3]) == sid: + GSes.append(_gs_name(gid)) + return GSes - def get_utility(self, time_index): - self.utility_checking_time.append(time_index) + def get_position(self, node, time_index): + if node.startswith('SH'): + shell_id, oid, sid = _sat2idx(node) + shell = self.shell_lst[shell_id] + + lla_mat = load_pos(os.path.join( + self.local_dir, + shell['name'], + 'position', + f'{time_index}.txt' + )) + return lla_mat[oid][sid] + elif node.startswith('GS'): + return self.gs_lat_long[_gs2idx(node)] + else: + raise NotImplementedError - def get_position(self, shell1, orbit1, sat1, time_index): - path = self.local_dir + '/position/%d.txt' % time_index - f = open(path) - ADJ = f.readlines() - f.close() - return ADJ[sat_index - 1] + def get_IP(self, node): + if not hasattr(self, 'node_map'): + self._load_node_map() + return self.node_map[node].get_IP(node) - def get_IP(self, sat_index): - raise NotImplementedError + # dynamic events + def get_utility(self, t): + def _check_utility(real_t): + for mid, machine in enumerate(self.remote_lst): + machine.check_utility(os.path.join( + self.local_dir, f'{real_t}-utility-machine{mid}.txt') + ) + self.events.append((t, _check_utility,)) def set_damage(self, damaging_ratio, t): - self.damage_events.append((t, damaging_ratio)) + def _damage(real_t, damaging_ratio): + damage_lsts = {machine:[] for machine in self.remote_lst} + cur_num = len(self.undamaged_lst) + need_damage_num = min(len(self.total_sat_lst) * damaging_ratio, cur_num) + while(cur_num - len(self.undamaged_lst) < need_damage_num): + sat = self.undamaged_lst.pop( + random.randint(0, len(self.undamaged_lst) - 1) + ) + machine = self.node_map[sat] + damage_lsts[machine].append(sat) + for machine, lst in damage_lsts.items(): + machine.damage(lst) + self.events.append((t, _damage, damaging_ratio,)) def set_recovery(self, t): - self.recovery_events.append(t) - - def check_routing_table(self, sat_index, t): - self.route_checking_events.append((t, sat_index)) - - def set_next_hop(self, - src_shell, src_orbit, src_sat, - des_shell, des_orbit, des_sat, - nxt_shell, nxt_orbit, nxt_sat, t - ): - self.sr_events.append( - (t, - (src_shell, src_orbit, src_sat), - (des_shell, des_orbit, des_sat), - (nxt_shell, nxt_orbit, nxt_sat)) - ) + def _recovery(real_t): + for machine in self.remote_lst: + machine.recovery(self.sat_loss) + self.undamaged_lst = self.total_sat_lst.copy() + self.events.append((t, _recovery,)) - def set_ping(self, shell1, orbit1, sat1, shell2, orbit2, sat2, t): - self.ping_events.append( - (t, (shell1-1, orbit1-1, sat1-1), (shell2-1, orbit2-1, sat2-1)) - ) + def check_routing_table(self, node, t): + def _check_route(real_t, node): + machine = self.node_map[node] + machine.check_route( + os.path.join(self.local_dir, f'{real_t}-route-{node}.txt'), + node + ) + self.events.append((t, _check_route, node,)) - def set_perf(self, shell1, orbit1, sat1, shell2, orbit2, sat2, t): - self.perf_events.append( - (t, (shell1-1, orbit1-1, sat1-1), (shell2-1, orbit2-1, sat2-1)) - ) - - def event(self, t): - while len(self.ping_events) > 0 and self.ping_events[-1][0] <= t: - ping_event = self.ping_events.pop(-1) - src_shell, src_sid = ping_event[1][0], ping_event[1][2] - machine = self.remote_lst[self.sat_mid_lst[src_shell][src_sid]] + def set_next_hop(self, src, dst, next_hop, t): + def _set_next_hop(real_t, src, dst, next_hop): + machine = self.node_map[src] + machine.sr(src, dst, next_hop) + self.events.append((t, _set_next_hop, src, dst, next_hop)) + + def set_ping(self, src, dst, t): + def _ping(real_t, src, dst): + machine = self.node_map[src] self.ping_threads.append(machine.ping_async( - os.path.join( - self.local_dir, f'{t}-ping-{ping_event[1]}-{ping_event[2]}.txt'), - ping_event[1], - ping_event[2] + os.path.join(self.local_dir, f'{real_t}-ping-{src}-{dst}.txt'), + src, dst )) - while len(self.perf_events) > 0 and self.perf_events[-1][0] <= t: - perf_event = self.perf_events.pop(-1) - machine = self.remote_lst[self.sat_mid_lst[perf_event[1][0]]] - self.perf_threads.append(machine.perf_async( - perf_event[1], - perf_event[2] + self.events.append((t, _ping, src, dst)) + + def set_iperf(self, src, dst, t): + def _iperf(real_t, src, dst): + machine = self.node_map[src] + self.iperf_threads.append(machine.iperf_async( + os.path.join(self.local_dir, f'{real_t}-iperf-{src}-{dst}.txt'), + src, dst )) - # TODO: other events + self.events.append((t, _iperf, src, dst)) + + def _event(self, real_t): + while len(self.events) > 0 and self.events[-1][0] <= real_t: + event = self.events.pop(-1) + event[1](real_t, *event[2:]) def start_emulation(self): - self.ping_events.sort(key=lambda x:x[0], reverse=True) - self.perf_events.sort(key=lambda x:x[0], reverse=True) + self.events.sort(key=lambda x:x[0], reverse=True) + + if not hasattr(self, 'node_map'): + self._load_node_map() + self.ping_threads = [] - self.perf_threads = [] + self.iperf_threads = [] t = 0.0 tid = 1 while t < self.duration: start = time.time() - print("Trigger events at", t, "s ...") - self.event(t) + print("\nTrigger events at", t, "s ...") + self._event(t) print("Update networks ...") update_start = time.time() if tid < self.duration: @@ -434,8 +617,8 @@ def start_emulation(self): tid += 1 for ping_thread in self.ping_threads: ping_thread.join() - for perf_thread in self.perf_threads: - perf_thread.join() + for iperf_thread in self.iperf_threads: + iperf_thread.join() def clean(self): print("Removing containers and links...") From b9e0fa435db5e9094063cfd0da0f8f28a374bf8c Mon Sep 17 00:00:00 2001 From: Xiex <562837197@qq.com> Date: Mon, 22 Apr 2024 19:51:46 +0800 Subject: [PATCH 06/49] fix: damage and recover --- bird.conf | 33 -------- example.py | 97 ++++++++++++---------- starrynet/sn_observer.py | 154 ++++++++++++++++++----------------- starrynet/sn_orchestrater.py | 86 +++++++++++++++---- starrynet/sn_synchronizer.py | 38 +++++---- 5 files changed, 222 insertions(+), 186 deletions(-) delete mode 100644 bird.conf diff --git a/bird.conf b/bird.conf deleted file mode 100644 index 3c367ca..0000000 --- a/bird.conf +++ /dev/null @@ -1,33 +0,0 @@ -log "/var/log/bird.log" { debug, trace, info, remote, warning, error, auth, fatal, bug }; -protocol device { -} -protocol direct { - disabled; # Disable by default - ipv4; # Connect to default IPv4 table - ipv6; # ... and to default IPv6 table -} -protocol kernel { - ipv4 { # Connect protocol to IPv4 table by channel - export all; # Export to protocol. default is export none - }; -} -# protocol static { -# ipv4; # Again, IPv6 channel with default options -# } -protocol ospf{ - ipv4 { - import all; - }; - area 0 { - interface "O*S*-O*S*" { - type broadcast; # Detected by default - cost 256; - hello 10; # Default hello perid 10 is too long - }; - interface "B*-default" { - type broadcast; # Detected by default - cost 256; - hello 10; # Default hello perid 10 is too long - }; - }; -} diff --git a/example.py b/example.py index 47fb05a..5a6bc09 100644 --- a/example.py +++ b/example.py @@ -16,80 +16,87 @@ GS_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817] ] # latitude and longitude of frankfurt and Austria configuration_file_path = "./config.json" - hello_interval = 1 # hello_interval(s) in OSPF. 1-200 are supported. + hello_interval = 5 # hello_interval(s) in OSPF. 1-200 are supported. print('Start StarryNet.') - sn = StarryNet(configuration_file_path, GS_lat_long, hello_interval) - sn.create_nodes() - sn.create_links() - # sn.run_routing_deamon(node_lst='all') - sn.run_routing_deamon(node_lst=['SH1O1S1', 'SH1O1S2', 'SH1O1S3', 'SH1O1S4',]) - - node1 = 'SH1O1S1' - node2 = 'SH1O1S2' + sn = StarryNet(configuration_file_path, GS_lat_long) + + node1 = 'SH1O25S14' + node1 = 'SH1O25S15' time_index = 2 + # LLA of a node at a certain time + LLA = sn.get_position(node1, time_index) + print(f'\nLatitude, Longitude, Altitude of {node1}: {LLA}') # distance between nodes at a certain time node_distance = sn.get_distance(node1, node2, time_index) - print("node_distance (km): " + str(node_distance)) - - # neighbor node indexes of node at a certain time - neighbors_index = sn.get_neighbors(node1, time_index) - print("neighbors_index: " + str(neighbors_index)) + print(f'\n{node1}-{node2} distance(km): {node_distance}') + # neighbor nodes at a certain time + neighbors = sn.get_neighbors(node1, time_index) + print(f'\n{node1} neighbors: {neighbors}') + # GS connected to the node at a certain time - node1 = 'SH1O25S14' GSes = sn.get_GSes(node1, time_index) - print("GSes are: " + str(GSes)) + print(f"\n{node1} GSes: {GSes}") - # LLA of a node at a certain time - LLA = sn.get_position(node1, time_index) - print("LLA: " + str(LLA)) + sn.create_nodes() + sn.create_links() + + time_index = 2 + sn.get_utility(time_index) # CPU and memory useage # IP dict of a node IP_dict = sn.get_IP(node1) - print("IP: " + str(IP_dict)) - - sn.get_utility(time_index) # CPU and memory useage - - ratio = 0.3 - time_index = 5 - # random damage of a given ratio at a certain time - sn.set_damage(ratio, time_index) - - time_index = 10 - sn.set_recovery(time_index) # recover the damages at a certain time - - node1 = 'GS1' - time_index = 15 - # routing table of a node at a certain time. The output file will be written at the working directory. - sn.check_routing_table(node1, time_index) + print(f'\n{node1} IP addresses: {IP_dict}') sat = 'SH1O1S1' des = 'GS2' next_hop_sat = 'SH1O1S2' time_index = 20 - # set the next hop at a certain time. Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors. + # set the next hop at a certain time. Sat and NextHopSat are neighbors. sn.set_next_hop(sat, des, next_hop_sat, time_index) + time_index = 22 + # routing table of a node at a certain time. The output file will be written at the working directory. + sn.check_routing_table(sat, time_index) + node1 = 'SH1O5S6' node2 = 'SH1O6S6' - time_index = 3 + time_index = 4 # ping msg of two nodes at a certain time. The output file will be written at the working directory. sn.set_ping(node1, node2, time_index) - for i in range(35, 80): + # perf msg of two nodes at a certain time. The output file will be written at the working directory. + sn.set_iperf(node1, node2, time_index) + + # run OSPF daemon on all nodes + # sn.run_routing_daemon() + + # run OSPF daemon on selected nodes + sn.run_routing_deamon(node_lst=[ + 'GS1', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS2']) + + sn.check_routing_table('GS2', 4) + + ratio = 0.3 + time_index = 5 + # random damage of a given ratio at a certain time + sn.set_damage(ratio, time_index) + + sn.check_routing_table('GS1', 10) + + time_index = 15 + # recover the damages at a certain time + sn.set_recovery(time_index) + + sn.check_routing_table('GS1', 25) + + for time_index in range(35, 80): node1 = 'SH1O9S10' node2 = 'SH1O10S10' - time_index = i # ping msg of two nodes at a certain time. The output file will be written at the working directory. sn.set_ping(node1, node2, time_index) - node1 = 'SH1O5S6' - node2 = 'SH1O6S6' - time_index = 4 - # perf msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_iperf(node1, node2, time_index) - sn.start_emulation() if input('clear environment?[y/n]').strip().lower()[:1] == 'y': sn.clean() diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index 742c2b5..50d4ae4 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -123,88 +123,92 @@ def calculate_delay( cached = True for shell in shell_lst: pos_dir = os.path.join(dir, shell['name'], 'position') - isl_dir = os.path.join(dir, shell['name'], 'isl') os.makedirs(pos_dir, exist_ok=True) - os.makedirs(isl_dir, exist_ok=True) - pos_files = glob.glob(os.path.join(pos_dir, '*.txt')) - isl_files = glob.glob(os.path.join(isl_dir, '*.txt')) - if len(pos_files) != ts_total or len(isl_files) != 2 * ts_total: + if len(glob.glob(os.path.join(pos_dir, '*.txt'))) != ts_total: cached = False - gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') - os.makedirs(gsl_dir, exist_ok=True) - gsl_files = glob.glob(os.path.join(gsl_dir, '*.txt')) - if len(gsl_files) != 2 * ts_total: - cached = False - if cached and input(f"Use cached local files [y/n]?").strip().lower()[:1] == 'y': - return - - for shell in shell_lst: - pos_files = glob.glob(os.path.join(dir, shell['name'], 'position', '*.txt')) - isl_files = glob.glob(os.path.join(dir, shell['name'], 'isl', '*.txt')) - for file in pos_files: - os.remove(file) - for file in isl_files: + os.makedirs(os.path.join(dir, shell['name'], 'isl'), exist_ok=True) + for file in glob.glob(os.path.join(dir, shell['name'], 'isl', '*.txt')): os.remove(file) - for file in gsl_files: + gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') + os.makedirs(gsl_dir, exist_ok=True) + for file in glob.glob(os.path.join(gsl_dir, '*.txt')): os.remove(file) - ts = load.timescale() - since = datetime.datetime(1949, 12, 31, 0, 0, 0) - start = datetime.datetime(2020, 1, 1, 0, 0, 0) - epoch = (start - since).days - GM = 3.9860044e14 - R = 6371393 - F = 18 sat_cbf_t_shell = [] - ts_lst = [i * step for i in range(ts_total)] - for i, shell in enumerate(shell_lst): - inclination = shell['inclination'] * 2 * np.pi / 360 - altitude = shell['altitude'] * 1000 - mean_motion = np.sqrt(GM / (R + altitude)**3) * 60 - orbit_number, sat_number = shell['orbit'], shell['sat'] - num_of_sat = orbit_number * sat_number + if cached and input(f"Use cached local files [y/n]?").strip().lower()[:1] == 'y': + for shell in shell_lst: + orbit_number, sat_number = shell['orbit'], shell['sat'] + sat_lla_t = np.zeros((ts_total, orbit_number, sat_number, 3)) + pos_dir = os.path.join(dir, shell['name'], 'position') + for t in range(ts_total): + f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'r') + for oid in range(orbit_number): + for sid in range(sat_number): + sat_lla_t[t,oid,sid]=list(map(float, f.readline().split(','))) + f.readline() + f.close() + sat_cbf_t_shell.append(to_cbf(sat_lla_t)) + else: + for shell in shell_lst: + for file in glob.glob(os.path.join(dir, shell['name'], 'position', '*.txt')): + os.remove(file) - sat_lla_t = np.zeros((ts_total, orbit_number, sat_number, 3)) - for oid in range(orbit_number): - raan = oid / orbit_number * 2 * np.pi - for sid in range(sat_number): - mean_anomaly = (sid * 360 / sat_number + oid * 360 * F / - num_of_sat) % 360 * 2 * np.pi / 360 - satrec = Satrec() - satrec.sgp4init( - WGS84, # gravity model - 'i', # 'a' = old AFSPC mode, 'i' = improved mode - oid * sat_number + sid, # satnum: Satellite number - epoch, # epoch: days since 1949 December 31 00:00 UT - 2.8098e-05, # bstar: drag coefficient (/earth radii) - 6.969196665e-13, # ndot: ballistic coefficient (revs/day) - 0.0, # nddot: second derivative of mean motion (revs/day^3) - 0.001, # ecco: eccentricity - 0.0, # argpo: argument of perigee (radians) - inclination, # inclo: inclination (radians) - mean_anomaly, # mo: mean anomaly (radians) - mean_motion, # no_kozai: mean motion (radians/minute) - raan, # nodeo: right ascension of ascending node (radians) - ) - sat = EarthSatellite.from_satrec(satrec, ts) - cur = datetime.datetime(2022, 1, 1, 1, 0, 0) - t_ts = ts.utc(*cur.timetuple()[:5], ts_lst) # [:4]:minute,[:5]:second - geocentric = sat.at(t_ts) - subpoint = wgs84.subpoint(geocentric) - # list: [subpoint.latitude.degrees] [subpoint.longitude.degrees] [subpoint.elevation.km] - for t in range(ts_total): - sat_lla_t[t, oid, sid] = (subpoint.latitude.degrees[t], - subpoint.longitude.degrees[t], - subpoint.elevation.km[t]) - pos_dir = os.path.join(dir, shell['name'], 'position') - for t, sat_lla in enumerate(sat_lla_t): - f = open(os.path.join(pos_dir, '%d.txt' % t), 'w') - for sat_lst in sat_lla: - for sat in sat_lst: - f.write('%f,%f,%f\n' % (sat[0], sat[1], sat[2])) - f.write('\n') - f.close() - sat_cbf_t_shell.append(to_cbf(sat_lla_t)) + ts = load.timescale() + since = datetime.datetime(1949, 12, 31, 0, 0, 0) + start = datetime.datetime(2020, 1, 1, 0, 0, 0) + epoch = (start - since).days + GM = 3.9860044e14 + R = 6371393 + F = 18 + ts_lst = [i * step for i in range(ts_total)] + for i, shell in enumerate(shell_lst): + inclination = shell['inclination'] * 2 * np.pi / 360 + altitude = shell['altitude'] * 1000 + mean_motion = np.sqrt(GM / (R + altitude)**3) * 60 + orbit_number, sat_number = shell['orbit'], shell['sat'] + num_of_sat = orbit_number * sat_number + + sat_lla_t = np.zeros((ts_total, orbit_number, sat_number, 3)) + for oid in range(orbit_number): + raan = oid / orbit_number * 2 * np.pi + for sid in range(sat_number): + mean_anomaly = (sid * 360 / sat_number + oid * 360 * F / + num_of_sat) % 360 * 2 * np.pi / 360 + satrec = Satrec() + satrec.sgp4init( + WGS84, # gravity model + 'i', # 'a' = old AFSPC mode, 'i' = improved mode + oid * sat_number + sid, # satnum: Satellite number + epoch, # epoch: days since 1949 December 31 00:00 UT + 2.8098e-05, # bstar: drag coefficient (/earth radii) + 6.969196665e-13, # ndot: ballistic coefficient (revs/day) + 0.0, # nddot: second derivative of mean motion (revs/day^3) + 0.001, # ecco: eccentricity + 0.0, # argpo: argument of perigee (radians) + inclination, # inclo: inclination (radians) + mean_anomaly, # mo: mean anomaly (radians) + mean_motion, # no_kozai: mean motion (radians/minute) + raan, # nodeo: right ascension of ascending node (radians) + ) + sat = EarthSatellite.from_satrec(satrec, ts) + cur = datetime.datetime(2022, 1, 1, 1, 0, 0) + t_ts = ts.utc(*cur.timetuple()[:5], ts_lst) # [:4]:minute,[:5]:second + geocentric = sat.at(t_ts) + subpoint = wgs84.subpoint(geocentric) + # list: [subpoint.latitude.degrees] [subpoint.longitude.degrees] [subpoint.elevation.km] + for t in range(ts_total): + sat_lla_t[t, oid, sid] = (subpoint.latitude.degrees[t], + subpoint.longitude.degrees[t], + subpoint.elevation.km[t]) + pos_dir = os.path.join(dir, shell['name'], 'position') + for t, sat_lla in enumerate(sat_lla_t): + f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') + for sat_lst in sat_lla: + for sat in sat_lst: + f.write('%f,%f,%f\n' % (sat[0], sat[1], sat[2])) + f.write('\n') + f.close() + sat_cbf_t_shell.append(to_cbf(sat_lla_t)) isls_t_shell = isl_styles[isl_style](sat_cbf_t_shell) for i, isls_t in enumerate(isls_t_shell): diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 51cb47f..3970729 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import os import subprocess import sys @@ -20,6 +21,7 @@ # FIXME CLONE_NEWNET = 0x40000000 libc = ctypes.CDLL(None) +main_net_fd = os.open('/proc/self/ns/net', os.O_RDONLY) def _sat_name(shell_id, orbit_id, sat_id): return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' @@ -144,9 +146,10 @@ def _update_if(name, if_name, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) + update_loss = '100' if name in damage_set else loss subprocess.check_call( ('tc', 'qdisc', 'change', 'dev', if_name, 'root', - 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', loss + '%') + 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') ) def _update_link_intra_machine(idx, name1, name2, delay, bw, loss): @@ -163,6 +166,7 @@ def _update_link_local(idx, name1, name2, delay, bw, loss): def _add_link_intra_machine(idx, name1, name2, prefix, delay, bw, loss): n1_n2 = f"{name2}" n2_n1 = f"{name1}" + libc.setns(main_net_fd, CLONE_NEWNET) subprocess.check_call( ('ip', 'link', 'add', n1_n2, 'netns', name1, 'type', 'veth', 'peer', n2_n1, 'netns', name2) @@ -207,7 +211,7 @@ def _load_netns(pid, name): if assign != machine_id: pid_file.write(' '.join(NOT_ASSIGNED for _ in range(orbit_num)) + '\n') continue - print(f'[{machine_id}] Satellite: {shell_id},{sid},(0-{orbit_num})') + print(f'[{machine_id}] Satellite: {shell_id},(0-{orbit_num}),{sid}') for oid in range(orbit_num): name = _sat_name(shell_id, oid, sid) node_dir = f'{overlay_dir}/{name}' @@ -387,6 +391,12 @@ def sn_container_check_call(pid, cmd, *args, **kwargs): *args, **kwargs ) +def sn_container_run(pid, cmd, *args, **kwargs): + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + *args, **kwargs + ) + def sn_container_check_output(pid, cmd, *args, **kwargs): return subprocess.check_output( ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), @@ -414,7 +424,7 @@ def get_IP(dir, node): def sn_init_route_daemons(dir, conf_path, nodes): def _init_route_daemon(pid, name): bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' - sn_container_check_call(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) + sn_container_run(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) if nodes == 'all': sn_operate_every_node(dir, _init_route_daemon) else: @@ -514,6 +524,9 @@ def sn_check_route(dir, node): ) def sn_clean(dir): + damage_file = f"{dir}/{DAMAGE_FILENAME}" + if os.path.exists(damage_file): + os.remove(damage_file) for ns_link in glob.glob(f"/run/netns/SH*O*S*"): if os.path.islink(ns_link): os.remove(ns_link) @@ -534,17 +547,20 @@ def sn_clean(dir): os.remove(pid_file) def _change_sat_link_loss(pid, loss): - out = sn_container_check_output(pid, ('ip', '-br', 'link', 'show')).decode() + out = subprocess.check_output( + ('nsenter', '-t', pid, '-n', + 'tc', 'qdisc', 'show')).decode() for line in out.splitlines(): line = line.strip() if len(line) == 0 or line.startswith('lo'): continue - dev_name = line.split('@')[0] - sn_container_check_call( - pid, - ('tc', 'qdisc', 'change', 'dev', dev_name, 'root', - 'netem', 'loss', loss+'%') - ) + qdisc_netem_hd_dev_name_ = line.split() + dev_name = qdisc_netem_hd_dev_name_[4] + delay = qdisc_netem_hd_dev_name_[qdisc_netem_hd_dev_name_.index('delay') + 1] + subprocess.check_call( + ('nsenter', '-t', pid, '-n', + 'tc', 'qdisc', 'change', 'dev', dev_name, 'root', + 'netem', 'delay', delay, 'loss', loss+'%')) def sn_damage(dir, random_list): with open(f"{dir}/{DAMAGE_FILENAME}", 'a') as f: @@ -568,27 +584,64 @@ def sn_recover(dir, sat_loss): os.remove(damage_file) if __name__ == '__main__': + _pid_map_cache = None + + if len(sys.argv) < 2: + print('Usage: sn_orchestrater.py ...') + exit(1) + cmd = sys.argv[1] + if cmd == 'exec': + pid_map = _pid_map(os.path.dirname(__file__) + '/' + PID_FILENAME) + if len(sys.argv) < 4: + print('Usage: sn_orchestrater.py exec ...') + exit(1) + if sys.argv[2] not in pid_map: + print('Error:', sys.argv[3], 'not found') + exit(1) + exit(subprocess.run( + ('nsenter', '-a', '-t', pid_map[sys.argv[2]], + *sys.argv[3:]) + ).returncode) + + if len(sys.argv) < 3: + machine_id = None + else: + try: + machine_id = int(sys.argv[2]) + except: + machine_id = None + if len(sys.argv) < 4: + workdir = os.path.dirname(__file__) + else: + workdir = sys.argv[3] + # C module try: import pyctr except ModuleNotFoundError: subprocess.check_call( + "cd " + workdir + " && " "gcc $(python3-config --cflags --ldflags)" - "-shared -fPIC -O2 pyctr.c -o pyctr.so'", + "-shared -fPIC -O2 pyctr.c -o pyctr.so", shell=True ) import pyctr - machine_id = int(sys.argv[1]) - _pid_map_cache = None - cmd = sys.argv[2] - workdir = sys.argv[3] + + damage_set = set() + damage_file = workdir + '/' + DAMAGE_FILENAME + if os.path.exists(damage_file): + with open(workdir + '/' + DAMAGE_FILENAME, 'r') as f: + for line in f: + damage_set.add(line.strip()) + gs_mid, sat_mid_lst, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) if cmd == 'nodes': sn_clean(workdir) sn_init_nodes(workdir, gs_mid, sat_mid_lst) elif cmd == 'list': + print(f"{'NODE':<20} STATE") for name in _pid_map(workdir + '/' + PID_FILENAME): - print(name) + print(f"{name:<20} {'Damaged' if name in damage_set else 'OK'}") elif cmd == 'networks': # lp = LineProfiler() # sn_update_network = lp(sn_update_network) @@ -619,3 +672,4 @@ def sn_recover(dir, sat_loss): sn_check_route(workdir, sys.argv[4]) else: print('Unknown command') + os.close(main_net_fd) diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 9874c55..6cd5504 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -106,14 +106,18 @@ def __init__(self, id, host, port, username, password, def init_nodes(self): sn_remote_wait_output( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} nodes {self.dir}" + f"python3 {self.dir}/sn_orchestrater.py nodes {self.id} {self.dir}" ) def get_nodes(self): - return sn_remote_cmd( + lines = sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} list {self.dir}" - ).splitlines() + f"python3 {self.dir}/sn_orchestrater.py list {self.id} {self.dir}" + ).splitlines()[1:] + nodes = [ + line.split()[0] for line in lines + ] + return nodes def init_network(self, isl_bw, isl_loss, gsl_bw, gsl_loss): for shell in self.shell_lst: @@ -144,21 +148,21 @@ def init_network(self, isl_bw, isl_loss, gsl_bw, gsl_loss): def update_network(self, t, isl_bw, isl_loss, gsl_bw, gsl_loss): sn_remote_wait_output( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} networks {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py networks {self.id} {self.dir} " f"{t} {isl_bw} {isl_loss} {gsl_bw} {gsl_loss}" ) def init_routed(self, nodes): print(sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} routed {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py routed {self.id} {self.dir} " +','.join(nodes) )) def get_IP(self, node): lines = sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} IP {self.dir} {node}" + f"python3 {self.dir}/sn_orchestrater.py IP {self.id} {self.dir} {node}" ).splitlines() IP_dict = {} for line in lines: @@ -170,7 +174,7 @@ def ping_async(self, res_path, src, dst): def _ping_inner(ssh, dir, res_path, src, dst): output = sn_remote_cmd( ssh, - f"python3 {dir}/sn_orchestrater.py {self.id} ping {dir} " + f"python3 {dir}/sn_orchestrater.py ping {self.id} {dir} " f"{src} {dst} 2>&1" ) with open(res_path, 'w') as f: @@ -186,7 +190,7 @@ def iperf_async(self, res_path, src, dst): def _iperf_inner(ssh, dir, res_path, src, dst): output = sn_remote_cmd( ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} iperf {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py iperf {self.id} {self.dir} " f"{src} {dst} 2>&1" ) with open(res_path, 'w') as f: @@ -202,14 +206,14 @@ def _iperf_inner(ssh, dir, res_path, src, dst): def sr(self, src, dst, next_hop): sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} sr {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py sr {self.id} {self.dir} " f"{src} {dst} {next_hop} 2>&1" ) def check_route(self, res_path, sat): output = sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} rtable {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py rtable {self.id} {self.dir} " f"{sat} 2>&1" ) with open(res_path, 'w') as f: @@ -221,23 +225,23 @@ def check_utility(self, res_path): f.write(output) def damage(self, random_lst): - sn_remote_cmd( + print(sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} damage {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py damage {self.id} {self.dir} " + ','.join(random_lst) - ) + )) def recovery(self, sat_loss): sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} recovery {self.dir} " + f"python3 {self.dir}/sn_orchestrater.py recovery {self.id} {self.dir} " f"{sat_loss}" ) def clean(self): sn_remote_cmd( self.ssh, - f"python3 {self.dir}/sn_orchestrater.py {self.id} clean {self.dir}" + f"python3 {self.dir}/sn_orchestrater.py clean {self.id} {self.dir}" ) class StarryNet(): @@ -399,7 +403,7 @@ def create_links(self): thread.join() print("Link initialization:", time.time() - begin, 's consumed.') - def run_routing_deamon(self, node_lst='all'): + def run_routing_daemon(self, node_lst='all'): print('Initializing routing ...') if node_lst == 'all': for remote in self.remote_lst: From b39ef6275fbefa26bdc7b65d7f7feeffb9976595 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 3 Jul 2024 10:57:06 +0800 Subject: [PATCH 07/49] Update example.py fix: typos --- example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example.py b/example.py index 5a6bc09..85c8b74 100644 --- a/example.py +++ b/example.py @@ -19,10 +19,10 @@ hello_interval = 5 # hello_interval(s) in OSPF. 1-200 are supported. print('Start StarryNet.') - sn = StarryNet(configuration_file_path, GS_lat_long) + sn = StarryNet(configuration_file_path, GS_lat_long, hello_interval) node1 = 'SH1O25S14' - node1 = 'SH1O25S15' + node2 = 'SH1O25S15' time_index = 2 # LLA of a node at a certain time LLA = sn.get_position(node1, time_index) From a727e2f6135a7c240e1477046516a57f54747025 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sat, 4 Jan 2025 12:47:07 +0800 Subject: [PATCH 08/49] feat: support topology specified by manual.json --- example.py | 20 +- manual.json | 83 ++++++ starrynet/sn_observer.py | 539 ++++++++++++++++++----------------- starrynet/sn_orchestrater.py | 211 ++++++-------- starrynet/sn_synchronizer.py | 168 ++++++----- starrynet/sn_utils.py | 18 +- 6 files changed, 553 insertions(+), 486 deletions(-) create mode 100644 manual.json diff --git a/example.py b/example.py index 85c8b74..e21a04c 100644 --- a/example.py +++ b/example.py @@ -15,14 +15,19 @@ GS_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817] ] # latitude and longitude of frankfurt and Austria - configuration_file_path = "./config.json" + configuration_file_path = "./manual.json" hello_interval = 5 # hello_interval(s) in OSPF. 1-200 are supported. print('Start StarryNet.') sn = StarryNet(configuration_file_path, GS_lat_long, hello_interval) - node1 = 'SH1O25S14' - node2 = 'SH1O25S15' + sn.create_nodes() + sn.create_links() + + exit(0) + + node1 = 'SH1O2S4' + node2 = 'SH1O2S5' time_index = 2 # LLA of a node at a certain time LLA = sn.get_position(node1, time_index) @@ -37,12 +42,9 @@ print(f'\n{node1} neighbors: {neighbors}') # GS connected to the node at a certain time - GSes = sn.get_GSes(node1, time_index) - print(f"\n{node1} GSes: {GSes}") + GSes = sn.get_GSes('SH1O2S2', time_index) + print(f"\nSH1O2S2 GSes: {GSes}") - sn.create_nodes() - sn.create_links() - time_index = 2 sn.get_utility(time_index) # CPU and memory useage @@ -73,7 +75,7 @@ # sn.run_routing_daemon() # run OSPF daemon on selected nodes - sn.run_routing_deamon(node_lst=[ + sn.run_routing_daemon(node_lst=[ 'GS1', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS2']) sn.check_routing_table('GS2', 4) diff --git a/manual.json b/manual.json new file mode 100644 index 0000000..f383d38 --- /dev/null +++ b/manual.json @@ -0,0 +1,83 @@ +{ + "Name": "starlink", + "Satellite link": "Arbitrary", + "Shells": [ + { + "timeslots": [ + { + "position": [ + { + "latitude": 56.5, + "longitude": -20.1, + "altitude": 550 + }, + { + "latitude": 57.5, + "longitude": -23.1, + "altitude": 545 + } + ], + "links":[ + { + "sat1":0, + "sat2":1 + } + ] + }, + { + "position": [ + { + "latitude": 56.5, + "longitude": -20.1, + "altitude": 550 + }, + { + "latitude": 57.5, + "longitude": -23.1, + "altitude": 545 + } + ], + "links":[] + }, + { + "position": [ + { + "latitude": 56.5, + "longitude": -20.1, + "altitude": 550 + }, + { + "latitude": 57.5, + "longitude": -23.1, + "altitude": 545 + } + ], + "links":[ + { + "sat1":0, + "sat2":1 + } + ] + } + ] + } + ], + "step (s)": 1, + "satellite link bandwidth (\"X\" Gbps)": 5, + "sat-ground bandwidth (\"X\" Gbps)": 5, + "satellite link loss (\"X\"% )": 0, + "sat-ground loss (\"X\"% )": 1, + "antenna number": 1, + "antenna elevation angle": 25, + "IP version": "IPv4", + "Link policy": "LeastDelay", + "Handover policy": "instant handover", + "Machines": [ + { + "IP": "127.0.0.1", + "port": 22, + "username": "root", + "password": "123456" + } + ] +} \ No newline at end of file diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index 50d4ae4..e336a18 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -7,48 +7,12 @@ from skyfield.api import load, wgs84, EarthSatellite -def _isl_grid(sat_cbf_t_shell): - # [[[[ [isl] for every satellite] for every orbit] for every t] for every shell] - isls_t_shell = [] - isl_offset = 0 - for sat_cbf_t in sat_cbf_t_shell: - orbit_num, sat_num = sat_cbf_t.shape[1], sat_cbf_t.shape[2] - down_cbf_t = np.roll(sat_cbf_t, -1, 2) - right_cbf_t = np.roll(sat_cbf_t, -1, 1) - delay_down_t = np.sqrt(np.sum(np.square(sat_cbf_t - down_cbf_t), -1)) / ( - 17.31 / 29.5 * 299792.458) * 1000 # ms - delay_right_t = np.sqrt(np.sum(np.square(sat_cbf_t - right_cbf_t), -1)) / ( - 17.31 / 29.5 * 299792.458) * 1000 # ms - isls_t = [] - for delay_down, delay_right in zip(delay_down_t, delay_right_t): - orbit_lst = [] - idx = isl_offset - for oid in range(orbit_num): - sat_lst = [] - for sid in range(sat_num): - sat_lst.append([ - # (isl_idx, orbit_id, sat_id, delay in ms) - # down isl - (idx, oid, sid + 1 if sid + 1 < sat_num else 0, - delay_down[oid, sid]), - # right isl - (idx + 1, oid + 1 if oid + 1 < orbit_num else 0, sid, - delay_right[oid, sid]), - ]) - idx += 2 - orbit_lst.append(sat_lst) - isls_t.append(orbit_lst) - isl_offset += orbit_num * sat_num * 2 - isls_t_shell.append(isls_t) - return isls_t_shell - -def _gsl_least_delay(sat_cbf_t_shell, gs_cbf, antenna_num, bound_dis): +def _gsl_least_delay(topo_t_shell, gs_cbf, antenna_num): gsls_t_shell = [] # [[[ [gsl] for every gs] for every ts] for every shell] - for sat_cbf_t in sat_cbf_t_shell: + for shellname, name_lst, sat_cbf_t, isls_t in topo_t_shell: gsls_t = [] for sat_cbf in sat_cbf_t: - orbit_num, sat_num = sat_cbf.shape[0], sat_cbf.shape[1] - # (gs_num) op (orbit_num, sat_num) -> (gs_num, orbit_num, sat_num) + # (gs_num) op (sat_num) -> (gs_num, sat_num) dx = np.subtract.outer(gs_cbf[..., 0], sat_cbf[..., 0]) dy = np.subtract.outer(gs_cbf[..., 1], sat_cbf[..., 1]) dz = np.subtract.outer(gs_cbf[..., 2], sat_cbf[..., 2]) @@ -56,13 +20,15 @@ def _gsl_least_delay(sat_cbf_t_shell, gs_cbf, antenna_num, bound_dis): gsls = [] for gs_dist in dist: gs_dist = gs_dist.flatten() - bound_mask = gs_dist < bound_dis + #TODO: elevation angle bound + # bound_mask = gs_dist < bound_dis + bound_mask = gs_dist == gs_dist sat_indices = np.arange(len(gs_dist))[bound_mask] gs_dist = gs_dist[bound_mask] sorted_sat = gs_dist.argsort() gsls.append([ - # (orbit_id, sat_id, delay in ms) - (sat_indices[sat] // sat_num, sat_indices[sat] % sat_num, + # (sat_id, delay in ms) + (name_lst[sat_indices[sat]], gs_dist[sat] / (17.31 / 29.5 * 299792.458) * 1000) for sat in sorted_sat[:antenna_num] ]) @@ -71,32 +37,17 @@ def _gsl_least_delay(sat_cbf_t_shell, gs_cbf, antenna_num, bound_dis): # merge different shell # [[gsls for every shell] for every gs] for every t] - gsl_shell_gs_t = [ + gsls_t = [ [list() for gid in range(len(gs_cbf))] for t in range(len(gsls_t_shell[0])) ] - gsl_idx_dict = {} - for t, gsl_shell_gs in enumerate(gsl_shell_gs_t): - for gid, gsl_lst in enumerate(gsl_shell_gs): + for t, gsls in enumerate(gsls_t): + for gid, gsl_lst in enumerate(gsls): for shell_id in range(len(gsls_t_shell)): - for oid, sid, delay in gsls_t_shell[shell_id][t][gid]: + for sat_name, delay in gsls_t_shell[shell_id][t][gid]: if len(gsl_lst) >= antenna_num: break - if (gid, shell_id, oid, sid) in gsl_idx_dict: - idx = gsl_idx_dict[(gid, shell_id, oid, sid)] - else: - idx = len(gsl_idx_dict) - gsl_idx_dict[(gid, shell_id, oid, sid)] = idx - gsl_lst.append((idx, shell_id, oid, sid, delay)) - return gsl_shell_gs_t - -#TODO: More ISL styles -isl_styles = { - 'Grid': _isl_grid, -} -#TODO: More GSL styles -gsl_styles = { - 'LeastDelay':_gsl_least_delay, -} + gsl_lst.append((sat_name, delay)) + return gsls_t def to_cbf(lat_long):# the xyz coordinate system. lat_long = np.array(lat_long) @@ -111,174 +62,237 @@ def to_cbf(lat_long):# the xyz coordinate system. y_mat = rho_mat * np.sin(phi_mat) return np.stack((x_mat, y_mat, z_mat), -1) -def _bound_gsl(antenna_elevation, altitude): - a = 6371 * np.cos(np.radians(90 + antenna_elevation)) - return a + np.sqrt(np.square(a) + np.square(altitude) + 2 * altitude * 6371) +# def _bound_gsl(antenna_elevation, altitude): +# a = 6371 * np.cos(np.radians(90 + antenna_elevation)) +# return a + np.sqrt(np.square(a) + np.square(altitude) + 2 * altitude * 6371) -def calculate_delay( - dir, duration, step, shell_lst, isl_style, - GS_lat_long, antenna_number, antenna_elevation, gsl_style - ): +def _sat_name(shell_id, orbit_id, sat_id): + return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' + +def _gs_name(gid): + return f'GS{gid}' + +def _isl_grid(sat_cbf_t, shell_id, orbit_num, sat_num): + # [[ [isl] for every satellite] for every t] + isls_t = [] + + sat_cbf_t = sat_cbf_t.reshape(-1, orbit_num, sat_num, 3) + down_cbf_t = np.roll(sat_cbf_t, -1, 2) + right_cbf_t = np.roll(sat_cbf_t, -1, 1) + delay_down_t = np.sqrt(np.sum(np.square(sat_cbf_t - down_cbf_t), -1)) / ( + 17.31 / 29.5 * 299792.458) * 1000 # ms + delay_right_t = np.sqrt(np.sum(np.square(sat_cbf_t - right_cbf_t), -1)) / ( + 17.31 / 29.5 * 299792.458) * 1000 # ms + for delay_down, delay_right in zip(delay_down_t, delay_right_t): + isl_lst = [] + for oid in range(orbit_num): + for sid in range(sat_num): + # down isl + down_oid = oid + down_sid = sid + 1 if sid + 1 < sat_num else 0 + # right isl + right_oid = oid + 1 if oid + 1 < orbit_num else 0 + right_sid = sid + isl_lst.append([ + # (sat_name, delay in ms) + (_sat_name(shell_id, down_oid, down_sid), delay_down[oid, sid]), + # right isl + (_sat_name(shell_id, right_oid, right_sid), delay_right[oid, sid]), + ]) + isls_t.append(isl_lst) + return isls_t + +def _topo_walker_delta(dir, duration, step, shell_lst): ts_total = int(duration / step) - cached = True - for shell in shell_lst: + topo_t_shell = [] + ts = load.timescale() + since = datetime.datetime(1949, 12, 31, 0, 0, 0) + start = datetime.datetime(2020, 1, 1, 0, 0, 0) + epoch = (start - since).days + GM = 3.9860044e14 + R = 6371393 + F = 18 + ts_lst = [i * step for i in range(ts_total)] + for i, shell in enumerate(shell_lst): + inclination = shell['inclination'] * 2 * np.pi / 360 + altitude = shell['altitude'] * 1000 + mean_motion = np.sqrt(GM / (R + altitude)**3) * 60 + orbit_number, sat_number = shell['orbit'], shell['sat'] + num_of_sat = orbit_number * sat_number + + sat_lla_t = np.zeros((ts_total, orbit_number * sat_number, 3)) + for oid in range(orbit_number): + raan = oid / orbit_number * 2 * np.pi + for sid in range(sat_number): + mean_anomaly = (sid * 360 / sat_number + oid * 360 * F / + num_of_sat) % 360 * 2 * np.pi / 360 + satrec = Satrec() + satrec.sgp4init( + WGS84, # gravity model + 'i', # 'a' = old AFSPC mode, 'i' = improved mode + oid * sat_number + sid, # satnum: Satellite number + epoch, # epoch: days since 1949 December 31 00:00 UT + 2.8098e-05, # bstar: drag coefficient (/earth radii) + 6.969196665e-13, # ndot: ballistic coefficient (revs/day) + 0.0, # nddot: second derivative of mean motion (revs/day^3) + 0.001, # ecco: eccentricity + 0.0, # argpo: argument of perigee (radians) + inclination, # inclo: inclination (radians) + mean_anomaly, # mo: mean anomaly (radians) + mean_motion, # no_kozai: mean motion (radians/minute) + raan, # nodeo: right ascension of ascending node (radians) + ) + sat = EarthSatellite.from_satrec(satrec, ts) + cur = datetime.datetime(2022, 1, 1, 1, 0, 0) + t_ts = ts.utc(*cur.timetuple()[:5], ts_lst) # [:4]:minute,[:5]:second + geocentric = sat.at(t_ts) + subpoint = wgs84.subpoint(geocentric) + # list: [subpoint.latitude.degrees] [subpoint.longitude.degrees] [subpoint.elevation.km] + for t in range(ts_total): + sat_lla_t[t, oid * sat_number + sid] = (subpoint.latitude.degrees[t], + subpoint.longitude.degrees[t], + subpoint.elevation.km[t]) + + name_lst = [_sat_name(i, oid, sid) + for oid in range(orbit_number) for sid in range(sat_number)] pos_dir = os.path.join(dir, shell['name'], 'position') os.makedirs(pos_dir, exist_ok=True) - if len(glob.glob(os.path.join(pos_dir, '*.txt'))) != ts_total: - cached = False - os.makedirs(os.path.join(dir, shell['name'], 'isl'), exist_ok=True) - for file in glob.glob(os.path.join(dir, shell['name'], 'isl', '*.txt')): - os.remove(file) - gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') - os.makedirs(gsl_dir, exist_ok=True) - for file in glob.glob(os.path.join(gsl_dir, '*.txt')): - os.remove(file) - - sat_cbf_t_shell = [] - if cached and input(f"Use cached local files [y/n]?").strip().lower()[:1] == 'y': - for shell in shell_lst: - orbit_number, sat_number = shell['orbit'], shell['sat'] - sat_lla_t = np.zeros((ts_total, orbit_number, sat_number, 3)) - pos_dir = os.path.join(dir, shell['name'], 'position') - for t in range(ts_total): - f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'r') - for oid in range(orbit_number): - for sid in range(sat_number): - sat_lla_t[t,oid,sid]=list(map(float, f.readline().split(','))) - f.readline() - f.close() - sat_cbf_t_shell.append(to_cbf(sat_lla_t)) - else: - for shell in shell_lst: - for file in glob.glob(os.path.join(dir, shell['name'], 'position', '*.txt')): - os.remove(file) + for t, lla_lst in enumerate(sat_lla_t): + f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') + for name, lla in zip(name_lst, lla_lst): + f.write('%s:%f,%f,%f\n' % (name, lla[0], lla[1], lla[2])) + f.close() + sat_cbf_t = to_cbf(sat_lla_t) + isls_t = _isl_grid(sat_cbf_t, i, orbit_number, sat_number) - ts = load.timescale() - since = datetime.datetime(1949, 12, 31, 0, 0, 0) - start = datetime.datetime(2020, 1, 1, 0, 0, 0) - epoch = (start - since).days - GM = 3.9860044e14 - R = 6371393 - F = 18 - ts_lst = [i * step for i in range(ts_total)] - for i, shell in enumerate(shell_lst): - inclination = shell['inclination'] * 2 * np.pi / 360 - altitude = shell['altitude'] * 1000 - mean_motion = np.sqrt(GM / (R + altitude)**3) * 60 - orbit_number, sat_number = shell['orbit'], shell['sat'] - num_of_sat = orbit_number * sat_number + topo_t_shell.append((shell['name'], name_lst, sat_cbf_t, isls_t)) + return topo_t_shell - sat_lla_t = np.zeros((ts_total, orbit_number, sat_number, 3)) - for oid in range(orbit_number): - raan = oid / orbit_number * 2 * np.pi - for sid in range(sat_number): - mean_anomaly = (sid * 360 / sat_number + oid * 360 * F / - num_of_sat) % 360 * 2 * np.pi / 360 - satrec = Satrec() - satrec.sgp4init( - WGS84, # gravity model - 'i', # 'a' = old AFSPC mode, 'i' = improved mode - oid * sat_number + sid, # satnum: Satellite number - epoch, # epoch: days since 1949 December 31 00:00 UT - 2.8098e-05, # bstar: drag coefficient (/earth radii) - 6.969196665e-13, # ndot: ballistic coefficient (revs/day) - 0.0, # nddot: second derivative of mean motion (revs/day^3) - 0.001, # ecco: eccentricity - 0.0, # argpo: argument of perigee (radians) - inclination, # inclo: inclination (radians) - mean_anomaly, # mo: mean anomaly (radians) - mean_motion, # no_kozai: mean motion (radians/minute) - raan, # nodeo: right ascension of ascending node (radians) - ) - sat = EarthSatellite.from_satrec(satrec, ts) - cur = datetime.datetime(2022, 1, 1, 1, 0, 0) - t_ts = ts.utc(*cur.timetuple()[:5], ts_lst) # [:4]:minute,[:5]:second - geocentric = sat.at(t_ts) - subpoint = wgs84.subpoint(geocentric) - # list: [subpoint.latitude.degrees] [subpoint.longitude.degrees] [subpoint.elevation.km] - for t in range(ts_total): - sat_lla_t[t, oid, sid] = (subpoint.latitude.degrees[t], - subpoint.longitude.degrees[t], - subpoint.elevation.km[t]) +def _topo_arbitrary(dir, duration, step, shell_lst): + topo_t_shell = [] + for i, shell in enumerate(shell_lst): + name_lst = [] + sat_cbf_t = [] + isls_t = [] + for t, slot in enumerate(shell['timeslots']): + sat_lla = [] + sat_names = [] pos_dir = os.path.join(dir, shell['name'], 'position') - for t, sat_lla in enumerate(sat_lla_t): - f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') - for sat_lst in sat_lla: - for sat in sat_lst: - f.write('%f,%f,%f\n' % (sat[0], sat[1], sat[2])) - f.write('\n') - f.close() - sat_cbf_t_shell.append(to_cbf(sat_lla_t)) + os.makedirs(pos_dir, exist_ok=True) + f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') + for sid, node in enumerate(slot['position']): + print(node) + lla = (node['latitude'], node['longitude'], node['altitude']) + sat_lla.append(lla) + name = f'SH{i+1}SAT{sid+1}' + f.write(name + (':%f,%f,%f\n' % lla)) + sat_names.append(name) + f.close() + sat_cbf = to_cbf(sat_lla) # np array, sat_num * 3 + sat_cbf_t.append(sat_cbf) + + if len(name_lst) == 0: + name_lst = sat_names + elif len(name_lst) != len(sat_names): + raise RuntimeError("satellites change between slots!") - isls_t_shell = isl_styles[isl_style](sat_cbf_t_shell) - for i, isls_t in enumerate(isls_t_shell): - isl_dir = os.path.join(dir, shell_lst[i]['name'], 'isl') + isls = [list() for _ in range(len(name_lst))] + for link in slot['links']: + sid1, sid2 = link['sat1'], link['sat2'] + if sid1 > sid2: + sid1, sid2 = sid2, sid1 + delay = np.sqrt(np.sum(np.square(sat_cbf[sid1] - sat_cbf[sid2]))) + isls[sid1].append((name_lst[sid2], delay)) + isls_t.append(isls) + topo_t_shell.append((shell['name'], name_lst, sat_cbf_t, isls_t)) + return topo_t_shell - isl_state = [ [list() for _ in range(shell_lst[i]['sat'])] - for _ in range(shell_lst[i]['orbit'])] - for t in range(ts_total): - f1 = open(f"{isl_dir}/{t}-state.txt", 'w') - f2 = open(f"{isl_dir}/{t}.txt", 'w') - for oid, sat_lst in enumerate(isls_t[t]): - for sid, isl_lst in enumerate(sat_lst): - # one line for each satellite - f1.write(f"{oid},{sid}:") - f1.write(' '.join(f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" - for isl in isl_lst)) - f1.write('\n') +def _write_link_files(dir, topo_t_shell, gsls_t, GS_lat_long): + # ISL + idx_dict = {} + cnt = 0 + for shell_name, sat_name_lst, sat_cbf_t, isls_t in topo_t_shell: + isl_dir = os.path.join(dir, shell_name, 'isl') + os.makedirs(isl_dir, exist_ok=True) + for file in glob.glob(os.path.join(isl_dir, '*.txt')): + os.remove(file) + + isl_state = [list() for _ in range(len(sat_name_lst))] + for t, isls in enumerate(isls_t): + f_state = open(f"{isl_dir}/{t}-state.txt", 'w') + # for update + f_update = open(f"{isl_dir}/{t}.txt", 'w') + for sid, isl_lst in enumerate(isls): + # one line for each satellite + f_state.write(f"{sat_name_lst[sid]}:") + f_state.write(' '.join(f"{isl[0]},{isl[1]:.2f}" + for isl in isl_lst)) + f_state.write('\n') - f2.write(f"{oid},{sid}|") - old_lst = isl_state[oid][sid] - old_del = [True] * len(old_lst) - new_add = [True] * len(isl_lst) - update = [] - for i, old in enumerate(old_lst): - for j, new in enumerate(isl_lst): - if old[0] != new[0]: - continue - old_del[i] = False - new_add[j] = False - if abs(new[3] - old[3]) > 1e-2: - update.append(new) - else: - isl_lst[j] = old - # del some isls - f2.write(' '.join( - f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" - for isl, de in zip(old_lst, old_del) if de - ) + '|') - # update some isls - f2.write(' '.join( - f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" - for isl in update - ) + '|') - # add some isls - f2.write(' '.join( - f"{isl[0]},{isl[1]},{isl[2]},{isl[3]:.2f}" - for isl, add in zip(isl_lst, new_add) if add - ) + '\n') - isl_state[oid][sid] = isl_lst - f1.write('\n') - f2.write('\n') - f1.close() - f2.close() + f_update.write(f"{sat_name_lst[sid]}|") + old_lst = isl_state[sid] + old_del = [True] * len(old_lst) + new_add = [True] * len(isl_lst) + update = [] + for i, old in enumerate(old_lst): + for j, new in enumerate(isl_lst): + if old[0] != new[0]: + continue + # same link, update + old_del[i] = False + new_add[j] = False + if abs(new[1] - old[1]) > 1e-2: + update.append(new) + else: + isl_lst[j] = old + # del some isls + f_update.write(' '.join( + f"{isl[0]}" + for isl, de in zip(old_lst, old_del) if de + ) + '|') + # update some isls + f_update.write(' '.join( + f"{isl[0]},{isl[1]:.2f}" + for isl in update + ) + '|') + # add some isls + add_lst = [] + for isl, add in zip(isl_lst, new_add): + if not add: + continue + key = f'{sat_name_lst[sid]}-{isl[0]}' + if key in idx_dict: + idx = idx_dict[key] + else: + cnt += 1 + idx_dict[key] = idx = cnt + add_lst.append(f"{isl[0]},{isl[1]:.2f},{idx}") + f_update.write(' '.join(add_lst)) + f_update.write('\n') + isl_state[sid] = isl_lst + f_state.close() + f_update.close() + # GSL + gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') + os.makedirs(gsl_dir, exist_ok=True) + for file in glob.glob(os.path.join(gsl_dir, '*.txt')): + os.remove(file) - gs_cbf = to_cbf(GS_lat_long) - bound_dis = _bound_gsl(antenna_elevation, shell['altitude']) - gsls_t = gsl_styles[gsl_style](sat_cbf_t_shell, gs_cbf, antenna_number, bound_dis) + idx_dict = {} + cnt = 0 gsl_state = [list() for _ in range(len(GS_lat_long))] for t, gsls in enumerate(gsls_t): - f1 = open(f"{gsl_dir}/{t}-state.txt", 'w') - f2 = open(f"{gsl_dir}/{t}.txt", 'w') + f_state = open(f"{gsl_dir}/{t}-state.txt", 'w') + f_update = open(f"{gsl_dir}/{t}.txt", 'w') for gid, gsl_lst in enumerate(gsls): # one line for each ground station - f1.write(f"{gid}:") - f1.write(' '.join(f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + f_state.write(f"{_gs_name(gid)}:") + f_state.write(' '.join(f"{gsl[0]},{gsl[1]:.2f}" for gsl in gsl_lst)) - f1.write('\n') + f_state.write('\n') - f2.write(f"{gid}|") - # add some isls + f_update.write(f"{_gs_name(gid)}|") + # del some gsls old_lst = gsl_state[gid] old_del = [True] * len(old_lst) new_add = [True] * len(gsl_lst) @@ -288,67 +302,74 @@ def calculate_delay( if old[0] == new[0]: old_del[i] = False new_add[j] = False - if abs(new[4] - old[4]) > 1e-2: + if abs(new[1] - old[1]) > 1e-2: update.append(new) else: gsl_lst[j] = old # if not update, remain old delay - f2.write(' '.join( - f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + f_update.write(' '.join( + f"{gsl[0]}" for gsl, de in zip(old_lst, old_del) if de ) + '|') # update some gsls - f2.write(' '.join( - f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" + f_update.write(' '.join( + f"{gsl[0]},{gsl[1]:.2f}" for gsl in update ) + '|') - # del some gsls - f2.write(' '.join( - f"{gsl[0]},{gsl[1]},{gsl[2]},{gsl[3]},{gsl[4]:.2f}" - for gsl, add in zip(gsl_lst, new_add) if add - ) + '\n') + # add some gsls + add_lst = [] + for gsl, add in zip(gsl_lst, new_add): + if not add: + continue + key = f'{_gs_name(gid)}-{gsl[0]}' + if key in idx_dict: + idx = idx_dict[key] + else: + cnt += 1 + idx_dict[key] = idx = cnt + add_lst.append(f"{gsl[0]},{gsl[1]:.2f},{idx}") + f_update.write(' '.join(add_lst)) + f_update.write('\n') gsl_state[gid] = gsl_lst - f1.close() - f2.close() + f_state.close() + f_update.close() + +def gen_topo(dir, duration, step, + shell_lst, isl_style, + GS_lat_long, antenna_number, antenna_elevation, gsl_style): + topo_t_shell = topo_styles[isl_style](dir, duration, step, shell_lst) + gsls_t = gsl_styles[gsl_style](topo_t_shell, to_cbf(GS_lat_long), antenna_number) + _write_link_files(dir, topo_t_shell, gsls_t, GS_lat_long) + sat_names_shell = [shell[1] for shell in topo_t_shell] + return sat_names_shell def load_pos(path): f = open(path, 'r') - lla_mat = [] - lla_lst = [] - oid, sid = 0, 0 + lla_dict = {} for line in f: - if len(line) == 0 or line.isspace(): - lla_mat.append(lla_lst) - lla_lst = [] - sid = 0 - oid += 1 - continue - lla_lst.append(list(map(float, line.strip().split(',')))) + toks = line.strip().split(':') + lla = tuple(map(float, toks[1].split(','))) + lla_dict[toks[0]] = lla f.close() - return lla_mat + return lla_dict -def load_isl_state(path): +def load_links_dict(path): f = open(path, 'r') - isl_mat = [] - isl_lst = [] - oid, sid = 0, 0 + links_dict = {} for line in f: - if len(line) == 0 or line.isspace(): - isl_mat.append(isl_lst) - isl_lst = [] - sid = 0 - oid += 1 - continue - line = line[line.find(':')+1:].strip() - isl_lst.append(line.split()) + toks = line.strip().split(':') + link_lst = [] + for isl in toks[1].split(): + link_lst.append(isl.split(',')) + links_dict[toks[0]] = link_lst f.close() - return isl_mat + return links_dict -def load_gsl_state(path): - f = open(path, 'r') - gsl_lst = [] - gid = 0 - for line in f: - line = line[line.find(':')+1:].strip() - gsl_lst.append(line.split()) - f.close() - return gsl_lst +#TODO: More ISL styles +topo_styles = { + 'Grid': _topo_walker_delta, + 'Arbitrary': _topo_arbitrary, +} +#TODO: More GSL styles +gsl_styles = { + 'LeastDelay':_gsl_least_delay, +} diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 3970729..b2a05ea 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -2,6 +2,8 @@ import os import subprocess import sys +import re +import json import glob import ctypes # from line_profiler import LineProfiler @@ -12,7 +14,7 @@ author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) and Zeqi Lai (zeqilai@tsinghua.edu.cn) """ -ASSIGN_FILENAME = 'assign.txt' +ASSIGN_FILENAME = 'assign.json' PID_FILENAME = 'container_pid.txt' DAMAGE_FILENAME = 'damage_list.txt' @@ -53,45 +55,29 @@ def _pid_map(pid_path, pop = False): def _get_params(path): with open(path, 'r') as f: - gs_mid = [int(mid) for mid in f.readline().split()] - sat_mid_lst, ip_lst = [], [] - for line in f: - if len(line) == 0 or line.isspace(): - break - toks = line.strip().split(' ') - orbit_num, shell_name = int(toks[0]), toks[1] - sat_mid = [int(mid) for mid in f.readline().split(' ')] - sat_mid_lst.append((orbit_num, shell_name, sat_mid)) - for line in f: - ip_lst.append(line.strip()) - return gs_mid, sat_mid_lst, ip_lst + obj = json.load(f) + sat_mid_dict_shell = obj['sat_mid_shell'] + gs_mid_dict = obj['gs_mid'] + ip_lst = obj['ip'] + return sat_mid_dict_shell, gs_mid_dict, ip_lst def _parse_isls(path): del_lst, update_lst, add_lst = [], [], [] f = open(path, 'r') - oid, sid = 0, 0 for line in f: - if len(line) == 0 or line.isspace(): - oid += 1 - sid = 0 - continue toks = line.strip().split('|') + sat_name = toks[0] if len(toks[1]) > 0: - for isl in toks[1].split(' '): - i_o_s_d = isl.split(',') - idx, isl_oid, isl_sid = int(i_o_s_d[0]),int(i_o_s_d[1]),int(i_o_s_d[2]) - del_lst.append((idx, oid, sid, isl_oid, isl_sid, i_o_s_d[3])) + for isl_sat in toks[1].split(' '): + del_lst.append((sat_name, isl_sat)) if len(toks[2]) > 0: - for isl in toks[2].split(' '): - i_o_s_d = isl.split(',') - idx, isl_oid, isl_sid = int(i_o_s_d[0]),int(i_o_s_d[1]),int(i_o_s_d[2]) - update_lst.append((idx, oid, sid, isl_oid, isl_sid, i_o_s_d[3])) + for isl_sat in toks[2].split(' '): + sat_delay = isl_sat.split(',') + update_lst.append((sat_name, sat_delay[0], sat_delay[1])) if len(toks[3]) > 0: - for isl in toks[3].split(' '): - i_o_s_d = isl.split(',') - idx, isl_oid, isl_sid = int(i_o_s_d[0]),int(i_o_s_d[1]),int(i_o_s_d[2]) - add_lst.append((idx, oid, sid, isl_oid, isl_sid, i_o_s_d[3])) - sid += 1 + for isl_sat in toks[3].split(' '): + idx_sat_delay = isl_sat.split(',') + add_lst.append((sat_name, idx_sat_delay[0], idx_sat_delay[1], int(idx_sat_delay[2]))) f.close() return del_lst, update_lst, add_lst @@ -124,7 +110,7 @@ def _parse_gsls(path): return del_lst, update_lst, add_lst # name1 in local machine -def _del_link(idx, name1, name2): +def _del_link(name1, name2): n1_n2 = f"{name2}" fd = os.open('/run/netns/' + name1, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) @@ -152,14 +138,14 @@ def _update_if(name, if_name, delay, bw, loss): 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') ) -def _update_link_intra_machine(idx, name1, name2, delay, bw, loss): +def _update_link_intra_machine(name1, name2, delay, bw, loss): n1_n2 = f"{name2}" n2_n1 = f"{name1}" _update_if(name1, n1_n2, delay, bw, loss) _update_if(name2, n2_n1, delay, bw, loss) # name1 in local machine -def _update_link_local(idx, name1, name2, delay, bw, loss): +def _update_link_local(name1, name2, delay, bw, loss): n1_n2 = f"{name2}" _update_if(name1, n1_n2, delay, bw, loss) @@ -184,7 +170,7 @@ def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix, delay, bw, los _init_if(name1, n1_n2, prefix+'.10/24', delay, bw, loss) _init_if(name2, n2_n1, prefix+'.40/24', delay, bw, loss) -def sn_init_nodes(dir, gs_mid, sat_mid_lst): +def sn_init_nodes(dir, sat_mid_dict_shell, gs_mid_dict): def _load_netns(pid, name): netns_link = f'/run/netns/{name}' if not os.path.exists(netns_link): @@ -200,130 +186,94 @@ def _load_netns(pid, name): stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) - pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') - for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): - if all(assign != machine_id for assign in sat_mid): - continue - shell_dir = f"{dir}/{shell_name}" - overlay_dir = shell_dir + '/overlay' - os.makedirs(overlay_dir, exist_ok=True) - for sid, assign in enumerate(sat_mid): - if assign != machine_id: - pid_file.write(' '.join(NOT_ASSIGNED for _ in range(orbit_num)) + '\n') - continue - print(f'[{machine_id}] Satellite: {shell_id},(0-{orbit_num}),{sid}') - for oid in range(orbit_num): - name = _sat_name(shell_id, oid, sid) - node_dir = f'{overlay_dir}/{name}' - pid_file.write(name+':'+str(pyctr.container_run(node_dir, name))+' ') - pid_file.write('\n') - if len(gs_mid) > 0 and any(assign == machine_id for assign in gs_mid): - gs_dir = f"{dir}/GS-{len(gs_mid)}" - overlay_dir = gs_dir + '/overlay' - os.makedirs(overlay_dir, exist_ok=True) - gs_lst = [] - for gid, assign in enumerate(gs_mid): - if assign != machine_id: - pid_file.write(NOT_ASSIGNED + ' ') - continue - gs_lst.append(str(gid)) - name = _gs_name(gid) - node_dir = f'{overlay_dir}/{name}' - pid_file.write(name+':'+str(pyctr.container_run(node_dir, name))+' ') - pid_file.write('\n') - print(f'[{machine_id}] GS:', ','.join(gs_lst)) - - pid_file.close() subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) + + pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') + sat_cnt = 0 + for shell_id, mid_dict in enumerate(sat_mid_dict_shell): + for node, mid in mid_dict.items(): + if mid != machine_id: + pid_file.write(NOT_ASSIGNED + ' ') + continue + node_dir = f"{dir}/shell{shell_id}/overlay/{node}" + sat_cnt += 1 + os.makedirs(node_dir, exist_ok=True) + pid_file.write(node+':'+str(pyctr.container_run(node_dir, node))+' ') + pid_file.write('\n') + print(f'[{machine_id}] shell {shell_id}: {sat_cnt} satellites initialized') + + gs_lst = [] + overlay_dir = f"{dir}/GS-{len(gs_mid_dict)}/overlay" + for node, mid in gs_mid_dict.items(): + if mid != machine_id: + pid_file.write(NOT_ASSIGNED + ' ') + continue + gs_lst.append(node) + node_dir = f'{overlay_dir}/{node}' + os.makedirs(node_dir, exist_ok=True) + pid_file.write(node+':'+str(pyctr.container_run(node_dir, node))+' ') + pid_file.write('\n') + print(f'[{machine_id}] GS:', ','.join(gs_lst)) + + pid_file.close() sn_operate_every_node(dir, _load_netns) def sn_update_network( - dir, ts, sat_mid_lst, gs_mid, ip_lst, + dir, ts, sat_mid_dict_shell, gs_mid_dict, ip_lst, isl_bw, isl_loss, gsl_bw, gsl_loss ): - for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): - shell_dir = f"{dir}/{shell_name}" + for shell_id, mid_dict in enumerate(sat_mid_dict_shell): + shell_dir = f"{dir}/shell{shell_id}" if not os.path.exists(shell_dir): continue del_cnt, update_cnt, add_cnt = 0, 0, 0 del_lst, update_lst, add_lst = _parse_isls(f'{shell_dir}/{ts}.txt') - for idx, oid, sid, isl_oid, isl_sid, delay in del_lst: - if sat_mid[sid] == machine_id: + for sat_name, isl_sat in del_lst: + if mid_dict[sat_name] == machine_id: del_cnt += 1 - _del_link( - idx, - _sat_name(shell_id, oid, sid), - _sat_name(shell_id, isl_oid, isl_sid) - ) - elif sat_mid[isl_sid] == machine_id: + _del_link(sat_name, isl_sat) + elif mid_dict[isl_sat] == machine_id: del_cnt += 1 - _del_link( - idx, - _sat_name(shell_id, isl_oid, isl_sid), - _sat_name(shell_id, oid, sid) - ) - for idx, oid, sid, isl_oid, isl_sid, delay in update_lst: - if sat_mid[sid] == machine_id: + _del_link(isl_sat, sat_name) + for sat_name, isl_sat, delay in update_lst: + if mid_dict[sat_name] == machine_id: update_cnt += 1 - if sat_mid[isl_sid] == machine_id: - _update_link_intra_machine( - idx, - _sat_name(shell_id, oid, sid), - _sat_name(shell_id, isl_oid, isl_sid), - delay, isl_bw, isl_loss - ) + if mid_dict[isl_sat] == machine_id: + _update_link_intra_machine(sat_name, isl_sat, delay, isl_bw, isl_loss) else: - _update_link_local( - idx, - _sat_name(shell_id, oid, sid), - _sat_name(shell_id, isl_oid, isl_sid), - delay, isl_bw, isl_loss - ) - elif sat_mid[isl_sid] == machine_id: + _update_link_local(sat_name, isl_sat, delay, isl_bw, isl_loss) + elif mid_dict[isl_sat] == machine_id: update_cnt += 1 - _update_link_local( - idx, - _sat_name(shell_id, isl_oid, isl_sid), - _sat_name(shell_id, oid, sid), - delay, isl_bw, isl_loss - ) - for idx, oid, sid, isl_oid, isl_sid, delay in add_lst: - if sat_mid[sid] == machine_id: + _update_link_local(isl_sat, sat_name, delay, isl_bw, isl_loss) + for sat_name, isl_sat, delay, idx in add_lst: + if mid_dict[sat_name] == machine_id: add_cnt += 1 - if sat_mid[isl_sid] == machine_id: + if mid_dict[isl_sat] == machine_id: _add_link_intra_machine( - idx, - _sat_name(shell_id, oid, sid), - _sat_name(shell_id, isl_oid, isl_sid), + idx, sat_name, isl_sat, f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss ) else: _add_link_inter_machine( - idx, - _sat_name(shell_id, oid, sid), - _sat_name(shell_id, isl_oid, isl_sid), - ip_lst[sat_mid[isl_sid]], + idx, sat_name, isl_sat, ip_lst[mid_dict[isl_sat]], f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss ) - elif sat_mid[isl_sid] == machine_id: + elif mid_dict[isl_sat] == machine_id: add_cnt += 1 _add_link_inter_machine( - idx, - _sat_name(shell_id, isl_oid, isl_sid), - _sat_name(shell_id, oid, sid), - ip_lst[sat_mid[sid]], + idx, isl_sat, sat_name, ip_lst[mid_dict[sat_name]], f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss ) print(f"[{machine_id}] Shell {shell_id}:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") - gs_dir = f"{dir}/GS-{len(gs_mid)}" - # return + gs_dir = f"{dir}/GS-{len(gs_mid_dict)}" + return if not os.path.exists(gs_dir): return del_cnt, update_cnt, add_cnt = 0, 0, 0 @@ -404,10 +354,9 @@ def sn_container_check_output(pid, cmd, *args, **kwargs): ) def sn_operate_every_node(dir, func, *args): - for shell_id, (orbit_num, shell_name, sat_mid) in enumerate(sat_mid_lst): - pid_map = _pid_map(dir + '/' + PID_FILENAME) - for name, pid in pid_map.items(): - func(pid, name, *args) + pid_map = _pid_map(dir + '/' + PID_FILENAME) + for name, pid in pid_map.items(): + func(pid, name, *args) def get_IP(dir, node): pid = _pid_map(f"{dir}/{PID_FILENAME}")[node] @@ -634,10 +583,10 @@ def sn_recover(dir, sat_loss): for line in f: damage_set.add(line.strip()) - gs_mid, sat_mid_lst, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) + sat_mid_dict_shell, gs_mid_dict, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) if cmd == 'nodes': sn_clean(workdir) - sn_init_nodes(workdir, gs_mid, sat_mid_lst) + sn_init_nodes(workdir, sat_mid_dict_shell, gs_mid_dict) elif cmd == 'list': print(f"{'NODE':<20} STATE") for name in _pid_map(workdir + '/' + PID_FILENAME): @@ -647,7 +596,7 @@ def sn_recover(dir, sat_loss): # sn_update_network = lp(sn_update_network) # lp.add_function(_update_link_intra_machine) sn_update_network( - workdir, sys.argv[4], sat_mid_lst, gs_mid, ip_lst, + workdir, sys.argv[4], sat_mid_dict_shell, gs_mid_dict, ip_lst, sys.argv[5], sys.argv[6], sys.argv[7], sys.argv[8] ) # with open('report.txt', 'w') as f: diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 6cd5504..b445e24 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -8,10 +8,12 @@ import threading import zipfile import math +import re +import json from starrynet.sn_observer import * from starrynet.sn_utils import * -ASSIGN_FILENAME = 'assign.txt' +ASSIGN_FILENAME = 'assign.json' BIRD_CONF_TEXT = """\ log "/var/log/bird.log" { warning, error, auth, fatal, bug }; @@ -120,13 +122,13 @@ def get_nodes(self): return nodes def init_network(self, isl_bw, isl_loss, gsl_bw, gsl_loss): - for shell in self.shell_lst: - rmt_path = f"{self.dir}/{shell['name']}.zip" + for shell_name, sat_names in self.shell_lst: + rmt_path = f"{self.dir}/{shell_name}.zip" rmt_f = self.sftp.open(rmt_path, "wb") zip_f = zipfile.ZipFile(rmt_f, mode='w') - pattern = os.path.join(self.local_dir, shell['name'], 'isl', '*.txt') + pattern = os.path.join(self.local_dir, shell_name, 'isl', '*.txt') for isl_txt in glob.glob(pattern): - zip_f.write(isl_txt, f"{shell['name']}/{os.path.basename(isl_txt)}") + zip_f.write(isl_txt, f"{shell_name}/{os.path.basename(isl_txt)}") zip_f.close() rmt_f.close() sn_remote_cmd(self.ssh, f"python3 -m zipfile -e {rmt_path} {self.dir}") @@ -268,19 +270,17 @@ def __init__(self, configuration_file_path, GS_lat_long, hello_interval): +'-'+ sn_args.link_style +'-'+ sn_args.link_policy self.gs_dirname = 'GS-' + str(len(self.gs_lat_long)) for shell_id, shell in enumerate(self.shell_lst): - shell['name'] = f"{shell_id}_{shell['altitude']}-{shell['inclination']}"\ - f"-{shell['orbit']}-{shell['sat']}"\ - f"-{shell['phase_shift']}" + shell['name'] = f"shell{shell_id}" self.local_dir = os.path.join(self.configuration_dir, self.experiment_name) self._init_local(hello_interval) # Initiate a necessary delay and position data for emulation - calculate_delay( + sat_names_shell = gen_topo( self.local_dir, self.duration, self.step, self.shell_lst, self.link_style, self.gs_lat_long, self.antenna_number, self.elevation, self.link_policy ) - (self.remote_lst, - self.sat_mid_lst, self.gs_mid) = self._assign_remote(sn_args.machine_lst) + (self.remote_lst, self.sat_mid_dict, + self.gs_mid_dict) = self._assign_remote(sat_names_shell, sn_args.machine_lst) self.events = [] @@ -293,61 +293,63 @@ def _init_local(self, hello_interval): with open(os.path.join(self.local_dir, 'bird.conf'), 'w') as f: f.write(BIRD_CONF_TEXT % (hello_interval, hello_interval, hello_interval)) - def _assign_remote(self, machine_lst): + def _assign_remote(self, sat_names_shell, machine_lst): + assert len(sat_names_shell) == len(self.shell_lst) + # TODO: better partition - remote_lst = [] - if len(self.shell_lst) * 2 <= len(machine_lst): + if len(sat_names_shell) * 2 <= len(machine_lst): # need intra-shell partition - machine_per_shell = len(machine_lst) // len(self.shell_lst) + machine_per_shell = len(machine_lst) // len(sat_names_shell) raise NotImplementedError else: # only divide shell shell_per_machine = len(self.shell_lst) // len(machine_lst) - remainder = len(self.shell_lst) % len(machine_lst) + remainder = len(sat_names_shell) % len(machine_lst) shell_id = 0 - sat_mid_lst = [] + sat_mid_dict_shell = [] assigned_shell_lst = [] for i, remote in enumerate(machine_lst): shell_num = shell_per_machine if i < remainder: shell_num += 1 assigned_shells = [ - self.shell_lst[j] for j in range(shell_id, shell_id + shell_num) + (self.shell_lst[j]['name'], sat_names_shell[j]) + for j in range(shell_id, shell_id + shell_num) ] # all satellites of a shell assigned to a single machine - sat_mid_lst.extend([ - (i,) * shell['sat'] for shell in assigned_shells - ]) + for shell_name, sat_names in assigned_shells: + sat_mid_dict = {} + for sat_name in sat_names: + sat_mid_dict[sat_name] = i + sat_mid_dict_shell.append(sat_mid_dict) assigned_shell_lst.append(assigned_shells) shell_id += shell_num - gs_mid = [] + gs_mid_dict = {} # TODO: better ground station assign with open(os.path.join(self.local_dir, self.gs_dirname,'gsl','0.txt'))as f: for line in f: line = line.strip() if len(line) == 0: continue - init = line.split('|')[3] - if len(init) == 0: - gs_mid.append(0) + toks = line.split('|') + gs_name = toks[0] + add_lst = toks[3] + if len(add_lst) == 0: + gs_mid_dict[gs_name] = 0 continue - gsl = init.split(' ')[0].split(',') - shell_id, sid = int(gsl[1]), int(gsl[3]) - mid = sat_mid_lst[shell_id][sid] - gs_mid.append(mid) + gsl = add_lst.split(' ')[0].split(',') + gs_mid_dict[gs_name] = sat_mid_dict[gsl[0]] + ip_lst = [remote['IP'] for remote in machine_lst] + assign_obj = { + 'sat_mid_shell': sat_mid_dict_shell, + 'gs_mid': gs_mid_dict, + 'ip': ip_lst, + } with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: - f.write(' '.join(str(mid) for mid in gs_mid) + '\n') - # every shell - for sat_mid, shell in zip(sat_mid_lst, self.shell_lst): - f.write( - str(shell['orbit']) + ' ' + shell['name'] + '\n' - + ' '.join(str(mid) for mid in sat_mid) + '\n' - ) - f.write('\n') - for remote in machine_lst: - f.write(remote['IP'] + '\n') + json.dump(assign_obj, f) + remote_lst = [] for i, remote in enumerate(machine_lst): remote_lst.append(RemoteMachine( i, @@ -358,10 +360,10 @@ def _assign_remote(self, machine_lst): assigned_shell_lst[i], self.experiment_name, self.local_dir, - self.gs_dirname if i in gs_mid else None + self.gs_dirname if i in gs_mid_dict.values() else None ) ) - return remote_lst, sat_mid_lst, gs_mid + return remote_lst, sat_mid_dict, gs_mid_dict def create_nodes(self): print('Initializing nodes ...') @@ -371,8 +373,14 @@ def create_nodes(self): print("Node initialization:", time.time() - begin, "s consumed.") self._load_node_map() + def node_map(self): + if hasattr(self, 'nodes'): + return self.nodes + self._load_node_map() + return self.nodes + def _load_node_map(self): - self.node_map = {} + self.nodes = {} self.undamaged_lst = list() self.total_sat_lst = list() for remote in self.remote_lst: @@ -380,10 +388,10 @@ def _load_node_map(self): if node.startswith('Error'): print(node) exit(1) - if node.startswith('SH'): + if not node.startswith('GS'): self.undamaged_lst.append(node) self.total_sat_lst.append(node) - self.node_map[node.strip()] = remote + self.nodes[node.strip()] = remote def create_links(self): print('Initializing links ...') @@ -410,9 +418,10 @@ def run_routing_daemon(self, node_lst='all'): remote.init_routed(['all']) print("Routing daemon initialized. Wait 30s for route converged") else: + node_map = self.node_map() rtd_lsts = {machine:[] for machine in self.remote_lst} for node in node_lst: - rtd_lsts[self.node_map[node]].append(node) + rtd_lsts[node_map[node]].append(node) for remote, nodes in rtd_lsts.items(): if len(nodes) > 0: remote.init_routed(nodes) @@ -426,15 +435,17 @@ def run_routing_daemon(self, node_lst='all'): def get_distance(self, node1, node2, time_index): def _get_xyz(node): if node.startswith('SH'): - shell_id, oid, sid = _sat2idx(node) + match = re.search(r'\d+', node) + shell_id = int(match.group(0))-1 shell = self.shell_lst[shell_id] - lla_mat = load_pos(os.path.join( + lla_dict = load_pos(os.path.join( self.local_dir, shell['name'], 'position', f'{time_index}.txt' )) - return to_cbf(lla_mat[oid][sid]) + lla = lla_dict[node] + return to_cbf(lla_dict[node]) elif node.startswith('GS'): return to_cbf(self.gs_lat_long[_gs2idx(node)]) else: @@ -447,25 +458,23 @@ def _get_xyz(node): def get_neighbors(self, sat, time_index): if not sat.startswith('SH'): raise RuntimeError('Not a satellite') - shell_id, oid, sid = _sat2idx(sat) + match = re.search(r'\d+', sat) + shell_id = int(match.group(0))-1 shell = self.shell_lst[shell_id] - isl_mat = load_isl_state(os.path.join( + isls_dict = load_links_dict(os.path.join( self.local_dir, shell['name'], 'isl', f'{time_index}-state.txt' )) neighbors = [] - for isl in isl_mat[oid][sid]: - isl = isl.split(',') - neighbors.append(_sat_name(shell_id, int(isl[1]), int(isl[2]))) - for orbit, isls_lst in enumerate(isl_mat): - for sat, isls in enumerate(isls_lst): - for isl in isls: - isl = isl.split(',') - if int(isl[1]) == oid and int(isl[2]) == sid: - neighbors.append(_sat_name(shell_id, orbit, sat)) + for isl in isls_dict[sat]: + neighbors.append(isl[0]) + for name, isl_lst in isls_dict.items(): + for isl in isl_lst: + if isl[0] == sat: + neighbors.append(name) return neighbors def get_GSes(self, sat, time_index): @@ -474,43 +483,39 @@ def get_GSes(self, sat, time_index): shell_id, oid, sid = _sat2idx(sat) shell = self.shell_lst[shell_id] - gsl_lst = load_gsl_state(os.path.join( + gsls_dict = load_links_dict(os.path.join( self.local_dir, self.gs_dirname, 'gsl', f'{time_index}-state.txt' )) GSes = [] - for gid, gsls in enumerate(gsl_lst): - for gsl in gsls: - gsl = gsl.split(',') - if int(gsl[1]) == shell_id \ - and int(gsl[2]) == oid \ - and int(gsl[3]) == sid: - GSes.append(_gs_name(gid)) + for gs, gsl_lst in gsls_dict.items(): + for gsl in gsl_lst: + if gsl[0] == sat: + GSes.append(gs) return GSes def get_position(self, node, time_index): if node.startswith('SH'): - shell_id, oid, sid = _sat2idx(node) + match = re.search(r'\d+', node) + shell_id = int(match.group(0))-1 shell = self.shell_lst[shell_id] - - lla_mat = load_pos(os.path.join( + lla_dict = load_pos(os.path.join( self.local_dir, shell['name'], 'position', f'{time_index}.txt' )) - return lla_mat[oid][sid] + return lla_dict[node] elif node.startswith('GS'): return self.gs_lat_long[_gs2idx(node)] else: raise NotImplementedError def get_IP(self, node): - if not hasattr(self, 'node_map'): - self._load_node_map() - return self.node_map[node].get_IP(node) + node_map = self.node_map() + return node_map[node].get_IP(node) # dynamic events def get_utility(self, t): @@ -523,6 +528,7 @@ def _check_utility(real_t): def set_damage(self, damaging_ratio, t): def _damage(real_t, damaging_ratio): + node_map = self.node_map() damage_lsts = {machine:[] for machine in self.remote_lst} cur_num = len(self.undamaged_lst) need_damage_num = min(len(self.total_sat_lst) * damaging_ratio, cur_num) @@ -530,7 +536,7 @@ def _damage(real_t, damaging_ratio): sat = self.undamaged_lst.pop( random.randint(0, len(self.undamaged_lst) - 1) ) - machine = self.node_map[sat] + machine = node_map[sat] damage_lsts[machine].append(sat) for machine, lst in damage_lsts.items(): machine.damage(lst) @@ -545,7 +551,8 @@ def _recovery(real_t): def check_routing_table(self, node, t): def _check_route(real_t, node): - machine = self.node_map[node] + node_map = self.node_map() + machine = node_map[node] machine.check_route( os.path.join(self.local_dir, f'{real_t}-route-{node}.txt'), node @@ -554,13 +561,15 @@ def _check_route(real_t, node): def set_next_hop(self, src, dst, next_hop, t): def _set_next_hop(real_t, src, dst, next_hop): - machine = self.node_map[src] + node_map = self.node_map() + machine = node_map[src] machine.sr(src, dst, next_hop) self.events.append((t, _set_next_hop, src, dst, next_hop)) def set_ping(self, src, dst, t): def _ping(real_t, src, dst): - machine = self.node_map[src] + node_map = self.node_map() + machine = node_map[src] self.ping_threads.append(machine.ping_async( os.path.join(self.local_dir, f'{real_t}-ping-{src}-{dst}.txt'), src, dst @@ -569,7 +578,8 @@ def _ping(real_t, src, dst): def set_iperf(self, src, dst, t): def _iperf(real_t, src, dst): - machine = self.node_map[src] + node_map = self.node_map() + machine = node_map[src] self.iperf_threads.append(machine.iperf_async( os.path.join(self.local_dir, f'{real_t}-iperf-{src}-{dst}.txt'), src, dst diff --git a/starrynet/sn_utils.py b/starrynet/sn_utils.py index 12ec1ad..2746e2d 100644 --- a/starrynet/sn_utils.py +++ b/starrynet/sn_utils.py @@ -17,7 +17,7 @@ def sn_load_file(path): parser.add_argument('--step', type=int, default=table['step (s)']) - parser.add_argument('--duration', type=int, default=table['Duration (s)']) + parser.add_argument('--duration', type=int, default=(table['Duration (s)'] if 'Duration (s)' in table else 0)) parser.add_argument('--sat_bandwidth', type=int, default=table['satellite link bandwidth ("X" Gbps)']) @@ -41,13 +41,15 @@ def sn_load_file(path): # TODO: parser.add_argument('--user_num', type=int, default=0) sn_args = parser.parse_args() sn_args.__setattr__('machine_lst', table['Machines']) - shell_lst = [{ - 'altitude': shell['Altitude (km)'], - 'inclination': shell['Inclination'], - 'phase_shift': shell['Phase shift'], - 'orbit': shell['Orbits'], - 'sat': shell['Satellites per orbit'], - } for shell in table["Shells"]] + shell_lst = table['Shells'] + for shell in shell_lst: + # for compatibility + update_keys = [ + ('Altitude (km)', 'altitude'), ('Inclination', 'inclination'), + ('Phase shift', 'phase_shift'), ('Orbits', 'orbit'), ('Satellites per orbit', 'sat')] + for key, new_key in update_keys: + if key in shell: + shell[new_key] = shell[key] sn_args.__setattr__('shell_lst', shell_lst) return sn_args From c5e1abb385f7bbfb647d6c6253a4a7cbdc7f1ad4 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Mon, 6 Jan 2025 16:16:26 +0800 Subject: [PATCH 09/49] fix: fix some bugs about nodes map --- example.py | 27 ++++++++++----------------- starrynet/sn_synchronizer.py | 20 ++------------------ 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/example.py b/example.py index e21a04c..5be851d 100644 --- a/example.py +++ b/example.py @@ -24,10 +24,8 @@ sn.create_nodes() sn.create_links() - exit(0) - - node1 = 'SH1O2S4' - node2 = 'SH1O2S5' + node1 = 'SH1SAT1' + node2 = 'SH1SAT2' time_index = 2 # LLA of a node at a certain time LLA = sn.get_position(node1, time_index) @@ -42,8 +40,8 @@ print(f'\n{node1} neighbors: {neighbors}') # GS connected to the node at a certain time - GSes = sn.get_GSes('SH1O2S2', time_index) - print(f"\nSH1O2S2 GSes: {GSes}") + GSes = sn.get_GSes(node1, time_index) + print(f"\n{node1} GSes: {GSes}") time_index = 2 sn.get_utility(time_index) # CPU and memory useage @@ -52,9 +50,9 @@ IP_dict = sn.get_IP(node1) print(f'\n{node1} IP addresses: {IP_dict}') - sat = 'SH1O1S1' + sat = 'SH1SAT2' des = 'GS2' - next_hop_sat = 'SH1O1S2' + next_hop_sat = 'SH1SAT1' time_index = 20 # set the next hop at a certain time. Sat and NextHopSat are neighbors. sn.set_next_hop(sat, des, next_hop_sat, time_index) @@ -63,8 +61,8 @@ # routing table of a node at a certain time. The output file will be written at the working directory. sn.check_routing_table(sat, time_index) - node1 = 'SH1O5S6' - node2 = 'SH1O6S6' + node1 = 'SH1SAT1' + node2 = 'SH1SAT2' time_index = 4 # ping msg of two nodes at a certain time. The output file will be written at the working directory. sn.set_ping(node1, node2, time_index) @@ -76,7 +74,7 @@ # run OSPF daemon on selected nodes sn.run_routing_daemon(node_lst=[ - 'GS1', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS2']) + 'GS1', 'SH1SAT1', 'SH1SAT2', 'GS2']) sn.check_routing_table('GS2', 4) @@ -92,13 +90,8 @@ sn.set_recovery(time_index) sn.check_routing_table('GS1', 25) - - for time_index in range(35, 80): - node1 = 'SH1O9S10' - node2 = 'SH1O10S10' - # ping msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_ping(node1, node2, time_index) sn.start_emulation() + if input('clear environment?[y/n]').strip().lower()[:1] == 'y': sn.clean() diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index b445e24..dcd1934 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -56,21 +56,8 @@ } """ -def _sat_name(shell_id, orbit_id, sat_id): - return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' - -def _sat2idx(sat_name): - idx1 = sat_name.find('O') - idx2 = sat_name.find('S', idx1) - shell_id = int(sat_name[2:idx1])-1 - oid, sid = int(sat_name[idx1+1:idx2])-1, int(sat_name[idx2+1:])-1 - return shell_id, oid, sid - def _gs2idx(gs_name): - return int(node[2:])-1 - -def _gs_name(gid): - return f'GS{gid+1}' + return int(gs_name[2:])-1 class RemoteMachine: @@ -480,8 +467,6 @@ def get_neighbors(self, sat, time_index): def get_GSes(self, sat, time_index): if not sat.startswith('SH'): raise RuntimeError('Not a Satellite') - shell_id, oid, sid = _sat2idx(sat) - shell = self.shell_lst[shell_id] gsls_dict = load_links_dict(os.path.join( self.local_dir, @@ -594,8 +579,7 @@ def _event(self, real_t): def start_emulation(self): self.events.sort(key=lambda x:x[0], reverse=True) - if not hasattr(self, 'node_map'): - self._load_node_map() + nodes = self.node_map() self.ping_threads = [] self.iperf_threads = [] From 6a6c08e5543418d0dfddf4b4a0412ba36a7e7b6f Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 14 Feb 2025 20:47:48 +0800 Subject: [PATCH 10/49] feat: address assignment modded and support for ipv6 --- starrynet/sn_orchestrater.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index b2a05ea..95f5a38 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -117,11 +117,12 @@ def _del_link(name1, name2): os.close(fd) subprocess.check_call(('ip', 'link', 'del', n1_n2)) -def _init_if(name, if_name, addr, delay, bw, loss): +def _init_if(name, if_name, addr, addr6, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) + subprocess.check_call(('ip', 'addr', 'add', addr6, 'dev', if_name)) subprocess.check_call( ('tc', 'qdisc', 'add', 'dev', if_name, 'root', 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbit') @@ -149,26 +150,26 @@ def _update_link_local(name1, name2, delay, bw, loss): n1_n2 = f"{name2}" _update_if(name1, n1_n2, delay, bw, loss) -def _add_link_intra_machine(idx, name1, name2, prefix, delay, bw, loss): - n1_n2 = f"{name2}" - n2_n1 = f"{name1}" +def _add_link_intra_machine(idx, name1, name2, prefix4, prefix6, delay, bw, loss): + n1_n2 = name2 + n2_n1 = name1 libc.setns(main_net_fd, CLONE_NEWNET) subprocess.check_call( ('ip', 'link', 'add', n1_n2, 'netns', name1, 'type', 'veth', 'peer', n2_n1, 'netns', name2) ) - _init_if(name1, n1_n2, prefix+'.10/24', delay, bw, loss) - _init_if(name2, n2_n1, prefix+'.40/24', delay, bw, loss) + _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) + _init_if(name2, n2_n1, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) -def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix, delay, bw, loss): - n1_n2 = f"{name2}" - n2_n1 = f"{name1}" +def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, delay, bw, loss): + n1_n2 = name2 + libc.setns(main_net_fd, CLONE_NEWNET) subprocess.check_call( - ('ip', 'link', 'add', n1_n2, 'type', 'vxlan', - 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) + ('ip', 'link', 'add', n1_n2, 'netns', name1, + 'type', 'vxlan', 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) ) - _init_if(name1, n1_n2, prefix+'.10/24', delay, bw, loss) - _init_if(name2, n2_n1, prefix+'.40/24', delay, bw, loss) + suffix6 = '::10/48' if name1 < name2 else '::40/48' + _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + suffix6, delay, bw, loss) def sn_init_nodes(dir, sat_mid_dict_shell, gs_mid_dict): def _load_netns(pid, name): @@ -256,18 +257,21 @@ def sn_update_network( if mid_dict[isl_sat] == machine_id: _add_link_intra_machine( idx, sat_name, isl_sat, - f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss + f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', + delay, isl_bw, isl_loss ) else: _add_link_inter_machine( idx, sat_name, isl_sat, ip_lst[mid_dict[isl_sat]], - f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss + f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', + delay, isl_bw, isl_loss ) elif mid_dict[isl_sat] == machine_id: add_cnt += 1 _add_link_inter_machine( idx, isl_sat, sat_name, ip_lst[mid_dict[sat_name]], - f'10.{idx >> 8}.{idx & 0xFF}', delay, isl_bw, isl_loss + f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', + delay, isl_bw, isl_loss ) print(f"[{machine_id}] Shell {shell_id}:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") From a53a143996d83386fce2e66e3c2ab775d8766b8e Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 28 Feb 2025 12:55:42 +0800 Subject: [PATCH 11/49] feat: online running --- sn.json | 13 + sn.py | 5 + starrynet/sn_controller.py | 210 ++++++++++++++ starrynet/sn_remote.py | 545 +++++++++++++++++++++++++++++++++++++ 4 files changed, 773 insertions(+) create mode 100644 sn.json create mode 100644 sn.py create mode 100644 starrynet/sn_controller.py create mode 100644 starrynet/sn_remote.py diff --git a/sn.json b/sn.json new file mode 100644 index 0000000..43664eb --- /dev/null +++ b/sn.json @@ -0,0 +1,13 @@ +{ + "constellation": "starlink", + "api_url": "http://127.0.0.1/Nodeinfo-2024-08-21-10-43-17.json", + "step": 1, + "machines": [ + { + "IP": "127.0.0.1", + "port": 22, + "username": "root", + "password": "123456" + } + ] +} \ No newline at end of file diff --git a/sn.py b/sn.py new file mode 100644 index 0000000..99b9aeb --- /dev/null +++ b/sn.py @@ -0,0 +1,5 @@ +from starrynet.sn_controller import * + +controller = TopoSync('./sn.json') + +controller.run() diff --git a/starrynet/sn_controller.py b/starrynet/sn_controller.py new file mode 100644 index 0000000..c1558ee --- /dev/null +++ b/starrynet/sn_controller.py @@ -0,0 +1,210 @@ +import time +import threading +import math +import json +import requests + +from starrynet.sn_utils import * + +ASSIGN_FILENAME = 'assign.json' +LINK_FILENAME = 'link.json' + +class Remote: + def __init__(self, id, host, port, username, password): + self.id = id + + self.ssh, self.sftp = sn_connect_remote( + host = host, + port = port, + username = username, + password = password, + ) + + self.dir = sn_remote_cmd(self.ssh, 'echo ~/SN') + sn_remote_cmd(self.ssh, 'mkdir ' + self.dir) + + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'sn_remote.py'), + self.dir + '/sn_remote.py' + ) + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'pyctr.c'), + self.dir + '/pyctr.c' + ) + self.sftp.put( + ASSIGN_FILENAME, + self.dir + '/' + ASSIGN_FILENAME + ) + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/sn_remote.py nodes {self.id} {self.dir}" + ) + + def update_network(self, del_links, add_links, update_links): + self.sftp.put(LINK_FILENAME, self.dir + '/' + LINK_FILENAME) + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/sn_remote.py networks {self.id} {self.dir} " + ) + +class TopoSync(): + + def __init__(self, config_path): + with open(config_path, 'r') as f: + config = json.load(f) + self.constellation = config['constellation'] + self.api_url = config['api_url'] + self.time_step = config['step'] + self.machine_lst = config['machines'] + self.node_init = False + self.link_dict = {} + + def run(self): + self.last_links = set() + last_t = time.time() + while True: + t = time.time() + if t - last_t < self.time_step: + time.sleep(last_t + self.time_step - t) + t = time.time() + + print('Time: ', t, '\n') + last_t = t + res = requests.get(self.api_url) + # + f'?time={t}&constellation={self.constellation}×tep={self.time_step}') + node_info = json.loads(res.text) + new_links, del_links, add_links, update_links = self._parse(node_info) + + with open(LINK_FILENAME, 'w') as f: + json.dump( + {'del_links': del_links, 'add_links': add_links, 'update_links': update_links}, + f + ) + + rmt_threads = [] + for rmt in self.remote_lst: + thread = threading.Thread( + target=rmt.update_network, + args=(del_links, add_links, update_links) + ) + thread.start() + rmt_threads.append(thread) + + self.last_links = new_links + + for thread in rmt_threads: + thread.join() + + def _parse(self, node_info): + EPS = 0.01 + def distance(lla1, lla2): + RADIUS = 6371 + + lat_rad1, lng_rad1 = lla1[0] * math.pi / 180, lla1[1] * math.pi / 180 + lat_rad2, lng_rad2 = lla2[0] * math.pi / 180, lla2[1] * math.pi / 180 + + sa = math.sin((lat_rad1 - lat_rad2)/2) + sb = math.sin((lng_rad1 - lng_rad2)/2) + # FIXME: Altitude + return 2 * RADIUS * math.asin(math.sqrt( + sa * sa + math.cos(lat_rad1) * math.cos(lat_rad2) * sb * sb + )) + + if not self.node_init: + self._init_node(node_info) + + new_links = set() + for isl in node_info['link_ISL']: + src_id, dst_id = isl['src'], isl['dst'] + if src_id == dst_id: + continue + if src_id > dst_id: + src_id, dst_id = dst_id, src_id + new_links.add((src_id, dst_id)) + + for gsl in node_info['link_GSL_Up']: + # GS - SAT + src_id, dst_id = gsl['src'] + self.sat_nr, gsl['dst'] + new_links.add((src_id, dst_id)) + + # assign GS to machine of the first connected SAT + if not self.node_init: + print('Initializing ...') + for gsl in node_info['link_GSL_Up']: + src_id, dst_id = gsl['src'] + self.sat_nr, gsl['dst'] + if self.node_mid[src_id] is not None: + continue + self.node_mid[src_id] = self.node_mid[dst_id] + + with open(ASSIGN_FILENAME, 'w') as f: + json.dump( + { + 'node_name':self.node_name, + 'node_mid': self.node_mid, + 'ip': [machine['IP'] for machine in self.machine_lst], + }, + f + ) + + self.remote_lst = [] + for mid, machine in enumerate(self.machine_lst): + self.remote_lst.append(Remote( + mid, + machine['IP'], + machine['port'], + machine['username'], + machine['password'], + )) + self.node_init = True + + del_set = self.last_links.difference(new_links) + add_set = new_links.difference(self.last_links) + remain_set = new_links.intersection(self.last_links) + + del_links = [de for de in del_set] + add_links = [] + update_links = [] + for add in add_set: + delay_ms = (distance(self.node_lla[add[0]], self.node_lla[add[1]]) + / 299.792458) + if add in self.link_dict: + idx = self.link_dict[add][0] + else: + idx = len(self.link_dict) + 1 + self.link_dict[add] = [idx, delay_ms] + add_links.append((add[0], add[1], delay_ms, idx)) + + for remain in remain_set: + delay_ms = (distance(self.node_lla[remain[0]], self.node_lla[remain[1]]) + / 299.792458) + cur_delay = self.link_dict[remain][1] + if abs(delay_ms - cur_delay) <= EPS: + continue + update_links.append((remain[0], remain[1], delay_ms)) + + return new_links, del_links, add_links, update_links + + def _init_node(self, node_info): + self.node_name = [] + self.node_lla = [] + self.node_mid = [None] * (len(node_info['sat']) + len(node_info['ground'])) + + sat_per_machine = (len(node_info['sat']) + len(self.machine_lst) - 1) // len(self.machine_lst) + + for idx, sat in enumerate(node_info['sat']): + if idx != sat['id']: + raise RuntimeError("'id' of sat is not incremented") + self.node_name.append(f'SAT{idx}') + self.node_lla.append((float(sat['lat']), float(sat['lon']), float(sat['alt']))) + + for i in range(len(self.machine_lst)): + for j in range(i * sat_per_machine, min((i+1) * sat_per_machine, len(self.node_name))): + self.node_mid[j] = i + + self.sat_nr = len(self.node_name) + + for idx, gs in enumerate(node_info['ground']): + if idx != gs['id']: + raise RuntimeError("'id' of gs is not incremented") + self.node_name.append(f'GS{idx}') + self.node_lla.append((float(gs['lat']), float(gs['lon']), float(gs['alt']))) diff --git a/starrynet/sn_remote.py b/starrynet/sn_remote.py new file mode 100644 index 0000000..24c411b --- /dev/null +++ b/starrynet/sn_remote.py @@ -0,0 +1,545 @@ +#!/usr/bin/python3 +import os +import subprocess +import sys +import json +import glob +import ctypes + +ASSIGN_FILENAME = 'assign.json' +LINK_FILENAME = 'link.json' +PID_FILENAME = 'container_pid.txt' +DAMAGE_FILENAME = 'damage_list.txt' + +NOT_ASSIGNED = 'NA' +VXLAN_PORT = 4789 +# FIXME +CLONE_NEWNET = 0x40000000 +libc = ctypes.CDLL(None) +main_net_fd = os.open('/proc/self/ns/net', os.O_RDONLY) + +def _sat_name(shell_id, orbit_id, sat_id): + return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' + +def _gs_name(gid): + return f'GS{gid+1}' + +def _pid_map(pid_path, pop = False): + global _pid_map_cache + if _pid_map_cache is None: + _pid_map_cache = {} + if not os.path.exists(pid_path): + print('Error: container index file not found, please create nodes') + exit(1) + with open(pid_path, 'r') as f: + for line in f: + if len(line) == 0 or line.isspace(): + continue + for name_pid in line.strip().split(): + if name_pid == NOT_ASSIGNED: + continue + name_pid = name_pid.split(':') + _pid_map_cache[name_pid[0]] = name_pid[1] + if pop: + ret = _pid_map_cache + _pid_map_cache = None + return ret + return _pid_map_cache + +def _get_params(path): + with open(path, 'r') as f: + obj = json.load(f) + node_name = obj['node_name'] + node_mid = obj['node_mid'] + ip_lst = obj['ip'] + return node_name, node_mid, ip_lst + +def _parse_isls(path): + del_lst, update_lst, add_lst = [], [], [] + f = open(path, 'r') + for line in f: + toks = line.strip().split('|') + sat_name = toks[0] + if len(toks[1]) > 0: + for isl_sat in toks[1].split(' '): + del_lst.append((sat_name, isl_sat)) + if len(toks[2]) > 0: + for isl_sat in toks[2].split(' '): + sat_delay = isl_sat.split(',') + update_lst.append((sat_name, sat_delay[0], sat_delay[1])) + if len(toks[3]) > 0: + for isl_sat in toks[3].split(' '): + idx_sat_delay = isl_sat.split(',') + add_lst.append((sat_name, idx_sat_delay[0], idx_sat_delay[1], int(idx_sat_delay[2]))) + f.close() + return del_lst, update_lst, add_lst + +def _parse_gsls(path): + del_lst, update_lst, add_lst = [], [], [] + f = open(path, 'r') + for gid, line in enumerate(f): + if len(line) == 0 or line.isspace(): + continue + toks = line.strip().split('|') + if len(toks[1]) > 0: + for isl in toks[1].split(' '): + i_s_o_s_d = isl.split(',') + idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) + oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) + del_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + if len(toks[2]) > 0: + for isl in toks[2].split(' '): + i_s_o_s_d = isl.split(',') + idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) + oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) + update_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + if len(toks[3]) > 0: + for isl in toks[3].split(' '): + i_s_o_s_d = isl.split(',') + idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) + oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) + add_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + f.close() + return del_lst, update_lst, add_lst + +# name1 in local machine +def _del_link(name1, name2): + n1_n2 = f"{name2}" + fd = os.open('/run/netns/' + name1, os.O_RDONLY) + libc.setns(fd, CLONE_NEWNET) + os.close(fd) + subprocess.check_call(('ip', 'link', 'del', n1_n2)) + +def _init_if(name, if_name, addr, addr6, delay, bw, loss): + fd = os.open('/run/netns/' + name, os.O_RDONLY) + libc.setns(fd, CLONE_NEWNET) + os.close(fd) + subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) + subprocess.check_call(('ip', 'addr', 'add', addr6, 'dev', if_name)) + subprocess.check_call( + ('tc', 'qdisc', 'add', 'dev', if_name, 'root', + 'netem', 'delay', str(delay) + 'ms', 'loss', loss+'%', 'rate', bw+'Gbit') + ) + subprocess.check_call(('ip', 'link', 'set', if_name, 'up')) + +def _update_if(name, if_name, delay, bw, loss): + fd = os.open('/run/netns/' + name, os.O_RDONLY) + libc.setns(fd, CLONE_NEWNET) + os.close(fd) + update_loss = '100' if name in damage_set else loss + subprocess.check_call( + ('tc', 'qdisc', 'change', 'dev', if_name, 'root', + 'netem', 'delay', str(delay) + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') + ) + +def _update_link_intra_machine(name1, name2, delay, bw, loss): + n1_n2 = f"{name2}" + n2_n1 = f"{name1}" + _update_if(name1, n1_n2, delay, bw, loss) + _update_if(name2, n2_n1, delay, bw, loss) + +# name1 in local machine +def _update_link_local(name1, name2, delay, bw, loss): + n1_n2 = f"{name2}" + _update_if(name1, n1_n2, delay, bw, loss) + +def _add_link_intra_machine(idx, name1, name2, prefix4, prefix6, delay, bw, loss): + n1_n2 = name2 + n2_n1 = name1 + libc.setns(main_net_fd, CLONE_NEWNET) + subprocess.check_call( + ('ip', 'link', 'add', n1_n2, 'netns', name1, + 'type', 'veth', 'peer', n2_n1, 'netns', name2) + ) + _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) + _init_if(name2, n2_n1, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) + +def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, delay, bw, loss): + n1_n2 = name2 + libc.setns(main_net_fd, CLONE_NEWNET) + subprocess.check_call( + ('ip', 'link', 'add', n1_n2, 'netns', name1, + 'type', 'vxlan', 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) + ) + suffix6 = '::10/48' if name1 < name2 else '::40/48' + _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + suffix6, delay, bw, loss) + +def sn_init_nodes(dir, node_name, node_mid): + def _load_netns(pid, name): + netns_link = f'/run/netns/{name}' + if os.path.islink(netns_link): + os.remove(netns_link) + subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) + sn_container_check_call( + pid, + ('sysctl', 'net.ipv6.conf.all.forwarding=1'), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + sn_container_check_call( + pid, + ('sysctl', 'net.ipv4.conf.all.forwarding=1'), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) + + pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') + node_cnt = 0 + for name, mid in zip(node_name, node_mid): + if mid != machine_id: + pid_file.write(NOT_ASSIGNED + ' ') + continue + node_dir = f"{dir}/overlay/{name}" + os.makedirs(node_dir, exist_ok=True) + ctr_pid = pyctr.container_run(node_dir, name) + pid_file.write(name+':'+str(ctr_pid)+' ') + node_cnt += 1 + pid_file.write('\n') + print(f'[{machine_id}]: {node_cnt} nodes initialized') + + pid_file.close() + os.makedirs('/run/netns', exist_ok=True) + sn_operate_every_node(dir, _load_netns) + +def sn_update_network( + dir, node_name, node_mid, ip_lst, + isl_bw, isl_loss, gsl_bw, gsl_loss + ): + with open(dir + '/' + LINK_FILENAME, 'r') as f: + obj = json.load(f) + del_links, add_links, update_links = obj['del_links'], obj['add_links'], obj['update_links'] + + del_cnt, update_cnt, add_cnt = 0, 0, 0 + for de in del_links: + if node_mid[de[0]] == machine_id: + del_cnt += 1 + _del_link(node_name[de[0]], node_name[de[1]]) + elif node_mid[de[1]] == machine_id: + del_cnt += 1 + _del_link(node_name[de[1]], node_name[de[0]]) + + for add in add_links: + node1, node2, delay, idx = add[0], add[1], add[2], add[3] + prefix4 = f'10.{idx >> 8}.{idx & 0xFF}' + prefix6 = f'2001:{idx >> 8}:{idx & 0xFF}' + if node_mid[node1] == machine_id: + add_cnt += 1 + if node_mid[node2] == machine_id: + _add_link_intra_machine( + idx, node_name[node1], node_name[node2], + prefix4, prefix6, delay, isl_bw, isl_loss + ) + else: + _add_link_inter_machine( + idx, node_name[node1], node_name[node2], ip_lst[node_mid[node2]], + prefix4, prefix6, delay, isl_bw, isl_loss + ) + elif node_mid[node2] == machine_id: + add_cnt += 1 + _add_link_inter_machine( + idx, node_name[node2], node_name[node1], ip_lst[node_mid[node1]], + prefix4, prefix6, delay, isl_bw, isl_loss + ) + + for update in update_links: + node1, node2, delay = update[0], update[1], update[2] + if node_mid[node1] == machine_id: + _update_if(node_name[node1], node_name[node2], delay, isl_bw, isl_loss) + if node_mid[node2] == machine_id: + _update_if(node_name[node2], node_name[node1], delay, isl_bw, isl_loss) + + print(f"[{machine_id}]", + f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") + +def sn_container_check_call(pid, cmd, *args, **kwargs): + subprocess.check_call( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + *args, **kwargs + ) + +def sn_container_run(pid, cmd, *args, **kwargs): + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + *args, **kwargs + ) + +def sn_container_check_output(pid, cmd, *args, **kwargs): + return subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + *args, **kwargs + ) + +def sn_operate_every_node(dir, func, *args): + pid_map = _pid_map(dir + '/' + PID_FILENAME) + for name, pid in pid_map.items(): + func(pid, name, *args) + +def get_IP(dir, node): + pid = _pid_map(f"{dir}/{PID_FILENAME}")[node] + addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in addr_lst: + dev_state_addrs = dev_state_addrs.split() + if len(dev_state_addrs) < 3: + continue + print(dev_state_addrs[0].split('@')[0], dev_state_addrs[2]) + +def sn_init_route_daemons(dir, conf_path, nodes): + def _init_route_daemon(pid, name): + bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' + sn_container_run(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) + if nodes == 'all': + sn_operate_every_node(dir, _init_route_daemon) + else: + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + nodes_lst = nodes.split(',') + for node in nodes_lst: + _init_route_daemon(pid_map[node], node) + +def sn_ping(dir, src, dst): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + # suppose src in this machine + src_pid = pid_map[src] + # TODO: dst in other machine + dst_pid = pid_map[dst] + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + if dev_state_addrs[0].split('@')[0] == src: + break + dst_addr = dst_addr[:dst_addr.rfind('/')] + print('ping', src, dst_addr) + + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, + 'ping', '-c', '4', '-i', '0.01', dst_addr), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + +def sn_iperf(dir, src, dst): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + # suppose src in this machine + src_pid = pid_map[src] + # TODO: dst in other machine + dst_pid = pid_map[dst] + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + if dev_state_addrs[0].split('@')[0] == src: + break + dst_addr = dst_addr[:dst_addr.rfind('/')] + + server = subprocess.Popen( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'iperf3', '-s'), + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, + 'iperf3', '-c', dst_addr, '-t5'), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + server.terminate() + +def sn_sr(dir, src, dst, nxt): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + # suppose src in this machine + src_pid = pid_map[src] + # TODO: dst in other machine + dst_pid = pid_map[dst] + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + dst_prefix = dst_addr[:dst_addr.rfind('.')] + '.0/24' + subprocess.run( + ('nsenter', '-n', '-t', src_pid, + 'ip', 'route', 'add', dst_prefix, 'dev', nxt), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + +def sn_check_route(dir, node): + pid_map = _pid_map(f"{dir}/{PID_FILENAME}") + subprocess.run( + ('nsenter', '-n', '-t', pid_map[node], + 'route'), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + +def sn_clean(dir): + damage_file = f"{dir}/{DAMAGE_FILENAME}" + if os.path.exists(damage_file): + os.remove(damage_file) + for ns_link in glob.glob(f"/run/netns/SH*O*S*"): + if os.path.islink(ns_link): + os.remove(ns_link) + for ns_link in glob.glob(f"/run/netns/G*"): + if os.path.islink(ns_link): + os.remove(ns_link) + pid_file = f"{dir}/{PID_FILENAME}" + if not os.path.exists(pid_file): + return + pid_map = _pid_map(pid_file, True) + for pid in pid_map.values(): + if pid == NOT_ASSIGNED: + continue + try: + os.kill(int(pid), 9) + except ProcessLookupError: + pass + os.remove(pid_file) + +def _change_sat_link_loss(pid, loss): + out = subprocess.check_output( + ('nsenter', '-t', pid, '-n', + 'tc', 'qdisc', 'show')).decode() + for line in out.splitlines(): + line = line.strip() + if len(line) == 0 or line.startswith('lo'): + continue + qdisc_netem_hd_dev_name_ = line.split() + dev_name = qdisc_netem_hd_dev_name_[4] + delay = qdisc_netem_hd_dev_name_[qdisc_netem_hd_dev_name_.index('delay') + 1] + subprocess.check_call( + ('nsenter', '-t', pid, '-n', + 'tc', 'qdisc', 'change', 'dev', dev_name, 'root', + 'netem', 'delay', delay, 'loss', loss+'%')) + +def sn_damage(dir, random_list): + with open(f"{dir}/{DAMAGE_FILENAME}", 'a') as f: + for node in random_list: + pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") + pid = pid_mat[node] + _change_sat_link_loss(pid, '100') + f.write(node + '\n') + print(f'[{machine_id}] damage node: {node}') + +def sn_recover(dir, sat_loss): + damage_file = f"{dir}/{DAMAGE_FILENAME}" + if not os.path.exists(damage_file): + return + with open(f"{dir}/{DAMAGE_FILENAME}", 'r') as f: + for node in f: + pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") + pid = pid_mat[node.strip()] + _change_sat_link_loss(pid, sat_loss) + print(f'[{machine_id}] recover sat: {node}') + os.remove(damage_file) + +if __name__ == '__main__': + _pid_map_cache = None + + if len(sys.argv) < 2: + print('Usage: sn_orchestrater.py ...') + exit(1) + cmd = sys.argv[1] + if cmd == 'exec': + pid_map = _pid_map(os.path.dirname(__file__) + '/' + PID_FILENAME) + if len(sys.argv) < 4: + print('Usage: sn_orchestrater.py exec ...') + exit(1) + if sys.argv[2] not in pid_map: + print('Error:', sys.argv[3], 'not found') + exit(1) + exit(subprocess.run( + ('nsenter', '-a', '-t', pid_map[sys.argv[2]], + *sys.argv[3:]) + ).returncode) + + if len(sys.argv) < 3: + machine_id = None + else: + try: + machine_id = int(sys.argv[2]) + except: + machine_id = None + if len(sys.argv) < 4: + workdir = os.path.dirname(__file__) + else: + workdir = sys.argv[3] + + # C module + try: + import pyctr + except ModuleNotFoundError: + subprocess.check_call( + "cd " + workdir + " && " + "gcc $(python3-config --cflags --ldflags)" + "-shared -fPIC -O2 pyctr.c -o pyctr.so", + shell=True + ) + import pyctr + + damage_set = set() + damage_file = workdir + '/' + DAMAGE_FILENAME + if os.path.exists(damage_file): + with open(workdir + '/' + DAMAGE_FILENAME, 'r') as f: + for line in f: + damage_set.add(line.strip()) + + node_name, node_mid, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) + if cmd == 'nodes': + sn_clean(workdir) + sn_init_nodes(workdir, node_name, node_mid) + elif cmd == 'list': + print(f"{'NODE':<20} STATE") + for name in _pid_map(workdir + '/' + PID_FILENAME): + print(f"{name:<20} {'Damaged' if name in damage_set else 'OK'}") + elif cmd == 'networks': + # lp = LineProfiler() + # sn_update_network = lp(sn_update_network) + # lp.add_function(_update_link_intra_machine) + sn_update_network( + workdir, node_name, node_mid, ip_lst, + '1', '0', '1', '0' + ) + # with open('report.txt', 'w') as f: + # lp.print_stats(f) + elif cmd == 'routed': + sn_init_route_daemons(workdir, workdir + '/bird.conf', sys.argv[4]) + elif cmd == 'IP': + get_IP(workdir, sys.argv[4]) + elif cmd == 'damage': + sn_damage(workdir, sys.argv[4].split(',')) + elif cmd == 'recovery': + sn_recover(workdir, sys.argv[4]) + elif cmd == 'clean': + sn_clean(workdir) + elif cmd == 'ping': + sn_ping(workdir, sys.argv[4], sys.argv[5]) + elif cmd == 'iperf': + sn_iperf(workdir, sys.argv[4], sys.argv[5]) + elif cmd == 'sr': + sn_sr(workdir, sys.argv[4], sys.argv[5], sys.argv[6]) + elif cmd == 'rtable': + sn_check_route(workdir, sys.argv[4]) + else: + print('Unknown command') + os.close(main_net_fd) From 6330d0604dc3b0aff7b456b3feab703bcc3eac25 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 5 Mar 2025 13:11:05 +0800 Subject: [PATCH 12/49] feat: HTTP cmd API and random node assignment --- sn.py | 27 +++++++++++++ starrynet/sn_controller.py | 78 ++++++++++++++++++++++++++++++-------- starrynet/sn_remote.py | 70 ++-------------------------------- 3 files changed, 93 insertions(+), 82 deletions(-) diff --git a/sn.py b/sn.py index 99b9aeb..1ccf240 100644 --- a/sn.py +++ b/sn.py @@ -1,5 +1,32 @@ +import threading + from starrynet.sn_controller import * +### Warning: Never use this in public network! + +from flask import Flask, request, Response, stream_with_context, jsonify + controller = TopoSync('./sn.json') +app = Flask(__name__) + +@app.route('/execute', methods=['POST']) +def execute_command(): + data = request.json + + if 'command' not in data: + return jsonify({'error': 'No command provided'}), 400 + if 'node' not in data: + return jsonify({'error': 'Node not specified'}), 400 + return Response( + stream_with_context(controller.exec(data['node'], data['command'])), + content_type='text/plain' + ) + +api_thread = threading.Thread( + target=app.run, + args=('0.0.0.0', 5000) +) +api_thread.start() + controller.run() diff --git a/starrynet/sn_controller.py b/starrynet/sn_controller.py index c1558ee..ae5d730 100644 --- a/starrynet/sn_controller.py +++ b/starrynet/sn_controller.py @@ -1,8 +1,12 @@ import time +import datetime import threading import math import json import requests +import subprocess +import gzip +import io from starrynet.sn_utils import * @@ -23,13 +27,17 @@ def __init__(self, id, host, port, username, password): self.dir = sn_remote_cmd(self.ssh, 'echo ~/SN') sn_remote_cmd(self.ssh, 'mkdir ' + self.dir) + # self.sftp.put( + # os.path.join(os.path.dirname(__file__), 'sn_remote.py'), + # self.dir + '/sn_remote.py' + # ) + # self.sftp.put( + # os.path.join(os.path.dirname(__file__), 'pyctr.c'), + # self.dir + '/pyctr.c' + # ) self.sftp.put( - os.path.join(os.path.dirname(__file__), 'sn_remote.py'), - self.dir + '/sn_remote.py' - ) - self.sftp.put( - os.path.join(os.path.dirname(__file__), 'pyctr.c'), - self.dir + '/pyctr.c' + os.path.join(os.path.dirname(__file__), 'pyctr.so'), + self.dir + '/pyctr.so' ) self.sftp.put( ASSIGN_FILENAME, @@ -41,11 +49,31 @@ def __init__(self, id, host, port, username, password): ) def update_network(self, del_links, add_links, update_links): + t1 = time.perf_counter() self.sftp.put(LINK_FILENAME, self.dir + '/' + LINK_FILENAME) + t2 = time.perf_counter() sn_remote_wait_output( self.ssh, f"python3 {self.dir}/sn_remote.py networks {self.id} {self.dir} " ) + t3 = time.perf_counter() + print(f'[{self.id}]', t1, t2, t3) + + def exec(self, node, cmd): + def generate(all_cmd): + try: + stdin, stdout, stderr = self.ssh.exec_command(all_cmd) + # 实时读取命令输出 + for line in stdout: + yield line + + for line in stderr: + yield line + + except Exception as e: + yield f"error: {str(e)}\n" + return generate(f'cd {self.dir} && python3 sn_remote.py exec {node} {cmd}') + class TopoSync(): @@ -70,9 +98,16 @@ def run(self): print('Time: ', t, '\n') last_t = t - res = requests.get(self.api_url) - # + f'?time={t}&constellation={self.constellation}×tep={self.time_step}') - node_info = json.loads(res.text) + url = (self.api_url + + f'?startTime={datetime.datetime.fromtimestamp(t).isoformat()}' + + f'&constellation={self.constellation}') + print(url) + res = requests.get(url) + if res.status_code != 200: + print(f'<{res.status_code}> Failed to fetch nodeInfo, skip.') + continue + with gzip.GzipFile(fileobj=io.BytesIO(res.content), mode='rb') as f: + node_info = json.load(f) new_links, del_links, add_links, update_links = self._parse(node_info) with open(LINK_FILENAME, 'w') as f: @@ -136,6 +171,10 @@ def distance(lla1, lla2): continue self.node_mid[src_id] = self.node_mid[dst_id] + for j in range(self.sat_nr, len(self.node_mid)): + if self.node_mid[j] is None: + self.node_mid[j] = random.randint(0, len(self.machine_lst)-1) + with open(ASSIGN_FILENAME, 'w') as f: json.dump( { @@ -145,8 +184,16 @@ def distance(lla1, lla2): }, f ) + + pyctr_dir = os.path.dirname(__file__) + subprocess.check_call( + "cd " + pyctr_dir + " && " + "gcc $(python3-config --cflags --ldflags)" + "-shared -fPIC -O2 pyctr.c -o pyctr.so", + shell=True + ) - self.remote_lst = [] + self.remote_lst = [] for mid, machine in enumerate(self.machine_lst): self.remote_lst.append(Remote( mid, @@ -188,8 +235,6 @@ def _init_node(self, node_info): self.node_name = [] self.node_lla = [] self.node_mid = [None] * (len(node_info['sat']) + len(node_info['ground'])) - - sat_per_machine = (len(node_info['sat']) + len(self.machine_lst) - 1) // len(self.machine_lst) for idx, sat in enumerate(node_info['sat']): if idx != sat['id']: @@ -197,9 +242,8 @@ def _init_node(self, node_info): self.node_name.append(f'SAT{idx}') self.node_lla.append((float(sat['lat']), float(sat['lon']), float(sat['alt']))) - for i in range(len(self.machine_lst)): - for j in range(i * sat_per_machine, min((i+1) * sat_per_machine, len(self.node_name))): - self.node_mid[j] = i + for i in range(len(self.node_name)): + self.node_mid[i] = random.randint(0, len(self.machine_lst)-1) self.sat_nr = len(self.node_name) @@ -208,3 +252,7 @@ def _init_node(self, node_info): raise RuntimeError("'id' of gs is not incremented") self.node_name.append(f'GS{idx}') self.node_lla.append((float(gs['lat']), float(gs['lon']), float(gs['alt']))) + + def exec(self, node, cmd): + idx = self.node_name.index(node) + return self.remote_lst[self.node_mid[idx]].exec(node, cmd) diff --git a/starrynet/sn_remote.py b/starrynet/sn_remote.py index 24c411b..52f4bd5 100644 --- a/starrynet/sn_remote.py +++ b/starrynet/sn_remote.py @@ -12,18 +12,12 @@ DAMAGE_FILENAME = 'damage_list.txt' NOT_ASSIGNED = 'NA' -VXLAN_PORT = 4789 +VXLAN_PORT = '4789' # FIXME CLONE_NEWNET = 0x40000000 libc = ctypes.CDLL(None) main_net_fd = os.open('/proc/self/ns/net', os.O_RDONLY) -def _sat_name(shell_id, orbit_id, sat_id): - return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' - -def _gs_name(gid): - return f'GS{gid+1}' - def _pid_map(pid_path, pop = False): global _pid_map_cache if _pid_map_cache is None: @@ -54,54 +48,6 @@ def _get_params(path): ip_lst = obj['ip'] return node_name, node_mid, ip_lst -def _parse_isls(path): - del_lst, update_lst, add_lst = [], [], [] - f = open(path, 'r') - for line in f: - toks = line.strip().split('|') - sat_name = toks[0] - if len(toks[1]) > 0: - for isl_sat in toks[1].split(' '): - del_lst.append((sat_name, isl_sat)) - if len(toks[2]) > 0: - for isl_sat in toks[2].split(' '): - sat_delay = isl_sat.split(',') - update_lst.append((sat_name, sat_delay[0], sat_delay[1])) - if len(toks[3]) > 0: - for isl_sat in toks[3].split(' '): - idx_sat_delay = isl_sat.split(',') - add_lst.append((sat_name, idx_sat_delay[0], idx_sat_delay[1], int(idx_sat_delay[2]))) - f.close() - return del_lst, update_lst, add_lst - -def _parse_gsls(path): - del_lst, update_lst, add_lst = [], [], [] - f = open(path, 'r') - for gid, line in enumerate(f): - if len(line) == 0 or line.isspace(): - continue - toks = line.strip().split('|') - if len(toks[1]) > 0: - for isl in toks[1].split(' '): - i_s_o_s_d = isl.split(',') - idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) - oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) - del_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) - if len(toks[2]) > 0: - for isl in toks[2].split(' '): - i_s_o_s_d = isl.split(',') - idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) - oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) - update_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) - if len(toks[3]) > 0: - for isl in toks[3].split(' '): - i_s_o_s_d = isl.split(',') - idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) - oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) - add_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) - f.close() - return del_lst, update_lst, add_lst - # name1 in local machine def _del_link(name1, name2): n1_n2 = f"{name2}" @@ -132,17 +78,6 @@ def _update_if(name, if_name, delay, bw, loss): 'netem', 'delay', str(delay) + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') ) -def _update_link_intra_machine(name1, name2, delay, bw, loss): - n1_n2 = f"{name2}" - n2_n1 = f"{name1}" - _update_if(name1, n1_n2, delay, bw, loss) - _update_if(name2, n2_n1, delay, bw, loss) - -# name1 in local machine -def _update_link_local(name1, name2, delay, bw, loss): - n1_n2 = f"{name2}" - _update_if(name1, n1_n2, delay, bw, loss) - def _add_link_intra_machine(idx, name1, name2, prefix4, prefix6, delay, bw, loss): n1_n2 = name2 n2_n1 = name1 @@ -515,7 +450,8 @@ def sn_recover(dir, sat_loss): elif cmd == 'networks': # lp = LineProfiler() # sn_update_network = lp(sn_update_network) - # lp.add_function(_update_link_intra_machine) + # lp.add_function(_add_link_intra_machine) + # lp.add_function(_add_link_inter_machine) sn_update_network( workdir, node_name, node_mid, ip_lst, '1', '0', '1', '0' From ed383d38dbf3e33d0f1d495d70230c6d5780d7c7 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Tue, 8 Apr 2025 18:10:14 +0800 Subject: [PATCH 13/49] fix: GSL in orchestration --- example.py | 47 +++++---- starrynet/sn_observer.py | 2 +- starrynet/sn_orchestrater.py | 187 ++++++++++++----------------------- starrynet/sn_synchronizer.py | 24 ++--- 4 files changed, 95 insertions(+), 165 deletions(-) diff --git a/example.py b/example.py index 5be851d..6fb7efa 100644 --- a/example.py +++ b/example.py @@ -15,7 +15,7 @@ GS_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817] ] # latitude and longitude of frankfurt and Austria - configuration_file_path = "./manual.json" + configuration_file_path = "./config.json" hello_interval = 5 # hello_interval(s) in OSPF. 1-200 are supported. print('Start StarryNet.') @@ -24,8 +24,8 @@ sn.create_nodes() sn.create_links() - node1 = 'SH1SAT1' - node2 = 'SH1SAT2' + node1 = 'SH1O1S1' + node2 = 'SH1O1S2' time_index = 2 # LLA of a node at a certain time LLA = sn.get_position(node1, time_index) @@ -43,53 +43,52 @@ GSes = sn.get_GSes(node1, time_index) print(f"\n{node1} GSes: {GSes}") - time_index = 2 + time_index = 1 sn.get_utility(time_index) # CPU and memory useage # IP dict of a node IP_dict = sn.get_IP(node1) print(f'\n{node1} IP addresses: {IP_dict}') - sat = 'SH1SAT2' - des = 'GS2' - next_hop_sat = 'SH1SAT1' - time_index = 20 + # run OSPF daemon on all nodes + # sn.run_routing_daemon() + + # run OSPF daemon on selected nodes + # sn.run_routing_daemon(node_lst=['GS1', 'SH1O2S2', 'SH1O2S3', 'SH1O3S3', 'GS1']) + + sat = 'SH1O1S1' + des = 'SH1O1S3' + next_hop = 'SH1O1S2' + time_index = 2 # set the next hop at a certain time. Sat and NextHopSat are neighbors. - sn.set_next_hop(sat, des, next_hop_sat, time_index) + sn.set_next_hop(sat, des, next_hop, time_index) - time_index = 22 + time_index = 3 # routing table of a node at a certain time. The output file will be written at the working directory. sn.check_routing_table(sat, time_index) - node1 = 'SH1SAT1' - node2 = 'SH1SAT2' + node1 = 'SH1O1S1' + node2 = 'SH1O1S2' time_index = 4 # ping msg of two nodes at a certain time. The output file will be written at the working directory. sn.set_ping(node1, node2, time_index) # perf msg of two nodes at a certain time. The output file will be written at the working directory. sn.set_iperf(node1, node2, time_index) - # run OSPF daemon on all nodes - # sn.run_routing_daemon() - - # run OSPF daemon on selected nodes - sn.run_routing_daemon(node_lst=[ - 'GS1', 'SH1SAT1', 'SH1SAT2', 'GS2']) - - sn.check_routing_table('GS2', 4) + sn.check_routing_table('GS1', 10) ratio = 0.3 - time_index = 5 + time_index = 15 # random damage of a given ratio at a certain time sn.set_damage(ratio, time_index) - sn.check_routing_table('GS1', 10) + sn.check_routing_table('GS1', 20) - time_index = 15 + time_index = 25 # recover the damages at a certain time sn.set_recovery(time_index) - sn.check_routing_table('GS1', 25) + sn.check_routing_table('GS1', 30) sn.start_emulation() diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index e336a18..7068ebb 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -273,7 +273,7 @@ def _write_link_files(dir, topo_t_shell, gsls_t, GS_lat_long): f_state.close() f_update.close() # GSL - gsl_dir = os.path.join(dir, 'GS-' + str(len(GS_lat_long)), 'gsl') + gsl_dir = os.path.join(dir, 'GS', 'gsl') os.makedirs(gsl_dir, exist_ok=True) for file in glob.glob(os.path.join(gsl_dir, '*.txt')): os.remove(file) diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 95f5a38..eb59ac4 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -25,12 +25,6 @@ libc = ctypes.CDLL(None) main_net_fd = os.open('/proc/self/ns/net', os.O_RDONLY) -def _sat_name(shell_id, orbit_id, sat_id): - return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' - -def _gs_name(gid): - return f'GS{gid+1}' - def _pid_map(pid_path, pop = False): global _pid_map_cache if _pid_map_cache is None: @@ -56,56 +50,28 @@ def _pid_map(pid_path, pop = False): def _get_params(path): with open(path, 'r') as f: obj = json.load(f) - sat_mid_dict_shell = obj['sat_mid_shell'] - gs_mid_dict = obj['gs_mid'] + shell_num = obj['shell_num'] + node_mid_dict = obj['node_mid_dict'] ip_lst = obj['ip'] - return sat_mid_dict_shell, gs_mid_dict, ip_lst + return shell_num, node_mid_dict, ip_lst -def _parse_isls(path): +def _parse_links(path): del_lst, update_lst, add_lst = [], [], [] f = open(path, 'r') for line in f: toks = line.strip().split('|') - sat_name = toks[0] - if len(toks[1]) > 0: - for isl_sat in toks[1].split(' '): - del_lst.append((sat_name, isl_sat)) - if len(toks[2]) > 0: - for isl_sat in toks[2].split(' '): - sat_delay = isl_sat.split(',') - update_lst.append((sat_name, sat_delay[0], sat_delay[1])) - if len(toks[3]) > 0: - for isl_sat in toks[3].split(' '): - idx_sat_delay = isl_sat.split(',') - add_lst.append((sat_name, idx_sat_delay[0], idx_sat_delay[1], int(idx_sat_delay[2]))) - f.close() - return del_lst, update_lst, add_lst - -def _parse_gsls(path): - del_lst, update_lst, add_lst = [], [], [] - f = open(path, 'r') - for gid, line in enumerate(f): - if len(line) == 0 or line.isspace(): - continue - toks = line.strip().split('|') + node = toks[0] if len(toks[1]) > 0: - for isl in toks[1].split(' '): - i_s_o_s_d = isl.split(',') - idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) - oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) - del_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + for link in toks[1].split(' '): + del_lst.append((node, link)) if len(toks[2]) > 0: - for isl in toks[2].split(' '): - i_s_o_s_d = isl.split(',') - idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) - oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) - update_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + for link in toks[2].split(' '): + peer_delay = link.split(',') + update_lst.append((node, peer_delay[0], peer_delay[1])) if len(toks[3]) > 0: - for isl in toks[3].split(' '): - i_s_o_s_d = isl.split(',') - idx, shell_id = int(i_s_o_s_d[0]), int(i_s_o_s_d[1]) - oid, sid = int(i_s_o_s_d[2]), int(i_s_o_s_d[3]) - add_lst.append((idx, gid, shell_id, oid, sid, i_s_o_s_d[4])) + for link in toks[3].split(' '): + peer_delay_idx = link.split(',') + add_lst.append((node, peer_delay_idx[0], peer_delay_idx[1], int(peer_delay_idx[2]))) f.close() return del_lst, update_lst, add_lst @@ -171,7 +137,7 @@ def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, dela suffix6 = '::10/48' if name1 < name2 else '::40/48' _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + suffix6, delay, bw, loss) -def sn_init_nodes(dir, sat_mid_dict_shell, gs_mid_dict): +def sn_init_nodes(dir, shell_num, node_mid_dict): def _load_netns(pid, name): netns_link = f'/run/netns/{name}' if not os.path.exists(netns_link): @@ -196,65 +162,50 @@ def _load_netns(pid, name): pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') sat_cnt = 0 - for shell_id, mid_dict in enumerate(sat_mid_dict_shell): - for node, mid in mid_dict.items(): - if mid != machine_id: - pid_file.write(NOT_ASSIGNED + ' ') - continue - node_dir = f"{dir}/shell{shell_id}/overlay/{node}" - sat_cnt += 1 - os.makedirs(node_dir, exist_ok=True) - pid_file.write(node+':'+str(pyctr.container_run(node_dir, node))+' ') - pid_file.write('\n') - print(f'[{machine_id}] shell {shell_id}: {sat_cnt} satellites initialized') - - gs_lst = [] - overlay_dir = f"{dir}/GS-{len(gs_mid_dict)}/overlay" - for node, mid in gs_mid_dict.items(): + for node, mid in node_mid_dict.items(): if mid != machine_id: pid_file.write(NOT_ASSIGNED + ' ') continue - gs_lst.append(node) - node_dir = f'{overlay_dir}/{node}' + node_dir = f"{dir}/overlay/{node}" + sat_cnt += 1 os.makedirs(node_dir, exist_ok=True) pid_file.write(node+':'+str(pyctr.container_run(node_dir, node))+' ') pid_file.write('\n') - print(f'[{machine_id}] GS:', ','.join(gs_lst)) - + print(f'[{machine_id}]: {sat_cnt} nodes initialized') pid_file.close() sn_operate_every_node(dir, _load_netns) def sn_update_network( - dir, ts, sat_mid_dict_shell, gs_mid_dict, ip_lst, + dir, ts, shell_num, node_mid_dict, ip_lst, isl_bw, isl_loss, gsl_bw, gsl_loss ): - for shell_id, mid_dict in enumerate(sat_mid_dict_shell): + for shell_id in range(shell_num): shell_dir = f"{dir}/shell{shell_id}" if not os.path.exists(shell_dir): continue del_cnt, update_cnt, add_cnt = 0, 0, 0 - del_lst, update_lst, add_lst = _parse_isls(f'{shell_dir}/{ts}.txt') + del_lst, update_lst, add_lst = _parse_links(f'{shell_dir}/{ts}.txt') for sat_name, isl_sat in del_lst: - if mid_dict[sat_name] == machine_id: + if node_mid_dict[sat_name] == machine_id: del_cnt += 1 _del_link(sat_name, isl_sat) - elif mid_dict[isl_sat] == machine_id: + elif node_mid_dict[isl_sat] == machine_id: del_cnt += 1 _del_link(isl_sat, sat_name) for sat_name, isl_sat, delay in update_lst: - if mid_dict[sat_name] == machine_id: + if node_mid_dict[sat_name] == machine_id: update_cnt += 1 - if mid_dict[isl_sat] == machine_id: + if node_mid_dict[isl_sat] == machine_id: _update_link_intra_machine(sat_name, isl_sat, delay, isl_bw, isl_loss) else: _update_link_local(sat_name, isl_sat, delay, isl_bw, isl_loss) - elif mid_dict[isl_sat] == machine_id: + elif node_mid_dict[isl_sat] == machine_id: update_cnt += 1 _update_link_local(isl_sat, sat_name, delay, isl_bw, isl_loss) for sat_name, isl_sat, delay, idx in add_lst: - if mid_dict[sat_name] == machine_id: + if node_mid_dict[sat_name] == machine_id: add_cnt += 1 - if mid_dict[isl_sat] == machine_id: + if node_mid_dict[isl_sat] == machine_id: _add_link_intra_machine( idx, sat_name, isl_sat, f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', @@ -262,79 +213,63 @@ def sn_update_network( ) else: _add_link_inter_machine( - idx, sat_name, isl_sat, ip_lst[mid_dict[isl_sat]], + idx, sat_name, isl_sat, ip_lst[node_mid_dict[isl_sat]], f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', delay, isl_bw, isl_loss ) - elif mid_dict[isl_sat] == machine_id: + elif node_mid_dict[isl_sat] == machine_id: add_cnt += 1 _add_link_inter_machine( - idx, isl_sat, sat_name, ip_lst[mid_dict[sat_name]], + idx, isl_sat, sat_name, ip_lst[node_mid_dict[sat_name]], f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', delay, isl_bw, isl_loss ) print(f"[{machine_id}] Shell {shell_id}:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") - gs_dir = f"{dir}/GS-{len(gs_mid_dict)}" - return + gs_dir = f"{dir}/GS" if not os.path.exists(gs_dir): return del_cnt, update_cnt, add_cnt = 0, 0, 0 - del_lst, update_lst, add_lst = _parse_gsls(f'{gs_dir}/{ts}.txt') - for idx, gid, shell_id, oid, sid, delay in del_lst: - orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] - if gs_mid[gid] == machine_id: + del_lst, update_lst, add_lst = _parse_links(f'{gs_dir}/{ts}.txt') + for gs, sat in del_lst: + if node_mid_dict[gs] == machine_id: del_cnt += 1 - _del_link(idx, _gs_name(gid), _sat_name(shell_id, oid, sid)) - elif sat_mid[sid] == machine_id: + _del_link(gs, sat) + elif node_mid_dict[sat] == machine_id: del_cnt += 1 - _del_link(idx, _sat_name(shell_id, isl_oid, isl_sid), _gs_name(gid)) - for idx, gid, shell_id, oid, sid, delay in update_lst: - orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] - if gs_mid[gid] == machine_id: + _del_link(sat, gs) + for gs, sat, delay in update_lst: + if node_mid_dict[gs] == machine_id: update_cnt += 1 - if sat_mid[sid] == machine_id: - _update_link_intra_machine( - idx, - _gs_name(gid), _sat_name(shell_id, oid, sid), - delay, gsl_bw, gsl_loss - ) + if node_mid_dict[sat] == machine_id: + _update_link_intra_machine(gs, sat, delay, gsl_bw, gsl_loss) else: - _update_link_local( - idx, - _gs_name(gid), _sat_name(shell_id, oid, sid), - delay, gsl_bw, gsl_loss - ) - elif sat_mid[sid] == machine_id: + _update_link_local(gs, sat, delay, gsl_bw, gsl_loss) + elif node_mid_dict[sat] == machine_id: update_cnt += 1 - _update_link_local( - idx, - _sat_name(shell_id, oid, sid), _gs_name(gid), - delay, gsl_bw, gsl_loss - ) - for idx, gid, shell_id, oid, sid, delay in add_lst: - orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] - if gs_mid[gid] == machine_id: + _update_link_local(sat, gs, delay, gsl_bw, gsl_loss) + for gs, sat, delay, idx in add_lst: + if node_mid_dict[gs] == machine_id: add_cnt += 1 - if sat_mid[sid] == machine_id: + if node_mid_dict[gs] == machine_id: _add_link_intra_machine( - idx, - _gs_name(gid), _sat_name(shell_id, oid, sid), - f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss + idx, gs, sat, + f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', + delay, gsl_bw, gsl_loss ) else: _add_link_inter_machine( - idx, - _gs_name(gid), _sat_name(shell_id, oid, sid), ip_lst[sat_mid[sid]], - f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss + idx, gs, sat, ip_lst[node_mid_dict[gs]], + f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', + delay, gsl_bw, gsl_loss ) - elif sat_mid[sid] == machine_id: + elif node_mid_dict[sat] == machine_id: add_cnt += 1 _add_link_inter_machine( - idx, - _sat_name(shell_id, oid, sid), _gs_name(gid), ip_lst[gs_mid[gid]], - f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss + idx, sat, gs, ip_lst[node_mid_dict[sat]], + f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', + delay, gsl_bw, gsl_loss ) print(f"[{machine_id}] GSL:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") @@ -587,10 +522,10 @@ def sn_recover(dir, sat_loss): for line in f: damage_set.add(line.strip()) - sat_mid_dict_shell, gs_mid_dict, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) + shell_num, node_mid_dict, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) if cmd == 'nodes': sn_clean(workdir) - sn_init_nodes(workdir, sat_mid_dict_shell, gs_mid_dict) + sn_init_nodes(workdir, shell_num, node_mid_dict) elif cmd == 'list': print(f"{'NODE':<20} STATE") for name in _pid_map(workdir + '/' + PID_FILENAME): @@ -600,7 +535,7 @@ def sn_recover(dir, sat_loss): # sn_update_network = lp(sn_update_network) # lp.add_function(_update_link_intra_machine) sn_update_network( - workdir, sys.argv[4], sat_mid_dict_shell, gs_mid_dict, ip_lst, + workdir, sys.argv[4], shell_num, node_mid_dict, ip_lst, sys.argv[5], sys.argv[6], sys.argv[7], sys.argv[8] ) # with open('report.txt', 'w') as f: diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index dcd1934..88101dd 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -255,7 +255,7 @@ def __init__(self, configuration_file_path, GS_lat_long, hello_interval): os.path.abspath(configuration_file_path)) self.experiment_name = sn_args.cons_name\ +'-'+ sn_args.link_style +'-'+ sn_args.link_policy - self.gs_dirname = 'GS-' + str(len(self.gs_lat_long)) + self.gs_dirname = 'GS' for shell_id, shell in enumerate(self.shell_lst): shell['name'] = f"shell{shell_id}" @@ -266,8 +266,7 @@ def __init__(self, configuration_file_path, GS_lat_long, hello_interval): self.local_dir, self.duration, self.step, self.shell_lst, self.link_style, self.gs_lat_long, self.antenna_number, self.elevation, self.link_policy ) - (self.remote_lst, self.sat_mid_dict, - self.gs_mid_dict) = self._assign_remote(sat_names_shell, sn_args.machine_lst) + self.remote_lst, self.node_mid_dict = self._assign_remote(sat_names_shell, sn_args.machine_lst) self.events = [] @@ -294,7 +293,7 @@ def _assign_remote(self, sat_names_shell, machine_lst): remainder = len(sat_names_shell) % len(machine_lst) shell_id = 0 - sat_mid_dict_shell = [] + node_mid_dict = {} assigned_shell_lst = [] for i, remote in enumerate(machine_lst): shell_num = shell_per_machine @@ -306,13 +305,10 @@ def _assign_remote(self, sat_names_shell, machine_lst): ] # all satellites of a shell assigned to a single machine for shell_name, sat_names in assigned_shells: - sat_mid_dict = {} for sat_name in sat_names: - sat_mid_dict[sat_name] = i - sat_mid_dict_shell.append(sat_mid_dict) + node_mid_dict[sat_name] = i assigned_shell_lst.append(assigned_shells) shell_id += shell_num - gs_mid_dict = {} # TODO: better ground station assign with open(os.path.join(self.local_dir, self.gs_dirname,'gsl','0.txt'))as f: for line in f: @@ -323,14 +319,14 @@ def _assign_remote(self, sat_names_shell, machine_lst): gs_name = toks[0] add_lst = toks[3] if len(add_lst) == 0: - gs_mid_dict[gs_name] = 0 + node_mid_dict[gs_name] = 0 continue gsl = add_lst.split(' ')[0].split(',') - gs_mid_dict[gs_name] = sat_mid_dict[gsl[0]] + node_mid_dict[gs_name] = node_mid_dict[gsl[0]] ip_lst = [remote['IP'] for remote in machine_lst] assign_obj = { - 'sat_mid_shell': sat_mid_dict_shell, - 'gs_mid': gs_mid_dict, + 'shell_num': len(self.shell_lst), + 'node_mid_dict': node_mid_dict, 'ip': ip_lst, } with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: @@ -347,10 +343,10 @@ def _assign_remote(self, sat_names_shell, machine_lst): assigned_shell_lst[i], self.experiment_name, self.local_dir, - self.gs_dirname if i in gs_mid_dict.values() else None + self.gs_dirname ) ) - return remote_lst, sat_mid_dict, gs_mid_dict + return remote_lst, node_mid_dict def create_nodes(self): print('Initializing nodes ...') From 79cacd4fd0a8246c1c01756ba0c292bed6373eed Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Tue, 22 Apr 2025 15:57:29 +0800 Subject: [PATCH 14/49] feat: intra-shell partition --- starrynet/sn_orchestrater.py | 2 +- starrynet/sn_synchronizer.py | 77 ++++++++++++++++++++++++------------ 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index eb59ac4..9e52bee 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -19,7 +19,7 @@ DAMAGE_FILENAME = 'damage_list.txt' NOT_ASSIGNED = 'NA' -VXLAN_PORT = 4789 +VXLAN_PORT = '4789' # FIXME CLONE_NEWNET = 0x40000000 libc = ctypes.CDLL(None) diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 88101dd..013ea3f 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -283,18 +283,42 @@ def _assign_remote(self, sat_names_shell, machine_lst): assert len(sat_names_shell) == len(self.shell_lst) # TODO: better partition + node_mid_dict = {} + assigned_shell_lst = [] if len(sat_names_shell) * 2 <= len(machine_lst): # need intra-shell partition - machine_per_shell = len(machine_lst) // len(sat_names_shell) - raise NotImplementedError + sat_total = sum(len(sat_names) for sat_names in sat_names_shell) + sat_per_machine = sat_total // len(machine_lst) + remainder = sat_total % len(machine_lst) + + shell_id, sat_id = 0, 0 + sat_names = [] + for i, remote in enumerate(machine_lst): + sat_nr = sat_per_machine + if i < remainder: + sat_nr += 1 + + assigned_shells = [] + for _ in range(sat_nr): + sat_names.append(sat_names_shell[shell_id][sat_id]) + node_mid_dict[sat_names[-1]] = i + sat_id += 1 + if(sat_id >= len(sat_names_shell[shell_id])): + assigned_shells.append((self.shell_lst[shell_id]['name'], sat_names)) + shell_id += 1 + sat_id = 0 + sat_names = [] + if len(sat_names) > 0: + assigned_shells.append((self.shell_lst[shell_id]['name'], sat_names)) + sat_names = [] + print(assigned_shells) + assigned_shell_lst.append(assigned_shells) else: # only divide shell shell_per_machine = len(self.shell_lst) // len(machine_lst) remainder = len(sat_names_shell) % len(machine_lst) shell_id = 0 - node_mid_dict = {} - assigned_shell_lst = [] for i, remote in enumerate(machine_lst): shell_num = shell_per_machine if i < remainder: @@ -309,28 +333,29 @@ def _assign_remote(self, sat_names_shell, machine_lst): node_mid_dict[sat_name] = i assigned_shell_lst.append(assigned_shells) shell_id += shell_num - # TODO: better ground station assign - with open(os.path.join(self.local_dir, self.gs_dirname,'gsl','0.txt'))as f: - for line in f: - line = line.strip() - if len(line) == 0: - continue - toks = line.split('|') - gs_name = toks[0] - add_lst = toks[3] - if len(add_lst) == 0: - node_mid_dict[gs_name] = 0 - continue - gsl = add_lst.split(' ')[0].split(',') - node_mid_dict[gs_name] = node_mid_dict[gsl[0]] - ip_lst = [remote['IP'] for remote in machine_lst] - assign_obj = { - 'shell_num': len(self.shell_lst), - 'node_mid_dict': node_mid_dict, - 'ip': ip_lst, - } - with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: - json.dump(assign_obj, f) + + # TODO: better ground station assign + with open(os.path.join(self.local_dir, self.gs_dirname,'gsl','0.txt'))as f: + for line in f: + line = line.strip() + if len(line) == 0: + continue + toks = line.split('|') + gs_name = toks[0] + add_lst = toks[3] + if len(add_lst) == 0: + node_mid_dict[gs_name] = 0 + continue + gsl = add_lst.split(' ')[0].split(',') + node_mid_dict[gs_name] = node_mid_dict[gsl[0]] + ip_lst = [remote['IP'] for remote in machine_lst] + assign_obj = { + 'shell_num': len(self.shell_lst), + 'node_mid_dict': node_mid_dict, + 'ip': ip_lst, + } + with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: + json.dump(assign_obj, f) remote_lst = [] for i, remote in enumerate(machine_lst): From 8e823c3831a9ba9f2ae7f4370276751f16ea5ba8 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sun, 27 Apr 2025 14:31:57 +0800 Subject: [PATCH 15/49] fix: IPv4 suffix assign in orchestrater --- example.py | 6 +++--- starrynet/sn_orchestrater.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/example.py b/example.py index 6fb7efa..351b620 100644 --- a/example.py +++ b/example.py @@ -82,13 +82,13 @@ # random damage of a given ratio at a certain time sn.set_damage(ratio, time_index) - sn.check_routing_table('GS1', 20) + sn.check_routing_table('GS1', 30) - time_index = 25 + time_index = 35 # recover the damages at a certain time sn.set_recovery(time_index) - sn.check_routing_table('GS1', 30) + sn.check_routing_table('GS1', 45) sn.start_emulation() diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 9e52bee..2530659 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -134,8 +134,9 @@ def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, dela ('ip', 'link', 'add', n1_n2, 'netns', name1, 'type', 'vxlan', 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) ) + suffix4 = '.10/24' if name1 < name2 else '.40/24' suffix6 = '::10/48' if name1 < name2 else '::40/48' - _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + suffix6, delay, bw, loss) + _init_if(name1, n1_n2, prefix4+suffix4, prefix6 + suffix6, delay, bw, loss) def sn_init_nodes(dir, shell_num, node_mid_dict): def _load_netns(pid, name): From 52b412a9d3e79d11133ab3a39ac97811517a13ef Mon Sep 17 00:00:00 2001 From: ZhifengHan1101 Date: Mon, 19 May 2025 21:47:59 +0800 Subject: [PATCH 16/49] Use netlink socket API for link configuration (replacing tc). --- example.py | 2 +- starrynet/pynetlink.c | 868 +++++++++++++++++++++++++++++++++++ starrynet/sn_orchestrater.py | 56 ++- starrynet/sn_synchronizer.py | 4 + 4 files changed, 918 insertions(+), 12 deletions(-) create mode 100644 starrynet/pynetlink.c diff --git a/example.py b/example.py index 85c8b74..190119d 100644 --- a/example.py +++ b/example.py @@ -73,7 +73,7 @@ # sn.run_routing_daemon() # run OSPF daemon on selected nodes - sn.run_routing_deamon(node_lst=[ + sn.run_routing_daemon(node_lst=[ 'GS1', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS2']) sn.check_routing_table('GS2', 4) diff --git a/starrynet/pynetlink.c b/starrynet/pynetlink.c new file mode 100644 index 0000000..6cb95af --- /dev/null +++ b/starrynet/pynetlink.c @@ -0,0 +1,868 @@ +// pynetlink.c - Direct netlink interface for traffic control +#include +// POSIX and Linux +#include +#include +#include +#include +#include +#include +#include +#include +#include +// std C +#include +#include +#include +#include +#include + + +// Update netem qdisc using netlink +static int update_netem_( + const char *if_name, uint32_t delay_ms, uint32_t loss_percent, + const char *rate_str, char *err_str, size_t max_len) +{ + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Parse rate string (e.g., "10Gbit" to bps) + double rate_value = 0.0; + char rate_unit[16] = {0}; + uint64_t rate_bps = 0; + + if (sscanf(rate_str, "%lf%15s", &rate_value, rate_unit) == 2) { + if (strcmp(rate_unit, "Gbit") == 0) { + rate_bps = (uint64_t)(rate_value * 1000000000 / 8); + } else if (strcmp(rate_unit, "Mbit") == 0) { + rate_bps = (uint64_t)(rate_value * 1000000 / 8); + } else if (strcmp(rate_unit, "Kbit") == 0) { + rate_bps = (uint64_t)(rate_value * 1000 / 8); + } else { + // Default to bps + rate_bps = (uint64_t)rate_value; + } + } else { + // Try to parse as just a number (bps) + if (sscanf(rate_str, "%lf", &rate_value) == 1) { + rate_bps = (uint64_t)rate_value * 1000000000 / 8; + } + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket to a random dynamic port + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate a unique sequence number based on time + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; // Use milliseconds as sequence + + // netlink msg buffer + uint8_t buf[1024]; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); + nl_hdr->nlmsg_type = RTM_NEWQDISC; + nl_hdr->nlmsg_seq = seq; + + // For existing qdisc change, not add - avoid "File exists" error + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE; + + // Use our assigned PID + nl_hdr->nlmsg_pid = sa.nl_pid; + + // TC message structure + struct tcmsg* tc_msg = NLMSG_DATA(nl_hdr); + tc_msg->tcm_family = AF_UNSPEC; + tc_msg->tcm__pad1 = tc_msg->tcm__pad2 = 0; + tc_msg->tcm_ifindex = if_idx; + + tc_msg->tcm_handle = 0; + tc_msg->tcm_parent = TC_H_ROOT; // Parent is ROOT + tc_msg->tcm_info = 0; + + // Add qdisc kind attribute (netem) + struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = TCA_KIND; + rta->rta_len = RTA_LENGTH(strlen("netem") + 1); + memcpy(RTA_DATA(rta), "netem", strlen("netem") + 1); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Start with options + struct rtattr* opts = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + opts->rta_type = TCA_OPTIONS; + opts->rta_len = RTA_LENGTH(sizeof(struct tc_netem_qopt)); + + // Set the netem parameters + struct tc_netem_qopt* qopt = (struct tc_netem_qopt*)RTA_DATA(opts); + memset(qopt, 0, sizeof(*qopt)); + qopt->limit = 1000; + qopt->latency = delay_ms * 1000; // Convert ms to us + qopt->loss = loss_percent * 10000 / 100; // Convert % to ppm + + // Update total length + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(opts->rta_len); + + // If we have a rate specified, add rate information + if (rate_bps > 0) { + struct rtattr* rate_attr = (struct rtattr*)((char*)opts + RTA_ALIGN(opts->rta_len)); + rate_attr->rta_type = TCA_NETEM_RATE; + rate_attr->rta_len = RTA_LENGTH(sizeof(struct tc_netem_rate)); + + struct tc_netem_rate* rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); + memset(rate, 0, sizeof(*rate)); + rate->rate = (rate_bps < (1ULL << 32)) ? rate_bps : ~0U; + + // Update options length + opts->rta_len += RTA_ALIGN(rate_attr->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate_attr->rta_len); + + // Add 64-bit value if needed + if (rate_bps >= (1ULL << 32)) { + struct rtattr* rate64 = (struct rtattr*)((char*)rate_attr + RTA_ALIGN(rate_attr->rta_len)); + rate64->rta_type = TCA_NETEM_RATE64; + rate64->rta_len = RTA_LENGTH(sizeof(uint64_t)); + *(uint64_t*)RTA_DATA(rate64) = rate_bps; + + // Update lengths + opts->rta_len += RTA_ALIGN(rate64->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate64->rta_len); + } + } + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[1024]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Delete a network interface using netlink (replaces ip link del) +static int del_link_(const char *if_name, char *err_str, size_t max_len) { + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for deleting the interface + uint8_t buf[512] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + nl_hdr->nlmsg_type = RTM_DELLINK; // Delete link + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // Interface info message + struct ifinfomsg* if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(struct ifinfomsg)); + if_msg->ifi_family = AF_UNSPEC; + if_msg->ifi_index = if_idx; // Specify interface by index + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[512]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Add IP address to interface using netlink (replaces ip addr add) +static int add_addr_(const char *if_name, const char *addr_str, char *err_str, size_t max_len) { + // Parse IP address string + char ip_str[40] = {0}; + int prefix_len = 24; // Default prefix length + + if (sscanf(addr_str, "%39[^/]/%d", ip_str, &prefix_len) < 1) { + snprintf(err_str, max_len, "Invalid IP address format: %s", addr_str); + return -1; + } + + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for adding IP address + uint8_t buf[512] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); + nl_hdr->nlmsg_type = RTM_NEWADDR; // Add address + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_EXCL; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // Address message + struct ifaddrmsg* addr_msg = NLMSG_DATA(nl_hdr); + memset(addr_msg, 0, sizeof(struct ifaddrmsg)); + addr_msg->ifa_family = AF_INET; // IPv4 + addr_msg->ifa_prefixlen = prefix_len; + addr_msg->ifa_flags = IFA_F_PERMANENT; + addr_msg->ifa_scope = RT_SCOPE_UNIVERSE; + addr_msg->ifa_index = if_idx; + + // Add IP address attribute + struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFA_LOCAL; + rta->rta_len = RTA_LENGTH(4); // IPv4 address length + + struct in_addr ip_addr; + if (inet_pton(AF_INET, ip_str, &ip_addr) <= 0) { + snprintf(err_str, max_len, "Invalid IP address: %s", ip_str); + close(sock_fd); + return -1; + } + + memcpy(RTA_DATA(rta), &ip_addr, sizeof(ip_addr)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Add address attribute (same as local for IPv4) + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFA_ADDRESS; + rta->rta_len = RTA_LENGTH(4); + memcpy(RTA_DATA(rta), &ip_addr, sizeof(ip_addr)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[512]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Set interface up using netlink (replaces ip link set up) +static int set_link_up_(const char *if_name, char *err_str, size_t max_len) { + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for setting interface up + uint8_t buf[512] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + nl_hdr->nlmsg_type = RTM_NEWLINK; // Modify link + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // Interface info message + struct ifinfomsg* if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(struct ifinfomsg)); + if_msg->ifi_family = AF_UNSPEC; + if_msg->ifi_index = if_idx; + if_msg->ifi_change = IFF_UP; // Change UP flag + if_msg->ifi_flags = IFF_UP; // Set UP flag + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[512]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Initialize interface (add addr, setup tc, set link up) +static int init_if_(const char *if_name, const char *addr_str, + uint32_t delay_ms, uint32_t loss_percent, + const char *rate_str, char *err_str, size_t max_len) { + // 1. Add IP address + if (add_addr_(if_name, addr_str, err_str, max_len) != 0) { + return -1; + } + + // 2. Add traffic control qdisc + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + close(sock_fd); + return -1; + } + + // Parse rate string (similar to update_netem_) + double rate_value = 0.0; + char rate_unit[16] = {0}; + uint64_t rate_bps = 0; + + if (sscanf(rate_str, "%lf%15s", &rate_value, rate_unit) == 2) { + if (strcmp(rate_unit, "Gbit") == 0) { + rate_bps = (uint64_t)(rate_value * 1000000000 / 8); + } else if (strcmp(rate_unit, "Mbit") == 0) { + rate_bps = (uint64_t)(rate_value * 1000000 / 8); + } else if (strcmp(rate_unit, "Kbit") == 0) { + rate_bps = (uint64_t)(rate_value * 1000 / 8); + } else { + rate_bps = (uint64_t)rate_value; + } + } else { + if (sscanf(rate_str, "%lf", &rate_value) == 1) { + rate_bps = (uint64_t)rate_value * 1000000000 / 8; + } + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for adding qdisc + uint8_t buf[1024] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); + nl_hdr->nlmsg_type = RTM_NEWQDISC; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // TC message structure + struct tcmsg* tc_msg = NLMSG_DATA(nl_hdr); + tc_msg->tcm_family = AF_UNSPEC; + tc_msg->tcm__pad1 = tc_msg->tcm__pad2 = 0; + tc_msg->tcm_ifindex = if_idx; + tc_msg->tcm_handle = 0; + tc_msg->tcm_parent = TC_H_ROOT; + tc_msg->tcm_info = 0; + + // Add qdisc kind attribute (netem) + struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = TCA_KIND; + rta->rta_len = RTA_LENGTH(strlen("netem") + 1); + memcpy(RTA_DATA(rta), "netem", strlen("netem") + 1); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Options + struct rtattr* opts = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + opts->rta_type = TCA_OPTIONS; + opts->rta_len = RTA_LENGTH(sizeof(struct tc_netem_qopt)); + + // Set netem parameters + struct tc_netem_qopt* qopt = (struct tc_netem_qopt*)RTA_DATA(opts); + memset(qopt, 0, sizeof(*qopt)); + qopt->limit = 1000; + qopt->latency = delay_ms * 1000; // Convert ms to us + qopt->loss = loss_percent * 10000 / 100; // Convert % to ppm + + // Update total length + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(opts->rta_len); + + // If we have a rate specified, add rate information + if (rate_bps > 0) { + struct rtattr* rate_attr = (struct rtattr*)((char*)opts + RTA_ALIGN(opts->rta_len)); + rate_attr->rta_type = TCA_NETEM_RATE; + rate_attr->rta_len = RTA_LENGTH(sizeof(struct tc_netem_rate)); + + struct tc_netem_rate* rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); + memset(rate, 0, sizeof(*rate)); + rate->rate = (rate_bps < (1ULL << 32)) ? rate_bps : ~0U; + + // Update options length + opts->rta_len += RTA_ALIGN(rate_attr->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate_attr->rta_len); + + // Add 64-bit value if needed + if (rate_bps >= (1ULL << 32)) { + struct rtattr* rate64 = (struct rtattr*)((char*)rate_attr + RTA_ALIGN(rate_attr->rta_len)); + rate64->rta_type = TCA_NETEM_RATE64; + rate64->rta_len = RTA_LENGTH(sizeof(uint64_t)); + *(uint64_t*)RTA_DATA(rate64) = rate_bps; + + // Update lengths + opts->rta_len += RTA_ALIGN(rate64->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate64->rta_len); + } + } + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[1024]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "TC error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + + // 3. Set interface up + if (set_link_up_(if_name, err_str, max_len) != 0) { + return -1; + } + + return 0; +} + +// Python function to update network interface parameters +static PyObject* pynetlink_update_if(PyObject* self, PyObject* args) { + const char *if_name; + const char *delay_str; + const char *rate_str; + const char *loss_str; + char err[256]; + + // Parse Python arguments + if (!PyArg_ParseTuple(args, "ssss", &if_name, &delay_str, &rate_str, &loss_str)) { + return NULL; + } + + // Convert delay string to integer (ms) + int delay_ms = atoi(delay_str); + + // Convert loss string to integer (%) + int loss_percent = atoi(loss_str); + + // Update netem qdisc + int result = update_netem_(if_name, delay_ms, loss_percent, rate_str, err, sizeof(err)); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +// Python function to delete a network interface +static PyObject* pynetlink_del_link(PyObject* self, PyObject* args) { + const char *if_name; + char err[256]; + + if (!PyArg_ParseTuple(args, "s", &if_name)) { + return NULL; + } + + int result = del_link_(if_name, err, sizeof(err)); + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +// Python function to initialize an interface (addr, tc, up) +static PyObject* pynetlink_init_if(PyObject* self, PyObject* args) { + const char *if_name; + const char *addr_str; + const char *delay_str; + const char *rate_str; + const char *loss_str; + char err[256]; + + if (!PyArg_ParseTuple(args, "sssss", &if_name, &addr_str, &delay_str, &rate_str, &loss_str)) { + return NULL; + } + + // Convert strings to numeric values + int delay_ms = atoi(delay_str); + int loss_percent = atoi(loss_str); + + int result = init_if_(if_name, addr_str, delay_ms, loss_percent, rate_str, err, sizeof(err)); + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +// Define module methods +static PyMethodDef PyNetlinkMethods[] = { + {"update_if", pynetlink_update_if, METH_VARARGS, "Update network interface parameters using netlink"}, + {"del_link", pynetlink_del_link, METH_VARARGS, "Delete a network interface using netlink"}, + {"init_if", pynetlink_init_if, METH_VARARGS, "Initialize an interface (add addr, setup tc, set up)"}, + {NULL, NULL, 0, NULL} +}; + +// Define module +static struct PyModuleDef pynetlink_module = { + PyModuleDef_HEAD_INIT, + "pynetlink", + "Python extension for efficient network interface updates using netlink", + -1, + PyNetlinkMethods +}; + +// Initialize module +PyMODINIT_FUNC PyInit_pynetlink(void) { + return PyModule_Create(&pynetlink_module); +} + diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 3970729..1617c1d 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -4,6 +4,7 @@ import sys import glob import ctypes +import time # from line_profiler import LineProfiler @@ -129,28 +130,31 @@ def _del_link(idx, name1, name2): fd = os.open('/run/netns/' + name1, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - subprocess.check_call(('ip', 'link', 'del', n1_n2)) + # subprocess.check_call(('ip', 'link', 'del', n1_n2)) + pynetlink.del_link(n1_n2) def _init_if(name, if_name, addr, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) - subprocess.check_call( - ('tc', 'qdisc', 'add', 'dev', if_name, 'root', - 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbit') - ) - subprocess.check_call(('ip', 'link', 'set', if_name, 'up')) + # subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) + # subprocess.check_call( + # ('tc', 'qdisc', 'add', 'dev', if_name, 'root', + # 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbit') + # ) + # subprocess.check_call(('ip', 'link', 'set', if_name, 'up')) + pynetlink.init_if(if_name, addr, delay, bw, loss) def _update_if(name, if_name, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) update_loss = '100' if name in damage_set else loss - subprocess.check_call( - ('tc', 'qdisc', 'change', 'dev', if_name, 'root', - 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') - ) + # subprocess.check_call( + # ('tc', 'qdisc', 'change', 'dev', if_name, 'root', + # 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') + # ) + pynetlink.update_if(if_name, delay, bw, update_loss) def _update_link_intra_machine(idx, name1, name2, delay, bw, loss): n1_n2 = f"{name2}" @@ -328,6 +332,7 @@ def sn_update_network( return del_cnt, update_cnt, add_cnt = 0, 0, 0 del_lst, update_lst, add_lst = _parse_gsls(f'{gs_dir}/{ts}.txt') + del_st = time.time() for idx, gid, shell_id, oid, sid, delay in del_lst: orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] if gs_mid[gid] == machine_id: @@ -336,6 +341,9 @@ def sn_update_network( elif sat_mid[sid] == machine_id: del_cnt += 1 _del_link(idx, _sat_name(shell_id, isl_oid, isl_sid), _gs_name(gid)) + del_ed = time.time() + del_time = del_ed - del_st + update_st = time.time() for idx, gid, shell_id, oid, sid, delay in update_lst: orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] if gs_mid[gid] == machine_id: @@ -359,6 +367,9 @@ def sn_update_network( _sat_name(shell_id, oid, sid), _gs_name(gid), delay, gsl_bw, gsl_loss ) + update_ed = time.time() + update_time = update_ed - update_st + add_st = time.time() for idx, gid, shell_id, oid, sid, delay in add_lst: orbit_num, shell_name, sat_mid = sat_mid_lst[shell_id] if gs_mid[gid] == machine_id: @@ -382,8 +393,20 @@ def sn_update_network( _sat_name(shell_id, oid, sid), _gs_name(gid), ip_lst[gs_mid[gid]], f'9.{idx >> 8}.{idx & 0xFF}', delay, gsl_bw, gsl_loss ) + add_ed = time.time() + add_time = add_ed - add_st print(f"[{machine_id}] GSL:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") + print(f"[{machine_id}] GSL time:", + f"{del_time} s for deleted, {update_time} s for updated, {add_time} s for added.") + + # GSL_TIMING_DIR = 'gsl_timing_all_change' + # timing_dir = os.path.join(dir, GSL_TIMING_DIR) + # os.makedirs(timing_dir, exist_ok=True) + # timing_file = os.path.join(timing_dir, f"gsl_timing_{ts}.txt") + # with open(timing_file, 'w') as f: + # f.write(f"{del_time:.6f} {update_time:.6f} {add_time:.6f}\n") + def sn_container_check_call(pid, cmd, *args, **kwargs): subprocess.check_call( @@ -626,6 +649,17 @@ def sn_recover(dir, sat_loss): shell=True ) import pyctr + + try: + import pynetlink + except ModuleNotFoundError: + subprocess.check_call( + "cd " + workdir + " && " + "gcc $(python3-config --cflags --ldflags) " + "-shared -fPIC -O2 pynetlink.c -o pynetlink.so", + shell=True + ) + import pynetlink damage_set = set() damage_file = workdir + '/' + DAMAGE_FILENAME diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 6cd5504..ac6c838 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -94,6 +94,10 @@ def __init__(self, id, host, port, username, password, os.path.join(os.path.dirname(__file__), 'pyctr.c'), self.dir + '/pyctr.c' ) + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'pynetlink.c'), + self.dir + '/pynetlink.c' + ) self.sftp.put( os.path.join(self.local_dir, 'bird.conf'), self.dir + '/bird.conf' From 634e91143f4f17e78ff319c26c7db9556a4284f4 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sat, 6 Sep 2025 16:31:33 +0800 Subject: [PATCH 17/49] feat: direct rtnl, damage/recovery, exec, reconf --- starrynet/pynetlink.c | 858 +++++++++++++++++++++++++++++++++++ starrynet/sn_controller.py | 72 +-- starrynet/sn_orchestrater.py | 74 ++- starrynet/sn_synchronizer.py | 36 +- 4 files changed, 982 insertions(+), 58 deletions(-) create mode 100644 starrynet/pynetlink.c diff --git a/starrynet/pynetlink.c b/starrynet/pynetlink.c new file mode 100644 index 0000000..bf92e98 --- /dev/null +++ b/starrynet/pynetlink.c @@ -0,0 +1,858 @@ +// pynetlink.c - Direct netlink interface for traffic control +#include +// POSIX and Linux +#include +#include +#include +#include +#include +#include +#include +#include +#include +// std C +#include +#include +#include +#include +#include + + +// Update netem qdisc using netlink +static int update_netem_( + const char *if_name, uint32_t delay, uint32_t loss, + uint64_t rate_Bps, char *err_str, size_t max_len) +{ + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket to a random dynamic port + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate a unique sequence number based on time + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; // Use milliseconds as sequence + + // netlink msg buffer + uint8_t buf[1024]; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); + nl_hdr->nlmsg_type = RTM_NEWQDISC; + nl_hdr->nlmsg_seq = seq; + + // For existing qdisc change, not add - avoid "File exists" error + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE; + + // Use our assigned PID + nl_hdr->nlmsg_pid = sa.nl_pid; + + // TC message structure + struct tcmsg* tc_msg = NLMSG_DATA(nl_hdr); + tc_msg->tcm_family = AF_UNSPEC; + tc_msg->tcm__pad1 = tc_msg->tcm__pad2 = 0; + tc_msg->tcm_ifindex = if_idx; + + tc_msg->tcm_handle = 0; + tc_msg->tcm_parent = TC_H_ROOT; // Parent is ROOT + tc_msg->tcm_info = 0; + + // Add qdisc kind attribute (netem) + struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = TCA_KIND; + rta->rta_len = RTA_LENGTH(strlen("netem") + 1); + memcpy(RTA_DATA(rta), "netem", strlen("netem") + 1); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Start with options + struct rtattr* opts = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + opts->rta_type = TCA_OPTIONS; + opts->rta_len = RTA_LENGTH(sizeof(struct tc_netem_qopt)); + + // Set the netem parameters + struct tc_netem_qopt* qopt = (struct tc_netem_qopt*)RTA_DATA(opts); + memset(qopt, 0, sizeof(*qopt)); + qopt->limit = 1000; + qopt->latency = delay; + qopt->loss = loss; + + // Update total length + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(opts->rta_len); + + // If we have a rate specified, add rate information + if (rate_Bps > 0) { + struct rtattr* rate_attr = (struct rtattr*)((char*)opts + RTA_ALIGN(opts->rta_len)); + rate_attr->rta_type = TCA_NETEM_RATE; + rate_attr->rta_len = RTA_LENGTH(sizeof(struct tc_netem_rate)); + + struct tc_netem_rate* rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); + memset(rate, 0, sizeof(*rate)); + rate->rate = (rate_Bps < (1ULL << 32)) ? rate_Bps : ~0U; + + // Update options length + opts->rta_len += RTA_ALIGN(rate_attr->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate_attr->rta_len); + + // Add 64-bit value if needed + if (rate_Bps >= (1ULL << 32)) { + struct rtattr* rate64 = (struct rtattr*)((char*)rate_attr + RTA_ALIGN(rate_attr->rta_len)); + rate64->rta_type = TCA_NETEM_RATE64; + rate64->rta_len = RTA_LENGTH(sizeof(uint64_t)); + *(uint64_t*)RTA_DATA(rate64) = rate_Bps; + + // Update lengths + opts->rta_len += RTA_ALIGN(rate64->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate64->rta_len); + } + } + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[1024]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Delete a network interface using netlink (replaces ip link del) +static int del_link_(const char *if_name, char *err_str, size_t max_len) { + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for deleting the interface + uint8_t buf[512] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + nl_hdr->nlmsg_type = RTM_DELLINK; // Delete link + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // Interface info message + struct ifinfomsg* if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(struct ifinfomsg)); + if_msg->ifi_family = AF_UNSPEC; + if_msg->ifi_index = if_idx; // Specify interface by index + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[512]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Add IP address to interface using netlink (replaces ip addr add) +static int add_addr_(const char *if_name, const char *addr_str, char *err_str, size_t max_len) { + // Parse IP address string + char ip_str[40] = {0}; + int prefix_len = 24; // Default prefix length + + if (sscanf(addr_str, "%39[^/]/%d", ip_str, &prefix_len) < 1) { + snprintf(err_str, max_len, "Invalid IP address format: %s", addr_str); + return -1; + } + + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for adding IP address + uint8_t buf[512] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); + nl_hdr->nlmsg_type = RTM_NEWADDR; // Add address + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_EXCL; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // Address message + struct ifaddrmsg* addr_msg = NLMSG_DATA(nl_hdr); + memset(addr_msg, 0, sizeof(struct ifaddrmsg)); + addr_msg->ifa_family = AF_INET; // IPv4 + addr_msg->ifa_prefixlen = prefix_len; + addr_msg->ifa_flags = IFA_F_PERMANENT; + addr_msg->ifa_scope = RT_SCOPE_UNIVERSE; + addr_msg->ifa_index = if_idx; + + // Add IP address attribute + struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFA_LOCAL; + rta->rta_len = RTA_LENGTH(4); // IPv4 address length + + struct in_addr ip_addr; + if (inet_pton(AF_INET, ip_str, &ip_addr) <= 0) { + snprintf(err_str, max_len, "Invalid IP address: %s", ip_str); + close(sock_fd); + return -1; + } + + memcpy(RTA_DATA(rta), &ip_addr, sizeof(ip_addr)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Add address attribute (same as local for IPv4) + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFA_ADDRESS; + rta->rta_len = RTA_LENGTH(4); + memcpy(RTA_DATA(rta), &ip_addr, sizeof(ip_addr)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[512]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Set interface up using netlink (replaces ip link set up) +static int set_link_up_(const char *if_name, char *err_str, size_t max_len) { + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel assign a unique PID + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for setting interface up + uint8_t buf[512] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + nl_hdr->nlmsg_type = RTM_NEWLINK; // Modify link + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // Interface info message + struct ifinfomsg* if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(struct ifinfomsg)); + if_msg->ifi_family = AF_UNSPEC; + if_msg->ifi_index = if_idx; + if_msg->ifi_change = IFF_UP; // Change UP flag + if_msg->ifi_flags = IFF_UP; // Set UP flag + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // To kernel + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[512]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + return 0; +} + +// Initialize interface (add addr, setup tc, set link up) +static int init_if_(const char *if_name, const char *addr_str, + uint32_t delay, uint32_t loss, + uint64_t rate_Bps, char *err_str, size_t max_len) { + // 1. Add IP address + if (add_addr_(if_name, addr_str, err_str, max_len) != 0) { + return -1; + } + + // 2. Add traffic control qdisc + // Create netlink socket + int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { + snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); + return -1; + } + + // Bind the socket + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + .nl_pid = 0, + .nl_groups = 0 + }; + + if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Get interface index + unsigned int if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + close(sock_fd); + return -1; + } + + // Get our port number + socklen_t addr_len = sizeof(sa); + if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { + snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Generate sequence number + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + // Prepare netlink message for adding qdisc + uint8_t buf[1024] = {0}; + struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); + nl_hdr->nlmsg_type = RTM_NEWQDISC; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = sa.nl_pid; + + // TC message structure + struct tcmsg* tc_msg = NLMSG_DATA(nl_hdr); + tc_msg->tcm_family = AF_UNSPEC; + tc_msg->tcm__pad1 = tc_msg->tcm__pad2 = 0; + tc_msg->tcm_ifindex = if_idx; + tc_msg->tcm_handle = 0; + tc_msg->tcm_parent = TC_H_ROOT; + tc_msg->tcm_info = 0; + + // Add qdisc kind attribute (netem) + struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = TCA_KIND; + rta->rta_len = RTA_LENGTH(strlen("netem") + 1); + memcpy(RTA_DATA(rta), "netem", strlen("netem") + 1); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // Options + struct rtattr* opts = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + opts->rta_type = TCA_OPTIONS; + opts->rta_len = RTA_LENGTH(sizeof(struct tc_netem_qopt)); + + // Set netem parameters + struct tc_netem_qopt* qopt = (struct tc_netem_qopt*)RTA_DATA(opts); + memset(qopt, 0, sizeof(*qopt)); + qopt->limit = 1000; + qopt->latency = delay; + qopt->loss = loss; + + // Update total length + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(opts->rta_len); + + // If we have a rate specified, add rate information + if (rate_Bps > 0) { + struct rtattr* rate_attr = (struct rtattr*)((char*)opts + RTA_ALIGN(opts->rta_len)); + rate_attr->rta_type = TCA_NETEM_RATE; + rate_attr->rta_len = RTA_LENGTH(sizeof(struct tc_netem_rate)); + + struct tc_netem_rate* rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); + memset(rate, 0, sizeof(*rate)); + rate->rate = (rate_Bps < (1ULL << 32)) ? rate_Bps : ~0U; + + // Update options length + opts->rta_len += RTA_ALIGN(rate_attr->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate_attr->rta_len); + + // Add 64-bit value if needed + if (rate_Bps >= (1ULL << 32)) { + struct rtattr* rate64 = (struct rtattr*)((char*)rate_attr + RTA_ALIGN(rate_attr->rta_len)); + rate64->rta_type = TCA_NETEM_RATE64; + rate64->rta_len = RTA_LENGTH(sizeof(uint64_t)); + *(uint64_t*)RTA_DATA(rate64) = rate_Bps; + + // Update lengths + opts->rta_len += RTA_ALIGN(rate64->rta_len); + nl_hdr->nlmsg_len += RTA_ALIGN(rate64->rta_len); + } + } + + // Send message + struct sockaddr_nl dst = { + .nl_family = AF_NETLINK, + .nl_pid = 0, + .nl_groups = 0 + }; + + struct iovec iov = { + .iov_base = nl_hdr, + .iov_len = nl_hdr->nlmsg_len + }; + + struct msghdr msg = { + .msg_name = &dst, + .msg_namelen = sizeof(dst), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + if (sendmsg(sock_fd, &msg, 0) < 0) { + snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Receive response + char resp[1024]; + iov.iov_base = resp; + iov.iov_len = sizeof(resp); + + int ret = recvmsg(sock_fd, &msg, 0); + if (ret < 0) { + snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); + close(sock_fd); + return -1; + } + + // Check for errors + struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; + if (resp_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); + if (err->error) { + snprintf(err_str, max_len, "TC error: %s (%d)", strerror(-err->error), -err->error); + close(sock_fd); + return -1; + } + } + + close(sock_fd); + + // 3. Set interface up + if (set_link_up_(if_name, err_str, max_len) != 0) { + return -1; + } + + return 0; +} + +// Python function to update network interface parameters +static PyObject* pynetlink_update_if(PyObject* self, PyObject* args) { + const char *if_name; + const char *delay_str; + const char *rate_str; + const char *loss_str; + char err[256]; + + // Parse Python arguments + if (!PyArg_ParseTuple(args, "ssss", &if_name, &delay_str, &rate_str, &loss_str)) { + return NULL; + } + + uint32_t delay = (uint32_t)(atof(delay_str) * 15625); + uint32_t loss = (uint32_t)(atof(loss_str) * (~0U/100U)); + double rate_value = 0.0; + char rate_unit[16] = {0}; + uint64_t rate_Bps = 0; + + if (sscanf(rate_str, "%lf%15s", &rate_value, rate_unit) == 2) { + if (strcmp(rate_unit, "Gbit") == 0) { + rate_Bps = (uint64_t)(rate_value * 1000000000 / 8); + } else if (strcmp(rate_unit, "Mbit") == 0) { + rate_Bps = (uint64_t)(rate_value * 1000000 / 8); + } else if (strcmp(rate_unit, "Kbit") == 0) { + rate_Bps = (uint64_t)(rate_value * 1000 / 8); + } else { + rate_Bps = (uint64_t)rate_value; + } + } else { + if (sscanf(rate_str, "%lf", &rate_value) == 1) { + rate_Bps = (uint64_t)rate_value * 1000000000 / 8; + } + } + + // Update netem qdisc + int result = update_netem_(if_name, delay, loss, rate_Bps, err, sizeof(err)); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +// Python function to delete a network interface +static PyObject* pynetlink_del_link(PyObject* self, PyObject* args) { + const char *if_name; + char err[256]; + + if (!PyArg_ParseTuple(args, "s", &if_name)) { + return NULL; + } + + int result = del_link_(if_name, err, sizeof(err)); + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +// Python function to initialize an interface (addr, tc, up) +static PyObject* pynetlink_init_if(PyObject* self, PyObject* args) { + const char *if_name; + const char *addr_str; + const char *delay_str; + const char *rate_str; + const char *loss_str; + char err[256]; + + if (!PyArg_ParseTuple(args, "sssss", &if_name, &addr_str, &delay_str, &rate_str, &loss_str)) { + return NULL; + } + + uint32_t delay = (uint32_t)(atof(delay_str) * 15625); + uint32_t loss = (uint32_t)(atof(loss_str) * (~0U/100U)); + double rate_value = 0.0; + char rate_unit[16] = {0}; + uint64_t rate_Bps = 0; + + if (sscanf(rate_str, "%lf%15s", &rate_value, rate_unit) == 2) { + if (strcmp(rate_unit, "Gbit") == 0) { + rate_Bps = (uint64_t)(rate_value * 1000000000 / 8); + } else if (strcmp(rate_unit, "Mbit") == 0) { + rate_Bps = (uint64_t)(rate_value * 1000000 / 8); + } else if (strcmp(rate_unit, "Kbit") == 0) { + rate_Bps = (uint64_t)(rate_value * 1000 / 8); + } else { + rate_Bps = (uint64_t)rate_value; + } + } else { + if (sscanf(rate_str, "%lf", &rate_value) == 1) { + rate_Bps = (uint64_t)rate_value * 1000000000 / 8; + } + } + + int result = init_if_(if_name, addr_str, delay, loss, rate_Bps, err, sizeof(err)); + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +// Define module methods +static PyMethodDef PyNetlinkMethods[] = { + {"update_if", pynetlink_update_if, METH_VARARGS, "Update network interface parameters using netlink"}, + {"del_link", pynetlink_del_link, METH_VARARGS, "Delete a network interface using netlink"}, + {"init_if", pynetlink_init_if, METH_VARARGS, "Initialize an interface (add addr, setup tc, set up)"}, + {NULL, NULL, 0, NULL} +}; + +// Define module +static struct PyModuleDef pynetlink_module = { + PyModuleDef_HEAD_INIT, + "pynetlink", + "Python extension for efficient network interface updates using netlink", + -1, + PyNetlinkMethods +}; + +// Initialize module +PyMODINIT_FUNC PyInit_pynetlink(void) { + return PyModule_Create(&pynetlink_module); +} + diff --git a/starrynet/sn_controller.py b/starrynet/sn_controller.py index ae5d730..2dfc832 100644 --- a/starrynet/sn_controller.py +++ b/starrynet/sn_controller.py @@ -13,6 +13,8 @@ ASSIGN_FILENAME = 'assign.json' LINK_FILENAME = 'link.json' +RECONF_CHKPT = [12, 27, 42, 57] + class Remote: def __init__(self, id, host, port, username, password): self.id = id @@ -51,13 +53,12 @@ def __init__(self, id, host, port, username, password): def update_network(self, del_links, add_links, update_links): t1 = time.perf_counter() self.sftp.put(LINK_FILENAME, self.dir + '/' + LINK_FILENAME) - t2 = time.perf_counter() sn_remote_wait_output( self.ssh, f"python3 {self.dir}/sn_remote.py networks {self.id} {self.dir} " ) t3 = time.perf_counter() - print(f'[{self.id}]', t1, t2, t3) + print(f'[{self.id}]', t3 - t1, 's') def exec(self, node, cmd): def generate(all_cmd): @@ -89,46 +90,51 @@ def __init__(self, config_path): def run(self): self.last_links = set() - last_t = time.time() while True: - t = time.time() - if t - last_t < self.time_step: - time.sleep(last_t + self.time_step - t) - t = time.time() + dt = datetime.datetime.now() + chkpt = 72 + for i in range(len(RECONF_CHKPT)): + if dt.second <= RECONF_CHKPT[i]: + chkpt = RECONF_CHKPT[i] + break + time.sleep(chkpt - dt.second) + dt = datetime.datetime.now() - print('Time: ', t, '\n') - last_t = t + print('Time: ', dt, '\n') url = (self.api_url - + f'?startTime={datetime.datetime.fromtimestamp(t).isoformat()}' + + f'?startTime={dt.isoformat()}' + f'&constellation={self.constellation}') print(url) - res = requests.get(url) - if res.status_code != 200: - print(f'<{res.status_code}> Failed to fetch nodeInfo, skip.') - continue - with gzip.GzipFile(fileobj=io.BytesIO(res.content), mode='rb') as f: - node_info = json.load(f) - new_links, del_links, add_links, update_links = self._parse(node_info) + try: + res = requests.get(url) + if res.status_code != 200: + print(f'<{res.status_code}> Failed to fetch nodeInfo, skip.') + continue + with gzip.GzipFile(fileobj=io.BytesIO(res.content), mode='rb') as f: + node_info = json.load(f) + new_links, del_links, add_links, update_links = self._parse(node_info) - with open(LINK_FILENAME, 'w') as f: - json.dump( - {'del_links': del_links, 'add_links': add_links, 'update_links': update_links}, - f - ) + with open(LINK_FILENAME, 'w') as f: + json.dump( + {'del_links': del_links, 'add_links': add_links, 'update_links': update_links}, + f + ) - rmt_threads = [] - for rmt in self.remote_lst: - thread = threading.Thread( - target=rmt.update_network, - args=(del_links, add_links, update_links) - ) - thread.start() - rmt_threads.append(thread) + rmt_threads = [] + for rmt in self.remote_lst: + thread = threading.Thread( + target=rmt.update_network, + args=(del_links, add_links, update_links) + ) + thread.start() + rmt_threads.append(thread) - self.last_links = new_links + self.last_links = new_links - for thread in rmt_threads: - thread.join() + for thread in rmt_threads: + thread.join() + except Exception as e: + print('Error:', e) def _parse(self, node_info): EPS = 0.01 diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 2530659..416e2e5 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -2,7 +2,6 @@ import os import subprocess import sys -import re import json import glob import ctypes @@ -81,29 +80,20 @@ def _del_link(name1, name2): fd = os.open('/run/netns/' + name1, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - subprocess.check_call(('ip', 'link', 'del', n1_n2)) + pynetlink.del_link(n1_n2) def _init_if(name, if_name, addr, addr6, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) subprocess.check_call(('ip', 'addr', 'add', addr6, 'dev', if_name)) - subprocess.check_call( - ('tc', 'qdisc', 'add', 'dev', if_name, 'root', - 'netem', 'delay', delay+'ms', 'loss', loss+'%', 'rate', bw+'Gbit') - ) - subprocess.check_call(('ip', 'link', 'set', if_name, 'up')) + pynetlink.init_if(if_name, addr, delay, bw, loss) def _update_if(name, if_name, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - update_loss = '100' if name in damage_set else loss - subprocess.check_call( - ('tc', 'qdisc', 'change', 'dev', if_name, 'root', - 'netem', 'delay', delay + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') - ) + pynetlink.update_if(if_name, delay, bw, loss) def _update_link_intra_machine(name1, name2, delay, bw, loss): n1_n2 = f"{name2}" @@ -140,7 +130,9 @@ def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, dela def sn_init_nodes(dir, shell_num, node_mid_dict): def _load_netns(pid, name): - netns_link = f'/run/netns/{name}' + netns_dir = '/run/netns' + os.makedirs(netns_dir, exist_ok=True) + netns_link = f'{netns_dir}/{name}' if not os.path.exists(netns_link): subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) sn_container_check_call( @@ -456,19 +448,48 @@ def sn_damage(dir, random_list): for node in random_list: pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") pid = pid_mat[node] - _change_sat_link_loss(pid, '100') - f.write(node + '\n') + out = subprocess.check_output( + ('nsenter', '-t', pid, '-n', + 'ip', '-br', 'addr', 'show')).decode() + dev_lst = [] + f.write(node + '|') + for line in out.splitlines(): + line = line.strip() + if len(line) == 0 or line.startswith('lo'): + continue + toks = line.split() + dev_name = toks[0].split('@')[0] + for addr in toks[1:]: + if ':' in addr: + # found first ip6 addr + subprocess.check_call( + ('nsenter', '-t', pid, '-n', + 'ip', 'link', 'set', 'dev', dev_name, 'down',)) + dev_lst.append(f'{dev_name},{addr}') + break + f.write(' '.join(dev_lst) + '\n') print(f'[{machine_id}] damage node: {node}') def sn_recover(dir, sat_loss): damage_file = f"{dir}/{DAMAGE_FILENAME}" if not os.path.exists(damage_file): return + + pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") with open(f"{dir}/{DAMAGE_FILENAME}", 'r') as f: - for node in f: - pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") - pid = pid_mat[node.strip()] - _change_sat_link_loss(pid, sat_loss) + for line in f: + toks = line.strip().split('|') + node = toks[0] + + pid = pid_mat[node] + for link in toks[1].split(): + dev_addr = link.split(',') + subprocess.check_call( + ('nsenter', '-t', pid, '-n', + 'ip', 'link', 'set', 'dev', dev_addr[0], 'up',)) + subprocess.check_call( + ('nsenter', '-t', pid, '-n', + 'ip', 'addr', 'add', 'dev', dev_addr[0], dev_addr[1])) print(f'[{machine_id}] recover sat: {node}') os.remove(damage_file) @@ -516,12 +537,23 @@ def sn_recover(dir, sat_loss): ) import pyctr + try: + import pynetlink + except ModuleNotFoundError: + subprocess.check_call( + "cd " + workdir + " && " + "gcc $(python3-config --cflags --ldflags) " + "-shared -fPIC -O2 pynetlink.c -o pynetlink.so", + shell=True + ) + import pynetlink + damage_set = set() damage_file = workdir + '/' + DAMAGE_FILENAME if os.path.exists(damage_file): with open(workdir + '/' + DAMAGE_FILENAME, 'r') as f: for line in f: - damage_set.add(line.strip()) + damage_set.add(line.strip().split(':')[0]) shell_num, node_mid_dict, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) if cmd == 'nodes': diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 013ea3f..aa8d113 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -39,17 +39,17 @@ area 0 { interface "SH*O*S*" { type broadcast; # Detected by default - cost 256; + cost 10; hello %d; }; interface "GS*" { type broadcast; # Detected by default - cost 256; + cost 10; hello %d; }; interface "POP" { type broadcast; # Detected by default - cost 256; + cost 10; hello %d; }; }; @@ -83,6 +83,10 @@ def __init__(self, id, host, port, username, password, os.path.join(os.path.dirname(__file__), 'pyctr.c'), self.dir + '/pyctr.c' ) + self.sftp.put( + os.path.join(os.path.dirname(__file__), 'pynetlink.c'), + self.dir + '/pynetlink.c' + ) self.sftp.put( os.path.join(self.local_dir, 'bird.conf'), self.dir + '/bird.conf' @@ -227,6 +231,20 @@ def recovery(self, sat_loss): f"{sat_loss}" ) + def exec(self, node, cmd): + sn_remote_wait_output( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py exec {node} {cmd}" + ) + + def print_nodes(self, f): + output = sn_remote_cmd( + self.ssh, + f"python3 {self.dir}/sn_orchestrater.py list" + ) + '\n' + lines = output.splitlines(True) + f.writelines(lines[1:]) + def clean(self): sn_remote_cmd( self.ssh, @@ -591,7 +609,17 @@ def _iperf(real_t, src, dst): src, dst )) self.events.append((t, _iperf, src, dst)) - + + def exec_now(self, node, cmd): + node_map = self.node_map() + machine = node_map[node] + machine.exec(node, cmd) + + def print_all_nodes(self, path): + with open(path, 'w') as f: + for machine in self.remote_lst: + machine.print_nodes(f) + def _event(self, real_t): while len(self.events) > 0 and self.events[-1][0] <= real_t: event = self.events.pop(-1) From 7278beaff23c0f0b755bfa3d9e9c0a99ce8df499 Mon Sep 17 00:00:00 2001 From: ZhifengHan1101 Date: Thu, 11 Dec 2025 13:34:33 +0000 Subject: [PATCH 18/49] Fix: fixed some bugs --- example.py | 10 +++++----- starrynet/sn_orchestrater.py | 4 ---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/example.py b/example.py index f88bf3c..1dd1b6c 100644 --- a/example.py +++ b/example.py @@ -54,7 +54,7 @@ # sn.run_routing_daemon() # run OSPF daemon on selected nodes - # sn.run_routing_daemon(node_lst=['GS1', 'SH1O2S2', 'SH1O2S3', 'SH1O3S3', 'GS1']) + sn.run_routing_daemon(node_lst=['GS0', 'SH1O2S2', 'SH1O2S3', 'SH1O3S3', 'GS1']) sat = 'SH1O1S1' des = 'SH1O1S3' @@ -81,22 +81,22 @@ # run OSPF daemon on selected nodes sn.run_routing_daemon(node_lst=[ - 'GS1', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS2']) + 'GS0', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS1']) - sn.check_routing_table('GS1', 4) + sn.check_routing_table('GS0', 4) ratio = 0.3 time_index = 15 # random damage of a given ratio at a certain time sn.set_damage(ratio, time_index) - sn.check_routing_table('GS1', 30) + sn.check_routing_table('GS0', 30) time_index = 35 # recover the damages at a certain time sn.set_recovery(time_index) - sn.check_routing_table('GS1', 45) + sn.check_routing_table('GS0', 45) sn.start_emulation() diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 8f76be8..96ea83a 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -266,12 +266,8 @@ def sn_update_network( f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', delay, gsl_bw, gsl_loss ) - add_ed = time.time() - add_time = add_ed - add_st print(f"[{machine_id}] GSL:", f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") - print(f"[{machine_id}] GSL time:", - f"{del_time} s for deleted, {update_time} s for updated, {add_time} s for added.") # GSL_TIMING_DIR = 'gsl_timing_all_change' # timing_dir = os.path.join(dir, GSL_TIMING_DIR) From 4d35a87a994c846f5d6d67c23acbe687ae5bbd16 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Thu, 8 Jan 2026 15:24:26 +0800 Subject: [PATCH 19/49] fix: prevent duplicate ISLs in small grid topology sn_observer: modify _isl_grid function to avoid creating duplicate ISLs when dealing with 2xn or nx2 grid topology. --- starrynet/sn_observer.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index 7068ebb..b592bfc 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -74,7 +74,7 @@ def _gs_name(gid): def _isl_grid(sat_cbf_t, shell_id, orbit_num, sat_num): # [[ [isl] for every satellite] for every t] - isls_t = [] + isls_lst_t = [] sat_cbf_t = sat_cbf_t.reshape(-1, orbit_num, sat_num, 3) down_cbf_t = np.roll(sat_cbf_t, -1, 2) @@ -84,7 +84,7 @@ def _isl_grid(sat_cbf_t, shell_id, orbit_num, sat_num): delay_right_t = np.sqrt(np.sum(np.square(sat_cbf_t - right_cbf_t), -1)) / ( 17.31 / 29.5 * 299792.458) * 1000 # ms for delay_down, delay_right in zip(delay_down_t, delay_right_t): - isl_lst = [] + isls_lst = [] for oid in range(orbit_num): for sid in range(sat_num): # down isl @@ -93,14 +93,17 @@ def _isl_grid(sat_cbf_t, shell_id, orbit_num, sat_num): # right isl right_oid = oid + 1 if oid + 1 < orbit_num else 0 right_sid = sid - isl_lst.append([ + + isls = [] + # to avoid duplication at small scale + if sat_num > 2 and down_sid > 0: # (sat_name, delay in ms) - (_sat_name(shell_id, down_oid, down_sid), delay_down[oid, sid]), - # right isl - (_sat_name(shell_id, right_oid, right_sid), delay_right[oid, sid]), - ]) - isls_t.append(isl_lst) - return isls_t + isls.append((_sat_name(shell_id, down_oid, down_sid), delay_down[oid, sid])) + if orbit_num > 2 and right_oid > 0: + isls.append((_sat_name(shell_id, right_oid, right_sid), delay_right[oid, sid])) + isls_lst.append(isls) + isls_lst_t.append(isls_lst) + return isls_lst_t def _topo_walker_delta(dir, duration, step, shell_lst): ts_total = int(duration / step) From 4f1f81d31af468b322631cd6384de347bf017d97 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Tue, 3 Feb 2026 21:50:03 +0800 Subject: [PATCH 20/49] feat: enhance and refactor pynetlink with link creation, routes modification and sock reuse --- starrynet/pynetlink.c | 1474 +++++++++++++++++++--------------- starrynet/sn_orchestrater.py | 23 +- 2 files changed, 835 insertions(+), 662 deletions(-) diff --git a/starrynet/pynetlink.c b/starrynet/pynetlink.c index bf92e98..3442039 100644 --- a/starrynet/pynetlink.c +++ b/starrynet/pynetlink.c @@ -10,835 +10,1007 @@ #include #include #include +#include // std C +#include #include #include #include #include #include +#define NETNS_DIR "/var/run/netns" +#define VXLAN_PORT 4789 -// Update netem qdisc using netlink -static int update_netem_( - const char *if_name, uint32_t delay, uint32_t loss, - uint64_t rate_Bps, char *err_str, size_t max_len) -{ - // Get interface index - unsigned int if_idx = if_nametoindex(if_name); - if (if_idx == 0) { - snprintf(err_str, max_len, "Interface not found: %s", if_name); - return -1; - } +// Missing constants for veth and vxlan +#ifndef VETH_INFO_PEER +#define VETH_INFO_PEER 1 +#endif - // Create netlink socket - int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (sock_fd < 0) { - snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); - return -1; - } - - // Bind the socket to a random dynamic port - struct sockaddr_nl sa = { +#ifndef IFLA_VXLAN_ID +#define IFLA_VXLAN_ID 1 +#endif + +#ifndef IFLA_VXLAN_REMOTE +#define IFLA_VXLAN_REMOTE 5 +#endif + +#ifndef IFLA_VXLAN_PORT +#define IFLA_VXLAN_PORT 7 +#endif + +static int init_rtnetlink_sock_() { + struct sockaddr_nl sa_nl = { .nl_family = AF_NETLINK, - .nl_pid = 0, // Let kernel assign a unique PID + .nl_pid = 0, .nl_groups = 0 }; - - if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { - snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); - close(sock_fd); + int sock_fd; + + sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (sock_fd < 0) { return -1; } - // Get our port number - socklen_t addr_len = sizeof(sa); - if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { - snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); + if (bind(sock_fd, (struct sockaddr*)&sa_nl, sizeof(sa_nl)) < 0) { close(sock_fd); return -1; } - - // Generate a unique sequence number based on time + + return sock_fd; +} + +static int rtnetlink_request(int sock_fd, struct nlmsghdr* nl_hdr, size_t buf_len, + char *err_str, size_t err_len) { + int ret; + + if(sendto(sock_fd, nl_hdr, nl_hdr->nlmsg_len, 0, NULL, 0) < 0) { + snprintf(err_str, err_len, "Failed to send netlink message: %s", strerror(errno)); + return -1; + } + + ret = recvfrom(sock_fd, nl_hdr, buf_len, 0, NULL, NULL); + if (ret < 0) { + snprintf(err_str, err_len, "Failed to receive netlink response: %s", strerror(errno)); + return -1; + } + + // Check for errors + if (nl_hdr->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(nl_hdr); + if (err->error) { + snprintf(err_str, err_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); + return -1; + } + } + + return 0; +} + +static int netem_update_or_create_( + int sock_fd, const char *if_name, uint32_t delay, uint32_t loss, + uint64_t rate_Bps, char *err_str, size_t max_len) { + unsigned int if_idx; struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct tcmsg* tc_msg; + struct rtattr* rta; + struct tc_netem_qopt* qopt; + uint8_t buf[1024]; + + if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } + clock_gettime(CLOCK_MONOTONIC, &ts); - uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; // Use milliseconds as sequence + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; - // netlink msg buffer - uint8_t buf[1024]; - struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; - nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*tc_msg)); nl_hdr->nlmsg_type = RTM_NEWQDISC; nl_hdr->nlmsg_seq = seq; - - // For existing qdisc change, not add - avoid "File exists" error - nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE; - - // Use our assigned PID - nl_hdr->nlmsg_pid = sa.nl_pid; - - // TC message structure - struct tcmsg* tc_msg = NLMSG_DATA(nl_hdr); + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE; + nl_hdr->nlmsg_pid = 0; + + tc_msg = NLMSG_DATA(nl_hdr); tc_msg->tcm_family = AF_UNSPEC; tc_msg->tcm__pad1 = tc_msg->tcm__pad2 = 0; tc_msg->tcm_ifindex = if_idx; - tc_msg->tcm_handle = 0; - tc_msg->tcm_parent = TC_H_ROOT; // Parent is ROOT + tc_msg->tcm_parent = TC_H_ROOT; // root tc_msg->tcm_info = 0; - // Add qdisc kind attribute (netem) - struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); rta->rta_type = TCA_KIND; rta->rta_len = RTA_LENGTH(strlen("netem") + 1); memcpy(RTA_DATA(rta), "netem", strlen("netem") + 1); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - // Start with options - struct rtattr* opts = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); - opts->rta_type = TCA_OPTIONS; - opts->rta_len = RTA_LENGTH(sizeof(struct tc_netem_qopt)); - - // Set the netem parameters - struct tc_netem_qopt* qopt = (struct tc_netem_qopt*)RTA_DATA(opts); + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = TCA_OPTIONS; + rta->rta_len = RTA_LENGTH(sizeof(*qopt)); + qopt = (struct tc_netem_qopt*)RTA_DATA(rta); memset(qopt, 0, sizeof(*qopt)); qopt->limit = 1000; qopt->latency = delay; qopt->loss = loss; - - // Update total length - nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(opts->rta_len); - // If we have a rate specified, add rate information if (rate_Bps > 0) { - struct rtattr* rate_attr = (struct rtattr*)((char*)opts + RTA_ALIGN(opts->rta_len)); + struct rtattr* rate_attr; + struct tc_netem_rate* rate; + + rate_attr = (struct rtattr*)((char*)rta + RTA_ALIGN(rta->rta_len)); rate_attr->rta_type = TCA_NETEM_RATE; rate_attr->rta_len = RTA_LENGTH(sizeof(struct tc_netem_rate)); - - struct tc_netem_rate* rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); + rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); memset(rate, 0, sizeof(*rate)); - rate->rate = (rate_Bps < (1ULL << 32)) ? rate_Bps : ~0U; - - // Update options length - opts->rta_len += RTA_ALIGN(rate_attr->rta_len); - nl_hdr->nlmsg_len += RTA_ALIGN(rate_attr->rta_len); - // Add 64-bit value if needed + rta->rta_len += RTA_ALIGN(rate_attr->rta_len); + if (rate_Bps >= (1ULL << 32)) { + rate->rate = ~0U; struct rtattr* rate64 = (struct rtattr*)((char*)rate_attr + RTA_ALIGN(rate_attr->rta_len)); rate64->rta_type = TCA_NETEM_RATE64; rate64->rta_len = RTA_LENGTH(sizeof(uint64_t)); *(uint64_t*)RTA_DATA(rate64) = rate_Bps; - // Update lengths - opts->rta_len += RTA_ALIGN(rate64->rta_len); - nl_hdr->nlmsg_len += RTA_ALIGN(rate64->rta_len); + rta->rta_len += RTA_ALIGN(rate64->rta_len); + } else { + rate->rate = rate_Bps; } } + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - // Send message - struct sockaddr_nl dst = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // To kernel - .nl_groups = 0 - }; - - struct iovec iov = { - .iov_base = nl_hdr, - .iov_len = nl_hdr->nlmsg_len - }; - - struct msghdr msg = { - .msg_name = &dst, - .msg_namelen = sizeof(dst), - .msg_iov = &iov, - .msg_iovlen = 1 - }; - - if (sendmsg(sock_fd, &msg, 0) < 0) { - snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Receive response - char resp[1024]; - iov.iov_base = resp; - iov.iov_len = sizeof(resp); - - int ret = recvmsg(sock_fd, &msg, 0); - if (ret < 0) { - snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Check for errors - struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; - if (resp_hdr->nlmsg_type == NLMSG_ERROR) { - struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); - if (err->error) { - snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); - close(sock_fd); - return -1; - } - } - - close(sock_fd); - return 0; + return rtnetlink_request(sock_fd, nl_hdr, sizeof(buf), err_str, max_len); } -// Delete a network interface using netlink (replaces ip link del) -static int del_link_(const char *if_name, char *err_str, size_t max_len) { - // Get interface index - unsigned int if_idx = if_nametoindex(if_name); +static int modify_addr4_(int sock_fd, uint16_t nlmsg_type, const char *if_name, + const struct in_addr *addr4, unsigned prefix_len, char *err_str, size_t max_len) { + unsigned int if_idx; + struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct ifaddrmsg* addr_msg; + struct rtattr* rta; + uint8_t buf[512]; + + if_idx = if_nametoindex(if_name); if (if_idx == 0) { snprintf(err_str, max_len, "Interface not found: %s", if_name); return -1; } - // Create netlink socket - int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (sock_fd < 0) { - snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); - return -1; - } - - // Bind the socket - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // Let kernel assign a unique PID - .nl_groups = 0 - }; - - if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { - snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Get our port number - socklen_t addr_len = sizeof(sa); - if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { - snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Generate sequence number - struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); - uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; - // Prepare netlink message for deleting the interface - uint8_t buf[512] = {0}; - struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; - nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); - nl_hdr->nlmsg_type = RTM_DELLINK; // Delete link - nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*addr_msg)); + nl_hdr->nlmsg_type = RTM_NEWADDR; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_EXCL; nl_hdr->nlmsg_seq = seq; - nl_hdr->nlmsg_pid = sa.nl_pid; + nl_hdr->nlmsg_pid = 0; - // Interface info message - struct ifinfomsg* if_msg = NLMSG_DATA(nl_hdr); - memset(if_msg, 0, sizeof(struct ifinfomsg)); - if_msg->ifi_family = AF_UNSPEC; - if_msg->ifi_index = if_idx; // Specify interface by index + addr_msg = NLMSG_DATA(nl_hdr); + memset(addr_msg, 0, sizeof(*addr_msg)); + addr_msg->ifa_family = AF_INET; + addr_msg->ifa_prefixlen = prefix_len; + addr_msg->ifa_flags = IFA_F_PERMANENT; + addr_msg->ifa_scope = RT_SCOPE_UNIVERSE; + addr_msg->ifa_index = if_idx; - // Send message - struct sockaddr_nl dst = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // To kernel - .nl_groups = 0 - }; - - struct iovec iov = { - .iov_base = nl_hdr, - .iov_len = nl_hdr->nlmsg_len - }; - - struct msghdr msg = { - .msg_name = &dst, - .msg_namelen = sizeof(dst), - .msg_iov = &iov, - .msg_iovlen = 1 - }; - - if (sendmsg(sock_fd, &msg, 0) < 0) { - snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); - close(sock_fd); - return -1; - } + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFA_LOCAL; + rta->rta_len = RTA_LENGTH(sizeof(*addr4)); + memcpy(RTA_DATA(rta), addr4, sizeof(*addr4)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - // Receive response - char resp[512]; - iov.iov_base = resp; - iov.iov_len = sizeof(resp); - - int ret = recvmsg(sock_fd, &msg, 0); - if (ret < 0) { - snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); - close(sock_fd); - return -1; - } + // Add address attribute (same as local for IPv4) + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFA_ADDRESS; + rta->rta_len = RTA_LENGTH(sizeof(*addr4)); + memcpy(RTA_DATA(rta), addr4, sizeof(*addr4)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - // Check for errors - struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; - if (resp_hdr->nlmsg_type == NLMSG_ERROR) { - struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); - if (err->error) { - snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); - close(sock_fd); - return -1; - } - } - - close(sock_fd); - return 0; + return rtnetlink_request(sock_fd, nl_hdr, sizeof(buf), err_str, max_len); } -// Add IP address to interface using netlink (replaces ip addr add) -static int add_addr_(const char *if_name, const char *addr_str, char *err_str, size_t max_len) { - // Parse IP address string - char ip_str[40] = {0}; - int prefix_len = 24; // Default prefix length - - if (sscanf(addr_str, "%39[^/]/%d", ip_str, &prefix_len) < 1) { - snprintf(err_str, max_len, "Invalid IP address format: %s", addr_str); - return -1; - } - - // Get interface index - unsigned int if_idx = if_nametoindex(if_name); +static int modify_addr6_(int sock_fd, uint16_t nlmsg_type, const char *if_name, + const struct in6_addr *addr6, unsigned prefix_len, char *err_str, size_t max_len) { + unsigned int if_idx; + struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct ifaddrmsg* addr_msg; + struct rtattr* rta; + uint8_t buf[512]; + + if_idx = if_nametoindex(if_name); if (if_idx == 0) { snprintf(err_str, max_len, "Interface not found: %s", if_name); return -1; } - - // Create netlink socket - int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (sock_fd < 0) { - snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); - return -1; - } - - // Bind the socket - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // Let kernel assign a unique PID - .nl_groups = 0 - }; - - if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { - snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Get our port number - socklen_t addr_len = sizeof(sa); - if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { - snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Generate sequence number - struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); - uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; - - // Prepare netlink message for adding IP address - uint8_t buf[512] = {0}; - struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; - nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); - nl_hdr->nlmsg_type = RTM_NEWADDR; // Add address + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*addr_msg)); + nl_hdr->nlmsg_type = nlmsg_type; nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_EXCL; nl_hdr->nlmsg_seq = seq; - nl_hdr->nlmsg_pid = sa.nl_pid; - - // Address message - struct ifaddrmsg* addr_msg = NLMSG_DATA(nl_hdr); - memset(addr_msg, 0, sizeof(struct ifaddrmsg)); - addr_msg->ifa_family = AF_INET; // IPv4 + nl_hdr->nlmsg_pid = 0; + + addr_msg = NLMSG_DATA(nl_hdr); + memset(addr_msg, 0, sizeof(*addr_msg)); + addr_msg->ifa_family = AF_INET6; addr_msg->ifa_prefixlen = prefix_len; addr_msg->ifa_flags = IFA_F_PERMANENT; addr_msg->ifa_scope = RT_SCOPE_UNIVERSE; addr_msg->ifa_index = if_idx; - - // Add IP address attribute - struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); rta->rta_type = IFA_LOCAL; - rta->rta_len = RTA_LENGTH(4); // IPv4 address length - - struct in_addr ip_addr; - if (inet_pton(AF_INET, ip_str, &ip_addr) <= 0) { - snprintf(err_str, max_len, "Invalid IP address: %s", ip_str); - close(sock_fd); - return -1; - } - - memcpy(RTA_DATA(rta), &ip_addr, sizeof(ip_addr)); + rta->rta_len = RTA_LENGTH(sizeof(*addr6)); + memcpy(RTA_DATA(rta), addr6, sizeof(*addr6)); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - - // Add address attribute (same as local for IPv4) + + // Add address attribute (same as local for IPv6) rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); rta->rta_type = IFA_ADDRESS; - rta->rta_len = RTA_LENGTH(4); - memcpy(RTA_DATA(rta), &ip_addr, sizeof(ip_addr)); + rta->rta_len = RTA_LENGTH(sizeof(*addr6)); + memcpy(RTA_DATA(rta), addr6, sizeof(*addr6)); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - - // Send message - struct sockaddr_nl dst = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // To kernel - .nl_groups = 0 - }; - - struct iovec iov = { - .iov_base = nl_hdr, - .iov_len = nl_hdr->nlmsg_len - }; - - struct msghdr msg = { - .msg_name = &dst, - .msg_namelen = sizeof(dst), - .msg_iov = &iov, - .msg_iovlen = 1 - }; - - if (sendmsg(sock_fd, &msg, 0) < 0) { - snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Receive response - char resp[512]; - iov.iov_base = resp; - iov.iov_len = sizeof(resp); - - int ret = recvmsg(sock_fd, &msg, 0); - if (ret < 0) { - snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Check for errors - struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; - if (resp_hdr->nlmsg_type == NLMSG_ERROR) { - struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); - if (err->error) { - snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); - close(sock_fd); - return -1; - } - } - - close(sock_fd); - return 0; + + return rtnetlink_request(sock_fd, nl_hdr, sizeof(buf), err_str, max_len); } -// Set interface up using netlink (replaces ip link set up) -static int set_link_up_(const char *if_name, char *err_str, size_t max_len) { - // Get interface index - unsigned int if_idx = if_nametoindex(if_name); +static int modify_link_(int sock_fd, const char *if_name, uint16_t nlmsg_type, + unsigned ifi_flags, unsigned ifi_change, char *err_str, size_t max_len) { + unsigned int if_idx; + struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct ifinfomsg* if_msg; + uint8_t buf[512]; + + if_idx = if_nametoindex(if_name); if (if_idx == 0) { snprintf(err_str, max_len, "Interface not found: %s", if_name); return -1; } - - // Create netlink socket - int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (sock_fd < 0) { - snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); - return -1; - } - - // Bind the socket - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // Let kernel assign a unique PID - .nl_groups = 0 - }; - - if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { - snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Get our port number - socklen_t addr_len = sizeof(sa); - if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { - snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Generate sequence number - struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); - uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; - - // Prepare netlink message for setting interface up - uint8_t buf[512] = {0}; - struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; - nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); - nl_hdr->nlmsg_type = RTM_NEWLINK; // Modify link + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*if_msg)); + nl_hdr->nlmsg_type = nlmsg_type; nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nl_hdr->nlmsg_seq = seq; - nl_hdr->nlmsg_pid = sa.nl_pid; - + nl_hdr->nlmsg_pid = 0; + // Interface info message - struct ifinfomsg* if_msg = NLMSG_DATA(nl_hdr); - memset(if_msg, 0, sizeof(struct ifinfomsg)); + if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(*if_msg)); if_msg->ifi_family = AF_UNSPEC; if_msg->ifi_index = if_idx; - if_msg->ifi_change = IFF_UP; // Change UP flag - if_msg->ifi_flags = IFF_UP; // Set UP flag - - // Send message - struct sockaddr_nl dst = { - .nl_family = AF_NETLINK, - .nl_pid = 0, // To kernel - .nl_groups = 0 - }; - - struct iovec iov = { - .iov_base = nl_hdr, - .iov_len = nl_hdr->nlmsg_len - }; - - struct msghdr msg = { - .msg_name = &dst, - .msg_namelen = sizeof(dst), - .msg_iov = &iov, - .msg_iovlen = 1 - }; - - if (sendmsg(sock_fd, &msg, 0) < 0) { - snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); - close(sock_fd); + if_msg->ifi_change = ifi_flags; + if_msg->ifi_flags = ifi_change; + + return rtnetlink_request(sock_fd, nl_hdr, sizeof(buf), err_str, max_len); +} + +static int add_link_veth_(int sock_fd, const char *if_name, const char *peer_name, + const char *netns, const char *netns_peer, char *err_str, size_t max_len) { + struct timespec ts; + uint32_t seq; + struct nlmsghdr *nl_hdr; + struct ifinfomsg *if_msg, *peer_ifi; + struct rtattr *rta, *nest, *peer_nest; + int netns_fd1, netns_fd2; + uint8_t buf[1024]; + char netns_path[256]; + + snprintf(netns_path, sizeof(netns_path), NETNS_DIR"/%s", netns); + netns_fd1 = open(netns_path, O_RDONLY); + if (netns_fd1 < 0) { + snprintf(err_str, max_len, "Failed to open netns: %s", netns); return -1; } - - // Receive response - char resp[512]; - iov.iov_base = resp; - iov.iov_len = sizeof(resp); - - int ret = recvmsg(sock_fd, &msg, 0); - if (ret < 0) { - snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); - close(sock_fd); + + snprintf(netns_path, sizeof(netns_path), NETNS_DIR"/%s", netns_peer); + netns_fd2 = open(netns_path, O_RDONLY); + if (netns_fd2 < 0) { + snprintf(err_str, max_len, "Failed to open netns: %s", netns_peer); + close(netns_fd1); return -1; } - - // Check for errors - struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; - if (resp_hdr->nlmsg_type == NLMSG_ERROR) { - struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); - if (err->error) { - snprintf(err_str, max_len, "Netlink error: %s (%d)", strerror(-err->error), -err->error); - close(sock_fd); - return -1; + + clock_gettime(CLOCK_MONOTONIC, &ts); + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*if_msg)); + nl_hdr->nlmsg_type = RTM_NEWLINK; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = 0; + + if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(*if_msg)); + if_msg->ifi_family = AF_UNSPEC; + + // netns + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFLA_NET_NS_FD; + rta->rta_len = RTA_LENGTH(sizeof(netns_fd1)); + memcpy(RTA_DATA(rta), &netns_fd1, sizeof(netns_fd1)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + // if name + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFLA_IFNAME; + rta->rta_len = RTA_LENGTH(strlen(if_name)); + memcpy(RTA_DATA(rta), if_name, strlen(if_name)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFLA_LINKINFO; + rta->rta_len = RTA_LENGTH(0); + { + // kind: veth + nest = (struct rtattr*)RTA_DATA(rta); + nest->rta_type = IFLA_INFO_KIND; + nest->rta_len = RTA_LENGTH(strlen("veth")); + memcpy(RTA_DATA(nest), "veth", strlen("veth")); + rta->rta_len += RTA_ALIGN(nest->rta_len); + + nest = (struct rtattr*)((char*)nest + RTA_ALIGN(nest->rta_len)); + nest->rta_type = IFLA_INFO_DATA; + nest->rta_len = RTA_LENGTH(0); + { + // peer ifi + peer_nest = (struct rtattr*)RTA_DATA(nest); + peer_nest->rta_type = VETH_INFO_PEER; + peer_nest->rta_len = RTA_LENGTH(sizeof(*peer_ifi)); + peer_ifi = (struct ifinfomsg*)RTA_DATA(peer_nest); + memset(peer_ifi, 0, sizeof(*peer_ifi)); + peer_ifi->ifi_family = AF_UNSPEC; + nest->rta_len += RTA_ALIGN(peer_nest->rta_len); + + // peer if name + peer_nest = (struct rtattr*)((char*)peer_nest + RTA_ALIGN(peer_nest->rta_len)); + peer_nest->rta_type = IFLA_IFNAME; + peer_nest->rta_len = RTA_LENGTH(strlen(peer_name)); + memcpy(RTA_DATA(peer_nest), peer_name, strlen(peer_name)); + nest->rta_len += RTA_ALIGN(peer_nest->rta_len); + + // peer netns + peer_nest = (struct rtattr*)((char*)peer_nest + RTA_ALIGN(peer_nest->rta_len)); + peer_nest->rta_type = IFLA_NET_NS_FD; + peer_nest->rta_len = RTA_LENGTH(sizeof(netns_fd2)); + memcpy(RTA_DATA(peer_nest), &netns_fd2, sizeof(netns_fd2)); + nest->rta_len += RTA_ALIGN(peer_nest->rta_len); } + rta->rta_len += RTA_ALIGN(nest->rta_len); } - - close(sock_fd); - return 0; + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + close(netns_fd1); + close(netns_fd2); + + return rtnetlink_request(sock_fd, nl_hdr, nl_hdr->nlmsg_len, err_str, max_len); } -// Initialize interface (add addr, setup tc, set link up) -static int init_if_(const char *if_name, const char *addr_str, - uint32_t delay, uint32_t loss, - uint64_t rate_Bps, char *err_str, size_t max_len) { - // 1. Add IP address - if (add_addr_(if_name, addr_str, err_str, max_len) != 0) { - return -1; - } - - // 2. Add traffic control qdisc - // Create netlink socket - int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (sock_fd < 0) { - snprintf(err_str, max_len, "Failed to open netlink socket: %s", strerror(errno)); - return -1; - } - - // Bind the socket - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - .nl_pid = 0, - .nl_groups = 0 - }; - - if (bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { - snprintf(err_str, max_len, "Failed to bind netlink socket: %s", strerror(errno)); - close(sock_fd); - return -1; - } - - // Get interface index - unsigned int if_idx = if_nametoindex(if_name); - if (if_idx == 0) { - snprintf(err_str, max_len, "Interface not found: %s", if_name); - close(sock_fd); - return -1; - } - - // Get our port number - socklen_t addr_len = sizeof(sa); - if (getsockname(sock_fd, (struct sockaddr*)&sa, &addr_len) < 0) { - snprintf(err_str, max_len, "Failed to get socket name: %s", strerror(errno)); - close(sock_fd); +static int add_link_vxlan_(int sock_fd, const char *if_name, int vxlan_id, + const char *remote_ip, const char *netns_name, char *err_str, size_t max_len) { + struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct ifinfomsg* if_msg; + struct rtattr *rta, *nest, *vxlan_nest; + int netns_fd; + uint16_t dstport; + uint8_t buf[1024]; + + // Open network namespace file descriptor + char netns_path[256]; + snprintf(netns_path, sizeof(netns_path), NETNS_DIR"/%s", netns_name); + netns_fd = open(netns_path, O_RDONLY); + if (netns_fd < 0) { + snprintf(err_str, max_len, "Failed to open netns: %s", netns_name); return -1; } - - // Generate sequence number - struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); - uint32_t seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; - - // Prepare netlink message for adding qdisc - uint8_t buf[1024] = {0}; - struct nlmsghdr* nl_hdr = (struct nlmsghdr*)buf; - nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)); - nl_hdr->nlmsg_type = RTM_NEWQDISC; + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*if_msg)); + nl_hdr->nlmsg_type = RTM_NEWLINK; nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; nl_hdr->nlmsg_seq = seq; - nl_hdr->nlmsg_pid = sa.nl_pid; - - // TC message structure - struct tcmsg* tc_msg = NLMSG_DATA(nl_hdr); - tc_msg->tcm_family = AF_UNSPEC; - tc_msg->tcm__pad1 = tc_msg->tcm__pad2 = 0; - tc_msg->tcm_ifindex = if_idx; - tc_msg->tcm_handle = 0; - tc_msg->tcm_parent = TC_H_ROOT; - tc_msg->tcm_info = 0; - - // Add qdisc kind attribute (netem) - struct rtattr* rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); - rta->rta_type = TCA_KIND; - rta->rta_len = RTA_LENGTH(strlen("netem") + 1); - memcpy(RTA_DATA(rta), "netem", strlen("netem") + 1); + nl_hdr->nlmsg_pid = 0; + + if_msg = NLMSG_DATA(nl_hdr); + memset(if_msg, 0, sizeof(*if_msg)); + if_msg->ifi_family = AF_UNSPEC; + + // netns + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFLA_NET_NS_FD; + rta->rta_len = RTA_LENGTH(sizeof(netns_fd)); + memcpy(RTA_DATA(rta), &netns_fd, sizeof(netns_fd)); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - - // Options - struct rtattr* opts = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); - opts->rta_type = TCA_OPTIONS; - opts->rta_len = RTA_LENGTH(sizeof(struct tc_netem_qopt)); - - // Set netem parameters - struct tc_netem_qopt* qopt = (struct tc_netem_qopt*)RTA_DATA(opts); - memset(qopt, 0, sizeof(*qopt)); - qopt->limit = 1000; - qopt->latency = delay; - qopt->loss = loss; - - // Update total length - nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(opts->rta_len); - - // If we have a rate specified, add rate information - if (rate_Bps > 0) { - struct rtattr* rate_attr = (struct rtattr*)((char*)opts + RTA_ALIGN(opts->rta_len)); - rate_attr->rta_type = TCA_NETEM_RATE; - rate_attr->rta_len = RTA_LENGTH(sizeof(struct tc_netem_rate)); - - struct tc_netem_rate* rate = (struct tc_netem_rate*)RTA_DATA(rate_attr); - memset(rate, 0, sizeof(*rate)); - rate->rate = (rate_Bps < (1ULL << 32)) ? rate_Bps : ~0U; - - // Update options length - opts->rta_len += RTA_ALIGN(rate_attr->rta_len); - nl_hdr->nlmsg_len += RTA_ALIGN(rate_attr->rta_len); - - // Add 64-bit value if needed - if (rate_Bps >= (1ULL << 32)) { - struct rtattr* rate64 = (struct rtattr*)((char*)rate_attr + RTA_ALIGN(rate_attr->rta_len)); - rate64->rta_type = TCA_NETEM_RATE64; - rate64->rta_len = RTA_LENGTH(sizeof(uint64_t)); - *(uint64_t*)RTA_DATA(rate64) = rate_Bps; - - // Update lengths - opts->rta_len += RTA_ALIGN(rate64->rta_len); - nl_hdr->nlmsg_len += RTA_ALIGN(rate64->rta_len); + + // if name + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFLA_IFNAME; + rta->rta_len = RTA_LENGTH(strlen(if_name)); + memcpy(RTA_DATA(rta), if_name, strlen(if_name)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = IFLA_LINKINFO; + rta->rta_len = RTA_LENGTH(0); + { + // kind: vxlan + nest = (struct rtattr*)RTA_DATA(rta); + nest->rta_type = IFLA_INFO_KIND; + nest->rta_len = RTA_LENGTH(strlen("vxlan")); + memcpy(RTA_DATA(nest), "vxlan", strlen("vxlan")); + rta->rta_len += RTA_ALIGN(nest->rta_len); + + nest = (struct rtattr*)((char*)rta + RTA_ALIGN(rta->rta_len)); + nest->rta_type = IFLA_INFO_DATA; + nest->rta_len = RTA_LENGTH(0); + { + // vxlan id + vxlan_nest = (struct rtattr*)RTA_DATA(nest); + vxlan_nest->rta_type = IFLA_VXLAN_ID; + vxlan_nest->rta_len = RTA_LENGTH(sizeof(vxlan_id)); + memcpy(RTA_DATA(vxlan_nest), &vxlan_id, sizeof(vxlan_id)); + nest->rta_len += RTA_ALIGN(vxlan_nest->rta_len); + + // Add remote IP + vxlan_nest = (struct rtattr*)((char*)vxlan_nest + RTA_ALIGN(vxlan_nest->rta_len)); + vxlan_nest->rta_type = IFLA_VXLAN_REMOTE; + vxlan_nest->rta_len = RTA_LENGTH(strlen(remote_ip)); + memcpy(RTA_DATA(vxlan_nest), remote_ip, strlen(remote_ip)); + nest->rta_len += RTA_ALIGN(vxlan_nest->rta_len); + + // Add destination port + dstport = htons(VXLAN_PORT); + vxlan_nest = (struct rtattr*)((char*)vxlan_nest + RTA_ALIGN(vxlan_nest->rta_len)); + vxlan_nest->rta_type = IFLA_VXLAN_PORT; + vxlan_nest->rta_len = RTA_LENGTH(sizeof(dstport)); + memcpy(RTA_DATA(vxlan_nest), &dstport, sizeof(dstport)); + nest->rta_len += RTA_ALIGN(vxlan_nest->rta_len); } + rta->rta_len += RTA_ALIGN(nest->rta_len); } - - // Send message - struct sockaddr_nl dst = { - .nl_family = AF_NETLINK, - .nl_pid = 0, - .nl_groups = 0 - }; - - struct iovec iov = { - .iov_base = nl_hdr, - .iov_len = nl_hdr->nlmsg_len - }; - - struct msghdr msg = { - .msg_name = &dst, - .msg_namelen = sizeof(dst), - .msg_iov = &iov, - .msg_iovlen = 1 - }; - - if (sendmsg(sock_fd, &msg, 0) < 0) { - snprintf(err_str, max_len, "Failed to send netlink message: %s", strerror(errno)); - close(sock_fd); - return -1; + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + + close(netns_fd); + + return rtnetlink_request(sock_fd, nl_hdr, nl_hdr->nlmsg_len, err_str, max_len); +} + +static int modify_route4(int sock_fd, uint16_t op, const struct in_addr *dst4, unsigned dst_prefix, + const struct in_addr *gw4, const char *if_name, int metric, char *err_str, size_t max_len) { + unsigned int if_idx = 0; + struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct rtmsg* rt_msg; + struct rtattr* rta; + struct in_addr dst_addr, gw_addr; + uint8_t buf[512]; + + if(if_name) { + if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); + return -1; + } } - - // Receive response - char resp[1024]; - iov.iov_base = resp; - iov.iov_len = sizeof(resp); - - int ret = recvmsg(sock_fd, &msg, 0); - if (ret < 0) { - snprintf(err_str, max_len, "Failed to receive netlink response: %s", strerror(errno)); - close(sock_fd); - return -1; + + clock_gettime(CLOCK_MONOTONIC, &ts); + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nl_hdr->nlmsg_type = op; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = 0; + + rt_msg = NLMSG_DATA(nl_hdr); + memset(rt_msg, 0, sizeof(struct rtmsg)); + rt_msg->rtm_family = AF_INET; + rt_msg->rtm_dst_len = dst_prefix; + rt_msg->rtm_src_len = 0; + rt_msg->rtm_tos = 0; + rt_msg->rtm_table = RT_TABLE_MAIN; + rt_msg->rtm_protocol = RTPROT_STATIC; + rt_msg->rtm_scope = RT_SCOPE_UNIVERSE; + rt_msg->rtm_type = RTN_UNICAST; + rt_msg->rtm_flags = 0; + + if (dst4) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_DST; + rta->rta_len = RTA_LENGTH(sizeof(*dst4)); + memcpy(RTA_DATA(rta), dst4, sizeof(*dst4)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); } - - // Check for errors - struct nlmsghdr* resp_hdr = (struct nlmsghdr*)resp; - if (resp_hdr->nlmsg_type == NLMSG_ERROR) { - struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(resp_hdr); - if (err->error) { - snprintf(err_str, max_len, "TC error: %s (%d)", strerror(-err->error), -err->error); - close(sock_fd); + + if (gw4) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_GATEWAY; + rta->rta_len = RTA_LENGTH(sizeof(*gw4)); + memcpy(RTA_DATA(rta), gw4, sizeof(*gw4)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + if (if_idx > 0) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_OIF; + rta->rta_len = RTA_LENGTH(sizeof(if_idx)); + memcpy(RTA_DATA(rta), &if_idx, sizeof(if_idx)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + if (metric > 0) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_PRIORITY; + rta->rta_len = RTA_LENGTH(sizeof(metric)); + memcpy(RTA_DATA(rta), &metric, sizeof(metric)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + return rtnetlink_request(sock_fd, nl_hdr, nl_hdr->nlmsg_len, err_str, max_len); +} + +static int modify_route6(int sock_fd, uint16_t op, const struct in6_addr *dst6, unsigned dst_prefix, + const struct in6_addr *gw6, const char *if_name, int metric, char *err_str, size_t max_len) { + unsigned int if_idx = 0; + struct timespec ts; + uint32_t seq; + struct nlmsghdr* nl_hdr; + struct rtmsg* rt_msg; + struct rtattr* rta; + struct in6_addr dst_addr, gw_addr; + uint8_t buf[512]; + + if(if_name) { + if_idx = if_nametoindex(if_name); + if (if_idx == 0) { + snprintf(err_str, max_len, "Interface not found: %s", if_name); return -1; } } - + + clock_gettime(CLOCK_MONOTONIC, &ts); + seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + + nl_hdr = (struct nlmsghdr*)buf; + nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nl_hdr->nlmsg_type = op; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_seq = seq; + nl_hdr->nlmsg_pid = 0; + + rt_msg = NLMSG_DATA(nl_hdr); + memset(rt_msg, 0, sizeof(struct rtmsg)); + rt_msg->rtm_family = AF_INET6; + rt_msg->rtm_dst_len = dst_prefix; + rt_msg->rtm_src_len = 0; + rt_msg->rtm_tos = 0; + rt_msg->rtm_table = RT_TABLE_MAIN; + rt_msg->rtm_protocol = RTPROT_STATIC; + rt_msg->rtm_scope = RT_SCOPE_UNIVERSE; + rt_msg->rtm_type = RTN_UNICAST; + rt_msg->rtm_flags = 0; + + if (dst6) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_DST; + rta->rta_len = RTA_LENGTH(sizeof(*dst6)); + memcpy(RTA_DATA(rta), dst6, sizeof(*dst6)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + if (gw6) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_GATEWAY; + rta->rta_len = RTA_LENGTH(sizeof(*gw6)); + memcpy(RTA_DATA(rta), gw6, sizeof(*gw6)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + if (if_idx > 0) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_OIF; + rta->rta_len = RTA_LENGTH(sizeof(if_idx)); + memcpy(RTA_DATA(rta), &if_idx, sizeof(if_idx)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + if (metric > 0) { + rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); + rta->rta_type = RTA_PRIORITY; + rta->rta_len = RTA_LENGTH(sizeof(metric)); + memcpy(RTA_DATA(rta), &metric, sizeof(metric)); + nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); + } + + return rtnetlink_request(sock_fd, nl_hdr, nl_hdr->nlmsg_len, err_str, max_len); +} + +static PyObject* pynetlink_init_socket(PyObject* self, PyObject* args) { + int sock_fd = init_rtnetlink_sock_(); + if (sock_fd < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return PyLong_FromLong(sock_fd); +} + +static PyObject* pynetlink_close_socket(PyObject* self, PyObject* args) { + int sock_fd; + if (!PyArg_ParseTuple(args, "i", &sock_fd)) { + return NULL; + } + close(sock_fd); - - // 3. Set interface up - if (set_link_up_(if_name, err_str, max_len) != 0) { - return -1; + Py_RETURN_NONE; +} + +static PyObject* pynetlink_modify_addr(PyObject* self, PyObject* args) { + int add; + const char *if_name; + void *addr; + Py_ssize_t addr_len; + unsigned prefix_len; + int sock_fd = -1, temp_sock = -1, result; + char err[256]; + + if (!PyArg_ParseTuple(args, "psy#I|i", &add, &if_name, &addr, &addr_len, &prefix_len, &sock_fd)) { + return NULL; } - - return 0; + + if (addr_len != sizeof(struct in_addr) && addr_len != sizeof(struct in6_addr)) { + PyErr_SetString(PyExc_ValueError, "Invalid IPv4 or IPv6 address length"); + return NULL; + } + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } + + if(addr_len == sizeof(struct in_addr)) { + result = modify_addr4_(sock_fd, add ? RTM_NEWADDR : RTM_DELADDR, + if_name, (struct in_addr*)addr, prefix_len, err, sizeof(err)); + } else { + result = modify_addr6_(sock_fd, add ? RTM_NEWADDR : RTM_DELADDR, + if_name, (struct in6_addr*)addr, prefix_len, err, sizeof(err)); + } + + if(temp_sock >= 0) + close(temp_sock); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; } -// Python function to update network interface parameters -static PyObject* pynetlink_update_if(PyObject* self, PyObject* args) { +static PyObject* pynetlink_traffic_control(PyObject* self, PyObject* args) { const char *if_name; const char *delay_str; const char *rate_str; const char *loss_str; - char err[256]; - - // Parse Python arguments - if (!PyArg_ParseTuple(args, "ssss", &if_name, &delay_str, &rate_str, &loss_str)) { + int sock_fd = -1, temp_sock = -1, result; + uint32_t delay, loss; + uint64_t rate_Bps = 0; + double rate_value = 0.0; + char rate_unit[16], err[256]; + + if (!PyArg_ParseTuple(args, "ssss|i", &if_name, &delay_str, &rate_str, &loss_str, &sock_fd)) { return NULL; } - - uint32_t delay = (uint32_t)(atof(delay_str) * 15625); - uint32_t loss = (uint32_t)(atof(loss_str) * (~0U/100U)); - double rate_value = 0.0; - char rate_unit[16] = {0}; - uint64_t rate_Bps = 0; + + delay = (uint32_t)(atof(delay_str) * 15625); + loss = (uint32_t)(atof(loss_str) * (~0U/100U)); if (sscanf(rate_str, "%lf%15s", &rate_value, rate_unit) == 2) { if (strcmp(rate_unit, "Gbit") == 0) { - rate_Bps = (uint64_t)(rate_value * 1000000000 / 8); + rate_Bps = (uint64_t)(rate_value * (1000000000 / 8)); } else if (strcmp(rate_unit, "Mbit") == 0) { - rate_Bps = (uint64_t)(rate_value * 1000000 / 8); + rate_Bps = (uint64_t)(rate_value * (1000000 / 8)); } else if (strcmp(rate_unit, "Kbit") == 0) { - rate_Bps = (uint64_t)(rate_value * 1000 / 8); + rate_Bps = (uint64_t)(rate_value * (1000 / 8)); } else { rate_Bps = (uint64_t)rate_value; } } else { if (sscanf(rate_str, "%lf", &rate_value) == 1) { - rate_Bps = (uint64_t)rate_value * 1000000000 / 8; + rate_Bps = (uint64_t)rate_value * (1000000000 / 8); + } + } + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; } + sock_fd = temp_sock; } - - // Update netem qdisc - int result = update_netem_(if_name, delay, loss, rate_Bps, err, sizeof(err)); - + + result = netem_update_or_create_(sock_fd, if_name, delay, loss, rate_Bps, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + if (result < 0) { PyErr_SetString(PyExc_RuntimeError, err); return NULL; } - + Py_RETURN_NONE; } -// Python function to delete a network interface -static PyObject* pynetlink_del_link(PyObject* self, PyObject* args) { +static PyObject* pynetlink_if_up(PyObject* self, PyObject* args) { + const char *if_name; + int sock_fd = -1, temp_sock = -1, result; + char err[256]; + + if (!PyArg_ParseTuple(args, "s|i", &if_name, &sock_fd)) { + return NULL; + } + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } + + result = modify_link_(sock_fd, if_name, RTM_NEWLINK, IFF_UP, IFF_UP, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +static PyObject* pynetlink_if_down(PyObject* self, PyObject* args) { const char *if_name; + int sock_fd = -1, temp_sock = -1, result; char err[256]; - - if (!PyArg_ParseTuple(args, "s", &if_name)) { + + if (!PyArg_ParseTuple(args, "s|i", &if_name, &sock_fd)) { return NULL; } - - int result = del_link_(if_name, err, sizeof(err)); + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } + + result = modify_link_(sock_fd, if_name, RTM_NEWLINK, 0, IFF_UP, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + if (result < 0) { PyErr_SetString(PyExc_RuntimeError, err); return NULL; } - + Py_RETURN_NONE; } -// Python function to initialize an interface (addr, tc, up) -static PyObject* pynetlink_init_if(PyObject* self, PyObject* args) { +static PyObject* pynetlink_del_link(PyObject* self, PyObject* args) { const char *if_name; - const char *addr_str; - const char *delay_str; - const char *rate_str; - const char *loss_str; + int sock_fd = -1, temp_sock = -1, result; char err[256]; - - if (!PyArg_ParseTuple(args, "sssss", &if_name, &addr_str, &delay_str, &rate_str, &loss_str)) { + + if (!PyArg_ParseTuple(args, "s|i", &if_name, &sock_fd)) { return NULL; } - uint32_t delay = (uint32_t)(atof(delay_str) * 15625); - uint32_t loss = (uint32_t)(atof(loss_str) * (~0U/100U)); - double rate_value = 0.0; - char rate_unit[16] = {0}; - uint64_t rate_Bps = 0; + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } - if (sscanf(rate_str, "%lf%15s", &rate_value, rate_unit) == 2) { - if (strcmp(rate_unit, "Gbit") == 0) { - rate_Bps = (uint64_t)(rate_value * 1000000000 / 8); - } else if (strcmp(rate_unit, "Mbit") == 0) { - rate_Bps = (uint64_t)(rate_value * 1000000 / 8); - } else if (strcmp(rate_unit, "Kbit") == 0) { - rate_Bps = (uint64_t)(rate_value * 1000 / 8); + result = modify_link_(sock_fd, if_name, RTM_DELLINK, 0, 0, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + +static PyObject* pynetlink_modify_routes(PyObject* self, PyObject* args) { + int sock_fd = -1; + PyObject* route_list = NULL; + Py_ssize_t count; + char err[256]; + + if(!PyArg_ParseTuple(args, "iO", &sock_fd, &route_list)) { + return NULL; + } + + if(sock_fd < 0) { + PyErr_SetString(PyExc_ValueError, "Not a valid socket fd"); + return NULL; + } + + if(!PyList_Check(route_list)) { + PyErr_SetString(PyExc_TypeError, "Need a sequence of routes"); + return NULL; + } + + count = PyList_Size(route_list); + + for (Py_ssize_t i = 0; i < count; i++) { + void *dst, *gw = NULL; + Py_ssize_t dst_len, gw_len; + const char *if_name = NULL; + unsigned dst_prefix; + int add, metric = 0, result; + PyObject* route_item; + + route_item = PyList_GetItem(route_list, i); + if (!PyTuple_Check(route_item)) { + PyErr_SetString(PyExc_TypeError, "Each route must be a tuple"); + return NULL; + } + + if (!PyArg_ParseTuple(route_item, "py#I|sy#i", + &add, &dst, &dst_len, &dst_prefix, &if_name, &gw, &gw_len, &metric)) { + PyErr_SetString(PyExc_ValueError, "Route format not correct"); + return NULL; + } + if (dst_len != sizeof(struct in_addr) && dst_len != sizeof(struct in6_addr)) { + PyErr_SetString(PyExc_ValueError, "Invalid IPv4 or IPv6 address length"); + return NULL; + } + if (gw && gw_len != dst_len) { + PyErr_SetString(PyExc_ValueError, "Gateway address not match destination"); + return NULL; + } + + if (dst_len == sizeof(struct in_addr)) { + result = modify_route4( + sock_fd, add ? RTM_NEWROUTE : RTM_DELROUTE, (struct in_addr*)dst, dst_prefix, + (struct in_addr*)gw, if_name, metric, err, sizeof(err) + ); } else { - rate_Bps = (uint64_t)rate_value; + result = modify_route6( + sock_fd, add ? RTM_NEWROUTE : RTM_DELROUTE, (struct in6_addr*)dst, dst_prefix, + (struct in6_addr*)gw, if_name, metric, err, sizeof(err) + ); } - } else { - if (sscanf(rate_str, "%lf", &rate_value) == 1) { - rate_Bps = (uint64_t)rate_value * 1000000000 / 8; + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; } } - - int result = init_if_(if_name, addr_str, delay, loss, rate_Bps, err, sizeof(err)); + + Py_RETURN_NONE; +} + +static PyObject* pynetlink_add_link_veth(PyObject* self, PyObject* args) { + const char *if_name, *peer_name, *netns, *netns_peer; + int sock_fd = -1, temp_sock = -1, result; + char err[256]; + + if (!PyArg_ParseTuple(args, "ssss|i", &if_name, &peer_name, &netns, &netns_peer, &sock_fd)) { + return NULL; + } + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } + + result = add_link_veth_(sock_fd, if_name, peer_name, netns, netns_peer, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + if (result < 0) { PyErr_SetString(PyExc_RuntimeError, err); return NULL; } - + + Py_RETURN_NONE; +} + +static PyObject* pynetlink_add_link_vxlan(PyObject* self, PyObject* args) { + const char *if_name, *remote_ip, *netns_name; + int vxlan_id, sock_fd = -1, temp_sock = -1, result; + char err[256]; + + if (!PyArg_ParseTuple(args, "siss|i", &if_name, &vxlan_id, &remote_ip, &netns_name, &sock_fd)) { + return NULL; + } + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } + + result = add_link_vxlan_(sock_fd, if_name, vxlan_id, remote_ip, netns_name, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + Py_RETURN_NONE; } // Define module methods static PyMethodDef PyNetlinkMethods[] = { - {"update_if", pynetlink_update_if, METH_VARARGS, "Update network interface parameters using netlink"}, + {"init_socket", pynetlink_init_socket, METH_VARARGS, "Initialize netlink socket and return descriptor"}, + {"close_socket", pynetlink_close_socket, METH_VARARGS, "Close netlink socket"}, + {"modify_addr", pynetlink_modify_addr, METH_VARARGS, "Add or delete an IP address (v4/v6) on an interface"}, + {"traffic_control", pynetlink_traffic_control, METH_VARARGS, "Configure traffic control parameters on an interface"}, + {"if_up", pynetlink_if_up, METH_VARARGS, "Bring a network interface up"}, + {"if_down", pynetlink_if_down, METH_VARARGS, "Bring a network interface down"}, + {"modify_routes", pynetlink_modify_routes, METH_VARARGS, "Modify a list of routes"}, {"del_link", pynetlink_del_link, METH_VARARGS, "Delete a network interface using netlink"}, - {"init_if", pynetlink_init_if, METH_VARARGS, "Initialize an interface (add addr, setup tc, set up)"}, + {"add_link_veth", pynetlink_add_link_veth, METH_VARARGS, "Create veth pair between two network namespaces"}, + {"add_link_vxlan", pynetlink_add_link_vxlan, METH_VARARGS, "Create vxlan interface"}, {NULL, NULL, 0, NULL} }; diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 96ea83a..5d793a3 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -6,6 +6,7 @@ import glob import ctypes import time +import ipaddress # from line_profiler import LineProfiler @@ -85,17 +86,21 @@ def _del_link(name1, name2): pynetlink.del_link(n1_n2) def _init_if(name, if_name, addr, addr6, delay, bw, loss): + addr = ipaddress.IPv4Interface(addr) + addr6 = ipaddress.IPv6Interface(addr6) fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - subprocess.check_call(('ip', 'addr', 'add', addr6, 'dev', if_name)) - pynetlink.init_if(if_name, addr, delay, bw, loss) + pynetlink.modify_addr(True, if_name, addr.packed, addr.network.prefixlen) + pynetlink.modify_addr(True, if_name, addr6.packed, addr6.network.prefixlen) + pynetlink.traffic_control(if_name, delay, bw, loss) + pynetlink.set_if_up(if_name) def _update_if(name, if_name, delay, bw, loss): fd = os.open('/run/netns/' + name, os.O_RDONLY) libc.setns(fd, CLONE_NEWNET) os.close(fd) - pynetlink.update_if(if_name, delay, bw, loss) + pynetlink.traffic_control(if_name, delay, bw, loss) def _update_link_intra_machine(name1, name2, delay, bw, loss): n1_n2 = f"{name2}" @@ -112,20 +117,16 @@ def _add_link_intra_machine(idx, name1, name2, prefix4, prefix6, delay, bw, loss n1_n2 = name2 n2_n1 = name1 libc.setns(main_net_fd, CLONE_NEWNET) - subprocess.check_call( - ('ip', 'link', 'add', n1_n2, 'netns', name1, - 'type', 'veth', 'peer', n2_n1, 'netns', name2) - ) + # Use pynetlink to create veth pair instead of subprocess + pynetlink.add_link_veth(n1_n2, n2_n1, name1, name2) _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) _init_if(name2, n2_n1, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, delay, bw, loss): n1_n2 = name2 libc.setns(main_net_fd, CLONE_NEWNET) - subprocess.check_call( - ('ip', 'link', 'add', n1_n2, 'netns', name1, - 'type', 'vxlan', 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) - ) + # Use pynetlink to create vxlan interface instead of subprocess + pynetlink.add_link_vxlan(n1_n2, idx, remote_ip, name1) suffix4 = '.10/24' if name1 < name2 else '.40/24' suffix6 = '::10/48' if name1 < name2 else '::40/48' _init_if(name1, n1_n2, prefix4+suffix4, prefix6 + suffix6, delay, bw, loss) From 044c3775c96fc8f88b88ef74dedd67e0760df425 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 4 Feb 2026 18:14:12 +0800 Subject: [PATCH 21/49] fix: link creation bugs Fix link creation failure due to netns fd and replace it with pid. Fix nested attributes in the construction of rtnetlink message when creating veth. Fix remote address attribute of rtnetlink message when creating vxlan. Fix constant definitions of veth and vxlan in pynetlink.c, use system headers instead. Replace string-based namespace parameter with PID int in orchestrator. --- starrynet/pynetlink.c | 172 ++++++++++++++--------------------- starrynet/sn_orchestrater.py | 148 +++++++++++++++--------------- 2 files changed, 147 insertions(+), 173 deletions(-) diff --git a/starrynet/pynetlink.c b/starrynet/pynetlink.c index 3442039..41cb043 100644 --- a/starrynet/pynetlink.c +++ b/starrynet/pynetlink.c @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include // std C @@ -22,23 +22,6 @@ #define NETNS_DIR "/var/run/netns" #define VXLAN_PORT 4789 -// Missing constants for veth and vxlan -#ifndef VETH_INFO_PEER -#define VETH_INFO_PEER 1 -#endif - -#ifndef IFLA_VXLAN_ID -#define IFLA_VXLAN_ID 1 -#endif - -#ifndef IFLA_VXLAN_REMOTE -#define IFLA_VXLAN_REMOTE 5 -#endif - -#ifndef IFLA_VXLAN_PORT -#define IFLA_VXLAN_PORT 7 -#endif - static int init_rtnetlink_sock_() { struct sockaddr_nl sa_nl = { .nl_family = AF_NETLINK, @@ -62,9 +45,14 @@ static int init_rtnetlink_sock_() { static int rtnetlink_request(int sock_fd, struct nlmsghdr* nl_hdr, size_t buf_len, char *err_str, size_t err_len) { + struct sockaddr_nl sa_nl = { + .nl_family = AF_NETLINK, + .nl_pid = 0, + .nl_groups = 0 + }; int ret; - if(sendto(sock_fd, nl_hdr, nl_hdr->nlmsg_len, 0, NULL, 0) < 0) { + if(sendto(sock_fd, nl_hdr, nl_hdr->nlmsg_len, 0, (struct sockaddr*)&sa_nl, sizeof(sa_nl)) < 0) { snprintf(err_str, err_len, "Failed to send netlink message: %s", strerror(errno)); return -1; } @@ -303,32 +291,16 @@ static int modify_link_(int sock_fd, const char *if_name, uint16_t nlmsg_type, return rtnetlink_request(sock_fd, nl_hdr, sizeof(buf), err_str, max_len); } -static int add_link_veth_(int sock_fd, const char *if_name, const char *peer_name, - const char *netns, const char *netns_peer, char *err_str, size_t max_len) { +static int add_link_veth_(int sock_fd, pid_t ns_pid, const char *if_name, + pid_t peer_ns_pid, const char *peer_name, char *err_str, size_t max_len) { struct timespec ts; uint32_t seq; struct nlmsghdr *nl_hdr; struct ifinfomsg *if_msg, *peer_ifi; - struct rtattr *rta, *nest, *peer_nest; - int netns_fd1, netns_fd2; + struct rtattr *rta, *nest_linkinfo, *nest_infodata, *nest_infopeer; uint8_t buf[1024]; char netns_path[256]; - snprintf(netns_path, sizeof(netns_path), NETNS_DIR"/%s", netns); - netns_fd1 = open(netns_path, O_RDONLY); - if (netns_fd1 < 0) { - snprintf(err_str, max_len, "Failed to open netns: %s", netns); - return -1; - } - - snprintf(netns_path, sizeof(netns_path), NETNS_DIR"/%s", netns_peer); - netns_fd2 = open(netns_path, O_RDONLY); - if (netns_fd2 < 0) { - snprintf(err_str, max_len, "Failed to open netns: %s", netns_peer); - close(netns_fd1); - return -1; - } - clock_gettime(CLOCK_MONOTONIC, &ts); seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; @@ -338,23 +310,22 @@ static int add_link_veth_(int sock_fd, const char *if_name, const char *peer_nam nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; nl_hdr->nlmsg_seq = seq; nl_hdr->nlmsg_pid = 0; - if_msg = NLMSG_DATA(nl_hdr); memset(if_msg, 0, sizeof(*if_msg)); if_msg->ifi_family = AF_UNSPEC; // netns rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); - rta->rta_type = IFLA_NET_NS_FD; - rta->rta_len = RTA_LENGTH(sizeof(netns_fd1)); - memcpy(RTA_DATA(rta), &netns_fd1, sizeof(netns_fd1)); + rta->rta_type = IFLA_NET_NS_PID; + rta->rta_len = RTA_LENGTH(sizeof(ns_pid)); + memcpy(RTA_DATA(rta), &ns_pid, sizeof(ns_pid)); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); // if name rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); rta->rta_type = IFLA_IFNAME; - rta->rta_len = RTA_LENGTH(strlen(if_name)); - memcpy(RTA_DATA(rta), if_name, strlen(if_name)); + rta->rta_len = RTA_LENGTH(strlen(if_name)+1); + memcpy(RTA_DATA(rta), if_name, strlen(if_name)+1); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); @@ -362,69 +333,56 @@ static int add_link_veth_(int sock_fd, const char *if_name, const char *peer_nam rta->rta_len = RTA_LENGTH(0); { // kind: veth - nest = (struct rtattr*)RTA_DATA(rta); - nest->rta_type = IFLA_INFO_KIND; - nest->rta_len = RTA_LENGTH(strlen("veth")); - memcpy(RTA_DATA(nest), "veth", strlen("veth")); - rta->rta_len += RTA_ALIGN(nest->rta_len); - - nest = (struct rtattr*)((char*)nest + RTA_ALIGN(nest->rta_len)); - nest->rta_type = IFLA_INFO_DATA; - nest->rta_len = RTA_LENGTH(0); + nest_linkinfo = (struct rtattr*)RTA_DATA(rta); + nest_linkinfo->rta_type = IFLA_INFO_KIND; + nest_linkinfo->rta_len = RTA_LENGTH(strlen("veth")); + memcpy(RTA_DATA(nest_linkinfo), "veth", strlen("veth")); + rta->rta_len += RTA_ALIGN(nest_linkinfo->rta_len); + + nest_linkinfo = (struct rtattr*)((char*)nest_linkinfo + RTA_ALIGN(nest_linkinfo->rta_len)); + nest_linkinfo->rta_type = IFLA_INFO_DATA; + nest_linkinfo->rta_len = RTA_LENGTH(0); { // peer ifi - peer_nest = (struct rtattr*)RTA_DATA(nest); - peer_nest->rta_type = VETH_INFO_PEER; - peer_nest->rta_len = RTA_LENGTH(sizeof(*peer_ifi)); - peer_ifi = (struct ifinfomsg*)RTA_DATA(peer_nest); + nest_infodata = (struct rtattr*)RTA_DATA(nest_linkinfo); + nest_infodata->rta_type = VETH_INFO_PEER; + nest_infodata->rta_len = RTA_LENGTH(sizeof(*peer_ifi)); + peer_ifi = (struct ifinfomsg*)RTA_DATA(nest_infodata); memset(peer_ifi, 0, sizeof(*peer_ifi)); peer_ifi->ifi_family = AF_UNSPEC; - nest->rta_len += RTA_ALIGN(peer_nest->rta_len); - - // peer if name - peer_nest = (struct rtattr*)((char*)peer_nest + RTA_ALIGN(peer_nest->rta_len)); - peer_nest->rta_type = IFLA_IFNAME; - peer_nest->rta_len = RTA_LENGTH(strlen(peer_name)); - memcpy(RTA_DATA(peer_nest), peer_name, strlen(peer_name)); - nest->rta_len += RTA_ALIGN(peer_nest->rta_len); - - // peer netns - peer_nest = (struct rtattr*)((char*)peer_nest + RTA_ALIGN(peer_nest->rta_len)); - peer_nest->rta_type = IFLA_NET_NS_FD; - peer_nest->rta_len = RTA_LENGTH(sizeof(netns_fd2)); - memcpy(RTA_DATA(peer_nest), &netns_fd2, sizeof(netns_fd2)); - nest->rta_len += RTA_ALIGN(peer_nest->rta_len); + { + // peer netns + nest_infopeer = (struct rtattr*)((char*)nest_infodata + RTA_ALIGN(nest_infodata->rta_len)); + nest_infopeer->rta_type = IFLA_NET_NS_PID; + nest_infopeer->rta_len = RTA_LENGTH(sizeof(peer_ns_pid)); + memcpy(RTA_DATA(nest_infopeer), &peer_ns_pid, sizeof(peer_ns_pid)); + nest_infodata->rta_len += RTA_ALIGN(nest_infopeer->rta_len); + // peer if name + nest_infopeer = (struct rtattr*)((char*)nest_infopeer + RTA_ALIGN(nest_infopeer->rta_len)); + nest_infopeer->rta_type = IFLA_IFNAME; + nest_infopeer->rta_len = RTA_LENGTH(strlen(peer_name)+1); + memcpy(RTA_DATA(nest_infopeer), peer_name, strlen(peer_name)+1); + nest_infodata->rta_len += RTA_ALIGN(nest_infopeer->rta_len); + } + nest_linkinfo->rta_len += RTA_ALIGN(nest_infodata->rta_len); } - rta->rta_len += RTA_ALIGN(nest->rta_len); + rta->rta_len += RTA_ALIGN(nest_linkinfo->rta_len); } nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - close(netns_fd1); - close(netns_fd2); - return rtnetlink_request(sock_fd, nl_hdr, nl_hdr->nlmsg_len, err_str, max_len); } -static int add_link_vxlan_(int sock_fd, const char *if_name, int vxlan_id, - const char *remote_ip, const char *netns_name, char *err_str, size_t max_len) { +static int add_link_vxlan_(int sock_fd, pid_t ns_pid, const char *if_name, + int vxlan_id, void *remote_addr, size_t addr_len, char *err_str, size_t max_len) { struct timespec ts; uint32_t seq; struct nlmsghdr* nl_hdr; struct ifinfomsg* if_msg; struct rtattr *rta, *nest, *vxlan_nest; - int netns_fd; uint16_t dstport; uint8_t buf[1024]; - // Open network namespace file descriptor - char netns_path[256]; - snprintf(netns_path, sizeof(netns_path), NETNS_DIR"/%s", netns_name); - netns_fd = open(netns_path, O_RDONLY); - if (netns_fd < 0) { - snprintf(err_str, max_len, "Failed to open netns: %s", netns_name); - return -1; - } - clock_gettime(CLOCK_MONOTONIC, &ts); seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; @@ -441,9 +399,9 @@ static int add_link_vxlan_(int sock_fd, const char *if_name, int vxlan_id, // netns rta = (struct rtattr*)((char*)nl_hdr + NLMSG_ALIGN(nl_hdr->nlmsg_len)); - rta->rta_type = IFLA_NET_NS_FD; - rta->rta_len = RTA_LENGTH(sizeof(netns_fd)); - memcpy(RTA_DATA(rta), &netns_fd, sizeof(netns_fd)); + rta->rta_type = IFLA_NET_NS_PID; + rta->rta_len = RTA_LENGTH(sizeof(ns_pid)); + memcpy(RTA_DATA(rta), &ns_pid, sizeof(ns_pid)); nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); // if name @@ -477,9 +435,9 @@ static int add_link_vxlan_(int sock_fd, const char *if_name, int vxlan_id, // Add remote IP vxlan_nest = (struct rtattr*)((char*)vxlan_nest + RTA_ALIGN(vxlan_nest->rta_len)); - vxlan_nest->rta_type = IFLA_VXLAN_REMOTE; - vxlan_nest->rta_len = RTA_LENGTH(strlen(remote_ip)); - memcpy(RTA_DATA(vxlan_nest), remote_ip, strlen(remote_ip)); + vxlan_nest->rta_type = IFLA_VXLAN_GROUP; + vxlan_nest->rta_len = RTA_LENGTH(addr_len); + memcpy(RTA_DATA(vxlan_nest), remote_addr, addr_len); nest->rta_len += RTA_ALIGN(vxlan_nest->rta_len); // Add destination port @@ -494,8 +452,6 @@ static int add_link_vxlan_(int sock_fd, const char *if_name, int vxlan_id, } nl_hdr->nlmsg_len = NLMSG_ALIGN(nl_hdr->nlmsg_len) + RTA_ALIGN(rta->rta_len); - close(netns_fd); - return rtnetlink_request(sock_fd, nl_hdr, nl_hdr->nlmsg_len, err_str, max_len); } @@ -938,11 +894,12 @@ static PyObject* pynetlink_modify_routes(PyObject* self, PyObject* args) { } static PyObject* pynetlink_add_link_veth(PyObject* self, PyObject* args) { - const char *if_name, *peer_name, *netns, *netns_peer; + const char *if_name, *peer_name; int sock_fd = -1, temp_sock = -1, result; + pid_t netns = 0, netns_peer = 0; char err[256]; - if (!PyArg_ParseTuple(args, "ssss|i", &if_name, &peer_name, &netns, &netns_peer, &sock_fd)) { + if (!PyArg_ParseTuple(args, "isis|i", &netns, &if_name, &netns_peer, &peer_name, &sock_fd)) { return NULL; } @@ -956,7 +913,8 @@ static PyObject* pynetlink_add_link_veth(PyObject* self, PyObject* args) { sock_fd = temp_sock; } - result = add_link_veth_(sock_fd, if_name, peer_name, netns, netns_peer, err, sizeof(err)); + result = add_link_veth_(sock_fd, netns, if_name, + netns_peer, peer_name, err, sizeof(err)); if(temp_sock >= 0) close(temp_sock); @@ -969,11 +927,20 @@ static PyObject* pynetlink_add_link_veth(PyObject* self, PyObject* args) { } static PyObject* pynetlink_add_link_vxlan(PyObject* self, PyObject* args) { - const char *if_name, *remote_ip, *netns_name; + const char *if_name; + pid_t netns_pid; int vxlan_id, sock_fd = -1, temp_sock = -1, result; + void *remote_addr; + Py_ssize_t remote_addr_len; char err[256]; - if (!PyArg_ParseTuple(args, "siss|i", &if_name, &vxlan_id, &remote_ip, &netns_name, &sock_fd)) { + if (!PyArg_ParseTuple(args, "isiy#|i", + &netns_pid, &if_name, &vxlan_id, &remote_addr, &remote_addr_len, &sock_fd)) { + return NULL; + } + + if (remote_addr_len != sizeof(struct in_addr) && remote_addr_len != sizeof(struct in6_addr)) { + PyErr_SetString(PyExc_ValueError, "Invalid IPv4 or IPv6 address length"); return NULL; } @@ -987,7 +954,8 @@ static PyObject* pynetlink_add_link_vxlan(PyObject* self, PyObject* args) { sock_fd = temp_sock; } - result = add_link_vxlan_(sock_fd, if_name, vxlan_id, remote_ip, netns_name, err, sizeof(err)); + result = add_link_vxlan_(sock_fd, netns_pid, if_name, + vxlan_id, remote_addr, remote_addr_len, err, sizeof(err)); if(temp_sock >= 0) close(temp_sock); diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 5d793a3..ebba535 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -41,7 +41,7 @@ def _pid_map(pid_path, pop = False): if name_pid == NOT_ASSIGNED: continue name_pid = name_pid.split(':') - _pid_map_cache[name_pid[0]] = name_pid[1] + _pid_map_cache[name_pid[0]] = int(name_pid[1]) if pop: ret = _pid_map_cache _pid_map_cache = None @@ -76,60 +76,51 @@ def _parse_links(path): f.close() return del_lst, update_lst, add_lst +def _switch_netns(node): + pid_fd = os.open(f'/proc/{node}/ns/net', os.O_RDONLY) + libc.setns(pid_fd, CLONE_NEWNET) + os.close(pid_fd) + # name1 in local machine -def _del_link(name1, name2): - n1_n2 = f"{name2}" - fd = os.open('/run/netns/' + name1, os.O_RDONLY) - libc.setns(fd, CLONE_NEWNET) - os.close(fd) +def _del_link(node, ifname): + _switch_netns(node) - pynetlink.del_link(n1_n2) + pynetlink.del_link(ifname) -def _init_if(name, if_name, addr, addr6, delay, bw, loss): +def _init_if(node, if_name, addr, addr6, delay, bw, loss): addr = ipaddress.IPv4Interface(addr) addr6 = ipaddress.IPv6Interface(addr6) - fd = os.open('/run/netns/' + name, os.O_RDONLY) - libc.setns(fd, CLONE_NEWNET) - os.close(fd) + _switch_netns(node) pynetlink.modify_addr(True, if_name, addr.packed, addr.network.prefixlen) pynetlink.modify_addr(True, if_name, addr6.packed, addr6.network.prefixlen) pynetlink.traffic_control(if_name, delay, bw, loss) - pynetlink.set_if_up(if_name) + pynetlink.if_up(if_name) -def _update_if(name, if_name, delay, bw, loss): - fd = os.open('/run/netns/' + name, os.O_RDONLY) - libc.setns(fd, CLONE_NEWNET) - os.close(fd) +def _update_if(node, if_name, delay, bw, loss): + _switch_netns(node) pynetlink.traffic_control(if_name, delay, bw, loss) -def _update_link_intra_machine(name1, name2, delay, bw, loss): - n1_n2 = f"{name2}" - n2_n1 = f"{name1}" - _update_if(name1, n1_n2, delay, bw, loss) - _update_if(name2, n2_n1, delay, bw, loss) +def _update_link_intra_machine(node1, ifname1, node2, ifname2, delay, bw, loss): + _update_if(node1, ifname1, delay, bw, loss) + _update_if(node2, ifname2, delay, bw, loss) # name1 in local machine -def _update_link_local(name1, name2, delay, bw, loss): - n1_n2 = f"{name2}" - _update_if(name1, n1_n2, delay, bw, loss) +def _update_link_local(node, ifname, delay, bw, loss): + _update_if(node, ifname, delay, bw, loss) -def _add_link_intra_machine(idx, name1, name2, prefix4, prefix6, delay, bw, loss): - n1_n2 = name2 - n2_n1 = name1 +def _add_link_intra_machine(idx, node1, ifname1, node2, ifname2, prefix4, prefix6, delay, bw, loss): libc.setns(main_net_fd, CLONE_NEWNET) # Use pynetlink to create veth pair instead of subprocess - pynetlink.add_link_veth(n1_n2, n2_n1, name1, name2) - _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) - _init_if(name2, n2_n1, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) + pynetlink.add_link_veth(node1, ifname1, node2, ifname2) + _init_if(node1, ifname1, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) + _init_if(node2, ifname2, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) -def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, delay, bw, loss): - n1_n2 = name2 +def _add_link_inter_machine(idx, node, ifname, remote_ip, addr4, addr6, delay, bw, loss): + remote_addr = ipaddress.ip_address(remote_ip) libc.setns(main_net_fd, CLONE_NEWNET) # Use pynetlink to create vxlan interface instead of subprocess - pynetlink.add_link_vxlan(n1_n2, idx, remote_ip, name1) - suffix4 = '.10/24' if name1 < name2 else '.40/24' - suffix6 = '::10/48' if name1 < name2 else '::40/48' - _init_if(name1, n1_n2, prefix4+suffix4, prefix6 + suffix6, delay, bw, loss) + pynetlink.add_link_vxlan(node, ifname, idx, remote_addr.packed) + _init_if(node, ifname, addr4, addr6, delay, bw, loss) def sn_init_nodes(dir, shell_num, node_mid_dict): def _load_netns(pid, name): @@ -175,6 +166,7 @@ def sn_update_network( dir, ts, shell_num, node_mid_dict, ip_lst, isl_bw, isl_loss, gsl_bw, gsl_loss ): + pid_map = _pid_map(dir + '/' + PID_FILENAME) for shell_id in range(shell_num): shell_dir = f"{dir}/shell{shell_id}" if not os.path.exists(shell_dir): @@ -184,40 +176,47 @@ def sn_update_network( for sat_name, isl_sat in del_lst: if node_mid_dict[sat_name] == machine_id: del_cnt += 1 - _del_link(sat_name, isl_sat) + _del_link(pid_map[sat_name], isl_sat) elif node_mid_dict[isl_sat] == machine_id: del_cnt += 1 - _del_link(isl_sat, sat_name) + _del_link(pid_map[isl_sat], sat_name) for sat_name, isl_sat, delay in update_lst: if node_mid_dict[sat_name] == machine_id: update_cnt += 1 if node_mid_dict[isl_sat] == machine_id: - _update_link_intra_machine(sat_name, isl_sat, delay, isl_bw, isl_loss) + _update_link_intra_machine( + pid_map[sat_name], isl_sat, pid_map[isl_sat], sat_name, + delay, isl_bw, isl_loss + ) else: - _update_link_local(sat_name, isl_sat, delay, isl_bw, isl_loss) + _update_link_local(pid_map[sat_name], isl_sat, delay, isl_bw, isl_loss) elif node_mid_dict[isl_sat] == machine_id: update_cnt += 1 - _update_link_local(isl_sat, sat_name, delay, isl_bw, isl_loss) + _update_link_local(pid_map[isl_sat], sat_name, delay, isl_bw, isl_loss) for sat_name, isl_sat, delay, idx in add_lst: if node_mid_dict[sat_name] == machine_id: add_cnt += 1 if node_mid_dict[isl_sat] == machine_id: _add_link_intra_machine( - idx, sat_name, isl_sat, + idx, pid_map[sat_name], isl_sat, pid_map[isl_sat], sat_name, f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', delay, isl_bw, isl_loss ) else: + addr4 = f'10.{idx >> 8}.{idx & 0xFF}.{'10' if sat_name < isl_sat else '40'}/24' + addr6 = f'2001:{idx >> 8}:{idx & 0xFF}::{'10' if sat_name < isl_sat else '40'}/48' _add_link_inter_machine( - idx, sat_name, isl_sat, ip_lst[node_mid_dict[isl_sat]], - f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', + idx, pid_map[sat_name], isl_sat, ip_lst[node_mid_dict[isl_sat]], + addr4, addr6, delay, isl_bw, isl_loss ) elif node_mid_dict[isl_sat] == machine_id: add_cnt += 1 + addr4 = f'10.{idx >> 8}.{idx & 0xFF}.{'10' if sat_name < isl_sat else '40'}/24' + addr6 = f'2001:{idx >> 8}:{idx & 0xFF}::{'10' if sat_name < isl_sat else '40'}/48' _add_link_inter_machine( - idx, isl_sat, sat_name, ip_lst[node_mid_dict[sat_name]], - f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', + idx, pid_map[isl_sat], sat_name, ip_lst[node_mid_dict[sat_name]], + addr4, addr6, delay, isl_bw, isl_loss ) print(f"[{machine_id}] Shell {shell_id}:", @@ -231,40 +230,47 @@ def sn_update_network( for gs, sat in del_lst: if node_mid_dict[gs] == machine_id: del_cnt += 1 - _del_link(gs, sat) + _del_link(pid_map[gs], sat) elif node_mid_dict[sat] == machine_id: del_cnt += 1 - _del_link(sat, gs) + _del_link(pid_map[sat], gs) for gs, sat, delay in update_lst: if node_mid_dict[gs] == machine_id: update_cnt += 1 if node_mid_dict[sat] == machine_id: - _update_link_intra_machine(gs, sat, delay, gsl_bw, gsl_loss) + _update_link_intra_machine( + pid_map[gs], sat, pid_map[sat], gs, + delay, gsl_bw, gsl_loss + ) else: - _update_link_local(gs, sat, delay, gsl_bw, gsl_loss) + _update_link_local(pid_map[gs], sat, delay, gsl_bw, gsl_loss) elif node_mid_dict[sat] == machine_id: update_cnt += 1 - _update_link_local(sat, gs, delay, gsl_bw, gsl_loss) + _update_link_local(pid_map[sat], gs, delay, gsl_bw, gsl_loss) for gs, sat, delay, idx in add_lst: if node_mid_dict[gs] == machine_id: add_cnt += 1 - if node_mid_dict[gs] == machine_id: + if node_mid_dict[sat] == machine_id: _add_link_intra_machine( - idx, gs, sat, + idx, pid_map[gs], sat, pid_map[sat], gs, f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', delay, gsl_bw, gsl_loss ) else: + addr4 = f'9.{idx >> 8}.{idx & 0xFF}.40/24' + addr6 = f'2002:{idx >> 8}:{idx & 0xFF}::40/48' _add_link_inter_machine( - idx, gs, sat, ip_lst[node_mid_dict[gs]], - f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', + idx, pid_map[gs], sat, ip_lst[node_mid_dict[sat]], + addr4, addr6, delay, gsl_bw, gsl_loss ) elif node_mid_dict[sat] == machine_id: add_cnt += 1 + addr4 = f'9.{idx >> 8}.{idx & 0xFF}.10/24' + addr6 = f'2002:{idx >> 8}:{idx & 0xFF}::10/48' _add_link_inter_machine( - idx, sat, gs, ip_lst[node_mid_dict[sat]], - f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', + idx, pid_map[sat], gs, ip_lst[node_mid_dict[gs]], + addr4, addr6, delay, gsl_bw, gsl_loss ) print(f"[{machine_id}] GSL:", @@ -280,13 +286,13 @@ def sn_update_network( def sn_container_check_call(pid, cmd, *args, **kwargs): subprocess.check_call( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(pid), *cmd), *args, **kwargs ) def sn_container_run(pid, cmd, *args, **kwargs): subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(pid), *cmd), *args, **kwargs ) @@ -304,7 +310,7 @@ def sn_operate_every_node(dir, func, *args): def get_IP(dir, node): pid = _pid_map(f"{dir}/{PID_FILENAME}")[node] addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(pid), 'ip', '-br', 'addr', 'show') ).decode().splitlines() for dev_state_addrs in addr_lst: @@ -333,7 +339,7 @@ def sn_ping(dir, src, dst): dst_pid = pid_map[dst] dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), 'ip', '-br', 'addr', 'show') ).decode().splitlines() for dev_state_addrs in dst_addr_lst: @@ -347,7 +353,7 @@ def sn_ping(dir, src, dst): print('ping', src, dst_addr) subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(src_pid), 'ping', '-c', '4', '-i', '0.01', dst_addr), stdout=sys.stdout, stderr=subprocess.STDOUT ) @@ -360,7 +366,7 @@ def sn_iperf(dir, src, dst): dst_pid = pid_map[dst] dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), 'ip', '-br', 'addr', 'show') ).decode().splitlines() for dev_state_addrs in dst_addr_lst: @@ -373,12 +379,12 @@ def sn_iperf(dir, src, dst): dst_addr = dst_addr[:dst_addr.rfind('/')] server = subprocess.Popen( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), 'iperf3', '-s'), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(src_pid), 'iperf3', '-c', dst_addr, '-t5'), stdout=sys.stdout, stderr=subprocess.STDOUT ) @@ -392,7 +398,7 @@ def sn_sr(dir, src, dst, nxt): dst_pid = pid_map[dst] dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), 'ip', '-br', 'addr', 'show') ).decode().splitlines() for dev_state_addrs in dst_addr_lst: @@ -410,7 +416,7 @@ def sn_sr(dir, src, dst, nxt): def sn_check_route(dir, node): pid_map = _pid_map(f"{dir}/{PID_FILENAME}") subprocess.run( - ('nsenter', '-n', '-t', pid_map[node], + ('nsenter', '-n', '-t', str(pid_map[node]), 'route'), stdout=sys.stdout, stderr=subprocess.STDOUT ) @@ -433,7 +439,7 @@ def sn_clean(dir): if pid == NOT_ASSIGNED: continue try: - os.kill(int(pid), 9) + os.kill(pid, 9) except ProcessLookupError: pass os.remove(pid_file) @@ -460,7 +466,7 @@ def sn_damage(dir, random_list): pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") pid = pid_mat[node] out = subprocess.check_output( - ('nsenter', '-t', pid, '-n', + ('nsenter', '-t', str(pid), '-n', 'ip', '-br', 'addr', 'show')).decode() dev_lst = [] f.write(node + '|') @@ -474,7 +480,7 @@ def sn_damage(dir, random_list): if ':' in addr: # found first ip6 addr subprocess.check_call( - ('nsenter', '-t', pid, '-n', + ('nsenter', '-t', str(pid), '-n', 'ip', 'link', 'set', 'dev', dev_name, 'down',)) dev_lst.append(f'{dev_name},{addr}') break @@ -492,7 +498,7 @@ def sn_recover(dir, sat_loss): toks = line.strip().split('|') node = toks[0] - pid = pid_mat[node] + pid = str(pid_mat[node]) for link in toks[1].split(): dev_addr = link.split(',') subprocess.check_call( From ab345b7f23632385cf156f94740d116eb390ee0c Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 3 Apr 2026 12:23:11 +0800 Subject: [PATCH 22/49] refactor: drop legacy functions and `TopoSync` Remove deprecated helper functions from `sn_utils.py` related to resource checks, link damage/recovery, static routing, and iperf tests (`sn_check_utility`, `sn_damage`, `sn_recover`, `sn_sr`, `sn_perf`). Remove TopoSync for api-based topology orchestration in `sn_controller.py` and `sn_remote.py`. --- starrynet/clean.py | 28 --- starrynet/sn_controller.py | 264 -------------------- starrynet/sn_remote.py | 481 ------------------------------------- starrynet/sn_utils.py | 83 ------- 4 files changed, 856 deletions(-) delete mode 100644 starrynet/clean.py delete mode 100644 starrynet/sn_controller.py delete mode 100644 starrynet/sn_remote.py diff --git a/starrynet/clean.py b/starrynet/clean.py deleted file mode 100644 index d20d772..0000000 --- a/starrynet/clean.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/python -# -*- coding: UTF-8 -*- -""" -Starrynet Cleanup -author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) -""" -import os - - -def cleanup(): - print("Deleting all native bridges and containers...") - #os.system("FOR /f \"tokens=*\" %i IN ('docker ps -q') DO docker stop %i") - os.system("docker service rm constellation-test") - with os.popen( - "docker rm -f $(docker ps | grep \"lwsen/starlab_node:1.0\" | awk '{ print $1 }')" - ) as f: - f.readlines() - with os.popen("docker network ls") as f: - all_br_info = f.readlines() - for line in all_br_info: - if "La" in line or "Le" or "GS" in line: - network_name = line.split()[1] - print('docker network rm ' + network_name) - os.system('docker network rm ' + network_name) - - -if __name__ == "__main__": - cleanup() \ No newline at end of file diff --git a/starrynet/sn_controller.py b/starrynet/sn_controller.py deleted file mode 100644 index 2dfc832..0000000 --- a/starrynet/sn_controller.py +++ /dev/null @@ -1,264 +0,0 @@ -import time -import datetime -import threading -import math -import json -import requests -import subprocess -import gzip -import io - -from starrynet.sn_utils import * - -ASSIGN_FILENAME = 'assign.json' -LINK_FILENAME = 'link.json' - -RECONF_CHKPT = [12, 27, 42, 57] - -class Remote: - def __init__(self, id, host, port, username, password): - self.id = id - - self.ssh, self.sftp = sn_connect_remote( - host = host, - port = port, - username = username, - password = password, - ) - - self.dir = sn_remote_cmd(self.ssh, 'echo ~/SN') - sn_remote_cmd(self.ssh, 'mkdir ' + self.dir) - - # self.sftp.put( - # os.path.join(os.path.dirname(__file__), 'sn_remote.py'), - # self.dir + '/sn_remote.py' - # ) - # self.sftp.put( - # os.path.join(os.path.dirname(__file__), 'pyctr.c'), - # self.dir + '/pyctr.c' - # ) - self.sftp.put( - os.path.join(os.path.dirname(__file__), 'pyctr.so'), - self.dir + '/pyctr.so' - ) - self.sftp.put( - ASSIGN_FILENAME, - self.dir + '/' + ASSIGN_FILENAME - ) - sn_remote_wait_output( - self.ssh, - f"python3 {self.dir}/sn_remote.py nodes {self.id} {self.dir}" - ) - - def update_network(self, del_links, add_links, update_links): - t1 = time.perf_counter() - self.sftp.put(LINK_FILENAME, self.dir + '/' + LINK_FILENAME) - sn_remote_wait_output( - self.ssh, - f"python3 {self.dir}/sn_remote.py networks {self.id} {self.dir} " - ) - t3 = time.perf_counter() - print(f'[{self.id}]', t3 - t1, 's') - - def exec(self, node, cmd): - def generate(all_cmd): - try: - stdin, stdout, stderr = self.ssh.exec_command(all_cmd) - # 实时读取命令输出 - for line in stdout: - yield line - - for line in stderr: - yield line - - except Exception as e: - yield f"error: {str(e)}\n" - return generate(f'cd {self.dir} && python3 sn_remote.py exec {node} {cmd}') - - -class TopoSync(): - - def __init__(self, config_path): - with open(config_path, 'r') as f: - config = json.load(f) - self.constellation = config['constellation'] - self.api_url = config['api_url'] - self.time_step = config['step'] - self.machine_lst = config['machines'] - self.node_init = False - self.link_dict = {} - - def run(self): - self.last_links = set() - while True: - dt = datetime.datetime.now() - chkpt = 72 - for i in range(len(RECONF_CHKPT)): - if dt.second <= RECONF_CHKPT[i]: - chkpt = RECONF_CHKPT[i] - break - time.sleep(chkpt - dt.second) - dt = datetime.datetime.now() - - print('Time: ', dt, '\n') - url = (self.api_url - + f'?startTime={dt.isoformat()}' - + f'&constellation={self.constellation}') - print(url) - try: - res = requests.get(url) - if res.status_code != 200: - print(f'<{res.status_code}> Failed to fetch nodeInfo, skip.') - continue - with gzip.GzipFile(fileobj=io.BytesIO(res.content), mode='rb') as f: - node_info = json.load(f) - new_links, del_links, add_links, update_links = self._parse(node_info) - - with open(LINK_FILENAME, 'w') as f: - json.dump( - {'del_links': del_links, 'add_links': add_links, 'update_links': update_links}, - f - ) - - rmt_threads = [] - for rmt in self.remote_lst: - thread = threading.Thread( - target=rmt.update_network, - args=(del_links, add_links, update_links) - ) - thread.start() - rmt_threads.append(thread) - - self.last_links = new_links - - for thread in rmt_threads: - thread.join() - except Exception as e: - print('Error:', e) - - def _parse(self, node_info): - EPS = 0.01 - def distance(lla1, lla2): - RADIUS = 6371 - - lat_rad1, lng_rad1 = lla1[0] * math.pi / 180, lla1[1] * math.pi / 180 - lat_rad2, lng_rad2 = lla2[0] * math.pi / 180, lla2[1] * math.pi / 180 - - sa = math.sin((lat_rad1 - lat_rad2)/2) - sb = math.sin((lng_rad1 - lng_rad2)/2) - # FIXME: Altitude - return 2 * RADIUS * math.asin(math.sqrt( - sa * sa + math.cos(lat_rad1) * math.cos(lat_rad2) * sb * sb - )) - - if not self.node_init: - self._init_node(node_info) - - new_links = set() - for isl in node_info['link_ISL']: - src_id, dst_id = isl['src'], isl['dst'] - if src_id == dst_id: - continue - if src_id > dst_id: - src_id, dst_id = dst_id, src_id - new_links.add((src_id, dst_id)) - - for gsl in node_info['link_GSL_Up']: - # GS - SAT - src_id, dst_id = gsl['src'] + self.sat_nr, gsl['dst'] - new_links.add((src_id, dst_id)) - - # assign GS to machine of the first connected SAT - if not self.node_init: - print('Initializing ...') - for gsl in node_info['link_GSL_Up']: - src_id, dst_id = gsl['src'] + self.sat_nr, gsl['dst'] - if self.node_mid[src_id] is not None: - continue - self.node_mid[src_id] = self.node_mid[dst_id] - - for j in range(self.sat_nr, len(self.node_mid)): - if self.node_mid[j] is None: - self.node_mid[j] = random.randint(0, len(self.machine_lst)-1) - - with open(ASSIGN_FILENAME, 'w') as f: - json.dump( - { - 'node_name':self.node_name, - 'node_mid': self.node_mid, - 'ip': [machine['IP'] for machine in self.machine_lst], - }, - f - ) - - pyctr_dir = os.path.dirname(__file__) - subprocess.check_call( - "cd " + pyctr_dir + " && " - "gcc $(python3-config --cflags --ldflags)" - "-shared -fPIC -O2 pyctr.c -o pyctr.so", - shell=True - ) - - self.remote_lst = [] - for mid, machine in enumerate(self.machine_lst): - self.remote_lst.append(Remote( - mid, - machine['IP'], - machine['port'], - machine['username'], - machine['password'], - )) - self.node_init = True - - del_set = self.last_links.difference(new_links) - add_set = new_links.difference(self.last_links) - remain_set = new_links.intersection(self.last_links) - - del_links = [de for de in del_set] - add_links = [] - update_links = [] - for add in add_set: - delay_ms = (distance(self.node_lla[add[0]], self.node_lla[add[1]]) - / 299.792458) - if add in self.link_dict: - idx = self.link_dict[add][0] - else: - idx = len(self.link_dict) + 1 - self.link_dict[add] = [idx, delay_ms] - add_links.append((add[0], add[1], delay_ms, idx)) - - for remain in remain_set: - delay_ms = (distance(self.node_lla[remain[0]], self.node_lla[remain[1]]) - / 299.792458) - cur_delay = self.link_dict[remain][1] - if abs(delay_ms - cur_delay) <= EPS: - continue - update_links.append((remain[0], remain[1], delay_ms)) - - return new_links, del_links, add_links, update_links - - def _init_node(self, node_info): - self.node_name = [] - self.node_lla = [] - self.node_mid = [None] * (len(node_info['sat']) + len(node_info['ground'])) - - for idx, sat in enumerate(node_info['sat']): - if idx != sat['id']: - raise RuntimeError("'id' of sat is not incremented") - self.node_name.append(f'SAT{idx}') - self.node_lla.append((float(sat['lat']), float(sat['lon']), float(sat['alt']))) - - for i in range(len(self.node_name)): - self.node_mid[i] = random.randint(0, len(self.machine_lst)-1) - - self.sat_nr = len(self.node_name) - - for idx, gs in enumerate(node_info['ground']): - if idx != gs['id']: - raise RuntimeError("'id' of gs is not incremented") - self.node_name.append(f'GS{idx}') - self.node_lla.append((float(gs['lat']), float(gs['lon']), float(gs['alt']))) - - def exec(self, node, cmd): - idx = self.node_name.index(node) - return self.remote_lst[self.node_mid[idx]].exec(node, cmd) diff --git a/starrynet/sn_remote.py b/starrynet/sn_remote.py deleted file mode 100644 index 52f4bd5..0000000 --- a/starrynet/sn_remote.py +++ /dev/null @@ -1,481 +0,0 @@ -#!/usr/bin/python3 -import os -import subprocess -import sys -import json -import glob -import ctypes - -ASSIGN_FILENAME = 'assign.json' -LINK_FILENAME = 'link.json' -PID_FILENAME = 'container_pid.txt' -DAMAGE_FILENAME = 'damage_list.txt' - -NOT_ASSIGNED = 'NA' -VXLAN_PORT = '4789' -# FIXME -CLONE_NEWNET = 0x40000000 -libc = ctypes.CDLL(None) -main_net_fd = os.open('/proc/self/ns/net', os.O_RDONLY) - -def _pid_map(pid_path, pop = False): - global _pid_map_cache - if _pid_map_cache is None: - _pid_map_cache = {} - if not os.path.exists(pid_path): - print('Error: container index file not found, please create nodes') - exit(1) - with open(pid_path, 'r') as f: - for line in f: - if len(line) == 0 or line.isspace(): - continue - for name_pid in line.strip().split(): - if name_pid == NOT_ASSIGNED: - continue - name_pid = name_pid.split(':') - _pid_map_cache[name_pid[0]] = name_pid[1] - if pop: - ret = _pid_map_cache - _pid_map_cache = None - return ret - return _pid_map_cache - -def _get_params(path): - with open(path, 'r') as f: - obj = json.load(f) - node_name = obj['node_name'] - node_mid = obj['node_mid'] - ip_lst = obj['ip'] - return node_name, node_mid, ip_lst - -# name1 in local machine -def _del_link(name1, name2): - n1_n2 = f"{name2}" - fd = os.open('/run/netns/' + name1, os.O_RDONLY) - libc.setns(fd, CLONE_NEWNET) - os.close(fd) - subprocess.check_call(('ip', 'link', 'del', n1_n2)) - -def _init_if(name, if_name, addr, addr6, delay, bw, loss): - fd = os.open('/run/netns/' + name, os.O_RDONLY) - libc.setns(fd, CLONE_NEWNET) - os.close(fd) - subprocess.check_call(('ip', 'addr', 'add', addr, 'dev', if_name)) - subprocess.check_call(('ip', 'addr', 'add', addr6, 'dev', if_name)) - subprocess.check_call( - ('tc', 'qdisc', 'add', 'dev', if_name, 'root', - 'netem', 'delay', str(delay) + 'ms', 'loss', loss+'%', 'rate', bw+'Gbit') - ) - subprocess.check_call(('ip', 'link', 'set', if_name, 'up')) - -def _update_if(name, if_name, delay, bw, loss): - fd = os.open('/run/netns/' + name, os.O_RDONLY) - libc.setns(fd, CLONE_NEWNET) - os.close(fd) - update_loss = '100' if name in damage_set else loss - subprocess.check_call( - ('tc', 'qdisc', 'change', 'dev', if_name, 'root', - 'netem', 'delay', str(delay) + 'ms', 'rate', bw + 'Gbit', 'loss', update_loss + '%') - ) - -def _add_link_intra_machine(idx, name1, name2, prefix4, prefix6, delay, bw, loss): - n1_n2 = name2 - n2_n1 = name1 - libc.setns(main_net_fd, CLONE_NEWNET) - subprocess.check_call( - ('ip', 'link', 'add', n1_n2, 'netns', name1, - 'type', 'veth', 'peer', n2_n1, 'netns', name2) - ) - _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) - _init_if(name2, n2_n1, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) - -def _add_link_inter_machine(idx, name1, name2, remote_ip, prefix4, prefix6, delay, bw, loss): - n1_n2 = name2 - libc.setns(main_net_fd, CLONE_NEWNET) - subprocess.check_call( - ('ip', 'link', 'add', n1_n2, 'netns', name1, - 'type', 'vxlan', 'id', str(idx), 'remote', remote_ip, 'dstport', VXLAN_PORT) - ) - suffix6 = '::10/48' if name1 < name2 else '::40/48' - _init_if(name1, n1_n2, prefix4+'.10/24', prefix6 + suffix6, delay, bw, loss) - -def sn_init_nodes(dir, node_name, node_mid): - def _load_netns(pid, name): - netns_link = f'/run/netns/{name}' - if os.path.islink(netns_link): - os.remove(netns_link) - subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) - sn_container_check_call( - pid, - ('sysctl', 'net.ipv6.conf.all.forwarding=1'), - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - sn_container_check_call( - pid, - ('sysctl', 'net.ipv4.conf.all.forwarding=1'), - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) - - pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') - node_cnt = 0 - for name, mid in zip(node_name, node_mid): - if mid != machine_id: - pid_file.write(NOT_ASSIGNED + ' ') - continue - node_dir = f"{dir}/overlay/{name}" - os.makedirs(node_dir, exist_ok=True) - ctr_pid = pyctr.container_run(node_dir, name) - pid_file.write(name+':'+str(ctr_pid)+' ') - node_cnt += 1 - pid_file.write('\n') - print(f'[{machine_id}]: {node_cnt} nodes initialized') - - pid_file.close() - os.makedirs('/run/netns', exist_ok=True) - sn_operate_every_node(dir, _load_netns) - -def sn_update_network( - dir, node_name, node_mid, ip_lst, - isl_bw, isl_loss, gsl_bw, gsl_loss - ): - with open(dir + '/' + LINK_FILENAME, 'r') as f: - obj = json.load(f) - del_links, add_links, update_links = obj['del_links'], obj['add_links'], obj['update_links'] - - del_cnt, update_cnt, add_cnt = 0, 0, 0 - for de in del_links: - if node_mid[de[0]] == machine_id: - del_cnt += 1 - _del_link(node_name[de[0]], node_name[de[1]]) - elif node_mid[de[1]] == machine_id: - del_cnt += 1 - _del_link(node_name[de[1]], node_name[de[0]]) - - for add in add_links: - node1, node2, delay, idx = add[0], add[1], add[2], add[3] - prefix4 = f'10.{idx >> 8}.{idx & 0xFF}' - prefix6 = f'2001:{idx >> 8}:{idx & 0xFF}' - if node_mid[node1] == machine_id: - add_cnt += 1 - if node_mid[node2] == machine_id: - _add_link_intra_machine( - idx, node_name[node1], node_name[node2], - prefix4, prefix6, delay, isl_bw, isl_loss - ) - else: - _add_link_inter_machine( - idx, node_name[node1], node_name[node2], ip_lst[node_mid[node2]], - prefix4, prefix6, delay, isl_bw, isl_loss - ) - elif node_mid[node2] == machine_id: - add_cnt += 1 - _add_link_inter_machine( - idx, node_name[node2], node_name[node1], ip_lst[node_mid[node1]], - prefix4, prefix6, delay, isl_bw, isl_loss - ) - - for update in update_links: - node1, node2, delay = update[0], update[1], update[2] - if node_mid[node1] == machine_id: - _update_if(node_name[node1], node_name[node2], delay, isl_bw, isl_loss) - if node_mid[node2] == machine_id: - _update_if(node_name[node2], node_name[node1], delay, isl_bw, isl_loss) - - print(f"[{machine_id}]", - f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") - -def sn_container_check_call(pid, cmd, *args, **kwargs): - subprocess.check_call( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), - *args, **kwargs - ) - -def sn_container_run(pid, cmd, *args, **kwargs): - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), - *args, **kwargs - ) - -def sn_container_check_output(pid, cmd, *args, **kwargs): - return subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), - *args, **kwargs - ) - -def sn_operate_every_node(dir, func, *args): - pid_map = _pid_map(dir + '/' + PID_FILENAME) - for name, pid in pid_map.items(): - func(pid, name, *args) - -def get_IP(dir, node): - pid = _pid_map(f"{dir}/{PID_FILENAME}")[node] - addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in addr_lst: - dev_state_addrs = dev_state_addrs.split() - if len(dev_state_addrs) < 3: - continue - print(dev_state_addrs[0].split('@')[0], dev_state_addrs[2]) - -def sn_init_route_daemons(dir, conf_path, nodes): - def _init_route_daemon(pid, name): - bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' - sn_container_run(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) - if nodes == 'all': - sn_operate_every_node(dir, _init_route_daemon) - else: - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - nodes_lst = nodes.split(',') - for node in nodes_lst: - _init_route_daemon(pid_map[node], node) - -def sn_ping(dir, src, dst): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - # suppose src in this machine - src_pid = pid_map[src] - # TODO: dst in other machine - dst_pid = pid_map[dst] - - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - if dev_state_addrs[0].split('@')[0] == src: - break - dst_addr = dst_addr[:dst_addr.rfind('/')] - print('ping', src, dst_addr) - - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, - 'ping', '-c', '4', '-i', '0.01', dst_addr), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) - -def sn_iperf(dir, src, dst): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - # suppose src in this machine - src_pid = pid_map[src] - # TODO: dst in other machine - dst_pid = pid_map[dst] - - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - if dev_state_addrs[0].split('@')[0] == src: - break - dst_addr = dst_addr[:dst_addr.rfind('/')] - - server = subprocess.Popen( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, - 'iperf3', '-s'), - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_pid, - 'iperf3', '-c', dst_addr, '-t5'), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) - server.terminate() - -def sn_sr(dir, src, dst, nxt): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - # suppose src in this machine - src_pid = pid_map[src] - # TODO: dst in other machine - dst_pid = pid_map[dst] - - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_pid, - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - dst_prefix = dst_addr[:dst_addr.rfind('.')] + '.0/24' - subprocess.run( - ('nsenter', '-n', '-t', src_pid, - 'ip', 'route', 'add', dst_prefix, 'dev', nxt), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) - -def sn_check_route(dir, node): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - subprocess.run( - ('nsenter', '-n', '-t', pid_map[node], - 'route'), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) - -def sn_clean(dir): - damage_file = f"{dir}/{DAMAGE_FILENAME}" - if os.path.exists(damage_file): - os.remove(damage_file) - for ns_link in glob.glob(f"/run/netns/SH*O*S*"): - if os.path.islink(ns_link): - os.remove(ns_link) - for ns_link in glob.glob(f"/run/netns/G*"): - if os.path.islink(ns_link): - os.remove(ns_link) - pid_file = f"{dir}/{PID_FILENAME}" - if not os.path.exists(pid_file): - return - pid_map = _pid_map(pid_file, True) - for pid in pid_map.values(): - if pid == NOT_ASSIGNED: - continue - try: - os.kill(int(pid), 9) - except ProcessLookupError: - pass - os.remove(pid_file) - -def _change_sat_link_loss(pid, loss): - out = subprocess.check_output( - ('nsenter', '-t', pid, '-n', - 'tc', 'qdisc', 'show')).decode() - for line in out.splitlines(): - line = line.strip() - if len(line) == 0 or line.startswith('lo'): - continue - qdisc_netem_hd_dev_name_ = line.split() - dev_name = qdisc_netem_hd_dev_name_[4] - delay = qdisc_netem_hd_dev_name_[qdisc_netem_hd_dev_name_.index('delay') + 1] - subprocess.check_call( - ('nsenter', '-t', pid, '-n', - 'tc', 'qdisc', 'change', 'dev', dev_name, 'root', - 'netem', 'delay', delay, 'loss', loss+'%')) - -def sn_damage(dir, random_list): - with open(f"{dir}/{DAMAGE_FILENAME}", 'a') as f: - for node in random_list: - pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") - pid = pid_mat[node] - _change_sat_link_loss(pid, '100') - f.write(node + '\n') - print(f'[{machine_id}] damage node: {node}') - -def sn_recover(dir, sat_loss): - damage_file = f"{dir}/{DAMAGE_FILENAME}" - if not os.path.exists(damage_file): - return - with open(f"{dir}/{DAMAGE_FILENAME}", 'r') as f: - for node in f: - pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") - pid = pid_mat[node.strip()] - _change_sat_link_loss(pid, sat_loss) - print(f'[{machine_id}] recover sat: {node}') - os.remove(damage_file) - -if __name__ == '__main__': - _pid_map_cache = None - - if len(sys.argv) < 2: - print('Usage: sn_orchestrater.py ...') - exit(1) - cmd = sys.argv[1] - if cmd == 'exec': - pid_map = _pid_map(os.path.dirname(__file__) + '/' + PID_FILENAME) - if len(sys.argv) < 4: - print('Usage: sn_orchestrater.py exec ...') - exit(1) - if sys.argv[2] not in pid_map: - print('Error:', sys.argv[3], 'not found') - exit(1) - exit(subprocess.run( - ('nsenter', '-a', '-t', pid_map[sys.argv[2]], - *sys.argv[3:]) - ).returncode) - - if len(sys.argv) < 3: - machine_id = None - else: - try: - machine_id = int(sys.argv[2]) - except: - machine_id = None - if len(sys.argv) < 4: - workdir = os.path.dirname(__file__) - else: - workdir = sys.argv[3] - - # C module - try: - import pyctr - except ModuleNotFoundError: - subprocess.check_call( - "cd " + workdir + " && " - "gcc $(python3-config --cflags --ldflags)" - "-shared -fPIC -O2 pyctr.c -o pyctr.so", - shell=True - ) - import pyctr - - damage_set = set() - damage_file = workdir + '/' + DAMAGE_FILENAME - if os.path.exists(damage_file): - with open(workdir + '/' + DAMAGE_FILENAME, 'r') as f: - for line in f: - damage_set.add(line.strip()) - - node_name, node_mid, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) - if cmd == 'nodes': - sn_clean(workdir) - sn_init_nodes(workdir, node_name, node_mid) - elif cmd == 'list': - print(f"{'NODE':<20} STATE") - for name in _pid_map(workdir + '/' + PID_FILENAME): - print(f"{name:<20} {'Damaged' if name in damage_set else 'OK'}") - elif cmd == 'networks': - # lp = LineProfiler() - # sn_update_network = lp(sn_update_network) - # lp.add_function(_add_link_intra_machine) - # lp.add_function(_add_link_inter_machine) - sn_update_network( - workdir, node_name, node_mid, ip_lst, - '1', '0', '1', '0' - ) - # with open('report.txt', 'w') as f: - # lp.print_stats(f) - elif cmd == 'routed': - sn_init_route_daemons(workdir, workdir + '/bird.conf', sys.argv[4]) - elif cmd == 'IP': - get_IP(workdir, sys.argv[4]) - elif cmd == 'damage': - sn_damage(workdir, sys.argv[4].split(',')) - elif cmd == 'recovery': - sn_recover(workdir, sys.argv[4]) - elif cmd == 'clean': - sn_clean(workdir) - elif cmd == 'ping': - sn_ping(workdir, sys.argv[4], sys.argv[5]) - elif cmd == 'iperf': - sn_iperf(workdir, sys.argv[4], sys.argv[5]) - elif cmd == 'sr': - sn_sr(workdir, sys.argv[4], sys.argv[5], sys.argv[6]) - elif cmd == 'rtable': - sn_check_route(workdir, sys.argv[4]) - else: - print('Unknown command') - os.close(main_net_fd) diff --git a/starrynet/sn_utils.py b/starrynet/sn_utils.py index 2746e2d..5b67986 100644 --- a/starrynet/sn_utils.py +++ b/starrynet/sn_utils.py @@ -66,86 +66,3 @@ def sn_remote_wait_output(remote_ssh, cmd): for line in remote_ssh.exec_command(cmd, get_pty=True)[1]: print(line, end='') -def sn_check_utility(time_index, remote_ssh, local_dir): - result = sn_remote_cmd(remote_ssh, "vmstat") - f = open(os.path.join(local_dir, f"utility-info_{time_index}.txt"), "w") - f.write(result) - f.close() - -def sn_damage(remote_ssh, remote_ftp, remote_dir, local_dir, - ratio, damage_list, constellation_size): - print("Randomly setting damaged links...\n") - cumulated_damage_list = damage_list - random_list = [ - random.randint(0, constellation_size - 1) - for _ in range(int(constellation_size * ratio)) - ] - cumulated_damage_list.extend(random_list) - list_file = os.path.join(local_dir, 'mid_files', 'damage_list.txt') - numpy.savetxt(list_file, random_list) - remote_ftp.put(list_file, f'{remote_dir}/damage_list.txt') - sn_remote_cmd(remote_ssh, f"python3 {remote_dir}/orchestrater.py {remote_dir}") - print("Damage done.\n") - - -def sn_recover(remote_ssh, remote_ftp, remote_dir, local_dir, damage_list, sat_loss): - print("Recovering damaged links...\n") - list_file = os.path.join(local_dir, 'mid_files', 'damage_list.txt') - numpy.savetxt(list_file, damage_list) - remote_ftp.put(list_file, f'{remote_dir}/damage_list.txt') - sn_remote_cmd(remote_ssh, - f"python3 {remote_dir}/orchestrater.py {remote_dir} {sat_loss}" - ) - damage_list.clear() - print("Link recover done.\n") - -def sn_sr(src, des, target, netns_list, remote_ssh): - ifconfig_output = sn_remote_cmd(remote_ssh, - f"ip netns exec {netns_list[des - 1]} " - r"ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'").splitlines() - des_IP = sn_remote_cmd(remote_ssh, - f"ip netns exec {netns_list[des - 1]} ifconfig {ifconfig_output[0][:-1]} " - "| awk -F '[ :]+' 'NR==2{print $4}'").splitlines() - target_IP = sn_remote_cmd(remote_ssh, - f"ip netns exec {netns_list[target - 1]} ifconfig B{target}-eth{src} " - "| awk -F '[ :]+' 'NR==2{print $4}'").splitlines() - sn_remote_cmd(remote_ssh, - f"ip netns exec {netns_list[src - 1]} " - f" ip route del {des_IP[0][:-3]}0/24") - sn_remote_cmd(remote_ssh, - f"ip netns exec {netns_list[src - 1]} " - f"ip route add {des_IP[0][:-3]}0/24 dev B{src}-eth{target} via {target_IP[0]}") - print( - f"ip netns exec {netns_list[src - 1]} " - f"ip route add {des_IP[0][:-3]}0/24 dev B{src}-eth{target} via {target_IP[0]}" - ) - -def sn_perf(src, des, time_index, constellation_size, container_id_list, - file_path, configuration_file_path, remote_ssh): - if des <= constellation_size: - ifconfig_output = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig | sed 's/[ \t].*//;/^\(eth0\|\)\(lo\|\)$/d'") - des_IP = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig " + ifconfig_output[0][:-1] + - "|awk -F '[ :]+' 'NR==2{print $4}'") - else: - des_IP = sn_remote_cmd( - remote_ssh, "docker exec -it " + str(container_id_list[des - 1]) + - " ifconfig B" + str(des) + - "-default |awk -F '[ :]+' 'NR==2{print $4}'") - print('<>', src, des, des_IP) - sn_remote_cmd( - remote_ssh, - "docker exec -id " + str(container_id_list[des - 1]) + " iperf3 -s ") - print("iperf server success") - perf_result = sn_remote_cmd( - remote_ssh, "docker exec -i " + str(container_id_list[src - 1]) + - " iperf3 -c " + str(des_IP[0][:-1]) + " -t 5 ") - print("iperf client success") - f = open( - configuration_file_path + "/" + file_path + "/perf-" + str(src) + "-" + - str(des) + "_" + str(time_index) + ".txt", "w") - f.writelines(perf_result) - f.close() From 5d5b1b6c788e228e46e8198821da008f9185a6fb Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 3 Apr 2026 14:17:47 +0800 Subject: [PATCH 23/49] refactor: orchestrator daemon and command-based interaction. --- starrynet/pynetlink.c | 60 +- starrynet/sn_daemon_client.py | 397 +++++++++++ starrynet/sn_observer.py | 210 ++---- starrynet/sn_orchestrater.py | 984 +++++++++++----------------- starrynet/sn_orchestrator_daemon.py | 632 ++++++++++++++++++ starrynet/sn_synchronizer.py | 705 +++++++++++--------- 6 files changed, 1920 insertions(+), 1068 deletions(-) create mode 100644 starrynet/sn_daemon_client.py create mode 100644 starrynet/sn_orchestrator_daemon.py diff --git a/starrynet/pynetlink.c b/starrynet/pynetlink.c index 41cb043..f9e4fe9 100644 --- a/starrynet/pynetlink.c +++ b/starrynet/pynetlink.c @@ -1,4 +1,4 @@ -// pynetlink.c - Direct netlink interface for traffic control +#define PY_SSIZE_T_CLEAN #include // POSIX and Linux #include @@ -291,15 +291,14 @@ static int modify_link_(int sock_fd, const char *if_name, uint16_t nlmsg_type, return rtnetlink_request(sock_fd, nl_hdr, sizeof(buf), err_str, max_len); } -static int add_link_veth_(int sock_fd, pid_t ns_pid, const char *if_name, - pid_t peer_ns_pid, const char *peer_name, char *err_str, size_t max_len) { +static int add_link_veth_(int sock_fd, pid_t ns_pid, unsigned ifidx1, const char *if_name, + pid_t peer_ns_pid, unsigned ifidx2, const char *peer_name, char *err_str, size_t max_len) { struct timespec ts; uint32_t seq; struct nlmsghdr *nl_hdr; struct ifinfomsg *if_msg, *peer_ifi; struct rtattr *rta, *nest_linkinfo, *nest_infodata, *nest_infopeer; uint8_t buf[1024]; - char netns_path[256]; clock_gettime(CLOCK_MONOTONIC, &ts); seq = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; @@ -312,6 +311,7 @@ static int add_link_veth_(int sock_fd, pid_t ns_pid, const char *if_name, nl_hdr->nlmsg_pid = 0; if_msg = NLMSG_DATA(nl_hdr); memset(if_msg, 0, sizeof(*if_msg)); + if_msg->ifi_index = ifidx1; if_msg->ifi_family = AF_UNSPEC; // netns @@ -349,6 +349,7 @@ static int add_link_veth_(int sock_fd, pid_t ns_pid, const char *if_name, nest_infodata->rta_len = RTA_LENGTH(sizeof(*peer_ifi)); peer_ifi = (struct ifinfomsg*)RTA_DATA(nest_infodata); memset(peer_ifi, 0, sizeof(*peer_ifi)); + peer_ifi->ifi_index = ifidx2; peer_ifi->ifi_family = AF_UNSPEC; { // peer netns @@ -463,7 +464,6 @@ static int modify_route4(int sock_fd, uint16_t op, const struct in_addr *dst4, u struct nlmsghdr* nl_hdr; struct rtmsg* rt_msg; struct rtattr* rta; - struct in_addr dst_addr, gw_addr; uint8_t buf[512]; if(if_name) { @@ -539,7 +539,6 @@ static int modify_route6(int sock_fd, uint16_t op, const struct in6_addr *dst6, struct nlmsghdr* nl_hdr; struct rtmsg* rt_msg; struct rtattr* rta; - struct in6_addr dst_addr, gw_addr; uint8_t buf[512]; if(if_name) { @@ -658,9 +657,12 @@ static PyObject* pynetlink_modify_addr(PyObject* self, PyObject* args) { if(addr_len == sizeof(struct in_addr)) { result = modify_addr4_(sock_fd, add ? RTM_NEWADDR : RTM_DELADDR, if_name, (struct in_addr*)addr, prefix_len, err, sizeof(err)); - } else { + } else if(addr_len == sizeof(struct in6_addr)) { result = modify_addr6_(sock_fd, add ? RTM_NEWADDR : RTM_DELADDR, if_name, (struct in6_addr*)addr, prefix_len, err, sizeof(err)); + } else { + result = -1; + snprintf(err, sizeof(err), "Unknown type of address, length: %ld", addr_len); } if(temp_sock >= 0) @@ -829,7 +831,7 @@ static PyObject* pynetlink_modify_routes(PyObject* self, PyObject* args) { Py_ssize_t count; char err[256]; - if(!PyArg_ParseTuple(args, "iO", &sock_fd, &route_list)) { + if(!PyArg_ParseTuple(args, "Oi", &route_list, &sock_fd)) { return NULL; } @@ -896,10 +898,11 @@ static PyObject* pynetlink_modify_routes(PyObject* self, PyObject* args) { static PyObject* pynetlink_add_link_veth(PyObject* self, PyObject* args) { const char *if_name, *peer_name; int sock_fd = -1, temp_sock = -1, result; + unsigned ifidx1 = 0, ifidx2 = 0; pid_t netns = 0, netns_peer = 0; char err[256]; - if (!PyArg_ParseTuple(args, "isis|i", &netns, &if_name, &netns_peer, &peer_name, &sock_fd)) { + if (!PyArg_ParseTuple(args, "iIsiIs|i", &netns, &ifidx1, &if_name, &netns_peer, &ifidx2, &peer_name, &sock_fd)) { return NULL; } @@ -913,8 +916,10 @@ static PyObject* pynetlink_add_link_veth(PyObject* self, PyObject* args) { sock_fd = temp_sock; } - result = add_link_veth_(sock_fd, netns, if_name, - netns_peer, peer_name, err, sizeof(err)); + result = add_link_veth_( + sock_fd, netns, ifidx1, if_name, netns_peer, ifidx2 + , peer_name, err, sizeof(err) + ); if(temp_sock >= 0) close(temp_sock); @@ -967,6 +972,38 @@ static PyObject* pynetlink_add_link_vxlan(PyObject* self, PyObject* args) { Py_RETURN_NONE; } +static PyObject* pynetlink_netlink_request(PyObject* self, PyObject* args) { + void *nlmsg_data; + Py_ssize_t nlmsg_len; + int sock_fd = -1, temp_sock = -1, result; + char err[256]; + + if (!PyArg_ParseTuple(args, "y#|i", &nlmsg_data, &nlmsg_len, &sock_fd)) { + return NULL; + } + + // temporary socket for legacy version + if(sock_fd < 0) { + temp_sock = init_rtnetlink_sock_(); + if(temp_sock < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + sock_fd = temp_sock; + } + + result = rtnetlink_request(sock_fd, nlmsg_data, nlmsg_len, err, sizeof(err)); + if(temp_sock >= 0) + close(temp_sock); + + if (result < 0) { + PyErr_SetString(PyExc_RuntimeError, err); + return NULL; + } + + Py_RETURN_NONE; +} + // Define module methods static PyMethodDef PyNetlinkMethods[] = { {"init_socket", pynetlink_init_socket, METH_VARARGS, "Initialize netlink socket and return descriptor"}, @@ -979,6 +1016,7 @@ static PyMethodDef PyNetlinkMethods[] = { {"del_link", pynetlink_del_link, METH_VARARGS, "Delete a network interface using netlink"}, {"add_link_veth", pynetlink_add_link_veth, METH_VARARGS, "Create veth pair between two network namespaces"}, {"add_link_vxlan", pynetlink_add_link_vxlan, METH_VARARGS, "Create vxlan interface"}, + {"netlink_request", pynetlink_netlink_request, METH_VARARGS, "Send a netlink message and check response"}, {NULL, NULL, 0, NULL} }; diff --git a/starrynet/sn_daemon_client.py b/starrynet/sn_daemon_client.py new file mode 100644 index 0000000..6353828 --- /dev/null +++ b/starrynet/sn_daemon_client.py @@ -0,0 +1,397 @@ +#!/usr/bin/python3 +import socket +import json +import time +import logging +import paramiko +import struct +from typing import Dict, Any, Optional, List + +""" +StarryNet Daemon Client +用于与orchestrater守护进程通信的客户端库 +""" + +CHUNK_SIZE = 4096 # 4KB + +MSG_MAX_SIZE = 10 * 1024 * 1024 # 10MB + +class SSHDaemonClient: + """SSH client for communicating with orchestrater daemon""" + + def __init__(self, host: str, port: int, username: str = None, + password: str = None, timeout: int = 30): + """ + Initialize SSH client + + Args: + host: Remote host where daemon is running + port: SSH port of daemon + username: SSH username + password: SSH password + timeout: Connection timeout + """ + self.host = host + self.port = port + self.username = username + self.password = password + self.timeout = timeout + self.logger = logging.getLogger(__name__) + self._client = None + self._channel = None + self._connected = False + + def connect(self): + try: + self._client = paramiko.SSHClient() + self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self._client.connect( + hostname=self.host, + port=self.port, + username=self.username, + password=self.password, + timeout=self.timeout, + allow_agent=False, + look_for_keys=False + ) + + # Open interactive session + self._channel = self._client.invoke_shell() + self._connected = True + self.logger.info(f"SSH connected to daemon at {self.host}:{self.port}") + + except Exception as e: + raise Exception(f"SSH connection failed: {e}") + + def disconnect(self): + try: + if self._channel: + self._channel.close() + if self._client: + self._client.close() + except: + pass + finally: + self._channel = None + self._client = None + self._connected = False + self.logger.info("SSH connection closed") + + def _ensure_connected(self): + if not self._connected or not self._client or not self._channel: + self.connect() + + def _send_command_via_ssh(self, command: Dict[str, Any]): + self._ensure_connected() + + try: + # Send command as JSON string + command_json = json.dumps(command) + + # Log large commands for debugging + if len(command_json) > 4096: + self.logger.info(f"Sending large command ({len(command_json)} chars): {command_json[:200]}...") + + # Clear any pending data + while self._channel.recv_ready(): + self._channel.recv(4096) + + # Send command with length prefix + self._send_message_with_length_ssh(command_json) + + # Wait for response with length prefix + response_data = self._receive_message_with_length_ssh() + if response_data is None: + raise Exception("No response received") + + # Parse response + try: + response = json.loads(response_data.decode('utf-8')) + return response + except json.JSONDecodeError: + # If response is not JSON, wrap it + return { + "status": "error", + "message": f"Invalid JSON response: {response_data.decode('utf-8')}" + } + + except Exception as e: + self.logger.error(f"SSH command failed: {e}") + self._connected = False + + def _send_message_with_length_ssh(self, data: str): + try: + # Convert data to bytes + data_bytes = data.encode('utf-8') + + # Pack length as 4-byte integer (big-endian) + length_prefix = struct.pack('!I', len(data_bytes)) + + # Send length prefix followed by data + self._channel.sendall(length_prefix + data_bytes) + except Exception as e: + self.logger.error(f"Error sending message with length via SSH: {e}") + raise + + def _receive_message_with_length_ssh(self) -> bytes: + try: + # Read 4-byte length prefix + length_data = self._recv_exact_ssh(4) + if not length_data: + return None + + # Unpack length + message_length = struct.unpack('!I', length_data)[0] + + # Validate message length (prevent excessive memory usage) + if message_length > MSG_MAX_SIZE: + raise Exception(f"Message too large: {message_length} bytes") + + # Read the actual message data + message_data = self._recv_exact_ssh(message_length) + if not message_data: + return None + + return message_data + except Exception as e: + self.logger.error(f"Error receiving message with length via SSH: {e}") + raise + + def _recv_exact_ssh(self, length: int) -> bytes: + data = bytearray() + while len(data) < length: + chunk = self._channel.recv(length - len(data)) + if not chunk: + return None + data.extend(chunk) + return bytes(data) + + def send_config(self, shell_num: int, node_mid_dict: dict, ip_lst: list): + command = { + 'c': 'config', + 't': time.time(), + 'p': { + 'shell_num': shell_num, + 'node_mid_dict': node_mid_dict, + 'ip_lst': ip_lst + } + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to send config: {response.get('message')}") + + def init_nodes(self): + command = { + 'c': 'nodes', + 't': time.time(), + 'p': {} + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to init nodes: {response.get('message')}") + return response.get('result', []) + + def get_nodes(self): + command = { + 'c': 'list', + 't': time.time(), + 'p': {} + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to get nodes: {response.get('message')}") + + nodes = [] + for node_info in response.get('result', {}).get('nodes', []): + nodes.append(node_info.get('name', '')) + + return nodes + + def update_network(self, link_updates: dict): + """Update network with link changes sent directly + + Args: + link_updates: Dictionary containing link update information + Structure: { + "shell_updates": { + "shell_id": { + "del": [("node1", "node2"), ...], + "update": [("node1", "node2", "delay"), ...], + "add": [("node1", "node2", "delay", idx), ...] + }, ... + }, + "gs_updates": { + "del": [("gs1", "sat1"), ...], + "update": [("gs1", "sat1", "delay"), ...], + "add": [("gs1", "sat1", "delay", idx), ...] + }, + "isl_bw": "bandwidth", + "isl_loss": "loss", + "gsl_bw": "bandwidth", + "gsl_loss": "loss" + } + + Returns: + Response from daemon + """ + command = { + 'c': 'update_network_batch', + 't': time.time(), + 'p': link_updates + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to update network: {response.get('message')}") + + def damage_nodes(self, nodes: list): + command = { + 'c': 'damage', + 't': time.time(), + 'p': { + 'nodes': nodes + } + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to damage nodes: {response.get('message')}") + return response + + def recover_nodes(self, sat_loss: float): + command = { + 'c': 'recovery', + 't': time.time(), + 'p': { + 'loss': sat_loss + } + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to recover nodes: {response.get('message')}") + + def init_routing(self, nodes: str | List = 'all'): + command = { + 'c': 'routed', + 't': time.time(), + 'p': { + 'nodes': nodes + } + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to init routing: {response.get('message')}") + + def get_ip(self, node: str): + command = { + 'c': 'IP', + 't': time.time(), + 'p': { + 'node': node + } + } + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to get IP: {response.get('message')}") + return response.get('result', {}) + + def ping(self, src: str, dst: str): + command = { + 'c': 'ping', + 't': time.time(), + 'p': { + 'src': src, + 'dst': dst + } + } + return self._send_command_via_ssh(command) + + def iperf(self, cmds): + command = { + 'c': 'iperf', + 't': time.time(), + 'p': { + 'cmds': cmds + } + } + return self._send_command_via_ssh(command) + + def set_static_route(self, src: str, dst: str, next_hop: str): + """Set static route""" + command = { + 'c': 'sr', + 't': time.time(), + 'p': { + 'src': src, + 'dst': dst, + 'next': next_hop + } + } + return self._send_command_via_ssh(command) + + def netlink(self, routes): + command = { + 'c': 'netlink', + 't': time.time(), + 'p': { + 'routes': routes, + } + } + + def check_routing_table(self, node: str): + """Check routing table for node""" + command = { + 'c': 'rtable', + 't': time.time(), + 'p': { + 'node': node + } + } + return self._send_command_via_ssh(command) + + def clean(self): + """Clean up all resources""" + command = { + 'c': 'clean', + 't': time.time(), + 'p': {} + } + return self._send_command_via_ssh(command) + + def exec_command(self, node: str, cmd: str): + """Execute command in node""" + command = { + 'c': 'exec', + 't': time.time(), + 'p': { + 'node': node, + 'cmd': cmd + } + } + return self._send_command_via_ssh(command) + + def set_static_routes_batch(self, routes_config: dict): + """Set static routes for multiple nodes in batch + + Args: + routes_config: Dictionary mapping node names to lists of route tuples + Each tuple: (dst, gw, dev, metric) + + Returns: + Response from daemon + """ + command = { + 'c': 'sr_batch', + 't': time.time(), + 'p': { + 'routes_config': routes_config + } + } + return self._send_command_via_ssh(command) + + def __enter__(self): + """Context manager entry""" + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit""" + self.disconnect() diff --git a/starrynet/sn_observer.py b/starrynet/sn_observer.py index b592bfc..c3c5306 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -7,9 +7,15 @@ from skyfield.api import load, wgs84, EarthSatellite -def _gsl_least_delay(topo_t_shell, gs_cbf, antenna_num): +def _bound_gsl(antenna_elevation, altitude): + a = 6371 * np.cos(np.radians(90 + antenna_elevation)) + return a + np.sqrt(np.square(a) + np.square(altitude) + 2 * altitude * 6371) + +def _gsl_least_delay(topo_t_shell, gs_cbf, antenna_num, antenna_elevation, shell_lst): gsls_t_shell = [] # [[[ [gsl] for every gs] for every ts] for every shell] - for shellname, name_lst, sat_cbf_t, isls_t in topo_t_shell: + for shell_id, (name_lst, sat_cbf_t, sat_lla_t, isls_t) in enumerate(topo_t_shell): + altitude = shell_lst[shell_id]['altitude'] + bound_dist = _bound_gsl(antenna_elevation, altitude) gsls_t = [] for sat_cbf in sat_cbf_t: # (gs_num) op (sat_num) -> (gs_num, sat_num) @@ -20,9 +26,7 @@ def _gsl_least_delay(topo_t_shell, gs_cbf, antenna_num): gsls = [] for gs_dist in dist: gs_dist = gs_dist.flatten() - #TODO: elevation angle bound - # bound_mask = gs_dist < bound_dis - bound_mask = gs_dist == gs_dist + bound_mask = gs_dist < bound_dist sat_indices = np.arange(len(gs_dist))[bound_mask] gs_dist = gs_dist[bound_mask] sorted_sat = gs_dist.argsort() @@ -62,10 +66,6 @@ def to_cbf(lat_long):# the xyz coordinate system. y_mat = rho_mat * np.sin(phi_mat) return np.stack((x_mat, y_mat, z_mat), -1) -# def _bound_gsl(antenna_elevation, altitude): -# a = 6371 * np.cos(np.radians(90 + antenna_elevation)) -# return a + np.sqrt(np.square(a) + np.square(altitude) + 2 * altitude * 6371) - def _sat_name(shell_id, orbit_id, sat_id): return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' @@ -75,7 +75,7 @@ def _gs_name(gid): def _isl_grid(sat_cbf_t, shell_id, orbit_num, sat_num): # [[ [isl] for every satellite] for every t] isls_lst_t = [] - + sat_cbf_t = sat_cbf_t.reshape(-1, orbit_num, sat_num, 3) down_cbf_t = np.roll(sat_cbf_t, -1, 2) right_cbf_t = np.roll(sat_cbf_t, -1, 1) @@ -158,7 +158,7 @@ def _topo_walker_delta(dir, duration, step, shell_lst): name_lst = [_sat_name(i, oid, sid) for oid in range(orbit_number) for sid in range(sat_number)] - pos_dir = os.path.join(dir, shell['name'], 'position') + pos_dir = os.path.join(dir, f'shell{i}', 'position') os.makedirs(pos_dir, exist_ok=True) for t, lla_lst in enumerate(sat_lla_t): f = open(os.path.join(pos_dir, '%d.txt' % (t + 1)), 'w') @@ -168,7 +168,7 @@ def _topo_walker_delta(dir, duration, step, shell_lst): sat_cbf_t = to_cbf(sat_lla_t) isls_t = _isl_grid(sat_cbf_t, i, orbit_number, sat_number) - topo_t_shell.append((shell['name'], name_lst, sat_cbf_t, isls_t)) + topo_t_shell.append((name_lst, sat_cbf_t, sat_lla_t, isls_t)) return topo_t_shell def _topo_arbitrary(dir, duration, step, shell_lst): @@ -210,141 +210,6 @@ def _topo_arbitrary(dir, duration, step, shell_lst): topo_t_shell.append((shell['name'], name_lst, sat_cbf_t, isls_t)) return topo_t_shell -def _write_link_files(dir, topo_t_shell, gsls_t, GS_lat_long): - # ISL - idx_dict = {} - cnt = 0 - for shell_name, sat_name_lst, sat_cbf_t, isls_t in topo_t_shell: - isl_dir = os.path.join(dir, shell_name, 'isl') - os.makedirs(isl_dir, exist_ok=True) - for file in glob.glob(os.path.join(isl_dir, '*.txt')): - os.remove(file) - - isl_state = [list() for _ in range(len(sat_name_lst))] - for t, isls in enumerate(isls_t): - f_state = open(f"{isl_dir}/{t}-state.txt", 'w') - # for update - f_update = open(f"{isl_dir}/{t}.txt", 'w') - for sid, isl_lst in enumerate(isls): - # one line for each satellite - f_state.write(f"{sat_name_lst[sid]}:") - f_state.write(' '.join(f"{isl[0]},{isl[1]:.2f}" - for isl in isl_lst)) - f_state.write('\n') - - f_update.write(f"{sat_name_lst[sid]}|") - old_lst = isl_state[sid] - old_del = [True] * len(old_lst) - new_add = [True] * len(isl_lst) - update = [] - for i, old in enumerate(old_lst): - for j, new in enumerate(isl_lst): - if old[0] != new[0]: - continue - # same link, update - old_del[i] = False - new_add[j] = False - if abs(new[1] - old[1]) > 1e-2: - update.append(new) - else: - isl_lst[j] = old - # del some isls - f_update.write(' '.join( - f"{isl[0]}" - for isl, de in zip(old_lst, old_del) if de - ) + '|') - # update some isls - f_update.write(' '.join( - f"{isl[0]},{isl[1]:.2f}" - for isl in update - ) + '|') - # add some isls - add_lst = [] - for isl, add in zip(isl_lst, new_add): - if not add: - continue - key = f'{sat_name_lst[sid]}-{isl[0]}' - if key in idx_dict: - idx = idx_dict[key] - else: - cnt += 1 - idx_dict[key] = idx = cnt - add_lst.append(f"{isl[0]},{isl[1]:.2f},{idx}") - f_update.write(' '.join(add_lst)) - f_update.write('\n') - isl_state[sid] = isl_lst - f_state.close() - f_update.close() - # GSL - gsl_dir = os.path.join(dir, 'GS', 'gsl') - os.makedirs(gsl_dir, exist_ok=True) - for file in glob.glob(os.path.join(gsl_dir, '*.txt')): - os.remove(file) - - idx_dict = {} - cnt = 0 - gsl_state = [list() for _ in range(len(GS_lat_long))] - for t, gsls in enumerate(gsls_t): - f_state = open(f"{gsl_dir}/{t}-state.txt", 'w') - f_update = open(f"{gsl_dir}/{t}.txt", 'w') - for gid, gsl_lst in enumerate(gsls): - # one line for each ground station - f_state.write(f"{_gs_name(gid)}:") - f_state.write(' '.join(f"{gsl[0]},{gsl[1]:.2f}" - for gsl in gsl_lst)) - f_state.write('\n') - - f_update.write(f"{_gs_name(gid)}|") - # del some gsls - old_lst = gsl_state[gid] - old_del = [True] * len(old_lst) - new_add = [True] * len(gsl_lst) - update = [] - for i, old in enumerate(old_lst): - for j, new in enumerate(gsl_lst): - if old[0] == new[0]: - old_del[i] = False - new_add[j] = False - if abs(new[1] - old[1]) > 1e-2: - update.append(new) - else: - gsl_lst[j] = old # if not update, remain old delay - f_update.write(' '.join( - f"{gsl[0]}" - for gsl, de in zip(old_lst, old_del) if de - ) + '|') - # update some gsls - f_update.write(' '.join( - f"{gsl[0]},{gsl[1]:.2f}" - for gsl in update - ) + '|') - # add some gsls - add_lst = [] - for gsl, add in zip(gsl_lst, new_add): - if not add: - continue - key = f'{_gs_name(gid)}-{gsl[0]}' - if key in idx_dict: - idx = idx_dict[key] - else: - cnt += 1 - idx_dict[key] = idx = cnt - add_lst.append(f"{gsl[0]},{gsl[1]:.2f},{idx}") - f_update.write(' '.join(add_lst)) - f_update.write('\n') - gsl_state[gid] = gsl_lst - f_state.close() - f_update.close() - -def gen_topo(dir, duration, step, - shell_lst, isl_style, - GS_lat_long, antenna_number, antenna_elevation, gsl_style): - topo_t_shell = topo_styles[isl_style](dir, duration, step, shell_lst) - gsls_t = gsl_styles[gsl_style](topo_t_shell, to_cbf(GS_lat_long), antenna_number) - _write_link_files(dir, topo_t_shell, gsls_t, GS_lat_long) - sat_names_shell = [shell[1] for shell in topo_t_shell] - return sat_names_shell - def load_pos(path): f = open(path, 'r') lla_dict = {} @@ -372,6 +237,57 @@ def load_links_dict(path): 'Grid': _topo_walker_delta, 'Arbitrary': _topo_arbitrary, } + +class Observer: + """Observer class for managing topology computation and updates""" + + def __init__(self, configuration_file_path, gs_lat_long, antenna_number, antenna_elevation): + """Initialize Observer with configuration + + Args: + configuration_file_path: Path to configuration file + gs_lat_long: Ground station coordinates + antenna_number: Number of antennas + antenna_elevation: Antenna elevation angle + """ + import os + from .sn_utils import sn_load_file + + sn_args = sn_load_file(configuration_file_path) + self.shell_lst = sn_args.shell_lst + self.link_style = sn_args.link_style + self.link_policy = sn_args.link_policy + self.step = sn_args.step + self.duration = sn_args.duration + self.gs_lat_long = gs_lat_long + self.antenna_number = antenna_number + self.antenna_elevation = antenna_elevation + self.configuration_dir = os.path.dirname(os.path.abspath(configuration_file_path)) + self.experiment_name = sn_args.cons_name + '-' + sn_args.link_style + '-' + sn_args.link_policy + self.data_dir = os.path.join(self.configuration_dir, self.experiment_name) + + + def compute_topology(self, gs_links): + """Compute topology and generate updates + + Returns: + dict: Topology data and updates + """ + sat_t_shell = topo_styles[self.link_style](self.data_dir, self.duration, self.step, self.shell_lst) + if gs_links: + gsls_t = gs_links + else: + gsls_t = gsl_styles[self.link_policy]( + sat_t_shell, to_cbf(self.gs_lat_long), + self.antenna_number, self.antenna_elevation, self.shell_lst + ) + + return sat_t_shell, ( + list(_gs_name(i) for i in range(len(self.gs_lat_long))), + to_cbf(self.gs_lat_long), + gsls_t + ) + #TODO: More GSL styles gsl_styles = { 'LeastDelay':_gsl_least_delay, diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index ebba535..eb2af09 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -2,623 +2,439 @@ import os import subprocess import sys -import json -import glob import ctypes import time import ipaddress +import threading +import queue +from typing import Dict, List, Tuple +from collections import defaultdict # from line_profiler import LineProfiler - -""" -Used in the remote machine for link updating, initializing links, damaging and recovering links and other functionalities。 -author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) and Zeqi Lai (zeqilai@tsinghua.edu.cn) -""" - -ASSIGN_FILENAME = 'assign.json' -PID_FILENAME = 'container_pid.txt' -DAMAGE_FILENAME = 'damage_list.txt' +module_dir = os.path.dirname(__file__) +try: + import pyctr +except ModuleNotFoundError: + subprocess.check_call( + f"cd {module_dir} && " + "gcc $(python3-config --cflags --ldflags) " + "-shared -fPIC -O2 pyctr.c -o pyctr.so", + shell=True + ) + import pyctr +try: + import pynetlink +except ModuleNotFoundError: + subprocess.check_call( + f"cd {module_dir} && " + "gcc $(python3-config --cflags --ldflags) " + "-shared -fPIC -O2 pynetlink.c -o pynetlink.so", + shell=True + ) + import pynetlink NOT_ASSIGNED = 'NA' -VXLAN_PORT = '4789' # FIXME CLONE_NEWNET = 0x40000000 -libc = ctypes.CDLL(None) -main_net_fd = os.open('/proc/self/ns/net', os.O_RDONLY) - -def _pid_map(pid_path, pop = False): - global _pid_map_cache - if _pid_map_cache is None: - _pid_map_cache = {} - if not os.path.exists(pid_path): - print('Error: container index file not found, please create nodes') - exit(1) - with open(pid_path, 'r') as f: - for line in f: - if len(line) == 0 or line.isspace(): - continue - for name_pid in line.strip().split(): - if name_pid == NOT_ASSIGNED: - continue - name_pid = name_pid.split(':') - _pid_map_cache[name_pid[0]] = int(name_pid[1]) - if pop: - ret = _pid_map_cache - _pid_map_cache = None - return ret - return _pid_map_cache - -def _get_params(path): - with open(path, 'r') as f: - obj = json.load(f) - shell_num = obj['shell_num'] - node_mid_dict = obj['node_mid_dict'] - ip_lst = obj['ip'] - return shell_num, node_mid_dict, ip_lst - -def _parse_links(path): - del_lst, update_lst, add_lst = [], [], [] - f = open(path, 'r') - for line in f: - toks = line.strip().split('|') - node = toks[0] - if len(toks[1]) > 0: - for link in toks[1].split(' '): - del_lst.append((node, link)) - if len(toks[2]) > 0: - for link in toks[2].split(' '): - peer_delay = link.split(',') - update_lst.append((node, peer_delay[0], peer_delay[1])) - if len(toks[3]) > 0: - for link in toks[3].split(' '): - peer_delay_idx = link.split(',') - add_lst.append((node, peer_delay_idx[0], peer_delay_idx[1], int(peer_delay_idx[2]))) - f.close() - return del_lst, update_lst, add_lst - -def _switch_netns(node): - pid_fd = os.open(f'/proc/{node}/ns/net', os.O_RDONLY) - libc.setns(pid_fd, CLONE_NEWNET) +NETNS_DIR = '/run/netns' + +_libc = ctypes.CDLL(None) + +def _switch_netns(node_pid: int): + pid_fd = os.open(f'/proc/{node_pid}/ns/net', os.O_RDONLY) + _libc.setns(pid_fd, CLONE_NEWNET) os.close(pid_fd) -# name1 in local machine -def _del_link(node, ifname): - _switch_netns(node) - - pynetlink.del_link(ifname) - -def _init_if(node, if_name, addr, addr6, delay, bw, loss): - addr = ipaddress.IPv4Interface(addr) - addr6 = ipaddress.IPv6Interface(addr6) - _switch_netns(node) - pynetlink.modify_addr(True, if_name, addr.packed, addr.network.prefixlen) - pynetlink.modify_addr(True, if_name, addr6.packed, addr6.network.prefixlen) - pynetlink.traffic_control(if_name, delay, bw, loss) - pynetlink.if_up(if_name) - -def _update_if(node, if_name, delay, bw, loss): - _switch_netns(node) - pynetlink.traffic_control(if_name, delay, bw, loss) - -def _update_link_intra_machine(node1, ifname1, node2, ifname2, delay, bw, loss): - _update_if(node1, ifname1, delay, bw, loss) - _update_if(node2, ifname2, delay, bw, loss) - -# name1 in local machine -def _update_link_local(node, ifname, delay, bw, loss): - _update_if(node, ifname, delay, bw, loss) - -def _add_link_intra_machine(idx, node1, ifname1, node2, ifname2, prefix4, prefix6, delay, bw, loss): - libc.setns(main_net_fd, CLONE_NEWNET) - # Use pynetlink to create veth pair instead of subprocess - pynetlink.add_link_veth(node1, ifname1, node2, ifname2) - _init_if(node1, ifname1, prefix4+'.10/24', prefix6 + '::10/48', delay, bw, loss) - _init_if(node2, ifname2, prefix4+'.40/24', prefix6 + '::40/48', delay, bw, loss) +class Interface: + """Interface object representing a network interface in the orchestrator""" -def _add_link_inter_machine(idx, node, ifname, remote_ip, addr4, addr6, delay, bw, loss): - remote_addr = ipaddress.ip_address(remote_ip) - libc.setns(main_net_fd, CLONE_NEWNET) - # Use pynetlink to create vxlan interface instead of subprocess - pynetlink.add_link_vxlan(node, ifname, idx, remote_addr.packed) - _init_if(node, ifname, addr4, addr6, delay, bw, loss) - -def sn_init_nodes(dir, shell_num, node_mid_dict): - def _load_netns(pid, name): - netns_dir = '/run/netns' - os.makedirs(netns_dir, exist_ok=True) - netns_link = f'{netns_dir}/{name}' - if not os.path.exists(netns_link): - subprocess.check_call(('ln', '-s', f'/proc/{pid}/ns/net', netns_link)) - sn_container_check_call( - pid, - ('sysctl', 'net.ipv6.conf.all.forwarding=1'), - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - sn_container_check_call( - pid, - ('sysctl', 'net.ipv4.conf.all.forwarding=1'), - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) + def __init__(self, ifname: str): + self.ifname = ifname - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) - - pid_file = open(dir + '/' + PID_FILENAME, 'w', encoding='utf-8') - sat_cnt = 0 - for node, mid in node_mid_dict.items(): - if mid != machine_id: - pid_file.write(NOT_ASSIGNED + ' ') - continue - node_dir = f"{dir}/overlay/{node}" - sat_cnt += 1 - os.makedirs(node_dir, exist_ok=True) - pid_file.write(node+':'+str(pyctr.container_run(node_dir, node))+' ') - pid_file.write('\n') - print(f'[{machine_id}]: {sat_cnt} nodes initialized') - pid_file.close() - sn_operate_every_node(dir, _load_netns) - -def sn_update_network( - dir, ts, shell_num, node_mid_dict, ip_lst, - isl_bw, isl_loss, gsl_bw, gsl_loss - ): - pid_map = _pid_map(dir + '/' + PID_FILENAME) - for shell_id in range(shell_num): - shell_dir = f"{dir}/shell{shell_id}" - if not os.path.exists(shell_dir): - continue - del_cnt, update_cnt, add_cnt = 0, 0, 0 - del_lst, update_lst, add_lst = _parse_links(f'{shell_dir}/{ts}.txt') - for sat_name, isl_sat in del_lst: - if node_mid_dict[sat_name] == machine_id: - del_cnt += 1 - _del_link(pid_map[sat_name], isl_sat) - elif node_mid_dict[isl_sat] == machine_id: - del_cnt += 1 - _del_link(pid_map[isl_sat], sat_name) - for sat_name, isl_sat, delay in update_lst: - if node_mid_dict[sat_name] == machine_id: - update_cnt += 1 - if node_mid_dict[isl_sat] == machine_id: - _update_link_intra_machine( - pid_map[sat_name], isl_sat, pid_map[isl_sat], sat_name, - delay, isl_bw, isl_loss - ) - else: - _update_link_local(pid_map[sat_name], isl_sat, delay, isl_bw, isl_loss) - elif node_mid_dict[isl_sat] == machine_id: - update_cnt += 1 - _update_link_local(pid_map[isl_sat], sat_name, delay, isl_bw, isl_loss) - for sat_name, isl_sat, delay, idx in add_lst: - if node_mid_dict[sat_name] == machine_id: - add_cnt += 1 - if node_mid_dict[isl_sat] == machine_id: - _add_link_intra_machine( - idx, pid_map[sat_name], isl_sat, pid_map[isl_sat], sat_name, - f'10.{idx >> 8}.{idx & 0xFF}', f'2001:{idx >> 8}:{idx & 0xFF}', - delay, isl_bw, isl_loss - ) - else: - addr4 = f'10.{idx >> 8}.{idx & 0xFF}.{'10' if sat_name < isl_sat else '40'}/24' - addr6 = f'2001:{idx >> 8}:{idx & 0xFF}::{'10' if sat_name < isl_sat else '40'}/48' - _add_link_inter_machine( - idx, pid_map[sat_name], isl_sat, ip_lst[node_mid_dict[isl_sat]], - addr4, addr6, - delay, isl_bw, isl_loss - ) - elif node_mid_dict[isl_sat] == machine_id: - add_cnt += 1 - addr4 = f'10.{idx >> 8}.{idx & 0xFF}.{'10' if sat_name < isl_sat else '40'}/24' - addr6 = f'2001:{idx >> 8}:{idx & 0xFF}::{'10' if sat_name < isl_sat else '40'}/48' - _add_link_inter_machine( - idx, pid_map[isl_sat], sat_name, ip_lst[node_mid_dict[sat_name]], - addr4, addr6, - delay, isl_bw, isl_loss - ) - print(f"[{machine_id}] Shell {shell_id}:", - f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") - - gs_dir = f"{dir}/GS" - if not os.path.exists(gs_dir): - return - del_cnt, update_cnt, add_cnt = 0, 0, 0 - del_lst, update_lst, add_lst = _parse_links(f'{gs_dir}/{ts}.txt') - for gs, sat in del_lst: - if node_mid_dict[gs] == machine_id: - del_cnt += 1 - _del_link(pid_map[gs], sat) - elif node_mid_dict[sat] == machine_id: - del_cnt += 1 - _del_link(pid_map[sat], gs) - for gs, sat, delay in update_lst: - if node_mid_dict[gs] == machine_id: - update_cnt += 1 - if node_mid_dict[sat] == machine_id: - _update_link_intra_machine( - pid_map[gs], sat, pid_map[sat], gs, - delay, gsl_bw, gsl_loss - ) - else: - _update_link_local(pid_map[gs], sat, delay, gsl_bw, gsl_loss) - elif node_mid_dict[sat] == machine_id: - update_cnt += 1 - _update_link_local(pid_map[sat], gs, delay, gsl_bw, gsl_loss) - for gs, sat, delay, idx in add_lst: - if node_mid_dict[gs] == machine_id: - add_cnt += 1 - if node_mid_dict[sat] == machine_id: - _add_link_intra_machine( - idx, pid_map[gs], sat, pid_map[sat], gs, - f'9.{idx >> 8}.{idx & 0xFF}', f'2002:{idx >> 8}:{idx & 0xFF}', - delay, gsl_bw, gsl_loss - ) - else: - addr4 = f'9.{idx >> 8}.{idx & 0xFF}.40/24' - addr6 = f'2002:{idx >> 8}:{idx & 0xFF}::40/48' - _add_link_inter_machine( - idx, pid_map[gs], sat, ip_lst[node_mid_dict[sat]], - addr4, addr6, - delay, gsl_bw, gsl_loss - ) - elif node_mid_dict[sat] == machine_id: - add_cnt += 1 - addr4 = f'9.{idx >> 8}.{idx & 0xFF}.10/24' - addr6 = f'2002:{idx >> 8}:{idx & 0xFF}::10/48' - _add_link_inter_machine( - idx, pid_map[sat], gs, ip_lst[node_mid_dict[gs]], - addr4, addr6, - delay, gsl_bw, gsl_loss - ) - print(f"[{machine_id}] GSL:", - f"{del_cnt} deleted, {update_cnt} updated, {add_cnt} added.") +class Node: + """Node object representing a network node in the orchestrator""" + + def __init__(self, name: str, node_dir: str, node_id: int = 0): + self.name = name + self.node_id = node_id + self.pid = pyctr.container_run(node_dir, name) + netns_link = f'{NETNS_DIR}/{name}' + if os.path.islink(netns_link): + os.unlink(netns_link) + subprocess.check_call(('ln', '-s', f'/proc/{self.pid}/ns/net', netns_link)) + _switch_netns(self.pid) + self.socket_fd = pynetlink.init_socket(self.pid) + + self.interfaces = {} + + # Initialize loopback addresses + self._init_loopback() - # GSL_TIMING_DIR = 'gsl_timing_all_change' - # timing_dir = os.path.join(dir, GSL_TIMING_DIR) - # os.makedirs(timing_dir, exist_ok=True) - # timing_file = os.path.join(timing_dir, f"gsl_timing_{ts}.txt") - # with open(timing_file, 'w') as f: - # f.write(f"{del_time:.6f} {update_time:.6f} {add_time:.6f}\n") + def __lt__(self, other): + return self.pid < other.pid + def _init_loopback(self): + ipv4_lo = f"16.{(self.node_id >> 8) & 0xFF}.{self.node_id & 0xFF}.1/32" + ipv6_lo = f"2000::{self.node_id:04x}/128" -def sn_container_check_call(pid, cmd, *args, **kwargs): - subprocess.check_call( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(pid), *cmd), - *args, **kwargs - ) + _switch_netns(self.pid) + pynetlink.if_up('lo', self.socket_fd) + try: + fd = os.open('/proc/sys/net/mpls/conf/lo/input', os.O_WRONLY) + os.write(fd, b'1') + os.close(fd) + except FileNotFoundError: + pass -def sn_container_run(pid, cmd, *args, **kwargs): - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(pid), *cmd), - *args, **kwargs - ) + try: + addr4 = ipaddress.IPv4Interface(ipv4_lo) + pynetlink.modify_addr(True, 'lo', addr4.packed, addr4.network.prefixlen, self.socket_fd) + self.loopback_ipv4 = addr4 + except Exception as e: + print(f"Warning: Failed to set IPv4 loopback {ipv4_lo} for node {self.name}: {e}") + + try: + addr6 = ipaddress.IPv6Interface(ipv6_lo) + pynetlink.modify_addr(True, 'lo', addr6.packed, addr6.network.prefixlen, self.socket_fd) + self.loopback_ipv6 = addr6 + except Exception as e: + print(f"Warning: Failed to set IPv6 loopback {ipv6_lo} for node {self.name}: {e}") -def sn_container_check_output(pid, cmd, *args, **kwargs): - return subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', pid, *cmd), - *args, **kwargs - ) -def sn_operate_every_node(dir, func, *args): - pid_map = _pid_map(dir + '/' + PID_FILENAME) - for name, pid in pid_map.items(): - func(pid, name, *args) - -def get_IP(dir, node): - pid = _pid_map(f"{dir}/{PID_FILENAME}")[node] - addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(pid), - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in addr_lst: - dev_state_addrs = dev_state_addrs.split() - if len(dev_state_addrs) < 3: - continue - print(dev_state_addrs[0].split('@')[0], dev_state_addrs[2]) - -def sn_init_route_daemons(dir, conf_path, nodes): - def _init_route_daemon(pid, name): - bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' - sn_container_run(pid, ('bird', '-c', conf_path, '-s', bird_ctl_path)) - if nodes == 'all': - sn_operate_every_node(dir, _init_route_daemon) - else: - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - nodes_lst = nodes.split(',') - for node in nodes_lst: - _init_route_daemon(pid_map[node], node) - -def sn_ping(dir, src, dst): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - # suppose src in this machine - src_pid = pid_map[src] - # TODO: dst in other machine - dst_pid = pid_map[dst] + def __del__(self): + """Destructor to cleanup resources""" + try: + os.kill(self.pid, 9) + netns_link = f'{NETNS_DIR}/{self.name}' + if os.path.islink(netns_link): + os.remove(netns_link) + if self.socket_fd >= 0: + pynetlink.close_socket(self.socket_fd) + except: + pass # Ignore errors during cleanup - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - if dev_state_addrs[0].split('@')[0] == src: - break - dst_addr = dst_addr[:dst_addr.rfind('/')] - print('ping', src, dst_addr) - - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(src_pid), - 'ping', '-c', '4', '-i', '0.01', dst_addr), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) + def run_command(self, command, *args, **kwargs): + """Run a command inside the node's network namespace""" + return subprocess.Popen( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(self.pid), *command), + *args, **kwargs + ) + + def register_if(self, if_name: str): + self.interfaces[if_name] = Interface(if_name) + + def init_if(self, if_name: str, addr: str, addr6: str, delay: str, bw: str, loss: str): + addr = ipaddress.IPv4Interface(addr) + addr6 = ipaddress.IPv6Interface(addr6) + _switch_netns(self.pid) + pynetlink.modify_addr(True, if_name, addr.packed, addr.network.prefixlen, self.socket_fd) + pynetlink.modify_addr(True, if_name, addr6.packed, addr6.network.prefixlen, self.socket_fd) + pynetlink.traffic_control(if_name, delay, bw, loss, self.socket_fd) + pynetlink.if_up(if_name, self.socket_fd) + try: + fd = os.open(f'/proc/sys/net/mpls/conf/{if_name}/input', os.O_WRONLY) + os.write(fd, b'1') + os.close(fd) + except FileNotFoundError: + pass -def sn_iperf(dir, src, dst): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - # suppose src in this machine - src_pid = pid_map[src] - # TODO: dst in other machine - dst_pid = pid_map[dst] + def update_if(self, if_name: str, delay: str, bw: str, loss: str): + _switch_netns(self.pid) + pynetlink.traffic_control(if_name, delay, bw, loss, self.socket_fd) + + def modify_routes(self, routes: List[Tuple[str, str, str, int]]): + _switch_netns(self.pid) + pynetlink.modify_routes(routes, self.socket_fd) + + def del_if(self, ifname: str): + _switch_netns(self.pid) + pynetlink.del_link(ifname, self.socket_fd) + +class OrchestratorContext: + """Context object to maintain state for orchestrator""" - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - if dev_state_addrs[0].split('@')[0] == src: - break - dst_addr = dst_addr[:dst_addr.rfind('/')] - - server = subprocess.Popen( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), - 'iperf3', '-s'), - stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(src_pid), - 'iperf3', '-c', dst_addr, '-t5'), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) - server.terminate() - -def sn_sr(dir, src, dst, nxt): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - # suppose src in this machine - src_pid = pid_map[src] - # TODO: dst in other machine - dst_pid = pid_map[dst] - - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(dst_pid), - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - dst_prefix = dst_addr[:dst_addr.rfind('.')] + '.0/24' - subprocess.run( - ('nsenter', '-n', '-t', src_pid, - 'ip', 'route', 'add', dst_prefix, 'dev', nxt), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) + def __init__(self, workdir): + self.workdir = workdir + self.damage_dict = {} -def sn_check_route(dir, node): - pid_map = _pid_map(f"{dir}/{PID_FILENAME}") - subprocess.run( - ('nsenter', '-n', '-t', str(pid_map[node]), - 'route'), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) + self._main_net_sock_fd = pynetlink.init_socket() -def sn_clean(dir): - damage_file = f"{dir}/{DAMAGE_FILENAME}" - if os.path.exists(damage_file): - os.remove(damage_file) - for ns_link in glob.glob(f"/run/netns/SH*O*S*"): - if os.path.islink(ns_link): - os.remove(ns_link) - for ns_link in glob.glob(f"/run/netns/G*"): - if os.path.islink(ns_link): - os.remove(ns_link) - pid_file = f"{dir}/{PID_FILENAME}" - if not os.path.exists(pid_file): - return - pid_map = _pid_map(pid_file, True) - for pid in pid_map.values(): - if pid == NOT_ASSIGNED: - continue + self.nodes: Dict[str, Node] = {} + + self.cmd_to_start = queue.PriorityQueue() + self.cmd_cnt_dict = defaultdict(int) + self.cmd_pending = set() + threading.Thread(target=self._check_commands, daemon=True).start() + + def __del__(self): try: - os.kill(pid, 9) - except ProcessLookupError: + os.close(self._main_net_fd) + except: pass - os.remove(pid_file) - -def _change_sat_link_loss(pid, loss): - out = subprocess.check_output( - ('nsenter', '-t', pid, '-n', - 'tc', 'qdisc', 'show')).decode() - for line in out.splitlines(): - line = line.strip() - if len(line) == 0 or line.startswith('lo'): - continue - qdisc_netem_hd_dev_name_ = line.split() - dev_name = qdisc_netem_hd_dev_name_[4] - delay = qdisc_netem_hd_dev_name_[qdisc_netem_hd_dev_name_.index('delay') + 1] - subprocess.check_call( - ('nsenter', '-t', pid, '-n', - 'tc', 'qdisc', 'change', 'dev', dev_name, 'root', - 'netem', 'delay', delay, 'loss', loss+'%')) - -def sn_damage(dir, random_list): - with open(f"{dir}/{DAMAGE_FILENAME}", 'a') as f: - for node in random_list: - pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") - pid = pid_mat[node] + + def _check_commands(self): + cur = None + while True: + time.sleep(0.1) + now = time.perf_counter() + + if cur is None: + try: + cur = self.cmd_to_start.get(block=False) + except queue.Empty: + pass + + while cur is not None and cur[0] <= now: + _, node, cmdline = cur + self.cmd_cnt_dict[node.name] += 1 + cmd_id = self.cmd_cnt_dict[node.name] + fd = os.open( + f'{self.workdir}/cmd_{node.name}_{cmd_id}_{cmdline[0]}.out', + os.O_WRONLY | os.O_CREAT | os.O_TRUNC + ) + os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) + proc = node.run_command(cmdline, stdout=fd, stderr=subprocess.STDOUT) + os.close(fd) + self.cmd_pending.add(proc) + try: + cur = self.cmd_to_start.get(False) + except queue.Empty: + cur = None + + finished = [] + for proc in self.cmd_pending: + if proc.poll() is None: + continue + finished.append(proc) + for proc in finished: + self.cmd_pending.remove(proc) + + def clean(self): + for node in self.nodes.values(): + del node + + self.nodes.clear() + self.damage_dict.clear() + + def init_nodes(self, base_dir, node_configs): + """ + Initialize nodes with unique loopback addresses + + Args: + base_dir: Base directory for node overlays + node_configs: Dict mapping node names to their global unique IDs + e.g., {'node1': 1, 'node2': 5, 'node3': 10} + """ + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) + subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) + subprocess.check_call(('sysctl', 'net.ipv4.fib_multipath_hash_policy=1')) + subprocess.check_call(('sysctl', 'net.ipv4.conf.all.rp_filter=0')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) + subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) + subprocess.run(('sysctl', 'net.ipv6.fib_multipath_hash_policy=1')) + subprocess.run(('sysctl', 'net.mpls.platform_labels=10000')) + + self.clean() + os.makedirs(NETNS_DIR, exist_ok=True) + + for node_name, node_id in node_configs.items(): + node_dir = f"{base_dir}/overlay/{node_name}" + os.makedirs(node_dir, exist_ok=True) + self.nodes[node_name] = Node(node_name, node_dir, node_id=node_id) + + _switch_netns(os.getpid()) + return {name: (node.loopback_ipv4.compressed, node.loopback_ipv6.compressed) for name, node in self.nodes.items()} + + def add_link_intra_machine(self, + name1: str, name2: str, + src_ifidx:int, src_addr4: str, src_addr6: str, + dst_ifidx:int, dst_addr4: str, dst_addr6, + delay: str, bw: str, loss: str + ): + node1 = self.nodes.get(name1) + node2 = self.nodes.get(name2) + if node1 is None or node2 is None: + return + + pynetlink.add_link_veth(node1.pid, src_ifidx, name2, node2.pid, dst_ifidx, name1, self._main_net_sock_fd) + node1.init_if(name2, src_addr4, src_addr6, delay, bw, loss) + node2.init_if(name1, dst_addr4, dst_addr6, delay, bw, loss) + + def add_link_inter_machine(self, + name1: str, name2: str, + remote_ip: str, addr4: str, addr6: str, + delay: str, bw: str, loss: str + ): + node1 = self.nodes.get(name1) + if node1 is None: + return + + pynetlink.add_link_vxlan( + node1.pid, name2, + idx, ipaddress.ip_address(remote_ip).packed, + self._main_net_sock_fd + ) + node1.init_if(name2, addr4, addr6, delay, bw, loss) + + def get_ip(self, node_name: str): + node = self.nodes.get(node_name) + if node is None: + return [] + addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(node.pid), + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + filtered_addrs = [] + for dev_state_addrs in addr_lst: + dev_state_addrs = dev_state_addrs.split() + if len(dev_state_addrs) < 3: + continue + filtered_addrs.append((dev_state_addrs[0].split('@')[0], dev_state_addrs[2])) + return filtered_addrs + + def init_route_daemons(self, conf_path: str, nodes: str): + bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' + if nodes == 'all': + nodes_lst = self.nodes.keys() + else: + nodes_lst = nodes.split(',') + + for node_name in nodes_lst: + node = self.nodes.get(node_name) + if node is None: + continue + proc = node.run_command(('bird', '-c', conf_path, '-s', bird_ctl_path)) + proc.wait() + + def ping(self, src: str, dst: str): + src_node = self.nodes.get(src) + dst_node = self.nodes.get(dst) + if src_node is None or dst_node is None: + return + + dst_addr_lst = subprocess.check_output( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_node.pid, + 'ip', '-br', 'addr', 'show') + ).decode().splitlines() + for dev_state_addrs in dst_addr_lst: + dev_state_addrs = dev_state_addrs.split() + if dev_state_addrs[0] == 'lo': + continue + dst_addr = dev_state_addrs[2] + if dev_state_addrs[0].split('@')[0] == src: + break + dst_addr = dst_node.loopback_ipv4.ip.compressed + + subprocess.run( + ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_node.pid, + 'ping', '-c', '4', '-i', '0.01', dst_addr), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + + def iperf(self, cmds): + time_point = time.perf_counter() + for cmd in cmds: + src_node = self.nodes.get(cmd[0]) + dst_node = self.nodes.get(cmd[1]) + if src_node is None or dst_node is None: + continue + + dst_addr = dst_node.loopback_ipv4.ip.compressed + self.cmd_to_start.put((time_point, dst_node, ('iperf3', '-s'))) + self.cmd_to_start.put((time_point + 0.5, src_node, ('iperf3', '-c', dst_addr, *cmd[2:]))) + + def exec(self, node_name: str, cmd: str): + """Execute command in node using context""" + node = self.nodes.get(node_name) + if node is None: + return None + + return subprocess.run( + 'nsenter -m -u -i -n -p -t ' + str(node.pid) + ' ' + cmd, + shell=True, + capture_output=True, + text=True + ) + + def netlink(self, routes): + for name, nlmsg in routes: + node = self.nodes.get(name) + if node is None: + print('warning: ', name, 'not exists, netlink skipped') + continue + + pynetlink.netlink_request(node.pid, nlmsg) + + def check_route(self, node_name: str): + node = self.nodes.get(node_name) + if node is None: + print(f"Node {node_name} not found") + return + + subprocess.run( + ('nsenter', '-n', '-t', node.pid, + 'route'), + stdout=sys.stdout, stderr=subprocess.STDOUT + ) + + def damage(self, random_list: List[str]): + for node_name in random_list: + node = self.nodes.get(node_name) + if node is None: + print(f"Node {node_name} not found") + continue + + _switch_netns(node.pid) out = subprocess.check_output( - ('nsenter', '-t', str(pid), '-n', - 'ip', '-br', 'addr', 'show')).decode() + ('ip', '-br', 'addr', 'show')).decode() dev_lst = [] - f.write(node + '|') + for line in out.splitlines(): line = line.strip() if len(line) == 0 or line.startswith('lo'): continue toks = line.split() dev_name = toks[0].split('@')[0] - for addr in toks[1:]: - if ':' in addr: + addr = None + for tok in toks[1:]: + if ':' in tok: # found first ip6 addr - subprocess.check_call( - ('nsenter', '-t', str(pid), '-n', - 'ip', 'link', 'set', 'dev', dev_name, 'down',)) - dev_lst.append(f'{dev_name},{addr}') + addr = ipaddress.IPv6Interface(tok) break - f.write(' '.join(dev_lst) + '\n') - print(f'[{machine_id}] damage node: {node}') - -def sn_recover(dir, sat_loss): - damage_file = f"{dir}/{DAMAGE_FILENAME}" - if not os.path.exists(damage_file): - return - - pid_mat = _pid_map(f"{dir}/{PID_FILENAME}") - with open(f"{dir}/{DAMAGE_FILENAME}", 'r') as f: - for line in f: - toks = line.strip().split('|') - node = toks[0] - - pid = str(pid_mat[node]) - for link in toks[1].split(): - dev_addr = link.split(',') - subprocess.check_call( - ('nsenter', '-t', pid, '-n', - 'ip', 'link', 'set', 'dev', dev_addr[0], 'up',)) - subprocess.check_call( - ('nsenter', '-t', pid, '-n', - 'ip', 'addr', 'add', 'dev', dev_addr[0], dev_addr[1])) - print(f'[{machine_id}] recover sat: {node}') - os.remove(damage_file) - -if __name__ == '__main__': - _pid_map_cache = None - - if len(sys.argv) < 2: - print('Usage: sn_orchestrater.py ...') - exit(1) - cmd = sys.argv[1] - if cmd == 'exec': - pid_map = _pid_map(os.path.dirname(__file__) + '/' + PID_FILENAME) - if len(sys.argv) < 4: - print('Usage: sn_orchestrater.py exec ...') - exit(1) - if sys.argv[2] not in pid_map: - print('Error:', sys.argv[3], 'not found') - exit(1) - exit(subprocess.run( - ('nsenter', '-a', '-t', pid_map[sys.argv[2]], - *sys.argv[3:]) - ).returncode) - - if len(sys.argv) < 3: - machine_id = None - else: - try: - machine_id = int(sys.argv[2]) - except: - machine_id = None - if len(sys.argv) < 4: - workdir = os.path.dirname(__file__) - else: - workdir = sys.argv[3] - - # C module - try: - import pyctr - except ModuleNotFoundError: - subprocess.check_call( - "cd " + workdir + " && " - "gcc $(python3-config --cflags --ldflags)" - "-shared -fPIC -O2 pyctr.c -o pyctr.so", - shell=True - ) - import pyctr - - try: - import pynetlink - except ModuleNotFoundError: - subprocess.check_call( - "cd " + workdir + " && " - "gcc $(python3-config --cflags --ldflags) " - "-shared -fPIC -O2 pynetlink.c -o pynetlink.so", - shell=True - ) - import pynetlink - - try: - import pynetlink - except ModuleNotFoundError: - subprocess.check_call( - "cd " + workdir + " && " - "gcc $(python3-config --cflags --ldflags) " - "-shared -fPIC -O2 pynetlink.c -o pynetlink.so", - shell=True - ) - import pynetlink - - damage_set = set() - damage_file = workdir + '/' + DAMAGE_FILENAME - if os.path.exists(damage_file): - with open(workdir + '/' + DAMAGE_FILENAME, 'r') as f: - for line in f: - damage_set.add(line.strip().split(':')[0]) - - shell_num, node_mid_dict, ip_lst = _get_params(workdir + '/' + ASSIGN_FILENAME) - if cmd == 'nodes': - sn_clean(workdir) - sn_init_nodes(workdir, shell_num, node_mid_dict) - elif cmd == 'list': - print(f"{'NODE':<20} STATE") - for name in _pid_map(workdir + '/' + PID_FILENAME): - print(f"{name:<20} {'Damaged' if name in damage_set else 'OK'}") - elif cmd == 'networks': - # lp = LineProfiler() - # sn_update_network = lp(sn_update_network) - # lp.add_function(_update_link_intra_machine) - sn_update_network( - workdir, sys.argv[4], shell_num, node_mid_dict, ip_lst, - sys.argv[5], sys.argv[6], sys.argv[7], sys.argv[8] - ) - # with open('report.txt', 'w') as f: - # lp.print_stats(f) - elif cmd == 'routed': - sn_init_route_daemons(workdir, workdir + '/bird.conf', sys.argv[4]) - elif cmd == 'IP': - get_IP(workdir, sys.argv[4]) - elif cmd == 'damage': - sn_damage(workdir, sys.argv[4].split(',')) - elif cmd == 'recovery': - sn_recover(workdir, sys.argv[4]) - elif cmd == 'clean': - sn_clean(workdir) - elif cmd == 'ping': - sn_ping(workdir, sys.argv[4], sys.argv[5]) - elif cmd == 'iperf': - sn_iperf(workdir, sys.argv[4], sys.argv[5]) - elif cmd == 'sr': - sn_sr(workdir, sys.argv[4], sys.argv[5], sys.argv[6]) - elif cmd == 'rtable': - sn_check_route(workdir, sys.argv[4]) - else: - print('Unknown command') - os.close(main_net_fd) + pynetlink.if_down(dev_name, node.socket_fd) + dev_lst.append((dev_name, addr)) + + self.damage_dict[node] = dev_lst + + def recover(self, sat_loss: str): + if not self.damage_dict: + return + + for node_name, dev_lst in self.damage_dict.items(): + node = self.nodes.get(node_name) + if node is None: + continue + + _switch_netns(node.pid) + + for dev_name, addr in dev_lst: + pynetlink.if_up(dev_name, node.socket_fd) + pynetlink.modify_addr(True, dev_name, addr.ip.packed, addr.network.prefixlen, node.socket_fd) + + self.damage_dict.clear() + + def update_if(self, node_name: str, ifname: str, delay: str, bw: str, loss: str): + node = self.nodes.get(node_name) + if node is None: + return + + node.update_if(ifname, delay, bw, loss) + + def del_if(self, node_name: str, ifname: str): + node = self.nodes.get(node_name) + if node is None: + return + + node.del_if(ifname) diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py new file mode 100644 index 0000000..800b3b3 --- /dev/null +++ b/starrynet/sn_orchestrator_daemon.py @@ -0,0 +1,632 @@ +#!/usr/bin/python3 +import os +import subprocess +import json +import time +import socket +import threading +import resource +import logging +import argparse +import threading +import selectors +from enum import Enum +from concurrent.futures import ThreadPoolExecutor +import struct + +import paramiko +from sn_orchestrater import OrchestratorContext + +MSG_MAX_SIZE = 10 * 1024 * 1024 # 10MB + +# Constants from original orchestrater +ASSIGN_FILENAME = 'assign.json' +PID_FILENAME = 'container_pid.txt' +DAMAGE_FILENAME = 'damage_list.txt' +NOT_ASSIGNED = 'NA' +VXLAN_PORT = '4789' +CLONE_NEWNET = 0x40000000 + +# Daemon specific constants +SOCKET_PATH = '/tmp/starrynet_orchestrater.sock' +DEFAULT_SSH_PORT = 18888 + +class CommandStatus(Enum): + SUCCESS = "success" + ERROR = "error" + TIMEOUT = "timeout" + +class SSHServerInterface(paramiko.ServerInterface): + + def __init__(self, username=None, password=None): + self.username = username + self.password = password + + def check_auth_password(self, username, password): + if (self.username and username == self.username and + self.password and password == self.password): + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_auth_publickey(self, username, key): + return paramiko.AUTH_FAILED + + def check_channel_request(self, kind, chanid): + if kind == 'session': + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def check_channel_shell_request(self, channel): + return True + + def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): + return True + +class OrchestraterDaemon: + def __init__(self, workdir=None, machine_id=0, log_level=logging.WARNING, + ssh_port=DEFAULT_SSH_PORT, ssh_username='starrynet', ssh_password='123456'): + self.workdir = workdir or os.path.curdir + self.machine_id = machine_id + self.ssh_port = ssh_port + self.ssh_username = ssh_username + self.ssh_password = ssh_password + self.socket_path = SOCKET_PATH + self.running = False + + # Setup logging + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(os.path.join(self.workdir, 'orchestrater_daemon.log')), + logging.StreamHandler() + ] + ) + self.logger = logging.getLogger(str(self.__class__)) + + self.node_mid_dict = {} + self.ip_lst = [] + + self._generate_ssh_keys() + + self.logger.info(f"Orchestrater daemon initialized on machine {self.machine_id}") + self.logger.info(f"Working directory: {self.workdir}") + + def _signal_handler(self, signum, frame): + self.logger.info(f"Received signal {signum}, shutting down...") + self.stop() + + def _generate_ssh_keys(self): + self.host_key_path = os.path.join(self.workdir, 'ssh_host_key') + + if os.path.exists(self.host_key_path): + self.logger.info("Loading existing SSH host key...") + with open(self.host_key_path, 'r') as f: + self.host_key = paramiko.RSAKey.from_private_key(f) + else: + self.logger.info("Generating new SSH host key...") + self.host_key = paramiko.RSAKey.generate(2048) + with open(self.host_key_path, 'w') as f: + self.host_key.write_private_key(f) + + def run(self): + sel = selectors.DefaultSelector() + + self._start_unix_socket_server() + sel.register(self.unix_socket, selectors.EVENT_READ, self._unix_socket_server_accept) + + self._start_ssh_server() + sel.register(self.ssh_socket, selectors.EVENT_READ, self._ssh_server_accept) + + self.running = True + self.logger.info("Orchestrater running ...") + while self.running: + events = sel.select() + for key, mask in events: + callback = key.data + callback() + self.stop() + + def _start_unix_socket_server(self): + """Start Unix socket server""" + if os.path.exists(self.socket_path): + os.remove(self.socket_path) + + self.unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.unix_socket.bind(self.socket_path) + self.unix_socket.listen(5) + + self.logger.info(f"Unix socket server listening on {self.socket_path}") + + def _unix_socket_server_accept(self): + """Unix socket server accept callback""" + try: + conn, addr = self.unix_socket.accept() + # Handle each connection in a separate thread + client_thread = threading.Thread( + target=self._handle_client, + args=(conn,), + daemon=True + ) + client_thread.start() + except OSError as e: + if self.running: + self.logger.error(f"Unix socket accept error: {e}") + + def _start_ssh_server(self): + """Start SSH server""" + try: + self.ssh_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ssh_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.ssh_socket.bind(('0.0.0.0', self.ssh_port)) + self.ssh_socket.listen(100) + + self.logger.info(f"SSH server listening on port {self.ssh_port}") + + except Exception as e: + self.logger.error(f"Failed to start SSH server: {e}") + raise e + + def _ssh_server_accept(self): + try: + client, addr = self.ssh_socket.accept() + self.logger.info(f"SSH connection from {addr[0]}:{addr[1]}") + + # Handle each SSH connection in a separate thread + ssh_thread = threading.Thread( + target=self._handle_ssh_client, + args=(client, addr), + daemon=True + ) + ssh_thread.start() + except Exception as e: + if self.running: + self.logger.error(f"SSH accept error: {e}") + + def _handle_ssh_client(self, client, addr): + try: + transport = paramiko.Transport(client) + transport.add_server_key(self.host_key) + + server = SSHServerInterface(self.ssh_username, self.ssh_password) + transport.start_server(server=server) + + channel = transport.accept(20) + if channel is None: + transport.close() + return + + self.logger.info(f"SSH session established from {addr[0]}:{addr[1]}") + + self._handle_client(channel) + + except Exception as e: + self.logger.error(f"SSH client handling error: {e}") + finally: + try: + transport.close() + except: + pass + + def stop(self): + self.logger.info("Stopping orchestrater daemon...") + self.running = False + + if hasattr(self, 'unix_socket'): + self.unix_socket.close() + + if hasattr(self, 'ssh_socket'): + self.ssh_socket.close() + + if os.path.exists(self.socket_path): + os.remove(self.socket_path) + + self.logger.info("Daemon stopped") + + def _send_message_with_length(self, conn, data): + try: + if isinstance(data, str): + data = data.encode('utf-8') + + length_prefix = struct.pack('!I', len(data)) + + conn.sendall(length_prefix + data) + except Exception as e: + self.logger.error(f"Error sending message with length: {e}") + raise + + def _receive_message_with_length(self, conn): + try: + length_data = self._recv_exact(conn, 4) + if not length_data: + return None + + message_length = struct.unpack('!I', length_data)[0] + + if message_length > MSG_MAX_SIZE: + raise Exception(f"Message too large: {message_length} bytes") + + message_data = self._recv_exact(conn, message_length) + if not message_data: + return None + + return message_data + except Exception as e: + self.logger.error(f"Error receiving message with length: {e}") + raise + + def _recv_exact(self, conn, length): + data = bytearray() + while len(data) < length: + chunk = conn.recv(length - len(data)) + if not chunk: + return None + data.extend(chunk) + return bytes(data) + + def _handle_client(self, conn): + try: + while self.running: + message_data = self._receive_message_with_length(conn) + if not message_data: + break + + try: + command = json.loads(message_data.decode('utf-8')) + response = self._process_command(command) + + response_data = json.dumps(response) + self._send_message_with_length(conn, response_data) + + except json.JSONDecodeError as e: + error_response = { + "status": CommandStatus.ERROR.value, + "message": f"Invalid JSON: {e}" + } + self._send_message_with_length(conn, json.dumps(error_response)) + except Exception as e: + error_response = { + "status": CommandStatus.ERROR.value, + "message": f"Command processing error: {e}" + } + self._send_message_with_length(conn, json.dumps(error_response)) + + except Exception as e: + self.logger.error(f"Client connection error: {e}") + finally: + conn.close() + + def _update_rlimits(self, wanted_soft=65536): + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + new_soft = min(hard, wanted_soft) + if soft < new_soft: + resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard)) + self.logger.info(f"Updated RLIMIT_NOFILE: soft={new_soft}, hard={hard}") + + def _get_context(self): + if not hasattr(self, 'orchestrator_context'): + self.orchestrator_context = OrchestratorContext(self.workdir) + self.logger.info("Orchestrator context initialized") + return self.orchestrator_context + + def _process_command(self, command): + try: + cmd_type = command.get('c') # command + timestamp = command.get('t', time.time()) + params = command.get('p', {}) + + self.logger.info(f"Processing command: {cmd_type} at {timestamp}") + + if cmd_type == 'config': + result = self._handle_config(params) + elif cmd_type == 'nodes': + result = self._handle_nodes(params) + elif cmd_type == 'damage': + result = self._handle_damage(params) + elif cmd_type == 'recovery': + result = self._handle_recovery(params) + elif cmd_type == 'routed': + result = self._handle_routed(params) + elif cmd_type == 'route_batch': + result = self._handle_route_batch(params) + elif cmd_type == 'list': + result = self._handle_list(params) + elif cmd_type == 'IP': + result = self._handle_ip(params) + elif cmd_type == 'ping': + result = self._handle_ping(params) + elif cmd_type == 'iperf': + result = self._handle_iperf(params) + elif cmd_type == 'rtable': + result = self._handle_rtable(params) + elif cmd_type == 'clean': + result = self._handle_clean(params) + elif cmd_type == 'exec': + result = self._handle_exec(params) + elif cmd_type == 'update_network_batch': + result = self._handle_update_network_batch(params) + elif cmd_type == 'netlink': + result = self._handle_netlink(params) + else: + return { + "status": CommandStatus.ERROR.value, + "message": f"Unknown command: {cmd_type}" + } + + return { + "status": CommandStatus.SUCCESS.value, + "result": result, + "timestamp": timestamp + } + + except Exception as e: + self.logger.error(f"Command processing failed: {type(e).__name__} {e}", exc_info=True) + return { + "status": CommandStatus.ERROR.value, + "message": str(e) + } + + def _handle_config(self, params): + try: + self.node_mid_dict = params.get('node_mid_dict') + self.ip_lst = params.get('ip_lst') + + self.logger.info(f"Configuration received and loaded: " + f"nodes={len(self.node_mid_dict) if self.node_mid_dict else 0}") + + return { + "message": "Configuration loaded successfully", + "nodes_count": len(self.node_mid_dict) if self.node_mid_dict else 0 + } + except Exception as e: + self.logger.error(f"Failed to load configuration: {e}") + raise Exception(f"Configuration loading failed: {e}") + + def _handle_nodes(self, params): + try: + context = self._get_context() + + all_node_names = sorted(self.node_mid_dict.keys()) + + node_configs = {} + for i, node_name in enumerate(all_node_names): + if self.node_mid_dict[node_name] == self.machine_id: + node_configs[node_name] = i + + self._update_rlimits(len(node_configs) * 4) + + return context.init_nodes(self.workdir, node_configs) + except Exception as e: + raise Exception(f"Nodes initialization failed: {e}") + + def _handle_damage(self, params): + try: + context = self._get_context() + random_list = params.get('nodes', []) + context.damage(random_list) + return {"message": "Damage applied successfully"} + except Exception as e: + raise Exception(f"Damage failed: {e}") + + def _handle_recovery(self, params): + try: + context = self._get_context() + sat_loss = params.get('loss') + context.recover(sat_loss) + return {"message": "Recovery completed successfully"} + except Exception as e: + raise Exception(f"Recovery failed: {e}") + + def _handle_routed(self, params): + try: + context = self._get_context() + nodes = params.get('nodes', 'all') + conf_path = os.path.join(self.workdir, 'bird.conf') + context.init_route_daemons(conf_path, nodes) + return {"message": "Routing daemon initialized successfully"} + except Exception as e: + raise Exception(f"Routing daemon initialization failed: {e}") + + def _handle_list(self, params): + try: + context = self._get_context() + result = [] + for name in context.nodes.keys(): + result.append({ + "name": name, + "state": "Damaged" if name in context.damage_dict else "OK" + }) + return {"nodes": result} + except Exception as e: + raise Exception(f"List command failed: {e}") + + def _handle_ip(self, params): + try: + context = self._get_context() + node = params.get('node') + context.get_ip(node) + return {"message": f"IP information for {node} retrieved"} + except Exception as e: + raise Exception(f"IP command failed: {e}") + + def _handle_ping(self, params): + try: + context = self._get_context() + src = params.get('src') + dst = params.get('dst') + context.ping(src, dst) + return {"message": f"Ping from {src} to {dst} completed"} + except Exception as e: + raise Exception(f"Ping failed: {e}") + + def _handle_iperf(self, params): + try: + context = self._get_context() + context.iperf(params.get('cmds')) + return {"message": f"iPerf commands submitted"} + except Exception as e: + raise Exception(f"iPerf failed: {e}") + + def _handle_route_batch(self, params): + try: + context = self._get_context() + routes_config = params.get('routes_config', {}) + + # Convert to the format expected by OrchestratorContext + formatted_routes = {} + total_routes = 0 + + for node_name, routes in routes_config.items(): + context.set_static_route_batch() + formatted_routes[node_name] = [] + for route in routes: + # Each route should be a tuple: (dst, gw, dev, metric) + if len(route) == 4: + formatted_routes[node_name].append(tuple(route)) + total_routes += 1 + else: + self.logger.warning(f"Invalid route format for node {node_name}: {route}") + + # Use the batch static route method + context.set_static_route_batch(formatted_routes) + + return { + "message": f"Batch static routes set successfully", + "nodes_count": len(formatted_routes), + "total_routes": total_routes + } + except Exception as e: + raise Exception(f"Batch static route failed: {e}") + + def _handle_netlink(self, params): + try: + context = self._get_context() + context.netlink(params.get('routes'), []) + return {"message": f"netlink commands submitted"} + except Exception as e: + raise Exception(f"netlink failed: {e}") + + def _handle_rtable(self, params): + try: + context = self._get_context() + node = params.get('node') + context.check_route(node) + return {"message": f"Routing table for {node} checked"} + except Exception as e: + raise Exception(f"Routing table check failed: {e}") + + def _handle_clean(self, params): + try: + context = self._get_context() + context.clean() + return {"message": "Clean completed successfully"} + except Exception as e: + raise Exception(f"Clean failed: {e}") + + def _handle_exec(self, params): + try: + context = self._get_context() + node = params.get('node') + cmd = params.get('cmd', 'echo Hello World') + result = context.exec_command(node, cmd) + return { + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr + } + except Exception as e: + raise Exception(f"Exec failed: {e}") + + def _handle_update_network_batch(self, params): + try: + context = self._get_context() + + isl_bw = params.get('isl_bw', '1000') + isl_loss = params.get('isl_loss', '0') + del_lst = params.get('del', []) + upd_lst = params.get('update', []) + add_lst = params.get('add', []) + + for src, dst in del_lst: + if self.node_mid_dict[src] == self.machine_id: + context.del_if(src, dst) + elif self.node_mid_dict[dst] == self.machine_id: + context.del_if(dst, src) + + for src, dst, delay in upd_lst: + if self.node_mid_dict[src] == self.machine_id: + context.update_if(src, dst, delay, isl_bw, isl_loss) + if self.node_mid_dict[dst] == self.machine_id: + context.update_if(dst, src, delay, isl_bw, isl_loss) + + for link in add_lst: + src, dst, delay = link[0], link[1], link[2] + src_ifidx, src_addr4, src_addr6 = link[3], link[4], link[5] + dst_ifidx, dst_addr4, dst_addr6 = link[6], link[7], link[8] + if self.node_mid_dict[src] == self.machine_id: + if self.node_mid_dict[dst] == self.machine_id: + context.add_link_intra_machine( + src, dst, + src_ifidx, src_addr4, src_addr6, + dst_ifidx, dst_addr4, dst_addr6, + delay, isl_bw, isl_loss + ) + else: + context.add_link_inter_machine( + src, dst, + context.ip_lst[context.node_mid_dict[dst]], + src_ifidx, src_addr4, src_addr6, + delay, isl_bw, isl_loss + ) + elif self.node_mid_dict[dst] == self.machine_id: + context.add_link_inter_machine( + dst, src, + context.ip_lst[context.node_mid_dict[dst]], + dst_ifidx, dst_addr4, dst_addr6, + delay, isl_bw, isl_loss + ) + return f'Delete {len(del_lst)}, update {len(upd_lst)}, add {len(add_lst)} links' + + except Exception as e: + raise Exception(f'Network update failed: {str(e)}') + + +def main(): + parser = argparse.ArgumentParser(description='StarryNet Orchestrater Daemon with SSH Server') + parser.add_argument('--workdir', type=str, default=None, help='Working directory') + parser.add_argument('--machine-id', type=int, help='Machine ID') + parser.add_argument('--daemon', action='store_true', help='Run as daemon') + parser.add_argument('--log-level', type=str, default='INFO', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], + help='Log level') + parser.add_argument('--ssh-port', type=int, default=DEFAULT_SSH_PORT, help='SSH server port') + parser.add_argument('--ssh-username', type=str, help='SSH username for authentication') + parser.add_argument('--ssh-password', type=str, help='SSH password for authentication') + + args = parser.parse_args() + + # Convert log level string to logging constant + log_level = getattr(logging, args.log_level.upper()) + + # Create daemon instance + daemon = OrchestraterDaemon( + workdir=args.workdir, + machine_id=args.machine_id, + log_level=log_level, + ssh_port=args.ssh_port, + ssh_username=args.ssh_username, + ssh_password=args.ssh_password + ) + + if args.daemon: + # Run as daemon + import daemon + with daemon.DaemonContext(): + daemon.run() + else: + # Run in foreground + daemon.run() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index aa8d113..2f3fec6 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -6,14 +6,22 @@ """ import time import threading -import zipfile import math import re import json -from starrynet.sn_observer import * -from starrynet.sn_utils import * - -ASSIGN_FILENAME = 'assign.json' +import os +import glob +import random +import ipaddress +from enum import Enum +from collections import defaultdict +from typing import List, Tuple, Dict +from dataclasses import dataclass, field +from .sn_observer import * +from .sn_utils import * +from .sn_daemon_client import SSHDaemonClient + +EXTRA_LINK_DELAY = 50 # ms, for extra nodes connected to GS BIRD_CONF_TEXT = """\ log "/var/log/bird.log" { warning, error, auth, fatal, bug }; @@ -56,204 +64,35 @@ } """ +class NodeType(Enum): + SAT = 1 + GS = 2 + EXTRA = 3 + +@dataclass +class LinkInfo: + dst: str + addr4: ipaddress.IPv4Interface + addr6: ipaddress.IPv6Interface + if_id: int = None + +@dataclass +class NodeInfo: + name: str + node_type: NodeType = NodeType.SAT + cbf_t: List[Tuple[float, float, float]] = field(default_factory=list) + links_t: List[Dict[str, LinkInfo]] = field(default_factory=list) + ifidx_next: int = 10 + addr4: ipaddress.IPv4Address = None + addr6: ipaddress.IPv6Address = None + worker: SSHDaemonClient = None + def _gs2idx(gs_name): return int(gs_name[2:])-1 -class RemoteMachine: - - def __init__(self, id, host, port, username, password, - shell_lst, experiment_name, local_dir, gs_dirname): - self.id = id - self.shell_lst = shell_lst - self.local_dir = local_dir - self.gs_dirname = gs_dirname - self.ssh, self.sftp = sn_connect_remote( - host = host, - port = port, - username = username, - password = password, - ) - sn_remote_cmd(self.ssh, 'mkdir ~/' + experiment_name) - self.dir = sn_remote_cmd(self.ssh, 'echo ~/' + experiment_name) - self.sftp.put( - os.path.join(os.path.dirname(__file__), 'sn_orchestrater.py'), - self.dir + '/sn_orchestrater.py' - ) - self.sftp.put( - os.path.join(os.path.dirname(__file__), 'pyctr.c'), - self.dir + '/pyctr.c' - ) - self.sftp.put( - os.path.join(os.path.dirname(__file__), 'pynetlink.c'), - self.dir + '/pynetlink.c' - ) - self.sftp.put( - os.path.join(self.local_dir, 'bird.conf'), - self.dir + '/bird.conf' - ) - self.sftp.put( - os.path.join(self.local_dir, ASSIGN_FILENAME), - self.dir + '/' + ASSIGN_FILENAME - ) - - def init_nodes(self): - sn_remote_wait_output( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py nodes {self.id} {self.dir}" - ) - - def get_nodes(self): - lines = sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py list {self.id} {self.dir}" - ).splitlines()[1:] - nodes = [ - line.split()[0] for line in lines - ] - return nodes - - def init_network(self, isl_bw, isl_loss, gsl_bw, gsl_loss): - for shell_name, sat_names in self.shell_lst: - rmt_path = f"{self.dir}/{shell_name}.zip" - rmt_f = self.sftp.open(rmt_path, "wb") - zip_f = zipfile.ZipFile(rmt_f, mode='w') - pattern = os.path.join(self.local_dir, shell_name, 'isl', '*.txt') - for isl_txt in glob.glob(pattern): - zip_f.write(isl_txt, f"{shell_name}/{os.path.basename(isl_txt)}") - zip_f.close() - rmt_f.close() - sn_remote_cmd(self.ssh, f"python3 -m zipfile -e {rmt_path} {self.dir}") - if self.gs_dirname: - rmt_path = f"{self.dir}/{self.gs_dirname}.zip" - rmt_f = self.sftp.open(rmt_path, "wb") - zip_f = zipfile.ZipFile(rmt_f, mode='w') - pattern = os.path.join(self.local_dir, self.gs_dirname, 'gsl', '*.txt') - for gsl_txt in glob.glob(pattern): - zip_f.write( - gsl_txt, - f"{self.gs_dirname}/{os.path.basename(gsl_txt)}" - ) - zip_f.close() - rmt_f.close() - sn_remote_cmd(self.ssh, f"python3 -m zipfile -e {rmt_path} {self.dir}") - self.update_network(0, isl_bw, isl_loss, gsl_bw, gsl_loss) - - def update_network(self, t, isl_bw, isl_loss, gsl_bw, gsl_loss): - sn_remote_wait_output( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py networks {self.id} {self.dir} " - f"{t} {isl_bw} {isl_loss} {gsl_bw} {gsl_loss}" - ) - - def init_routed(self, nodes): - print(sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py routed {self.id} {self.dir} " - +','.join(nodes) - )) - - def get_IP(self, node): - lines = sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py IP {self.id} {self.dir} {node}" - ).splitlines() - IP_dict = {} - for line in lines: - dev_IP = line.strip().split() - IP_dict[dev_IP[0]] = dev_IP[1] - return IP_dict - - def ping_async(self, res_path, src, dst): - def _ping_inner(ssh, dir, res_path, src, dst): - output = sn_remote_cmd( - ssh, - f"python3 {dir}/sn_orchestrater.py ping {self.id} {dir} " - f"{src} {dst} 2>&1" - ) - with open(res_path, 'w') as f: - f.write(output) - thread = threading.Thread( - target=_ping_inner, - args=(self.ssh, self.dir, res_path, src, dst) - ) - thread.start() - return thread - - def iperf_async(self, res_path, src, dst): - def _iperf_inner(ssh, dir, res_path, src, dst): - output = sn_remote_cmd( - ssh, - f"python3 {self.dir}/sn_orchestrater.py iperf {self.id} {self.dir} " - f"{src} {dst} 2>&1" - ) - with open(res_path, 'w') as f: - f.write(output) - - thread = threading.Thread( - target=_iperf_inner, - args=(self.ssh, self.dir, res_path, src, dst) - ) - thread.start() - return thread - - def sr(self, src, dst, next_hop): - sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py sr {self.id} {self.dir} " - f"{src} {dst} {next_hop} 2>&1" - ) - - def check_route(self, res_path, sat): - output = sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py rtable {self.id} {self.dir} " - f"{sat} 2>&1" - ) - with open(res_path, 'w') as f: - f.write(output) - - def check_utility(self, res_path): - output = sn_remote_cmd(self.ssh, "vmstat 2>&1") - with open(res_path, 'w') as f: - f.write(output) - - def damage(self, random_lst): - print(sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py damage {self.id} {self.dir} " - + ','.join(random_lst) - )) - - def recovery(self, sat_loss): - sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py recovery {self.id} {self.dir} " - f"{sat_loss}" - ) - - def exec(self, node, cmd): - sn_remote_wait_output( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py exec {node} {cmd}" - ) - - def print_nodes(self, f): - output = sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py list" - ) + '\n' - lines = output.splitlines(True) - f.writelines(lines[1:]) - - def clean(self): - sn_remote_cmd( - self.ssh, - f"python3 {self.dir}/sn_orchestrater.py clean {self.id} {self.dir}" - ) - class StarryNet(): - def __init__(self, configuration_file_path, GS_lat_long, hello_interval): + def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_nodes_links = None, hello_interval = 5): # Initialize constellation information. sn_args = sn_load_file(configuration_file_path) self.shell_lst = sn_args.shell_lst @@ -279,14 +118,20 @@ def __init__(self, configuration_file_path, GS_lat_long, hello_interval): self.local_dir = os.path.join(self.configuration_dir, self.experiment_name) self._init_local(hello_interval) - # Initiate a necessary delay and position data for emulation - sat_names_shell = gen_topo( - self.local_dir, self.duration, self.step, self.shell_lst, self.link_style, - self.gs_lat_long, self.antenna_number, self.elevation, self.link_policy - ) - self.remote_lst, self.node_mid_dict = self._assign_remote(sat_names_shell, sn_args.machine_lst) + + # Initialize Observer for topology computation + self.observer = Observer(configuration_file_path, GS_lat_long, + self.antenna_number, self.elevation) + + # Compute topology and get updates + sat_t_shell, gs_t = self.observer.compute_topology(GS_links) + self.nodes, self.changes_t = self._process_topo(sat_t_shell, gs_t, extra_nodes_links) + + self._assign_worker([shell[0] for shell in sat_t_shell], sn_args.machine_lst) self.events = [] + self.netlink_events = [list() for _ in range(math.ceil(self.duration))] + self.cmd_events = [list() for _ in range(math.ceil(self.duration))] def _init_local(self, hello_interval): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): @@ -297,7 +142,168 @@ def _init_local(self, hello_interval): with open(os.path.join(self.local_dir, 'bird.conf'), 'w') as f: f.write(BIRD_CONF_TEXT % (hello_interval, hello_interval, hello_interval)) - def _assign_remote(self, sat_names_shell, machine_lst): + def _process_topo(self, sat_t_shell, gs_t, extra_nodes_links): + gs_name_lst, gs_cbf, gsls_t = gs_t + nodes = {} + idx_dict = {} + cnt = 0 + changes_t = [{'add': [], 'update':[], 'del': []} for _ in range(len(gsls_t))] + + for name_lst, sat_cbf_t, sat_lla_t, isls_t in sat_t_shell: + for sat_name in name_lst: + nodes[sat_name] = NodeInfo(name=sat_name, node_type=NodeType.SAT, cbf_t=[], links_t=[{} for _ in range(len(isls_t))]) + for name_lst, sat_cbf_t, sat_lla_t, isls_t in sat_t_shell: + old_states = {sat_name: {} for sat_name in name_lst} + for t, (cbf_lst, isls_lst) in enumerate(zip(sat_cbf_t, isls_t)): + for sat_name, cbf, isls in zip(name_lst, cbf_lst, isls_lst): + nodes[sat_name].cbf_t.append((cbf[0], cbf[1], cbf[2])) + new_state = {} + for isl in isls: + dst = isl[0] + new_state[dst] = isl[1] + key = (sat_name, dst) + if key in idx_dict: + idx = idx_dict[key] + else: + idx = cnt + idx_dict[key] = idx + cnt += 1 + nodes[sat_name].links_t[t][dst] = LinkInfo( + dst=dst, + addr4=ipaddress.IPv4Interface(f'10.{idx >> 8}.{idx & 0xFF}.10/24'), + addr6=ipaddress.IPv6Interface(f'2000:{idx >> 8}:{idx & 0xFF}::10/48') + ) + nodes[dst].links_t[t][sat_name] = LinkInfo( + dst=sat_name, + addr4=ipaddress.IPv4Interface(f'10.{idx >> 8}.{idx & 0xFF}.40/24'), + addr6=ipaddress.IPv6Interface(f'2000:{idx >> 8}:{idx & 0xFF}::40/48') + ) + old_state = old_states[sat_name] + for dst, old_delay in old_state.items(): + if dst not in new_state: + changes_t[t]['del'].append((sat_name, dst)) + elif abs(old_delay - new_state[dst]) > 1e-2: + changes_t[t]['update'].append((sat_name, dst, f'{new_state[dst]:.2f}')) + else: + # not accumlate + new_state[dst] = old_delay + for dst, delay in new_state.items(): + if dst not in old_state: + src_node = nodes[sat_name] + src_link = src_node.links_t[t][dst] + src_link.if_id = src_node.ifidx_next + src_node.ifidx_next += 1 + dst_node = nodes[dst] + dst_link = dst_node.links_t[t][sat_name] + dst_link.if_id = dst_node.ifidx_next + dst_node.ifidx_next += 1 + changes_t[t]['add'].append(( + sat_name, dst, f'{delay:.2f}', + src_link.if_id, src_link.addr4.compressed, src_link.addr6.compressed, + dst_link.if_id, dst_link.addr4.compressed, dst_link.addr6.compressed, + )) + old_states[sat_name] = new_state + + old_state = {} + for gs_name in gs_name_lst: + nodes[gs_name] = NodeInfo(name=gs_name, node_type=NodeType.GS, cbf_t=[], links_t=[]) + old_states[gs_name] = {} + for t, gsls_lst in enumerate(gsls_t): + for gs_name, cbf, gsls in zip(gs_name_lst, gs_cbf, gsls_lst): + nodes[gs_name].cbf_t.append((cbf[0], cbf[1], cbf[2])) + links = {} + new_state = {} + for gsl in gsls: + sat = gsl[0] + new_state[sat] = gsl[1] + key = (gs_name, sat) + if key in idx_dict: + idx = idx_dict[key] + else: + idx = cnt + idx_dict[key] = idx + cnt += 1 + links[sat] = LinkInfo( + dst=sat, + addr4=ipaddress.IPv4Interface(f'11.{idx >> 8}.{idx & 0xFF}.40/24'), + addr6=ipaddress.IPv6Interface(f'2001:{idx >> 8}:{idx & 0xFF}::40/48') + ) + nodes[sat].links_t[t][gs_name] = LinkInfo( + dst=gs_name, + addr4=ipaddress.IPv4Interface(f'11.{idx >> 8}.{idx & 0xFF}.10/24'), + addr6=ipaddress.IPv6Interface(f'2001:{idx >> 8}:{idx & 0xFF}::10/48') + ) + nodes[gs_name].links_t.append(links) + + old_state = old_states[gs_name] + for dst, old_delay in old_state.items(): + if dst not in new_state: + changes_t[t]['del'].append((gs_name, dst)) + elif abs(old_delay - new_state[dst]) > 1e-2: + changes_t[t]['update'].append((gs_name, dst, f'{new_state[dst]:.2f}')) + else: + # not accumlate + new_state[dst] = old_delay + for dst, delay in new_state.items(): + if dst not in old_state: + src_node = nodes[gs_name] + src_link = links[dst] + src_link.if_id = src_node.ifidx_next + src_node.ifidx_next += 1 + dst_node = nodes[dst] + dst_link = dst_node.links_t[t][gs_name] + dst_link.if_id = dst_node.ifidx_next + dst_node.ifidx_next += 1 + changes_t[t]['add'].append(( + gs_name, dst, f'{delay:.2f}', + src_link.if_id, src_link.addr4.compressed, src_link.addr6.compressed, + dst_link.if_id, dst_link.addr4.compressed, dst_link.addr6.compressed, + )) + old_states[gs_name] = new_state + if extra_nodes_links: + for name, links in extra_nodes_links.items(): + extra_node = NodeInfo(name=name, node_type=NodeType.EXTRA, cbf_t=[], links_t=[{} for _ in range(len(gsls_t))]) + for dst in links: + dst_node = nodes.get(dst) + if dst_node is None: + continue # single directed + # raise ValueError(f"Extra node {name} links to non-existent node {dst}") + if dst_node.node_type == NodeType.SAT: + raise ValueError(f"Extra node {name} cannot link to satellite {dst}") + for t in range(len(dst_node.links_t)): + dst_links = dst_node.links_t[t] + key = (name, dst) + if key in idx_dict: + idx = idx_dict[key] + else: + idx = cnt + idx_dict[key] = idx + cnt += 1 + extra_node.links_t[t][dst] = LinkInfo( + dst=dst, + addr4=ipaddress.IPv4Interface(f'12.{idx >> 8}.{idx & 0xFF}.40/24'), + addr6=ipaddress.IPv6Interface(f'2002:{idx >> 8}:{idx & 0xFF}::40/48') + ) + dst_links[name] = LinkInfo( + dst=name, + addr4=ipaddress.IPv4Interface(f'12.{idx >> 8}.{idx & 0xFF}.10/24'), + addr6=ipaddress.IPv6Interface(f'2002:{idx >> 8}:{idx & 0xFF}::10/48') + ) + src_link = extra_node.links_t[0][dst] + src_link.if_id = extra_node.ifidx_next + extra_node.ifidx_next += 1 + dst_link = dst_node.links_t[0][name] + dst_link.if_id = dst_node.ifidx_next + dst_node.ifidx_next += 1 + changes_t[0]['add'].append(( + name, dst, f'{EXTRA_LINK_DELAY:.2f}', + src_link.if_id, src_link.addr4.compressed, src_link.addr6.compressed, + dst_link.if_id, dst_link.addr4.compressed, dst_link.addr6.compressed, + )) + nodes[name] = extra_node + return nodes, changes_t + + def _assign_worker(self, sat_names_shell, machine_lst): assert len(sat_names_shell) == len(self.shell_lst) # TODO: better partition @@ -311,7 +317,7 @@ def _assign_remote(self, sat_names_shell, machine_lst): shell_id, sat_id = 0, 0 sat_names = [] - for i, remote in enumerate(machine_lst): + for i, worker in enumerate(machine_lst): sat_nr = sat_per_machine if i < remainder: sat_nr += 1 @@ -337,7 +343,7 @@ def _assign_remote(self, sat_names_shell, machine_lst): remainder = len(sat_names_shell) % len(machine_lst) shell_id = 0 - for i, remote in enumerate(machine_lst): + for i, worker in enumerate(machine_lst): shell_num = shell_per_machine if i < remainder: shell_num += 1 @@ -353,104 +359,99 @@ def _assign_remote(self, sat_names_shell, machine_lst): shell_id += shell_num # TODO: better ground station assign - with open(os.path.join(self.local_dir, self.gs_dirname,'gsl','0.txt'))as f: - for line in f: - line = line.strip() - if len(line) == 0: - continue - toks = line.split('|') - gs_name = toks[0] - add_lst = toks[3] - if len(add_lst) == 0: - node_mid_dict[gs_name] = 0 - continue - gsl = add_lst.split(' ')[0].split(',') - node_mid_dict[gs_name] = node_mid_dict[gsl[0]] - ip_lst = [remote['IP'] for remote in machine_lst] + for name, node in self.nodes.items(): + if name in node_mid_dict: + continue + # assigned to the machine of its first neighbor + for dst in node.links_t[0]: + dst_mid = node_mid_dict.get(dst) + if dst_mid is not None: + node_mid_dict[name] = dst_mid + break + # TODO: otherwise? + if name not in node_mid_dict: + node_mid_dict[name] = 0 + + ip_lst = [worker['IP'] for worker in machine_lst] assign_obj = { 'shell_num': len(self.shell_lst), 'node_mid_dict': node_mid_dict, 'ip': ip_lst, } - with open(os.path.join(self.local_dir, ASSIGN_FILENAME), 'w') as f: - json.dump(assign_obj, f) - - remote_lst = [] - for i, remote in enumerate(machine_lst): - remote_lst.append(RemoteMachine( - i, - remote['IP'], - remote['port'], - remote['username'], - remote['password'], - assigned_shell_lst[i], - self.experiment_name, - self.local_dir, - self.gs_dirname - ) - ) - return remote_lst, node_mid_dict + + worker_lst = [] + for i, worker in enumerate(machine_lst): + worker_lst.append(SSHDaemonClient( + host=worker.get('IP', '127.0.0.1'), + port=worker.get('port', 18888), + username=worker.get('username', 'root'), + password=worker.get('password', ''), + timeout=30 + )) + self.worker_lst: List[SSHDaemonClient] = worker_lst + self.node_mid_dict = node_mid_dict + self.config_json = assign_obj def create_nodes(self): print('Initializing nodes ...') begin = time.time() - for remote in self.remote_lst: - remote.init_nodes() - print("Node initialization:", time.time() - begin, "s consumed.") - self._load_node_map() - - def node_map(self): - if hasattr(self, 'nodes'): - return self.nodes - self._load_node_map() - return self.nodes - - def _load_node_map(self): - self.nodes = {} self.undamaged_lst = list() self.total_sat_lst = list() - for remote in self.remote_lst: - for node in remote.get_nodes(): - if node.startswith('Error'): - print(node) - exit(1) - if not node.startswith('GS'): - self.undamaged_lst.append(node) - self.total_sat_lst.append(node) - self.nodes[node.strip()] = remote + for worker in self.worker_lst: + worker.send_config( + self.config_json['shell_num'], + self.config_json['node_mid_dict'], + self.config_json['ip'] + ) + node_addrs = worker.init_nodes() + for name, (addr4, addr6) in node_addrs.items(): + node = self.nodes[name] + node.addr4 = ipaddress.IPv4Interface(addr4) + node.addr6 = ipaddress.IPv6Interface(addr6) + node.worker = worker + if node.node_type == NodeType.SAT: + self.undamaged_lst.append(name) + self.total_sat_lst.append(name) + print("Node initialization:", time.time() - begin, "s consumed.") def create_links(self): - print('Initializing links ...') + print('Initializing links using pre-computed topology data...') thread_lst = [] begin = time.time() - for remote in self.remote_lst: + + # Get initial topology data from Observer (t=1 corresponds to initial state) + initial_network_update = self.changes_t[0] + + initial_network_update['isl_bw'] = str(self.sat_bandwidth) + initial_network_update['isl_loss'] = str(self.sat_loss) + initial_network_update['gsl_bw'] = str(self.sat_ground_bandwidth) + initial_network_update['gsl_loss'] = str(self.sat_ground_loss) + + for worker in self.worker_lst: thread = threading.Thread( - target=remote.init_network, - args=(self.sat_bandwidth, - self.sat_loss, - self.sat_ground_bandwidth, - self.sat_ground_loss), + target=worker.update_network, + args=(initial_network_update,) ) thread.start() thread_lst.append(thread) + for thread in thread_lst: thread.join() + print("Link initialization:", time.time() - begin, 's consumed.') def run_routing_daemon(self, node_lst='all'): print('Initializing routing ...') if node_lst == 'all': - for remote in self.remote_lst: - remote.init_routed(['all']) + for worker in self.worker_lst: + worker.init_routing('all') print("Routing daemon initialized. Wait 30s for route converged") else: - node_map = self.node_map() - rtd_lsts = {machine:[] for machine in self.remote_lst} - for node in node_lst: - rtd_lsts[node_map[node]].append(node) - for remote, nodes in rtd_lsts.items(): - if len(nodes) > 0: - remote.init_routed(nodes) + rtd_lsts = defaultdict(list) + for name, node in self.nodes.items(): + rtd_lsts[node.worker].append(name) + for worker, names in rtd_lsts.items(): + worker.init_routing(names) for i in range(30): print(f'\r{i} / 30', end=' ') @@ -537,14 +538,16 @@ def get_position(self, node, time_index): else: raise NotImplementedError - def get_IP(self, node): - node_map = self.node_map() - return node_map[node].get_IP(node) + def get_IP(self, name): + node = self.nodes.get(name) + if node is None: + return () + return node.addr4, node.addr6 # dynamic events def get_utility(self, t): def _check_utility(real_t): - for mid, machine in enumerate(self.remote_lst): + for mid, machine in enumerate(self.worker_lst): machine.check_utility(os.path.join( self.local_dir, f'{real_t}-utility-machine{mid}.txt') ) @@ -552,15 +555,14 @@ def _check_utility(real_t): def set_damage(self, damaging_ratio, t): def _damage(real_t, damaging_ratio): - node_map = self.node_map() - damage_lsts = {machine:[] for machine in self.remote_lst} + damage_lsts = {machine:[] for machine in self.worker_lst} cur_num = len(self.undamaged_lst) need_damage_num = min(len(self.total_sat_lst) * damaging_ratio, cur_num) while(cur_num - len(self.undamaged_lst) < need_damage_num): sat = self.undamaged_lst.pop( random.randint(0, len(self.undamaged_lst) - 1) ) - machine = node_map[sat] + machine = self.nodes[sat].worker damage_lsts[machine].append(sat) for machine, lst in damage_lsts.items(): machine.damage(lst) @@ -568,16 +570,14 @@ def _damage(real_t, damaging_ratio): def set_recovery(self, t): def _recovery(real_t): - for machine in self.remote_lst: + for machine in self.worker_lst: machine.recovery(self.sat_loss) self.undamaged_lst = self.total_sat_lst.copy() self.events.append((t, _recovery,)) def check_routing_table(self, node, t): def _check_route(real_t, node): - node_map = self.node_map() - machine = node_map[node] - machine.check_route( + self.nodes[node].worker.check_route( os.path.join(self.local_dir, f'{real_t}-route-{node}.txt'), node ) @@ -585,50 +585,100 @@ def _check_route(real_t, node): def set_next_hop(self, src, dst, next_hop, t): def _set_next_hop(real_t, src, dst, next_hop): - node_map = self.node_map() - machine = node_map[src] - machine.sr(src, dst, next_hop) - self.events.append((t, _set_next_hop, src, dst, next_hop)) + self.nodes[src].worker.sr(src, dst, next_hop) + if src in self.nodes and dst in self.nodes: + self.events.append((t, _set_next_hop, src, dst, next_hop)) + else: + raise ValueError('Specified node not found') + + def set_static_routes_batch(self, routes_config, t): + """Set static routes for multiple nodes at specified time + + Args: + routes_config: Dictionary mapping node names to lists of route tuples + Each tuple: (dst, gw, dev, metric) + t: Time when the batch routes should be applied + """ + def _set_static_routes_batch(real_t, routes_config): + # Group routes by worker machine to minimize communication + worker_routes = defaultdict(list) + + for node_name, routes in routes_config.items(): + node=self.nodes.get(node_name) + if node is None: + continue + worker_routes[node.worker].append((node_name, routes)) + + for worker, node_routes_lst in worker_routes.items(): + worker.sr_batch(node_routes_lst) + + self.events.append((t, _set_static_routes_batch, routes_config)) + + def set_netlink_route(self, node, nlmsg, t): + if t >= self.duration: + t = round(self.duration) - 1 + elif t < 0: + t = 0 + else: + t = round(t) + self.cmd_events[t].append((node, nlmsg)) + + def _netlink_route(self, route_lst): + worker_msgs = defaultdict(list) + for node, nlmsg in route_lst: + worker_msgs[self.nodes[node].worker].append((node, nlmsg)) + for worker, routes in worker_msgs.items(): + worker.netlink(routes) def set_ping(self, src, dst, t): def _ping(real_t, src, dst): - node_map = self.node_map() - machine = node_map[src] - self.ping_threads.append(machine.ping_async( + self.ping_threads.append(self.nodes[src].worker.ping_async( os.path.join(self.local_dir, f'{real_t}-ping-{src}-{dst}.txt'), src, dst )) self.events.append((t, _ping, src, dst)) - def set_iperf(self, src, dst, t): - def _iperf(real_t, src, dst): - node_map = self.node_map() - machine = node_map[src] - self.iperf_threads.append(machine.iperf_async( - os.path.join(self.local_dir, f'{real_t}-iperf-{src}-{dst}.txt'), - src, dst - )) - self.events.append((t, _iperf, src, dst)) + def set_iperf(self, src, dst, t, extra_args = []): + if t >= self.duration: + t = round(self.duration) - 1 + elif t < 0: + t = 0 + else: + t = round(t) + self.cmd_events[t].append((src, dst, extra_args)) + + def _iperf(self, cmd_lst): + node_cmds = defaultdict(list) + for src, dst, extra_args in cmd_lst: + node_cmds[self.nodes[src].worker].append([src, dst, *extra_args]) + for node, cmds in node_cmds.items(): + node.iperf(cmds) + + def exec_at(self, node, cmd, t): + def _exec(real_t, node, cmd): + self.nodes[node].worker.exec(node, cmd) + self.events.append((t, _exec, node, cmd)) def exec_now(self, node, cmd): - node_map = self.node_map() - machine = node_map[node] - machine.exec(node, cmd) + self.nodes[node].worker.exec(node, cmd) def print_all_nodes(self, path): with open(path, 'w') as f: - for machine in self.remote_lst: + for machine in self.worker_lst: machine.print_nodes(f) def _event(self, real_t): while len(self.events) > 0 and self.events[-1][0] <= real_t: event = self.events.pop(-1) event[1](real_t, *event[2:]) + while self.last_t <= real_t: + self._netlink_route(self.netlink_events[self.last_t]) + self._iperf(self.cmd_events[self.last_t]) + self.last_t += 1 def start_emulation(self): self.events.sort(key=lambda x:x[0], reverse=True) - - nodes = self.node_map() + self.last_t = 0 self.ping_threads = [] self.iperf_threads = [] @@ -636,27 +686,30 @@ def start_emulation(self): tid = 1 while t < self.duration: start = time.time() - print("\nTrigger events at", t, "s ...") - self._event(t) - print("Update networks ...") - update_start = time.time() + print("Update networks using pre-computed topology...") if tid < self.duration: + network_change = self.changes_t[tid] + + network_change['isl_bw'] = str(self.sat_bandwidth) + network_change['isl_loss'] = str(self.sat_loss) + network_change['gsl_bw'] = str(self.sat_ground_bandwidth) + network_change['gsl_loss'] = str(self.sat_ground_loss) + conn_threads = [] - for remote in self.remote_lst: + for worker in self.worker_lst: thread = threading.Thread( - target=remote.update_network, - args=(tid, - self.sat_bandwidth, - self.sat_loss, - self.sat_ground_bandwidth, - self.sat_ground_loss - )) + target=worker.update_network, + args=(network_change,) + ) thread.start() conn_threads.append(thread) for thread in conn_threads: thread.join() + update_end = time.time() + print("\nTrigger events at", t, "s ...") + self._event(t) end = time.time() - print(end-start, "s elapsed,", end-update_start, "s for network update") + print(end-start, "s elapsed,", update_end-start, "s for network update") if end - start < 1: print('Sleep', 1 + start - end, 's') time.sleep(1 + start - end) @@ -669,6 +722,6 @@ def start_emulation(self): def clean(self): print("Removing containers and links...") - for remote in self.remote_lst: - remote.clean() - print("All containers and links remoted.") + for worker in self.worker_lst: + worker.clean() + print("All containers and links workerd.") From af2e549b28bd146c93994c52c8285e96fe8e0387 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Tue, 7 Apr 2026 11:26:55 +0800 Subject: [PATCH 24/49] feat: pass BIRD config text to orchestrator daemon Move BIRD routing configuration from a hardcoded synchronizer string to a tracked `bird.conf` file and send its content through the daemon client API. The orchestrator daemon now writes the received config to its workdir before starting `bird`. Also update the example to stop passing the removed OSPF hello-interval constructor argument. --- bird.conf | 43 +++++++++++++++++++++ example.py | 4 +- starrynet/sn_daemon_client.py | 5 ++- starrynet/sn_orchestrater.py | 5 ++- starrynet/sn_orchestrator_daemon.py | 3 ++ starrynet/sn_synchronizer.py | 59 +++++------------------------ 6 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 bird.conf diff --git a/bird.conf b/bird.conf new file mode 100644 index 0000000..db18b86 --- /dev/null +++ b/bird.conf @@ -0,0 +1,43 @@ +log "/bird.log" { warning, error, auth, fatal, bug }; +protocol device { +} +protocol direct { + disabled; # Disable by default + ipv4; # Connect to default IPv4 table + ipv6; # ... and to default IPv6 table +} +protocol kernel { + ipv4 { # Connect protocol to IPv4 table by channel + export all; # Export to protocol. default is export none + }; +} +# protocol static { +# ipv4; # Again, IPv6 channel with default options +# } +protocol ospf{ + ipv4 { + import all; + }; + area 0 { + interface "SH*O*S*" { + type broadcast; # Detected by default + cost 10; + hello 5; + }; + interface "GS*" { + type broadcast; # Detected by default + cost 10; + hello 5; + }; + interface "POP" { + type broadcast; # Detected by default + cost 10; + hello 5; + }; + interface "eth*" { + type broadcast; # Detected by default + cost 10; + hello 5; + }; + }; +} diff --git a/example.py b/example.py index 1dd1b6c..52a674e 100644 --- a/example.py +++ b/example.py @@ -16,10 +16,10 @@ GS_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817] ] # latitude and longitude of frankfurt and Austria configuration_file_path = "./config.json" - hello_interval = 5 # hello_interval(s) in OSPF. 1-200 are supported. + bird_conf_path = "./bird.conf" print('Start StarryNet.') - sn = StarryNet(configuration_file_path, GS_lat_long, hello_interval) + sn = StarryNet(configuration_file_path, GS_lat_long) sn.create_nodes() sn.create_links() diff --git a/starrynet/sn_daemon_client.py b/starrynet/sn_daemon_client.py index 6353828..693f94f 100644 --- a/starrynet/sn_daemon_client.py +++ b/starrynet/sn_daemon_client.py @@ -268,12 +268,13 @@ def recover_nodes(self, sat_loss: float): if response.get('status') != 'success': raise Exception(f"Failed to recover nodes: {response.get('message')}") - def init_routing(self, nodes: str | List = 'all'): + def init_routing(self, nodes: str | List, conf_text: str): command = { 'c': 'routed', 't': time.time(), 'p': { - 'nodes': nodes + 'nodes': nodes, + 'conf': conf_text, } } response = self._send_command_via_ssh(command) diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index eb2af09..8bf0bc4 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -295,7 +295,8 @@ def get_ip(self, node_name: str): return filtered_addrs def init_route_daemons(self, conf_path: str, nodes: str): - bird_ctl_path = conf_path[:conf_path.rfind('/')] + '/bird.ctl' + conf_path = os.path.abspath(conf_path) + ctl_path = os.path.join(os.path.dirname(conf_path), 'bird.ctl') if nodes == 'all': nodes_lst = self.nodes.keys() else: @@ -305,7 +306,7 @@ def init_route_daemons(self, conf_path: str, nodes: str): node = self.nodes.get(node_name) if node is None: continue - proc = node.run_command(('bird', '-c', conf_path, '-s', bird_ctl_path)) + proc = node.run_command(('bird', '-c', conf_path, '-s', ctl_path)) proc.wait() def ping(self, src: str, dst: str): diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index 800b3b3..a73a715 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -421,7 +421,10 @@ def _handle_routed(self, params): try: context = self._get_context() nodes = params.get('nodes', 'all') + conf_text = params['conf'] conf_path = os.path.join(self.workdir, 'bird.conf') + with open(conf_path, 'w') as f: + f.write(conf_text) context.init_route_daemons(conf_path, nodes) return {"message": "Routing daemon initialized successfully"} except Exception as e: diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 2f3fec6..b8e5560 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -21,48 +21,7 @@ from .sn_utils import * from .sn_daemon_client import SSHDaemonClient -EXTRA_LINK_DELAY = 50 # ms, for extra nodes connected to GS - -BIRD_CONF_TEXT = """\ -log "/var/log/bird.log" { warning, error, auth, fatal, bug }; -protocol device { -} -protocol direct { - disabled; # Disable by default - ipv4; # Connect to default IPv4 table - ipv6; # ... and to default IPv6 table -} -protocol kernel { - ipv4 { # Connect protocol to IPv4 table by channel - export all; # Export to protocol. default is export none - }; -} -# protocol static { -# ipv4; # Again, IPv6 channel with default options -# } -protocol ospf{ - ipv4 { - import all; - }; - area 0 { - interface "SH*O*S*" { - type broadcast; # Detected by default - cost 10; - hello %d; - }; - interface "GS*" { - type broadcast; # Detected by default - cost 10; - hello %d; - }; - interface "POP" { - type broadcast; # Detected by default - cost 10; - hello %d; - }; - }; -} -""" +EXTRA_LINK_DELAY = 1 # ms, for extra nodes connected to GS class NodeType(Enum): SAT = 1 @@ -92,7 +51,7 @@ def _gs2idx(gs_name): class StarryNet(): - def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_nodes_links = None, hello_interval = 5): + def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_nodes_links = None): # Initialize constellation information. sn_args = sn_load_file(configuration_file_path) self.shell_lst = sn_args.shell_lst @@ -117,7 +76,7 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ shell['name'] = f"shell{shell_id}" self.local_dir = os.path.join(self.configuration_dir, self.experiment_name) - self._init_local(hello_interval) + self._init_local() # Initialize Observer for topology computation self.observer = Observer(configuration_file_path, GS_lat_long, @@ -133,14 +92,12 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ self.netlink_events = [list() for _ in range(math.ceil(self.duration))] self.cmd_events = [list() for _ in range(math.ceil(self.duration))] - def _init_local(self, hello_interval): + def _init_local(self): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): os.remove(txt_file) for shell in self.shell_lst: os.makedirs(os.path.join(self.local_dir, shell['name']), exist_ok=True) os.makedirs(os.path.join(self.local_dir, self.gs_dirname), exist_ok=True) - with open(os.path.join(self.local_dir, 'bird.conf'), 'w') as f: - f.write(BIRD_CONF_TEXT % (hello_interval, hello_interval, hello_interval)) def _process_topo(self, sat_t_shell, gs_t, extra_nodes_links): gs_name_lst, gs_cbf, gsls_t = gs_t @@ -440,18 +397,20 @@ def create_links(self): print("Link initialization:", time.time() - begin, 's consumed.') - def run_routing_daemon(self, node_lst='all'): + def run_routing_daemon(self, bird_conf_path, node_lst='all'): + with open(bird_conf_path, 'r') as f: + bird_conf = f.read() print('Initializing routing ...') if node_lst == 'all': for worker in self.worker_lst: - worker.init_routing('all') + worker.init_routing('all', bird_conf) print("Routing daemon initialized. Wait 30s for route converged") else: rtd_lsts = defaultdict(list) for name, node in self.nodes.items(): rtd_lsts[node.worker].append(name) for worker, names in rtd_lsts.items(): - worker.init_routing(names) + worker.init_routing(names, bird_conf) for i in range(30): print(f'\r{i} / 30', end=' ') From 9266f81e797f62c0b771b79ac3bcd4c8a9597837 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Thu, 23 Apr 2026 20:11:52 +0800 Subject: [PATCH 25/49] fix: ifidx_next --- starrynet/sn_synchronizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index b8e5560..314da6a 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -41,7 +41,7 @@ class NodeInfo: node_type: NodeType = NodeType.SAT cbf_t: List[Tuple[float, float, float]] = field(default_factory=list) links_t: List[Dict[str, LinkInfo]] = field(default_factory=list) - ifidx_next: int = 10 + ifidx_next: int = 100 addr4: ipaddress.IPv4Address = None addr6: ipaddress.IPv6Address = None worker: SSHDaemonClient = None From 4e3d52bf3f8f726d5a1a67129f3b6692e1a0da09 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sat, 25 Apr 2026 10:30:01 +0800 Subject: [PATCH 26/49] refactor: batch command --- starrynet/sn_daemon_client.py | 108 +++------ starrynet/sn_orchestrater.py | 33 +-- starrynet/sn_orchestrator_daemon.py | 83 +++---- starrynet/sn_synchronizer.py | 333 ++++++++++++---------------- 4 files changed, 221 insertions(+), 336 deletions(-) diff --git a/starrynet/sn_daemon_client.py b/starrynet/sn_daemon_client.py index 693f94f..f17442c 100644 --- a/starrynet/sn_daemon_client.py +++ b/starrynet/sn_daemon_client.py @@ -213,18 +213,12 @@ def update_network(self, link_updates: dict): Args: link_updates: Dictionary containing link update information Structure: { - "shell_updates": { - "shell_id": { - "del": [("node1", "node2"), ...], - "update": [("node1", "node2", "delay"), ...], - "add": [("node1", "node2", "delay", idx), ...] - }, ... - }, - "gs_updates": { - "del": [("gs1", "sat1"), ...], - "update": [("gs1", "sat1", "delay"), ...], - "add": [("gs1", "sat1", "delay", idx), ...] - }, + "del": [("GS1", "SH1O1S1"), ...], + "update": [("SH1O1S1", "SH1O1S2", "delay"), ...], + "add": [ + ("GS1", "SH1O1S2", "delay", src_ifi, "src_in4", "src_in6", dst_ifi, ...), + ... + ], "isl_bw": "bandwidth", "isl_loss": "loss", "gsl_bw": "bandwidth", @@ -243,6 +237,24 @@ def update_network(self, link_updates: dict): if response.get('status') != 'success': raise Exception(f"Failed to update network: {response.get('message')}") + def check_utility(self): + command = { + 'c': 'utility', + 't': time.time(), + 'p': {} + } + return self._send_command_via_ssh(command).get('result', '') + + def check_routing_table(self, node: str): + command = { + 'c': 'rtable', + 't': time.time(), + 'p': { + 'node': node + } + } + return self._send_command_via_ssh(command).get('result', '') + def damage_nodes(self, nodes: list): command = { 'c': 'damage', @@ -281,69 +293,52 @@ def init_routing(self, nodes: str | List, conf_text: str): if response.get('status') != 'success': raise Exception(f"Failed to init routing: {response.get('message')}") - def get_ip(self, node: str): - command = { - 'c': 'IP', - 't': time.time(), - 'p': { - 'node': node - } - } - response = self._send_command_via_ssh(command) - if response.get('status') != 'success': - raise Exception(f"Failed to get IP: {response.get('message')}") - return response.get('result', {}) - - def ping(self, src: str, dst: str): + def ping_batch(self, ping_cmds): command = { 'c': 'ping', 't': time.time(), 'p': { - 'src': src, - 'dst': dst + 'batch': ping_cmds, } } return self._send_command_via_ssh(command) - def iperf(self, cmds): + def iperf_batch(self, iperf_cmds): command = { 'c': 'iperf', 't': time.time(), 'p': { - 'cmds': cmds + 'batch': iperf_cmds, } } return self._send_command_via_ssh(command) - def set_static_route(self, src: str, dst: str, next_hop: str): - """Set static route""" + def static_route_batch(self, rt_cmds): command = { 'c': 'sr', 't': time.time(), 'p': { - 'src': src, - 'dst': dst, - 'next': next_hop + 'batch': rt_cmds, } } return self._send_command_via_ssh(command) - def netlink(self, routes): + def netlink_batch(self, nl_cmds): command = { 'c': 'netlink', 't': time.time(), 'p': { - 'routes': routes, + 'batch': nl_cmds, } } + return self._send_command_via_ssh(command) - def check_routing_table(self, node: str): - """Check routing table for node""" + def exec_batch(self, exec_cmds): command = { - 'c': 'rtable', + 'c': 'exec', 't': time.time(), 'p': { - 'node': node + 'batch': exec_cmds, } } return self._send_command_via_ssh(command) @@ -357,37 +352,6 @@ def clean(self): } return self._send_command_via_ssh(command) - def exec_command(self, node: str, cmd: str): - """Execute command in node""" - command = { - 'c': 'exec', - 't': time.time(), - 'p': { - 'node': node, - 'cmd': cmd - } - } - return self._send_command_via_ssh(command) - - def set_static_routes_batch(self, routes_config: dict): - """Set static routes for multiple nodes in batch - - Args: - routes_config: Dictionary mapping node names to lists of route tuples - Each tuple: (dst, gw, dev, metric) - - Returns: - Response from daemon - """ - command = { - 'c': 'sr_batch', - 't': time.time(), - 'p': { - 'routes_config': routes_config - } - } - return self._send_command_via_ssh(command) - def __enter__(self): """Context manager entry""" self.connect() diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index 8bf0bc4..b98e409 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -278,22 +278,6 @@ def add_link_inter_machine(self, ) node1.init_if(name2, addr4, addr6, delay, bw, loss) - def get_ip(self, node_name: str): - node = self.nodes.get(node_name) - if node is None: - return [] - addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', str(node.pid), - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - filtered_addrs = [] - for dev_state_addrs in addr_lst: - dev_state_addrs = dev_state_addrs.split() - if len(dev_state_addrs) < 3: - continue - filtered_addrs.append((dev_state_addrs[0].split('@')[0], dev_state_addrs[2])) - return filtered_addrs - def init_route_daemons(self, conf_path: str, nodes: str): conf_path = os.path.abspath(conf_path) ctl_path = os.path.join(os.path.dirname(conf_path), 'bird.ctl') @@ -309,12 +293,21 @@ def init_route_daemons(self, conf_path: str, nodes: str): proc = node.run_command(('bird', '-c', conf_path, '-s', ctl_path)) proc.wait() + def set_static_route(self, src: str, dst: str, next_hop: str): + src_node = self.nodes.get(src) + dst_node = self.nodes.get(dst) + next_hop_node = self.nodes.get(next_hop) + if src_node is None or dst_node is None or next_hop_node is None: + return + + src_node.modify_routes([(dst_node.interfaces[0], dst_node.network_address.packed, dst_node.prefixlen, next_hop_node.packed)]) + def ping(self, src: str, dst: str): src_node = self.nodes.get(src) dst_node = self.nodes.get(dst) if src_node is None or dst_node is None: return - + dst_addr_lst = subprocess.check_output( ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_node.pid, 'ip', '-br', 'addr', 'show') @@ -371,13 +364,11 @@ def netlink(self, routes): def check_route(self, node_name: str): node = self.nodes.get(node_name) if node is None: - print(f"Node {node_name} not found") - return + return '' - subprocess.run( + return subprocess.check_output( ('nsenter', '-n', '-t', node.pid, 'route'), - stdout=sys.stdout, stderr=subprocess.STDOUT ) def damage(self, random_list: List[str]): diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index a73a715..e9419a6 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -311,12 +311,11 @@ def _get_context(self): def _process_command(self, command): try: + t_begin = time.time() cmd_type = command.get('c') # command timestamp = command.get('t', time.time()) params = command.get('p', {}) - - self.logger.info(f"Processing command: {cmd_type} at {timestamp}") - + if cmd_type == 'config': result = self._handle_config(params) elif cmd_type == 'nodes': @@ -327,18 +326,18 @@ def _process_command(self, command): result = self._handle_recovery(params) elif cmd_type == 'routed': result = self._handle_routed(params) - elif cmd_type == 'route_batch': + elif cmd_type == 'sr': result = self._handle_route_batch(params) elif cmd_type == 'list': result = self._handle_list(params) - elif cmd_type == 'IP': - result = self._handle_ip(params) elif cmd_type == 'ping': result = self._handle_ping(params) elif cmd_type == 'iperf': result = self._handle_iperf(params) elif cmd_type == 'rtable': result = self._handle_rtable(params) + elif cmd_type == 'utility': + result = self._handle_utility(params) elif cmd_type == 'clean': result = self._handle_clean(params) elif cmd_type == 'exec': @@ -352,7 +351,10 @@ def _process_command(self, command): "status": CommandStatus.ERROR.value, "message": f"Unknown command: {cmd_type}" } - + + t_finish = time.time() + self.logger.info(f"Command: {cmd_type} at {timestamp}, duration: {t_finish - t_begin:.6f} seconds") + return { "status": CommandStatus.SUCCESS.value, "result": result, @@ -443,29 +445,19 @@ def _handle_list(self, params): except Exception as e: raise Exception(f"List command failed: {e}") - def _handle_ip(self, params): - try: - context = self._get_context() - node = params.get('node') - context.get_ip(node) - return {"message": f"IP information for {node} retrieved"} - except Exception as e: - raise Exception(f"IP command failed: {e}") - def _handle_ping(self, params): try: context = self._get_context() - src = params.get('src') - dst = params.get('dst') - context.ping(src, dst) - return {"message": f"Ping from {src} to {dst} completed"} + for cmd in params.get('batch', []): + context.ping(cmd[0], cmd[1]) + return {"message": "All pings completed"} except Exception as e: raise Exception(f"Ping failed: {e}") def _handle_iperf(self, params): try: context = self._get_context() - context.iperf(params.get('cmds')) + context.iperf(params.get('batch', [])) return {"message": f"iPerf commands submitted"} except Exception as e: raise Exception(f"iPerf failed: {e}") @@ -473,29 +465,16 @@ def _handle_iperf(self, params): def _handle_route_batch(self, params): try: context = self._get_context() - routes_config = params.get('routes_config', {}) - - # Convert to the format expected by OrchestratorContext - formatted_routes = {} + routes_lst = params.get('batch', []) + total_routes = 0 - - for node_name, routes in routes_config.items(): - context.set_static_route_batch() - formatted_routes[node_name] = [] - for route in routes: - # Each route should be a tuple: (dst, gw, dev, metric) - if len(route) == 4: - formatted_routes[node_name].append(tuple(route)) - total_routes += 1 - else: - self.logger.warning(f"Invalid route format for node {node_name}: {route}") - - # Use the batch static route method - context.set_static_route_batch(formatted_routes) - + + for src, dst, next_hop in routes_lst: + context.set_static_route(src, dst, next_hop) + total_routes += 1 + return { "message": f"Batch static routes set successfully", - "nodes_count": len(formatted_routes), "total_routes": total_routes } except Exception as e: @@ -504,7 +483,7 @@ def _handle_route_batch(self, params): def _handle_netlink(self, params): try: context = self._get_context() - context.netlink(params.get('routes'), []) + context.netlink(params.get('batch', []), []) return {"message": f"netlink commands submitted"} except Exception as e: raise Exception(f"netlink failed: {e}") @@ -513,11 +492,16 @@ def _handle_rtable(self, params): try: context = self._get_context() node = params.get('node') - context.check_route(node) - return {"message": f"Routing table for {node} checked"} + return context.check_route(node) except Exception as e: raise Exception(f"Routing table check failed: {e}") + def _handle_utility(self, params): + try: + return subprocess.check_output(('vmstat', '-s')) + except Exception as e: + raise Exception(f"Utility check failed: {e}") + def _handle_clean(self, params): try: context = self._get_context() @@ -529,14 +513,9 @@ def _handle_clean(self, params): def _handle_exec(self, params): try: context = self._get_context() - node = params.get('node') - cmd = params.get('cmd', 'echo Hello World') - result = context.exec_command(node, cmd) - return { - "returncode": result.returncode, - "stdout": result.stdout, - "stderr": result.stderr - } + for node, cmd in params.get('batch', []): + context.exec_command(node, cmd) + return {"message": "All commands executed successfully"} except Exception as e: raise Exception(f"Exec failed: {e}") diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 314da6a..a4db426 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -8,14 +8,13 @@ import threading import math import re -import json import os import glob import random import ipaddress from enum import Enum from collections import defaultdict -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Callable from dataclasses import dataclass, field from .sn_observer import * from .sn_utils import * @@ -46,6 +45,11 @@ class NodeInfo: addr6: ipaddress.IPv6Address = None worker: SSHDaemonClient = None +@dataclass +class BatchCommand: + func: Callable + args_lst: list + def _gs2idx(gs_name): return int(gs_name[2:])-1 @@ -89,8 +93,7 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ self._assign_worker([shell[0] for shell in sat_t_shell], sn_args.machine_lst) self.events = [] - self.netlink_events = [list() for _ in range(math.ceil(self.duration))] - self.cmd_events = [list() for _ in range(math.ceil(self.duration))] + self.batch_events = [defaultdict(BatchCommand) for _ in range(math.ceil(self.duration))] def _init_local(self): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): @@ -346,7 +349,6 @@ def _assign_worker(self, sat_names_shell, machine_lst): timeout=30 )) self.worker_lst: List[SSHDaemonClient] = worker_lst - self.node_mid_dict = node_mid_dict self.config_json = assign_obj def create_nodes(self): @@ -418,84 +420,45 @@ def run_routing_daemon(self, bird_conf_path, node_lst='all'): print("Routing started!") # static information - def get_distance(self, node1, node2, time_index): - def _get_xyz(node): - if node.startswith('SH'): - match = re.search(r'\d+', node) - shell_id = int(match.group(0))-1 - shell = self.shell_lst[shell_id] - lla_dict = load_pos(os.path.join( - self.local_dir, - shell['name'], - 'position', - f'{time_index}.txt' - )) - lla = lla_dict[node] - return to_cbf(lla_dict[node]) - elif node.startswith('GS'): - return to_cbf(self.gs_lat_long[_gs2idx(node)]) - else: - raise NotImplementedError - - xyz1, xyz2 = _get_xyz(node1), _get_xyz(node2) + def get_distance(self, name1, name2, t): + node1, node2 = self.nodes.get(name1), self.nodes.get(name2) + if node1 is None or node2 is None: + return None + + tid = t // self.step + xyz1 = node1.cbf_t[tid] if tid < len(node1.cbf_t) else node1.cbf_t[-1] + xyz2 = node2.cbf_t[tid] if tid < len(node2.cbf_t) else node2.cbf_t[-1] dx, dy, dz = xyz1[0] - xyz2[0], xyz1[1] - xyz2[1], xyz1[2] - xyz2[2] return math.sqrt(dx * dx + dy * dy + dz * dz) - def get_neighbors(self, sat, time_index): - if not sat.startswith('SH'): - raise RuntimeError('Not a satellite') - match = re.search(r'\d+', sat) - shell_id = int(match.group(0))-1 - shell = self.shell_lst[shell_id] - - isls_dict = load_links_dict(os.path.join( - self.local_dir, - shell['name'], - 'isl', - f'{time_index}-state.txt' - )) - neighbors = [] - for isl in isls_dict[sat]: - neighbors.append(isl[0]) - for name, isl_lst in isls_dict.items(): - for isl in isl_lst: - if isl[0] == sat: - neighbors.append(name) - return neighbors - - def get_GSes(self, sat, time_index): - if not sat.startswith('SH'): - raise RuntimeError('Not a Satellite') - - gsls_dict = load_links_dict(os.path.join( - self.local_dir, - self.gs_dirname, - 'gsl', - f'{time_index}-state.txt' - )) + def get_neighbors(self, sat, t): + node = self.nodes.get(sat) + if node is None: + return [] + + tid = t // self.step + return list(node.links_t[tid].keys()) + + def get_GSes(self, sat_name, t): + node = self.nodes.get(sat_name) + if node is None: + return [] + + tid = t // self.step GSes = [] - for gs, gsl_lst in gsls_dict.items(): - for gsl in gsl_lst: - if gsl[0] == sat: - GSes.append(gs) + for dst in node.links_t[tid]: + dst_node = self.nodes[dst] + if dst_node.node_type == NodeType.GS: + GSes.append(dst) return GSes - def get_position(self, node, time_index): - if node.startswith('SH'): - match = re.search(r'\d+', node) - shell_id = int(match.group(0))-1 - shell = self.shell_lst[shell_id] - lla_dict = load_pos(os.path.join( - self.local_dir, - shell['name'], - 'position', - f'{time_index}.txt' - )) - return lla_dict[node] - elif node.startswith('GS'): - return self.gs_lat_long[_gs2idx(node)] - else: - raise NotImplementedError + def get_position(self, node_name, t): + node = self.nodes.get(node_name) + if node is None: + return None + + tid = t // self.step + return node.cbf_t[tid] if tid < len(node.cbf_t) else node.cbf_t[-1] def get_IP(self, name): node = self.nodes.get(name) @@ -504,12 +467,23 @@ def get_IP(self, name): return node.addr4, node.addr6 # dynamic events + def _validate_t(self, t): + if t >= self.duration: + t = round(self.duration) - 1 + elif t < 0: + t = 0 + else: + t = round(t) + return t + def get_utility(self, t): def _check_utility(real_t): - for mid, machine in enumerate(self.worker_lst): - machine.check_utility(os.path.join( - self.local_dir, f'{real_t}-utility-machine{mid}.txt') - ) + for mid, worker in enumerate(self.worker_lst): + result = worker.check_utility() + with open(os.path.join( + self.local_dir, f'{real_t}-utility-machine{mid}.txt'), 'w') as f: + f.write(result) + self.events.append((t, _check_utility,)) def set_damage(self, damaging_ratio, t): @@ -536,103 +510,69 @@ def _recovery(real_t): def check_routing_table(self, node, t): def _check_route(real_t, node): - self.nodes[node].worker.check_route( - os.path.join(self.local_dir, f'{real_t}-route-{node}.txt'), - node - ) + result = self.nodes[node].worker.check_routing_table(node) + with open(os.path.join(self.local_dir, f'{real_t}-route-{node}.txt'), 'w') as f: + f.write(result) + self.events.append((t, _check_route, node,)) - def set_next_hop(self, src, dst, next_hop, t): - def _set_next_hop(real_t, src, dst, next_hop): - self.nodes[src].worker.sr(src, dst, next_hop) - if src in self.nodes and dst in self.nodes: - self.events.append((t, _set_next_hop, src, dst, next_hop)) - else: - raise ValueError('Specified node not found') + def set_static_route(self, src, dst, next_hop, t): + def _static_route(worker, args_lst): + worker.static_route_batch(args_lst) - def set_static_routes_batch(self, routes_config, t): - """Set static routes for multiple nodes at specified time - - Args: - routes_config: Dictionary mapping node names to lists of route tuples - Each tuple: (dst, gw, dev, metric) - t: Time when the batch routes should be applied - """ - def _set_static_routes_batch(real_t, routes_config): - # Group routes by worker machine to minimize communication - worker_routes = defaultdict(list) - - for node_name, routes in routes_config.items(): - node=self.nodes.get(node_name) - if node is None: - continue - worker_routes[node.worker].append((node_name, routes)) - - for worker, node_routes_lst in worker_routes.items(): - worker.sr_batch(node_routes_lst) - - self.events.append((t, _set_static_routes_batch, routes_config)) + t = self._validate_t(t) + batch_cmd = self.batch_events[t]['static_route'] + batch_cmd.func = _static_route + batch_cmd.args_lst.append((src, dst, next_hop)) - def set_netlink_route(self, node, nlmsg, t): - if t >= self.duration: - t = round(self.duration) - 1 - elif t < 0: - t = 0 - else: - t = round(t) - self.cmd_events[t].append((node, nlmsg)) - - def _netlink_route(self, route_lst): - worker_msgs = defaultdict(list) - for node, nlmsg in route_lst: - worker_msgs[self.nodes[node].worker].append((node, nlmsg)) - for worker, routes in worker_msgs.items(): - worker.netlink(routes) - - def set_ping(self, src, dst, t): - def _ping(real_t, src, dst): - self.ping_threads.append(self.nodes[src].worker.ping_async( - os.path.join(self.local_dir, f'{real_t}-ping-{src}-{dst}.txt'), - src, dst - )) - self.events.append((t, _ping, src, dst)) + def set_netlink(self, node, nlmsg, t): + def _netlink(worker, args_lst): + worker.netlink_batch(args_lst) - def set_iperf(self, src, dst, t, extra_args = []): - if t >= self.duration: - t = round(self.duration) - 1 - elif t < 0: - t = 0 - else: - t = round(t) - self.cmd_events[t].append((src, dst, extra_args)) - - def _iperf(self, cmd_lst): - node_cmds = defaultdict(list) - for src, dst, extra_args in cmd_lst: - node_cmds[self.nodes[src].worker].append([src, dst, *extra_args]) - for node, cmds in node_cmds.items(): - node.iperf(cmds) + t = self._validate_t(t) + batch_cmd = self.batch_events[t]['netlink'] + batch_cmd.func = _netlink + batch_cmd.args_lst.append((node, nlmsg)) - def exec_at(self, node, cmd, t): - def _exec(real_t, node, cmd): - self.nodes[node].worker.exec(node, cmd) - self.events.append((t, _exec, node, cmd)) + def set_ping(self, src, dst, t, extra_args=[]): + def _ping(worker, args_lst): + worker.ping_batch(args_lst) - def exec_now(self, node, cmd): - self.nodes[node].worker.exec(node, cmd) + t = self._validate_t(t) + batch_cmd = self.batch_events[t]['ping'] + batch_cmd.func = _ping + batch_cmd.args_lst.append((src, dst, extra_args)) - def print_all_nodes(self, path): - with open(path, 'w') as f: - for machine in self.worker_lst: - machine.print_nodes(f) + def set_iperf(self, src, dst, t, src_args = [], dst_args = []): + def _iperf(worker, args_lst): + worker.iperf_batch(args_lst) + + t = self._validate_t(t) + batch_cmd = self.batch_events[t]['iperf'] + batch_cmd.func = _iperf + batch_cmd.args_lst.append((src, dst, src_args, dst_args)) + + def exec_at(self, node, cmd, t): + def _exec(worker, args_lst): + worker.exec_batch(args_lst) + + t = self._validate_t(t) + batch_cmd = self.batch_events[t]['exec'] + batch_cmd.func = _exec + batch_cmd.args_lst.append((node, cmd)) def _event(self, real_t): while len(self.events) > 0 and self.events[-1][0] <= real_t: event = self.events.pop(-1) event[1](real_t, *event[2:]) while self.last_t <= real_t: - self._netlink_route(self.netlink_events[self.last_t]) - self._iperf(self.cmd_events[self.last_t]) + for batch_cmd in self.batch_events[self.last_t].values(): + node_args = defaultdict(list) + for args in batch_cmd.args_lst: + node_args[self.nodes[args[0]].worker].append(args) + for worker, args_lst in node_args.items(): + batch_cmd.func(worker, args_lst) + self.last_t += 1 def start_emulation(self): @@ -641,39 +581,50 @@ def start_emulation(self): self.ping_threads = [] self.iperf_threads = [] - t = 0.0 + + start_time = time.time() + print('Tick event at 0 s') + self._event(0) + tid = 1 - while t < self.duration: + while tid < len(self.changes_t): + t = tid * self.step + target_time = start_time + t + + now = time.time() + if now < target_time: + sleep_time = target_time - now + print('Sleeping', sleep_time, 's until', t, 's') + time.sleep(sleep_time) + start = time.time() - print("Update networks using pre-computed topology...") - if tid < self.duration: - network_change = self.changes_t[tid] - - network_change['isl_bw'] = str(self.sat_bandwidth) - network_change['isl_loss'] = str(self.sat_loss) - network_change['gsl_bw'] = str(self.sat_ground_bandwidth) - network_change['gsl_loss'] = str(self.sat_ground_loss) - - conn_threads = [] - for worker in self.worker_lst: - thread = threading.Thread( - target=worker.update_network, - args=(network_change,) - ) - thread.start() - conn_threads.append(thread) - for thread in conn_threads: - thread.join() + print("\nUpdate networks using pre-computed topology...") + + network_change = self.changes_t[tid] + network_change['isl_bw'] = str(self.sat_bandwidth) + network_change['isl_loss'] = str(self.sat_loss) + network_change['gsl_bw'] = str(self.sat_ground_bandwidth) + network_change['gsl_loss'] = str(self.sat_ground_loss) + + conn_threads = [] + for worker in self.worker_lst: + thread = threading.Thread( + target=worker.update_network, + args=(network_change,) + ) + thread.start() + conn_threads.append(thread) + for thread in conn_threads: + thread.join() update_end = time.time() - print("\nTrigger events at", t, "s ...") + + print("Trigger events at", t, "s ...") self._event(t) end = time.time() - print(end-start, "s elapsed,", update_end-start, "s for network update") - if end - start < 1: - print('Sleep', 1 + start - end, 's') - time.sleep(1 + start - end) - t += self.step + elapsed = end - start + print(elapsed, "s elapsed,", update_end-start, "s for network update") tid += 1 + for ping_thread in self.ping_threads: ping_thread.join() for iperf_thread in self.iperf_threads: From 2ba6b04fb5e83825aaae99b210462a86567659e9 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 29 Apr 2026 14:44:40 +0800 Subject: [PATCH 27/49] fix: modify_route in pynetlink.c fix netlink error caused by NLM_F_CREATE missing in nlmsg_flags --- starrynet/pynetlink.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starrynet/pynetlink.c b/starrynet/pynetlink.c index f9e4fe9..29ee161 100644 --- a/starrynet/pynetlink.c +++ b/starrynet/pynetlink.c @@ -480,7 +480,7 @@ static int modify_route4(int sock_fd, uint16_t op, const struct in_addr *dst4, u nl_hdr = (struct nlmsghdr*)buf; nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); nl_hdr->nlmsg_type = op; - nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE; nl_hdr->nlmsg_seq = seq; nl_hdr->nlmsg_pid = 0; @@ -555,7 +555,7 @@ static int modify_route6(int sock_fd, uint16_t op, const struct in6_addr *dst6, nl_hdr = (struct nlmsghdr*)buf; nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); nl_hdr->nlmsg_type = op; - nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nl_hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE; nl_hdr->nlmsg_seq = seq; nl_hdr->nlmsg_pid = 0; From 2a49356628e7dbfc93fa126680cc74852146cdb8 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 29 Apr 2026 14:57:51 +0800 Subject: [PATCH 28/49] refactor(api): align example, synchronizer and orchestrator with new API Update `example.py` to use keyword-based method calls, and simplify duplicated demo flows for clearer API guidance. Update `example.py` to use keyword-based method calls, rename routing usage to `set_static_route`, and simplify duplicated demo flows for clearer, current API guidance. Fix the implementation of batch command and damage/recovery in synchronizer. In orchestrator, use NetInterace to track link status and implement damage/recovery, ping/iperf based on that. Additionally, fix the bugs in route modification, iperf command startup and route checking. --- example.py | 82 +++++--------- starrynet/sn_daemon_client.py | 5 +- starrynet/sn_orchestrater.py | 162 +++++++++++++--------------- starrynet/sn_orchestrator_daemon.py | 3 +- starrynet/sn_synchronizer.py | 44 ++++---- 5 files changed, 127 insertions(+), 169 deletions(-) diff --git a/example.py b/example.py index 52a674e..209740c 100644 --- a/example.py +++ b/example.py @@ -16,7 +16,6 @@ GS_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817] ] # latitude and longitude of frankfurt and Austria configuration_file_path = "./config.json" - bird_conf_path = "./bird.conf" print('Start StarryNet.') sn = StarryNet(configuration_file_path, GS_lat_long) @@ -24,79 +23,52 @@ sn.create_nodes() sn.create_links() - node1 = 'SH1O1S1' - node2 = 'SH1O1S2' - time_index = 2 # LLA of a node at a certain time - LLA = sn.get_position(node1, time_index) - print(f'\nLatitude, Longitude, Altitude of {node1}: {LLA}') + LLA = sn.get_position(node='SH1O1S1', t=2) + print(f'\nLatitude, Longitude, Altitude of SH1O1S1: {LLA}') # distance between nodes at a certain time - node_distance = sn.get_distance(node1, node2, time_index) - print(f'\n{node1}-{node2} distance(km): {node_distance}') + node_distance = sn.get_distance(node1='SH1O1S1', node2='SH1O1S2', t=2) + print(f'\nSH1O1S1 - SH1O1S2 distance(km): {node_distance}') # neighbor nodes at a certain time - neighbors = sn.get_neighbors(node1, time_index) - print(f'\n{node1} neighbors: {neighbors}') - + neighbors = sn.get_neighbors(node='SH1O1S1', t=2) + print(f'\nSH1O1S1 neighbors: {neighbors}') + # GS connected to the node at a certain time - GSes = sn.get_GSes(node1, time_index) - print(f"\n{node1} GSes: {GSes}") + GSes = sn.get_GSes(node='SH1O1S1', t=2) + print(f"\nSH1O1S1 GSes: {GSes}") - time_index = 1 - sn.get_utility(time_index) # CPU and memory useage + # CPU and memory useage + sn.get_utility(t=2) - # IP dict of a node - IP_dict = sn.get_IP(node1) - print(f'\n{node1} IP addresses: {IP_dict}') + # IP addresses of a node + IPs = sn.get_IP(node='SH1O1S1') + print(f'\nSH1O1S1 IP addresses: {IPs}') + bird_conf_path = "./bird.conf" # run OSPF daemon on all nodes - # sn.run_routing_daemon() - + # sn.run_routing_daemon(bird_conf_path=bird_conf_path) # run OSPF daemon on selected nodes - sn.run_routing_daemon(node_lst=['GS0', 'SH1O2S2', 'SH1O2S3', 'SH1O3S3', 'GS1']) - - sat = 'SH1O1S1' - des = 'SH1O1S3' - next_hop = 'SH1O1S2' - time_index = 2 - # set the next hop at a certain time. Sat and NextHopSat are neighbors. - sn.set_next_hop(sat, des, next_hop, time_index) + # sn.run_routing_daemon(bird_conf_path=bird_conf_path, node_lst=['GS0', 'SH1O2S2', 'SH1O2S3', 'SH1O3S3', 'GS1']) - time_index = 3 - # routing table of a node at a certain time. The output file will be written at the working directory. - sn.check_routing_table(sat, time_index) + # set the next hop at a certain time. + sn.set_static_route(src='SH1O1S1', dst='SH1O1S2', next_hop='SH1O1S2', t=2) - node1 = 'SH1O1S1' - node2 = 'SH1O1S2' - time_index = 4 - # ping msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_ping(node1, node2, time_index) - # perf msg of two nodes at a certain time. The output file will be written at the working directory. - sn.set_iperf(node1, node2, time_index) + # routing table of a node at a certain time. + print(sn.check_routing_table(node='SH1O1S1', t=3)) + # ping msg of two nodes at a certain time. + sn.set_ping(src='SH1O1S1', dst='SH1O1S2', t=4) - # run OSPF daemon on all nodes - # sn.run_routing_daemon() - - # run OSPF daemon on selected nodes - sn.run_routing_daemon(node_lst=[ - 'GS0', 'SH1O25S14', 'SH1O26S14', 'SH1O27S14', 'SH1O27S13', 'GS1']) + # perf msg of two nodes at a certain time. + sn.set_iperf(src='SH1O1S1', dst='SH1O1S2', t=5) - sn.check_routing_table('GS0', 4) - - ratio = 0.3 - time_index = 15 # random damage of a given ratio at a certain time - sn.set_damage(ratio, time_index) - - sn.check_routing_table('GS0', 30) + sn.set_damage(damaging_ratio=0.3, t=6) - time_index = 35 # recover the damages at a certain time - sn.set_recovery(time_index) - - sn.check_routing_table('GS0', 45) + sn.set_recovery(t=7) sn.start_emulation() diff --git a/starrynet/sn_daemon_client.py b/starrynet/sn_daemon_client.py index f17442c..adad833 100644 --- a/starrynet/sn_daemon_client.py +++ b/starrynet/sn_daemon_client.py @@ -268,13 +268,10 @@ def damage_nodes(self, nodes: list): raise Exception(f"Failed to damage nodes: {response.get('message')}") return response - def recover_nodes(self, sat_loss: float): + def recover_nodes(self): command = { 'c': 'recovery', 't': time.time(), - 'p': { - 'loss': sat_loss - } } response = self._send_command_via_ssh(command) if response.get('status') != 'success': diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py index b98e409..1774f5d 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrater.py @@ -8,6 +8,7 @@ import threading import queue from typing import Dict, List, Tuple +from enum import Enum from collections import defaultdict # from line_profiler import LineProfiler @@ -33,23 +34,26 @@ ) import pynetlink -NOT_ASSIGNED = 'NA' # FIXME -CLONE_NEWNET = 0x40000000 NETNS_DIR = '/run/netns' _libc = ctypes.CDLL(None) def _switch_netns(node_pid: int): + CLONE_NEWNET = 0x40000000 pid_fd = os.open(f'/proc/{node_pid}/ns/net', os.O_RDONLY) _libc.setns(pid_fd, CLONE_NEWNET) os.close(pid_fd) -class Interface: - """Interface object representing a network interface in the orchestrator""" - - def __init__(self, ifname: str): - self.ifname = ifname +class NetInterface: + def __init__(self, if_idx: int, ipv4: ipaddress.IPv4Interface = None, ipv6: ipaddress.IPv6Interface = None): + self.if_idx = if_idx + self.ipv4 = ipv4 + self.ipv6 = ipv6 + +class NodeStatus(Enum): + UP = 1 + DOWN = 2 class Node: """Node object representing a network node in the orchestrator""" @@ -58,6 +62,7 @@ def __init__(self, name: str, node_dir: str, node_id: int = 0): self.name = name self.node_id = node_id self.pid = pyctr.container_run(node_dir, name) + self.status = NodeStatus.UP netns_link = f'{NETNS_DIR}/{name}' if os.path.islink(netns_link): os.unlink(netns_link) @@ -65,7 +70,8 @@ def __init__(self, name: str, node_dir: str, node_id: int = 0): _switch_netns(self.pid) self.socket_fd = pynetlink.init_socket(self.pid) - self.interfaces = {} + self.idle_links: List[NetInterface] = list() + self.peer2link: Dict[str, NetInterface] = dict() # Initialize loopback addresses self._init_loopback() @@ -74,8 +80,6 @@ def __lt__(self, other): return self.pid < other.pid def _init_loopback(self): - ipv4_lo = f"16.{(self.node_id >> 8) & 0xFF}.{self.node_id & 0xFF}.1/32" - ipv6_lo = f"2000::{self.node_id:04x}/128" _switch_netns(self.pid) pynetlink.if_up('lo', self.socket_fd) @@ -86,20 +90,18 @@ def _init_loopback(self): except FileNotFoundError: pass - try: - addr4 = ipaddress.IPv4Interface(ipv4_lo) - pynetlink.modify_addr(True, 'lo', addr4.packed, addr4.network.prefixlen, self.socket_fd) - self.loopback_ipv4 = addr4 - except Exception as e: - print(f"Warning: Failed to set IPv4 loopback {ipv4_lo} for node {self.name}: {e}") - - try: - addr6 = ipaddress.IPv6Interface(ipv6_lo) - pynetlink.modify_addr(True, 'lo', addr6.packed, addr6.network.prefixlen, self.socket_fd) - self.loopback_ipv6 = addr6 - except Exception as e: - print(f"Warning: Failed to set IPv6 loopback {ipv6_lo} for node {self.name}: {e}") + addr4 = ipaddress.IPv4Interface( + f"16.{(self.node_id >> 8) & 0xFF}.{self.node_id & 0xFF}.1/32") + addr6 = ipaddress.IPv6Interface( + f"2000::{self.node_id:04x}/128" + ) + lo_link = NetInterface(1, addr4, addr6) + pynetlink.modify_addr(True, 'lo', addr4.packed, addr4.network.prefixlen, self.socket_fd) + pynetlink.modify_addr(True, 'lo', addr6.packed, addr6.network.prefixlen, self.socket_fd) + pynetlink.if_up('lo', self.socket_fd) + + self.peer2link['lo'] = lo_link def __del__(self): """Destructor to cleanup resources""" @@ -120,8 +122,8 @@ def run_command(self, command, *args, **kwargs): *args, **kwargs ) - def register_if(self, if_name: str): - self.interfaces[if_name] = Interface(if_name) + def register_if(self, if_name: str, if_idx: int): + self.peer2link[if_name] = NetInterface(if_idx = if_idx) def init_if(self, if_name: str, addr: str, addr6: str, delay: str, bw: str, loss: str): addr = ipaddress.IPv4Interface(addr) @@ -131,6 +133,11 @@ def init_if(self, if_name: str, addr: str, addr6: str, delay: str, bw: str, loss pynetlink.modify_addr(True, if_name, addr6.packed, addr6.network.prefixlen, self.socket_fd) pynetlink.traffic_control(if_name, delay, bw, loss, self.socket_fd) pynetlink.if_up(if_name, self.socket_fd) + link = self.peer2link.get(if_name) + if link is not None: + link.ipv4 = addr + link.ipv6 = addr6 + try: fd = os.open(f'/proc/sys/net/mpls/conf/{if_name}/input', os.O_WRONLY) os.write(fd, b'1') @@ -142,7 +149,7 @@ def update_if(self, if_name: str, delay: str, bw: str, loss: str): _switch_netns(self.pid) pynetlink.traffic_control(if_name, delay, bw, loss, self.socket_fd) - def modify_routes(self, routes: List[Tuple[str, str, str, int]]): + def modify_routes(self, routes: List): _switch_netns(self.pid) pynetlink.modify_routes(routes, self.socket_fd) @@ -155,11 +162,11 @@ class OrchestratorContext: def __init__(self, workdir): self.workdir = workdir - self.damage_dict = {} self._main_net_sock_fd = pynetlink.init_socket() self.nodes: Dict[str, Node] = {} + self.damage_lst: List[Node] = [] self.cmd_to_start = queue.PriorityQueue() self.cmd_cnt_dict = defaultdict(int) @@ -168,7 +175,7 @@ def __init__(self, workdir): def __del__(self): try: - os.close(self._main_net_fd) + os.close(self._main_net_sock_fd) except: pass @@ -189,7 +196,7 @@ def _check_commands(self): self.cmd_cnt_dict[node.name] += 1 cmd_id = self.cmd_cnt_dict[node.name] fd = os.open( - f'{self.workdir}/cmd_{node.name}_{cmd_id}_{cmdline[0]}.out', + f'{self.workdir}/cmd_{node.name}_{cmd_id}.out', os.O_WRONLY | os.O_CREAT | os.O_TRUNC ) os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) @@ -214,7 +221,7 @@ def clean(self): del node self.nodes.clear() - self.damage_dict.clear() + self.damage_lst.clear() def init_nodes(self, base_dir, node_configs): """ @@ -245,7 +252,10 @@ def init_nodes(self, base_dir, node_configs): self.nodes[node_name] = Node(node_name, node_dir, node_id=node_id) _switch_netns(os.getpid()) - return {name: (node.loopback_ipv4.compressed, node.loopback_ipv6.compressed) for name, node in self.nodes.items()} + return { + name: (node.peer2link['lo'].ipv4.compressed, node.peer2link['lo'].ipv6.compressed) + for name, node in self.nodes.items() + } def add_link_intra_machine(self, name1: str, name2: str, @@ -259,6 +269,8 @@ def add_link_intra_machine(self, return pynetlink.add_link_veth(node1.pid, src_ifidx, name2, node2.pid, dst_ifidx, name1, self._main_net_sock_fd) + node1.register_if(name2, src_ifidx) + node2.register_if(name1, dst_ifidx) node1.init_if(name2, src_addr4, src_addr6, delay, bw, loss) node2.init_if(name1, dst_addr4, dst_addr6, delay, bw, loss) @@ -300,7 +312,11 @@ def set_static_route(self, src: str, dst: str, next_hop: str): if src_node is None or dst_node is None or next_hop_node is None: return - src_node.modify_routes([(dst_node.interfaces[0], dst_node.network_address.packed, dst_node.prefixlen, next_hop_node.packed)]) + dst_network = dst_node.peer2link['lo'].ipv4.network + via_addr = next_hop_node.peer2link[src].ipv4.ip.packed + src_node.modify_routes([ + (True, dst_network.network_address.packed, dst_network.prefixlen, next_hop, via_addr) + ]) def ping(self, src: str, dst: str): src_node = self.nodes.get(src) @@ -308,36 +324,22 @@ def ping(self, src: str, dst: str): if src_node is None or dst_node is None: return - dst_addr_lst = subprocess.check_output( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', dst_node.pid, - 'ip', '-br', 'addr', 'show') - ).decode().splitlines() - for dev_state_addrs in dst_addr_lst: - dev_state_addrs = dev_state_addrs.split() - if dev_state_addrs[0] == 'lo': - continue - dst_addr = dev_state_addrs[2] - if dev_state_addrs[0].split('@')[0] == src: - break - dst_addr = dst_node.loopback_ipv4.ip.compressed - - subprocess.run( - ('nsenter', '-m', '-u', '-i', '-n', '-p', '-t', src_node.pid, - 'ping', '-c', '4', '-i', '0.01', dst_addr), - stdout=sys.stdout, stderr=subprocess.STDOUT - ) + dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed + + self.cmd_to_start.put((time.perf_counter(), src_node, ('ping', '-c', '4', '-i', '0.01', dst_addr))) def iperf(self, cmds): time_point = time.perf_counter() for cmd in cmds: src_node = self.nodes.get(cmd[0]) dst_node = self.nodes.get(cmd[1]) + src_args, dst_args = cmd[2], cmd[3] if src_node is None or dst_node is None: continue - dst_addr = dst_node.loopback_ipv4.ip.compressed - self.cmd_to_start.put((time_point, dst_node, ('iperf3', '-s'))) - self.cmd_to_start.put((time_point + 0.5, src_node, ('iperf3', '-c', dst_addr, *cmd[2:]))) + dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed + self.cmd_to_start.put((time_point, dst_node, ('iperf3', '-s', '-1', *dst_args))) + self.cmd_to_start.put((time_point + 0.5, src_node, ('iperf3', '-c', dst_addr, *src_args))) def exec(self, node_name: str, cmd: str): """Execute command in node using context""" @@ -367,8 +369,7 @@ def check_route(self, node_name: str): return '' return subprocess.check_output( - ('nsenter', '-n', '-t', node.pid, - 'route'), + ('nsenter', '-n', '-t', str(node.pid), 'route'), ) def damage(self, random_list: List[str]): @@ -379,43 +380,32 @@ def damage(self, random_list: List[str]): continue _switch_netns(node.pid) - out = subprocess.check_output( - ('ip', '-br', 'addr', 'show')).decode() - dev_lst = [] - - for line in out.splitlines(): - line = line.strip() - if len(line) == 0 or line.startswith('lo'): + for ifname, link in node.peer2link.items(): + if link.if_idx == 1: continue - toks = line.split() - dev_name = toks[0].split('@')[0] - addr = None - for tok in toks[1:]: - if ':' in tok: - # found first ip6 addr - addr = ipaddress.IPv6Interface(tok) - break - pynetlink.if_down(dev_name, node.socket_fd) - dev_lst.append((dev_name, addr)) - - self.damage_dict[node] = dev_lst + pynetlink.if_down(ifname, node.socket_fd) + + self.damage_lst.append(node) - def recover(self, sat_loss: str): - if not self.damage_dict: + def recover(self): + if not self.damage_lst: return - for node_name, dev_lst in self.damage_dict.items(): - node = self.nodes.get(node_name) - if node is None: - continue - + for node in self.damage_lst: _switch_netns(node.pid) - for dev_name, addr in dev_lst: - pynetlink.if_up(dev_name, node.socket_fd) - pynetlink.modify_addr(True, dev_name, addr.ip.packed, addr.network.prefixlen, node.socket_fd) - - self.damage_dict.clear() + for ifname, link in node.peer2link.items(): + if link.if_idx == 1: + continue + try: + pynetlink.if_up(ifname, node.socket_fd) + pynetlink.modify_addr( + True, ifname, link.ipv6.ip.packed, link.ipv6.network.prefixlen, node.socket_fd + ) + except: + pass + + self.damage_lst.clear() def update_if(self, node_name: str, ifname: str, delay: str, bw: str, loss: str): node = self.nodes.get(node_name) diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index e9419a6..e78ec00 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -413,8 +413,7 @@ def _handle_damage(self, params): def _handle_recovery(self, params): try: context = self._get_context() - sat_loss = params.get('loss') - context.recover(sat_loss) + context.recover() return {"message": "Recovery completed successfully"} except Exception as e: raise Exception(f"Recovery failed: {e}") diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index a4db426..53391c9 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -47,8 +47,8 @@ class NodeInfo: @dataclass class BatchCommand: - func: Callable - args_lst: list + func: Callable = None + args_lst: list = field(default_factory=list) def _gs2idx(gs_name): return int(gs_name[2:])-1 @@ -412,16 +412,16 @@ def run_routing_daemon(self, bird_conf_path, node_lst='all'): for name, node in self.nodes.items(): rtd_lsts[node.worker].append(name) for worker, names in rtd_lsts.items(): - worker.init_routing(names, bird_conf) - + worker.init_routing(','.join(names), bird_conf) + for i in range(30): print(f'\r{i} / 30', end=' ') time.sleep(1) print("Routing started!") # static information - def get_distance(self, name1, name2, t): - node1, node2 = self.nodes.get(name1), self.nodes.get(name2) + def get_distance(self, node1, node2, t): + node1, node2 = self.nodes.get(node1), self.nodes.get(node2) if node1 is None or node2 is None: return None @@ -431,16 +431,16 @@ def get_distance(self, name1, name2, t): dx, dy, dz = xyz1[0] - xyz2[0], xyz1[1] - xyz2[1], xyz1[2] - xyz2[2] return math.sqrt(dx * dx + dy * dy + dz * dz) - def get_neighbors(self, sat, t): - node = self.nodes.get(sat) + def get_neighbors(self, node, t): + node = self.nodes.get(node) if node is None: return [] tid = t // self.step return list(node.links_t[tid].keys()) - def get_GSes(self, sat_name, t): - node = self.nodes.get(sat_name) + def get_GSes(self, node, t): + node = self.nodes.get(node) if node is None: return [] @@ -452,19 +452,19 @@ def get_GSes(self, sat_name, t): GSes.append(dst) return GSes - def get_position(self, node_name, t): - node = self.nodes.get(node_name) + def get_position(self, node, t): + node = self.nodes.get(node) if node is None: return None tid = t // self.step return node.cbf_t[tid] if tid < len(node.cbf_t) else node.cbf_t[-1] - def get_IP(self, name): - node = self.nodes.get(name) + def get_IP(self, node): + node = self.nodes.get(node) if node is None: return () - return node.addr4, node.addr6 + return node.addr4.compressed, node.addr6.compressed # dynamic events def _validate_t(self, t): @@ -488,23 +488,23 @@ def _check_utility(real_t): def set_damage(self, damaging_ratio, t): def _damage(real_t, damaging_ratio): - damage_lsts = {machine:[] for machine in self.worker_lst} + damage_lsts = {worker:[] for worker in self.worker_lst} cur_num = len(self.undamaged_lst) need_damage_num = min(len(self.total_sat_lst) * damaging_ratio, cur_num) while(cur_num - len(self.undamaged_lst) < need_damage_num): sat = self.undamaged_lst.pop( random.randint(0, len(self.undamaged_lst) - 1) ) - machine = self.nodes[sat].worker - damage_lsts[machine].append(sat) - for machine, lst in damage_lsts.items(): - machine.damage(lst) + worker = self.nodes[sat].worker + damage_lsts[worker].append(sat) + for worker, lst in damage_lsts.items(): + worker.damage_nodes(lst) self.events.append((t, _damage, damaging_ratio,)) def set_recovery(self, t): def _recovery(real_t): - for machine in self.worker_lst: - machine.recovery(self.sat_loss) + for worker in self.worker_lst: + worker.recover_nodes() self.undamaged_lst = self.total_sat_lst.copy() self.events.append((t, _recovery,)) From cc21c5cf801ba43c46f2ff85ce51e6425c1d4f3b Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 29 Apr 2026 17:31:12 +0800 Subject: [PATCH 29/49] fix: align docs and CLI with current StarryNet workflow - rewrite README around the current non-Docker runtime - document worker daemon startup and config expectations - align CLI commands with current node-name-based APIs - add basic CLI argument validation and status helpers - update install script and setup.py for current dependencies --- README.md | 319 +++++++++++++++++-------------- bin/sn | 204 ++++++++------------ config.json | 44 ++--- install.sh | 12 +- setup.py | 67 ++++--- starrynet/cli.py | 414 +++++++++++++++++++++++------------------ tools/requirements.txt | 3 - 7 files changed, 551 insertions(+), 512 deletions(-) mode change 100644 => 100755 bin/sn diff --git a/README.md b/README.md index 2af5ab0..46bc04d 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,245 @@ # StarryNet -StarryNet for the emulation of satellite Internet constellations. +StarryNet is a satellite-network emulator for building constellation topologies, creating links between satellites and ground stations, and scheduling runtime events such as link/routing changes, ping, iperf, link damage, and recovery. -## What is StarryNet? +## Current Runtime Model -StarryNet helps you to emulate your customized constellation and ground stations with run-time routing for a given duration. With StarryNet, you can test availability/bandwidth/loss within nodes, check routing states of a node and even damage certain links. +StarryNet no longer depends on Docker. It uses: -## What are the components? +- Python code for topology generation and control +- a small CPython C extension for namespace/container orchestration +- remote StarryNet worker daemons described in `config.json` -1. A configuration file (`config.json`). -2. An API Library (`starrynet`). -3. An example leveraging APIs to run your trials (`example.py`). -4. A `setup.py` and `./bin/sn`. +If you are migrating from older documentation, ignore Docker-specific setup steps. -## Preparation +## Repository Layout -CentOS 7.9.2009 or above, Docker 20.10.17 or above and Python 3.6 or above. +- `config.json`: sample topology and worker configuration +- `example.py`: Python API example +- `bin/sn`: interactive CLI entrypoint +- `bird.conf`: sample BIRD routing config +- `starrynet/`: library code -1. Support for CentOS 7.9.2009 and Python 3 or Python 2. Also support Ubuntu 20.04 LTS (and 18.04). -2. Install Docker on the machine for emulation. +## Requirements -## Installation - -Then run `bash ./install.sh` to install CLI sn, which will also install packets like `python3 -m pip install setuptools xlrd copy argparse time numpy random requests math skyfield sgp4 datetime paramiko`. - -## How to use it? - -1. Prepare a data directory: - -Finish *remote_machine_IP, remote_machine_username and remote_machine_password* in config.json to specify the remote machine for running the emulation. - -Put `config.json`, `starrynet`, and `example.py` at the same working directory. - -3. Start emulation: - -To speficy your own constellation, copy `config.json` and fill in the fields in it according to your satellite emulation environment, including the constellation name, orbit number, satellite number per orbit, ground station number, ground user number connected to each ground station and so on. You are only allowed to change `Name`, `Altitude (km)`, `Cycle (s)`, `Inclination`, `Phase shift`, `# of orbit`, `# of satellites`, `Duration(s)`, `update_time (s)`, `satellite link bandwidth ("X" Gbps)`, `sat-ground bandwidth ("X" Gbps)`, `satellite link loss ( 'X'% )`, `sat-ground loss ( 'X'% )`, `GS number`, `multi-machine('0' for no, '1' for yes)`, `antenna number`, `antenna_inclination_angle`, `remote_machine_IP`, `remote_machine_username`, `remote_machine_password` in `config.json`. - -Then use the APIs in `example.py` to start your trails. Remember to change the configuration_path of your `config.json`. - -4. OSPF is the only intra-routing protocol. In `example.py` you need to set he hello-interval. (example in example.py): - -> HelloInterval = 1 - -5. In `example.py`, you need to specify the latitude and longitude of ground stations in sequence. Their node indexes will be named right after the satellite nodes. - -> GS_lat_long=[[50.110924,8.682127],[46.635700,14.311817]] # frankfurt and Austria - -6. `ConfigurationFilePath` is where you put your config.json file, specified in `example.py`. - > ConfigurationFilePath = "./config.json" - -## What are the APIs? - -> sn.create_nodes() - -This API creates nodes for emulation, including satellite and GS nodes. - -> sn.create_links() - -This API creates initial network links for emulation. +The exact system packages depend on your Linux distribution, but in practice you need: -> sn.run_routing_deamon() +- Python 3 +- `pip` +- a C compiler such as `gcc` +- Python development headers for compiling CPython extensions +- network namespace support on the host +- reachable worker machines matching the `Machines` section in `config.json` (`127.0.0.1` recommended for initial trials) -This API initiates the OSPF routing for the network, otherwise the network has no routing protocol running. +Python packages are listed in `tools/requirements.txt`. -> sn.get_distance(node_index1, node_index2, time_index) - -This API returns distance between nodes at a certain time. - -> sn.get_neighbors(node_index1, time_index) - -This API returns neighbor node indexes of a node at a certain time. - -> sn.get_GSes(node_index1, time_index) - -This API returns GSes connected to the node at a certain time. - -> sn.get_position(node_index1, time_index) - -This API returns the LLA of a node at a certain time. - -> sn.get_utility(time_index) - -This API returns the current CPU utility and memory utility. - -> sn.get_IP(node_index1) - -This API returns a list of IPs of a node at a certain time. - -> sn.set_damage(ratio, time_index) - -This API sets a random damage for the network links of a given ratio at a certain time. - -> sn.set_recovery(time_index) - -This API will recover all the damaged links at a certain time. - -> sn.check_routing_table(node_index1, time_index) - -This API returns a routing table file of a node at a certain time. The output file could be found at the working directory. +## Installation -> sn.set_next_hop(sat, des, next_hop_sat, time_index) +Install Python dependencies: -This API sets the next hop at a certain time. Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors. +```bash +python3 -m pip install -r tools/requirements.txt +``` -> sn.set_ping(node_index1, node_index2, time_index) +Install the package and CLI: -This API will starts pinging msg of two nodes at a certain time. The output file could be found at the working directory. +```bash +python3 setup.py install +``` -> sn.set_perf(node_index1, node_index2, time_index) +If you prefer the bundled installer, it now installs Python dependencies and the package only: -This API will starts perfing msg of two nodes at a certain time. The output file could be found at the working directory. +```bash +bash ./install.sh +``` -> sn.start_emulation() +## Quick Start -This API starts the entire emulation of the duration. +### 1. Start a worker daemon -> sn.stop_emulation() +Before running `example.py` or `sn`, start at least one worker daemon that matches the machine entry in `config.json`. -This API stops the eimulation and clears the environment. +Example: -## Example one: use APIs in python +```bash +python3 starrynet/sn_orchestrator_daemon.py \ + --workdir test \ + --machine-id 0 \ + --ssh-username abc \ + --ssh-password 123456 +``` -Run example.py to emulate the network. +Important details: -In this example, 5\*5 satellites from Starlink in 550km with an inclination of 53 degree and two ground stations in Frankfurt and Austria are emulated. The node index sequence is: 25 sattelites, 2 ground stations. 25 satellites and 2 ground stations are in one AS, where OSPF is running within it. Hello_interval(s) in OSPF is set as one second. AS specified in `config.json`, each GS has one antenna with an 25 degree inclination angle to connect the nearest satellite. Loss and throuput are alse set in `config.json`. Link delay updating granularity is one second. +- `--ssh-username` and `--ssh-password` must match the `username` and `password` fields in `config.json` +- `--ssh-port` defaults to `18888`, which should match the `port` field in `config.json` +- `--workdir` is where the daemon writes logs, SSH host keys, and runtime artifacts +- for the sample `config.json`, `127.0.0.1:18888` is the expected local worker endpoint -The emulation duration is set as 100 seconds in `config.json`. A 30 percent damage ratio is set in #5 second and the network will be recovered in #10 second. In #15 second, we'd like to see the routing table of node 27, which will be found in the working directory. In #20 second, we set the next hop of node 1 to node 2 in order to get to node 27. And we will have a ping information from node 26 to node 27 from #30 second to #80 second. After running, a new directory will be made in the current path, where the output information will be found. +You can inspect all daemon options with: -Other APIs help show the distance in km between node #1 and node #2 at #2 second, neighbor indexes of node #1 at the time, connected GS of node #7 at #2 second, LLA information of the node at the same time and all the IP of the node. Besides, get_utility will download a memory and CPU utility information at the time. +```bash +python3 starrynet/sn_orchestrator_daemon.py --help +``` -## Example two: use CLI in shell +### 2. Update `config.json` -Finish *remote_machine_IP, remote_machine_username and remote_machine_password* in config.json to specify the remote machine for running the emulation. Other fields should also be filled as described above. +At minimum, review the `Machines` section: -> sn +```json +"Machines": [ + { + "IP": "127.0.0.1", + "port": 18888, + "username": "abc", + "password": "123456" + } +] +``` -In the same path of `config.json`, run `sn` in shell, you will see the starrynet CLI. `sn` automatically starts a 5*5(satellites)+2(GS) scale network as described above if you only finish *remote_machine_IP, remote_machine_username and remote_machine_password* in config.json without changing other fields. See an example below. +Other commonly changed fields: -> sn -h +- `Shells` +- `Duration (s)` +- `step (s)` +- `satellite link bandwidth ("X" Gbps)` +- `sat-ground bandwidth ("X" Gbps)` +- `satellite link loss ("X"% )` +- `sat-ground loss ("X"% )` +- `antenna number` +- `antenna elevation angle` -> sn +### 3. Run the Python example -*This starts the CLI with the default 5*5+2 scale. You may also specify your customized scale in a config.json and run `sn -p "./config.json" -i 1 -n 27 -g 50.110924/8.682127/46.635700/14.311817` to start your own emulation. Here `-p` infers to the customized config.json path, `-i` infers to your customized hello packet intervall (10 by default), `-n` infers to the total node number and `-g` infers to the latitude and longitude of the GSes.* +```bash +python3 example.py +``` -> starrynet> help +The example: -> starrynet> create_nodes +- loads `./config.json` +- creates nodes and links +- queries topology state +- schedules ping, iperf, damage, recovery, and route dump events +- starts the emulation -> starrynet> create_links +The example uses node names such as `SH1O1S1` and `GS0`. This is the naming style used by the current API and CLI. -> starrynet> run_routing_deamon +### 4. Run the interactive CLI -> starrynet> get_distance 1 2 10 +```bash +sn +``` -*It means getting the distance of two node (#1 and #2) at #10 second.* +Useful options: -> starrynet> get_neighbors 5 16 +```bash +sn --help +sn --path ./config.json +sn --gs 50.110924/8.682127/46.635700/14.311817 +sn --bird-conf ./bird.conf +sn --clean +``` -*It means getting the neighbor node indexes of node #5 at #16 second.* +## CLI Workflow -> starrynet> get_GSes 7 20 +Start with: -*It means getting the connected GS node indexes of node #6 at #20 second.* +```text +starrynet> create_nodes +starrynet> create_links +starrynet> run_routing_daemon +starrynet> start_emulation +``` -> starrynet> get_position 7 23 +Useful inspection commands: -*It means getting the LLA position of node #7 at #23 second.* +```text +starrynet> status +starrynet> nodes +starrynet> nodes SH1O1 +starrynet> path +starrynet> get_distance SH1O1S1 SH1O1S2 2 +starrynet> get_neighbors SH1O1S1 2 +starrynet> get_GSes SH1O1S1 2 +starrynet> get_position SH1O1S1 2 +starrynet> get_IP SH1O1S1 +``` -> starrynet> get_IP 8 +Scheduling commands: -*It means getting the IP addresses of node #8. "create_nodes" and "create_links" must be runned before this.* +```text +starrynet> get_utility 2 +starrynet> check_routing_table SH1O1S1 3 +starrynet> set_static_route SH1O1S1 SH1O1S2 SH1O1S2 4 +starrynet> set_ping SH1O1S1 SH1O1S2 5 +starrynet> set_iperf SH1O1S1 SH1O1S2 6 +starrynet> set_damage 0.3 7 +starrynet> set_recovery 8 +starrynet> start_emulation +``` -> starrynet> get_utility 27 +Notes: -*It means getting the memory and CPU utility information at #27 second. The output file will be generated at the working directory once the emulation starts.* +- `run_routing_daemon` uses `./bird.conf` by default. +- You may pass a different BIRD config path: -> starrynet> set_damage 0.3 30 +```text +starrynet> run_routing_daemon ./bird.conf +``` + +- You may restrict routing startup to selected nodes: -*It means setting a random damage of a given ratio of 0.3 at #30 second, which will be processed during emulation.* +```text +starrynet> run_routing_daemon ./bird.conf GS0 SH1O1S1 SH1O1S2 +``` -> starrynet> set_recovery 50 +- Event commands are queued first and executed when `start_emulation` advances to the target time. -*It means setting a recovery of the damages at #50 second, which will be processed during emulation.* +## Python API Example -> starrynet> check_routing_table 26 40 +The current Python API looks like this: -*It means listing the routing table of node #26 at #40 second. The output file will be written at the working directory.* +```python +from starrynet.sn_synchronizer import StarryNet -> starrynet> set_next_hop 1 26 2 45 +gs_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817]] +sn = StarryNet("./config.json", gs_lat_long) -*It means setting the next hop to node #2 for node #1 for the destination of node #26 at #45 second. Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors, which will be processed during emulation.* +sn.create_nodes() +sn.create_links() +sn.run_routing_daemon("./bird.conf") -> starrynet> set_ping 1 26 46 +print(sn.get_distance("SH1O1S1", "SH1O1S2", 2)) +print(sn.get_neighbors("SH1O1S1", 2)) +print(sn.get_GSes("SH1O1S1", 2)) +print(sn.get_position("SH1O1S1", 2)) +print(sn.get_IP("SH1O1S1")) -*It means pinging from node #1 to node #26 at #46 second. The output file will be written at the working directory.* +sn.set_ping("SH1O1S1", "SH1O1S2", 4) +sn.set_iperf("SH1O1S1", "SH1O1S2", 5) +sn.set_damage(0.3, 6) +sn.set_recovery(7) +sn.start_emulation() +sn.clean() +``` -> starrynet> set_perf 1 26 46 +## Output -*It means perfing from node #1 to node #26 at #46 second. The perfing output file will be written at the working directory.* +StarryNet writes generated files under a directory derived from the config location and experiment name, for example: -> starrynet> start_emulation +```text +./starlink-Grid-LeastDelay/ +``` -*"create_nodes", "create_links" and "run_routing_deamon" must be runned before this. +You can print the active output path in the CLI with: -> starrynet> stop_emulation +```text +starrynet> path +``` -> starrynet> exit +## Known Gaps -*After running the commands above, you will find a working directory at the Starrynet/starrynet directory, containing the output files.* \ No newline at end of file +- The project still contains some legacy files and names from older interfaces. +- The CLI is node-name based; older numeric examples are obsolete. +- Worker connectivity and host namespace permissions are not auto-validated yet. diff --git a/bin/sn b/bin/sn old mode 100644 new mode 100755 index 5a5de5e..d798ce0 --- a/bin/sn +++ b/bin/sn @@ -1,137 +1,93 @@ -#!/usr/bin/env python -""" -Starrynet runner -author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) +#!/usr/bin/env python3 +"""StarryNet CLI runner.""" -To see options: - sudo sn -h - -""" - -from optparse import OptionParser +import argparse import os import sys -import time +import traceback + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.dirname(SCRIPT_DIR) +if REPO_ROOT not in sys.path: + sys.path.insert(0, REPO_ROOT) if 'PYTHONPATH' in os.environ: sys.path = os.environ['PYTHONPATH'].split(':') + sys.path -from starrynet.sn_observer import * -from starrynet.sn_orchestrater import * -from starrynet.sn_synchronizer import * -from starrynet.clean import cleanup -import starrynet.cli -from starrynet.log import lg, LEVELS, info, debug, warn, error, output - - -class StarrynetRunner(object): - "Build, setup, and run Starrynet." - - def __init__(self): - "Init." - self.options = None - self.args = None - self.validate = None - self.AS = [] - self.GS_lat_long = [] - self.configuration_file_path = "./config.xls" - self.hello_interval = None # hello_interval(s) in OSPF. 1-200 are supported. - self.parseArgs() - self.begin() - - def parseArgs(self): - """Parse command-line args and return options object. - returns: opts parse options dict""" - - desc = ("The %prog utility creates Starrynet network from the\n" - "command line. It can create parametrized topologies,\n" - "invoke the Starrynet CLI, and run tests.") - - usage = ('%prog [options]\n' - '(type %prog -h for details)') - - opts = OptionParser(description=desc, usage=usage) - opts.add_option( - '--path', - '-p', - type='string', - default="./config.xls", - help=('path of the configuration file, e.g. ./config.xls')) - opts.add_option( - '--hello_interval', - '-i', - type='int', - default=10, - help=('interval to send hello packet, larger than 1, defaul 10')) - opts.add_option( - '--node_number', - '-n', - type='int', - default=27, - help=('number of network nodes, including satellites and GSes')) - opts.add_option( - '--GS', - '-g', - type='string', - default="50.110924/8.682127/46.635700/14.311817", - help= - ('latitude and longitude of GSes sequentially, split by "/" like "la1/lo1/la2/lo2", e.g. 50.110924/8.682127/46.635700/14.311817' - )) - opts.add_option('--clean', - '-c', - action='store_true', - default=False, - help='clean and exit') - self.options, self.args = opts.parse_args() +from starrynet.cli import CLI +from starrynet.log import debug, error, info +from starrynet.sn_synchronizer import StarryNet - # Extra arguments might be added in the future. - if self.args: - opts.print_help() - exit() - def begin(self): - "Create and run starrynet." +def parse_gs(value): + parts = value.split("/") + if len(parts) % 2 != 0: + raise argparse.ArgumentTypeError( + "GS coordinates must be la1/lo1/la2/lo2") - CLI = starrynet.cli.CLI - - opts = self.options - self.AS.append([1, opts.node_number]) - self.configuration_file_path = opts.path - self.hello_interval = opts.hello_interval - lat_long = opts.GS.split("/") - for i in range(int(len(lat_long) / 2)): - self.GS_lat_long.append( - [float(lat_long[2 * i]), - float(lat_long[2 * i + 1])]) - - if opts.clean: - cleanup() - exit() - start = time.time() - - sn = StarryNet(self.configuration_file_path, self.GS_lat_long, - self.hello_interval, self.AS) - CLI(sn) - cleanup() - - emulation_time = float(time.time() - start) - info('completed in %0.3f seconds\n' % emulation_time) + coordinates = [] + try: + for idx in range(0, len(parts), 2): + coordinates.append([float(parts[idx]), float(parts[idx + 1])]) + except ValueError as exc: + raise argparse.ArgumentTypeError(str(exc)) from exc + return coordinates + + +def build_parser(): + parser = argparse.ArgumentParser( + description="Start the interactive StarryNet CLI.") + parser.add_argument( + "-p", "--path", + default="./config.json", + help="path to the JSON configuration file (default: ./config.json)") + parser.add_argument( + "-g", "--gs", + default="50.110924/8.682127/46.635700/14.311817", + type=parse_gs, + help="ground-station coordinates as la1/lo1/la2/lo2") + parser.add_argument( + "-b", "--bird-conf", + default="./bird.conf", + help="default BIRD configuration used by run_routing_daemon") + parser.add_argument( + "--clean", + action="store_true", + help="connect to configured workers, clean the environment, then exit") + return parser + + +def main(): + parser = build_parser() + args = parser.parse_args() + if not os.path.exists(args.path): + parser.error(f"configuration file not found: {args.path}") + + sn = None + cleaned = False + try: + sn = StarryNet(args.path, args.gs) + if args.clean: + sn.clean() + cleaned = True + return 0 + + CLI(sn, default_bird_conf=args.bird_conf) + return 0 + except KeyboardInterrupt: + info("\nKeyboard Interrupt. Shutting down...\n") + return 130 + except Exception as exc: + error(f"Caught exception: {type(exc).__name__}: {exc}\n") + debug(traceback.format_exc() + "\n") + return 1 + finally: + if sn is not None and not cleaned: + try: + sn.clean() + except Exception as exc: + error(f"Cleanup failed: {exc}\n") if __name__ == "__main__": - try: - StarrynetRunner() - except KeyboardInterrupt: - info("\n\nKeyboard Interrupt. Shutting down and cleaning up...\n\n") - cleanup() - except Exception: - # Print exception - type_, val_, trace_ = sys.exc_info() - errorMsg = ("-" * 80 + "\n" + "Caught exception. Cleaning up...\n\n" + - "%s: %s\n" % (type_.__name__, val_) + "-" * 80 + "\n") - error(errorMsg) - # Print stack trace to debug log - import traceback - stackTrace = traceback.format_exc() - debug(stackTrace + "\n") - cleanup() + raise SystemExit(main()) diff --git a/config.json b/config.json index 631fc60..c23acb6 100644 --- a/config.json +++ b/config.json @@ -4,40 +4,19 @@ { "Altitude (km)": 550, "Inclination": 53, - "Orbits": 72, - "Satellites per orbit": 22, + "Orbits": 10, + "Satellites per orbit": 10, "Phase shift": 1 }, { "Altitude (km)": 540, "Inclination": 53.2, - "Orbits": 72, - "Satellites per orbit": 22, - "Phase shift": 1 - }, - { - "Altitude (km)": 550, - "Inclination": 70, - "Orbits": 36, - "Satellites per orbit": 20, - "Phase shift": 1 - }, - { - "Altitude (km)": 550, - "Inclination": 97.6, - "Orbits": 6, - "Satellites per orbit": 58, - "Phase shift": 1 - }, - { - "Altitude (km)": 550, - "Inclination": 97.6, - "Orbits": 4, - "Satellites per orbit": 43, - "Phase shift": 1 + "Orbits": 5, + "Satellites per orbit": 5, + "Phase shift": 0 } ], - "Duration (s)": 100, + "Duration (s)": 10, "step (s)": 1, "satellite link bandwidth (\"X\" Gbps)": 5, "sat-ground bandwidth (\"X\" Gbps)": 5, @@ -50,8 +29,11 @@ "Link policy": "LeastDelay", "Handover policy": "instant handover", "Machines": [ - {"IP":"101.6.21.1", "port":22, "username":"root", "password":"123456"}, - {"IP":"101.6.21.2", "port":22, "username":"root", "password":"123456"}, - {"IP":"101.6.21.3", "port":22, "username":"root", "password":"123456"} + { + "IP": "127.0.0.1", + "port": 18888, + "username": "abc", + "password": "123456" + } ] -} \ No newline at end of file +} diff --git a/install.sh b/install.sh index 6010304..bbb83c1 100644 --- a/install.sh +++ b/install.sh @@ -37,8 +37,14 @@ if [ "$DIST" = "Fedora" -o "$DIST" = "RedHatEnterpriseServer" ] || [ "$DIST" = fi echo "Installing dependencies" -$install python3 python-setuptools python3-pip -$install docker-ce docker-ce-cli containerd.io +if [ "$DIST" = "Ubuntu" ] || [ "$DIST" = "Debian" ]; then + $install python3 python3-dev python3-setuptools gcc +elif [ "$DIST" = "Fedora" -o "$DIST" = "RedHatEnterpriseServer" ] || [ "$DIST" = "CentOS" ]; then + $install python3 python3-devel python3-setuptools gcc +else + echo "Unsupported distribution for automatic package installation." + echo "Install Python 3, pip, Python development headers, and gcc manually." +fi sudo python3 -m pip install --upgrade pip sudo python3 -m pip install -r tools/requirements.txt -sudo python3 setup.py install \ No newline at end of file +sudo python3 setup.py install diff --git a/setup.py b/setup.py index 7780b16..5d315c8 100644 --- a/setup.py +++ b/setup.py @@ -1,40 +1,53 @@ -#!/usr/bin/env python -"Setuptools params" +#!/usr/bin/env python3 +"""Build and installation metadata for StarryNet.""" -from setuptools import setup, find_packages, Extension -from os.path import join +from pathlib import Path -# Get version number from source tree -import sys +from setuptools import Extension, find_packages, setup -sys.path.append('.') -scripts = [join('bin', filename) for filename in ['sn']] -ext_modules = [Extension('pyctr', [join('starrynet', 'pyctr.c')])] +ROOT = Path(__file__).resolve().parent +README = ROOT / "README.md" +REQUIREMENTS = ROOT / "tools" / "requirements.txt" + + +def read_requirements(): + requirements = [] + for line in REQUIREMENTS.read_text(encoding="utf-8").splitlines(): + requirement = line.strip() + if not requirement or requirement.startswith("#"): + continue + requirements.append(requirement) + return requirements -modname = distname = 'starrynet' setup( - name=distname, + name="starrynet", version="1.0.0", - description= - 'StarryNet for the emulation of satellite Internet constellations.', - author=' Yangtao Deng', - author_email='dengyt21@mails.tsinghua.edu.cn', - packages=['starrynet'], - long_description=""" - StarryNet is a network emulator for satellite Internet constellations. - """, + description="StarryNet satellite-network emulator", + long_description=README.read_text(encoding="utf-8"), + long_description_content_type="text/markdown", + author="Yangtao Deng", + author_email="dengyt21@mails.tsinghua.edu.cn", + maintainer="Xin Xie", + maintainer_email="xiex24@mails.tsinghua.edu.cn", + url="https://github.com/SpaceNetLab/StarryNet", + license="BSD", + packages=find_packages(include=["starrynet", "starrynet.*"]), + python_requires=">=3.7", + install_requires=read_requirements(), + scripts=["bin/sn"], + ext_modules=[ + Extension("pyctr", [str(ROOT / "starrynet" / "pyctr.c")]), + Extension("pynetlink", [str(ROOT / "starrynet" / "pynetlink.c")]), + ], classifiers=[ - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Development Status :: 1 - Production/Stable", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Topic :: System :: Emulators", ], - keywords='satellite Internet constellations emulator protocol', - license='BSD', - install_requires=['setuptools'], - scripts=scripts, - ext_modules=ext_modules, + keywords="satellite network emulator constellation protocol", ) diff --git a/starrynet/cli.py b/starrynet/cli.py index 138817a..5f66f36 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -1,252 +1,302 @@ -""" -A simple command-line interface for Starrynet. +"""Interactive CLI for StarryNet.""" -The Starrynet CLI provides a simple control console which -makes it easy to control the network and have access to nodes. For example, the command - -starrynet> create_nodes - -simply starts the network nodes. - -starrynet> get_neighbors 1, 5 - -gets the neighbors of node#1 at 5 second. - -starrynet> set_ping 26 27 30 - -should work correctly and allow node#26 to ping node#26 at 30 second. - -starrynet> set_perf 26 27 30 - -should work correctly and allow node#26 to perf node#26 at 30 second. - -author: Yangtao Deng (dengyt21@mails.tsinghua.edu.cn) -""" - -from subprocess import call from cmd import Cmd -from os import isatty -from select import poll, POLLIN +import shlex import sys -from starrynet.log import info, output, error -# from starrynet.term import makeTerms, runX11 -# from starrynet.util import ( quietRun, dumpNodeConnections, -# dumpPorts ) +from starrynet.log import error, info, output class CLI(Cmd): - "Simple command-line interface to talk to nodes." + """Simple command-line interface for StarryNet.""" - prompt = 'starrynet> ' + prompt = "starrynet> " - def __init__(self, starrynet, stdin=sys.stdin, *args, **kwargs): - """Start and run interactive or batch mode CLI - starrynet: Starrynet network object - stdin: standard input for CLI - script: script to run in batch mode""" + def __init__(self, starrynet, default_bird_conf="./bird.conf", + stdin=sys.stdin, *args, **kwargs): self.sn = starrynet - # Local variable bindings for py command - self.locals = {'net': starrynet} - # Attempt to handle input - self.inPoller = poll() - self.inPoller.register(stdin) - Cmd.__init__(self, *args, stdin=stdin, **kwargs) - info('*** Starting CLI:\n') - + self.default_bird_conf = default_bird_conf + super().__init__(*args, stdin=stdin, **kwargs) + info("*** Starting CLI. Type 'help' for commands.\n") self.run() def run(self): - "Run our cmdloop(), catching KeyboardInterrupt" while True: try: self.cmdloop() break except KeyboardInterrupt: - # Output a message - unless it's also interrupted - # pylint: disable=broad-except - try: - output('\nInterrupt\n') - except Exception: - pass - # pylint: enable=broad-except + output("\nInterrupt\n") def emptyline(self): - "Don't repeat last command when you hit return." pass - def getLocals(self): - "Local variable bindings for py command" - self.locals.update(self.mn) - return self.locals - - helpStr = ( - 'Supported commands are as follows:\n' - ' starrynet> help\n' - ' starrynet> create_nodes\n' - ' starrynet> create_links\n' - ' starrynet> run_routing_deamon\n' - ' starrynet> get_distance 1 2 10\n' - ' // It means getting the distance of two node (#1 and #2) at #10 second.\n' - ' starrynet> get_neighbors 5 16\n' - ' // It means getting the neighbor node indexes of node #5 at #16 second.\n' - ' starrynet> get_GSes 7 20\n' - ' // It means getting the connected GS node indexes of node #6 at #20 second.\n' - ' starrynet> get_position 7 23 \n' - ' // It means getting the LLA position of node #7 at #23 second.\n' - ' starrynet> get_IP 8 \n' - ' // It means getting the IP addresses of node #8. "create_nodes" and "create_links" must be runned before this.\n' - ' starrynet> get_utility 27\n' - ' // It means getting the memory and CPU utility information at #27 second. The output file will be generated at the working directory once the emulation starts.\n' - ' starrynet> set_damage 0.3 30\n' - ' // It means setting a random damage of a given ratio of 0.3 at #30 second, which will be processed during emulation.\n' - ' starrynet> set_recovery 50\n' - ' // It means setting a recovery of the damages at #50 second, which will be processed during emulation.\n' - ' starrynet> check_routing_table 26 40\n' - ' // It means listing the routing table of node #26 at #40 second. The output file will be written at the working directory.\n' - ' starrynet> set_next_hop 1 26 2 45\n' - ' // It means setting the next hop to node #2 for node #1 for the destination of node #26 at #45 second. Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors, which will be processed during emulation.\n' - ' starrynet> set_ping 1 26 46\n' - ' // It means pinging msg of from node #1 to node #26 at #46 second. The output file will be written at the working directory.\n' - ' starrynet> set_perf 1 26 46\n' - ' // It means perfing from node #1 to node #26 at #46 second. The perfing output file will be written at the working directory.\n' - ' starrynet> start_emulation\n' - ' // "create_nodes", "create_links" and "run_routing_deamon" must be runned before this.' - ' starrynet> stop_emulation\n' - ' starrynet> exit\n' - ' starrynet> quit\n' - ' starrynet> EOF\n' - ' You may use the commands multiple times.\n') + def _parse_args(self, line): + return shlex.split(line) + + def _require_args(self, line, expected, usage): + args = self._parse_args(line) + if len(args) != expected: + error(f"usage: {usage}\n") + return None + return args + + def _require_at_least_args(self, line, minimum, usage): + args = self._parse_args(line) + if len(args) < minimum: + error(f"usage: {usage}\n") + return None + return args + + def _parse_int(self, value, name): + try: + return int(value) + except ValueError: + error(f"{name} must be an integer: {value}\n") + return None + + def _parse_float(self, value, name): + try: + return float(value) + except ValueError: + error(f"{name} must be a number: {value}\n") + return None + + def _check_node(self, node): + if node not in self.sn.nodes: + error(f"unknown node: {node}\n") + return False + return True def do_help(self, line): - "Describe available CLI commands." - Cmd.do_help(self, line) - if line == '': - output(self.helpStr) + if line: + return super().do_help(line) + output( + "Common workflow:\n" + " create_nodes\n" + " create_links\n" + " run_routing_daemon [bird.conf] [node1 node2 ...]\n" + " start_emulation\n\n" + "Useful commands:\n" + " status\n" + " nodes [prefix]\n" + " path\n" + " get_distance NODE1 NODE2 TIME\n" + " get_neighbors NODE TIME\n" + " get_GSes NODE TIME\n" + " get_position NODE TIME\n" + " get_IP NODE\n" + " get_utility TIME\n" + " set_damage RATIO TIME\n" + " set_recovery TIME\n" + " check_routing_table NODE TIME\n" + " set_static_route SRC DST NEXT_HOP TIME\n" + " set_ping SRC DST TIME\n" + " set_iperf SRC DST TIME\n" + " clean\n" + " exit\n\n" + "Notes:\n" + " NODE values use names such as SH1O1S1 or GS0.\n" + " Scheduled commands run when start_emulation is executed.\n" + ) + + def do_status(self, _line): + output( + f"experiment: {self.sn.experiment_name}\n" + f"config dir: {self.sn.configuration_dir}\n" + f"output dir: {self.sn.local_dir}\n" + f"duration: {self.sn.duration}s\n" + f"step: {self.sn.step}s\n" + f"nodes: {len(self.sn.nodes)}\n" + f"workers: {len(self.sn.worker_lst)}\n" + f"queued events: {len(self.sn.events)}\n" + ) + + def do_nodes(self, line): + args = self._parse_args(line) + prefix = args[0] if args else "" + nodes = sorted(name for name in self.sn.nodes if name.startswith(prefix)) + if not nodes: + output("No nodes matched.\n") + return + output("\n".join(nodes) + "\n") def do_create_nodes(self, _line): - "initialize the entire network nodes" self.sn.create_nodes() def do_create_links(self, _line): - "initialize the entire network links" self.sn.create_links() - def do_run_routing_deamon(self, _line): - "run routing deamon for each node" - self.sn.run_routing_deamon() + def do_run_routing_daemon(self, line): + args = self._parse_args(line) + bird_conf_path = args[0] if args else self.default_bird_conf + node_lst = args[1:] if len(args) > 1 else "all" + self.sn.run_routing_daemon(bird_conf_path=bird_conf_path, + node_lst=node_lst) + + def do_run_routing_deamon(self, line): + self.do_run_routing_daemon(line) def do_get_distance(self, line): - "calculate the distance of two node at a certain time" - arg, args, line = self.parseline(line) - rest = line.split(' ') - node_distance = self.sn.get_distance(int(rest[0]), int(rest[1]), - int(rest[2])) - output("The distance between node#%d and node#%d is %.2fkm.\n" % - (int(rest[0]), int(rest[1]), node_distance)) + args = self._require_args( + line, 3, "get_distance NODE1 NODE2 TIME") + if args is None: + return + node1, node2 = args[0], args[1] + if not self._check_node(node1) or not self._check_node(node2): + return + t = self._parse_int(args[2], "TIME") + if t is None: + return + distance = self.sn.get_distance(node1=node1, node2=node2, t=t) + output(f"{node1} <-> {node2}: {distance:.2f} km\n") def do_get_neighbors(self, line): - "list the neighbor node indexes of node at a certain time" - arg, args, line = self.parseline(line) - rest = line.split(' ') - neighbors_index = self.sn.get_neighbors(int(rest[0]), int(rest[1])) - output("The neighbors are: " + str(neighbors_index) + ".\n") + args = self._require_args(line, 2, "get_neighbors NODE TIME") + if args is None or not self._check_node(args[0]): + return + t = self._parse_int(args[1], "TIME") + if t is None: + return + neighbors = self.sn.get_neighbors(node=args[0], t=t) + output(f"neighbors: {neighbors}\n") def do_get_GSes(self, line): - "list the GS connected to the node at a certain time" - arg, args, line = self.parseline(line) - rest = line.split(' ') - GSes = self.sn.get_GSes(int(rest[0]), int(rest[1])) - output("The connected GS(es) is(are): " + str(GSes) + ".\n") + args = self._require_args(line, 2, "get_GSes NODE TIME") + if args is None or not self._check_node(args[0]): + return + t = self._parse_int(args[1], "TIME") + if t is None: + return + gses = self.sn.get_GSes(node=args[0], t=t) + output(f"ground stations: {gses}\n") def do_get_position(self, line): - "list the LLA of a node at a certain time" - arg, args, line = self.parseline(line) - rest = line.split(' ') - LLA = self.sn.get_position(int(rest[0]), int(rest[1])) - output("The LLA is: " + str(LLA)) + args = self._require_args(line, 2, "get_position NODE TIME") + if args is None or not self._check_node(args[0]): + return + t = self._parse_int(args[1], "TIME") + if t is None: + return + position = self.sn.get_position(node=args[0], t=t) + output(f"position: {position}\n") def do_get_IP(self, line): - "list the IP of a node" - arg, args, line = self.parseline(line) - IP = self.sn.get_IP(int(arg)) - output("The IP list of the node is(are): " + str(IP) + ".\n") + args = self._require_args(line, 1, "get_IP NODE") + if args is None or not self._check_node(args[0]): + return + ip_list = self.sn.get_IP(node=args[0]) + output(f"IPs: {ip_list}\n") def do_get_utility(self, line): - "list the CPU and memory useage at a certain time" - "The output file will be generated once the emulation starts" - arg, args, line = self.parseline(line) - self.sn.get_utility(int(arg)) + args = self._require_args(line, 1, "get_utility TIME") + if args is None: + return + t = self._parse_int(args[0], "TIME") + if t is None: + return + self.sn.get_utility(t=t) + output("utility check scheduled.\n") def do_set_damage(self, line): - "set a random damage of a given ratio at a certain time, which will be processed during emulation" - arg, args, line = self.parseline(line) - rest = line.split(' ') - self.sn.set_damage(float(rest[0]), int(rest[1])) + args = self._require_args(line, 2, "set_damage RATIO TIME") + if args is None: + return + ratio = self._parse_float(args[0], "RATIO") + t = self._parse_int(args[1], "TIME") + if ratio is None or t is None: + return + self.sn.set_damage(damaging_ratio=ratio, t=t) + output("damage event scheduled.\n") def do_set_recovery(self, line): - "set a recovery of the damages at a certain time, which will be processed during emulation" - arg, args, line = self.parseline(line) - rest = line.split(' ') - self.sn.set_recovery(int(rest[0])) + args = self._require_args(line, 1, "set_recovery TIME") + if args is None: + return + t = self._parse_int(args[0], "TIME") + if t is None: + return + self.sn.set_recovery(t=t) + output("recovery event scheduled.\n") def do_check_routing_table(self, line): - "list the routing table of a node at a certain time." - "The output file will be written at the working directory." - arg, args, line = self.parseline(line) - rest = line.split(' ') - self.sn.check_routing_table(int(rest[0]), int(rest[1])) + args = self._require_args(line, 2, "check_routing_table NODE TIME") + if args is None or not self._check_node(args[0]): + return + t = self._parse_int(args[1], "TIME") + if t is None: + return + self.sn.check_routing_table(node=args[0], t=t) + output("routing table dump scheduled.\n") + + def do_set_static_route(self, line): + args = self._require_args( + line, 4, "set_static_route SRC DST NEXT_HOP TIME") + if args is None: + return + src, dst, next_hop = args[:3] + if not all(self._check_node(node) for node in [src, dst, next_hop]): + return + t = self._parse_int(args[3], "TIME") + if t is None: + return + self.sn.set_static_route(src=src, dst=dst, next_hop=next_hop, t=t) + output("static route scheduled.\n") def do_set_next_hop(self, line): - "set the nhelpext hop at a certain time" - "Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors, which will be processed during emulation" - arg, args, line = self.parseline(line) - rest = line.split(' ') - self.sn.set_next_hop(int(rest[0]), int(rest[1]), int(rest[2]), - int(rest[3])) - - def do_path(self, line): - "get the working directory" - output(self.sn.configuration_file_path + "\n") + self.do_set_static_route(line) def do_set_ping(self, line): - "ping msg of two nodes at a certain time" - "The output file will be written at the working directory" - arg, args, line = self.parseline(line) - rest = line.split(' ') - self.sn.set_ping(int(rest[0]), int(rest[1]), int(rest[2])) + args = self._require_args(line, 3, "set_ping SRC DST TIME") + if args is None: + return + src, dst = args[:2] + if not self._check_node(src) or not self._check_node(dst): + return + t = self._parse_int(args[2], "TIME") + if t is None: + return + self.sn.set_ping(src=src, dst=dst, t=t) + output("ping scheduled.\n") + + def do_set_iperf(self, line): + args = self._require_args(line, 3, "set_iperf SRC DST TIME") + if args is None: + return + src, dst = args[:2] + if not self._check_node(src) or not self._check_node(dst): + return + t = self._parse_int(args[2], "TIME") + if t is None: + return + self.sn.set_iperf(src=src, dst=dst, t=t) + output("iperf scheduled.\n") def do_set_perf(self, line): - "perf msg of two nodes at a certain time" - "The output file will be written at the working directory" - arg, args, line = self.parseline(line) - rest = line.split(' ') - self.sn.set_perf(int(rest[0]), int(rest[1]), int(rest[2])) + self.do_set_iperf(line) def do_start_emulation(self, _line): - "start the emulation" self.sn.start_emulation() + def do_clean(self, _line): + self.sn.clean() + + def do_path(self, _line): + output(self.sn.local_dir + "\n") + def do_stop_emulation(self, _line): - "stop the emulation" - self.sn.stop_emulation() - return 'exited by user command' + self.sn.clean() + return "exited by user command" def do_exit(self, _line): - "stop the emulation" - self.sn.stop_emulation() - return 'exited by user command' + return "exited by user command" def do_quit(self, line): - "Exit" return self.do_exit(line) def do_EOF(self, line): - "Exit" - output('\n') + output("\n") return self.do_exit(line) def default(self, line): - "Exit" - error('*** Unknown command: %s\n' % line) - return + error(f"*** Unknown command: {line}\n") diff --git a/tools/requirements.txt b/tools/requirements.txt index 49fb3ac..8746d50 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,8 +1,5 @@ setuptools -xlrd -argparse numpy -requests skyfield sgp4 paramiko \ No newline at end of file From 9b836e6e22e4866feb80dea26b353a52bed38c06 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 29 Apr 2026 18:37:29 +0800 Subject: [PATCH 30/49] refactor: worker, cli and C modules for installation package. - add bin/sn-worker for launching the worker daemon, and drop main() from sn_orchestrator_daemon - remove sys.path hacking for running without installation in bin/sn - remove runtime compilation for pyctr and pynetlink in sn_orchestrator.py - relative import in starrynet - restrict platform to Linux in setup.py - restore python3-pip in install.sh, and clarify build-time vs runtime dependencies in README - ignore build/, dist/, *.egg-info/ and starlink-*/ in .gitignore --- .gitignore | 7 ++- README.md | 13 +++-- bin/sn | 9 ---- bin/sn-worker | 52 +++++++++++++++++++ install.sh | 4 +- setup.py | 9 +++- starrynet/cli.py | 2 +- ...{sn_orchestrater.py => sn_orchestrator.py} | 23 ++------ starrynet/sn_orchestrator_daemon.py | 45 +--------------- 9 files changed, 84 insertions(+), 80 deletions(-) create mode 100755 bin/sn-worker rename starrynet/{sn_orchestrater.py => sn_orchestrator.py} (96%) diff --git a/.gitignore b/.gitignore index dcbbc5b..7ed0905 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,9 @@ dkms.conf .DS_Store .idea -__pycache__ \ No newline at end of file +__pycache__ +build/ +dist/ +*.egg-info/ + +starlink-*/ diff --git a/README.md b/README.md index 46bc04d..cd5bbe9 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,18 @@ The exact system packages depend on your Linux distribution, but in practice you - Python 3 - `pip` -- a C compiler such as `gcc` -- Python development headers for compiling CPython extensions - network namespace support on the host - reachable worker machines matching the `Machines` section in `config.json` (`127.0.0.1` recommended for initial trials) Python packages are listed in `tools/requirements.txt`. +If you install StarryNet from this source tree with `python3 setup.py install`, you also need: + +- a C compiler such as `gcc` +- Python development headers for compiling the `pyctr` and `pynetlink` extensions + +Those build dependencies are needed at install time, not for normal use after a successful install. + ## Installation Install Python dependencies: @@ -62,7 +67,7 @@ Before running `example.py` or `sn`, start at least one worker daemon that match Example: ```bash -python3 starrynet/sn_orchestrator_daemon.py \ +sn-worker \ --workdir test \ --machine-id 0 \ --ssh-username abc \ @@ -79,7 +84,7 @@ Important details: You can inspect all daemon options with: ```bash -python3 starrynet/sn_orchestrator_daemon.py --help +sn-worker --help ``` ### 2. Update `config.json` diff --git a/bin/sn b/bin/sn index d798ce0..5f0155d 100755 --- a/bin/sn +++ b/bin/sn @@ -3,17 +3,8 @@ import argparse import os -import sys import traceback -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -REPO_ROOT = os.path.dirname(SCRIPT_DIR) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -if 'PYTHONPATH' in os.environ: - sys.path = os.environ['PYTHONPATH'].split(':') + sys.path - from starrynet.cli import CLI from starrynet.log import debug, error, info from starrynet.sn_synchronizer import StarryNet diff --git a/bin/sn-worker b/bin/sn-worker new file mode 100755 index 0000000..93e8e05 --- /dev/null +++ b/bin/sn-worker @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""StarryNet worker starter.""" + +import argparse +import logging + +from starrynet.sn_orchestrator_daemon import DEFAULT_SSH_PORT, OrchestraterDaemon + + +def build_parser(): + parser = argparse.ArgumentParser( + description="StarryNet Orchestrater Daemon with SSH Server") + parser.add_argument("--workdir", type=str, help="Working directory") + parser.add_argument("--machine-id", type=int, help="Machine ID", default=0) + parser.add_argument("--daemon", action="store_true", help="Run as daemon") + parser.add_argument("--log-level", type=str, default="INFO", + choices=["DEBUG", "INFO", "WARNING", "ERROR"], + help="Log level") + parser.add_argument("--ssh-port", type=int, default=DEFAULT_SSH_PORT, + help="SSH server port") + parser.add_argument("--ssh-username", type=str, + help="SSH username for authentication") + parser.add_argument("--ssh-password", type=str, + help="SSH password for authentication") + return parser + + +def main(): + parser = build_parser() + args = parser.parse_args() + + daemon = OrchestraterDaemon( + workdir=args.workdir, + machine_id=args.machine_id, + log_level=getattr(logging, args.log_level.upper()), + ssh_port=args.ssh_port, + ssh_username=args.ssh_username, + ssh_password=args.ssh_password, + ) + + if args.daemon: + import daemon + with daemon.DaemonContext(): + daemon.run() + return 0 + + daemon.run() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/install.sh b/install.sh index bbb83c1..186f8f5 100644 --- a/install.sh +++ b/install.sh @@ -38,9 +38,9 @@ fi echo "Installing dependencies" if [ "$DIST" = "Ubuntu" ] || [ "$DIST" = "Debian" ]; then - $install python3 python3-dev python3-setuptools gcc + $install python3 python3-dev python3-pip python3-setuptools gcc elif [ "$DIST" = "Fedora" -o "$DIST" = "RedHatEnterpriseServer" ] || [ "$DIST" = "CentOS" ]; then - $install python3 python3-devel python3-setuptools gcc + $install python3 python3-devel python3-pip python3-setuptools gcc else echo "Unsupported distribution for automatic package installation." echo "Install Python 3, pip, Python development headers, and gcc manually." diff --git a/setup.py b/setup.py index 5d315c8..199352d 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Build and installation metadata for StarryNet.""" +import sys from pathlib import Path from setuptools import Extension, find_packages, setup @@ -20,6 +21,11 @@ def read_requirements(): requirements.append(requirement) return requirements +if sys.platform != "linux": + raise RuntimeError( + f"This package only supports Linux. " + f"Detected platform: {sys.platform}" + ) setup( name="starrynet", @@ -36,11 +42,12 @@ def read_requirements(): packages=find_packages(include=["starrynet", "starrynet.*"]), python_requires=">=3.7", install_requires=read_requirements(), - scripts=["bin/sn"], + scripts=["bin/sn", "bin/sn-worker"], ext_modules=[ Extension("pyctr", [str(ROOT / "starrynet" / "pyctr.c")]), Extension("pynetlink", [str(ROOT / "starrynet" / "pynetlink.c")]), ], + platforms=["Linux"], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", diff --git a/starrynet/cli.py b/starrynet/cli.py index 5f66f36..4d5e316 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -4,7 +4,7 @@ import shlex import sys -from starrynet.log import error, info, output +from .log import error, info, output class CLI(Cmd): diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrator.py similarity index 96% rename from starrynet/sn_orchestrater.py rename to starrynet/sn_orchestrator.py index 1774f5d..c14337e 100644 --- a/starrynet/sn_orchestrater.py +++ b/starrynet/sn_orchestrator.py @@ -12,27 +12,14 @@ from collections import defaultdict # from line_profiler import LineProfiler -module_dir = os.path.dirname(__file__) try: import pyctr -except ModuleNotFoundError: - subprocess.check_call( - f"cd {module_dir} && " - "gcc $(python3-config --cflags --ldflags) " - "-shared -fPIC -O2 pyctr.c -o pyctr.so", - shell=True - ) - import pyctr -try: - import pynetlink -except ModuleNotFoundError: - subprocess.check_call( - f"cd {module_dir} && " - "gcc $(python3-config --cflags --ldflags) " - "-shared -fPIC -O2 pynetlink.c -o pynetlink.so", - shell=True - ) import pynetlink +except ModuleNotFoundError as exc: + raise RuntimeError( + "StarryNet C extensions are not installed. " + "Reinstall the package or rebuild the extensions before running." + ) from exc # FIXME NETNS_DIR = '/run/netns' diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index e78ec00..c18c28c 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -7,15 +7,13 @@ import threading import resource import logging -import argparse import threading import selectors from enum import Enum -from concurrent.futures import ThreadPoolExecutor import struct import paramiko -from sn_orchestrater import OrchestratorContext +from .sn_orchestrator import OrchestratorContext MSG_MAX_SIZE = 10 * 1024 * 1024 # 10MB @@ -570,44 +568,3 @@ def _handle_update_network_batch(self, params): except Exception as e: raise Exception(f'Network update failed: {str(e)}') - - -def main(): - parser = argparse.ArgumentParser(description='StarryNet Orchestrater Daemon with SSH Server') - parser.add_argument('--workdir', type=str, default=None, help='Working directory') - parser.add_argument('--machine-id', type=int, help='Machine ID') - parser.add_argument('--daemon', action='store_true', help='Run as daemon') - parser.add_argument('--log-level', type=str, default='INFO', - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], - help='Log level') - parser.add_argument('--ssh-port', type=int, default=DEFAULT_SSH_PORT, help='SSH server port') - parser.add_argument('--ssh-username', type=str, help='SSH username for authentication') - parser.add_argument('--ssh-password', type=str, help='SSH password for authentication') - - args = parser.parse_args() - - # Convert log level string to logging constant - log_level = getattr(logging, args.log_level.upper()) - - # Create daemon instance - daemon = OrchestraterDaemon( - workdir=args.workdir, - machine_id=args.machine_id, - log_level=log_level, - ssh_port=args.ssh_port, - ssh_username=args.ssh_username, - ssh_password=args.ssh_password - ) - - if args.daemon: - # Run as daemon - import daemon - with daemon.DaemonContext(): - daemon.run() - else: - # Run in foreground - daemon.run() - - -if __name__ == '__main__': - main() \ No newline at end of file From 5e1ef58abe4ecb8e33cfb6c8f15a54583e874d5c Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 29 Apr 2026 20:45:18 +0800 Subject: [PATCH 31/49] chore: delete unused sn.* files, and update installation guide in README --- README.md | 31 ++++++++++++++++++++++++------- sn.json | 13 ------------- sn.py | 32 -------------------------------- 3 files changed, 24 insertions(+), 52 deletions(-) delete mode 100644 sn.json delete mode 100644 sn.py diff --git a/README.md b/README.md index cd5bbe9..471aa70 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The exact system packages depend on your Linux distribution, but in practice you - Python 3 - `pip` -- network namespace support on the host +- network namespace support on the host (most Linux distributions satisfy this by default) - reachable worker machines matching the `Machines` section in `config.json` (`127.0.0.1` recommended for initial trials) Python packages are listed in `tools/requirements.txt`. @@ -40,24 +40,41 @@ Those build dependencies are needed at install time, not for normal use after a ## Installation -Install Python dependencies: +Quick install: ```bash -python3 -m pip install -r tools/requirements.txt +bash ./install.sh ``` -Install the package and CLI: +Or, step by step: + +1. Install system dependencies (Ubuntu example): ```bash -python3 setup.py install +sudo apt update +sudo apt install python3 python3-pip python3-dev build-essential ``` -If you prefer the bundled installer, it now installs Python dependencies and the package only: +2. (Optional) Create a Python virtual environment: ```bash -bash ./install.sh +python3 -m venv sn-env +source sn-env/bin/activate +``` + +3. Install Python dependencies: + +```bash +python3 -m pip install -r tools/requirements.txt +``` + +4. Install the package and CLI: + +```bash +python3 setup.py install ``` + ## Quick Start ### 1. Start a worker daemon diff --git a/sn.json b/sn.json deleted file mode 100644 index 43664eb..0000000 --- a/sn.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "constellation": "starlink", - "api_url": "http://127.0.0.1/Nodeinfo-2024-08-21-10-43-17.json", - "step": 1, - "machines": [ - { - "IP": "127.0.0.1", - "port": 22, - "username": "root", - "password": "123456" - } - ] -} \ No newline at end of file diff --git a/sn.py b/sn.py deleted file mode 100644 index 1ccf240..0000000 --- a/sn.py +++ /dev/null @@ -1,32 +0,0 @@ -import threading - -from starrynet.sn_controller import * - -### Warning: Never use this in public network! - -from flask import Flask, request, Response, stream_with_context, jsonify - -controller = TopoSync('./sn.json') - -app = Flask(__name__) - -@app.route('/execute', methods=['POST']) -def execute_command(): - data = request.json - - if 'command' not in data: - return jsonify({'error': 'No command provided'}), 400 - if 'node' not in data: - return jsonify({'error': 'Node not specified'}), 400 - return Response( - stream_with_context(controller.exec(data['node'], data['command'])), - content_type='text/plain' - ) - -api_thread = threading.Thread( - target=app.run, - args=('0.0.0.0', 5000) -) -api_thread.start() - -controller.run() From 497c893377ea49818033e3d64af991385a0cc730 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Thu, 30 Apr 2026 11:27:22 +0800 Subject: [PATCH 32/49] docs: clarify quick/manual install and privilege steps in README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 471aa70..d8d299a 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,13 @@ Those build dependencies are needed at install time, not for normal use after a ## Installation -Quick install: +### Quick install: ```bash bash ./install.sh ``` -Or, step by step: +### Manual install (recommended for explicit control): 1. Install system dependencies (Ubuntu example): @@ -58,6 +58,7 @@ sudo apt install python3 python3-pip python3-dev build-essential 2. (Optional) Create a Python virtual environment: ```bash +sudo apt install python3-venv python3 -m venv sn-env source sn-env/bin/activate ``` @@ -65,13 +66,13 @@ source sn-env/bin/activate 3. Install Python dependencies: ```bash -python3 -m pip install -r tools/requirements.txt +sudo python3 -m pip install -r tools/requirements.txt ``` 4. Install the package and CLI: ```bash -python3 setup.py install +sudo python3 setup.py install ``` From 10d67de7312e8ea9ddeb04c2a6c74701138035a2 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Thu, 30 Apr 2026 18:13:27 +0800 Subject: [PATCH 33/49] feat: task management for asynchronous commands - add `events`, `tasks`, `task`, `task_output` in CLI and their counterparts in StarryNet API - move task_queue management from `OrchestratorContext` to `OrchestraterDaemon` - add `TaskRecord` to track task status in `OrchestraterDaemon` --- README.md | 2 +- starrynet/cli.py | 62 +++++- starrynet/sn_daemon_client.py | 49 ++++- starrynet/sn_orchestrator.py | 89 ++------- starrynet/sn_orchestrator_daemon.py | 291 +++++++++++++++++++++++++++- starrynet/sn_synchronizer.py | 88 ++++++++- 6 files changed, 486 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index d8d299a..6f11625 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Before running `example.py` or `sn`, start at least one worker daemon that match Example: ```bash -sn-worker \ +sudo sn-worker \ --workdir test \ --machine-id 0 \ --ssh-username abc \ diff --git a/starrynet/cli.py b/starrynet/cli.py index 4d5e316..a7af109 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -93,6 +93,10 @@ def do_help(self, line): " set_static_route SRC DST NEXT_HOP TIME\n" " set_ping SRC DST TIME\n" " set_iperf SRC DST TIME\n" + " events\n" + " tasks [NODE]\n" + " task TASK_ID\n" + " task_output TASK_ID [NODE]\n" " clean\n" " exit\n\n" "Notes:\n" @@ -109,9 +113,9 @@ def do_status(self, _line): f"step: {self.sn.step}s\n" f"nodes: {len(self.sn.nodes)}\n" f"workers: {len(self.sn.worker_lst)}\n" - f"queued events: {len(self.sn.events)}\n" + f"events: {len(self.sn.list_events())}\n" ) - + def do_nodes(self, line): args = self._parse_args(line) prefix = args[0] if args else "" @@ -275,6 +279,60 @@ def do_set_iperf(self, line): def do_set_perf(self, line): self.do_set_iperf(line) + def do_events(self, _line): + events = self.sn.list_events() + if not events: + output("No events.\n") + return + lines = [] + for item in events: + lines.append( + f"t={item['time']} type={item['kind']} name={item['name']} args={item['args']}" + ) + output("\n".join(lines) + "\n") + + def do_tasks(self, line): + args = self._parse_args(line) + node = args[0] if args else None + if node is not None and not self._check_node(node): + return + tasks = self.sn.list_tasks(node=node) + if not tasks: + output("No tasks found.\n") + return + lines = [] + for task in tasks: + lines.append( + f"{task.get('task_id')} {task.get('task_type')} {task.get('node')} " + f"{task.get('status')} -> {task.get('output_file')}" + ) + output("\n".join(lines) + "\n") + + def do_task(self, line): + args = self._require_args(line, 1, "task TASK_ID") + if args is None: + return + task_id = args[0] + task = self.sn.get_task(task_id) + if not task: + output("Task not found.\n") + return + output(str(task) + "\n") + + def do_task_output(self, line): + args = self._require_at_least_args(line, 1, "task_output TASK_ID [NODE]") + if args is None: + return + task_id = args[0] + node = args[1] if len(args) > 1 else None + if node is not None and not self._check_node(node): + return + result = self.sn.get_task_output(task_id, node=node) + if not result: + output("Task not found.\n") + return + output(result.get('output', '') + "\n") + def do_start_emulation(self, _line): self.sn.start_emulation() diff --git a/starrynet/sn_daemon_client.py b/starrynet/sn_daemon_client.py index adad833..f31d090 100644 --- a/starrynet/sn_daemon_client.py +++ b/starrynet/sn_daemon_client.py @@ -243,7 +243,10 @@ def check_utility(self): 't': time.time(), 'p': {} } - return self._send_command_via_ssh(command).get('result', '') + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to check utility: {response.get('message')}") + return response.get('result', {}) def check_routing_table(self, node: str): command = { @@ -253,7 +256,10 @@ def check_routing_table(self, node: str): 'node': node } } - return self._send_command_via_ssh(command).get('result', '') + response = self._send_command_via_ssh(command) + if response.get('status') != 'success': + raise Exception(f"Failed to check routing table: {response.get('message')}") + return response.get('result', {}) def damage_nodes(self, nodes: list): command = { @@ -266,7 +272,6 @@ def damage_nodes(self, nodes: list): response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to damage nodes: {response.get('message')}") - return response def recover_nodes(self): command = { @@ -340,6 +345,44 @@ def exec_batch(self, exec_cmds): } return self._send_command_via_ssh(command) + def list_tasks(self, node: str = None, status: str = None, task_type: str = None): + params = {} + if node is not None: + params['node'] = node + if status is not None: + params['status'] = status + if task_type is not None: + params['type'] = task_type + command = { + 'c': 'tasks', + 't': time.time(), + 'p': params + } + response = self._send_command_via_ssh(command) + return response.get('result', []) + + def get_task(self, task_id: str): + command = { + 'c': 'task', + 't': time.time(), + 'p': { + 'task_id': task_id, + } + } + response = self._send_command_via_ssh(command) + return response.get('result', {}) + + def get_task_output(self, task_id: str): + command = { + 'c': 'task_output', + 't': time.time(), + 'p': { + 'task_id': task_id, + } + } + response = self._send_command_via_ssh(command) + return response.get('result', {}) + def clean(self): """Clean up all resources""" command = { diff --git a/starrynet/sn_orchestrator.py b/starrynet/sn_orchestrator.py index c14337e..0f8a99d 100644 --- a/starrynet/sn_orchestrator.py +++ b/starrynet/sn_orchestrator.py @@ -1,15 +1,10 @@ #!/usr/bin/python3 import os import subprocess -import sys import ctypes -import time import ipaddress -import threading -import queue -from typing import Dict, List, Tuple +from typing import Dict, List from enum import Enum -from collections import defaultdict # from line_profiler import LineProfiler try: @@ -155,54 +150,12 @@ def __init__(self, workdir): self.nodes: Dict[str, Node] = {} self.damage_lst: List[Node] = [] - self.cmd_to_start = queue.PriorityQueue() - self.cmd_cnt_dict = defaultdict(int) - self.cmd_pending = set() - threading.Thread(target=self._check_commands, daemon=True).start() - def __del__(self): try: os.close(self._main_net_sock_fd) except: pass - def _check_commands(self): - cur = None - while True: - time.sleep(0.1) - now = time.perf_counter() - - if cur is None: - try: - cur = self.cmd_to_start.get(block=False) - except queue.Empty: - pass - - while cur is not None and cur[0] <= now: - _, node, cmdline = cur - self.cmd_cnt_dict[node.name] += 1 - cmd_id = self.cmd_cnt_dict[node.name] - fd = os.open( - f'{self.workdir}/cmd_{node.name}_{cmd_id}.out', - os.O_WRONLY | os.O_CREAT | os.O_TRUNC - ) - os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) - proc = node.run_command(cmdline, stdout=fd, stderr=subprocess.STDOUT) - os.close(fd) - self.cmd_pending.add(proc) - try: - cur = self.cmd_to_start.get(False) - except queue.Empty: - cur = None - - finished = [] - for proc in self.cmd_pending: - if proc.poll() is None: - continue - finished.append(proc) - for proc in finished: - self.cmd_pending.remove(proc) - def clean(self): for node in self.nodes.values(): del node @@ -305,41 +258,32 @@ def set_static_route(self, src: str, dst: str, next_hop: str): (True, dst_network.network_address.packed, dst_network.prefixlen, next_hop, via_addr) ]) - def ping(self, src: str, dst: str): + def get_ping_command(self, src: str, dst: str, extra_args: List[str] = []): src_node = self.nodes.get(src) dst_node = self.nodes.get(dst) if src_node is None or dst_node is None: - return + return None dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed + return src_node, ('ping', '-c', '4', '-i', '0.01', *extra_args, dst_addr) - self.cmd_to_start.put((time.perf_counter(), src_node, ('ping', '-c', '4', '-i', '0.01', dst_addr))) - - def iperf(self, cmds): - time_point = time.perf_counter() - for cmd in cmds: - src_node = self.nodes.get(cmd[0]) - dst_node = self.nodes.get(cmd[1]) - src_args, dst_args = cmd[2], cmd[3] - if src_node is None or dst_node is None: - continue + def get_iperf_commands(self, src: str, dst: str, src_args: List[str] = [], dst_args: List[str] = []): + src_node = self.nodes.get(src) + dst_node = self.nodes.get(dst) + if src_node is None or dst_node is None: + return None - dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed - self.cmd_to_start.put((time_point, dst_node, ('iperf3', '-s', '-1', *dst_args))) - self.cmd_to_start.put((time_point + 0.5, src_node, ('iperf3', '-c', dst_addr, *src_args))) + dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed + return ( + (dst_node, ('iperf3', '-s', '-1', *dst_args)), + (src_node, ('iperf3', '-c', dst_addr, *src_args)), + ) - def exec(self, node_name: str, cmd: str): - """Execute command in node using context""" + def get_exec_command(self, node_name: str, cmd: str): node = self.nodes.get(node_name) if node is None: return None - - return subprocess.run( - 'nsenter -m -u -i -n -p -t ' + str(node.pid) + ' ' + cmd, - shell=True, - capture_output=True, - text=True - ) + return node, ('sh', '-lc', cmd) def netlink(self, routes): for name, nlmsg in routes: @@ -357,6 +301,7 @@ def check_route(self, node_name: str): return subprocess.check_output( ('nsenter', '-n', '-t', str(node.pid), 'route'), + text=True ) def damage(self, random_list: List[str]): diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index c18c28c..1a2fd15 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -7,10 +7,12 @@ import threading import resource import logging -import threading import selectors from enum import Enum import struct +import queue +from dataclasses import dataclass, asdict, field +from typing import Dict, Optional, Any import paramiko from .sn_orchestrator import OrchestratorContext @@ -34,6 +36,27 @@ class CommandStatus(Enum): ERROR = "error" TIMEOUT = "timeout" +class TaskStatus(Enum): + SCHEDULED = "scheduled" + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + +@dataclass +class TaskRecord: + task_id: str + task_type: str + node: str + cmd: str + output_file: str + status: str + created_at: float + scheduled_at: float + started_at: Optional[float] = None + finished_at: Optional[float] = None + returncode: Optional[int] = None + metadata: Dict[str, Any] = field(default_factory=dict) + class SSHServerInterface(paramiko.ServerInterface): def __init__(self, username=None, password=None): @@ -84,8 +107,15 @@ def __init__(self, workdir=None, machine_id=0, log_level=logging.WARNING, self.node_mid_dict = {} self.ip_lst = [] + self.task_seq = 0 + self.tasks: Dict[str, TaskRecord] = {} + self.task_lock = threading.Lock() + self.task_queue = queue.PriorityQueue() + self.running_tasks = {} self._generate_ssh_keys() + self.task_thread = threading.Thread(target=self._task_loop, daemon=True) + self.task_thread.start() self.logger.info(f"Orchestrater daemon initialized on machine {self.machine_id}") self.logger.info(f"Working directory: {self.workdir}") @@ -307,6 +337,131 @@ def _get_context(self): self.logger.info("Orchestrator context initialized") return self.orchestrator_context + def _next_task_id(self): + with self.task_lock: + self.task_seq += 1 + return f"w{self.machine_id}-t{self.task_seq}" + + def _task_to_dict(self, task: TaskRecord): + return asdict(task) + + def _enqueue_task(self, task_type, node, cmdline, delay=0.0, metadata=None): + task_id = self._next_task_id() + output_file = f'{task_id}.out' + now = time.time() + task = TaskRecord( + task_id=task_id, + task_type=task_type, + node=node.name, + cmd=" ".join(cmdline), + output_file=output_file, + status=TaskStatus.SCHEDULED.value, + created_at=now, + scheduled_at=now + delay, + metadata=metadata or {} + ) + with self.task_lock: + self.tasks[task_id] = task + self.task_queue.put(( + task.scheduled_at, + task_id, + { + "kind": "process", + "node": node, + "cmdline": tuple(cmdline), + } + )) + return task + + def _enqueue_function_task(self, task_type, node_name, func, delay=0.0, metadata=None): + task_id = self._next_task_id() + output_file = f'{task_id}.out' + now = time.time() + task = TaskRecord( + task_id=task_id, + task_type=task_type, + node=node_name, + cmd=task_type, + output_file=output_file, + status=TaskStatus.SCHEDULED.value, + created_at=now, + scheduled_at=now + delay, + metadata=metadata or {} + ) + with self.task_lock: + self.tasks[task_id] = task + self.task_queue.put(( + task.scheduled_at, + task_id, + { + "kind": "function", + "func": func, + } + )) + return task + + def _task_loop(self): + current = None + while True: + time.sleep(0.1) + + if current is None: + try: + current = self.task_queue.get(block=False) + except queue.Empty: + current = None + + now = time.time() + while current is not None and current[0] <= now: + _, task_id, payload = current + task = self.tasks.get(task_id) + if task is not None: + output_path = os.path.join(self.workdir, task.output_file) + task.status = TaskStatus.RUNNING.value + task.started_at = now + if payload["kind"] == "process": + node = payload["node"] + cmdline = payload["cmdline"] + fd = os.open( + output_path, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC + ) + os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) + proc = node.run_command(cmdline, stdout=fd, stderr=subprocess.STDOUT) + os.close(fd) + self.running_tasks[proc] = task_id + else: + rc, output, err = payload["func"]() + with open(output_path, 'w') as f: + if output: + f.write(output) + if err: + if output: + f.write("\n") + f.write(err) + task.finished_at = time.time() + task.returncode = rc + task.error = err or None + task.status = TaskStatus.SUCCEEDED.value if rc == 0 else TaskStatus.FAILED.value + try: + current = self.task_queue.get(block=False) + except queue.Empty: + current = None + + finished = [] + for proc, task_id in self.running_tasks.items(): + rc = proc.poll() + if rc is None: + continue + task = self.tasks.get(task_id) + if task is not None: + task.finished_at = time.time() + task.returncode = rc + task.status = TaskStatus.SUCCEEDED.value if rc == 0 else TaskStatus.FAILED.value + finished.append(proc) + for proc in finished: + self.running_tasks.pop(proc, None) + def _process_command(self, command): try: t_begin = time.time() @@ -340,6 +495,12 @@ def _process_command(self, command): result = self._handle_clean(params) elif cmd_type == 'exec': result = self._handle_exec(params) + elif cmd_type == 'tasks': + result = self._handle_tasks(params) + elif cmd_type == 'task': + result = self._handle_task(params) + elif cmd_type == 'task_output': + result = self._handle_task_output(params) elif cmd_type == 'update_network_batch': result = self._handle_update_network_batch(params) elif cmd_type == 'netlink': @@ -445,17 +606,60 @@ def _handle_list(self, params): def _handle_ping(self, params): try: context = self._get_context() + results = [] for cmd in params.get('batch', []): - context.ping(cmd[0], cmd[1]) - return {"message": "All pings completed"} + src, dst = cmd[0], cmd[1] + extra_args = cmd[2] if len(cmd) > 2 else [] + prepared = context.get_ping_command(src, dst, extra_args) + if prepared is None: + results.append({ + "src": src, + "dst": dst, + "ok": False, + "error": "src or dst node not found", + }) + continue + node, cmdline = prepared + task = self._enqueue_task("ping", node, cmdline, metadata={"src": src, "dst": dst}) + results.append({ + "src": src, + "dst": dst, + "ok": True, + "task_id": task.task_id, + "status": task.status, + "output_file": task.output_file, + }) + return results except Exception as e: raise Exception(f"Ping failed: {e}") def _handle_iperf(self, params): try: context = self._get_context() - context.iperf(params.get('batch', [])) - return {"message": f"iPerf commands submitted"} + results = [] + for cmd in params.get('batch', []): + src, dst, src_args, dst_args = cmd[0], cmd[1], cmd[2], cmd[3] + prepared = context.get_iperf_commands(src, dst, src_args, dst_args) + if prepared is None: + results.append({ + "src": src, + "dst": dst, + "ok": False, + "error": "src or dst node not found", + }) + continue + server_node, server_cmd = prepared[0] + client_node, client_cmd = prepared[1] + server_task = self._enqueue_task("iperf_server", server_node, server_cmd, metadata={"src": src, "dst": dst}) + client_task = self._enqueue_task("iperf_client", client_node, client_cmd, delay=1.0, metadata={"src": src, "dst": dst}) + results.append({ + "src": src, + "dst": dst, + "ok": True, + "server_task_id": server_task.task_id, + "client_task_id": client_task.task_id, + }) + return results except Exception as e: raise Exception(f"iPerf failed: {e}") @@ -480,7 +684,7 @@ def _handle_route_batch(self, params): def _handle_netlink(self, params): try: context = self._get_context() - context.netlink(params.get('batch', []), []) + context.netlink(params.get('batch', [])) return {"message": f"netlink commands submitted"} except Exception as e: raise Exception(f"netlink failed: {e}") @@ -503,6 +707,10 @@ def _handle_clean(self, params): try: context = self._get_context() context.clean() + with self.task_lock: + self.tasks.clear() + self.running_tasks.clear() + self.task_queue = queue.PriorityQueue() return {"message": "Clean completed successfully"} except Exception as e: raise Exception(f"Clean failed: {e}") @@ -510,12 +718,77 @@ def _handle_clean(self, params): def _handle_exec(self, params): try: context = self._get_context() - for node, cmd in params.get('batch', []): - context.exec_command(node, cmd) - return {"message": "All commands executed successfully"} + results = [] + for node_name, cmd in params.get('batch', []): + prepared = context.get_exec_command(node_name, cmd) + if prepared is None: + results.append({ + "node": node_name, + "cmd": cmd, + "ok": False, + "error": "node not found", + }) + continue + node, cmdline = prepared + task = self._enqueue_task("exec", node, cmdline, metadata={"raw_cmd": cmd}) + results.append({ + "node": node_name, + "cmd": cmd, + "ok": True, + "task_id": task.task_id, + "status": task.status, + "output_file": task.output_file, + }) + return results except Exception as e: raise Exception(f"Exec failed: {e}") + def _handle_tasks(self, params): + task_type = params.get('type') + status = params.get('status') + node = params.get('node') + with self.task_lock: + tasks = list(self.tasks.values()) + result = [] + for task in tasks: + if task_type and task.task_type != task_type: + continue + if status and task.status != status: + continue + if node and task.node != node: + continue + result.append(self._task_to_dict(task)) + result.sort(key=lambda item: item['created_at']) + return result + + def _handle_task(self, params): + task_id = params.get('task_id') + if not task_id: + raise Exception("task_id is required") + with self.task_lock: + task = self.tasks.get(task_id) + if task is None: + raise Exception(f"task not found: {task_id}") + return self._task_to_dict(task) + + def _handle_task_output(self, params): + task_id = params.get('task_id') + if not task_id: + raise Exception("task_id is required") + with self.task_lock: + task = self.tasks.get(task_id) + if task is None: + raise Exception(f"task not found: {task_id}") + output_path = os.path.join(self.workdir, task.output_file) + content = "" + if os.path.exists(output_path): + with open(output_path, 'r') as f: + content = f.read() + return { + "task": self._task_to_dict(task), + "output": content, + } + def _handle_update_network_batch(self, params): try: context = self._get_context() diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 53391c9..79ba8b4 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -7,7 +7,6 @@ import time import threading import math -import re import os import glob import random @@ -94,7 +93,7 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ self.events = [] self.batch_events = [defaultdict(BatchCommand) for _ in range(math.ceil(self.duration))] - + def _init_local(self): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): os.remove(txt_file) @@ -419,6 +418,79 @@ def run_routing_daemon(self, bird_conf_path, node_lst='all'): time.sleep(1) print("Routing started!") + def _worker_for_node(self, node): + node_info = self.nodes.get(node) + if node_info is None: + return None + return node_info.worker + + def list_tasks(self, node=None, status=None, task_type=None): + if node is not None: + worker = self._worker_for_node(node) + if worker is None: + return [] + return worker.list_tasks(node=node, status=status, task_type=task_type) + + tasks = [] + seen = set() + for worker in self.worker_lst: + worker_tasks = worker.list_tasks(status=status, task_type=task_type) + for task in worker_tasks: + task_id = task.get('task_id') + if task_id in seen: + continue + seen.add(task_id) + tasks.append(task) + tasks.sort(key=lambda item: item.get('created_at', 0)) + return tasks + + def get_task(self, task_id, node=None): + if node is not None: + worker = self._worker_for_node(node) + if worker is None: + return {} + return worker.get_task(task_id) + + for worker in self.worker_lst: + result = worker.get_task(task_id) + if result: + return result + return {} + + def get_task_output(self, task_id, node=None): + if node is not None: + worker = self._worker_for_node(node) + if worker is None: + return {} + return worker.get_task_output(task_id) + + for worker in self.worker_lst: + result = worker.get_task_output(task_id) + if result: + return result + return {} + + def list_events(self): + events = [] + for t, event, *args in sorted(self.events, key=lambda item: item[0]): + events.append({ + "time": t, + "kind": "event", + "name": event.__name__.lstrip('_'), + "args": args, + }) + for t, batch_map in enumerate(self.batch_events): + for name, batch_cmd in batch_map.items(): + for args in batch_cmd.args_lst: + events.append({ + "time": t, + "kind": "batch", + "name": name, + "args": list(args), + }) + events.sort(key=lambda item: (item['time'], item['name'])) + return events + # static information def get_distance(self, node1, node2, t): node1, node2 = self.nodes.get(node1), self.nodes.get(node2) @@ -518,7 +590,7 @@ def _check_route(real_t, node): def set_static_route(self, src, dst, next_hop, t): def _static_route(worker, args_lst): - worker.static_route_batch(args_lst) + return worker.static_route_batch(args_lst) t = self._validate_t(t) batch_cmd = self.batch_events[t]['static_route'] @@ -527,7 +599,7 @@ def _static_route(worker, args_lst): def set_netlink(self, node, nlmsg, t): def _netlink(worker, args_lst): - worker.netlink_batch(args_lst) + return worker.netlink_batch(args_lst) t = self._validate_t(t) batch_cmd = self.batch_events[t]['netlink'] @@ -536,7 +608,7 @@ def _netlink(worker, args_lst): def set_ping(self, src, dst, t, extra_args=[]): def _ping(worker, args_lst): - worker.ping_batch(args_lst) + return worker.ping_batch(args_lst) t = self._validate_t(t) batch_cmd = self.batch_events[t]['ping'] @@ -545,7 +617,7 @@ def _ping(worker, args_lst): def set_iperf(self, src, dst, t, src_args = [], dst_args = []): def _iperf(worker, args_lst): - worker.iperf_batch(args_lst) + return worker.iperf_batch(args_lst) t = self._validate_t(t) batch_cmd = self.batch_events[t]['iperf'] @@ -554,7 +626,7 @@ def _iperf(worker, args_lst): def exec_at(self, node, cmd, t): def _exec(worker, args_lst): - worker.exec_batch(args_lst) + return worker.exec_batch(args_lst) t = self._validate_t(t) batch_cmd = self.batch_events[t]['exec'] @@ -571,7 +643,7 @@ def _event(self, real_t): for args in batch_cmd.args_lst: node_args[self.nodes[args[0]].worker].append(args) for worker, args_lst in node_args.items(): - batch_cmd.func(worker, args_lst) + response = batch_cmd.func(worker, args_lst) self.last_t += 1 From 41a771d133e8c67ae13d9a202dcda39da3666fec Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 1 May 2026 10:33:40 +0800 Subject: [PATCH 34/49] refactor: simply arg names of daemon, improve output messages, add BIRD dependency - remove "ssh" prefix of daemon configuration - improve "tasks" and "clean" output - add BIRD dependency in install.sh and README --- README.md | 4 ++-- bin/sn-worker | 20 ++++++++++---------- install.sh | 6 +++--- starrynet/cli.py | 2 +- starrynet/sn_orchestrator_daemon.py | 21 ++++++++++++--------- starrynet/sn_synchronizer.py | 2 +- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6f11625..d13a8d1 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The exact system packages depend on your Linux distribution, but in practice you - Python 3 - `pip` -- network namespace support on the host (most Linux distributions satisfy this by default) +- (Optional) [BIRD](https://bird.network.cz/) - reachable worker machines matching the `Machines` section in `config.json` (`127.0.0.1` recommended for initial trials) Python packages are listed in `tools/requirements.txt`. @@ -52,7 +52,7 @@ bash ./install.sh ```bash sudo apt update -sudo apt install python3 python3-pip python3-dev build-essential +sudo apt install python3 python3-pip python3-dev gcc bird ``` 2. (Optional) Create a Python virtual environment: diff --git a/bin/sn-worker b/bin/sn-worker index 93e8e05..c072ba3 100755 --- a/bin/sn-worker +++ b/bin/sn-worker @@ -4,7 +4,7 @@ import argparse import logging -from starrynet.sn_orchestrator_daemon import DEFAULT_SSH_PORT, OrchestraterDaemon +from starrynet.sn_orchestrator_daemon import DEFAULT_PORT, OrchestraterDaemon def build_parser(): @@ -16,12 +16,12 @@ def build_parser(): parser.add_argument("--log-level", type=str, default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log level") - parser.add_argument("--ssh-port", type=int, default=DEFAULT_SSH_PORT, - help="SSH server port") - parser.add_argument("--ssh-username", type=str, - help="SSH username for authentication") - parser.add_argument("--ssh-password", type=str, - help="SSH password for authentication") + parser.add_argument("--port", type=int, default=DEFAULT_PORT, + help="server port") + parser.add_argument("--username", type=str, + help="username for authentication") + parser.add_argument("--password", type=str, + help="password for authentication") return parser @@ -33,9 +33,9 @@ def main(): workdir=args.workdir, machine_id=args.machine_id, log_level=getattr(logging, args.log_level.upper()), - ssh_port=args.ssh_port, - ssh_username=args.ssh_username, - ssh_password=args.ssh_password, + port=args.port, + username=args.username, + password=args.password, ) if args.daemon: diff --git a/install.sh b/install.sh index 186f8f5..6d1dfc4 100644 --- a/install.sh +++ b/install.sh @@ -38,12 +38,12 @@ fi echo "Installing dependencies" if [ "$DIST" = "Ubuntu" ] || [ "$DIST" = "Debian" ]; then - $install python3 python3-dev python3-pip python3-setuptools gcc + $install python3 python3-dev python3-pip python3-setuptools gcc bird2 elif [ "$DIST" = "Fedora" -o "$DIST" = "RedHatEnterpriseServer" ] || [ "$DIST" = "CentOS" ]; then - $install python3 python3-devel python3-pip python3-setuptools gcc + $install python3 python3-devel python3-pip python3-setuptools gcc bird else echo "Unsupported distribution for automatic package installation." - echo "Install Python 3, pip, Python development headers, and gcc manually." + echo "Install Python 3, pip, Python development headers, gcc and bird manually." fi sudo python3 -m pip install --upgrade pip sudo python3 -m pip install -r tools/requirements.txt diff --git a/starrynet/cli.py b/starrynet/cli.py index a7af109..752ea23 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -298,7 +298,7 @@ def do_tasks(self, line): return tasks = self.sn.list_tasks(node=node) if not tasks: - output("No tasks found.\n") + output("No tasks running.\n") return lines = [] for task in tasks: diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index 1a2fd15..da7408d 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -29,7 +29,7 @@ # Daemon specific constants SOCKET_PATH = '/tmp/starrynet_orchestrater.sock' -DEFAULT_SSH_PORT = 18888 +DEFAULT_PORT = 18888 class CommandStatus(Enum): SUCCESS = "success" @@ -85,15 +85,18 @@ def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pi class OrchestraterDaemon: def __init__(self, workdir=None, machine_id=0, log_level=logging.WARNING, - ssh_port=DEFAULT_SSH_PORT, ssh_username='starrynet', ssh_password='123456'): + port=DEFAULT_PORT, username='starrynet', password='123456'): self.workdir = workdir or os.path.curdir self.machine_id = machine_id - self.ssh_port = ssh_port - self.ssh_username = ssh_username - self.ssh_password = ssh_password + self.port = port + self.username = username + self.password = password self.socket_path = SOCKET_PATH self.running = False + if not os.path.exists(self.workdir): + os.makedirs(self.workdir) + # Setup logging logging.basicConfig( level=log_level, @@ -186,10 +189,10 @@ def _start_ssh_server(self): try: self.ssh_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ssh_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.ssh_socket.bind(('0.0.0.0', self.ssh_port)) + self.ssh_socket.bind(('0.0.0.0', self.port)) self.ssh_socket.listen(100) - self.logger.info(f"SSH server listening on port {self.ssh_port}") + self.logger.info(f"SSH server listening on port {self.port}") except Exception as e: self.logger.error(f"Failed to start SSH server: {e}") @@ -216,7 +219,7 @@ def _handle_ssh_client(self, client, addr): transport = paramiko.Transport(client) transport.add_server_key(self.host_key) - server = SSHServerInterface(self.ssh_username, self.ssh_password) + server = SSHServerInterface(self.username, self.password) transport.start_server(server=server) channel = transport.accept(20) @@ -699,7 +702,7 @@ def _handle_rtable(self, params): def _handle_utility(self, params): try: - return subprocess.check_output(('vmstat', '-s')) + return subprocess.check_output(('vmstat', '-s'), text=True) except Exception as e: raise Exception(f"Utility check failed: {e}") diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 79ba8b4..21e1842 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -706,4 +706,4 @@ def clean(self): print("Removing containers and links...") for worker in self.worker_lst: worker.clean() - print("All containers and links workerd.") + print("All containers and links removed.") From 9b427bca5e2ef9f743a17aa2f2e93e8ee13f2646 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 1 May 2026 15:38:45 +0800 Subject: [PATCH 35/49] refactor: replace sysctl subprocess call with function call --- starrynet/sn_orchestrator.py | 45 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/starrynet/sn_orchestrator.py b/starrynet/sn_orchestrator.py index 0f8a99d..0b50e8d 100644 --- a/starrynet/sn_orchestrator.py +++ b/starrynet/sn_orchestrator.py @@ -27,6 +27,15 @@ def _switch_netns(node_pid: int): _libc.setns(pid_fd, CLONE_NEWNET) os.close(pid_fd) +def _sysctl(param: str, value: str, check: bool = True): + try: + fd = os.open(f'/proc/sys/{param.replace(".", "/")}', os.O_WRONLY) + os.write(fd, value.encode()) + os.close(fd) + except (FileNotFoundError, PermissionError): + if check: + raise + class NetInterface: def __init__(self, if_idx: int, ipv4: ipaddress.IPv4Interface = None, ipv6: ipaddress.IPv6Interface = None): self.if_idx = if_idx @@ -65,12 +74,7 @@ def _init_loopback(self): _switch_netns(self.pid) pynetlink.if_up('lo', self.socket_fd) - try: - fd = os.open('/proc/sys/net/mpls/conf/lo/input', os.O_WRONLY) - os.write(fd, b'1') - os.close(fd) - except FileNotFoundError: - pass + _sysctl('net.mpls.conf.lo.input', '1', check=False) addr4 = ipaddress.IPv4Interface( f"16.{(self.node_id >> 8) & 0xFF}.{self.node_id & 0xFF}.1/32") @@ -120,12 +124,7 @@ def init_if(self, if_name: str, addr: str, addr6: str, delay: str, bw: str, loss link.ipv4 = addr link.ipv6 = addr6 - try: - fd = os.open(f'/proc/sys/net/mpls/conf/{if_name}/input', os.O_WRONLY) - os.write(fd, b'1') - os.close(fd) - except FileNotFoundError: - pass + _sysctl(f'net.mpls.conf.{if_name}.input', '1', check=False) def update_if(self, if_name: str, delay: str, bw: str, loss: str): _switch_netns(self.pid) @@ -172,16 +171,16 @@ def init_nodes(self, base_dir, node_configs): node_configs: Dict mapping node names to their global unique IDs e.g., {'node1': 1, 'node2': 5, 'node3': 10} """ - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh1=4096')) - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh2=8192')) - subprocess.check_call(('sysctl', 'net.ipv4.neigh.default.gc_thresh3=16384')) - subprocess.check_call(('sysctl', 'net.ipv4.fib_multipath_hash_policy=1')) - subprocess.check_call(('sysctl', 'net.ipv4.conf.all.rp_filter=0')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh1=4096')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh2=8192')) - subprocess.run(('sysctl', 'net.ipv6.neigh.default.gc_thresh3=16384')) - subprocess.run(('sysctl', 'net.ipv6.fib_multipath_hash_policy=1')) - subprocess.run(('sysctl', 'net.mpls.platform_labels=10000')) + _sysctl('net.ipv4.neigh.default.gc_thresh1', '4096') + _sysctl('net.ipv4.neigh.default.gc_thresh2', '8192') + _sysctl('net.ipv4.neigh.default.gc_thresh3', '16384') + _sysctl('net.ipv4.fib_multipath_hash_policy', '1') + _sysctl('net.ipv4.conf.all.rp_filter', '0') + _sysctl('net.ipv6.neigh.default.gc_thresh1', '4096', check=False) + _sysctl('net.ipv6.neigh.default.gc_thresh2', '8192', check=False) + _sysctl('net.ipv6.neigh.default.gc_thresh3', '16384', check=False) + _sysctl('net.ipv6.fib_multipath_hash_policy', '1', check=False) + _sysctl('net.mpls.platform_labels', '10000', check=False) self.clean() os.makedirs(NETNS_DIR, exist_ok=True) @@ -300,7 +299,7 @@ def check_route(self, node_name: str): return '' return subprocess.check_output( - ('nsenter', '-n', '-t', str(node.pid), 'route'), + ('nsenter', '-n', '-t', str(node.pid), 'ip', 'route'), text=True ) From 28a5582251430703d3c348f0aac99cbe30de81a6 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Fri, 1 May 2026 17:02:37 +0800 Subject: [PATCH 36/49] feat: tabular formatting in CLI output, renaming utility command - tabular formatting output for CLI command `events`, `tasks` and `task` - rename `get_utility` to `check_utility` --- README.md | 8 ++- example.py | 2 +- starrynet/cli.py | 130 ++++++++++++++++++++++++++++------- starrynet/sn_synchronizer.py | 2 +- 4 files changed, 115 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d13a8d1..d3288ce 100644 --- a/README.md +++ b/README.md @@ -189,17 +189,21 @@ starrynet> get_position SH1O1S1 2 starrynet> get_IP SH1O1S1 ``` -Scheduling commands: +Dynamic commands: ```text -starrynet> get_utility 2 +starrynet> check_utility 2 starrynet> check_routing_table SH1O1S1 3 starrynet> set_static_route SH1O1S1 SH1O1S2 SH1O1S2 4 starrynet> set_ping SH1O1S1 SH1O1S2 5 starrynet> set_iperf SH1O1S1 SH1O1S2 6 starrynet> set_damage 0.3 7 starrynet> set_recovery 8 +starrynet> events starrynet> start_emulation +starrynet> tasks +starrynet> task w0-t1 +starrynet> task_output w0-t1 ``` Notes: diff --git a/example.py b/example.py index 209740c..f0fb079 100644 --- a/example.py +++ b/example.py @@ -40,7 +40,7 @@ print(f"\nSH1O1S1 GSes: {GSes}") # CPU and memory useage - sn.get_utility(t=2) + sn.check_utility(t=2) # IP addresses of a node IPs = sn.get_IP(node='SH1O1S1') diff --git a/starrynet/cli.py b/starrynet/cli.py index 752ea23..57d7f1b 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -3,6 +3,7 @@ from cmd import Cmd import shlex import sys +import time from .log import error, info, output @@ -68,6 +69,60 @@ def _check_node(self, node): return False return True + def _stringify_cell(self, value): + if value is None: + return "-" + if isinstance(value, (list, tuple)): + return ", ".join(str(item) for item in value) if value else "-" + if isinstance(value, dict): + if not value: + return "-" + return ", ".join(f"{key}={val}" for key, val in value.items()) + return str(value) + + def _format_wall_time(self, value): + if value is None: + return "-" + try: + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(value)) + except (TypeError, ValueError, OSError): + return str(value) + + def _render_table(self, headers, rows): + if not rows: + return "" + + rendered_rows = [ + [self._stringify_cell(cell) for cell in row] + for row in rows + ] + widths = [ + max(len(str(header)), *(len(row[idx]) for row in rendered_rows)) + for idx, header in enumerate(headers) + ] + + def build_row(values): + return "| " + " | ".join( + value.ljust(widths[idx]) for idx, value in enumerate(values) + ) + " |" + + separator = "+-" + "-+-".join("-" * width for width in widths) + "-+" + lines = [separator, build_row(headers), separator] + lines.extend(build_row(row) for row in rendered_rows) + lines.append(separator) + return "\n".join(lines) + + def _render_kv_table(self, title, mapping): + rows = [ + (key, value) + for key, value in mapping.items() + if value not in (None, "", [], {}, ()) + ] + table = self._render_table(["Field", "Value"], rows) + if not table: + return f"{title}: " + return f"{title}:\n{table}" + def do_help(self, line): if line: return super().do_help(line) @@ -86,10 +141,10 @@ def do_help(self, line): " get_GSes NODE TIME\n" " get_position NODE TIME\n" " get_IP NODE\n" - " get_utility TIME\n" + " check_utility TIME\n" + " check_routing_table NODE TIME\n" " set_damage RATIO TIME\n" " set_recovery TIME\n" - " check_routing_table NODE TIME\n" " set_static_route SRC DST NEXT_HOP TIME\n" " set_ping SRC DST TIME\n" " set_iperf SRC DST TIME\n" @@ -108,7 +163,6 @@ def do_status(self, _line): output( f"experiment: {self.sn.experiment_name}\n" f"config dir: {self.sn.configuration_dir}\n" - f"output dir: {self.sn.local_dir}\n" f"duration: {self.sn.duration}s\n" f"step: {self.sn.step}s\n" f"nodes: {len(self.sn.nodes)}\n" @@ -153,7 +207,7 @@ def do_get_distance(self, line): if t is None: return distance = self.sn.get_distance(node1=node1, node2=node2, t=t) - output(f"{node1} <-> {node2}: {distance:.2f} km\n") + output(f"{node1} <-> {node2} at {t} s: {distance:.2f} km\n") def do_get_neighbors(self, line): args = self._require_args(line, 2, "get_neighbors NODE TIME") @@ -163,7 +217,7 @@ def do_get_neighbors(self, line): if t is None: return neighbors = self.sn.get_neighbors(node=args[0], t=t) - output(f"neighbors: {neighbors}\n") + output(f"neighbors of {args[0]} at {t} s: {neighbors}\n") def do_get_GSes(self, line): args = self._require_args(line, 2, "get_GSes NODE TIME") @@ -173,7 +227,7 @@ def do_get_GSes(self, line): if t is None: return gses = self.sn.get_GSes(node=args[0], t=t) - output(f"ground stations: {gses}\n") + output(f"connected ground stations of {args[0]} at {t} s: {gses}\n") def do_get_position(self, line): args = self._require_args(line, 2, "get_position NODE TIME") @@ -183,23 +237,23 @@ def do_get_position(self, line): if t is None: return position = self.sn.get_position(node=args[0], t=t) - output(f"position: {position}\n") + output(f"position of {args[0]} at {t} s: {position}\n") def do_get_IP(self, line): args = self._require_args(line, 1, "get_IP NODE") if args is None or not self._check_node(args[0]): return ip_list = self.sn.get_IP(node=args[0]) - output(f"IPs: {ip_list}\n") + output(f"IPs of {args[0]}: {ip_list}\n") - def do_get_utility(self, line): - args = self._require_args(line, 1, "get_utility TIME") + def do_check_utility(self, line): + args = self._require_args(line, 1, "check_utility TIME") if args is None: return t = self._parse_int(args[0], "TIME") if t is None: return - self.sn.get_utility(t=t) + self.sn.check_utility(t=t) output("utility check scheduled.\n") def do_set_damage(self, line): @@ -284,12 +338,16 @@ def do_events(self, _line): if not events: output("No events.\n") return - lines = [] - for item in events: - lines.append( - f"t={item['time']} type={item['kind']} name={item['name']} args={item['args']}" + rows = [ + ( + item.get("time"), + item.get("kind"), + item.get("name"), + item.get("args"), ) - output("\n".join(lines) + "\n") + for item in events + ] + output(self._render_table(["Time", "Kind", "Name", "Args"], rows) + "\n") def do_tasks(self, line): args = self._parse_args(line) @@ -300,13 +358,25 @@ def do_tasks(self, line): if not tasks: output("No tasks running.\n") return - lines = [] - for task in tasks: - lines.append( - f"{task.get('task_id')} {task.get('task_type')} {task.get('node')} " - f"{task.get('status')} -> {task.get('output_file')}" + rows = [ + ( + task.get("task_id"), + task.get("task_type"), + task.get("node"), + task.get("status"), + task.get("output_file"), + self._format_wall_time(task.get("scheduled_at")), + self._format_wall_time(task.get("started_at")), + self._format_wall_time(task.get("finished_at")), ) - output("\n".join(lines) + "\n") + for task in tasks + ] + output( + self._render_table( + ["Task ID", "Type", "Node", "Status", "Output", "Scheduled", "Started", "Finished"], + rows, + ) + "\n" + ) def do_task(self, line): args = self._require_args(line, 1, "task TASK_ID") @@ -317,7 +387,21 @@ def do_task(self, line): if not task: output("Task not found.\n") return - output(str(task) + "\n") + task_fields = { + "task_id": task.get("task_id"), + "task_type": task.get("task_type"), + "node": task.get("node"), + "status": task.get("status"), + "cmd": task.get("cmd"), + "output_file": task.get("output_file"), + "created_at": self._format_wall_time(task.get("created_at")), + "scheduled_at": self._format_wall_time(task.get("scheduled_at")), + "started_at": self._format_wall_time(task.get("started_at")), + "finished_at": self._format_wall_time(task.get("finished_at")), + "returncode": task.get("returncode"), + "metadata": task.get("metadata"), + } + output(self._render_kv_table("Task", task_fields) + "\n") def do_task_output(self, line): args = self._require_at_least_args(line, 1, "task_output TASK_ID [NODE]") diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 21e1842..ffd385e 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -548,7 +548,7 @@ def _validate_t(self, t): t = round(t) return t - def get_utility(self, t): + def check_utility(self, t): def _check_utility(real_t): for mid, worker in enumerate(self.worker_lst): result = worker.check_utility() From a6a85bbcc64fe3a20cdfc29787f1eb1c797eee81 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sat, 9 May 2026 16:45:55 +0800 Subject: [PATCH 37/49] feat: unified event management in synchronizer and CLI - unify `events` and `batch_events` to `EventRecord` - organize event queue using heap - add APIs and CLI commands to list and inspect events - drop unused `_enqueue_function_task` in `OrchestraterDaemon` --- starrynet/cli.py | 77 ++++-- starrynet/sn_orchestrator_daemon.py | 61 +---- starrynet/sn_synchronizer.py | 406 ++++++++++++++++++++-------- 3 files changed, 356 insertions(+), 188 deletions(-) diff --git a/starrynet/cli.py b/starrynet/cli.py index 57d7f1b..053f053 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -149,6 +149,8 @@ def do_help(self, line): " set_ping SRC DST TIME\n" " set_iperf SRC DST TIME\n" " events\n" + " event EVENT_ID\n" + " event_result EVENT_ID\n" " tasks [NODE]\n" " task TASK_ID\n" " task_output TASK_ID [NODE]\n" @@ -253,8 +255,8 @@ def do_check_utility(self, line): t = self._parse_int(args[0], "TIME") if t is None: return - self.sn.check_utility(t=t) - output("utility check scheduled.\n") + event_id = self.sn.check_utility(t=t) + output(f"utility check scheduled: {event_id}\n") def do_set_damage(self, line): args = self._require_args(line, 2, "set_damage RATIO TIME") @@ -264,8 +266,8 @@ def do_set_damage(self, line): t = self._parse_int(args[1], "TIME") if ratio is None or t is None: return - self.sn.set_damage(damaging_ratio=ratio, t=t) - output("damage event scheduled.\n") + event_id = self.sn.set_damage(damaging_ratio=ratio, t=t) + output(f"damage event scheduled: {event_id}\n") def do_set_recovery(self, line): args = self._require_args(line, 1, "set_recovery TIME") @@ -274,8 +276,8 @@ def do_set_recovery(self, line): t = self._parse_int(args[0], "TIME") if t is None: return - self.sn.set_recovery(t=t) - output("recovery event scheduled.\n") + event_id = self.sn.set_recovery(t=t) + output(f"recovery event scheduled: {event_id}\n") def do_check_routing_table(self, line): args = self._require_args(line, 2, "check_routing_table NODE TIME") @@ -284,8 +286,8 @@ def do_check_routing_table(self, line): t = self._parse_int(args[1], "TIME") if t is None: return - self.sn.check_routing_table(node=args[0], t=t) - output("routing table dump scheduled.\n") + event_id = self.sn.check_routing_table(node=args[0], t=t) + output(f"routing table dump scheduled: {event_id}\n") def do_set_static_route(self, line): args = self._require_args( @@ -298,8 +300,8 @@ def do_set_static_route(self, line): t = self._parse_int(args[3], "TIME") if t is None: return - self.sn.set_static_route(src=src, dst=dst, next_hop=next_hop, t=t) - output("static route scheduled.\n") + event_id = self.sn.set_static_route(src=src, dst=dst, next_hop=next_hop, t=t) + output(f"static route scheduled: {event_id}\n") def do_set_next_hop(self, line): self.do_set_static_route(line) @@ -314,8 +316,8 @@ def do_set_ping(self, line): t = self._parse_int(args[2], "TIME") if t is None: return - self.sn.set_ping(src=src, dst=dst, t=t) - output("ping scheduled.\n") + event_id = self.sn.set_ping(src=src, dst=dst, t=t) + output(f"ping scheduled: {event_id}\n") def do_set_iperf(self, line): args = self._require_args(line, 3, "set_iperf SRC DST TIME") @@ -327,8 +329,8 @@ def do_set_iperf(self, line): t = self._parse_int(args[2], "TIME") if t is None: return - self.sn.set_iperf(src=src, dst=dst, t=t) - output("iperf scheduled.\n") + event_id = self.sn.set_iperf(src=src, dst=dst, t=t) + output(f"iperf scheduled: {event_id}\n") def do_set_perf(self, line): self.do_set_iperf(line) @@ -340,14 +342,53 @@ def do_events(self, _line): return rows = [ ( + item.get("event_id"), item.get("time"), - item.get("kind"), - item.get("name"), - item.get("args"), + item.get("type"), + item.get("status"), + item.get("result_mode"), + item.get("params"), ) for item in events ] - output(self._render_table(["Time", "Kind", "Name", "Args"], rows) + "\n") + output(self._render_table(["Event ID", "Time", "Type", "Status", "Result", "Params"], rows) + "\n") + + def do_event(self, line): + args = self._require_args(line, 1, "event EVENT_ID") + if args is None: + return + event = self.sn.get_event(args[0]) + if not event: + output("Event not found.\n") + return + event_fields = { + "event_id": event.get("event_id"), + "time": event.get("time"), + "type": event.get("type"), + "status": event.get("status"), + "result_mode": event.get("result_mode"), + "params": event.get("params"), + "created_at": self._format_wall_time(event.get("created_at")), + "triggered_at": self._format_wall_time(event.get("triggered_at")), + "finished_at": self._format_wall_time(event.get("finished_at")), + "error": event.get("error"), + "task_refs": event.get("task_refs"), + } + output(self._render_kv_table("Event", event_fields) + "\n") + + def do_event_result(self, line): + args = self._require_args(line, 1, "event_result EVENT_ID") + if args is None: + return + event = self.sn.get_event(args[0]) + if not event: + output("Event not found.\n") + return + + if event.get("error"): + output(f"{event.get('error')}\n") + else: + output(f"{event.get('result', 'No result available.')}\n") def do_tasks(self, line): args = self._parse_args(line) diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index da7408d..a2fcd35 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -376,33 +376,6 @@ def _enqueue_task(self, task_type, node, cmdline, delay=0.0, metadata=None): )) return task - def _enqueue_function_task(self, task_type, node_name, func, delay=0.0, metadata=None): - task_id = self._next_task_id() - output_file = f'{task_id}.out' - now = time.time() - task = TaskRecord( - task_id=task_id, - task_type=task_type, - node=node_name, - cmd=task_type, - output_file=output_file, - status=TaskStatus.SCHEDULED.value, - created_at=now, - scheduled_at=now + delay, - metadata=metadata or {} - ) - with self.task_lock: - self.tasks[task_id] = task - self.task_queue.put(( - task.scheduled_at, - task_id, - { - "kind": "function", - "func": func, - } - )) - return task - def _task_loop(self): current = None while True: @@ -422,30 +395,16 @@ def _task_loop(self): output_path = os.path.join(self.workdir, task.output_file) task.status = TaskStatus.RUNNING.value task.started_at = now - if payload["kind"] == "process": - node = payload["node"] - cmdline = payload["cmdline"] - fd = os.open( - output_path, - os.O_WRONLY | os.O_CREAT | os.O_TRUNC - ) - os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) - proc = node.run_command(cmdline, stdout=fd, stderr=subprocess.STDOUT) - os.close(fd) - self.running_tasks[proc] = task_id - else: - rc, output, err = payload["func"]() - with open(output_path, 'w') as f: - if output: - f.write(output) - if err: - if output: - f.write("\n") - f.write(err) - task.finished_at = time.time() - task.returncode = rc - task.error = err or None - task.status = TaskStatus.SUCCEEDED.value if rc == 0 else TaskStatus.FAILED.value + node = payload["node"] + cmdline = payload["cmdline"] + fd = os.open( + output_path, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC + ) + os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) + proc = node.run_command(cmdline, stdout=fd, stderr=subprocess.STDOUT) + os.close(fd) + self.running_tasks[proc] = task_id try: current = self.task_queue.get(block=False) except queue.Empty: diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index ffd385e..5300a1d 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -11,9 +11,10 @@ import glob import random import ipaddress +import heapq from enum import Enum from collections import defaultdict -from typing import List, Tuple, Dict, Callable +from typing import List, Tuple, Dict, Any from dataclasses import dataclass, field from .sn_observer import * from .sn_utils import * @@ -45,12 +46,24 @@ class NodeInfo: worker: SSHDaemonClient = None @dataclass -class BatchCommand: - func: Callable = None - args_lst: list = field(default_factory=list) - -def _gs2idx(gs_name): - return int(gs_name[2:])-1 +class EventRecord: + event_id: str + time: int + event_type: str + params: Dict[str, Any] = field(default_factory=dict) + result_mode: str = 'none' + status: str = 'queuing' + created_at: float = field(default_factory=time.time) + triggered_at: float = None + finished_at: float = None + result: Any = None + error: str = None + task_refs: List[Dict[str, Any]] = field(default_factory=list) + + def __lt__(self, other): + return (self.time, self.created_at, self.event_id) < ( + other.time, other.created_at, other.event_id + ) class StarryNet(): @@ -91,8 +104,10 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ self._assign_worker([shell[0] for shell in sat_t_shell], sn_args.machine_lst) - self.events = [] - self.batch_events = [defaultdict(BatchCommand) for _ in range(math.ceil(self.duration))] + self.events: List[EventRecord] = [] + self.event_history: Dict[str, EventRecord] = {} + self._event_seq = 0 + self._event_lock = threading.Lock() def _init_local(self): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): @@ -424,6 +439,63 @@ def _worker_for_node(self, node): return None return node_info.worker + def _next_event_id(self): + self._event_seq += 1 + return f"e{self._event_seq}" + + def _event_to_dict(self, event: EventRecord): + result = { + "event_id": event.event_id, + "time": event.time, + "type": event.event_type, + "params": event.params, + "result_mode": event.result_mode, + "status": event.status, + "created_at": event.created_at, + "triggered_at": event.triggered_at, + "finished_at": event.finished_at, + "error": event.error, + "task_refs": event.task_refs, + } + if event.result is not None: + result["result"] = event.result + return result + + def _queue_event(self, event_type, t, params=None, result_mode='none'): + with self._event_lock: + event = EventRecord( + event_id=self._next_event_id(), + time=self._validate_t(t), + event_type=event_type, + params=params or {}, + result_mode=result_mode, + ) + heapq.heappush(self.events, event) + self.event_history[event.event_id] = event + return event.event_id + + def _set_event_result(self, event: EventRecord, result=None, error=None, task_refs=None): + event.finished_at = time.time() + event.result = result + event.error = error + if task_refs is not None: + event.task_refs = task_refs + event.status = 'failed' if error else 'succeeded' + return event + + def list_events(self): + with self._event_lock: + events = list(self.event_history.values()) + events.sort(key=lambda item: (item.time, item.created_at, item.event_id)) + return [self._event_to_dict(event) for event in events] + + def get_event(self, event_id): + with self._event_lock: + event = self.event_history.get(event_id) + if event is None: + return {} + return self._event_to_dict(event) + def list_tasks(self, node=None, status=None, task_type=None): if node is not None: worker = self._worker_for_node(node) @@ -470,27 +542,6 @@ def get_task_output(self, task_id, node=None): return result return {} - def list_events(self): - events = [] - for t, event, *args in sorted(self.events, key=lambda item: item[0]): - events.append({ - "time": t, - "kind": "event", - "name": event.__name__.lstrip('_'), - "args": args, - }) - for t, batch_map in enumerate(self.batch_events): - for name, batch_cmd in batch_map.items(): - for args in batch_cmd.args_lst: - events.append({ - "time": t, - "kind": "batch", - "name": name, - "args": list(args), - }) - events.sort(key=lambda item: (item['time'], item['name'])) - return events - # static information def get_distance(self, node1, node2, t): node1, node2 = self.nodes.get(node1), self.nodes.get(node2) @@ -549,111 +600,233 @@ def _validate_t(self, t): return t def check_utility(self, t): - def _check_utility(real_t): - for mid, worker in enumerate(self.worker_lst): - result = worker.check_utility() - with open(os.path.join( - self.local_dir, f'{real_t}-utility-machine{mid}.txt'), 'w') as f: - f.write(result) + return self._queue_event('check_utility', t, result_mode='inline') - self.events.append((t, _check_utility,)) + def _run_check_utility_event(self, event: EventRecord): + results = {} + for mid, worker in enumerate(self.worker_lst): + result = worker.check_utility() + key = f'machine{mid}' + results[key] = result + self._set_event_result(event, result=results) + + def check_routing_table(self, node, t): + return self._queue_event( + 'check_routing_table', + t, + params={'node': node}, + result_mode='inline', + ) + + def _run_check_routing_table_event(self, event: EventRecord): + node = event.params['node'] + result = self.nodes[node].worker.check_routing_table(node) + self._set_event_result(event, result=result) def set_damage(self, damaging_ratio, t): - def _damage(real_t, damaging_ratio): - damage_lsts = {worker:[] for worker in self.worker_lst} - cur_num = len(self.undamaged_lst) - need_damage_num = min(len(self.total_sat_lst) * damaging_ratio, cur_num) - while(cur_num - len(self.undamaged_lst) < need_damage_num): - sat = self.undamaged_lst.pop( - random.randint(0, len(self.undamaged_lst) - 1) - ) - worker = self.nodes[sat].worker - damage_lsts[worker].append(sat) - for worker, lst in damage_lsts.items(): + return self._queue_event( + 'damage', + t, + params={'damaging_ratio': damaging_ratio}, + ) + + def _run_damage_event(self, event: EventRecord): + damaging_ratio = event.params['damaging_ratio'] + damage_lsts = {worker: [] for worker in self.worker_lst} + cur_num = len(self.undamaged_lst) + need_damage_num = min(len(self.total_sat_lst) * damaging_ratio, cur_num) + while cur_num - len(self.undamaged_lst) < need_damage_num and self.undamaged_lst: + sat = self.undamaged_lst.pop( + random.randint(0, len(self.undamaged_lst) - 1) + ) + worker = self.nodes[sat].worker + damage_lsts[worker].append(sat) + for worker, lst in damage_lsts.items(): + if lst: worker.damage_nodes(lst) - self.events.append((t, _damage, damaging_ratio,)) + machine_map = {} + for mid, worker in enumerate(self.worker_lst): + if damage_lsts[worker]: + machine_map[f'machine{mid}'] = damage_lsts[worker] + self._set_event_result( + event, + result={ + 'damaged_nodes': sum(len(lst) for lst in damage_lsts.values()), + 'machines': machine_map, + }, + ) def set_recovery(self, t): - def _recovery(real_t): - for worker in self.worker_lst: - worker.recover_nodes() - self.undamaged_lst = self.total_sat_lst.copy() - self.events.append((t, _recovery,)) - - def check_routing_table(self, node, t): - def _check_route(real_t, node): - result = self.nodes[node].worker.check_routing_table(node) - with open(os.path.join(self.local_dir, f'{real_t}-route-{node}.txt'), 'w') as f: - f.write(result) + return self._queue_event('recovery', t) - self.events.append((t, _check_route, node,)) + def _run_recovery_event(self, event: EventRecord): + for worker in self.worker_lst: + worker.recover_nodes() + self.undamaged_lst = self.total_sat_lst.copy() + self._set_event_result(event, result={'recovered': True}) def set_static_route(self, src, dst, next_hop, t): - def _static_route(worker, args_lst): - return worker.static_route_batch(args_lst) - - t = self._validate_t(t) - batch_cmd = self.batch_events[t]['static_route'] - batch_cmd.func = _static_route - batch_cmd.args_lst.append((src, dst, next_hop)) + return self._queue_event( + 'static_route', + t, + params={'src': src, 'dst': dst, 'next_hop': next_hop}, + ) def set_netlink(self, node, nlmsg, t): - def _netlink(worker, args_lst): - return worker.netlink_batch(args_lst) - - t = self._validate_t(t) - batch_cmd = self.batch_events[t]['netlink'] - batch_cmd.func = _netlink - batch_cmd.args_lst.append((node, nlmsg)) + return self._queue_event( + 'netlink', + t, + params={'node': node, 'nlmsg': nlmsg}, + ) def set_ping(self, src, dst, t, extra_args=[]): - def _ping(worker, args_lst): - return worker.ping_batch(args_lst) - - t = self._validate_t(t) - batch_cmd = self.batch_events[t]['ping'] - batch_cmd.func = _ping - batch_cmd.args_lst.append((src, dst, extra_args)) + return self._queue_event( + 'ping', + t, + params={'src': src, 'dst': dst, 'extra_args': list(extra_args)}, + result_mode='task', + ) def set_iperf(self, src, dst, t, src_args = [], dst_args = []): - def _iperf(worker, args_lst): - return worker.iperf_batch(args_lst) - - t = self._validate_t(t) - batch_cmd = self.batch_events[t]['iperf'] - batch_cmd.func = _iperf - batch_cmd.args_lst.append((src, dst, src_args, dst_args)) + return self._queue_event( + 'iperf', + t, + params={ + 'src': src, + 'dst': dst, + 'src_args': list(src_args), + 'dst_args': list(dst_args), + }, + result_mode='task', + ) def exec_at(self, node, cmd, t): - def _exec(worker, args_lst): - return worker.exec_batch(args_lst) - - t = self._validate_t(t) - batch_cmd = self.batch_events[t]['exec'] - batch_cmd.func = _exec - batch_cmd.args_lst.append((node, cmd)) + return self._queue_event( + 'exec', + t, + params={'node': node, 'cmd': cmd}, + result_mode='task', + ) + + def _run_batch_events(self, events: List[EventRecord], event_type, builder, sender, task_ref_builder=None): + worker_args = defaultdict(list) + worker_events = defaultdict(list) + for event in events: + args = builder(event) + worker = self.nodes[args[0]].worker + worker_args[worker].append(args) + worker_events[worker].append(event) + + for worker, args_lst in worker_args.items(): + response = sender(worker, args_lst) or {} + results = response.get('result', []) if isinstance(response, dict) else [] + if not isinstance(results, list): + for event in worker_events[worker]: + self._set_event_result(event, result=results) + continue + for index, event in enumerate(worker_events[worker]): + if index >= len(results): + self._set_event_result( + event, + error=f'{event_type} returned fewer results than expected', + ) + continue + result = results[index] + if result.get('ok', True): + task_refs = task_ref_builder(result) if task_ref_builder else None + self._set_event_result(event, result=result, task_refs=task_refs) + else: + self._set_event_result(event, error=result.get('error', f'{event_type} failed'), result=result) + + def _pop_due_events(self, real_t): + due_events = [] + with self._event_lock: + while self.events and self.events[0].time <= real_t: + due_events.append(heapq.heappop(self.events)) + return due_events def _event(self, real_t): - while len(self.events) > 0 and self.events[-1][0] <= real_t: - event = self.events.pop(-1) - event[1](real_t, *event[2:]) - while self.last_t <= real_t: - for batch_cmd in self.batch_events[self.last_t].values(): - node_args = defaultdict(list) - for args in batch_cmd.args_lst: - node_args[self.nodes[args[0]].worker].append(args) - for worker, args_lst in node_args.items(): - response = batch_cmd.func(worker, args_lst) - - self.last_t += 1 + due_events = self._pop_due_events(real_t) + if not due_events: + return + + grouped = defaultdict(list) + for event in due_events: + event.status = 'running' + event.triggered_at = time.time() + grouped[event.event_type].append(event) + + for event in grouped.get('damage', []): + self._run_damage_event(event) + for event in grouped.get('recovery', []): + self._run_recovery_event(event) + for event in grouped.get('check_utility', []): + self._run_check_utility_event(event) + for event in grouped.get('check_routing_table', []): + self._run_check_routing_table_event(event) + + if grouped.get('static_route'): + self._run_batch_events( + grouped['static_route'], + 'static_route', + lambda event: ( + event.params['src'], + event.params['dst'], + event.params['next_hop'], + ), + lambda worker, args_lst: worker.static_route_batch(args_lst), + ) + if grouped.get('netlink'): + self._run_batch_events( + grouped['netlink'], + 'netlink', + lambda event: ( + event.params['node'], + event.params['nlmsg'], + ), + lambda worker, args_lst: worker.netlink_batch(args_lst), + ) + if grouped.get('ping'): + self._run_batch_events( + grouped['ping'], + 'ping', + lambda event: ( + event.params['src'], + event.params['dst'], + event.params.get('extra_args', []), + ), + lambda worker, args_lst: worker.ping_batch(args_lst), + lambda result: [{'task_id': result['task_id'], 'output_file': result.get('output_file')}], + ) + if grouped.get('iperf'): + self._run_batch_events( + grouped['iperf'], + 'iperf', + lambda event: ( + event.params['src'], + event.params['dst'], + event.params.get('src_args', []), + event.params.get('dst_args', []), + ), + lambda worker, args_lst: worker.iperf_batch(args_lst), + lambda result: [ + {'task_id': result['server_task_id'], 'role': 'server'}, + {'task_id': result['client_task_id'], 'role': 'client'}, + ], + ) + if grouped.get('exec'): + self._run_batch_events( + grouped['exec'], + 'exec', + lambda event: ( + event.params['node'], + event.params['cmd'], + ), + lambda worker, args_lst: worker.exec_batch(args_lst), + lambda result: [{'task_id': result['task_id'], 'output_file': result.get('output_file')}], + ) def start_emulation(self): - self.events.sort(key=lambda x:x[0], reverse=True) - self.last_t = 0 - - self.ping_threads = [] - self.iperf_threads = [] - start_time = time.time() print('Tick event at 0 s') self._event(0) @@ -697,11 +870,6 @@ def start_emulation(self): print(elapsed, "s elapsed,", update_end-start, "s for network update") tid += 1 - for ping_thread in self.ping_threads: - ping_thread.join() - for iperf_thread in self.iperf_threads: - iperf_thread.join() - def clean(self): print("Removing containers and links...") for worker in self.worker_lst: From 36a8e82a5312eac6e90392a8c845291ab504a957 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sun, 10 May 2026 17:18:58 +0800 Subject: [PATCH 38/49] feat: add multi-run control plane for StarryNet Introduce a FastAPI-based backend control plane with experiment and run resources. Refactor the runtime and worker protocol to scope state, artifacts, and tasks by run ID. --- starrynet/sn_daemon_client.py | 171 ++-- starrynet/sn_orchestrator.py | 5 +- starrynet/sn_orchestrator_daemon.py | 1085 ++++++++++++------------- starrynet/sn_synchronizer.py | 25 +- starrynet/sn_utils.py | 4 +- tools/requirements.txt | 5 +- web/backend/app/__init__.py | 1 + web/backend/app/api/__init__.py | 1 + web/backend/app/api/routes.py | 257 ++++++ web/backend/app/core/__init__.py | 1 + web/backend/app/core/configuration.py | 56 ++ web/backend/app/core/dependencies.py | 34 + web/backend/app/core/models.py | 109 +++ web/backend/app/core/runtime.py | 202 +++++ web/backend/app/core/store.py | 168 ++++ web/backend/app/main.py | 7 + 16 files changed, 1424 insertions(+), 707 deletions(-) create mode 100644 web/backend/app/__init__.py create mode 100644 web/backend/app/api/__init__.py create mode 100644 web/backend/app/api/routes.py create mode 100644 web/backend/app/core/__init__.py create mode 100644 web/backend/app/core/configuration.py create mode 100644 web/backend/app/core/dependencies.py create mode 100644 web/backend/app/core/models.py create mode 100644 web/backend/app/core/runtime.py create mode 100644 web/backend/app/core/store.py create mode 100644 web/backend/app/main.py diff --git a/starrynet/sn_daemon_client.py b/starrynet/sn_daemon_client.py index f31d090..602875b 100644 --- a/starrynet/sn_daemon_client.py +++ b/starrynet/sn_daemon_client.py @@ -19,8 +19,9 @@ class SSHDaemonClient: """SSH client for communicating with orchestrater daemon""" - def __init__(self, host: str, port: int, username: str = None, - password: str = None, timeout: int = 30): + def __init__(self, host: str, port: int, username: str = None, + password: str = None, timeout: int = 30, + run_id: str = "default"): """ Initialize SSH client @@ -36,11 +37,20 @@ def __init__(self, host: str, port: int, username: str = None, self.username = username self.password = password self.timeout = timeout + self.run_id = run_id self.logger = logging.getLogger(__name__) self._client = None self._channel = None self._connected = False + def _command(self, command_type: str, params: Optional[Dict[str, Any]] = None): + return { + 'c': command_type, + 't': time.time(), + 'rid': self.run_id, + 'p': params or {}, + } + def connect(self): try: self._client = paramiko.SSHClient() @@ -167,36 +177,24 @@ def _recv_exact_ssh(self, length: int) -> bytes: return bytes(data) def send_config(self, shell_num: int, node_mid_dict: dict, ip_lst: list): - command = { - 'c': 'config', - 't': time.time(), - 'p': { - 'shell_num': shell_num, - 'node_mid_dict': node_mid_dict, - 'ip_lst': ip_lst - } - } + command = self._command('config', { + 'shell_num': shell_num, + 'node_mid_dict': node_mid_dict, + 'ip_lst': ip_lst + }) response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to send config: {response.get('message')}") def init_nodes(self): - command = { - 'c': 'nodes', - 't': time.time(), - 'p': {} - } + command = self._command('nodes') response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to init nodes: {response.get('message')}") return response.get('result', []) def get_nodes(self): - command = { - 'c': 'list', - 't': time.time(), - 'p': {} - } + command = self._command('list') response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to get nodes: {response.get('message')}") @@ -228,121 +226,78 @@ def update_network(self, link_updates: dict): Returns: Response from daemon """ - command = { - 'c': 'update_network_batch', - 't': time.time(), - 'p': link_updates - } + command = self._command('update_network_batch', link_updates) response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to update network: {response.get('message')}") def check_utility(self): - command = { - 'c': 'utility', - 't': time.time(), - 'p': {} - } + command = self._command('utility') response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to check utility: {response.get('message')}") return response.get('result', {}) def check_routing_table(self, node: str): - command = { - 'c': 'rtable', - 't': time.time(), - 'p': { - 'node': node - } - } + command = self._command('rtable', { + 'node': node + }) response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to check routing table: {response.get('message')}") return response.get('result', {}) def damage_nodes(self, nodes: list): - command = { - 'c': 'damage', - 't': time.time(), - 'p': { - 'nodes': nodes - } - } + command = self._command('damage', { + 'nodes': nodes + }) response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to damage nodes: {response.get('message')}") def recover_nodes(self): - command = { - 'c': 'recovery', - 't': time.time(), - } + command = self._command('recovery') response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to recover nodes: {response.get('message')}") def init_routing(self, nodes: str | List, conf_text: str): - command = { - 'c': 'routed', - 't': time.time(), - 'p': { - 'nodes': nodes, - 'conf': conf_text, - } - } + command = self._command('routed', { + 'nodes': nodes, + 'conf': conf_text, + }) response = self._send_command_via_ssh(command) if response.get('status') != 'success': raise Exception(f"Failed to init routing: {response.get('message')}") def ping_batch(self, ping_cmds): - command = { - 'c': 'ping', - 't': time.time(), - 'p': { - 'batch': ping_cmds, - } - } + command = self._command('ping', { + 'batch': ping_cmds, + }) return self._send_command_via_ssh(command) def iperf_batch(self, iperf_cmds): - command = { - 'c': 'iperf', - 't': time.time(), - 'p': { - 'batch': iperf_cmds, - } - } + command = self._command('iperf', { + 'batch': iperf_cmds, + }) return self._send_command_via_ssh(command) def static_route_batch(self, rt_cmds): - command = { - 'c': 'sr', - 't': time.time(), - 'p': { - 'batch': rt_cmds, - } - } + command = self._command('sr', { + 'batch': rt_cmds, + }) return self._send_command_via_ssh(command) def netlink_batch(self, nl_cmds): - command = { - 'c': 'netlink', - 't': time.time(), - 'p': { - 'batch': nl_cmds, - } - } + command = self._command('netlink', { + 'batch': nl_cmds, + }) return self._send_command_via_ssh(command) def exec_batch(self, exec_cmds): - command = { - 'c': 'exec', - 't': time.time(), - 'p': { - 'batch': exec_cmds, - } - } + command = self._command('exec', { + 'batch': exec_cmds, + }) return self._send_command_via_ssh(command) def list_tasks(self, node: str = None, status: str = None, task_type: str = None): @@ -353,43 +308,27 @@ def list_tasks(self, node: str = None, status: str = None, task_type: str = None params['status'] = status if task_type is not None: params['type'] = task_type - command = { - 'c': 'tasks', - 't': time.time(), - 'p': params - } + command = self._command('tasks', params) response = self._send_command_via_ssh(command) return response.get('result', []) def get_task(self, task_id: str): - command = { - 'c': 'task', - 't': time.time(), - 'p': { - 'task_id': task_id, - } - } + command = self._command('task', { + 'task_id': task_id, + }) response = self._send_command_via_ssh(command) return response.get('result', {}) def get_task_output(self, task_id: str): - command = { - 'c': 'task_output', - 't': time.time(), - 'p': { - 'task_id': task_id, - } - } + command = self._command('task_output', { + 'task_id': task_id, + }) response = self._send_command_via_ssh(command) return response.get('result', {}) def clean(self): """Clean up all resources""" - command = { - 'c': 'clean', - 't': time.time(), - 'p': {} - } + command = self._command('clean') return self._send_command_via_ssh(command) def __enter__(self): diff --git a/starrynet/sn_orchestrator.py b/starrynet/sn_orchestrator.py index 0b50e8d..ca56a74 100644 --- a/starrynet/sn_orchestrator.py +++ b/starrynet/sn_orchestrator.py @@ -215,7 +215,7 @@ def add_link_intra_machine(self, def add_link_inter_machine(self, name1: str, name2: str, - remote_ip: str, addr4: str, addr6: str, + ifidx: int, remote_ip: str, addr4: str, addr6: str, delay: str, bw: str, loss: str ): node1 = self.nodes.get(name1) @@ -224,9 +224,10 @@ def add_link_inter_machine(self, pynetlink.add_link_vxlan( node1.pid, name2, - idx, ipaddress.ip_address(remote_ip).packed, + ifidx, ipaddress.ip_address(remote_ip).packed, self._main_net_sock_fd ) + node1.register_if(name2, ifidx) node1.init_if(name2, addr4, addr6, delay, bw, loss) def init_route_daemons(self, conf_path: str, nodes: str): diff --git a/starrynet/sn_orchestrator_daemon.py b/starrynet/sn_orchestrator_daemon.py index a2fcd35..03f00be 100644 --- a/starrynet/sn_orchestrator_daemon.py +++ b/starrynet/sn_orchestrator_daemon.py @@ -1,40 +1,32 @@ #!/usr/bin/python3 -import os -import subprocess import json -import time -import socket -import threading -import resource import logging +import os +import queue +import resource import selectors -from enum import Enum +import socket import struct -import queue -from dataclasses import dataclass, asdict, field -from typing import Dict, Optional, Any +import subprocess +import threading +import time +from dataclasses import asdict, dataclass, field +from enum import Enum +from typing import Any, Dict, Optional import paramiko + from .sn_orchestrator import OrchestratorContext MSG_MAX_SIZE = 10 * 1024 * 1024 # 10MB - -# Constants from original orchestrater -ASSIGN_FILENAME = 'assign.json' -PID_FILENAME = 'container_pid.txt' -DAMAGE_FILENAME = 'damage_list.txt' -NOT_ASSIGNED = 'NA' -VXLAN_PORT = '4789' -CLONE_NEWNET = 0x40000000 - -# Daemon specific constants -SOCKET_PATH = '/tmp/starrynet_orchestrater.sock' DEFAULT_PORT = 18888 +SOCKET_PATH = "/tmp/starrynet_orchestrater.sock" + class CommandStatus(Enum): SUCCESS = "success" ERROR = "error" - TIMEOUT = "timeout" + class TaskStatus(Enum): SCHEDULED = "scheduled" @@ -42,6 +34,7 @@ class TaskStatus(Enum): SUCCEEDED = "succeeded" FAILED = "failed" + @dataclass class TaskRecord: task_id: str @@ -57,35 +50,168 @@ class TaskRecord: returncode: Optional[int] = None metadata: Dict[str, Any] = field(default_factory=dict) -class SSHServerInterface(paramiko.ServerInterface): +class SSHServerInterface(paramiko.ServerInterface): def __init__(self, username=None, password=None): self.username = username self.password = password def check_auth_password(self, username, password): - if (self.username and username == self.username and - self.password and password == self.password): + if (self.username and username == self.username and + self.password and password == self.password): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED - + def check_auth_publickey(self, username, key): return paramiko.AUTH_FAILED - + def check_channel_request(self, kind, chanid): - if kind == 'session': + if kind == "session": return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED - + def check_channel_shell_request(self, channel): return True - - def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): + + def check_channel_pty_request( + self, channel, term, width, height, pixelwidth, pixelheight, modes): return True + +class RunContextState: + def __init__(self, run_id: str, base_workdir: str, machine_id: int, + logger: logging.Logger): + self.run_id = run_id + self.machine_id = machine_id + self.logger = logger + self.safe_run_id = self._safe_run_id(run_id) + self.root_dir = os.path.join(base_workdir, "runs", self.safe_run_id) + os.makedirs(self.root_dir, exist_ok=True) + + self.node_mid_dict: Dict[str, int] = {} + self.ip_lst = [] + self.task_seq = 0 + self.tasks: Dict[str, TaskRecord] = {} + self.task_lock = threading.Lock() + self.task_queue = queue.PriorityQueue() + self.running_tasks = {} + self.stop_event = threading.Event() + self.orchestrator_context: Optional[OrchestratorContext] = None + + self.task_thread = threading.Thread(target=self._task_loop, daemon=True) + self.task_thread.start() + + @staticmethod + def _safe_run_id(run_id: str) -> str: + safe = "".join(ch if ch.isalnum() or ch in ("-", "_") else "-" for ch in run_id) + return safe[:48] or "default" + + def ensure_context(self): + if self.orchestrator_context is None: + self.orchestrator_context = OrchestratorContext(self.root_dir) + return self.orchestrator_context + + def _next_task_id(self): + with self.task_lock: + self.task_seq += 1 + return f"{self.safe_run_id}-w{self.machine_id}-t{self.task_seq}" + + def task_to_dict(self, task: TaskRecord): + return asdict(task) + + def enqueue_task(self, task_type, node, cmdline, delay=0.0, metadata=None): + task_id = self._next_task_id() + output_file = f"{task_id}.out" + now = time.time() + task = TaskRecord( + task_id=task_id, + task_type=task_type, + node=node.name, + cmd=" ".join(cmdline), + output_file=output_file, + status=TaskStatus.SCHEDULED.value, + created_at=now, + scheduled_at=now + delay, + metadata=metadata or {}, + ) + with self.task_lock: + self.tasks[task_id] = task + self.task_queue.put(( + task.scheduled_at, + task_id, + { + "node": node, + "cmdline": tuple(cmdline), + } + )) + return task + + def _task_loop(self): + current = None + while not self.stop_event.is_set(): + time.sleep(0.1) + + if current is None: + try: + current = self.task_queue.get(block=False) + except queue.Empty: + current = None + + now = time.time() + while current is not None and current[0] <= now: + _, task_id, payload = current + task = self.tasks.get(task_id) + if task is not None: + output_path = os.path.join(self.root_dir, task.output_file) + task.status = TaskStatus.RUNNING.value + task.started_at = now + fd = os.open( + output_path, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + ) + cmdline = payload["cmdline"] + os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) + proc = payload["node"].run_command( + cmdline, stdout=fd, stderr=subprocess.STDOUT + ) + os.close(fd) + self.running_tasks[proc] = task_id + try: + current = self.task_queue.get(block=False) + except queue.Empty: + current = None + + finished = [] + for proc, task_id in list(self.running_tasks.items()): + rc = proc.poll() + if rc is None: + continue + task = self.tasks.get(task_id) + if task is not None: + task.finished_at = time.time() + task.returncode = rc + task.status = ( + TaskStatus.SUCCEEDED.value if rc == 0 + else TaskStatus.FAILED.value + ) + finished.append(proc) + for proc in finished: + self.running_tasks.pop(proc, None) + + def cleanup(self): + self.stop_event.set() + if self.orchestrator_context is not None: + self.orchestrator_context.clean() + self.orchestrator_context = None + with self.task_lock: + self.tasks.clear() + self.running_tasks.clear() + self.task_queue = queue.PriorityQueue() + + class OrchestraterDaemon: def __init__(self, workdir=None, machine_id=0, log_level=logging.WARNING, - port=DEFAULT_PORT, username='starrynet', password='123456'): + port=DEFAULT_PORT, username="starrynet", password="123456"): self.workdir = workdir or os.path.curdir self.machine_id = machine_id self.port = port @@ -93,56 +219,55 @@ def __init__(self, workdir=None, machine_id=0, log_level=logging.WARNING, self.password = password self.socket_path = SOCKET_PATH self.running = False + self.run_contexts: Dict[str, RunContextState] = {} + self.run_contexts_lock = threading.Lock() - if not os.path.exists(self.workdir): - os.makedirs(self.workdir) + os.makedirs(self.workdir, exist_ok=True) - # Setup logging logging.basicConfig( level=log_level, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ - logging.FileHandler(os.path.join(self.workdir, 'orchestrater_daemon.log')), + logging.FileHandler(os.path.join(self.workdir, "orchestrater_daemon.log")), logging.StreamHandler() ] ) self.logger = logging.getLogger(str(self.__class__)) - self.node_mid_dict = {} - self.ip_lst = [] - self.task_seq = 0 - self.tasks: Dict[str, TaskRecord] = {} - self.task_lock = threading.Lock() - self.task_queue = queue.PriorityQueue() - self.running_tasks = {} - self._generate_ssh_keys() - self.task_thread = threading.Thread(target=self._task_loop, daemon=True) - self.task_thread.start() - - self.logger.info(f"Orchestrater daemon initialized on machine {self.machine_id}") - self.logger.info(f"Working directory: {self.workdir}") - - def _signal_handler(self, signum, frame): - self.logger.info(f"Received signal {signum}, shutting down...") - self.stop() def _generate_ssh_keys(self): - self.host_key_path = os.path.join(self.workdir, 'ssh_host_key') + self.host_key_path = os.path.join(self.workdir, "ssh_host_key") if os.path.exists(self.host_key_path): - self.logger.info("Loading existing SSH host key...") - with open(self.host_key_path, 'r') as f: + with open(self.host_key_path, "r") as f: self.host_key = paramiko.RSAKey.from_private_key(f) else: - self.logger.info("Generating new SSH host key...") self.host_key = paramiko.RSAKey.generate(2048) - with open(self.host_key_path, 'w') as f: + with open(self.host_key_path, "w") as f: self.host_key.write_private_key(f) + def _safe_run_id(self, run_id: str): + return RunContextState._safe_run_id(run_id) + + def _get_run(self, run_id: str, create: bool = True): + safe_run_id = self._safe_run_id(run_id or "default") + with self.run_contexts_lock: + run_ctx = self.run_contexts.get(safe_run_id) + if run_ctx is None and create: + run_ctx = RunContextState(safe_run_id, self.workdir, self.machine_id, self.logger) + self.run_contexts[safe_run_id] = run_ctx + return run_ctx + + def _drop_run(self, run_id: str): + safe_run_id = self._safe_run_id(run_id or "default") + with self.run_contexts_lock: + run_ctx = self.run_contexts.pop(safe_run_id, None) + if run_ctx is not None: + run_ctx.cleanup() + def run(self): sel = selectors.DefaultSelector() - self._start_unix_socket_server() sel.register(self.unix_socket, selectors.EVENT_READ, self._unix_socket_server_accept) @@ -153,13 +278,11 @@ def run(self): self.logger.info("Orchestrater running ...") while self.running: events = sel.select() - for key, mask in events: - callback = key.data - callback() + for key, _mask in events: + key.data() self.stop() def _start_unix_socket_server(self): - """Start Unix socket server""" if os.path.exists(self.socket_path): os.remove(self.socket_path) @@ -167,124 +290,80 @@ def _start_unix_socket_server(self): self.unix_socket.bind(self.socket_path) self.unix_socket.listen(5) - self.logger.info(f"Unix socket server listening on {self.socket_path}") + def _start_ssh_server(self): + self.ssh_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ssh_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.ssh_socket.bind(("0.0.0.0", self.port)) + self.ssh_socket.listen(100) def _unix_socket_server_accept(self): - """Unix socket server accept callback""" try: - conn, addr = self.unix_socket.accept() - # Handle each connection in a separate thread - client_thread = threading.Thread( + conn, _addr = self.unix_socket.accept() + threading.Thread( target=self._handle_client, args=(conn,), - daemon=True - ) - client_thread.start() - except OSError as e: + daemon=True, + ).start() + except OSError as exc: if self.running: - self.logger.error(f"Unix socket accept error: {e}") - - def _start_ssh_server(self): - """Start SSH server""" - try: - self.ssh_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.ssh_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.ssh_socket.bind(('0.0.0.0', self.port)) - self.ssh_socket.listen(100) - - self.logger.info(f"SSH server listening on port {self.port}") - - except Exception as e: - self.logger.error(f"Failed to start SSH server: {e}") - raise e + self.logger.error(f"Unix socket accept error: {exc}") def _ssh_server_accept(self): try: client, addr = self.ssh_socket.accept() - self.logger.info(f"SSH connection from {addr[0]}:{addr[1]}") - - # Handle each SSH connection in a separate thread - ssh_thread = threading.Thread( + threading.Thread( target=self._handle_ssh_client, args=(client, addr), - daemon=True - ) - ssh_thread.start() - except Exception as e: + daemon=True, + ).start() + except Exception as exc: if self.running: - self.logger.error(f"SSH accept error: {e}") + self.logger.error(f"SSH accept error: {exc}") def _handle_ssh_client(self, client, addr): + transport = None try: transport = paramiko.Transport(client) transport.add_server_key(self.host_key) - server = SSHServerInterface(self.username, self.password) transport.start_server(server=server) - channel = transport.accept(20) if channel is None: - transport.close() return - - self.logger.info(f"SSH session established from {addr[0]}:{addr[1]}") - self._handle_client(channel) - - except Exception as e: - self.logger.error(f"SSH client handling error: {e}") + except Exception as exc: + self.logger.error(f"SSH client handling error from {addr}: {exc}") finally: - try: - transport.close() - except: - pass + if transport is not None: + try: + transport.close() + except Exception: + pass def stop(self): - self.logger.info("Stopping orchestrater daemon...") self.running = False - - if hasattr(self, 'unix_socket'): + if hasattr(self, "unix_socket"): self.unix_socket.close() - - if hasattr(self, 'ssh_socket'): + if hasattr(self, "ssh_socket"): self.ssh_socket.close() - if os.path.exists(self.socket_path): os.remove(self.socket_path) - - self.logger.info("Daemon stopped") + for run_id in list(self.run_contexts): + self._drop_run(run_id) def _send_message_with_length(self, conn, data): - try: - if isinstance(data, str): - data = data.encode('utf-8') - - length_prefix = struct.pack('!I', len(data)) - - conn.sendall(length_prefix + data) - except Exception as e: - self.logger.error(f"Error sending message with length: {e}") - raise + if isinstance(data, str): + data = data.encode("utf-8") + conn.sendall(struct.pack("!I", len(data)) + data) def _receive_message_with_length(self, conn): - try: - length_data = self._recv_exact(conn, 4) - if not length_data: - return None - - message_length = struct.unpack('!I', length_data)[0] - - if message_length > MSG_MAX_SIZE: - raise Exception(f"Message too large: {message_length} bytes") - - message_data = self._recv_exact(conn, message_length) - if not message_data: - return None - - return message_data - except Exception as e: - self.logger.error(f"Error receiving message with length: {e}") - raise + length_data = self._recv_exact(conn, 4) + if not length_data: + return None + message_length = struct.unpack("!I", length_data)[0] + if message_length > MSG_MAX_SIZE: + raise Exception(f"Message too large: {message_length} bytes") + return self._recv_exact(conn, message_length) def _recv_exact(self, conn, length): data = bytearray() @@ -301,416 +380,259 @@ def _handle_client(self, conn): message_data = self._receive_message_with_length(conn) if not message_data: break - try: - command = json.loads(message_data.decode('utf-8')) + command = json.loads(message_data.decode("utf-8")) response = self._process_command(command) - - response_data = json.dumps(response) - self._send_message_with_length(conn, response_data) - - except json.JSONDecodeError as e: - error_response = { + except json.JSONDecodeError as exc: + response = { "status": CommandStatus.ERROR.value, - "message": f"Invalid JSON: {e}" + "message": f"Invalid JSON: {exc}", } - self._send_message_with_length(conn, json.dumps(error_response)) - except Exception as e: - error_response = { + except Exception as exc: + response = { "status": CommandStatus.ERROR.value, - "message": f"Command processing error: {e}" + "message": f"Command processing error: {exc}", } - self._send_message_with_length(conn, json.dumps(error_response)) - - except Exception as e: - self.logger.error(f"Client connection error: {e}") + self._send_message_with_length(conn, json.dumps(response)) finally: - conn.close() + conn.close() def _update_rlimits(self, wanted_soft=65536): soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) new_soft = min(hard, wanted_soft) if soft < new_soft: resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard)) - self.logger.info(f"Updated RLIMIT_NOFILE: soft={new_soft}, hard={hard}") - - def _get_context(self): - if not hasattr(self, 'orchestrator_context'): - self.orchestrator_context = OrchestratorContext(self.workdir) - self.logger.info("Orchestrator context initialized") - return self.orchestrator_context - - def _next_task_id(self): - with self.task_lock: - self.task_seq += 1 - return f"w{self.machine_id}-t{self.task_seq}" - - def _task_to_dict(self, task: TaskRecord): - return asdict(task) - - def _enqueue_task(self, task_type, node, cmdline, delay=0.0, metadata=None): - task_id = self._next_task_id() - output_file = f'{task_id}.out' - now = time.time() - task = TaskRecord( - task_id=task_id, - task_type=task_type, - node=node.name, - cmd=" ".join(cmdline), - output_file=output_file, - status=TaskStatus.SCHEDULED.value, - created_at=now, - scheduled_at=now + delay, - metadata=metadata or {} - ) - with self.task_lock: - self.tasks[task_id] = task - self.task_queue.put(( - task.scheduled_at, - task_id, - { - "kind": "process", - "node": node, - "cmdline": tuple(cmdline), - } - )) - return task - - def _task_loop(self): - current = None - while True: - time.sleep(0.1) - - if current is None: - try: - current = self.task_queue.get(block=False) - except queue.Empty: - current = None - - now = time.time() - while current is not None and current[0] <= now: - _, task_id, payload = current - task = self.tasks.get(task_id) - if task is not None: - output_path = os.path.join(self.workdir, task.output_file) - task.status = TaskStatus.RUNNING.value - task.started_at = now - node = payload["node"] - cmdline = payload["cmdline"] - fd = os.open( - output_path, - os.O_WRONLY | os.O_CREAT | os.O_TRUNC - ) - os.write(fd, f"{now}: {' '.join(cmdline)}\n".encode()) - proc = node.run_command(cmdline, stdout=fd, stderr=subprocess.STDOUT) - os.close(fd) - self.running_tasks[proc] = task_id - try: - current = self.task_queue.get(block=False) - except queue.Empty: - current = None - - finished = [] - for proc, task_id in self.running_tasks.items(): - rc = proc.poll() - if rc is None: - continue - task = self.tasks.get(task_id) - if task is not None: - task.finished_at = time.time() - task.returncode = rc - task.status = TaskStatus.SUCCEEDED.value if rc == 0 else TaskStatus.FAILED.value - finished.append(proc) - for proc in finished: - self.running_tasks.pop(proc, None) def _process_command(self, command): - try: - t_begin = time.time() - cmd_type = command.get('c') # command - timestamp = command.get('t', time.time()) - params = command.get('p', {}) - - if cmd_type == 'config': - result = self._handle_config(params) - elif cmd_type == 'nodes': - result = self._handle_nodes(params) - elif cmd_type == 'damage': - result = self._handle_damage(params) - elif cmd_type == 'recovery': - result = self._handle_recovery(params) - elif cmd_type == 'routed': - result = self._handle_routed(params) - elif cmd_type == 'sr': - result = self._handle_route_batch(params) - elif cmd_type == 'list': - result = self._handle_list(params) - elif cmd_type == 'ping': - result = self._handle_ping(params) - elif cmd_type == 'iperf': - result = self._handle_iperf(params) - elif cmd_type == 'rtable': - result = self._handle_rtable(params) - elif cmd_type == 'utility': - result = self._handle_utility(params) - elif cmd_type == 'clean': - result = self._handle_clean(params) - elif cmd_type == 'exec': - result = self._handle_exec(params) - elif cmd_type == 'tasks': - result = self._handle_tasks(params) - elif cmd_type == 'task': - result = self._handle_task(params) - elif cmd_type == 'task_output': - result = self._handle_task_output(params) - elif cmd_type == 'update_network_batch': - result = self._handle_update_network_batch(params) - elif cmd_type == 'netlink': - result = self._handle_netlink(params) + t_begin = time.time() + cmd_type = command.get("c") + timestamp = command.get("t", time.time()) + run_id = command.get("rid", "default") + params = command.get("p", {}) + + if cmd_type == "utility": + result = self._handle_utility(params) + else: + run_ctx = self._get_run(run_id) + if cmd_type == "config": + result = self._handle_config(run_ctx, params) + elif cmd_type == "nodes": + result = self._handle_nodes(run_ctx, params) + elif cmd_type == "damage": + result = self._handle_damage(run_ctx, params) + elif cmd_type == "recovery": + result = self._handle_recovery(run_ctx, params) + elif cmd_type == "routed": + result = self._handle_routed(run_ctx, params) + elif cmd_type == "sr": + result = self._handle_route_batch(run_ctx, params) + elif cmd_type == "list": + result = self._handle_list(run_ctx, params) + elif cmd_type == "ping": + result = self._handle_ping(run_ctx, params) + elif cmd_type == "iperf": + result = self._handle_iperf(run_ctx, params) + elif cmd_type == "rtable": + result = self._handle_rtable(run_ctx, params) + elif cmd_type == "clean": + result = self._handle_clean(run_ctx, params) + self._drop_run(run_id) + elif cmd_type == "exec": + result = self._handle_exec(run_ctx, params) + elif cmd_type == "tasks": + result = self._handle_tasks(run_ctx, params) + elif cmd_type == "task": + result = self._handle_task(run_ctx, params) + elif cmd_type == "task_output": + result = self._handle_task_output(run_ctx, params) + elif cmd_type == "update_network_batch": + result = self._handle_update_network_batch(run_ctx, params) + elif cmd_type == "netlink": + result = self._handle_netlink(run_ctx, params) else: return { "status": CommandStatus.ERROR.value, - "message": f"Unknown command: {cmd_type}" + "message": f"Unknown command: {cmd_type}", } - t_finish = time.time() - self.logger.info(f"Command: {cmd_type} at {timestamp}, duration: {t_finish - t_begin:.6f} seconds") - - return { - "status": CommandStatus.SUCCESS.value, - "result": result, - "timestamp": timestamp - } - - except Exception as e: - self.logger.error(f"Command processing failed: {type(e).__name__} {e}", exc_info=True) - return { - "status": CommandStatus.ERROR.value, - "message": str(e) - } - - def _handle_config(self, params): - try: - self.node_mid_dict = params.get('node_mid_dict') - self.ip_lst = params.get('ip_lst') - - self.logger.info(f"Configuration received and loaded: " - f"nodes={len(self.node_mid_dict) if self.node_mid_dict else 0}") - - return { - "message": "Configuration loaded successfully", - "nodes_count": len(self.node_mid_dict) if self.node_mid_dict else 0 - } - except Exception as e: - self.logger.error(f"Failed to load configuration: {e}") - raise Exception(f"Configuration loading failed: {e}") - - def _handle_nodes(self, params): - try: - context = self._get_context() - - all_node_names = sorted(self.node_mid_dict.keys()) - - node_configs = {} - for i, node_name in enumerate(all_node_names): - if self.node_mid_dict[node_name] == self.machine_id: - node_configs[node_name] = i - - self._update_rlimits(len(node_configs) * 4) - - return context.init_nodes(self.workdir, node_configs) - except Exception as e: - raise Exception(f"Nodes initialization failed: {e}") - - def _handle_damage(self, params): - try: - context = self._get_context() - random_list = params.get('nodes', []) - context.damage(random_list) - return {"message": "Damage applied successfully"} - except Exception as e: - raise Exception(f"Damage failed: {e}") - - def _handle_recovery(self, params): - try: - context = self._get_context() - context.recover() - return {"message": "Recovery completed successfully"} - except Exception as e: - raise Exception(f"Recovery failed: {e}") + self.logger.info( + "Command: %s run=%s at %.3f duration=%.6f", + cmd_type, run_id, timestamp, time.time() - t_begin + ) + return { + "status": CommandStatus.SUCCESS.value, + "result": result, + "timestamp": timestamp, + } - def _handle_routed(self, params): - try: - context = self._get_context() - nodes = params.get('nodes', 'all') - conf_text = params['conf'] - conf_path = os.path.join(self.workdir, 'bird.conf') - with open(conf_path, 'w') as f: - f.write(conf_text) - context.init_route_daemons(conf_path, nodes) - return {"message": "Routing daemon initialized successfully"} - except Exception as e: - raise Exception(f"Routing daemon initialization failed: {e}") - - def _handle_list(self, params): - try: - context = self._get_context() - result = [] - for name in context.nodes.keys(): - result.append({ - "name": name, - "state": "Damaged" if name in context.damage_dict else "OK" - }) - return {"nodes": result} - except Exception as e: - raise Exception(f"List command failed: {e}") + def _handle_config(self, run_ctx: RunContextState, params): + run_ctx.node_mid_dict = params.get("node_mid_dict") or {} + run_ctx.ip_lst = params.get("ip_lst") or [] + return { + "message": "Configuration loaded successfully", + "nodes_count": len(run_ctx.node_mid_dict), + } - def _handle_ping(self, params): - try: - context = self._get_context() - results = [] - for cmd in params.get('batch', []): - src, dst = cmd[0], cmd[1] - extra_args = cmd[2] if len(cmd) > 2 else [] - prepared = context.get_ping_command(src, dst, extra_args) - if prepared is None: - results.append({ - "src": src, - "dst": dst, - "ok": False, - "error": "src or dst node not found", - }) - continue - node, cmdline = prepared - task = self._enqueue_task("ping", node, cmdline, metadata={"src": src, "dst": dst}) + def _handle_nodes(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + all_node_names = sorted(run_ctx.node_mid_dict.keys()) + node_configs = {} + for i, node_name in enumerate(all_node_names): + if run_ctx.node_mid_dict[node_name] == self.machine_id: + node_configs[node_name] = i + self._update_rlimits(len(node_configs) * 4) + return context.init_nodes(run_ctx.root_dir, node_configs) + + def _handle_damage(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + context.damage(params.get("nodes", [])) + return {"message": "Damage applied successfully"} + + def _handle_recovery(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + context.recover() + return {"message": "Recovery completed successfully"} + + def _handle_routed(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + conf_path = os.path.join(run_ctx.root_dir, "bird.conf") + with open(conf_path, "w") as f: + f.write(params["conf"]) + context.init_route_daemons(conf_path, params.get("nodes", "all")) + return {"message": "Routing daemon initialized successfully"} + + def _handle_list(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + damaged = {node.name for node in context.damage_lst} + result = [] + for name in context.nodes.keys(): + result.append({ + "name": name, + "state": "Damaged" if name in damaged else "OK", + }) + return {"nodes": result} + + def _handle_ping(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + results = [] + for cmd in params.get("batch", []): + src, dst = cmd[0], cmd[1] + extra_args = cmd[2] if len(cmd) > 2 else [] + prepared = context.get_ping_command(src, dst, extra_args) + if prepared is None: results.append({ "src": src, "dst": dst, - "ok": True, - "task_id": task.task_id, - "status": task.status, - "output_file": task.output_file, + "ok": False, + "error": "src or dst node not found", }) - return results - except Exception as e: - raise Exception(f"Ping failed: {e}") - - def _handle_iperf(self, params): - try: - context = self._get_context() - results = [] - for cmd in params.get('batch', []): - src, dst, src_args, dst_args = cmd[0], cmd[1], cmd[2], cmd[3] - prepared = context.get_iperf_commands(src, dst, src_args, dst_args) - if prepared is None: - results.append({ - "src": src, - "dst": dst, - "ok": False, - "error": "src or dst node not found", - }) - continue - server_node, server_cmd = prepared[0] - client_node, client_cmd = prepared[1] - server_task = self._enqueue_task("iperf_server", server_node, server_cmd, metadata={"src": src, "dst": dst}) - client_task = self._enqueue_task("iperf_client", client_node, client_cmd, delay=1.0, metadata={"src": src, "dst": dst}) + continue + node, cmdline = prepared + task = run_ctx.enqueue_task( + "ping", node, cmdline, metadata={"src": src, "dst": dst} + ) + results.append({ + "src": src, + "dst": dst, + "ok": True, + "task_id": task.task_id, + "status": task.status, + "output_file": task.output_file, + }) + return results + + def _handle_iperf(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + results = [] + for cmd in params.get("batch", []): + src, dst, src_args, dst_args = cmd[0], cmd[1], cmd[2], cmd[3] + prepared = context.get_iperf_commands(src, dst, src_args, dst_args) + if prepared is None: results.append({ "src": src, "dst": dst, - "ok": True, - "server_task_id": server_task.task_id, - "client_task_id": client_task.task_id, + "ok": False, + "error": "src or dst node not found", }) - return results - except Exception as e: - raise Exception(f"iPerf failed: {e}") - - def _handle_route_batch(self, params): - try: - context = self._get_context() - routes_lst = params.get('batch', []) - - total_routes = 0 - - for src, dst, next_hop in routes_lst: - context.set_static_route(src, dst, next_hop) - total_routes += 1 + continue + server_node, server_cmd = prepared[0] + client_node, client_cmd = prepared[1] + server_task = run_ctx.enqueue_task( + "iperf_server", server_node, server_cmd, metadata={"src": src, "dst": dst} + ) + client_task = run_ctx.enqueue_task( + "iperf_client", client_node, client_cmd, delay=1.0, + metadata={"src": src, "dst": dst} + ) + results.append({ + "src": src, + "dst": dst, + "ok": True, + "server_task_id": server_task.task_id, + "client_task_id": client_task.task_id, + }) + return results + + def _handle_route_batch(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + total_routes = 0 + for src, dst, next_hop in params.get("batch", []): + context.set_static_route(src, dst, next_hop) + total_routes += 1 + return { + "message": "Batch static routes set successfully", + "total_routes": total_routes, + } - return { - "message": f"Batch static routes set successfully", - "total_routes": total_routes - } - except Exception as e: - raise Exception(f"Batch static route failed: {e}") + def _handle_netlink(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + context.netlink(params.get("batch", [])) + return {"message": "netlink commands submitted"} - def _handle_netlink(self, params): - try: - context = self._get_context() - context.netlink(params.get('batch', [])) - return {"message": f"netlink commands submitted"} - except Exception as e: - raise Exception(f"netlink failed: {e}") - - def _handle_rtable(self, params): - try: - context = self._get_context() - node = params.get('node') - return context.check_route(node) - except Exception as e: - raise Exception(f"Routing table check failed: {e}") + def _handle_rtable(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + return context.check_route(params.get("node")) def _handle_utility(self, params): - try: - return subprocess.check_output(('vmstat', '-s'), text=True) - except Exception as e: - raise Exception(f"Utility check failed: {e}") - - def _handle_clean(self, params): - try: - context = self._get_context() - context.clean() - with self.task_lock: - self.tasks.clear() - self.running_tasks.clear() - self.task_queue = queue.PriorityQueue() - return {"message": "Clean completed successfully"} - except Exception as e: - raise Exception(f"Clean failed: {e}") - - def _handle_exec(self, params): - try: - context = self._get_context() - results = [] - for node_name, cmd in params.get('batch', []): - prepared = context.get_exec_command(node_name, cmd) - if prepared is None: - results.append({ - "node": node_name, - "cmd": cmd, - "ok": False, - "error": "node not found", - }) - continue - node, cmdline = prepared - task = self._enqueue_task("exec", node, cmdline, metadata={"raw_cmd": cmd}) + return subprocess.check_output(("vmstat", "-s"), text=True) + + def _handle_clean(self, run_ctx: RunContextState, params): + run_ctx.cleanup() + return {"message": "Clean completed successfully"} + + def _handle_exec(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + results = [] + for node_name, cmd in params.get("batch", []): + prepared = context.get_exec_command(node_name, cmd) + if prepared is None: results.append({ "node": node_name, "cmd": cmd, - "ok": True, - "task_id": task.task_id, - "status": task.status, - "output_file": task.output_file, + "ok": False, + "error": "node not found", }) - return results - except Exception as e: - raise Exception(f"Exec failed: {e}") - - def _handle_tasks(self, params): - task_type = params.get('type') - status = params.get('status') - node = params.get('node') - with self.task_lock: - tasks = list(self.tasks.values()) + continue + node, cmdline = prepared + task = run_ctx.enqueue_task( + "exec", node, cmdline, metadata={"raw_cmd": cmd} + ) + results.append({ + "node": node_name, + "cmd": cmd, + "ok": True, + "task_id": task.task_id, + "status": task.status, + "output_file": task.output_file, + }) + return results + + def _handle_tasks(self, run_ctx: RunContextState, params): + task_type = params.get("type") + status = params.get("status") + node = params.get("node") + with run_ctx.task_lock: + tasks = list(run_ctx.tasks.values()) result = [] for task in tasks: if task_type and task.task_type != task_type: @@ -719,87 +641,86 @@ def _handle_tasks(self, params): continue if node and task.node != node: continue - result.append(self._task_to_dict(task)) - result.sort(key=lambda item: item['created_at']) + result.append(run_ctx.task_to_dict(task)) + result.sort(key=lambda item: item["created_at"]) return result - def _handle_task(self, params): - task_id = params.get('task_id') + def _handle_task(self, run_ctx: RunContextState, params): + task_id = params.get("task_id") if not task_id: raise Exception("task_id is required") - with self.task_lock: - task = self.tasks.get(task_id) + with run_ctx.task_lock: + task = run_ctx.tasks.get(task_id) if task is None: raise Exception(f"task not found: {task_id}") - return self._task_to_dict(task) + return run_ctx.task_to_dict(task) - def _handle_task_output(self, params): - task_id = params.get('task_id') + def _handle_task_output(self, run_ctx: RunContextState, params): + task_id = params.get("task_id") if not task_id: raise Exception("task_id is required") - with self.task_lock: - task = self.tasks.get(task_id) + with run_ctx.task_lock: + task = run_ctx.tasks.get(task_id) if task is None: raise Exception(f"task not found: {task_id}") - output_path = os.path.join(self.workdir, task.output_file) + output_path = os.path.join(run_ctx.root_dir, task.output_file) content = "" if os.path.exists(output_path): - with open(output_path, 'r') as f: + with open(output_path, "r") as f: content = f.read() return { - "task": self._task_to_dict(task), + "task": run_ctx.task_to_dict(task), "output": content, } - def _handle_update_network_batch(self, params): - try: - context = self._get_context() - - isl_bw = params.get('isl_bw', '1000') - isl_loss = params.get('isl_loss', '0') - del_lst = params.get('del', []) - upd_lst = params.get('update', []) - add_lst = params.get('add', []) - - for src, dst in del_lst: - if self.node_mid_dict[src] == self.machine_id: - context.del_if(src, dst) - elif self.node_mid_dict[dst] == self.machine_id: - context.del_if(dst, src) - - for src, dst, delay in upd_lst: - if self.node_mid_dict[src] == self.machine_id: - context.update_if(src, dst, delay, isl_bw, isl_loss) - if self.node_mid_dict[dst] == self.machine_id: - context.update_if(dst, src, delay, isl_bw, isl_loss) - - for link in add_lst: - src, dst, delay = link[0], link[1], link[2] - src_ifidx, src_addr4, src_addr6 = link[3], link[4], link[5] - dst_ifidx, dst_addr4, dst_addr6 = link[6], link[7], link[8] - if self.node_mid_dict[src] == self.machine_id: - if self.node_mid_dict[dst] == self.machine_id: - context.add_link_intra_machine( - src, dst, - src_ifidx, src_addr4, src_addr6, - dst_ifidx, dst_addr4, dst_addr6, - delay, isl_bw, isl_loss - ) - else: - context.add_link_inter_machine( - src, dst, - context.ip_lst[context.node_mid_dict[dst]], - src_ifidx, src_addr4, src_addr6, - delay, isl_bw, isl_loss - ) - elif self.node_mid_dict[dst] == self.machine_id: - context.add_link_inter_machine( - dst, src, - context.ip_lst[context.node_mid_dict[dst]], + def _handle_update_network_batch(self, run_ctx: RunContextState, params): + context = run_ctx.ensure_context() + + isl_bw = params.get("isl_bw", "1000") + isl_loss = params.get("isl_loss", "0") + del_lst = params.get("del", []) + upd_lst = params.get("update", []) + add_lst = params.get("add", []) + + for src, dst in del_lst: + if run_ctx.node_mid_dict[src] == self.machine_id: + context.del_if(src, dst) + elif run_ctx.node_mid_dict[dst] == self.machine_id: + context.del_if(dst, src) + + for src, dst, delay in upd_lst: + if run_ctx.node_mid_dict[src] == self.machine_id: + context.update_if(src, dst, delay, isl_bw, isl_loss) + if run_ctx.node_mid_dict[dst] == self.machine_id: + context.update_if(dst, src, delay, isl_bw, isl_loss) + + for link in add_lst: + src, dst, delay = link[0], link[1], link[2] + src_ifidx, src_addr4, src_addr6 = link[3], link[4], link[5] + dst_ifidx, dst_addr4, dst_addr6 = link[6], link[7], link[8] + if run_ctx.node_mid_dict[src] == self.machine_id: + if run_ctx.node_mid_dict[dst] == self.machine_id: + context.add_link_intra_machine( + src, dst, + src_ifidx, src_addr4, src_addr6, dst_ifidx, dst_addr4, dst_addr6, - delay, isl_bw, isl_loss + delay, isl_bw, isl_loss, ) - return f'Delete {len(del_lst)}, update {len(upd_lst)}, add {len(add_lst)} links' - - except Exception as e: - raise Exception(f'Network update failed: {str(e)}') + else: + context.add_link_inter_machine( + src, dst, + src_ifidx, + run_ctx.ip_lst[run_ctx.node_mid_dict[dst]], + src_addr4, src_addr6, + delay, isl_bw, isl_loss, + ) + elif run_ctx.node_mid_dict[dst] == self.machine_id: + context.add_link_inter_machine( + dst, src, + dst_ifidx, + run_ctx.ip_lst[run_ctx.node_mid_dict[src]], + dst_addr4, dst_addr6, + delay, isl_bw, isl_loss, + ) + + return f"Delete {len(del_lst)}, update {len(upd_lst)}, add {len(add_lst)} links" diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 5300a1d..5868a6e 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -67,9 +67,12 @@ def __lt__(self, other): class StarryNet(): - def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_nodes_links = None): + def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, + extra_nodes_links = None, run_id: str = "default", + artifact_root: str = None): # Initialize constellation information. sn_args = sn_load_file(configuration_file_path) + self.run_id = run_id self.shell_lst = sn_args.shell_lst self.gs_lat_long = GS_lat_long self.link_style = sn_args.link_style @@ -91,7 +94,10 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ for shell_id, shell in enumerate(self.shell_lst): shell['name'] = f"shell{shell_id}" - self.local_dir = os.path.join(self.configuration_dir, self.experiment_name) + base_artifact_root = artifact_root or os.path.join( + self.configuration_dir, self.experiment_name + ) + self.local_dir = os.path.join(base_artifact_root, self.run_id) self._init_local() # Initialize Observer for topology computation @@ -108,6 +114,7 @@ def __init__(self, configuration_file_path, GS_lat_long, GS_links = None, extra_ self.event_history: Dict[str, EventRecord] = {} self._event_seq = 0 self._event_lock = threading.Lock() + self._stop_event = threading.Event() def _init_local(self): for txt_file in glob.glob(os.path.join(self.local_dir, '*.txt')): @@ -360,7 +367,8 @@ def _assign_worker(self, sat_names_shell, machine_lst): port=worker.get('port', 18888), username=worker.get('username', 'root'), password=worker.get('password', ''), - timeout=30 + timeout=30, + run_id=self.run_id, )) self.worker_lst: List[SSHDaemonClient] = worker_lst self.config_json = assign_obj @@ -429,10 +437,15 @@ def run_routing_daemon(self, bird_conf_path, node_lst='all'): worker.init_routing(','.join(names), bird_conf) for i in range(30): + if self._stop_event.is_set(): + break print(f'\r{i} / 30', end=' ') time.sleep(1) print("Routing started!") + def request_stop(self): + self._stop_event.set() + def _worker_for_node(self, node): node_info = self.nodes.get(node) if node_info is None: @@ -833,6 +846,8 @@ def start_emulation(self): tid = 1 while tid < len(self.changes_t): + if self._stop_event.is_set(): + break t = tid * self.step target_time = start_time + t @@ -840,7 +855,8 @@ def start_emulation(self): if now < target_time: sleep_time = target_time - now print('Sleeping', sleep_time, 's until', t, 's') - time.sleep(sleep_time) + if self._stop_event.wait(sleep_time): + break start = time.time() print("\nUpdate networks using pre-computed topology...") @@ -871,6 +887,7 @@ def start_emulation(self): tid += 1 def clean(self): + self.request_stop() print("Removing containers and links...") for worker in self.worker_lst: worker.clean() diff --git a/starrynet/sn_utils.py b/starrynet/sn_utils.py index 5b67986..56441f9 100644 --- a/starrynet/sn_utils.py +++ b/starrynet/sn_utils.py @@ -39,7 +39,8 @@ def sn_load_file(path): # TODO: parser.add_argument('--handover', default=table["Handover policy"]) # TODO: parser.add_argument('--time_slot', type=int, default=100) # TODO: parser.add_argument('--user_num', type=int, default=0) - sn_args = parser.parse_args() + # Library callers should not inherit unrelated process argv flags. + sn_args = parser.parse_args(args=[]) sn_args.__setattr__('machine_lst', table['Machines']) shell_lst = table['Shells'] for shell in shell_lst: @@ -65,4 +66,3 @@ def sn_remote_cmd(remote_ssh, cmd): def sn_remote_wait_output(remote_ssh, cmd): for line in remote_ssh.exec_command(cmd, get_pty=True)[1]: print(line, end='') - diff --git a/tools/requirements.txt b/tools/requirements.txt index 8746d50..de18124 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -2,4 +2,7 @@ setuptools numpy skyfield sgp4 -paramiko \ No newline at end of file +paramiko +fastapi +pydantic +uvicorn diff --git a/web/backend/app/__init__.py b/web/backend/app/__init__.py new file mode 100644 index 0000000..10fd1c7 --- /dev/null +++ b/web/backend/app/__init__.py @@ -0,0 +1 @@ +"""StarryNet backend application package.""" diff --git a/web/backend/app/api/__init__.py b/web/backend/app/api/__init__.py new file mode 100644 index 0000000..1ce04c3 --- /dev/null +++ b/web/backend/app/api/__init__.py @@ -0,0 +1 @@ +"""API routes.""" diff --git a/web/backend/app/api/routes.py b/web/backend/app/api/routes.py new file mode 100644 index 0000000..ab549a4 --- /dev/null +++ b/web/backend/app/api/routes.py @@ -0,0 +1,257 @@ +import os + +from fastapi import APIRouter, Depends, HTTPException, Query, status + +from ..core.dependencies import ARTIFACTS_DIR, get_current_user_id, get_runtime_manager, get_store +from ..core.configuration import write_bird_conf_artifact, write_config_artifact +from ..core.models import EventCreate, ExperimentCreate, ExperimentUpdate, RunStatus + +router = APIRouter() + + +def _require_experiment(store, experiment_id: str, user_id: str): + experiment = store.get_experiment(experiment_id) + if experiment is None or experiment.owner_user_id != user_id: + raise HTTPException(status_code=404, detail="experiment not found") + return experiment + + +def _require_run(store, run_id: str, user_id: str): + run = store.get_run(run_id) + if run is None or run.owner_user_id != user_id: + raise HTTPException(status_code=404, detail="run not found") + return run + + +def _experiment_dir(experiment_id: str): + return ARTIFACTS_DIR / "experiments" / experiment_id + + +def _persist_experiment_artifacts(store, experiment): + experiment_dir = _experiment_dir(experiment.experiment_id) + config_path = experiment_dir / "experiment_config.json" + generated_config_path = write_config_artifact(experiment, config_path) + generated_bird_conf_path = write_bird_conf_artifact( + experiment, + experiment_dir / "bird.conf", + ) + return store.update_experiment_fields( + experiment.experiment_id, + config_path=generated_config_path, + bird_conf_path=generated_bird_conf_path, + ) + + +@router.post("/experiments", status_code=status.HTTP_201_CREATED) +def create_experiment( + payload: ExperimentCreate, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store)): + experiment = store.create_experiment(user_id, payload) + return _persist_experiment_artifacts(store, experiment) + + +@router.get("/experiments") +def list_experiments( + user_id: str = Depends(get_current_user_id), + store=Depends(get_store)): + return store.list_experiments(user_id) + + +@router.get("/experiments/{experiment_id}") +def get_experiment( + experiment_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store)): + return _require_experiment(store, experiment_id, user_id) + + +@router.patch("/experiments/{experiment_id}") +def update_experiment( + experiment_id: str, + payload: ExperimentUpdate, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store)): + _require_experiment(store, experiment_id, user_id) + updated = store.update_experiment(experiment_id, payload) + if updated is None: + raise HTTPException(status_code=404, detail="experiment not found") + if ( + payload.configuration is not None + or payload.name is not None + or payload.bird_conf_content is not None + ): + updated = _persist_experiment_artifacts(store, updated) + return updated + + +@router.get("/experiments/{experiment_id}/runs") +def list_runs_for_experiment( + experiment_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store)): + _require_experiment(store, experiment_id, user_id) + return store.list_runs(experiment_id=experiment_id, owner_user_id=user_id) + + +@router.post("/experiments/{experiment_id}/runs", status_code=status.HTTP_201_CREATED) +def create_run( + experiment_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + experiment = _require_experiment(store, experiment_id, user_id) + artifact_dir = ARTIFACTS_DIR / "experiments" / experiment_id / "runs" + os.makedirs(artifact_dir, exist_ok=True) + run = store.create_run(experiment, str(artifact_dir / "pending")) + final_artifact_dir = artifact_dir / run.run_id + os.makedirs(final_artifact_dir, exist_ok=True) + run = store.update_run(run.run_id, artifact_dir=str(final_artifact_dir)) + runtime_manager.get_or_create(experiment, run).ensure_runtime() + return run + + +@router.get("/runs/{run_id}") +def get_run( + run_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store)): + return _require_run(store, run_id, user_id) + + +@router.post("/runs/{run_id}/start") +def start_run( + run_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + if run.status not in (RunStatus.READY, RunStatus.FAILED): + raise HTTPException(status_code=409, detail=f"run cannot be started from status {run.status}") + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + managed.start() + return store.get_run(run_id) + + +@router.post("/runs/{run_id}/stop") +def stop_run( + run_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + managed.stop() + return store.get_run(run_id) + + +@router.post("/runs/{run_id}/cleanup") +def cleanup_run( + run_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + managed.cleanup() + return store.get_run(run_id) + + +@router.get("/runs/{run_id}/topology") +def get_run_topology( + run_id: str, + time: int = Query(default=0, ge=0), + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + return { + "run_id": run_id, + "time": time, + "nodes": managed.list_nodes(at_time=time), + } + + +@router.get("/runs/{run_id}/nodes") +def get_run_nodes( + run_id: str, + time: int = Query(default=0, ge=0), + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + return managed.list_nodes(at_time=time) + + +@router.get("/runs/{run_id}/events") +def list_run_events( + run_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + return managed.list_events() + + +@router.post("/runs/{run_id}/events", status_code=status.HTTP_201_CREATED) +def create_run_event( + run_id: str, + payload: EventCreate, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + try: + return managed.schedule_event(payload) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.get("/runs/{run_id}/tasks") +def list_run_tasks( + run_id: str, + node: str | None = Query(default=None), + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + return managed.list_tasks(node=node) + + +@router.get("/runs/{run_id}/tasks/{task_id}") +def get_run_task( + run_id: str, + task_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + return managed.get_task(task_id) + + +@router.get("/runs/{run_id}/tasks/{task_id}/output") +def get_run_task_output( + run_id: str, + task_id: str, + user_id: str = Depends(get_current_user_id), + store=Depends(get_store), + runtime_manager=Depends(get_runtime_manager)): + run = _require_run(store, run_id, user_id) + experiment = _require_experiment(store, run.experiment_id, user_id) + managed = runtime_manager.get_or_create(experiment, run) + return managed.get_task_output(task_id) diff --git a/web/backend/app/core/__init__.py b/web/backend/app/core/__init__.py new file mode 100644 index 0000000..3fb528a --- /dev/null +++ b/web/backend/app/core/__init__.py @@ -0,0 +1 @@ +"""Core backend services and models.""" diff --git a/web/backend/app/core/configuration.py b/web/backend/app/core/configuration.py new file mode 100644 index 0000000..4a17fb9 --- /dev/null +++ b/web/backend/app/core/configuration.py @@ -0,0 +1,56 @@ +import json +from pathlib import Path + +from .models import ExperimentRecord + + +ROOT = Path(__file__).resolve().parents[4] +DEFAULT_CONFIG_TEMPLATE = ROOT / "config.json" + + +def build_config_payload(experiment: ExperimentRecord): + cfg = experiment.configuration + with DEFAULT_CONFIG_TEMPLATE.open("r", encoding="utf-8") as fh: + template = json.load(fh) + + template["Name"] = experiment.name + template["Shells"] = [ + { + "Altitude (km)": shell.altitude_km, + "Inclination": shell.inclination, + "Orbits": shell.orbits, + "Satellites per orbit": shell.satellites_per_orbit, + "Phase shift": shell.phase_shift, + } + for shell in cfg.shells + ] + template["Duration (s)"] = cfg.duration_s + template["step (s)"] = cfg.step_s + template['satellite link bandwidth ("X" Gbps)'] = cfg.satellite_link_bandwidth_gbps + template['sat-ground bandwidth ("X" Gbps)'] = cfg.sat_ground_bandwidth_gbps + template['satellite link loss ("X"% )'] = cfg.satellite_link_loss_percent + template['sat-ground loss ("X"% )'] = cfg.sat_ground_loss_percent + template["antenna number"] = cfg.antenna_number + template["antenna elevation angle"] = cfg.antenna_elevation_angle + template["Satellite link"] = cfg.satellite_link + template["IP version"] = cfg.ip_version + template["Link policy"] = cfg.link_policy + template["Handover policy"] = cfg.handover_policy + return template + + +def write_config_artifact(experiment: ExperimentRecord, config_path: Path): + config_path.parent.mkdir(parents=True, exist_ok=True) + payload = build_config_payload(experiment) + with config_path.open("w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2) + return str(config_path) + + +def write_bird_conf_artifact(experiment: ExperimentRecord, bird_conf_path: Path): + bird_conf_path.parent.mkdir(parents=True, exist_ok=True) + if experiment.bird_conf_content is None: + return None + with bird_conf_path.open("w", encoding="utf-8") as fh: + fh.write(experiment.bird_conf_content) + return str(bird_conf_path) diff --git a/web/backend/app/core/dependencies.py b/web/backend/app/core/dependencies.py new file mode 100644 index 0000000..52365b4 --- /dev/null +++ b/web/backend/app/core/dependencies.py @@ -0,0 +1,34 @@ +import os +from functools import lru_cache +from pathlib import Path + +from fastapi import Header, HTTPException, status + +from .runtime import RuntimeManager +from .store import MetadataStore + + +ROOT = Path(__file__).resolve().parents[4] +DATA_DIR = ROOT / "web" / "backend" / "data" +ARTIFACTS_DIR = DATA_DIR / "artifacts" +METADATA_PATH = DATA_DIR / "metadata.json" + + +def get_current_user_id(x_user_id: str = Header(default=None)): + if not x_user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="X-User-Id header is required", + ) + return x_user_id + + +@lru_cache(maxsize=1) +def get_store(): + os.makedirs(ARTIFACTS_DIR, exist_ok=True) + return MetadataStore(str(METADATA_PATH)) + + +@lru_cache(maxsize=1) +def get_runtime_manager(): + return RuntimeManager(get_store()) diff --git a/web/backend/app/core/models.py b/web/backend/app/core/models.py new file mode 100644 index 0000000..89073b7 --- /dev/null +++ b/web/backend/app/core/models.py @@ -0,0 +1,109 @@ +from enum import Enum +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class ShellDefinition(BaseModel): + altitude_km: float + inclination: float + orbits: int + satellites_per_orbit: int + phase_shift: int + + +class ExperimentConfiguration(BaseModel): + shells: List[ShellDefinition] + duration_s: int + step_s: int + satellite_link_bandwidth_gbps: int + sat_ground_bandwidth_gbps: int + satellite_link_loss_percent: int + sat_ground_loss_percent: int + antenna_number: int + antenna_elevation_angle: int + satellite_link: str + ip_version: str + link_policy: str + handover_policy: str = "instant handover" + + +class ExperimentStatus(str, Enum): + DRAFT = "draft" + READY = "ready" + ARCHIVED = "archived" + + +class RunStatus(str, Enum): + READY = "ready" + PROVISIONING = "provisioning" + ACTIVE = "active" + STOPPING = "stopping" + COMPLETED = "completed" + FAILED = "failed" + CLEANED = "cleaned" + + +class ExperimentRecord(BaseModel): + experiment_id: str + owner_user_id: str + name: str + configuration: ExperimentConfiguration + config_path: str + gs_lat_long: List[List[float]] + bird_conf_content: Optional[str] = None + bird_conf_path: Optional[str] = None + extra_nodes_links: Dict[str, List[str]] = Field(default_factory=dict) + status: ExperimentStatus = ExperimentStatus.READY + created_at: float + updated_at: float + + +class RunRecord(BaseModel): + run_id: str + experiment_id: str + owner_user_id: str + status: RunStatus = RunStatus.READY + artifact_dir: str + created_at: float + updated_at: float + started_at: Optional[float] = None + finished_at: Optional[float] = None + error: Optional[str] = None + + +class ExperimentCreate(BaseModel): + name: str + configuration: ExperimentConfiguration + gs_lat_long: List[List[float]] + bird_conf_content: Optional[str] = None + extra_nodes_links: Dict[str, List[str]] = Field(default_factory=dict) + + +class ExperimentUpdate(BaseModel): + name: Optional[str] = None + configuration: Optional[ExperimentConfiguration] = None + gs_lat_long: Optional[List[List[float]]] = None + bird_conf_content: Optional[str] = None + extra_nodes_links: Optional[Dict[str, List[str]]] = None + status: Optional[ExperimentStatus] = None + + +class RunCreate(BaseModel): + pass + + +class EventCreate(BaseModel): + time: int + event_type: str + params: Dict[str, Any] = Field(default_factory=dict) + + +class TopologyNode(BaseModel): + name: str + node_type: str + position: Optional[List[float]] = None + neighbors: List[str] = Field(default_factory=list) + ground_stations: List[str] = Field(default_factory=list) + ipv4: Optional[str] = None + ipv6: Optional[str] = None diff --git a/web/backend/app/core/runtime.py b/web/backend/app/core/runtime.py new file mode 100644 index 0000000..8c0b07a --- /dev/null +++ b/web/backend/app/core/runtime.py @@ -0,0 +1,202 @@ +import os +import threading +import time +from pathlib import Path +from typing import Dict, Optional + +from starrynet.sn_synchronizer import StarryNet + +from .models import EventCreate, ExperimentRecord, RunRecord, RunStatus, TopologyNode +from .store import MetadataStore + + +class ManagedRun: + def __init__(self, experiment: ExperimentRecord, run: RunRecord, store: MetadataStore): + self.experiment = experiment + self.run = run + self.store = store + self._lock = threading.Lock() + self._thread: Optional[threading.Thread] = None + self._runtime: Optional[StarryNet] = None + + def ensure_runtime(self): + with self._lock: + if self._runtime is None: + runtime_root = os.path.join(self.run.artifact_dir, "runtime") + os.makedirs(runtime_root, exist_ok=True) + self._runtime = StarryNet( + self.experiment.config_path, + self.experiment.gs_lat_long, + extra_nodes_links=self.experiment.extra_nodes_links, + run_id=self.run.run_id, + artifact_root=runtime_root, + ) + return self._runtime + + def start(self): + with self._lock: + if self._thread is not None and self._thread.is_alive(): + return + self.store.update_run( + self.run.run_id, + status=RunStatus.PROVISIONING, + error=None, + started_at=time.time(), + finished_at=None, + ) + self.run = self.store.get_run(self.run.run_id) + self._thread = threading.Thread(target=self._run_loop, daemon=True) + self._thread.start() + + def _run_loop(self): + runtime = self.ensure_runtime() + try: + runtime.create_nodes() + runtime.create_links() + if self.experiment.bird_conf_path: + runtime.run_routing_daemon(self.experiment.bird_conf_path) + self.store.update_run(self.run.run_id, status=RunStatus.ACTIVE) + runtime.start_emulation() + final_status = RunStatus.CLEANED if runtime._stop_event.is_set() else RunStatus.COMPLETED + self.store.update_run( + self.run.run_id, + status=final_status, + finished_at=time.time(), + ) + except Exception as exc: + current = self.store.get_run(self.run.run_id) + if current is not None and current.status in (RunStatus.STOPPING, RunStatus.CLEANED): + self.store.update_run( + self.run.run_id, + status=RunStatus.CLEANED, + finished_at=time.time(), + ) + else: + self.store.update_run( + self.run.run_id, + status=RunStatus.FAILED, + error=str(exc), + finished_at=time.time(), + ) + + def stop(self): + runtime = self.ensure_runtime() + self.store.update_run(self.run.run_id, status=RunStatus.STOPPING) + runtime.request_stop() + runtime.clean() + self.store.update_run( + self.run.run_id, + status=RunStatus.CLEANED, + finished_at=time.time(), + ) + + def cleanup(self): + runtime = self.ensure_runtime() + runtime.clean() + self.store.update_run( + self.run.run_id, + status=RunStatus.CLEANED, + finished_at=time.time(), + ) + + def schedule_event(self, event: EventCreate): + runtime = self.ensure_runtime() + dispatcher = { + "check_utility": lambda: runtime.check_utility(event.time), + "check_routing_table": lambda: runtime.check_routing_table( + node=event.params["node"], t=event.time + ), + "damage": lambda: runtime.set_damage( + damaging_ratio=event.params["damaging_ratio"], t=event.time + ), + "recovery": lambda: runtime.set_recovery(t=event.time), + "static_route": lambda: runtime.set_static_route( + src=event.params["src"], + dst=event.params["dst"], + next_hop=event.params["next_hop"], + t=event.time, + ), + "ping": lambda: runtime.set_ping( + src=event.params["src"], + dst=event.params["dst"], + t=event.time, + extra_args=event.params.get("extra_args", []), + ), + "iperf": lambda: runtime.set_iperf( + src=event.params["src"], + dst=event.params["dst"], + t=event.time, + src_args=event.params.get("src_args", []), + dst_args=event.params.get("dst_args", []), + ), + "exec": lambda: runtime.exec_at( + node=event.params["node"], + cmd=event.params["cmd"], + t=event.time, + ), + "netlink": lambda: runtime.set_netlink( + node=event.params["node"], + nlmsg=event.params["nlmsg"], + t=event.time, + ), + } + try: + event_id = dispatcher[event.event_type]() + except KeyError as exc: + raise ValueError(f"unsupported event_type: {event.event_type}") from exc + events = runtime.list_events() + self.store.replace_events(self.run.run_id, events) + return runtime.get_event(event_id) + + def list_events(self): + runtime = self.ensure_runtime() + events = runtime.list_events() + self.store.replace_events(self.run.run_id, events) + return events + + def list_nodes(self, at_time: int = 0): + runtime = self.ensure_runtime() + nodes = [] + for name, node in sorted(runtime.nodes.items()): + ipv4 = ipv6 = None + if node.addr4 is not None: + ipv4 = node.addr4.compressed + if node.addr6 is not None: + ipv6 = node.addr6.compressed + nodes.append(TopologyNode( + name=name, + node_type=node.node_type.name.lower(), + position=list(runtime.get_position(name, at_time) or []), + neighbors=runtime.get_neighbors(name, at_time), + ground_stations=runtime.get_GSes(name, at_time), + ipv4=ipv4, + ipv6=ipv6, + )) + return nodes + + def list_tasks(self, node: Optional[str] = None): + runtime = self.ensure_runtime() + return runtime.list_tasks(node=node) + + def get_task(self, task_id: str): + runtime = self.ensure_runtime() + return runtime.get_task(task_id) + + def get_task_output(self, task_id: str): + runtime = self.ensure_runtime() + return runtime.get_task_output(task_id) + + +class RuntimeManager: + def __init__(self, store: MetadataStore): + self.store = store + self._runs: Dict[str, ManagedRun] = {} + self._lock = threading.Lock() + + def get_or_create(self, experiment: ExperimentRecord, run: RunRecord): + with self._lock: + managed = self._runs.get(run.run_id) + if managed is None: + managed = ManagedRun(experiment, run, self.store) + self._runs[run.run_id] = managed + return managed diff --git a/web/backend/app/core/store.py b/web/backend/app/core/store.py new file mode 100644 index 0000000..ac0df0f --- /dev/null +++ b/web/backend/app/core/store.py @@ -0,0 +1,168 @@ +import json +import os +import threading +import time +import uuid +from pathlib import Path +from typing import List, Optional + +from .models import ExperimentCreate, ExperimentRecord, ExperimentUpdate, RunRecord + + +class MetadataStore: + def __init__(self, metadata_path: str): + self.metadata_path = Path(metadata_path) + self.metadata_path.parent.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + + def _default_payload(self): + return { + "experiments": {}, + "runs": {}, + "events": {}, + } + + def _read(self): + if not self.metadata_path.exists(): + return self._default_payload() + with self.metadata_path.open("r", encoding="utf-8") as fh: + return json.load(fh) + + def _write(self, payload): + tmp_path = self.metadata_path.with_suffix(".tmp") + with tmp_path.open("w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2, sort_keys=True) + os.replace(tmp_path, self.metadata_path) + + def create_experiment(self, owner_user_id: str, data: ExperimentCreate, + config_path: str = "", experiment_id: Optional[str] = None): + now = time.time() + experiment = ExperimentRecord( + experiment_id=experiment_id or f"exp-{uuid.uuid4().hex[:12]}", + owner_user_id=owner_user_id, + name=data.name, + configuration=data.configuration, + config_path=config_path, + gs_lat_long=data.gs_lat_long, + bird_conf_content=data.bird_conf_content, + bird_conf_path=None, + extra_nodes_links=data.extra_nodes_links, + created_at=now, + updated_at=now, + ) + with self._lock: + payload = self._read() + payload["experiments"][experiment.experiment_id] = experiment.model_dump(mode="json") + self._write(payload) + return experiment + + def list_experiments(self, owner_user_id: str): + with self._lock: + payload = self._read() + experiments = [ + ExperimentRecord.model_validate(item) + for item in payload["experiments"].values() + if item["owner_user_id"] == owner_user_id + ] + experiments.sort(key=lambda item: item.created_at) + return experiments + + def get_experiment(self, experiment_id: str) -> Optional[ExperimentRecord]: + with self._lock: + payload = self._read() + item = payload["experiments"].get(experiment_id) + if item is None: + return None + return ExperimentRecord.model_validate(item) + + def update_experiment(self, experiment_id: str, patch: ExperimentUpdate): + with self._lock: + payload = self._read() + item = payload["experiments"].get(experiment_id) + if item is None: + return None + current = ExperimentRecord.model_validate(item) + updates = patch.model_dump(exclude_unset=True) + updated = ExperimentRecord.model_validate({ + **current.model_dump(mode="json"), + **updates, + "updated_at": time.time(), + }) + payload["experiments"][experiment_id] = updated.model_dump(mode="json") + self._write(payload) + return updated + + def update_experiment_fields(self, experiment_id: str, **updates): + with self._lock: + payload = self._read() + item = payload["experiments"].get(experiment_id) + if item is None: + return None + current = ExperimentRecord.model_validate(item) + updated = ExperimentRecord.model_validate({ + **current.model_dump(mode="json"), + **updates, + "updated_at": time.time(), + }) + payload["experiments"][experiment_id] = updated.model_dump(mode="json") + self._write(payload) + return updated + + def create_run(self, experiment: ExperimentRecord, artifact_dir: str): + now = time.time() + run = RunRecord( + run_id=f"run-{uuid.uuid4().hex[:12]}", + experiment_id=experiment.experiment_id, + owner_user_id=experiment.owner_user_id, + artifact_dir=artifact_dir, + created_at=now, + updated_at=now, + ) + with self._lock: + payload = self._read() + payload["runs"][run.run_id] = run.model_dump(mode="json") + payload["events"][run.run_id] = [] + self._write(payload) + return run + + def get_run(self, run_id: str) -> Optional[RunRecord]: + with self._lock: + payload = self._read() + item = payload["runs"].get(run_id) + if item is None: + return None + return RunRecord.model_validate(item) + + def list_runs(self, experiment_id: Optional[str] = None, owner_user_id: Optional[str] = None): + with self._lock: + payload = self._read() + runs = [RunRecord.model_validate(item) for item in payload["runs"].values()] + if experiment_id is not None: + runs = [item for item in runs if item.experiment_id == experiment_id] + if owner_user_id is not None: + runs = [item for item in runs if item.owner_user_id == owner_user_id] + runs.sort(key=lambda item: item.created_at) + return runs + + def update_run(self, run_id: str, **updates): + with self._lock: + payload = self._read() + item = payload["runs"].get(run_id) + if item is None: + return None + current = RunRecord.model_validate(item) + updated = current.model_copy(update={**updates, "updated_at": time.time()}) + payload["runs"][run_id] = updated.model_dump(mode="json") + self._write(payload) + return updated + + def replace_events(self, run_id: str, events: List[dict]): + with self._lock: + payload = self._read() + payload["events"][run_id] = events + self._write(payload) + + def list_events(self, run_id: str): + with self._lock: + payload = self._read() + return payload["events"].get(run_id, []) diff --git a/web/backend/app/main.py b/web/backend/app/main.py new file mode 100644 index 0000000..c42e036 --- /dev/null +++ b/web/backend/app/main.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +from .api.routes import router + + +app = FastAPI(title="StarryNet Control Plane", version="0.1.0") +app.include_router(router) From 5b1220c62600e8692b4f1f847f923a3aa3759746 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Sun, 10 May 2026 17:19:14 +0800 Subject: [PATCH 39/49] chore: track repository AGENTS guidance and ignore generated data Track the project AGENTS instructions in the repository. Ignore local editor files and generated backend data artifacts from the web control plane. --- .gitignore | 10 ++++++++++ AGENTS.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index 7ed0905..0669d96 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ Module.symvers Mkfile.old dkms.conf +# Python .DS_Store .idea __pycache__ @@ -58,4 +59,13 @@ build/ dist/ *.egg-info/ +# NodeJS +node_modules/ + +# VSCode +.vscode/ + starlink-*/ +web/backend/data/ +exp/ +test/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2fe9a53 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +## Project Overview + +StarryNet is a novel experimentation emulator that enables researchers to conveniently build credible and flexible experimental network environments (ENE) mimicking satellite dynamics and network behaviors of large-scale ISTNs. + +For more detailed information, refer to README.md + +### Building and Running + +Install python package `starrynet` and executables `sn` and `sn-worker`: +```bash +python3 setup.py install +``` + +Run local worker: +```bash +sudo sn-worker --workdir test --machine-id 0 --username abc --password 123456 +``` + +Run experiment: +```bash +python3 example.py +``` + +## Development + +### Repository Structure + +Focus on the following files and directories: +- `config.json`: sample topology and worker configuration +- `example.py`: Python API example +- `bin/`: entrypoints for CLI `sn` and worker `sn-worker` +- `starrynet/`: library code +- `web/` (DEVELOPING): web interface code + +### Rules + +- Planning before coding is highly recommended, especially for complex features. All plans should be documented in the `plans/` directory as markdown files to be reviewed by other developers and agents. Each plan should include the motivation, design, and implementation details. +- No compatibility consideration for now, feel free to refactor and break any APIs and data formats. +- Make the codebase and git history clean and organized. No agents/AI related signatures in commit messages. + +### Commit Message Format +``` +: + + +``` \ No newline at end of file From 7c37396199d68602c5ebbda8b6e8c1bbf5d712de Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 13 May 2026 17:40:44 +0800 Subject: [PATCH 40/49] feat: include topology links in backend topology API Return full topology snapshots from the run topology endpoint, including deduplicated links with link types and interface IPs. Add corresponding `TopologyLink` and `TopologySnapshot` pydantic models. --- web/backend/app/api/routes.py | 6 +---- web/backend/app/core/models.py | 17 ++++++++++++ web/backend/app/core/runtime.py | 46 +++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/web/backend/app/api/routes.py b/web/backend/app/api/routes.py index ab549a4..725ab32 100644 --- a/web/backend/app/api/routes.py +++ b/web/backend/app/api/routes.py @@ -170,11 +170,7 @@ def get_run_topology( run = _require_run(store, run_id, user_id) experiment = _require_experiment(store, run.experiment_id, user_id) managed = runtime_manager.get_or_create(experiment, run) - return { - "run_id": run_id, - "time": time, - "nodes": managed.list_nodes(at_time=time), - } + return managed.get_topology_snapshot(at_time=time) @router.get("/runs/{run_id}/nodes") diff --git a/web/backend/app/core/models.py b/web/backend/app/core/models.py index 89073b7..93b7261 100644 --- a/web/backend/app/core/models.py +++ b/web/backend/app/core/models.py @@ -107,3 +107,20 @@ class TopologyNode(BaseModel): ground_stations: List[str] = Field(default_factory=list) ipv4: Optional[str] = None ipv6: Optional[str] = None + + +class TopologyLink(BaseModel): + source: str + target: str + link_type: str + source_ipv4: Optional[str] = None + target_ipv4: Optional[str] = None + source_ipv6: Optional[str] = None + target_ipv6: Optional[str] = None + + +class TopologySnapshot(BaseModel): + run_id: str + time: int + nodes: List[TopologyNode] = Field(default_factory=list) + links: List[TopologyLink] = Field(default_factory=list) diff --git a/web/backend/app/core/runtime.py b/web/backend/app/core/runtime.py index 8c0b07a..052c6e3 100644 --- a/web/backend/app/core/runtime.py +++ b/web/backend/app/core/runtime.py @@ -6,7 +6,7 @@ from starrynet.sn_synchronizer import StarryNet -from .models import EventCreate, ExperimentRecord, RunRecord, RunStatus, TopologyNode +from .models import EventCreate, ExperimentRecord, RunRecord, RunStatus, TopologyLink, TopologyNode, TopologySnapshot from .store import MetadataStore @@ -155,8 +155,14 @@ def list_events(self): return events def list_nodes(self, at_time: int = 0): + return self.get_topology_snapshot(at_time).nodes + + def get_topology_snapshot(self, at_time: int = 0): runtime = self.ensure_runtime() + time_index = self._resolve_time_index(runtime, at_time) nodes = [] + links = [] + seen_links = set() for name, node in sorted(runtime.nodes.items()): ipv4 = ipv6 = None if node.addr4 is not None: @@ -172,7 +178,43 @@ def list_nodes(self, at_time: int = 0): ipv4=ipv4, ipv6=ipv6, )) - return nodes + + current_links = node.links_t[time_index] if time_index < len(node.links_t) else node.links_t[-1] + for dst_name, link in current_links.items(): + dedupe_key = tuple(sorted((name, dst_name))) + if dedupe_key in seen_links: + continue + seen_links.add(dedupe_key) + dst_node = runtime.nodes[dst_name] + reverse_link = dst_node.links_t[time_index].get(name) if time_index < len(dst_node.links_t) else dst_node.links_t[-1].get(name) + link_type = self._resolve_link_type(node.node_type.name.lower(), dst_node.node_type.name.lower()) + links.append(TopologyLink( + source=name, + target=dst_name, + link_type=link_type, + source_ipv4=link.addr4.compressed if link.addr4 is not None else None, + target_ipv4=reverse_link.addr4.compressed if reverse_link and reverse_link.addr4 is not None else None, + source_ipv6=link.addr6.compressed if link.addr6 is not None else None, + target_ipv6=reverse_link.addr6.compressed if reverse_link and reverse_link.addr6 is not None else None, + )) + return TopologySnapshot(run_id=self.run.run_id, time=at_time, nodes=nodes, links=links) + + def _resolve_time_index(self, runtime: StarryNet, at_time: int) -> int: + max_steps = max((len(node.links_t) for node in runtime.nodes.values()), default=1) + if max_steps <= 1: + return 0 + step = max(1, int(runtime.step)) + return max(0, min(at_time // step, max_steps - 1)) + + def _resolve_link_type(self, source_type: str, target_type: str) -> str: + node_types = {source_type, target_type} + if node_types == {"sat"}: + return "inter-satellite" + if node_types == {"gs", "sat"}: + return "ground-satellite" + if "extra" in node_types: + return "extra" + return "mixed" def list_tasks(self, node: Optional[str] = None): runtime = self.ensure_runtime() From b1cc5637903bfcd6b810dfc5ef4e29333c531196 Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 13 May 2026 17:43:10 +0800 Subject: [PATCH 41/49] fix: CORS configuration for frontend access --- web/backend/app/main.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/web/backend/app/main.py b/web/backend/app/main.py index c42e036..f91601e 100644 --- a/web/backend/app/main.py +++ b/web/backend/app/main.py @@ -1,7 +1,22 @@ +import os + from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from .api.routes import router app = FastAPI(title="StarryNet Control Plane", version="0.1.0") +cors_origin_regex = os.getenv("STARRYNET_CORS_ALLOW_ORIGIN_REGEX", ".*") +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://127.0.0.1:5173", + "http://localhost:5173", + ], + allow_origin_regex=cors_origin_regex, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) app.include_router(router) From 84f8b1b7285eebd3c94acd9aca99a6e41e82647e Mon Sep 17 00:00:00 2001 From: xiex386 <562837197@qq.com> Date: Wed, 13 May 2026 18:14:14 +0800 Subject: [PATCH 42/49] feat: add web frontend scaffold Add a Vite React frontend for browsing StarryNet experiments, runs, topology snapshots, events, and task outputs. Includes shared API models/hooks, route definitions, reusable UI components, page-level views, styling, frontend README, TypeScript config, and package metadata. Ignore generated TypeScript build info. --- .gitignore | 1 + web/frontend/README.md | 33 + web/frontend/index.html | 12 + web/frontend/package-lock.json | 1810 +++++++++++++++++ web/frontend/package.json | 23 + web/frontend/src/app/AppShell.tsx | 80 + web/frontend/src/components/Breadcrumbs.tsx | 23 + web/frontend/src/components/EmptyState.tsx | 13 + web/frontend/src/components/ErrorPanel.tsx | 12 + web/frontend/src/components/LoadingBlock.tsx | 3 + web/frontend/src/components/MetricCard.tsx | 13 + web/frontend/src/components/PageHeader.tsx | 33 + web/frontend/src/components/SectionNav.tsx | 34 + web/frontend/src/components/StatusPill.tsx | 8 + web/frontend/src/lib/api/client.ts | 73 + web/frontend/src/lib/format.ts | 27 + web/frontend/src/lib/hooks.ts | 36 + web/frontend/src/lib/models.ts | 103 + web/frontend/src/main.tsx | 37 + .../src/pages/CreateExperimentPage.tsx | 368 ++++ .../src/pages/ExperimentDetailPage.tsx | 223 ++ web/frontend/src/pages/ExperimentsPage.tsx | 95 + web/frontend/src/pages/RunDetailPage.tsx | 143 ++ web/frontend/src/pages/RunEventsPage.tsx | 75 + web/frontend/src/pages/RunTasksPage.tsx | 99 + web/frontend/src/pages/RunTopologyPage.tsx | 269 +++ web/frontend/src/routes.ts | 15 + web/frontend/src/styles.css | 994 +++++++++ web/frontend/src/vite-env.d.ts | 1 + web/frontend/tsconfig.app.json | 21 + web/frontend/tsconfig.json | 8 + web/frontend/vite.config.ts | 10 + 32 files changed, 4695 insertions(+) create mode 100644 web/frontend/README.md create mode 100644 web/frontend/index.html create mode 100644 web/frontend/package-lock.json create mode 100644 web/frontend/package.json create mode 100644 web/frontend/src/app/AppShell.tsx create mode 100644 web/frontend/src/components/Breadcrumbs.tsx create mode 100644 web/frontend/src/components/EmptyState.tsx create mode 100644 web/frontend/src/components/ErrorPanel.tsx create mode 100644 web/frontend/src/components/LoadingBlock.tsx create mode 100644 web/frontend/src/components/MetricCard.tsx create mode 100644 web/frontend/src/components/PageHeader.tsx create mode 100644 web/frontend/src/components/SectionNav.tsx create mode 100644 web/frontend/src/components/StatusPill.tsx create mode 100644 web/frontend/src/lib/api/client.ts create mode 100644 web/frontend/src/lib/format.ts create mode 100644 web/frontend/src/lib/hooks.ts create mode 100644 web/frontend/src/lib/models.ts create mode 100644 web/frontend/src/main.tsx create mode 100644 web/frontend/src/pages/CreateExperimentPage.tsx create mode 100644 web/frontend/src/pages/ExperimentDetailPage.tsx create mode 100644 web/frontend/src/pages/ExperimentsPage.tsx create mode 100644 web/frontend/src/pages/RunDetailPage.tsx create mode 100644 web/frontend/src/pages/RunEventsPage.tsx create mode 100644 web/frontend/src/pages/RunTasksPage.tsx create mode 100644 web/frontend/src/pages/RunTopologyPage.tsx create mode 100644 web/frontend/src/routes.ts create mode 100644 web/frontend/src/styles.css create mode 100644 web/frontend/src/vite-env.d.ts create mode 100644 web/frontend/tsconfig.app.json create mode 100644 web/frontend/tsconfig.json create mode 100644 web/frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index 0669d96..2157bae 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ dist/ # NodeJS node_modules/ +tsconfig.app.tsbuildinfo # VSCode .vscode/ diff --git a/web/frontend/README.md b/web/frontend/README.md new file mode 100644 index 0000000..9e53324 --- /dev/null +++ b/web/frontend/README.md @@ -0,0 +1,33 @@ +# StarryNet Frontend + +This directory contains the standalone web GUI scaffold for StarryNet. + +## Development + +Start the backend: + +```bash +uvicorn web.backend.app.main:app --reload +``` + +Start the frontend: + +```bash +cd web/frontend +npm install +npm run dev +``` + +The frontend expects: + +- `VITE_API_BASE_URL` pointing at the FastAPI backend +- `VITE_USER_ID` for the required `X-User-Id` header + +## Current scope + +- experiments list +- experiment detail +- run detail +- topology snapshot +- events list +- tasks list and output preview diff --git a/web/frontend/index.html b/web/frontend/index.html new file mode 100644 index 0000000..3fd9ae9 --- /dev/null +++ b/web/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + StarryNet + + +
+ + + diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json new file mode 100644 index 0000000..5bfccd8 --- /dev/null +++ b/web/frontend/package-lock.json @@ -0,0 +1,1810 @@ +{ + "name": "starrynet-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "starrynet-web", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 0000000..06bf02a --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "starrynet-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/web/frontend/src/app/AppShell.tsx b/web/frontend/src/app/AppShell.tsx new file mode 100644 index 0000000..53b44f2 --- /dev/null +++ b/web/frontend/src/app/AppShell.tsx @@ -0,0 +1,80 @@ +import { NavLink, Outlet, useLocation } from "react-router-dom"; + +import { appRoutes } from "../routes"; + +export function AppShell() { + const location = useLocation(); + const runMatch = location.pathname.match(/^\/runs\/([^/]+)/); + const experimentMatch = location.pathname.match(/^\/experiments\/([^/]+)/); + const runId = runMatch?.[1] ?? null; + const experimentId = + experimentMatch?.[1] && experimentMatch[1] !== "new" ? experimentMatch[1] : null; + + const workspaceLinks = [ + { label: "Experiments", to: appRoutes.experiments(), caption: "Browse and monitor" }, + { label: "New Experiment", to: appRoutes.experimentCreatePath() } + ]; + + const contextLinks = runId + ? [ + { label: "Topology", to: appRoutes.runTopologyPath(runId) }, + { label: "Events", to: appRoutes.runEventsPath(runId) }, + { label: "Tasks", to: appRoutes.runTasksPath(runId) } + ] + : experimentId + ? [{ label: "Experiment Overview", to: appRoutes.experimentDetailPath(experimentId) }] + : []; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/web/frontend/src/components/Breadcrumbs.tsx b/web/frontend/src/components/Breadcrumbs.tsx new file mode 100644 index 0000000..af1c766 --- /dev/null +++ b/web/frontend/src/components/Breadcrumbs.tsx @@ -0,0 +1,23 @@ +import { Link } from "react-router-dom"; + +export interface BreadcrumbItem { + label: string; + to?: string; +} + +interface BreadcrumbsProps { + items: BreadcrumbItem[]; +} + +export function Breadcrumbs({ items }: BreadcrumbsProps) { + return ( + + ); +} diff --git a/web/frontend/src/components/EmptyState.tsx b/web/frontend/src/components/EmptyState.tsx new file mode 100644 index 0000000..bfc88ca --- /dev/null +++ b/web/frontend/src/components/EmptyState.tsx @@ -0,0 +1,13 @@ +interface EmptyStateProps { + title: string; + body: string; +} + +export function EmptyState({ title, body }: EmptyStateProps) { + return ( +
+

{title}

+

{body}

+
+ ); +} diff --git a/web/frontend/src/components/ErrorPanel.tsx b/web/frontend/src/components/ErrorPanel.tsx new file mode 100644 index 0000000..dc3742c --- /dev/null +++ b/web/frontend/src/components/ErrorPanel.tsx @@ -0,0 +1,12 @@ +interface ErrorPanelProps { + message: string; +} + +export function ErrorPanel({ message }: ErrorPanelProps) { + return ( +
+

Request failed

+

{message}

+
+ ); +} diff --git a/web/frontend/src/components/LoadingBlock.tsx b/web/frontend/src/components/LoadingBlock.tsx new file mode 100644 index 0000000..1b3ac28 --- /dev/null +++ b/web/frontend/src/components/LoadingBlock.tsx @@ -0,0 +1,3 @@ +export function LoadingBlock() { + return
Loading...
; +} diff --git a/web/frontend/src/components/MetricCard.tsx b/web/frontend/src/components/MetricCard.tsx new file mode 100644 index 0000000..c1d7a4e --- /dev/null +++ b/web/frontend/src/components/MetricCard.tsx @@ -0,0 +1,13 @@ +interface MetricCardProps { + label: string; + value: string | number; +} + +export function MetricCard({ label, value }: MetricCardProps) { + return ( +
+

{label}

+

{value}

+
+ ); +} diff --git a/web/frontend/src/components/PageHeader.tsx b/web/frontend/src/components/PageHeader.tsx new file mode 100644 index 0000000..6171f2d --- /dev/null +++ b/web/frontend/src/components/PageHeader.tsx @@ -0,0 +1,33 @@ +import type { ReactNode } from "react"; + +import { BreadcrumbItem, Breadcrumbs } from "./Breadcrumbs"; + +interface PageHeaderProps { + title: string; + description?: string; + eyebrow?: string; + tone?: "default" | "experiments" | "experiment" | "run" | "topology" | "events" | "tasks" | "create"; + breadcrumbs?: BreadcrumbItem[]; + actions?: ReactNode; +} + +export function PageHeader({ + title, + description, + eyebrow, + tone = "default", + breadcrumbs, + actions +}: PageHeaderProps) { + return ( +
+
+ {breadcrumbs?.length ? : null} + {eyebrow ?

{eyebrow}

: null} +

{title}

+

{description}

+
+ {actions ?
{actions}
: null} +
+ ); +} diff --git a/web/frontend/src/components/SectionNav.tsx b/web/frontend/src/components/SectionNav.tsx new file mode 100644 index 0000000..f5a0801 --- /dev/null +++ b/web/frontend/src/components/SectionNav.tsx @@ -0,0 +1,34 @@ +import { NavLink } from "react-router-dom"; + +interface SectionNavItem { + label: string; + to: string; + description?: string; +} + +interface SectionNavProps { + title: string; + items: SectionNavItem[]; +} + +export function SectionNav({ title, items }: SectionNavProps) { + return ( +
+

{title}

+
+ {items.map(item => ( + + isActive ? "section-nav-link section-nav-link-active" : "section-nav-link" + } + > + {item.label} + {item.description ? {item.description} : null} + + ))} +
+
+ ); +} diff --git a/web/frontend/src/components/StatusPill.tsx b/web/frontend/src/components/StatusPill.tsx new file mode 100644 index 0000000..8f39ae7 --- /dev/null +++ b/web/frontend/src/components/StatusPill.tsx @@ -0,0 +1,8 @@ +interface StatusPillProps { + status: string; +} + +export function StatusPill({ status }: StatusPillProps) { + const tone = `status-pill status-${status.toLowerCase()}`; + return {status}; +} diff --git a/web/frontend/src/lib/api/client.ts b/web/frontend/src/lib/api/client.ts new file mode 100644 index 0000000..be46c72 --- /dev/null +++ b/web/frontend/src/lib/api/client.ts @@ -0,0 +1,73 @@ +import type { + EventRecord, + ExperimentCreatePayload, + ExperimentRecord, + RunRecord, + TaskRecord, + TopologySnapshot +} from "../models"; + +// FIXME +const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, "") ?? "http://127.0.0.1:8000"; +const USER_ID = (import.meta.env.VITE_USER_ID as string | undefined) ?? "demo-user"; + +export class ApiError extends Error { + status: number; + detail: unknown; + + constructor(message: string, status: number, detail: unknown) { + super(message); + this.status = status; + this.detail = detail; + } +} + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${API_BASE_URL}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + "X-User-Id": USER_ID, + ...(init?.headers ?? {}) + } + }); + + if (!response.ok) { + let detail: unknown = null; + try { + detail = await response.json(); + } catch { + detail = await response.text(); + } + throw new ApiError(`Request failed with status ${response.status}`, response.status, detail); + } + + if (response.status === 204) { + return undefined as T; + } + + return (await response.json()) as T; +} + +export const apiClient = { + listExperiments: () => request("/experiments"), + createExperiment: (payload: ExperimentCreatePayload) => + request("/experiments", { + method: "POST", + body: JSON.stringify(payload) + }), + getExperiment: (experimentId: string) => request(`/experiments/${experimentId}`), + listRunsForExperiment: (experimentId: string) => request(`/experiments/${experimentId}/runs`), + createRun: (experimentId: string) => + request(`/experiments/${experimentId}/runs`, { method: "POST" }), + getRun: (runId: string) => request(`/runs/${runId}`), + startRun: (runId: string) => request(`/runs/${runId}/start`, { method: "POST" }), + stopRun: (runId: string) => request(`/runs/${runId}/stop`, { method: "POST" }), + cleanupRun: (runId: string) => request(`/runs/${runId}/cleanup`, { method: "POST" }), + getTopology: (runId: string, time = 0) => + request(`/runs/${runId}/topology?time=${encodeURIComponent(String(time))}`), + listEvents: (runId: string) => request(`/runs/${runId}/events`), + listTasks: (runId: string) => request(`/runs/${runId}/tasks`), + getTaskOutput: (runId: string, taskId: string) => + request(`/runs/${runId}/tasks/${taskId}/output`) +}; diff --git a/web/frontend/src/lib/format.ts b/web/frontend/src/lib/format.ts new file mode 100644 index 0000000..d80a34c --- /dev/null +++ b/web/frontend/src/lib/format.ts @@ -0,0 +1,27 @@ +export function formatDateTime(value: number | null): string { + if (!value) { + return "Not available"; + } + return new Date(value * 1000).toLocaleString(); +} + +export function formatRelativeDuration(start: number | null, end: number | null): string { + if (!start) { + return "Not started"; + } + const endMs = end ? end * 1000 : Date.now(); + const durationMs = Math.max(0, endMs - start * 1000); + const seconds = Math.floor(durationMs / 1000); + const minutes = Math.floor(seconds / 60); + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } + return `${seconds}s`; +} + +export function formatCoordinates(points: number[][]): string { + if (!points.length) { + return "None"; + } + return points.map(([lat, lon]) => `${lat.toFixed(3)}, ${lon.toFixed(3)}`).join(" | "); +} diff --git a/web/frontend/src/lib/hooks.ts b/web/frontend/src/lib/hooks.ts new file mode 100644 index 0000000..8b5b03a --- /dev/null +++ b/web/frontend/src/lib/hooks.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; + +export function useAsyncData(load: () => Promise, deps: unknown[] = []) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + load() + .then(result => { + if (!cancelled) { + setData(result); + } + }) + .catch(err => { + if (!cancelled) { + setError(err instanceof Error ? err : new Error("Unknown error")); + } + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, deps); + + return { data, loading, error, reload: () => load().then(setData) }; +} diff --git a/web/frontend/src/lib/models.ts b/web/frontend/src/lib/models.ts new file mode 100644 index 0000000..8999b60 --- /dev/null +++ b/web/frontend/src/lib/models.ts @@ -0,0 +1,103 @@ +export type ExperimentStatus = "draft" | "ready" | "archived"; +export type RunStatus = "ready" | "provisioning" | "active" | "stopping" | "completed" | "failed" | "cleaned"; + +export interface ShellDefinition { + altitude_km: number; + inclination: number; + orbits: number; + satellites_per_orbit: number; + phase_shift: number; +} + +export interface ExperimentConfiguration { + shells: ShellDefinition[]; + duration_s: number; + step_s: number; + satellite_link_bandwidth_gbps: number; + sat_ground_bandwidth_gbps: number; + satellite_link_loss_percent: number; + sat_ground_loss_percent: number; + antenna_number: number; + antenna_elevation_angle: number; + satellite_link: string; + ip_version: string; + link_policy: string; + handover_policy: string; +} + +export interface ExperimentRecord { + experiment_id: string; + owner_user_id: string; + name: string; + configuration: ExperimentConfiguration; + config_path: string; + gs_lat_long: number[][]; + bird_conf_content: string | null; + bird_conf_path: string | null; + extra_nodes_links: Record; + status: ExperimentStatus; + created_at: number; + updated_at: number; +} + +export interface ExperimentCreatePayload { + name: string; + configuration: ExperimentConfiguration; + gs_lat_long: number[][]; + bird_conf_content: string; + extra_nodes_links: Record; +} + +export interface RunRecord { + run_id: string; + experiment_id: string; + owner_user_id: string; + status: RunStatus; + artifact_dir: string; + created_at: number; + updated_at: number; + started_at: number | null; + finished_at: number | null; + error: string | null; +} + +export interface TopologyNode { + name: string; + node_type: string; + position: number[] | null; + neighbors: string[]; + ground_stations: string[]; + ipv4: string | null; + ipv6: string | null; +} + +export interface TopologyLink { + source: string; + target: string; + link_type: string; + source_ipv4: string | null; + target_ipv4: string | null; + source_ipv6: string | null; + target_ipv6: string | null; +} + +export interface TopologySnapshot { + run_id: string; + time: number; + nodes: TopologyNode[]; + links: TopologyLink[]; +} + +export interface EventRecord { + event_id?: string; + time: number; + event_type: string; + params: Record; +} + +export interface TaskRecord { + task_id?: string; + node?: string; + status?: string; + [key: string]: unknown; +} diff --git a/web/frontend/src/main.tsx b/web/frontend/src/main.tsx new file mode 100644 index 0000000..8f08ec3 --- /dev/null +++ b/web/frontend/src/main.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom"; + +import { AppShell } from "./app/AppShell"; +import { CreateExperimentPage } from "./pages/CreateExperimentPage"; +import { ExperimentDetailPage } from "./pages/ExperimentDetailPage"; +import { ExperimentsPage } from "./pages/ExperimentsPage"; +import { RunDetailPage } from "./pages/RunDetailPage"; +import { RunEventsPage } from "./pages/RunEventsPage"; +import { RunTasksPage } from "./pages/RunTasksPage"; +import { RunTopologyPage } from "./pages/RunTopologyPage"; +import { appRoutes } from "./routes"; +import "./styles.css"; + +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { index: true, element: }, + { path: appRoutes.experiments(), element: }, + { path: appRoutes.experimentCreate(), element: }, + { path: appRoutes.experimentDetail(), element: }, + { path: appRoutes.runDetail(), element: }, + { path: appRoutes.runTopology(), element: }, + { path: appRoutes.runEvents(), element: }, + { path: appRoutes.runTasks(), element: } + ] + } +]); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/web/frontend/src/pages/CreateExperimentPage.tsx b/web/frontend/src/pages/CreateExperimentPage.tsx new file mode 100644 index 0000000..cb1278f --- /dev/null +++ b/web/frontend/src/pages/CreateExperimentPage.tsx @@ -0,0 +1,368 @@ +import { FormEvent, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { ErrorPanel } from "../components/ErrorPanel"; +import { PageHeader } from "../components/PageHeader"; +import { apiClient } from "../lib/api/client"; +import type { ExperimentCreatePayload } from "../lib/models"; +import { appRoutes } from "../routes"; + +function buildDefaultPayload(): ExperimentCreatePayload { + return { + name: "demo-constellation", + configuration: { + shells: [ + { + altitude_km: 550, + inclination: 53, + orbits: 72, + satellites_per_orbit: 22, + phase_shift: 1 + } + ], + duration_s: 120, + step_s: 2, + satellite_link_bandwidth_gbps: 10, + sat_ground_bandwidth_gbps: 10, + satellite_link_loss_percent: 1, + sat_ground_loss_percent: 1, + antenna_number: 1, + antenna_elevation_angle: 25, + satellite_link: "on", + ip_version: "ipv4", + link_policy: "least delay", + handover_policy: "instant handover" + }, + gs_lat_long: [ + [50.110924, 8.682127], + [46.6357, 14.311817] + ], + bird_conf_content: "", + extra_nodes_links: {} + }; +} + +export function CreateExperimentPage() { + const navigate = useNavigate(); + const [payload, setPayload] = useState(buildDefaultPayload); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const shell = payload.configuration.shells[0]; + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setSubmitting(true); + setError(null); + try { + const created = await apiClient.createExperiment(payload); + navigate(`/experiments/${created.experiment_id}`); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create experiment"); + } finally { + setSubmitting(false); + } + } + + return ( +
+ + + {error ? : null} + +
+
+ + + + + + + + + + + +
+ +

Primary shell

+
+ + + + + + + +
+ +

Ground stations

+
+ + + + + + + +
+ +