diff --git a/docs/tutorials/common/grpc.md b/docs/tutorials/common/grpc.md new file mode 100644 index 00000000..ad76c81d --- /dev/null +++ b/docs/tutorials/common/grpc.md @@ -0,0 +1,22 @@ +## gRPC Python Scripts + +You can also run the same steps above, using the provided gRPC script versions of scenarios. +Below are the steps to run and join one of these scenario, then you can continue with +the remaining steps of a given section. + +1. Make sure the CORE daemon is running a terminal, if not already + ``` shell + sudop core-daemon + ``` +2. From another terminal run the tutorial python script, which will create a session to join + ``` shell + /opt/core/venv/bin/python scenario.py + ``` +3. In another terminal run the CORE GUI + ``` shell + core-gui + ``` +4. You will be presented with sessions to join, select the one created by the script +

+ +

\ No newline at end of file diff --git a/docs/tutorials/setup.md b/docs/tutorials/setup.md new file mode 100644 index 00000000..858b0f1d --- /dev/null +++ b/docs/tutorials/setup.md @@ -0,0 +1,82 @@ +# Tutorial Setup + +## Setup for CORE + +We assume the prior installation of CORE, using a virtual environment. You can +then adjust your PATH and add an alias to help more conveniently run CORE +commands. + +This can be setup in your **.bashrc** + +```shell +export PATH=$PATH:/opt/core/venv/bin +alias sudop='sudo env PATH=$PATH' +``` + +## Setup for Chat App + +There is a simple TCP chat app provided as example software to use and run within +the tutorials provided. + +### Installation + +The following will install chatapp and its scripts into **/usr/local**, which you +may need to add to PATH within node to be able to use command directly. + +``` shell +sudo python3 -m pip install . +``` + +!!! note + + Some Linux distros will not have **/usr/local** in their PATH and you + will need to compensate. + +``` shell +export PATH=$PATH:/usr/local +``` + +### Running the Server + +The server will print and log connected clients and their messages. + +``` shell +usage: chatapp-server [-h] [-a ADDRESS] [-p PORT] + +chat app server + +optional arguments: + -h, --help show this help message and exit + -a ADDRESS, --address ADDRESS + address to listen on (default: ) + -p PORT, --port PORT port to listen on (default: 9001) +``` + +### Running the Client + +The client will print and log messages from other clients and their join/leave status. + +``` shell +usage: chatapp-client [-h] -a ADDRESS [-p PORT] + +chat app client + +optional arguments: + -h, --help show this help message and exit + -a ADDRESS, --address ADDRESS + address to listen on (default: None) + -p PORT, --port PORT port to listen on (default: 9001) +``` + +### Installing the Chat App Service + +1. You will first need to edit **/etc/core/core.conf** to update the config + service path to pick up your service + ``` shell + custom_config_services_dir = + ``` +2. Then you will need to copy/move **chatapp/chatapp_service.py** to the directory + configured above +3. Then you will need to restart the **core-daemon** to pick up this new service +4. Now the service will be an available option under the group **ChatApp** with + the name **ChatApp Server** diff --git a/docs/tutorials/tutorial1.md b/docs/tutorials/tutorial1.md new file mode 100644 index 00000000..c92f4b4e --- /dev/null +++ b/docs/tutorials/tutorial1.md @@ -0,0 +1,250 @@ +# Tutorial 1 - Wired Network + +This tutorial will cover some use cases when using a wired 2 node +scenario in CORE. + +

+ +

+ +## Files + +Below is the list of files used for this tutorial. + +* 2 node wired scenario + * scenario.xml + * scenario.py +* 2 node wired scenario, with **n1** running the "Chat App Server" service + * scenario_service.xml + * scenario_service.py + +## Running this Tutorial + +This section covers interactions that can be carried out for this scenario. + +Our scenario has the following nodes and addresses: + +* n1 - 10.0.0.20 +* n2 - 10.0.0.21 + +All usages below assume a clean scenario start. + +### Using Ping + +Using the command line utility **ping** can be a good way to verify connectivity +between nodes in CORE. + +* Make sure the CORE daemon is running a terminal, if not already + ``` shell + sudop core-daemon + ``` +* In another terminal run the GUI + ``` shell + core-gui + ``` +* In the GUI menu bar select **File->Open...**, then navigate to and select **scenario.xml** +

