From 5616ab0e9af2f2ec0da053600369a1de4f0c354b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 26 May 2017 17:20:18 +0200 Subject: [PATCH 1/3] Fake console for simple Ethernet switch Fix #454 --- .../compute/dynamips/nodes/ethernet_switch.py | 57 +++++- gns3server/schemas/ethernet_switch.py | 20 ++ gns3server/utils/asyncio/embed_shell.py | 187 ++++++++++++++++++ .../compute/dynamips/test_ethernet_switch.py | 31 +++ tests/utils/asyncio/test_embed_shell.py | 75 +++++++ 5 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 gns3server/utils/asyncio/embed_shell.py create mode 100644 tests/compute/dynamips/test_ethernet_switch.py create mode 100644 tests/utils/asyncio/test_embed_shell.py diff --git a/gns3server/compute/dynamips/nodes/ethernet_switch.py b/gns3server/compute/dynamips/nodes/ethernet_switch.py index efeb6da7..2cb80243 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_switch.py +++ b/gns3server/compute/dynamips/nodes/ethernet_switch.py @@ -22,6 +22,8 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 import asyncio from gns3server.utils import parse_version +from gns3server.utils.asyncio.embed_shell import EmbedShell, create_telnet_shell + from .device import Device from ..nios.nio_udp import NIOUDP @@ -32,6 +34,36 @@ import logging log = logging.getLogger(__name__) +class EthernetSwitchConsole(EmbedShell): + """ + Console for the ethernet switch + """ + + def __init__(self, node): + super().__init__(welcome_message="Welcome to GNS3 builtin ethernet switch.\n\nType help to get help\n") + self._node = node + + @asyncio.coroutine + def arp(self): + """ + Show arp table + """ + res = 'Mac VLAN\n' + result = (yield from self._node._hypervisor.send('ethsw show_mac_addr_table {}'.format(self._node.name))) + for line in result: + mac, vlan, _ = line.replace(' ', ' ').split(' ') + mac = mac.replace('.', '') + mac = "{}:{}:{}:{}:{}:{}".format( + mac[0:2], + mac[2:4], + mac[4:6], + mac[6:8], + mac[8:10], + mac[10:12]) + res += mac + ' ' + vlan + '\n' + return res + + class EthernetSwitch(Device): """ @@ -50,6 +82,9 @@ class EthernetSwitch(Device): super().__init__(name, node_id, project, manager, hypervisor) self._nios = {} self._mappings = {} + self._telnet_console = None + self._telnet_shell = None + self._console = self._manager.port_manager.get_free_tcp_port(self._project) if ports is None: # create 8 ports by default self._ports = [] @@ -61,15 +96,29 @@ class EthernetSwitch(Device): else: self._ports = ports + @property + def console(self): + return self._console + def __json__(self): ethernet_switch_info = {"name": self.name, + "console": self.console, + "console_type": "telnet", "node_id": self.id, "project_id": self.project.id, "ports_mapping": self._ports, "status": "started"} return ethernet_switch_info + @property + def console(self): + return self._console + + @console.setter + def console(self, val): + self._console = val + @property def ports_mapping(self): """ @@ -115,6 +164,12 @@ class EthernetSwitch(Device): yield from self._hypervisor.send('ethsw create "{}"'.format(self._name)) log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id)) + + self._telnet_shell = EthernetSwitchConsole(self) + self._telnet_shell.prompt = self._name + '> ' + telnet = create_telnet_shell(self._telnet_shell) + self._telnet_server = (yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)) + self._hypervisor.devices.append(self) @asyncio.coroutine @@ -164,7 +219,7 @@ class EthernetSwitch(Device): for nio in self._nios.values(): if nio and isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) - + self.manager.port_manager.release_tcp_port(self._console, self._project) if self._hypervisor: try: yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name)) diff --git a/gns3server/schemas/ethernet_switch.py b/gns3server/schemas/ethernet_switch.py index 40dd2baa..af9cf952 100644 --- a/gns3server/schemas/ethernet_switch.py +++ b/gns3server/schemas/ethernet_switch.py @@ -58,6 +58,16 @@ ETHERNET_SWITCH_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, + "console": { + "description": "Console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, "node_id": { "description": "Node UUID", "oneOf": [ @@ -149,6 +159,16 @@ ETHERNET_SWITCH_OBJECT_SCHEMA = { "description": "Node status", "enum": ["started", "stopped", "suspended"] }, + "console": { + "description": "Console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, }, "additionalProperties": False, "required": ["name", "node_id", "project_id"] diff --git a/gns3server/utils/asyncio/embed_shell.py b/gns3server/utils/asyncio/embed_shell.py new file mode 100644 index 00000000..ccd3520c --- /dev/null +++ b/gns3server/utils/asyncio/embed_shell.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# +# Copyright (C) 2017 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import sys +import asyncio +import inspect + +from .telnet_server import AsyncioTelnetServer + + +class EmbedShell: + """ + An asynchronous shell use for stuff like EthernetSwitch console + or built in VPCS + """ + + def __init__(self, reader=None, writer=None, loop=None, welcome_message=None): + self._loop = loop + self._reader = reader + self._writer = writer + self._prompt = '> ' + self._welcome_message = welcome_message + + @property + def writer(self): + return self._writer + + @writer.setter + def writer(self, val): + self._writer = val + + @property + def reader(self): + return self._reader + + @reader.setter + def reader(self, val): + self._reader = val + + @property + def prompt(self): + return self._prompt + + @prompt.setter + def prompt(self, val): + self._prompt = val + + @asyncio.coroutine + def help(self, *args): + """ + Show help + """ + res = '' + if len(args) == 0: + res = 'Help:\n' + for name, value in inspect.getmembers(self): + if not inspect.isgeneratorfunction(value): + continue + if name.startswith('_') or (len(args) and name != args[0]) or name == 'run': + continue + doc = inspect.getdoc(value) + res += name + if len(args) and doc: + res += ': ' + doc + elif doc: + res += ': ' + doc.split('\n')[0] + res += '\n' + if len(args) == 0: + res += '\nhelp command for details about a command\n' + return res + + @asyncio.coroutine + def _parse_command(self, text): + cmd = text.split(' ') + found = False + if cmd[0] == '?': + cmd[0] = 'help' + for (name, meth) in inspect.getmembers(self): + if name == cmd[0]: + cmd.pop(0) + res = yield from meth(*cmd) + found = True + break + if not found: + res = ('Command not found {}'.format(cmd[0]) + (yield from self.help())) + return res + + @asyncio.coroutine + def run(self): + if self._welcome_message: + self._writer.feed_data(self._welcome_message.encode()) + while True: + self._writer.feed_data(self._prompt.encode()) + result = yield from self._reader.readline() + result = result.decode().strip('\n') + res = yield from self._parse_command(result) + self._writer.feed_data(res.encode()) + + +def create_telnet_shell(shell, loop=None): + """ + Run a shell application with a telnet frontend + + :param application: An EmbedShell instance + :param loop: The event loop + :returns: Telnet server + """ + class Stream(asyncio.StreamReader): + + def write(self, data): + self.feed_data(data) + + @asyncio.coroutine + def drain(self): + pass + shell.reader = Stream() + shell.writer = Stream() + if loop is None: + loop = asyncio.get_event_loop() + loop.create_task(shell.run()) + return AsyncioTelnetServer(reader=shell.writer, writer=shell.reader, binary=False, echo=False) + + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + + class Demo(EmbedShell): + + @asyncio.coroutine + def hello(self, *args): + """ + Hello world + + This command accept arguments: hello tutu will display tutu + """ + if len(args): + return ' '.join(args) + else: + return 'world\n' + + # Demo using telnet + server = create_telnet_shell(Demo()) + coro = asyncio.start_server(server.run, '127.0.0.1', 4444, loop=loop) + s = loop.run_until_complete(coro) + try: + loop.run_forever() + except KeyboardInterrupt: + pass + + # Demo using stdin + # @asyncio.coroutine + # def feed_stdin(loop, reader): + # while True: + # line = yield from loop.run_in_executor(None, sys.stdin.readline) + # reader.feed_data(line.encode()) + # + # @asyncio.coroutine + # def read_stdout(writer): + # while True: + # c = yield from writer.read(1) + # print(c.decode(), end='') + # sys.stdout.flush() + # + # reader = asyncio.StreamReader() + # writer = asyncio.StreamReader() + # shell = Demo(reader, writer, loop=loop) + # + # reader_task = loop.create_task(feed_stdin(loop, reader)) + # writer_task = loop.create_task(read_stdout(writer)) + # shell_task = loop.create_task(shell.run()) + # loop.run_until_complete(asyncio.gather(shell_task, writer_task, reader_task)) + # loop.close() diff --git a/tests/compute/dynamips/test_ethernet_switch.py b/tests/compute/dynamips/test_ethernet_switch.py new file mode 100644 index 00000000..9e6dbcb3 --- /dev/null +++ b/tests/compute/dynamips/test_ethernet_switch.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# Copyright (C) 2017 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.utils import AsyncioMagicMock +from gns3server.compute.dynamips.nodes.ethernet_switch import EthernetSwitchConsole + + +def test_arp_command(async_run): + node = AsyncioMagicMock() + node.name = "Test" + node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 nio1", "0050.7966.6802 1 nio2"]) + console = EthernetSwitchConsole(node) + assert async_run(console.arp()) == \ + "Mac VLAN\n" \ + "00:50:79:66:68:01 1\n" \ + "00:50:79:66:68:02 1\n" + node._hypervisor.send.assert_called_with("ethsw show_mac_addr_table Test") diff --git a/tests/utils/asyncio/test_embed_shell.py b/tests/utils/asyncio/test_embed_shell.py new file mode 100644 index 00000000..29169ca7 --- /dev/null +++ b/tests/utils/asyncio/test_embed_shell.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# Copyright (C) 2017 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import asyncio + +from gns3server.utils.asyncio.embed_shell import EmbedShell + + +def test_embed_shell_help(async_run): + class Application(EmbedShell): + + @asyncio.coroutine + def hello(self): + """ + The hello world function + + The hello usage + """ + pass + reader = asyncio.StreamReader() + writer = asyncio.StreamReader() + app = Application(reader, writer) + assert async_run(app._parse_command('help')) == 'Help:\nhello: The hello world function\nhelp: Show help\n\nhelp command for details about a command\n' + assert async_run(app._parse_command('?')) == 'Help:\nhello: The hello world function\nhelp: Show help\n\nhelp command for details about a command\n' + assert async_run(app._parse_command('? hello')) == 'hello: The hello world function\n\nThe hello usage\n' + + +def test_embed_shell_execute(async_run): + class Application(EmbedShell): + + @asyncio.coroutine + def hello(self): + """ + The hello world function + + The hello usage + """ + return 'world' + reader = asyncio.StreamReader() + writer = asyncio.StreamReader() + app = Application(reader, writer) + assert async_run(app._parse_command('hello')) == 'world' + + +def test_embed_shell_welcome(async_run, loop): + reader = asyncio.StreamReader() + writer = asyncio.StreamReader() + app = EmbedShell(reader, writer, welcome_message="Hello") + t = loop.create_task(app.run()) + assert async_run(writer.read(5)) == b"Hello" + t.cancel() + + +def test_embed_shell_prompt(async_run, loop): + reader = asyncio.StreamReader() + writer = asyncio.StreamReader() + app = EmbedShell(reader, writer) + app.prompt = "gbash# " + t = loop.create_task(app.run()) + assert async_run(writer.read(7)) == b"gbash# " + t.cancel() From ca3f8d0b43d9826636115b44ffb40247a1a34c3a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 2 Jun 2017 14:50:34 +0200 Subject: [PATCH 2/3] Provide easy to test a shell by using stdin as an input --- gns3server/utils/asyncio/embed_shell.py | 71 +++++++++++++++---------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/gns3server/utils/asyncio/embed_shell.py b/gns3server/utils/asyncio/embed_shell.py index ccd3520c..fac81d3f 100644 --- a/gns3server/utils/asyncio/embed_shell.py +++ b/gns3server/utils/asyncio/embed_shell.py @@ -136,6 +136,39 @@ def create_telnet_shell(shell, loop=None): return AsyncioTelnetServer(reader=shell.writer, writer=shell.reader, binary=False, echo=False) +def create_stdin_shell(shell, loop=None): + """ + Run a shell application with a stdin frontend + + :param application: An EmbedShell instance + :param loop: The event loop + :returns: Telnet server + """ + @asyncio.coroutine + def feed_stdin(loop, reader): + while True: + line = yield from loop.run_in_executor(None, sys.stdin.readline) + reader.feed_data(line.encode()) + + @asyncio.coroutine + def read_stdout(writer): + while True: + c = yield from writer.read(1) + print(c.decode(), end='') + sys.stdout.flush() + + reader = asyncio.StreamReader() + writer = asyncio.StreamReader() + shell.reader = reader + shell.writer = writer + if loop is None: + loop = asyncio.get_event_loop() + + reader_task = loop.create_task(feed_stdin(loop, reader)) + writer_task = loop.create_task(read_stdout(writer)) + shell_task = loop.create_task(shell.run()) + return asyncio.gather(shell_task, writer_task, reader_task) + if __name__ == '__main__': loop = asyncio.get_event_loop() @@ -154,34 +187,14 @@ if __name__ == '__main__': return 'world\n' # Demo using telnet - server = create_telnet_shell(Demo()) - coro = asyncio.start_server(server.run, '127.0.0.1', 4444, loop=loop) - s = loop.run_until_complete(coro) - try: - loop.run_forever() - except KeyboardInterrupt: - pass + # server = create_telnet_shell(Demo()) + # coro = asyncio.start_server(server.run, '127.0.0.1', 4444, loop=loop) + # s = loop.run_until_complete(coro) + # try: + # loop.run_forever() + # except KeyboardInterrupt: + # pass # Demo using stdin - # @asyncio.coroutine - # def feed_stdin(loop, reader): - # while True: - # line = yield from loop.run_in_executor(None, sys.stdin.readline) - # reader.feed_data(line.encode()) - # - # @asyncio.coroutine - # def read_stdout(writer): - # while True: - # c = yield from writer.read(1) - # print(c.decode(), end='') - # sys.stdout.flush() - # - # reader = asyncio.StreamReader() - # writer = asyncio.StreamReader() - # shell = Demo(reader, writer, loop=loop) - # - # reader_task = loop.create_task(feed_stdin(loop, reader)) - # writer_task = loop.create_task(read_stdout(writer)) - # shell_task = loop.create_task(shell.run()) - # loop.run_until_complete(asyncio.gather(shell_task, writer_task, reader_task)) - # loop.close() + loop.run_until_complete(create_stdin_shell(Demo())) + loop.close() From ad850f3857a1746c0e416a98eb6f7a49fee9dc32 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 22 Jun 2017 00:23:33 +0200 Subject: [PATCH 3/3] Add port number in arp command output for Ethernet switch. --- gns3server/compute/dynamips/nodes/ethernet_switch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/dynamips/nodes/ethernet_switch.py b/gns3server/compute/dynamips/nodes/ethernet_switch.py index 2cb80243..6e17246f 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_switch.py +++ b/gns3server/compute/dynamips/nodes/ethernet_switch.py @@ -40,7 +40,7 @@ class EthernetSwitchConsole(EmbedShell): """ def __init__(self, node): - super().__init__(welcome_message="Welcome to GNS3 builtin ethernet switch.\n\nType help to get help\n") + super().__init__(welcome_message="Welcome to GNS3 builtin Ethernet switch.\n\nType help for available commands\n") self._node = node @asyncio.coroutine @@ -48,10 +48,10 @@ class EthernetSwitchConsole(EmbedShell): """ Show arp table """ - res = 'Mac VLAN\n' + res = 'Port Mac VLAN\n' result = (yield from self._node._hypervisor.send('ethsw show_mac_addr_table {}'.format(self._node.name))) for line in result: - mac, vlan, _ = line.replace(' ', ' ').split(' ') + mac, vlan, nio = line.replace(' ', ' ').split(' ') mac = mac.replace('.', '') mac = "{}:{}:{}:{}:{}:{}".format( mac[0:2], @@ -60,7 +60,10 @@ class EthernetSwitchConsole(EmbedShell): mac[6:8], mac[8:10], mac[10:12]) - res += mac + ' ' + vlan + '\n' + for port_number, switch_nio in self._node.nios.items(): + if switch_nio.name == nio: + res += 'Ethernet' + str(port_number) + ' ' + mac + ' ' + vlan + '\n' + break return res