diff --git a/back/src/emulator.py b/back/src/emulator.py index d6d29969..92bcf79e 100644 --- a/back/src/emulator.py +++ b/back/src/emulator.py @@ -27,11 +27,18 @@ def emulate( # Validate job limit MAX_JOBS_COUNT = 30 + MAX_TIME_SLEEP = 60 if len(network.jobs) > MAX_JOBS_COUNT: raise ValueError( f"Превышен лимит! В сети максимальное количество команд ({MAX_JOBS_COUNT}). " f"Текущее количество: {len(network.jobs)}" ) + sleep_jobs = [j for j in network.jobs if j.job_id == 7] + total_time = sum(int(j.arg_1) for j in sleep_jobs) + if total_time > 60 or total_time < 0: + raise ValueError( + f"Превышен лимит! В сети максимальное количество команд sleep {MAX_TIME_SLEEP})." + ) if len(network.jobs) == 0: return [], [] diff --git a/back/src/jobs.py b/back/src/jobs.py index 1239e05c..fcd02aeb 100755 --- a/back/src/jobs.py +++ b/back/src/jobs.py @@ -1,7 +1,7 @@ import re import shlex import ipaddress - +import time from netaddr import EUI, AddrFormatError from typing import Any, Callable, List, Dict from network_schema import Job @@ -194,6 +194,31 @@ def valid_iface(iface) -> bool: return True +def valid_sleep(time) -> bool: + try: + _ = int(time) + except (ValueError, TypeError): + return False + if int(time) > 50 or int(time) <= 0: + return False + + return True + + +def link_down_handler(job: Job, job_host: Any) -> None: + arg_interface = job.arg_1 + if not net_dev_checker(arg_interface): + return + job_host.cmd(f"ip link set {arg_interface} down") + + +def sleep_handler(job: Job, job_host: Any) -> None: + arg_time = job.arg_1 + if not valid_sleep(arg_time): + return + time.sleep(int(arg_time)) + + def ping_handler(job: Job, job_host: Any) -> None: """Execute ping -c 1""" arg_ip = job.arg_1 @@ -461,6 +486,8 @@ def __init__(self, job: Job, job_host: Any, **kwargs) -> None: 3: sending_udp_data_handler, 4: sending_tcp_data_handler, 5: traceroute_handler, + 6: link_down_handler, + 7: sleep_handler, 100: ip_addr_add_handler, 101: iptables_handler, 102: ip_route_add_handler, diff --git a/back/tests/test_json/link_down_answer.json b/back/tests/test_json/link_down_answer.json new file mode 100644 index 00000000..dbfa2f87 --- /dev/null +++ b/back/tests/test_json/link_down_answer.json @@ -0,0 +1,110 @@ +[ + [ + { + "data": { + "id": "", + "label": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "type": "packet" + }, + "config": { + "type": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "path": "edge_mjcztrism9ezamqqda", + "source": "host_1", + "target": "l2sw1", + "loss_percentage": 0.0, + "duplicate_percentage": 0.0 + }, + "timestamp": "" + } + ], + [ + { + "data": { + "id": "", + "label": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "type": "packet" + }, + "config": { + "type": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "path": "edge_mjcztsiqna2lxz6nnwd", + "source": "l2sw1", + "target": "l2sw2", + "loss_percentage": 0.0, + "duplicate_percentage": 0.0 + }, + "timestamp": "" + } + ], + [ + { + "data": { + "id": "", + "label": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "type": "packet" + }, + "config": { + "type": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "path": "edge_mjcztrism9ezamqqda", + "source": "host_1", + "target": "l2sw1", + "loss_percentage": 0.0, + "duplicate_percentage": 0.0 + }, + "timestamp": "" + } + ], + [ + { + "data": { + "id": "", + "label": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "type": "packet" + }, + "config": { + "type": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "path": "edge_mjcztsiqna2lxz6nnwd", + "source": "l2sw1", + "target": "l2sw2", + "loss_percentage": 0.0, + "duplicate_percentage": 0.0 + }, + "timestamp": "" + } + ], + [ + { + "data": { + "id": "", + "label": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "type": "packet" + }, + "config": { + "type": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "path": "edge_mjcztrism9ezamqqda", + "source": "host_1", + "target": "l2sw1", + "loss_percentage": 0.0, + "duplicate_percentage": 0.0 + }, + "timestamp": "" + } + ], + [ + { + "data": { + "id": "", + "label": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "type": "packet" + }, + "config": { + "type": "ARP-request\nWho has 192.168.0.2? Tell 192.168.0.1", + "path": "edge_mjcztsiqna2lxz6nnwd", + "source": "l2sw1", + "target": "l2sw2", + "loss_percentage": 0.0, + "duplicate_percentage": 0.0 + }, + "timestamp": "" + } + ] +] \ No newline at end of file diff --git a/back/tests/test_json/link_down_network.json b/back/tests/test_json/link_down_network.json new file mode 100644 index 00000000..03054bb4 --- /dev/null +++ b/back/tests/test_json/link_down_network.json @@ -0,0 +1,181 @@ +{ + "nodes": [ + { + "classes": [ + "host" + ], + "config": { + "default_gw": "", + "label": "host_1", + "type": "host" + }, + "data": { + "id": "host_1", + "label": "host_1" + }, + "interface": [ + { + "connect": "edge_mjcztrism9ezamqqda", + "id": "iface_60266344", + "ip": "192.168.0.1", + "name": "iface_60266344", + "netmask": 24 + } + ], + "position": { + "x": 208.25, + "y": 192.5999984741211 + } + }, + { + "classes": [ + "l2_switch" + ], + "config": { + "label": "l2sw1", + "stp": 0, + "type": "l2_switch" + }, + "data": { + "id": "l2sw1", + "label": "l2sw1" + }, + "interface": [ + { + "connect": "edge_mjcztrism9ezamqqda", + "id": "l2sw1_1", + "name": "l2sw1_1", + "type_connection": null, + "vlan": null + }, + { + "connect": "edge_mjcztsiqna2lxz6nnwd", + "id": "l2sw1_2", + "name": "l2sw1_2", + "type_connection": null, + "vlan": null + } + ], + "position": { + "x": 277.75, + "y": 184.8000030517578 + } + }, + { + "classes": [ + "l2_switch" + ], + "config": { + "label": "l2sw2", + "stp": 0, + "type": "l2_switch" + }, + "data": { + "id": "l2sw2", + "label": "l2sw2" + }, + "interface": [ + { + "connect": "edge_mjcztsiqna2lxz6nnwd", + "id": "l2sw2_1", + "name": "l2sw2_1", + "type_connection": null, + "vlan": null + }, + { + "connect": "edge_mjczttixwtd2jtzl29", + "id": "l2sw2_2", + "name": "l2sw2_2", + "type_connection": null, + "vlan": null + } + ], + "position": { + "x": 345.75, + "y": 178.3000030517578 + } + }, + { + "classes": [ + "host" + ], + "config": { + "default_gw": "", + "label": "host_2", + "type": "host" + }, + "data": { + "id": "host_2", + "label": "host_2" + }, + "interface": [ + { + "connect": "edge_mjczttixwtd2jtzl29", + "id": "iface_65503551", + "ip": "192.168.0.2", + "name": "iface_65503551", + "netmask": 24 + } + ], + "position": { + "x": 410.75, + "y": 182.5999984741211 + } + } + ], + "edges": [ + { + "data": { + "duplicate_percentage": 0, + "id": "edge_mjcztrism9ezamqqda", + "loss_percentage": 0, + "source": "host_1", + "target": "l2sw1" + } + }, + { + "data": { + "duplicate_percentage": 0, + "id": "edge_mjcztsiqna2lxz6nnwd", + "loss_percentage": 0, + "source": "l2sw1", + "target": "l2sw2" + } + }, + { + "data": { + "duplicate_percentage": 0, + "id": "edge_mjczttixwtd2jtzl29", + "loss_percentage": 0, + "source": "l2sw2", + "target": "host_2" + } + } + ], + "jobs": [ + { + "arg_1": "l2sw2_2", + "arg_2": "host_2", + "host_id": "l2sw2", + "id": "179fdf3667d84b6ca28e127bf12387f6", + "job_id": 6, + "level": 0, + "print_cmd": "link down host_2" + }, + { + "arg_1": "192.168.0.2", + "host_id": "host_1", + "id": "a4e92ee1d12b4f499821de2f03693be4", + "job_id": 1, + "level": 1, + "print_cmd": "ping -c 1 192.168.0.2" + } + ], + "config": { + "zoom": 2, + "pan_x": -183.5437076535751, + "pan_y": 107.00501192502065 + }, + "pcap": [], + "packets": "" +} \ No newline at end of file diff --git a/front/.env b/front/.env index 6b694e49..118ca745 100755 --- a/front/.env +++ b/front/.env @@ -22,4 +22,4 @@ POSTGRES_HOST=172.18.0.4 # YANDEX_POSTGRES_SSLMODE=verify-full # Режим работы: dev (локальный PostgreSQL) или prod (Yandex Cloud PostgreSQL) -MODE=dev +MODE=prod \ No newline at end of file diff --git a/front/src/configurators.py b/front/src/configurators.py index 62ae023b..d625018c 100644 --- a/front/src/configurators.py +++ b/front/src/configurators.py @@ -185,6 +185,8 @@ def __init__(self, device_type: str): self._device_node = None # current device node in miminet network __MAX_JOBS_COUNT: int = 30 + __SLEEP_JOB_ID: int = 7 + __MAX_SLEEP_TIME: int = 60 def create_job(self, job_id: int, job_sign: str) -> JobConfigurator: """ @@ -269,10 +271,9 @@ def _conf_jobs(self): job_id_str = get_data(f"config_{self._device_type}_job_select_field") if not job_id_str: - raise ConfigurationError("Не указан параметр job_id") + return job_id = int(job_id_str) - if job_id not in self.__jobs.keys(): return # if user didn't select job @@ -308,6 +309,19 @@ def _conf_jobs(self): job_conf_res["level"] = job_level job_conf_res["host_id"] = self._device_node["data"]["id"] + sleep_job_list = [ + job + for job in self._json_network["jobs"] + if job["job_id"] == self.__SLEEP_JOB_ID + ] + current_time = sum(int(j["arg_1"]) for j in sleep_job_list) + + if job_id == self.__SLEEP_JOB_ID: + new_job_arg = int(job_conf_res["arg_1"]) + if current_time + new_job_arg > 60: + raise ConfigurationError( + f"Превышен лимит по времени для команды sleep ({self.__MAX_SLEEP_TIME} секунд на сеть)" + ) if editing_job_id and old_job_index is not None: # Insert at the same position where the old job was @@ -415,7 +429,11 @@ def __init__(self): def _configure(self): self._conf_prepare() self._conf_label_update() - + res = {} + try: # catch argument check errors + self._conf_jobs() + except ArgCheckError as e: + res.update({"warning": str(e)}) # RSTP/STP setup switch_stp = get_data("config_rstp_stp") @@ -430,8 +448,14 @@ def _configure(self): if stp_priority: self._device_node["config"]["priority"] = int(stp_priority) - - return {"message": "Конфигурация обновлена", "nodes": self._nodes} + res.update( + { + "message": "Конфигурация обновлена", + "nodes": self._nodes, + "jobs": self._json_network["jobs"], + } + ) + return res class HostConfigurator(AbstractDeviceConfigurator): diff --git a/front/src/miminet_host.py b/front/src/miminet_host.py index a5b6e4fb..48594001 100755 --- a/front/src/miminet_host.py +++ b/front/src/miminet_host.py @@ -95,6 +95,11 @@ def emptiness_check(arg: str) -> bool: return bool(arg and str(arg).strip()) and str(arg) != "0" +def time_check(arg: str) -> bool: + """Check if a string is >=50 or <= 0 or empty""" + return range_check(arg, range(1, 51)) + + def regex_check(arg: str, regex: str) -> bool: """Check if a string matches the given regex""" return bool(re.match(regex, arg)) @@ -375,6 +380,22 @@ def build_error(error_type: str, cmd: str) -> str: emptiness_check ).set_error_msg('Не указан интерфейс для команды "Добавить ARP Proxy-интерфейс"') + +# ~ ~ ~ SWITCH JOBS ~ ~ ~ +link_down_job = switch.create_job(6, "link down [1]") +link_down_job.add_param("config_switch_link_down_iface_select_field").add_check( + emptiness_check +).set_error_msg('Не указан интерфейс для команды "Удалить линк"') +link_down_job.add_param("switch_connection_host_label_hidden").add_check( + emptiness_check +).set_error_msg('Не указан интерфейс для команды "Удалить линк"') + +sleep_job = switch.create_job(7, "sleep [0] seconds") +sleep_job.add_param("config_switch_sleep").add_check(time_check).set_error_msg( + build_error(ErrorType.options, "sleep") +) + + # ~ ~ ~ SERVER JOBS ~ ~ ~ # ping -c 1 diff --git a/front/src/miminet_network.py b/front/src/miminet_network.py index c2d7f634..0763a228 100644 --- a/front/src/miminet_network.py +++ b/front/src/miminet_network.py @@ -354,6 +354,7 @@ def post_nodes_edges(): if request.method == "POST": nodes = request.json[0] edges = request.json[1] + jobs = request.json[2] for edge in edges: edge_data = edge.get("data", {}) @@ -363,28 +364,11 @@ def post_nodes_edges(): jnet = json.loads(net.network) jnet["edges"] = edges jnet["nodes"] = nodes + jnet["jobs"] = jobs # Remove all pcaps jnet["pcap"] = [] - # If we delete host, remove all jobs without hosts - new_jobs = [] - jobs = jnet["jobs"] - for job in jobs: - job_host = job.get("host_id") - - if not job_host: - continue - - nn = list(filter(lambda x: x["data"]["id"] == job_host, nodes)) - - # Good, append job and continue - if nn: - new_jobs.append(job) - continue - - jnet["jobs"] = new_jobs - net.network = json.dumps(jnet) # Remove all previous simulations @@ -545,7 +529,6 @@ def get_emulation_queue_size(): """Answer with current emulation queue size filtered by emulation time.""" time_filter_req: str = request.args.get("time-filter", type=str).replace(" ", "+") time_filter: datetime.datetime = datetime.datetime.fromisoformat(time_filter_req) - if not time_filter: return make_response( jsonify({"message": "Пропущен параметр 'time-filter'."}), 400 diff --git a/front/src/miminet_simulation.py b/front/src/miminet_simulation.py index eed4aa08..515e01ef 100644 --- a/front/src/miminet_simulation.py +++ b/front/src/miminet_simulation.py @@ -40,7 +40,6 @@ def run_simulation() -> Response: # Get saved emulations sims = Simulate.query.filter(Simulate.network_id == net.id).all() - # Remove all previous emulations for s in sims: db.session.delete(s) @@ -90,7 +89,6 @@ def check_simulation(): return make_response(jsonify(ret), 400) sim = Simulate.query.filter(Simulate.id == sim_id).first() - if not sim: ret = {"message": "Нет такой симуляции."} return make_response(jsonify(ret), 400) diff --git a/front/src/static/config.js b/front/src/static/config.js index 5c6bda5f..d824ea6c 100755 --- a/front/src/static/config.js +++ b/front/src/static/config.js @@ -39,6 +39,13 @@ const HostWarningMsg = function (msg) { $(config_content_id).prepend(warning_msg); } +const SwitchWarningMsg = function (msg) { + + let warning_msg = '