+ +

+* You can now click on the **Start Session** button to run the scenario +

+ +

+* Open a terminal on **n1** by double clicking it in the GUI +* Run the following in **n1** terminal + ``` shell + ping -c 3 10.0.0.21 + ``` +* You should see the following output + ``` shell + PING 10.0.0.21 (10.0.0.21) 56(84) bytes of data. + 64 bytes from 10.0.0.21: icmp_seq=1 ttl=64 time=0.085 ms + 64 bytes from 10.0.0.21: icmp_seq=2 ttl=64 time=0.079 ms + 64 bytes from 10.0.0.21: icmp_seq=3 ttl=64 time=0.072 ms + + --- 10.0.0.21 ping statistics --- + 3 packets transmitted, 3 received, 0% packet loss, time 1999ms + rtt min/avg/max/mdev = 0.072/0.078/0.085/0.011 ms + ``` + +### Using Tcpdump + +Using **tcpdump** can be very beneficial for examining a network. You can verify +traffic being sent/received among many other uses. + +* Make sure the CORE daemon is running a terminal, if not already + ``` shell + sudop core-daemon + ``` +* In another terminal run the GUI + ``` shell + core-gui + ``` +* In the GUI menu bar select **File->Open...**, then navigate to and select **scenario.xml** +

+ +

+* You can now click on the **Start Session** button to run the scenario +

+ +

+* Open a terminal on **n1** by double clicking it in the GUI +* Open a terminal on **n2** by double clicking it in the GUI +* Run the following in **n2** terminal + ``` shell + tcpdump -lenni eth0 + ``` +* Run the following in **n1** terminal + ``` shell + ping -c 1 10.0.0.21 + ``` +* You should see the following in **n2** terminal + ``` shell + tcpdump: verbose output suppressed, use -v or -vv for full protocol decode + listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes + 10:23:04.685292 00:00:00:aa:00:00 > 00:00:00:aa:00:01, ethertype IPv4 (0x0800), length 98: 10.0.0.20 > 10.0.0.21: ICMP echo request, id 67, seq 1, length 64 + 10:23:04.685329 00:00:00:aa:00:01 > 00:00:00:aa:00:00, ethertype IPv4 (0x0800), length 98: 10.0.0.21 > 10.0.0.20: ICMP echo reply, id 67, seq 1, length 64 + ``` + +### Editing a Link + +You can edit links between nodes in CORE to modify loss, delay, bandwidth, and more. This can be +beneficial for understanding how software will behave in adverse conditions. + +* Make sure the CORE daemon is running a terminal, if not already + ``` shell + sudop core-daemon + ``` +* In another terminal run the GUI + ``` shell + core-gui + ``` +* In the GUI menu bar select **File->Open...**, then navigate to and select **scenario.xml** +

+ +

+* You can now click on the **Start Session** button to run the scenario +

+ +

+* Right click the link between **n1** and **n2** +* Select **Configure** +

+ +

+* Update the loss to **25** +

+ +

+* Open a terminal on **n1** by double clicking it in the GUI +* Run the following in **n1** terminal + ``` shell + ping -c 10 10.0.0.21 + ``` +* You should see something similar for the summary output, reflecting the change in loss + ``` shell + --- 10.0.0.21 ping statistics --- + 10 packets transmitted, 6 received, 40% packet loss, time 9000ms + rtt min/avg/max/mdev = 0.077/0.093/0.108/0.016 ms + ``` +* Remember that the loss above is compounded, since a ping and the loss applied occurs in both directions + +### Running Software + +We will now leverage the installed Chat App software to stand up a server and client +within the nodes of our scenario. + +* Make sure the CORE daemon is running a terminal, if not already + ``` shell + sudop core-daemon + ``` +* In another terminal run the GUI + ``` shell + core-gui + ``` +* In the GUI menu bar select **File->Open...**, then navigate to and select **scenario.xml** +

