diff --git a/scripts/brk2tmuxp/Pipfile b/scripts/brk2tmuxp/Pipfile new file mode 100644 index 0000000..e541ddd --- /dev/null +++ b/scripts/brk2tmuxp/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyyaml = "*" + +[dev-packages] +mypy = "*" +types-pyyaml = "*" + +[requires] +python_version = "3.9" diff --git a/scripts/brk2tmuxp/Pipfile.lock b/scripts/brk2tmuxp/Pipfile.lock new file mode 100644 index 0000000..58e264b --- /dev/null +++ b/scripts/brk2tmuxp/Pipfile.lock @@ -0,0 +1,121 @@ +{ + "_meta": { + "hash": { + "sha256": "2924ea98850621a19a9fdc0e93c8dec4fdf2af78648276c0a2f0ae8cb8994f97" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pyyaml": { + "hashes": [ + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + ], + "index": "pypi", + "version": "==6.0" + } + }, + "develop": { + "mypy": { + "hashes": [ + "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d", + "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8", + "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de", + "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038", + "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed", + "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334", + "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff", + "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2", + "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22", + "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2", + "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2", + "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605", + "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb", + "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519", + "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0", + "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc", + "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b", + "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f", + "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075", + "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef", + "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb", + "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a", + "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b" + ], + "index": "pypi", + "version": "==0.950" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "types-pyyaml": { + "hashes": [ + "sha256:59480cf44595d836aaae050f35e3c39f197f3a833679ef3978d97aa9f2fb7def", + "sha256:7b273a34f32af9910cf9405728c9d2ad3afc4be63e4048091a1a73d76681fe67" + ], + "index": "pypi", + "version": "==6.0.7" + }, + "typing-extensions": { + "hashes": [ + "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", + "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + ], + "markers": "python_version >= '3.7'", + "version": "==4.2.0" + } + } +} diff --git a/scripts/brk2tmuxp/README.md b/scripts/brk2tmuxp/README.md new file mode 100644 index 0000000..1c56130 --- /dev/null +++ b/scripts/brk2tmuxp/README.md @@ -0,0 +1,151 @@ +## CML breakout to tmuxp + +The script reads the CML breakout `labs.yaml` and generates [tmuxp](https://github.com/tmux-python/tmuxp) sessions. +Both YAML and JSON format are supported (default to YAML). + +The sessions can then be loaded with `tmuxp load [labs]` and will perform the following: + +The `windows` variant (the default) will: + +1. create a tmux session named after the lab title +2. the first window created will run `breakout run` for that particular lab +3. for each node, open a node-named new window and telnet to the node + +the `panes` variant will: + +1. create a tmux session named after the lab title +2. the first window created will run `breakout run` for that particular lab +3. the second window will open a pane per node + +
+ +## Getting Started + +### Prerequisites + +Use your favorite package manager to install `tmux` and `tmuxp`, for example: + +- tmux + +```sh +brew install tmux +``` + +```sh +apt install tmux +``` + +- tmuxp + +```sh +brew install tmuxp +``` + +```sh +apt install tmuxp +``` + +### Installation + +Clone the repository, cd in the directory and install the requirements: + +- pipenv + +```sh +pipenv install +``` + +- pip + +```sh +python3 -m venv /path/to/directory +pip install -r requirements +``` + +Note: the only dependency is `pyyaml`. + +## Usage + +The following options are available: + +```sh +❯ python brk2tmuxp.py -h +usage: brk2tmuxp.py [-h] [-d BRK_DIR] [-f YAML_FILE] [-p] [-l LISTEN_ADDR] [-s SLEEP] [-j] + +reads a CML breakout labs.yaml and generates a tmuxp session files + +optional arguments: + -h, --help show this help message and exit + -d BRK_DIR, --brk-dir BRK_DIR + path to dir containing the breakout labs YAML files (default:: current directory) + -f YAML_FILE, --yaml-file YAML_FILE + name of the 'labs' YAML file, (default to labs.yaml) + -p, --panes if set, all telnet sessions will be in one window (default: each telnet session has its own window) + -l LISTEN_ADDR, --listen_addr LISTEN_ADDR + specify the listen address (default: ::1) + -s SLEEP, --sleep SLEEP + sleep time (in seconds) before initiating telnet session (default: 3) + -j, --format-json output JSON tmuxp session files (default: YAML) +``` + +### Use `breakout init` to fetch the labs and nodes from the controller: + +```sh +~/CML via 🐍 v3.9.12 (brk2tmuxp) +❯ breakout init +get simplified node definitions from controller... +get active console keys from controller... +get active VNC keys from controller... +get all the labs from controller... +get all the nodes for the labs from controller... +get nodes for lab L2L IKEv2 from controller... +get nodes for lab cisco_isis_sr_101_v1 from controller... +config written. + +~/CML via 🐍 v3.9.12 (brk2tmuxp) +❯ ls -l +.rwxrwxrwx 746 sgherdao 11 Mar 21:04 config.yaml +.rwxrwxrwx 1.3k sgherdao 3 May 21:50 labs.yaml +``` + +### Generate the "windows" variant and load it with `tmuxp`: + +```sh +~/CML via 🐍 v3.9.12 (brk2tmuxp) +❯ python brk2tmuxp.py + +~/CML via 🐍 v3.9.12 (brk2tmuxp) +❯ ls -l +.rw-r--r-- 513 sgherdao 3 May 21:55 1eaf2c3b-9207-4524-9e7c-3eeaada67886.yaml +.rwxrwxrwx 746 sgherdao 11 Mar 21:04 config.yaml +.rw-r--r-- 883 sgherdao 3 May 22:04 d231681f-a88c-4004-884a-4c9639ad8b07.yaml +.rwxrwxrwx 4.3k sgherdao 3 May 22:01 labs.yaml + +~/CML via 🐍 v3.9.12 (brk2tmuxp) took 3s +❯ tmuxp load d231681f-a88c-4004-884a-4c9639ad8b07.yaml +[Loading] CML/d231681f-a88c-4004-884a-4c9639ad8b07.yaml +Already inside TMUX, switch to session? yes/no +Or (a)ppend windows in the current active session? +[y/n/a]: y +``` + + + +### Generate the "panes" variant and load it with `tmuxp`: + +```sh +~/CML via 🐍 v3.9.12 (brk2tmuxp) +❯ python PythonTemp/brk2tmuxp/brk2tmuxp.py -p + +~/CML via 🐍 v3.9.12 (brk2tmuxp) +❯ tmuxp load 1eaf2c3b-9207-4524-9e7c-3eeaada67886.yaml +[Loading] CML/1eaf2c3b-9207-4524-9e7c-3eeaada67886.yaml +Already inside TMUX, switch to session? yes/no +Or (a)ppend windows in the current active session? +[y/n/a]: y +❯ +``` + + + + diff --git a/scripts/brk2tmuxp/brk2tmuxp.py b/scripts/brk2tmuxp/brk2tmuxp.py new file mode 100644 index 0000000..347c3d2 --- /dev/null +++ b/scripts/brk2tmuxp/brk2tmuxp.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys + +import yaml + + +def main() -> int: + """ + reads a CML breakout labs.yaml file and returns a list of dict ready to be + translated into multiple JSON or YAML tmuxp session files. + + By default, each telnet session will have its own window but if the `-p` or + `--panes` flag is set, then each telnet session will be in a pane in one + (and only one) window. + + VNC sessions are ignored. + """ + parser = argparse.ArgumentParser( + description="reads a CML breakout labs.yaml and generates a tmuxp session files" + ) + parser.add_argument( + "-d", + "--brk-dir", + type=str, + default=".", + help="path to dir containing the breakout labs YAML files (default:: current directory)", + ) + parser.add_argument( + "-f", + "--yaml-file", + type=str, + default="labs.yaml", + help="name of the 'labs' YAML file, (default to labs.yaml)", + ) + parser.add_argument( + "-p", + "--panes", + help="if set, all telnet sessions will be in one window (default: each telnet session has its own window)", + action="store_true", + ) + parser.add_argument( + "-l", + "--listen_addr", + default="::1", + help="specify the listen address (default: ::1)", + ) + parser.add_argument( + "-s", + "--sleep", + type=float, + default="3", + help="sleep time (in seconds) before initiating telnet session (default: 3)", + ) + parser.add_argument( + "-j", + "--format-json", + action="store_true", + help="output JSON tmuxp session files (default: YAML)", + ) + args = parser.parse_args() + + # load labs yaml file + brk_dir = args.brk_dir + yaml_file = args.yaml_file + + with open(f"{brk_dir}/{yaml_file}") as f: + labs = yaml.safe_load(f) + + panes = args.panes + listen_addr = args.listen_addr + sleep = args.sleep + format_json = args.format_json + tmux_sessions = {} + + for uuid, lab in labs.items(): + if panes: + tmux_sessions[uuid] = panes_configs( + lab=lab, brk_dir=brk_dir, listen_addr=listen_addr, sleep=sleep + ) + + else: + tmux_sessions[uuid] = windows_configs( + lab=lab, brk_dir=brk_dir, listen_addr=listen_addr, sleep=sleep + ) + + for uuid, tmux_session in tmux_sessions.items(): + if format_json: + with open(f"{uuid}.json", "w") as f: + json.dump(tmux_session, f, indent=2) + else: + with open(f"{uuid}.yaml", "w") as f: + yaml.dump(tmux_session, f) + + return 0 + + +def panes_configs(lab: dict, brk_dir: str, listen_addr: str, sleep: float) -> dict: + """ + returns a dict representing a tmuxp session file for one CML lab + + This is the 'panes' variant, the tmuxp session will consist of: + - one session named based on the lab title containing: + - the first window will cd into the CML dir and launch `breakout run` + for that lab + - subsequent windows, will telnet to one lab node + """ + lab_title = lab["lab_title"] + conf = { + "session_name": lab_title, + "windows": [ + { + "window_name": "breakout", + "panes": [ + { + "shell_command": [ + f"cd {brk_dir}", + f"breakout run '{lab_title}'", + ] + } + ], + }, + {"window_name": "nodes", "layout": "tiled"}, + ], + } + panes: list = [] + for _, node in lab["nodes"].items(): + node_label = node["label"] + + panes.extend( + { + "shell_command": + # dirty way to set tmux pane title + # see https://github.com/tmux-python/tmuxp/issues/384 + [ + f"printf '\\033]2;%s\\033\\\\' '{node_label}/{n['name'][-1]}'", + f"time sleep {sleep}", + f"telnet {listen_addr} {n['listen_port']}", + ] + } + for n in node["devices"] + if n["enabled"] and n["name"] != "vnc" + ) + + conf["windows"][1]["panes"] = panes + return conf + + +def windows_configs(lab: dict, brk_dir: str, listen_addr: str, sleep: int) -> dict: + """ + returns a dict representing a tmuxp session file for one CML lab + + This is the 'windows' variant, the tmuxp session will consist of: + - one session named based on the lab title containing: + - the first window will launch `breakout run` for that lab + - next windows, will telnet to one lab node + """ + lab_title = lab["lab_title"] + conf = { + "session_name": lab_title, + "windows": [ + { + "window_name": "breakout", + "panes": [ + { + "shell_command": [ + f"cd {brk_dir}", + f"breakout run '{lab_title}'", + ] + } + ], + }, + ], + } + + for _, node in lab["nodes"].items(): + node_label = node["label"] + + conf["windows"].extend( + { + "window_name": f"{node_label}/{n['name'][-1]}", + "panes": [ + { + "shell_command": [ + f"time sleep {sleep}", + f"telnet {listen_addr} {n['listen_port']}", + ] + } + ], + } + for n in node["devices"] + if n["enabled"] and n["name"] != "vnc" + ) + + return conf + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/brk2tmuxp/panes-01.jpg b/scripts/brk2tmuxp/panes-01.jpg new file mode 100644 index 0000000..c74a303 Binary files /dev/null and b/scripts/brk2tmuxp/panes-01.jpg differ diff --git a/scripts/brk2tmuxp/requirements.txt b/scripts/brk2tmuxp/requirements.txt new file mode 100644 index 0000000..c3726e8 --- /dev/null +++ b/scripts/brk2tmuxp/requirements.txt @@ -0,0 +1 @@ +pyyaml diff --git a/scripts/brk2tmuxp/windows-01.jpg b/scripts/brk2tmuxp/windows-01.jpg new file mode 100644 index 0000000..7868e39 Binary files /dev/null and b/scripts/brk2tmuxp/windows-01.jpg differ