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 2af5ab0..72110f3 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,274 @@ # 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() +The exact system packages depend on your Linux distribution, but in practice you need: -This API creates initial network links for emulation. +- Python 3 +- `pip` +- (Optional) [BIRD](https://bird.network.cz/) +- reachable worker machines matching the `Machines` section in `config.json` (`127.0.0.1` recommended for initial trials) -> sn.run_routing_deamon() +Python packages are listed in `tools/requirements.txt`. -This API initiates the OSPF routing for the network, otherwise the network has no routing protocol running. +If you install StarryNet from this source tree with `python3 setup.py install`, you also need: -> sn.get_distance(node_index1, node_index2, time_index) +- a C compiler such as `gcc` +- `make` +- Python development headers for compiling the `pyctr` and `pynetlink` extensions +- StellarNet backend sources under `./stellarnet`, which `setup.py` builds into `libpreload.so` and `liblkl-posix.so` -This API returns distance between nodes at a certain time. +Those build dependencies are needed at install time, not for normal use after a successful install. -> 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. - -> sn.set_next_hop(sat, des, next_hop_sat, time_index) - -This API sets the next hop at a certain time. Sat, Des and NextHopSat are indexes and Sat and NextHopSat are neighbors. +## Installation -> sn.set_ping(node_index1, node_index2, time_index) +### Quick install: -This API will starts pinging msg of two nodes at a certain time. The output file could be found at the working directory. +```bash +bash ./install.sh +``` -> sn.set_perf(node_index1, node_index2, time_index) +### Manual install (recommended for explicit control): -This API will starts perfing msg of two nodes at a certain time. The output file could be found at the working directory. +1. Install system dependencies (Ubuntu example): -> sn.start_emulation() +```bash +sudo apt update +sudo apt install python3 python3-pip python3-dev gcc make bird +``` -This API starts the entire emulation of the duration. +2. (Optional) Create a Python virtual environment: -> sn.stop_emulation() +```bash +sudo apt install python3-venv +python3 -m venv sn-env +source sn-env/bin/activate +``` -This API stops the eimulation and clears the environment. +3. Install Python dependencies: -## Example one: use APIs in python +```bash +sudo python3 -m pip install -r tools/requirements.txt +``` -Run example.py to emulate the network. +4. Install the package and CLI: -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. +```bash +sudo python3 setup.py install +``` -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. -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. +## Quick Start -## Example two: use CLI in shell +### 1. Start a worker daemon -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. +Before running `example.py` or `sn`, start at least one worker daemon that matches the machine entry in `config.json`. -> sn +Example: -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. +```bash +sudo sn-worker \ + --workdir test \ + --machine-id 0 \ + --ssh-username abc \ + --ssh-password 123456 +``` -> sn -h +Important details: -> sn +- `--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 -*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.* +You can inspect all daemon options with: -> starrynet> help +```bash +sn-worker --help +``` -> starrynet> create_nodes +### 2. Update `config.json` -> starrynet> create_links +At minimum, review the `Machines` section: -> starrynet> run_routing_deamon +```json +"Machines": [ + { + "IP": "127.0.0.1", + "port": 18888, + "username": "abc", + "password": "123456" + } +] +``` -> starrynet> get_distance 1 2 10 +Other commonly changed fields: -*It means getting the distance of two node (#1 and #2) at #10 second.* +- `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` -> starrynet> get_neighbors 5 16 +### 3. Run the Python example -*It means getting the neighbor node indexes of node #5 at #16 second.* +```bash +python3 example.py +``` -> starrynet> get_GSes 7 20 +The example: -*It means getting the connected GS node indexes of node #6 at #20 second.* +- loads `./config.json` +- creates nodes and links +- queries topology state +- schedules ping, iperf, damage, recovery, and route dump events +- starts the emulation -> starrynet> get_position 7 23 +The example uses node names such as `SH1O1S1` and `GS0`. This is the naming style used by the current API and CLI. -*It means getting the LLA position of node #7 at #23 second.* +### 4. Run the interactive CLI -> starrynet> get_IP 8 +```bash +sn +``` -*It means getting the IP addresses of node #8. "create_nodes" and "create_links" must be runned before this.* +Useful options: -> starrynet> get_utility 27 +```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 memory and CPU utility information at #27 second. The output file will be generated at the working directory once the emulation starts.* +## CLI Workflow -> starrynet> set_damage 0.3 30 +Start with: -*It means setting a random damage of a given ratio of 0.3 at #30 second, which will be processed during emulation.* +```text +starrynet> create_nodes +starrynet> create_links +starrynet> run_routing_daemon +starrynet> start_emulation +``` -> starrynet> set_recovery 50 +Useful inspection commands: -*It means setting a recovery of the damages at #50 second, which will be processed during emulation.* +```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> check_routing_table 26 40 +Dynamic commands: -*It means listing the routing table of node #26 at #40 second. The output file will be written at the working directory.* +```text +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: + +- `run_routing_daemon` uses `./bird.conf` by default. +- You may pass a different BIRD config path: + +```text +starrynet> run_routing_daemon ./bird.conf +``` + +- You may restrict routing startup to selected nodes: -> starrynet> set_next_hop 1 26 2 45 +```text +starrynet> run_routing_daemon ./bird.conf GS0 SH1O1S1 SH1O1S2 +``` -*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.* +- Event commands are queued first and executed when `start_emulation` advances to the target time. + +## Python API Example -> starrynet> set_ping 1 26 46 +The current Python API looks like this: -*It means pinging from node #1 to node #26 at #46 second. The output file will be written at the working directory.* +```python +from starrynet.sn_synchronizer import StarryNet -> starrynet> set_perf 1 26 46 +gs_lat_long = [[50.110924, 8.682127], [46.635700, 14.311817]] +sn = StarryNet("./config.json", gs_lat_long) -*It means perfing from node #1 to node #26 at #46 second. The perfing output file will be written at the working directory.* +sn.create_nodes() +sn.create_links() +sn.run_routing_daemon("./bird.conf") -> starrynet> start_emulation +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")) -*"create_nodes", "create_links" and "run_routing_deamon" must be runned before this. +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() +``` + +## Output -> starrynet> stop_emulation +StarryNet writes generated files under a directory derived from the config location and experiment name, for example: + +```text +./starlink-Grid-LeastDelay/ +``` + +You can print the active output path in the CLI with: -> starrynet> exit +```text +starrynet> path +``` -*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 +## Known Gaps + +- 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..5f0155d --- a/bin/sn +++ b/bin/sn @@ -1,137 +1,84 @@ -#!/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 - -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') +import traceback - 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/bin/sn-worker b/bin/sn-worker new file mode 100755 index 0000000..c072ba3 --- /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_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("--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 + + +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()), + port=args.port, + username=args.username, + password=args.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/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/config.json b/config.json index 43374a6..add2a64 100644 --- a/config.json +++ b/config.json @@ -1,28 +1,32 @@ { "Name": "starlink", - "Altitude (km)": 550, - "Cycle (s)": 5731, - "Inclination": 53, - "Phase shift": 1, - "# of orbit": 5, - "# of satellites": 5, - "Duration (s)": 100, - "update_time (s)": 10, + "Shells": [ + { + "Altitude (km)": 550, + "Inclination": 53, + "Orbits": 5, + "Satellites per orbit": 5, + "Phase shift": 1 + } + ], + "Duration (s)": 10, + "step (s)": 1, "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 -} \ No newline at end of file + "Machines": [ + { + "IP": "127.0.0.1", + "port": 18888, + "username": "abc", + "password": "123456" + } + ] +} diff --git a/example.py b/example.py index ef41ccb..b9f81fe 100644 --- a/example.py +++ b/example.py @@ -6,90 +6,74 @@ """ from starrynet.sn_observer import * -from starrynet.sn_orchestrater import * from starrynet.sn_synchronizer import * if __name__ == "__main__": # Starlink 5*5: 25 satellite nodes, 2 ground stations. - # The node index sequence is: 25 sattelites, 2 ground stations. + # The node index sequence is: 25 satellites, 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 + ] # latitude and longitude of Frankfurt and Austria configuration_file_path = "./config.json" - hello_interval = 1 # 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, AS) + sn = StarryNet(configuration_file_path, GS_lat_long) + sn.create_nodes() sn.create_links() - sn.run_routing_deamon() - node_index1 = 1 - node_index2 = 2 - time_index = 2 + # LLA of a node at a certain time + 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(node_index1, node_index2, time_index) - print("node_distance (km): " + str(node_distance)) + node_distance = sn.get_distance(node1='SH1O1S1', node2='SH1O1S2', t=2) + print(f'\nSH1O1S1 - SH1O1S2 distance(km): {node_distance}') - # neighbor node indexes of node at a certain time - neighbors_index = sn.get_neighbors(node_index1, time_index) - print("neighbors_index: " + str(neighbors_index)) + # neighbor nodes at a certain time + neighbors = sn.get_neighbors(node='SH1O1S1', t=2) + print(f'\nSH1O1S1 neighbors: {neighbors}') # GS connected to the node at a certain time - node_index1 = 7 - GSes = sn.get_GSes(node_index1, time_index) - print("GSes are: " + str(GSes)) + GSes = sn.get_GSes(node='SH1O1S1', t=2) + print(f"\nSH1O1S1 GSes: {GSes}") - # LLA of a node at a certain time - LLA = sn.get_position(node_index1, time_index) - print("LLA: " + str(LLA)) + # CPU and memory usage + sn.check_utility(t=2) + + # IP addresses of a node + IPs = sn.get_IP(node='SH1O1S1') + print(f'\nSH1O1S1 IP addresses: {IPs}') + + # run OSPF daemon on all nodes + sn.run_routing_daemon(bird_conf_path=bird_conf_path) + # run OSPF daemon on selected nodes + # sn.run_routing_daemon( + # bird_conf_path=bird_conf_path, + # node_lst=['GS0', 'SH1O2S2', 'SH1O2S3', 'SH1O3S3', 'GS1'], + # ) - sn.get_utility(time_index) # CPU and memory useage + # set the next hop at a certain time. + sn.set_static_route(src='SH1O1S1', dst='SH1O1S2', next_hop='SH1O1S2', t=2) - # IPList of a node - IP_list = sn.get_IP(node_index1) - print("IP: " + str(IP_list)) + # 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) + + # perf msg of two nodes at a certain time. + sn.set_iperf(src='SH1O1S1', dst='SH1O1S2', t=5) - 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 - - node_index1 = 27 - 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) - - sat = 1 - des = 27 - next_hop_sat = 2 - 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 - 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) - for i in range(35, 80): - node_index1 = 26 - node_index2 = 27 - 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) - - node_index1 = 13 - node_index2 = 14 - 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_damage(damaging_ratio=0.3, t=6) + + # recover the damages at a certain time + sn.set_recovery(t=7) sn.start_emulation() - sn.stop_emulation() + + if input('clear environment?[y/n]').strip().lower()[:1] == 'y': + sn.clean() diff --git a/install.sh b/install.sh index 6010304..4908b53 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-pip python3-setuptools gcc make bird2 +elif [ "$DIST" = "Fedora" -o "$DIST" = "RedHatEnterpriseServer" ] || [ "$DIST" = "CentOS" ]; then + $install python3 python3-devel python3-pip python3-setuptools gcc make bird +else + echo "Unsupported distribution for automatic package installation." + echo "Install Python 3, pip, Python development headers, gcc, make and bird 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/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/setup.py b/setup.py index 77e031e..8f8b3eb 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,110 @@ -#!/usr/bin/env python -"Setuptools params" +#!/usr/bin/env python3 +"""Build and installation metadata for StarryNet.""" -from setuptools import setup, find_packages -from os.path import join - -# Get version number from source tree import sys +import shutil +import subprocess +from pathlib import Path + +from setuptools import Extension, find_packages, setup +from setuptools.command.build_ext import build_ext +from setuptools.command.build_py import build_py + + +ROOT = Path(__file__).resolve().parent +README = ROOT / "README.md" +REQUIREMENTS = ROOT / "tools" / "requirements.txt" +STELLARNET = ROOT / "stellarnet" +STARRYNET_PACKAGE = ROOT / "starrynet" +BACKEND_LIBS = ("libpreload.so", "liblkl-posix.so") + + +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 + + +def build_stellarnet_backend(): + if (STELLARNET / "Makefile").exists(): + if shutil.which("make") is None: + raise RuntimeError("Building the StellarNet backend requires make.") + subprocess.check_call(["make", "-C", str(STELLARNET)]) + for lib in BACKEND_LIBS: + src = STELLARNET / lib + dst = STARRYNET_PACKAGE / lib + if not src.exists(): + raise RuntimeError(f"StellarNet build did not produce {src}") + if dst.is_symlink(): + dst.unlink() + shutil.copy2(src, dst) + return -sys.path.append('.') + if all((STARRYNET_PACKAGE / lib).exists() for lib in BACKEND_LIBS): + return -scripts = [join('bin', filename) for filename in ['sn']] + missing = ", ".join(BACKEND_LIBS) + raise RuntimeError( + "StellarNet backend sources are missing. " + f"Expected {STELLARNET}/Makefile or prebuilt {missing} in " + f"{STARRYNET_PACKAGE}." + ) -modname = distname = 'starrynet' + +class BuildPyWithStellarNet(build_py): + def run(self): + build_stellarnet_backend() + super().run() + + +class BuildExtWithStellarNet(build_ext): + def run(self): + build_stellarnet_backend() + super().run() + +if sys.platform != "linux": + raise RuntimeError( + f"This package only supports Linux. " + f"Detected platform: {sys.platform}" + ) 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.*"]), + package_data={"starrynet": list(BACKEND_LIBS)}, + zip_safe=False, + python_requires=">=3.7", + install_requires=read_requirements(), + scripts=["bin/sn", "bin/sn-worker"], + cmdclass={ + "build_py": BuildPyWithStellarNet, + "build_ext": BuildExtWithStellarNet, + }, + ext_modules=[ + Extension("pyctr", [str(ROOT / "starrynet" / "pyctr.c")]), + ], + platforms=["Linux"], 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, + keywords="satellite network emulator constellation protocol", ) 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/cli.py b/starrynet/cli.py index 138817a..f6debdd 100644 --- a/starrynet/cli.py +++ b/starrynet/cli.py @@ -1,252 +1,485 @@ -""" -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 +import time -from starrynet.log import info, output, error -# from starrynet.term import makeTerms, runX11 -# from starrynet.util import ( quietRun, dumpNodeConnections, -# dumpPorts ) +from .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 _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): - "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" + " check_utility TIME\n" + " check_routing_table NODE TIME\n" + " set_damage RATIO TIME\n" + " set_recovery TIME\n" + " set_static_route SRC DST NEXT_HOP TIME\n" + " 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" + " 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"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"events: {len(self.sn.list_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} at {t} s: {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 of {args[0]} at {t} s: {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"connected ground stations of {args[0]} at {t} s: {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 of {args[0]} at {t} s: {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") - - 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_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 of {args[0]}: {ip_list}\n") + + 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 + event_id = self.sn.check_utility(t=t) + output(f"utility check scheduled: {event_id}\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 + 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): - "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 + event_id = self.sn.set_recovery(t=t) + output(f"recovery event scheduled: {event_id}\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 + 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( + 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 + 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): - "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 + 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") + 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 + 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): - "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_events(self, _line): + events = self.sn.list_events() + if not events: + output("No events.\n") + return + rows = [ + ( + item.get("event_id"), + item.get("time"), + item.get("type"), + item.get("status"), + item.get("result_mode"), + item.get("params"), + ) + for item in events + ] + 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) + 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 running.\n") + return + 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")), + ) + 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") + 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 + 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]") + 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): - "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/starrynet/pyctr.c b/starrynet/pyctr.c new file mode 100644 index 0000000..03de9e5 --- /dev/null +++ b/starrynet/pyctr.c @@ -0,0 +1,385 @@ +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +const int NS = CLONE_NEWNS|CLONE_NEWPID|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; +} + +static int set_err_msg(const char *prefix, char *msg_buf, size_t max_len) { + int err = errno; + snprintf(msg_buf, max_len, "%s: %s", prefix, strerror(err)); + return err; +} + +// in child process with new namespace +static int container_init( + const char* newroot, + const char* overlay_opt, + const char* hostname, + const char* preload_path, + const char* lib_path, + int err_fd + ) { + const char STDOUT_FILE[] = "stdout.log"; + const char STDERR_FILE[] = "stderr.log"; + int flags; + char env_preload[256], env_instance[256], env_lib[256]; + + flags = fcntl(err_fd, F_GETFD); + flags |= FD_CLOEXEC; + fcntl(err_fd, F_SETFD, flags); + + 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); + } + if(mount("sysfs", "/sys", "sysfs", MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL) != 0) { + return child_err("mount /sys failed: ", err_fd); + } + if(mount("none", "/dev", "devtmpfs", MS_NOSUID|MS_STRICTATIME, "mode=755") != 0) { + return child_err("mount /dev failed: ", err_fd); + } + // new session, detach to become a daemon process + if(setsid() < 0) { + return child_err("setsid failed: ", err_fd); + } + + freopen(STDOUT_FILE, "w", stdout); + freopen(STDERR_FILE, "w", stderr); + + // 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); + } + shm_unlink(hostname); + + if(snprintf(env_preload, sizeof(env_preload), "LD_PRELOAD=%s", preload_path) <= 0 + || putenv(env_preload) + || snprintf(env_instance, sizeof(env_instance), "LKL_INSTANCE=%s", hostname) <= 0 + || putenv(env_instance) + || snprintf(env_lib, sizeof(env_lib), "LKL_PATH=%s", lib_path) <= 0 + || putenv(env_lib)) { + 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, const char* hostname, const char* preload_path, const char* lib_path, + char *const* argv, char *err_msg, size_t max_len) { + int pid_fd, ret; + char env_preload[256], env_instance[256], env_lib[256]; + + pid_fd = syscall(SYS_pidfd_open, ctr_pid, 0); + if(pid_fd < 0) { + return set_err_msg("pidfd_open", err_msg, max_len); + } + + ret = setns(pid_fd, NS); + close(pid_fd); + if(ret != 0) { + return set_err_msg("setns", err_msg, max_len); + } + + if(snprintf(env_preload, sizeof(env_preload), "LD_PRELOAD=%s", preload_path) <= 0 + || putenv(env_preload) + || snprintf(env_instance, sizeof(env_instance), "LKL_INSTANCE=%s", hostname) <= 0 + || putenv(env_instance) + || snprintf(env_lib, sizeof(env_lib), "LKL_PATH=%s", lib_path) <= 0 + || putenv(env_lib)) { + return set_err_msg("put environment variables failed", err_msg, max_len); + } + + execvp(argv[0], &argv[0]); + return set_err_msg("execvp", err_msg, max_len); +} + +// 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, const char *preload_path, const char *lib_path, + 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); + + if(mount("none", "/", NULL, MS_PRIVATE|MS_REC, NULL) != 0) { + int err = errno; + snprintf(chd_err, max_len, "mount rprivate / failed: %s", strerror(err)); + return err; + } + // mount overlay + if(mount("overlay", new_root, "overlay", 0, overlay_opt) != 0) { + int err = errno; + snprintf(chd_err, max_len, "mount overlay failed: %s", strerror(err)); + return err; + } + // if(mount("none", newroot, NULL, MS_PRIVATE|MS_REC, NULL) != 0) { + // return child_err("mount rprivate newroot failed: ", err_fd); + // } + + 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, preload_path, lib_path, 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-1); + // 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_subprocess_exec( + pid_t ctr_pid, const char* hostname, const char* preload_path, const char* lib_path, + char *const* argv, char *err_msg, size_t max_len) { + int err_fds[2], err, flags; + pid_t pid; + ssize_t err_len; + + if(pipe(err_fds) != 0) { + set_err_msg("pipe", err_msg, max_len); + return -1; + } + + pid = fork(); + if(pid < 0) { + set_err_msg("fork", err_msg, max_len); + close(err_fds[0]), close(err_fds[1]); + return -1; + } else if(pid == 0) { // in child process + close(err_fds[0]); + flags = fcntl(err_fds[1], F_GETFD); + if(flags < 0) + exit(child_err("fcntl F_GETFD: ", err_fds[1])); + flags |= FD_CLOEXEC; + if(fcntl(err_fds[1], F_SETFD, flags) < 0) + exit(child_err("fcntl F_SETFD: ", err_fds[1])); + err = container_enter(ctr_pid, hostname, preload_path, lib_path, argv, err_msg, max_len); + // should not be executed if success + write(err_fds[1], err_msg, strlen(err_msg)); + exit(err); + } + close(err_fds[1]); + + err_len = read(err_fds[0], err_msg, max_len-1); + if(err_len > 0) { + err_msg[err_len] = '\0'; + waitpid(pid, NULL, 0); + pid = 0; + } + close(err_fds[0]); + return pid; +} + +// ========================Python wrapper======================== + +static PyObject *container_run(PyObject *self, PyObject *args) { + const char *base_dir = NULL; + const char *hostname = NULL; + const char *preload_path = NULL; + const char *lib_path = NULL; + char chd_err[256]; + int pid; + + if (!PyArg_ParseTuple(args, + "ssss:container_run(base_dir, hostname, preload_path, lib_path)", + &base_dir, &hostname, &preload_path, &lib_path)) + return NULL; + + pid = container_run_inner(base_dir, hostname, preload_path, lib_path, chd_err, sizeof(chd_err)); + 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; + const char *hostname = NULL; + const char *preload_path = NULL; + const char *lib_path = NULL; + PyObject *cmdline; + int no_return = 0; + Py_ssize_t argc; + char **argv; + char err_msg[256]; + + if(!PyArg_ParseTuple( + args, + "isssO|p:container_exec(container_pid, hostname, preload_path, lib_path, cmdline, no_return)", + &pid, &hostname, &preload_path, &lib_path, &cmdline, &no_return)) + 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; + + if(no_return) { + container_enter(pid, hostname, preload_path, lib_path, argv, err_msg, sizeof(err_msg)); + // should not be executed if success + PyErr_SetString(PyExc_OSError, err_msg); + } else { + int sub_pid = container_subprocess_exec(pid, hostname, preload_path, lib_path, argv, err_msg, sizeof(err_msg)); + free(argv); + + if(sub_pid < 0) { + PyErr_SetString(PyExc_OSError, err_msg); + } else if(sub_pid == 0) { + PyErr_SetString(PyExc_ChildProcessError, err_msg); + } else { + return PyLong_FromLong(sub_pid); + } + } + + 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_daemon_client.py b/starrynet/sn_daemon_client.py new file mode 100644 index 0000000..83e5f99 --- /dev/null +++ b/starrynet/sn_daemon_client.py @@ -0,0 +1,402 @@ +#!/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: { + "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", + "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 check_utility(self): + command = { + 'c': 'utility', + 't': time.time(), + 'p': {} + } + 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 + } + } + 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 + } + } + 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(), + } + 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, + } + } + 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, + } + } + return self._send_command_via_ssh(command) + + def iperf_batch(self, iperf_cmds): + command = { + 'c': 'iperf', + 't': time.time(), + 'p': { + '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, + } + } + return self._send_command_via_ssh(command) + + def netlink_batch(self, nl_cmds): + command = { + 'c': 'netlink', + 't': time.time(), + 'p': { + '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, + } + } + 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 = { + 'c': 'clean', + 't': time.time(), + 'p': {} + } + 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 3209114..7bef2a7 100644 --- a/starrynet/sn_observer.py +++ b/starrynet/sn_observer.py @@ -1,265 +1,99 @@ #encoding: utf-8 -import math +import os +import datetime +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 +def _dist_km_to_delay_ms(dist): + return dist / (17.31 / 29.5 * 299792.458) * 1000 -# To calculate the connection between satellites and GSes in time_in -# fac_num: number of GSes +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 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] + 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) -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 _sat_name(shell_id, orbit_id, sat_id): + return f'SH{shell_id+1}O{orbit_id+1}S{sat_id+1}' - 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 _gs_name(gid): + return f'GS{gid}' - 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 _isl_grid(sat_cbf_t, shell_id, orbit_num, sat_num): + # [[ [isl] for every satellite] for every t] + isls_lst_t = [] - 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 + 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): + isls_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 - 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 - 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") + isls = [] + # to avoid duplication at small scale + if sat_num > 2 and down_sid > 0: + # (sat_name, delay in ms) + 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 - 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 +def _topo_walker_delta(dir, duration, step, shell_lst): + ts_total = int(duration / step) + 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 - 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 + orbit_number, sat_number = shell['orbit'], shell['sat'] + num_of_sat = orbit_number * sat_number - 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 / + 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 - 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 +106,192 @@ 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) - t_ts = ts.utc(*cur.timetuple()[:5], - range(duration)) # [:4]:minute,[:5]:second + 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(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) + 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, 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') + 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) - 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]) + topo_t_shell.append((name_lst, sat_cbf_t, sat_lla_t, isls_t)) + return topo_t_shell - if len(self.GS_lat_long) != 0: - fac_cbf = self.to_cbf(self.GS_lat_long, len(self.GS_lat_long)) +def _topo_arbitrary(dir, duration, step, shell_lst): + #TODO: new format + 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') + 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) - 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) + if len(name_lst) == 0: + name_lst = sat_names + elif len(name_lst) != len(sat_names): + raise RuntimeError("satellites change between slots!") - 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 + 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 + +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 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) + 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_dist + sat_indices = np.arange(len(gs_dist))[bound_mask] + gs_dist = gs_dist[bound_mask] + sorted_sat = gs_dist.argsort() + gsls.append([ + # (sat_id, delay in ms) + (name_lst[sat_indices[sat]], + _dist_km_to_delay_ms(gs_dist[sat])) + for sat in sorted_sat[:antenna_num] + ]) + gsls_t.append(gsls) + gsls_t_shell.append(gsls_t) + + # merge different shell + # [[gsls for every shell] for every gs] for every t] + gsls_t = [ + [list() for gid in range(len(gs_cbf))] for t in range(len(gsls_t_shell[0])) + ] + for t, gsls in enumerate(gsls_t): + for gid, gsl_lst in enumerate(gsls): + for shell_id in range(len(gsls_t_shell)): + for sat_name, delay in gsls_t_shell[shell_id][t][gid]: + if len(gsl_lst) >= antenna_num: + break + gsl_lst.append((sat_name, delay)) + return gsls_t + +def load_pos(path): + f = open(path, 'r') + lla_dict = {} + for line in f: + toks = line.strip().split(':') + lla = tuple(map(float, toks[1].split(','))) + lla_dict[toks[0]] = lla + f.close() + return lla_dict + +def load_links_dict(path): + f = open(path, 'r') + links_dict = {} + for line in f: + 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 links_dict - 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) +#TODO: More ISL styles +topo_styles = { + 'Grid': _topo_walker_delta, + 'Arbitrary': _topo_arbitrary, +} - 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))) +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: - 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) + 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 + ) - return error +#TODO: More GSL styles +gsl_styles = { + 'LeastDelay':_gsl_least_delay, +} diff --git a/starrynet/sn_orchestrater.py b/starrynet/sn_orchestrater.py deleted file mode 100644 index d677469..0000000 --- a/starrynet/sn_orchestrater.py +++ /dev/null @@ -1,589 +0,0 @@ -import os -import threading -import sys -from time import sleep -import numpy -""" -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) -""" - - -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] - - -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] - f.close() - return ADJ - - -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): - # 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): - 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) - # 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) - # 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): - 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]) + - " 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): - damage_threads = [] - 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() - - -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): - os.system("docker exec -d " + - str(container_id_list[damaged_satellite]) + - " 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): - os.system('docker network rm ' + network_name) - - -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() - - -def sn_recover(damage_list, container_id_list, sat_loss): - recover_threads = [] - 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 = [] - 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() - 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]) + - " 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]) + - " 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]) + - " 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]) + - " 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() diff --git a/starrynet/sn_orchestrator.py b/starrynet/sn_orchestrator.py new file mode 100644 index 0000000..9c14547 --- /dev/null +++ b/starrynet/sn_orchestrator.py @@ -0,0 +1,415 @@ +#!/usr/bin/python3 +import os +import subprocess +import time +import socket +import ipaddress +import threading +from typing import Dict, List, Optional + +try: + import pyctr +except ModuleNotFoundError as exc: + raise RuntimeError( + "StarryNet C extension pyctr is not installed. " + "Reinstall the package or rebuild the extension before running." + ) from exc + + +MODULE_DIR = os.path.dirname(__file__) +REPO_DIR = os.path.dirname(MODULE_DIR) + + +def _resolve_backend_path(env_name, *candidates): + env_path = os.environ.get(env_name) + if env_path: + return env_path + for path in candidates: + if os.path.exists(path): + return path + return candidates[0] + + +PRELOAD_PATH = _resolve_backend_path( + 'STELLARNET_PRELOAD_PATH', + os.path.join(MODULE_DIR, 'libpreload.so'), + os.path.join(REPO_DIR, 'stellarnet', 'libpreload.so'), +) +LIB_PATH = _resolve_backend_path( + 'STELLARNET_LIB_PATH', + os.path.join(MODULE_DIR, 'liblkl-posix.so'), + os.path.join(REPO_DIR, 'stellarnet', 'liblkl-posix.so'), +) + + +class NetworkManageSession: + def __init__(self, node_dir, name): + sock_path = os.path.abspath(f'{node_dir}/rootfs/{name}') + self.sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sk.connect(sock_path) + + def shutdown(self): + self.sk.close() + + def _readline(self): + data = bytearray() + while not data.endswith(b'\n'): + chunk = self.sk.recv(1) + if not chunk: + break + data.extend(chunk) + return bytes(data) + + def _read_int(self): + response = self._readline().strip() + if not response: + raise RuntimeError("backend did not return a response") + return int(response) + + def _expect_ok(self, command): + result = self._read_int() + if result < 0: + raise RuntimeError(f"backend command {command} failed: {result}") + return result + + def register_if(self): + self.sk.send(b'A\n') + if_idx = self._read_int() + if if_idx < 0: + raise RuntimeError("backend failed to register NIC") + return if_idx + + def connect_peer(self, if_idx, peer_name, peer_if_idx): + self.sk.send(f'L {if_idx} {peer_name} {peer_if_idx}\n'.encode()) + self._expect_ok('L') + + def if_up(self, if_idx): + self.sk.send(f'X {if_idx} 1\n'.encode()) + self._expect_ok('X') + + def if_down(self, if_idx): + self.sk.send(f'X {if_idx} 0\n'.encode()) + self._expect_ok('X') + + def modify_addr(self, if_idx, addr4str, addr6str): + self.sk.send(f'I {if_idx} {addr4str} {addr6str}\n'.encode()) + self._expect_ok('I') + + def traffic_control(self, if_idx, delay, bw, loss): + self.sk.send(f'U {if_idx} {delay} {loss} {bw}\n'.encode()) + self._expect_ok('U') + + def disconnect_peer(self, if_idx): + self.sk.send(f'X {if_idx} 0\n'.encode()) + self._expect_ok('X') + self.sk.send(f'D {if_idx}\n'.encode()) + self._expect_ok('D') + + +class ManagedProcess: + def __init__(self, pid: int): + self.pid = pid + self.returncode: Optional[int] = None + + def poll(self): + if self.returncode is not None: + return self.returncode + try: + waited_pid, status = os.waitpid(self.pid, os.WNOHANG) + except ChildProcessError: + self.returncode = 0 + return self.returncode + if waited_pid == 0: + return None + self.returncode = os.waitstatus_to_exitcode(status) + return self.returncode + + def wait(self): + if self.returncode is not None: + return self.returncode + try: + _, status = os.waitpid(self.pid, 0) + self.returncode = os.waitstatus_to_exitcode(status) + except ChildProcessError: + self.returncode = 0 + return self.returncode + + +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 Node: + def __init__(self, name: str, node_dir: str, node_id: int = 0): + self.name = name + self.node_dir = node_dir + self.node_id = node_id + self.pid = pyctr.container_run(node_dir, name, PRELOAD_PATH, LIB_PATH) + self.session: Optional[NetworkManageSession] = None + self.idle_links: List[NetInterface] = [] + self.peer2link: Dict[str, NetInterface] = {} + self._exec_lock = threading.Lock() + + def __lt__(self, other): + return self.pid < other.pid + + def __del__(self): + try: + if self.session: + self.session.shutdown() + os.kill(self.pid, 9) + except Exception: + pass + + def _ensure_session(self): + if self.session is None: + self.session = NetworkManageSession(self.node_dir, self.name) + + def init_loopback(self): + self._ensure_session() + + 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) + + self.session.modify_addr(lo_link.if_idx, addr4.compressed, addr6.compressed) + self.session.if_up(lo_link.if_idx) + self.peer2link['lo'] = lo_link + return addr4.compressed, addr6.compressed + + def register_if(self, peer_name: str): + self._ensure_session() + if self.idle_links: + link = self.idle_links.pop() + link.ipv4 = None + link.ipv6 = None + else: + link = NetInterface(self.session.register_if()) + self.peer2link[peer_name] = link + return link.if_idx + + def connect_if(self, peer_name: str, peer_if_idx: int): + self._ensure_session() + link = self.peer2link.get(peer_name) + if link is None: + return + self.session.connect_peer(link.if_idx, peer_name, peer_if_idx) + + def init_if(self, peer_name: str, addr4: str, addr6: str, delay: str, bw: str, loss: str): + link = self.peer2link.get(peer_name) + if link is None: + return + + addr4 = ipaddress.IPv4Interface(addr4) + addr6 = ipaddress.IPv6Interface(addr6) + self._ensure_session() + self.session.modify_addr(link.if_idx, addr4.compressed, addr6.compressed) + self.session.traffic_control(link.if_idx, delay, bw, loss) + self.session.if_up(link.if_idx) + link.ipv4 = addr4 + link.ipv6 = addr6 + + def update_if(self, peer_name: str, delay: str, bw: str, loss: str): + link = self.peer2link.get(peer_name) + if link is None: + return + self._ensure_session() + self.session.traffic_control(link.if_idx, delay, bw, loss) + + def del_if(self, peer_name: str): + link = self.peer2link.pop(peer_name, None) + if link is None: + return + self._ensure_session() + self.session.disconnect_peer(link.if_idx) + self.idle_links.append(link) + + def run_command(self, command, stdout=None, stderr=None): + cmdline = tuple(arg.encode() for arg in command) + with self._exec_lock: + saved_stdout = os.dup(1) if stdout is not None else None + saved_stderr = os.dup(2) if stderr is not None else None + try: + if stdout is not None: + os.dup2(stdout, 1) + if stderr is not None and stderr != subprocess.STDOUT: + os.dup2(stderr, 2) + elif stderr == subprocess.STDOUT and stdout is not None: + os.dup2(stdout, 2) + pid = pyctr.container_exec( + self.pid, self.name, PRELOAD_PATH, LIB_PATH, cmdline + ) + finally: + if saved_stdout is not None: + os.dup2(saved_stdout, 1) + os.close(saved_stdout) + if saved_stderr is not None: + os.dup2(saved_stderr, 2) + os.close(saved_stderr) + return ManagedProcess(pid) + + +class OrchestratorContext: + def __init__(self, workdir): + self.workdir = workdir + self.nodes: Dict[str, Node] = {} + self.damage_lst: List[Node] = [] + + def clean(self): + for node in self.nodes.values(): + del node + self.nodes.clear() + self.damage_lst.clear() + + def init_nodes(self, base_dir, node_configs): + self.clean() + 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) + + time.sleep(5) + return { + node_name: node.init_loopback() + for node_name, node in self.nodes.items() + } + + def add_link_intra_machine( + self, + name1: str, name2: str, + src_addr4: str, src_addr6: str, + dst_addr4: str, dst_addr6: str, + 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 + + src_ifidx = node1.register_if(name2) + dst_ifidx = node2.register_if(name1) + node1.connect_if(name2, dst_ifidx) + node2.connect_if(name1, src_ifidx) + 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, + ): + raise NotImplementedError("StellarNet inter-machine links are not implemented yet") + + 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') + nodes_lst = self.nodes.keys() if nodes == 'all' else 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', 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 + + dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed + next_hop_link = next_hop_node.peer2link.get(src) + if next_hop_link is None or next_hop_link.ipv4 is None: + return + via_addr = next_hop_link.ipv4.ip.compressed + proc = src_node.run_command(( + 'ip', 'route', 'replace', f'{dst_addr}/32', 'via', via_addr + )) + proc.wait() + + 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 None + + dst_addr = dst_node.peer2link['lo'].ipv4.ip.compressed + return src_node, ('ping', '-c', '4', '-i', '0.01', *extra_args, dst_addr) + + 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 + return ( + (dst_node, ('iperf3', '-s', '-1', *dst_args)), + (src_node, ('iperf3', '-c', dst_addr, *src_args)), + ) + + def get_exec_command(self, node_name: str, cmd: str): + node = self.nodes.get(node_name) + if node is None: + return None + return node, ('sh', '-lc', cmd) + + def netlink(self, routes): + raise NotImplementedError("Raw netlink injection is not available on the StellarNet backend") + + def check_route(self, node_name: str): + node = self.nodes.get(node_name) + if node is None: + return '' + + output_path = os.path.join(self.workdir, f'route-{node_name}.txt') + fd = os.open(output_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) + try: + task = node.run_command(('ip', 'route'), stdout=fd, stderr=subprocess.STDOUT) + task.wait() + finally: + os.close(fd) + with open(output_path, 'r') as f: + return f.read() + + def damage(self, random_list: List[str]): + for node_name in random_list: + node = self.nodes.get(node_name) + if node is None: + continue + for peer_name, link in node.peer2link.items(): + if peer_name == 'lo': + continue + node.session.if_down(link.if_idx) + self.damage_lst.append(node) + + def recover(self): + for node in self.damage_lst: + for peer_name, link in node.peer2link.items(): + if peer_name == 'lo': + continue + try: + node.session.if_up(link.if_idx) + except Exception: + 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) + 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..596d789 --- /dev/null +++ b/starrynet/sn_orchestrator_daemon.py @@ -0,0 +1,805 @@ +#!/usr/bin/python3 +import os +import subprocess +import json +import time +import socket +import threading +import resource +import logging +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 + +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 + +class CommandStatus(Enum): + SUCCESS = "success" + 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): + 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, + port=DEFAULT_PORT, username='starrynet', password='123456'): + self.workdir = workdir or os.path.curdir + self.machine_id = machine_id + 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, + 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.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') + + 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.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 + + 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.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}") + 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 _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) + else: + return { + "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, + "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}") + + 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_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}) + 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() + 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}") + + 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 + + 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, 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_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}) + 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() + + 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) + if 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_addr4, src_addr6 = link[3], link[4] + dst_addr4, dst_addr6 = link[5], link[6] + 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_addr4, src_addr6, + 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_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_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)}') diff --git a/starrynet/sn_synchronizer.py b/starrynet/sn_synchronizer.py index 0a8a711..3fc761c 100644 --- a/starrynet/sn_synchronizer.py +++ b/starrynet/sn_synchronizer.py @@ -4,237 +4,861 @@ 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) """ -from starrynet.sn_observer import * -from starrynet.sn_utils import * +import time +import threading +import math +import os +import glob +import random +import ipaddress +import heapq +from enum import Enum +from collections import defaultdict +from typing import List, Tuple, Dict, Any +from dataclasses import dataclass, field +from .sn_observer import * +from .sn_utils import * +from .sn_daemon_client import SSHDaemonClient +EXTRA_LINK_DELAY = 1 # ms, for extra nodes connected to GS + +class NodeType(Enum): + SAT = 1 + GS = 2 + EXTRA = 3 + +@dataclass +class LinkInfo: + dst: str + addr4: ipaddress.IPv4Interface + addr6: ipaddress.IPv6Interface + +@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) + addr4: ipaddress.IPv4Address = None + addr6: ipaddress.IPv6Address = None + worker: SSHDaemonClient = None + +@dataclass +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(): - def __init__(self, - configuration_file_path, - GS_lat_long, - hello_interval=10, - AS=[]): + 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, 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 + 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.IP_version = sn_args.IP_version self.link_policy = sn_args.link_policy - self.update_interval = sn_args.update_interval + self.IP_version = sn_args.IP_version + self.step = sn_args.step 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.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.damage_list = [] - self.recovery_time = [] - self.route_src = [] - self.route_time = [] - - # 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) + self.experiment_name = sn_args.cons_name\ + +'-'+ sn_args.link_style +'-'+ sn_args.link_policy + self.gs_dirname = 'GS' + 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) + self._init_local() + + # 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: 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')): + 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) + + 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] + dst_node = nodes[dst] + dst_link = dst_node.links_t[t][sat_name] + changes_t[t]['add'].append(( + sat_name, dst, f'{delay:.2f}', + src_link.addr4.compressed, src_link.addr6.compressed, + 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] + dst_node = nodes[dst] + dst_link = dst_node.links_t[t][gs_name] + changes_t[t]['add'].append(( + gs_name, dst, f'{delay:.2f}', + src_link.addr4.compressed, src_link.addr6.compressed, + 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] + dst_link = dst_node.links_t[0][name] + changes_t[0]['add'].append(( + name, dst, f'{EXTRA_LINK_DELAY:.2f}', + src_link.addr4.compressed, src_link.addr6.compressed, + 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 + node_mid_dict = {} + assigned_shell_lst = [] + if len(sat_names_shell) * 2 <= len(machine_lst): + # need intra-shell partition + 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, worker 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 = [] + 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 + for i, worker in enumerate(machine_lst): + shell_num = shell_per_machine + if i < remainder: + shell_num += 1 + assigned_shells = [ + (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 + for shell_name, sat_names in assigned_shells: + for sat_name in sat_names: + node_mid_dict[sat_name] = i + assigned_shell_lst.append(assigned_shells) + shell_id += shell_num + + # TODO: better ground station assign + 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, + } + + 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.config_json = assign_obj 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() + self.undamaged_lst = list() + self.total_sat_lst = list() + 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("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.") - - 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.") - - 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 - - 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 - - def get_GSes(self, sat_index, time_index): + print('Initializing links using pre-computed topology data...') + thread_lst = [] + begin = time.time() + + # 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=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, 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', bird_conf) + print("Routing daemon initialized. Wait 30s for route converged") + else: + rtd_lsts = defaultdict(list) + for name in node_lst: + node = self.nodes.get(name) + if node is not None: + rtd_lsts[node.worker].append(name) + for worker, names in rtd_lsts.items(): + worker.init_routing(','.join(names), bird_conf) + + for i in range(30): + print(f'\r{i} / 30', end=' ') + 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 _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) + 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 {} + + # static information + 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 + + 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, 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, node, t): + node = self.nodes.get(node) + if node is None: + return [] + + tid = t // self.step 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) + 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_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 - f = open(path) - ADJ = f.readlines() - 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 - - def set_damage(self, damaging_ratio, time_index): - self.damage_ratio.append(damaging_ratio) - self.damage_time.append(time_index) - - def set_recovery(self, time_index): - self.recovery_time.append(time_index) - - def check_routing_table(self, sat_index, time_index): - self.route_src.append(sat_index) - self.route_time.append(time_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_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_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 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, node): + node = self.nodes.get(node) + if node is None: + return () + return node.addr4.compressed, node.addr6.compressed + + # 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 check_utility(self, t): + return self._queue_event('check_utility', t, result_mode='inline') + + 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): + 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) + 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): + return self._queue_event('recovery', t) + + 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): + return self._queue_event( + 'static_route', + t, + params={'src': src, 'dst': dst, 'next_hop': next_hop}, + ) + + def set_netlink(self, node, nlmsg, t): + return self._queue_event( + 'netlink', + t, + params={'node': node, 'nlmsg': nlmsg}, + ) + + def set_ping(self, src, dst, t, 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 = []): + 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): + 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): + 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): - # 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() - - 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() + start_time = time.time() + print('Tick event at 0 s') + self._event(0) + + tid = 1 + 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("\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("Trigger events at", t, "s ...") + self._event(t) + end = time.time() + elapsed = end - start + print(elapsed, "s elapsed,", update_end-start, "s for network update") + tid += 1 + + def clean(self): + print("Removing containers and links...") + for worker in self.worker_lst: + worker.clean() + print("All containers and links removed.") diff --git a/starrynet/sn_utils.py b/starrynet/sn_utils.py index a0b9ae3..5b67986 100644 --- a/starrynet/sn_utils.py +++ b/starrynet/sn_utils.py @@ -1,905 +1,68 @@ import os -import threading 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') +def sn_load_file(path): + 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', + parser.add_argument('--step', 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['step (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)']) 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 = 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 - -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") - - -# 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): - result = sn_remote_cmd(remote_ssh, "vmstat") - f = open(file_path + "/utility-info" + "_" + str(time_index) + ".txt", "w") - f.writelines(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") - 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") - - -def sn_damage(ratio, damage_list, constellation_size, remote_ssh, remote_ftp, - file_path, configuration_file_path): - 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) - print("Damage done.\n") - - -def sn_recover(damage_list, sat_loss, remote_ssh, remote_ftp, file_path, - configuration_file_path): - 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() - 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_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'") - 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: - 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}'") - - 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() - - -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]) + - " 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) + - " 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 10." + str(address_16_23) + - "." + str(address_8_15) + ".50") - 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:]") - 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]) + - " 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) + - " 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") - - -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) - - -# A thread designed for stopping the emulation. -class sn_Emulation_Stop_Thread(threading.Thread): + return remote_ssh.exec_command(cmd)[1].read().decode().strip() - 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 sn_remote_wait_output(remote_ssh, cmd): + for line in remote_ssh.exec_command(cmd, get_pty=True)[1]: + print(line, end='') - 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") 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