+ +

+* You can now click on the **Start Session** button to run the scenario +

+ +

+* Open a terminal on **n1** by double clicking it in the GUI +* Run the following in **n1** terminal + ``` shell + export PATH=$PATH:/usr/local/bin + chatapp-server + ``` +* Open a terminal on **n2** by double clicking it in the GUI +* Run the following in **n2** terminal + ``` shell + export PATH=$PATH:/usr/local/bin + chatapp-client -a 10.0.0.20 + ``` +* You will see the following output in **n1** terminal + ``` shell + chat server listening on: :9001 + [server] 10.0.0.21:44362 joining + ``` +* Type the following in **n2** terminal and hit enter + ``` shell + hello world + ``` +* You will see the following output in **n1** terminal + ``` shell + chat server listening on: :9001 + [server] 10.0.0.21:44362 joining + [10.0.0.21:44362] hello world + ``` + +### Tailing a Log + +In this case we are using the service based scenario. This will automatically start +and run the Chat App Server on **n1** and log to a file. This case will demonstrate +using `tail -f` to observe the output of running software. + +* Make sure the CORE daemon is running a terminal, if not already + ``` shell + sudop core-daemon + ``` +* In another terminal run the GUI + ``` shell + core-gui + ``` +* In the GUI menu bar select **File->Open...**, then navigate to and select **scenario_service.xml** +

+ +

+* You can now click on the **Start Session** button to run the scenario +

+ +

+* Open a terminal on **n1** by double clicking it in the GUI +* Run the following in **n1** terminal + ``` shell + tail -f chatapp.log + ``` +* Open a terminal on **n2** by double clicking it in the GUI +* Run the following in **n2** terminal + ``` shell + export PATH=$PATH:/usr/local/bin + chatapp-client -a 10.0.0.20 + ``` +* You will see the following output in **n1** terminal + ``` shell + chat server listening on: :9001 + [server] 10.0.0.21:44362 joining + ``` +* Type the following in **n2** terminal and hit enter + ``` shell + hello world + ``` +* You will see the following output in **n1** terminal + ``` shell + chat server listening on: :9001 + [server] 10.0.0.21:44362 joining + [10.0.0.21:44362] hello world + ``` + +--8<-- "common/grpc.md" \ No newline at end of file diff --git a/package/examples/tutorials/chatapp/chatapp/__init__.py b/package/examples/tutorials/chatapp/chatapp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/package/examples/tutorials/chatapp/chatapp/client.py b/package/examples/tutorials/chatapp/chatapp/client.py new file mode 100644 index 00000000..87a839d5 --- /dev/null +++ b/package/examples/tutorials/chatapp/chatapp/client.py @@ -0,0 +1,63 @@ +import argparse +import select +import socket +import sys +import termios + +DEFAULT_PORT: int = 9001 +READ_SIZE: int = 4096 + + +def prompt(): + sys.stdout.write(">> ") + sys.stdout.flush() + + +class ChatClient: + def __init__(self, address, port): + self.address = address + self.port = port + + def run(self): + server = socket.create_connection((self.address, self.port)) + sockname = server.getsockname() + print(f"connected to server({self.address}:{self.port}) as client({sockname[0]}:{sockname[1]})") + sockets = [sys.stdin, server] + prompt() + try: + while True: + read_sockets, write_socket, error_socket = select.select(sockets, [], []) + for sock in read_sockets: + if sock == server: + message = server.recv(READ_SIZE) + if not message: + print("server closed") + sys.exit(1) + else: + termios.tcflush(sys.stdin, termios.TCIOFLUSH) + print("\x1b[2K\r", end="") + print(message.decode().strip()) + prompt() + else: + message = sys.stdin.readline().strip() + server.sendall(f"{message}\n".encode()) + prompt() + except KeyboardInterrupt: + print("client exiting") + server.close() + + +def main(): + parser = argparse.ArgumentParser( + description="chat app client", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("-a", "--address", help="address to listen on", required=True) + parser.add_argument("-p", "--port", type=int, help="port to listen on", default=DEFAULT_PORT) + args = parser.parse_args() + client = ChatClient(args.address, args.port) + client.run() + + +if __name__ == "__main__": + main() diff --git a/package/examples/tutorials/chatapp/chatapp/server.py b/package/examples/tutorials/chatapp/chatapp/server.py new file mode 100644 index 00000000..fb9c01fd --- /dev/null +++ b/package/examples/tutorials/chatapp/chatapp/server.py @@ -0,0 +1,73 @@ +import argparse +import select +import socket + +DEFAULT_ADDRESS: str = "" +DEFAULT_PORT: int = 9001 +READ_SIZE: int = 4096 + + +class ChatServer: + def __init__(self, address, port): + self.address = address + self.port = port + self.sockets = [] + + def broadcast(self, ignore, message): + for sock in self.sockets: + if sock not in ignore: + sock.sendall(message.encode()) + + def run(self): + print(f"chat server listening on: {self.address}:{self.port}") + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((self.address, self.port)) + server.listen() + self.sockets.append(server) + try: + while True: + read_sockets, write_sockets, error_sockets = select.select(self.sockets, [], []) + for sock in read_sockets: + if sock == server: + client_sock, addr = server.accept() + self.sockets.append(client_sock) + name = f"{addr[0]}:{addr[1]}" + print(f"[server] {name} joining") + self.broadcast({server, client_sock}, f"[server] {name} entered room\n") + else: + peer = sock.getpeername() + name = f"{peer[0]}:{peer[1]}" + try: + data = sock.recv(READ_SIZE).decode().strip() + if data: + print(f"[{name}] {data}") + self.broadcast({server, sock}, f"[{name}] {data}\n") + else: + print(f"[server] {name} leaving") + self.broadcast({server, sock}, f"[server] {name} leaving\n") + sock.close() + self.sockets.remove(sock) + except socket.error: + print(f"[server] {name} leaving") + self.broadcast({server, sock}, f"[server] {name} leaving\n") + sock.close() + self.sockets.remove(sock) + except KeyboardInterrupt: + print("closing server") + + +def main(): + parser = argparse.ArgumentParser( + description="chat app server", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("-a", "--address", help="address to listen on", default=DEFAULT_ADDRESS) + parser.add_argument("-p", "--port", type=int, help="port to listen on", default=DEFAULT_PORT) + args = parser.parse_args() + server = ChatServer(args.address, args.port) + server.run() + + +if __name__ == "__main__": + main() diff --git a/package/examples/tutorials/chatapp/chatapp_service.py b/package/examples/tutorials/chatapp/chatapp_service.py new file mode 100644 index 00000000..6faf8071 --- /dev/null +++ b/package/examples/tutorials/chatapp/chatapp_service.py @@ -0,0 +1,26 @@ +from typing import Dict, List + +from core.config import Configuration +from core.configservice.base import ConfigService, ConfigServiceMode, ShadowDir + + +class ChatAppService(ConfigService): + name: str = "ChatApp Server" + group: str = "ChatApp" + directories: List[str] = [] + files: List[str] = ["chatapp.sh"] + executables: List[str] = [] + dependencies: List[str] = [] + startup: List[str] = [f"bash {files[0]}"] + validate: List[str] = [] + shutdown: List[str] = [] + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + default_configs: List[Configuration] = [] + modes: Dict[str, Dict[str, str]] = {} + shadow_directories: List[ShadowDir] = [] + + def get_text_template(self, _name: str) -> str: + return """ + export PATH=$PATH:/usr/local/bin + PYTHONUNBUFFERED=1 chatapp-server > chatapp.log 2>&1 & + """ diff --git a/package/examples/tutorials/chatapp/setup.py b/package/examples/tutorials/chatapp/setup.py new file mode 100644 index 00000000..7c48ecd3 --- /dev/null +++ b/package/examples/tutorials/chatapp/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages + +setup( + name="chatapp", + version="0.1.0", + packages=find_packages(), + description="Chat App", + entry_points={ + "console_scripts": [ + "chatapp-client = chatapp.client:main", + "chatapp-server = chatapp.server:main", + ], + }, + include_package_data=True, + zip_safe=False, + python_requires=">=3.6", +) diff --git a/package/examples/tutorials/tutorial1/scenario.py b/package/examples/tutorials/tutorial1/scenario.py new file mode 100644 index 00000000..1f5a45e6 --- /dev/null +++ b/package/examples/tutorials/tutorial1/scenario.py @@ -0,0 +1,36 @@ +from core.api.grpc import client +from core.api.grpc.wrappers import Position + + +def main(): + # interface helper + iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") + + # create grpc client and connect + core = client.CoreGrpcClient() + core.connect() + + # create session + session = core.create_session() + + # create nodes + position = Position(x=250, y=250) + node1 = session.add_node(_id=1, name="n1", position=position) + position = Position(x=500, y=250) + node2 = session.add_node(_id=2, name="n2", position=position) + + # create link + node1_iface = iface_helper.create_iface(node_id=node1.id, iface_id=0) + node1_iface.ip4 = "10.0.0.20" + node1_iface.ip6 = "2001::14" + node2_iface = iface_helper.create_iface(node_id=node2.id, iface_id=0) + node2_iface.ip4 = "10.0.0.21" + node2_iface.ip6 = "2001::15" + session.add_link(node1=node1, node2=node2, iface1=node1_iface, iface2=node2_iface) + + # start session + core.start_session(session=session) + + +if __name__ == "__main__": + main() diff --git a/package/examples/tutorials/tutorial1/scenario.xml b/package/examples/tutorials/tutorial1/scenario.xml new file mode 100644 index 00000000..428fe4ca --- /dev/null +++ b/package/examples/tutorials/tutorial1/scenario.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/examples/tutorials/tutorial1/scenario_service.py b/package/examples/tutorials/tutorial1/scenario_service.py new file mode 100644 index 00000000..bc2c6cc8 --- /dev/null +++ b/package/examples/tutorials/tutorial1/scenario_service.py @@ -0,0 +1,37 @@ +from core.api.grpc import client +from core.api.grpc.wrappers import Position + + +def main(): + # interface helper + iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") + + # create grpc client and connect + core = client.CoreGrpcClient() + core.connect() + + # create session + session = core.create_session() + + # create nodes + position = Position(x=250, y=250) + node1 = session.add_node(_id=1, name="n1", position=position) + node1.config_services.add("ChatApp Server") + position = Position(x=500, y=250) + node2 = session.add_node(_id=2, name="n2", position=position) + + # create link + node1_iface = iface_helper.create_iface(node_id=node1.id, iface_id=0) + node1_iface.ip4 = "10.0.0.20" + node1_iface.ip6 = "2001::14" + node2_iface = iface_helper.create_iface(node_id=node2.id, iface_id=0) + node2_iface.ip4 = "10.0.0.21" + node2_iface.ip6 = "2001::15" + session.add_link(node1=node1, node2=node2, iface1=node1_iface, iface2=node2_iface) + + # start session + core.start_session(session=session) + + +if __name__ == "__main__": + main() diff --git a/package/examples/tutorials/tutorial1/scenario_service.xml b/package/examples/tutorials/tutorial1/scenario_service.xml new file mode 100644 index 00000000..ab092f4c --- /dev/null +++ b/package/examples/tutorials/tutorial1/scenario_service.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +