From 6c7ca7f24ee434e9b582d1ea3dd0d7eb95e98e9e Mon Sep 17 00:00:00 2001 From: Steve McIntyre Date: Mon, 5 Mar 2018 18:15:51 +0000 Subject: Re-organise the source tree Make this look more like a typical python source tree: * Move all the module code under a new Vland directory * Rename vland.py to vland * Remove lots of old hacky path-mangling code Change-Id: I6ec18ab5af69db1c213b1ceaddc6a0c4b52baad8 --- Vland/__init__.py | 0 Vland/config/__init__.py | 23 + Vland/config/config.py | 270 +++++++++ Vland/config/test-clashing-ports.cfg | 33 ++ Vland/config/test-invalid-DB.cfg | 5 + Vland/config/test-invalid-logging-level.cfg | 30 + Vland/config/test-invalid-vland.cfg | 11 + Vland/config/test-known-good.cfg | 35 ++ Vland/config/test-missing-db-username.cfg | 5 + Vland/config/test-missing-dbname.cfg | 6 + Vland/config/test-reused-switch-names.cfg | 26 + Vland/config/test-unknown-section.cfg | 29 + Vland/config/test.py | 101 ++++ Vland/db/__init__.py | 0 Vland/db/db.py | 825 ++++++++++++++++++++++++++ Vland/db/init.doc | 5 + Vland/db/setup_db.py | 56 ++ Vland/drivers/CiscoCatalyst.py | 721 +++++++++++++++++++++++ Vland/drivers/CiscoSX300.py | 697 ++++++++++++++++++++++ Vland/drivers/Dummy.py | 361 ++++++++++++ Vland/drivers/Mellanox.py | 795 +++++++++++++++++++++++++ Vland/drivers/NetgearXSM.py | 782 +++++++++++++++++++++++++ Vland/drivers/TPLinkTLSG2XXX.py | 695 ++++++++++++++++++++++ Vland/drivers/__init__.py | 0 Vland/drivers/common.py | 167 ++++++ Vland/errors.py | 59 ++ Vland/ipc/__init__.py | 0 Vland/ipc/client-new.py | 18 + Vland/ipc/ipc.py | 177 ++++++ Vland/ipc/server-new.py | 24 + Vland/util.py | 871 ++++++++++++++++++++++++++++ Vland/visualisation/__init__.py | 0 Vland/visualisation/graphics.py | 484 ++++++++++++++++ Vland/visualisation/visualisation.py | 498 ++++++++++++++++ config/__init__.py | 23 - config/config.py | 275 --------- config/test-clashing-ports.cfg | 33 -- config/test-invalid-DB.cfg | 5 - config/test-invalid-logging-level.cfg | 30 - config/test-invalid-vland.cfg | 11 - config/test-known-good.cfg | 35 -- config/test-missing-db-username.cfg | 5 - config/test-missing-dbname.cfg | 6 - config/test-reused-switch-names.cfg | 26 - config/test-unknown-section.cfg | 29 - config/test.py | 101 ---- db/__init__.py | 0 db/db.py | 830 -------------------------- db/init.doc | 5 - db/setup_db.py | 56 -- drivers/CiscoCatalyst.py | 721 ----------------------- drivers/CiscoSX300.py | 697 ---------------------- drivers/Dummy.py | 361 ------------ drivers/Mellanox.py | 795 ------------------------- drivers/NetgearXSM.py | 782 ------------------------- drivers/TPLinkTLSG2XXX.py | 695 ---------------------- drivers/__init__.py | 0 drivers/common.py | 167 ------ errors.py | 59 -- ipc/__init__.py | 0 ipc/client-new.py | 18 - ipc/ipc.py | 179 ------ ipc/server-new.py | 24 - util.py | 871 ---------------------------- visualisation/__init__.py | 0 visualisation/graphics.py | 489 ---------------- visualisation/visualisation.py | 503 ---------------- vland | 232 ++++++++ vland-admin | 9 +- vland.py | 235 -------- vland.service | 2 +- 71 files changed, 8045 insertions(+), 8073 deletions(-) create mode 100644 Vland/__init__.py create mode 100644 Vland/config/__init__.py create mode 100644 Vland/config/config.py create mode 100644 Vland/config/test-clashing-ports.cfg create mode 100644 Vland/config/test-invalid-DB.cfg create mode 100644 Vland/config/test-invalid-logging-level.cfg create mode 100644 Vland/config/test-invalid-vland.cfg create mode 100644 Vland/config/test-known-good.cfg create mode 100644 Vland/config/test-missing-db-username.cfg create mode 100644 Vland/config/test-missing-dbname.cfg create mode 100644 Vland/config/test-reused-switch-names.cfg create mode 100644 Vland/config/test-unknown-section.cfg create mode 100644 Vland/config/test.py create mode 100644 Vland/db/__init__.py create mode 100644 Vland/db/db.py create mode 100644 Vland/db/init.doc create mode 100755 Vland/db/setup_db.py create mode 100644 Vland/drivers/CiscoCatalyst.py create mode 100644 Vland/drivers/CiscoSX300.py create mode 100644 Vland/drivers/Dummy.py create mode 100644 Vland/drivers/Mellanox.py create mode 100644 Vland/drivers/NetgearXSM.py create mode 100644 Vland/drivers/TPLinkTLSG2XXX.py create mode 100644 Vland/drivers/__init__.py create mode 100644 Vland/drivers/common.py create mode 100644 Vland/errors.py create mode 100644 Vland/ipc/__init__.py create mode 100644 Vland/ipc/client-new.py create mode 100644 Vland/ipc/ipc.py create mode 100644 Vland/ipc/server-new.py create mode 100644 Vland/util.py create mode 100644 Vland/visualisation/__init__.py create mode 100644 Vland/visualisation/graphics.py create mode 100644 Vland/visualisation/visualisation.py delete mode 100644 config/__init__.py delete mode 100644 config/config.py delete mode 100644 config/test-clashing-ports.cfg delete mode 100644 config/test-invalid-DB.cfg delete mode 100644 config/test-invalid-logging-level.cfg delete mode 100644 config/test-invalid-vland.cfg delete mode 100644 config/test-known-good.cfg delete mode 100644 config/test-missing-db-username.cfg delete mode 100644 config/test-missing-dbname.cfg delete mode 100644 config/test-reused-switch-names.cfg delete mode 100644 config/test-unknown-section.cfg delete mode 100644 config/test.py delete mode 100644 db/__init__.py delete mode 100644 db/db.py delete mode 100644 db/init.doc delete mode 100755 db/setup_db.py delete mode 100644 drivers/CiscoCatalyst.py delete mode 100644 drivers/CiscoSX300.py delete mode 100644 drivers/Dummy.py delete mode 100644 drivers/Mellanox.py delete mode 100644 drivers/NetgearXSM.py delete mode 100644 drivers/TPLinkTLSG2XXX.py delete mode 100644 drivers/__init__.py delete mode 100644 drivers/common.py delete mode 100644 errors.py delete mode 100644 ipc/__init__.py delete mode 100644 ipc/client-new.py delete mode 100644 ipc/ipc.py delete mode 100644 ipc/server-new.py delete mode 100644 util.py delete mode 100644 visualisation/__init__.py delete mode 100644 visualisation/graphics.py delete mode 100644 visualisation/visualisation.py create mode 100755 vland delete mode 100755 vland.py diff --git a/Vland/__init__.py b/Vland/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Vland/config/__init__.py b/Vland/config/__init__.py new file mode 100644 index 0000000..89e40d5 --- /dev/null +++ b/Vland/config/__init__.py @@ -0,0 +1,23 @@ +#! /usr/bin/python + +# Copyright 2014-2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# VLANd configuration module +# +# Set the defaults here, over-ride later +# diff --git a/Vland/config/config.py b/Vland/config/config.py new file mode 100644 index 0000000..802f881 --- /dev/null +++ b/Vland/config/config.py @@ -0,0 +1,270 @@ +#! /usr/bin/python + +# Copyright 2014-2015 Linaro Limited +# Author: Steve McIntyre +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# VLANd simple config parser +# + +import ConfigParser +import os, sys, re + +from Vland.errors import ConfigError + +def is_positive(text): + valid_true = ('1', 'y', 'yes', 't', 'true') + valid_false = ('0', 'n', 'no', 'f', 'false') + + if str(text) in valid_true or str(text).lower() in valid_true: + return True + elif str(text) in valid_false or str(text).lower() in valid_false: + return False + +def is_valid_logging_level(text): + valid = ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG') + if text in valid: + return True + return False + +class DaemonConfigClass: + """ Simple container for stuff to make for nicer syntax """ + + def __repr__(self): + return "" % (self.port) + +class DBConfigClass: + """ Simple container for stuff to make for nicer syntax """ + + def __repr__(self): + return "" % (self.server, self.port, self.dbname, self.username, self.password) + +class LoggingConfigClass: + """ Simple container for stuff to make for nicer syntax """ + + def __repr__(self): + return "" % (self.level, self.filename) + +class VisualisationConfigClass: + """ Simple container for stuff to make for nicer syntax """ + def __repr__(self): + return "" % (self.enabled, self.port) + +class SwitchConfigClass: + """ Simple container for stuff to make for nicer syntax """ + def __repr__(self): + return "" % (self.name, self.section, self.driver, self.username, self.password, self.enable_password) + +class VlanConfig: + """VLANd config class""" + def __init__(self, filenames): + + config = ConfigParser.RawConfigParser({ + # Set default values + 'dbname': None, + 'debug': False, + 'driver': None, + 'enable_password': None, + 'enabled': False, + 'name': None, + 'password': None, + 'port': None, + 'refresh': None, + 'server': None, + 'username': None, + }) + + config.read(filenames) + + # Parse out the config file + # Must have a [database] section + # May have a [vland] section + # May have a [logging] section + # May have multiple [switch 'foo'] sections + if not config.has_section('database'): + raise ConfigError('No database configuration section found') + + # No DB-specific defaults to set + self.database = DBConfigClass() + + # Set defaults logging details + self.logging = LoggingConfigClass() + self.logging.level = 'CRITICAL' + self.logging.filename = None + + # Set default port number and VLAN tag + self.vland = DaemonConfigClass() + self.vland.port = 3080 + self.vland.default_vlan_tag = 1 + + # Visualisation is disabled by default + self.visualisation = VisualisationConfigClass() + self.visualisation.port = 3081 + self.visualisation.enabled = False + + # No switch-specific defaults to set + self.switches = {} + + sw_regex = re.compile(r'(switch)\ (.*)', flags=re.I) + for section in config.sections(): + if section == 'database': + try: + self.database.server = config.get(section, 'server') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid database configuration (server)') + + try: + port = config.get(section, 'port') + if port is not None: + self.database.port = config.getint(section, 'port') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid database configuration (port)') + + try: + self.database.dbname = config.get(section, 'dbname') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid database configuration (dbname)') + + try: + self.database.username = config.get(section, 'username') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid database configuration (username)') + + try: + self.database.password = config.get(section, 'password') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid database configuration (password)') + + # Other database config options are optional, but these are not + if self.database.dbname is None or self.database.username is None: + raise ConfigError('Database configuration section incomplete') + + elif section == 'logging': + try: + self.logging.level = config.get(section, 'level') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid logging configuration (level)') + self.logging.level = self.logging.level.upper() + if not is_valid_logging_level(self.logging.level): + raise ConfigError('Invalid logging configuration (level)') + + try: + self.logging.filename = config.get(section, 'filename') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid logging configuration (filename)') + + elif section == 'vland': + try: + self.vland.port = config.getint(section, 'port') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid vland configuration (port)') + + try: + self.vland.default_vlan_tag = config.getint(section, 'default_vlan_tag') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid vland configuration (default_vlan_tag)') + + elif section == 'visualisation': + try: + self.visualisation.port = config.getint(section, 'port') + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid visualisation configuration (port)') + + try: + self.visualisation.enabled = config.get(section, 'enabled') + if not is_positive(self.visualisation.enabled): + self.visualisation.enabled = False + elif is_positive(self.visualisation.enabled): + self.visualisation.enabled = True + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid visualisation configuration (enabled)') + + try: + self.visualisation.refresh = config.get(section, 'refresh') + if self.visualisation.refresh is not None: + if not is_positive(self.visualisation.refresh): + self.visualisation.refresh = None + else: + self.visualisation.refresh = int(self.visualisation.refresh) + except ConfigParser.NoOptionError: + pass + except: + raise ConfigError('Invalid visualisation configuration (refresh)') + + else: + match = sw_regex.match(section) + if match: + # Constraint: switch names must be unique! See if + # there's already a switch with this name + name = config.get(section, 'name') + for key in self.switches.keys(): + if name == key: + raise ConfigError('Found switches with the same name (%s)' % name) + self.switches[name] = SwitchConfigClass() + self.switches[name].name = name + self.switches[name].section = section + self.switches[name].driver = config.get(section, 'driver') + self.switches[name].username = config.get(section, 'username') + self.switches[name].password = config.get(section, 'password') + self.switches[name].enable_password = config.get(section, 'enable_password') + self.switches[name].debug = config.get(section, 'debug') + if not is_positive(self.switches[name].debug): + self.switches[name].debug = False + elif is_positive(self.switches[name].debug): + self.switches[name].debug = True + else: + raise ConfigError('Invalid vland configuration (switch "%s", debug "%s"' % (name, self.switches[name].debug)) + else: + raise ConfigError('Unrecognised config section %s' % section) + + # Generic checking for config values + if self.visualisation.enabled: + if self.visualisation.port == self.vland.port: + raise ConfigError('Invalid configuration: VLANd and the visualisation service must use distinct port numbers') + + def __del__(self): + pass + +if __name__ == '__main__': + c = VlanConfig(filenames=('./vland.cfg',)) + print c.database + print c.vland + for switch in c.switches: + print c.switches[switch] + diff --git a/Vland/config/test-clashing-ports.cfg b/Vland/config/test-clashing-ports.cfg new file mode 100644 index 0000000..46b2f3e --- /dev/null +++ b/Vland/config/test-clashing-ports.cfg @@ -0,0 +1,33 @@ +[database] +server = foo +port = 123 +dbname = vland +username = vland +password = vland + +[switch foo] +name = 10.172.2.51 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch bar] +name = 10.172.2.52 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch baz] +name = baz +driver = CiscoSX300 +username = cisco +password = cisco + +[vland] +port = 245 + +[visualisation] +enabled = yes +port = 245 diff --git a/Vland/config/test-invalid-DB.cfg b/Vland/config/test-invalid-DB.cfg new file mode 100644 index 0000000..a80fcbc --- /dev/null +++ b/Vland/config/test-invalid-DB.cfg @@ -0,0 +1,5 @@ +[database] +port = bar +dbname = vland +username = vland + diff --git a/Vland/config/test-invalid-logging-level.cfg b/Vland/config/test-invalid-logging-level.cfg new file mode 100644 index 0000000..d2de278 --- /dev/null +++ b/Vland/config/test-invalid-logging-level.cfg @@ -0,0 +1,30 @@ +[database] +server = foo +port = 123 +dbname = vland +username = vland +password = vland + +[switch foo] +name = 10.172.2.51 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch bar] +name = 10.172.2.52 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch baz] +name = baz +driver = CiscoSX300 +username = cisco +password = cisco + +[logging] +level = bibble + diff --git a/Vland/config/test-invalid-vland.cfg b/Vland/config/test-invalid-vland.cfg new file mode 100644 index 0000000..4478e7a --- /dev/null +++ b/Vland/config/test-invalid-vland.cfg @@ -0,0 +1,11 @@ +[database] +server = foo +port = 123 +dbname = vland +username = vland +password = vland + +[vland] +port = foo +default_vlan_tag = bar + diff --git a/Vland/config/test-known-good.cfg b/Vland/config/test-known-good.cfg new file mode 100644 index 0000000..bd060db --- /dev/null +++ b/Vland/config/test-known-good.cfg @@ -0,0 +1,35 @@ +# Example config for VLANd + +[database] +server=foo +port=123 +dbname = vland +username = user +password= pass + +[vland] +port = 9997 +default_vlan_tag = 42 + +[switch foo] +name = 10.172.2.51 +driver = CiscoSX300 +username = cisco_user +password = cisco_pass +enable_password = foobar + +[switch bar] +name = 10.172.2.52 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch baz] +name = baz +driver = CiscoSX300 +username = cisco +password = cisco + +[logging] + diff --git a/Vland/config/test-missing-db-username.cfg b/Vland/config/test-missing-db-username.cfg new file mode 100644 index 0000000..160934c --- /dev/null +++ b/Vland/config/test-missing-db-username.cfg @@ -0,0 +1,5 @@ +[database] +server = foo +port = 123 +dbname = vland +password = vland diff --git a/Vland/config/test-missing-dbname.cfg b/Vland/config/test-missing-dbname.cfg new file mode 100644 index 0000000..ddc7168 --- /dev/null +++ b/Vland/config/test-missing-dbname.cfg @@ -0,0 +1,6 @@ +[database] +server = foo +port = 123 +username = vland +password = vland + diff --git a/Vland/config/test-reused-switch-names.cfg b/Vland/config/test-reused-switch-names.cfg new file mode 100644 index 0000000..2d8485a --- /dev/null +++ b/Vland/config/test-reused-switch-names.cfg @@ -0,0 +1,26 @@ +[database] +server = foo +port = 123 +dbname = vland +username = vland +password = vland + +[switch foo] +name = 10.172.2.51 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch bar] +name = 10.172.2.51 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch baz] +name = baz +driver = CiscoSX300 +username = cisco +password = cisco diff --git a/Vland/config/test-unknown-section.cfg b/Vland/config/test-unknown-section.cfg new file mode 100644 index 0000000..446e0a5 --- /dev/null +++ b/Vland/config/test-unknown-section.cfg @@ -0,0 +1,29 @@ +[database] +server = foo +port = 123 +dbname = vland +username = vland +password = vland + +[switch foo] +name = 10.172.2.51 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch bar] +name = 10.172.2.52 +driver = CiscoSX300 +username = cisco +password = cisco +#enable_password = + +[switch baz] +name = baz +driver = CiscoSX300 +username = cisco +password = cisco + +[bibble] +foo = 1 diff --git a/Vland/config/test.py b/Vland/config/test.py new file mode 100644 index 0000000..046fbb2 --- /dev/null +++ b/Vland/config/test.py @@ -0,0 +1,101 @@ +import unittest, os, sys +vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) +sys.path.insert(0, vlandpath) +sys.path.insert(0, "%s/../.." % vlandpath) + +os.chdir(vlandpath) + +from Vland.config.config import VlanConfig +from Vland.errors import ConfigError + +class MyTest(unittest.TestCase): + + # Check that we raise on missing database section + def test_missing_database_section(self): + with self.assertRaisesRegexp(ConfigError, 'No database'): + config = VlanConfig(filenames=("/dev/null",)) + del config + + # Check that we raise on broken database config values + def test_missing_database_config(self): + with self.assertRaisesRegexp(ConfigError, 'Invalid database'): + config = VlanConfig(filenames=("test-invalid-DB.cfg",)) + del config + + # Check that we raise on missing database config values + def test_missing_dbname(self): + with self.assertRaisesRegexp(ConfigError, 'Database.*incomplete'): + config = VlanConfig(filenames=("test-missing-dbname.cfg",)) + del config + + # Check that we raise on missing database config values + def test_missing_db_username(self): + with self.assertRaisesRegexp(ConfigError, 'Database.*incomplete'): + config = VlanConfig(filenames=("test-missing-db-username.cfg",)) + del config + + # Check that we raise on broken vland config values + def test_missing_vlan_config(self): + with self.assertRaisesRegexp(ConfigError, 'Invalid vland'): + config = VlanConfig(filenames=("test-invalid-vland.cfg",)) + del config + + # Check that we raise on broken logging level + def test_invalid_logging_level(self): + with self.assertRaisesRegexp(ConfigError, 'Invalid logging.*level'): + config = VlanConfig(filenames=("test-invalid-logging-level.cfg",)) + del config + + # Check that we raise when VLANd and visn are configured for the + # same port + def test_clashing_ports(self): + with self.assertRaisesRegexp(ConfigError, 'Invalid.*distinct port'): + config = VlanConfig(filenames=("test-clashing-ports.cfg",)) + del config + + # Check that we raise on repeated switch names + def test_missing_repeated_switch_names(self): + with self.assertRaisesRegexp(ConfigError, 'same name'): + config = VlanConfig(filenames=("test-reused-switch-names.cfg",)) + del config + + # Check that we raise on unknown config section + def test_unknown_config(self): + with self.assertRaisesRegexp(ConfigError, 'Unrecognised config'): + config = VlanConfig(filenames=("test-unknown-section.cfg",)) + del config + + # Check we get expected values on a known-good config + def test_known_good(self): + config = VlanConfig(filenames=("test-known-good.cfg",)) + self.assertEqual(config.database.server, 'foo') + self.assertEqual(config.database.port, 123) + self.assertEqual(config.database.dbname, 'vland') + self.assertEqual(config.database.username, 'user') + self.assertEqual(config.database.password, 'pass') + + self.assertEqual(config.vland.port, 9997) + self.assertEqual(config.vland.default_vlan_tag, 42) + + self.assertEqual(len(config.switches), 3) + self.assertEqual(config.switches["10.172.2.51"].name, '10.172.2.51') + self.assertEqual(config.switches["10.172.2.51"].driver, 'CiscoSX300') + self.assertEqual(config.switches["10.172.2.51"].username, 'cisco_user') + self.assertEqual(config.switches["10.172.2.51"].password, 'cisco_pass') + self.assertEqual(config.switches["10.172.2.51"].enable_password, 'foobar') + self.assertEqual(config.switches["10.172.2.51"].driver, 'CiscoSX300') + + self.assertEqual(config.switches["10.172.2.52"].name, '10.172.2.52') + self.assertEqual(config.switches["10.172.2.52"].driver, 'CiscoSX300') + self.assertEqual(config.switches["10.172.2.52"].username, 'cisco') + self.assertEqual(config.switches["10.172.2.52"].password, 'cisco') + self.assertEqual(config.switches["10.172.2.52"].enable_password, None) + self.assertEqual(config.switches["10.172.2.52"].driver, 'CiscoSX300') + + self.assertEqual(config.switches["baz"].name, 'baz') + self.assertEqual(config.switches["baz"].driver, 'CiscoSX300') + self.assertEqual(config.switches["baz"].username, 'cisco') + self.assertEqual(config.switches["baz"].password, 'cisco') + +if __name__ == '__main__': + unittest.main() diff --git a/Vland/db/__init__.py b/Vland/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Vland/db/db.py b/Vland/db/db.py new file mode 100644 index 0000000..d5b541b --- /dev/null +++ b/Vland/db/db.py @@ -0,0 +1,825 @@ +#! /usr/bin/python + +# Copyright 2014-2018 Linaro Limited +# Authors: Dave Pigott , +# Steve McIntyre +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import psycopg2 +import psycopg2.extras +import datetime, os, sys +import logging + +TRUNK_ID_NONE = -1 + +# The schema version that this code expects. If it finds an older version (or +# no version!) at startup, it will auto-migrate to the latest version +# +# Version 0: Base, no version found +# +# Version 1: No changes, except adding the version and coping with upgrade +# +# Version 2: Add "lock_reason" field in the port table, and code to deal with +# it +DATABASE_SCHEMA_VERSION = 2 + +from Vland.errors import CriticalError, InputError, NotFoundError + +class VlanDB: + def __init__(self, db_name="vland", username="vland", readonly=True): + try: + self.connection = psycopg2.connect(database=db_name, user=username) + # Create first cursor for normal usage - returns tuples + self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor) + # Create second cursor for full-row lookups - returns a dict + # instead, much more useful in the admin interface + self.dictcursor = self.connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + if not readonly: + self._init_state() + except Exception as e: + logging.error("Failed to access database: %s", e) + raise + + def __del__(self): + self.cursor.close() + self.dictcursor.close() + self.connection.close() + + # Create the state table (if needed) and add its only record + # + # Use the stored record of the expected database schema to track what + # version the on-disk database is, and upgrade it to match the current code + # if necessary. + def _init_state(self): + found_db = False + current_db_version = 0 + try: + sql = "SELECT * FROM state" + self.cursor.execute(sql) + found_db = True + except psycopg2.ProgrammingError: + self.connection.commit() # state doesn't exist; clear error + sql = "CREATE TABLE state (last_modified TIMESTAMP, schema_version INTEGER)" + self.cursor.execute(sql) + # We've just created a version 1 database + current_db_version = 1 + + if found_db: + # Grab the version of the database we have + try: + sql = "SELECT schema_version FROM state" + self.cursor.execute(sql) + current_db_version = self.cursor.fetchone()[0] + # No version found ==> we have "version 0" + except psycopg2.ProgrammingError: + self.connection.commit() # state doesn't exist; clear error + current_db_version = 0 + + # Now delete the existing state record, we'll write a new one in a + # moment + self.cursor.execute('DELETE FROM state') + logging.info("Found a database, version %d", current_db_version) + + # Apply upgrades here! + if current_db_version < 1: + logging.info("Upgrading database to match schema version 1") + sql = "ALTER TABLE state ADD schema_version INTEGER" + self.cursor.execute(sql) + logging.info("Schema version 1 upgrade successful") + + if current_db_version < 2: + logging.info("Upgrading database to match schema version 2") + sql = "ALTER TABLE port ADD lock_reason VARCHAR(64)" + self.cursor.execute(sql) + logging.info("Schema version 2 upgrade successful") + + sql = "INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)" + data = (datetime.datetime.now(), DATABASE_SCHEMA_VERSION) + self.cursor.execute(sql, data) + self.connection.commit() + + # Create a new switch in the database. Switches are really simple + # devices - they're just containers for ports. + # + # Constraints: + # Switches must be uniquely named + def create_switch(self, name): + + switch_id = self.get_switch_id_by_name(name) + if switch_id is not None: + raise InputError("Switch name %s already exists" % name) + + try: + sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id" + data = (name, ) + self.cursor.execute(sql, data) + switch_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + + return switch_id + + # Create a new port in the database. Three of the fields are + # created with default values (is_locked, is_trunk, trunk_id) + # here, and should be updated separately if desired. For the + # current_vlan_id and base_vlan_id fields, *BE CAREFUL* that you + # have already looked up the correct VLAN_ID for each. This is + # *NOT* the same as the VLAN tag (likely to be 1). You Have Been + # Warned! + # + # Constraints: + # 1. The switch referred to must already exist + # 2. The VLANs mentioned here must already exist + # 3. (Switch/name) must be unique + # 4. (Switch/number) must be unique + def create_port(self, switch_id, name, number, current_vlan_id, base_vlan_id): + + switch = self.get_switch_by_id(switch_id) + if switch is None: + raise NotFoundError("Switch ID %d does not exist" % int(switch_id)) + + for vlan_id in (current_vlan_id, base_vlan_id): + vlan = self.get_vlan_by_id(vlan_id) + if vlan is None: + raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) + + port_id = self.get_port_by_switch_and_name(switch_id, name) + if port_id is not None: + raise InputError("Already have a port %s on switch ID %d" % (name, int(switch_id))) + + port_id = self.get_port_by_switch_and_number(switch_id, int(number)) + if port_id is not None: + raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id))) + + try: + sql = "INSERT INTO port (name, number, switch_id, is_locked, lock_reason, is_trunk, current_vlan_id, base_vlan_id, trunk_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING port_id" + data = (name, number, switch_id, + False, "", + False, + current_vlan_id, base_vlan_id, TRUNK_ID_NONE) + self.cursor.execute(sql, data) + port_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + + return port_id + + # Create a new vlan in the database. We locally add a creation + # timestamp, for debug purposes. If vlans seems to be sticking + # around, we'll be able to see when they were created. + # + # Constraints: + # Names and tags must be unique + # Tags must be in the range 1-4095 (802.1q spec) + # Names can be any free-form text, length 1-32 characters + def create_vlan(self, name, tag, is_base_vlan): + + if int(tag) < 1 or int(tag) > 4095: + raise InputError("VLAN tag %d is outside of the valid range (1-4095)" % int(tag)) + + if (len(name) < 1) or (len(name) > 32): + raise InputError("VLAN name %s is invalid (must be 1-32 chars)" % name) + + vlan_id = self.get_vlan_id_by_name(name) + if vlan_id is not None: + raise InputError("VLAN name %s is already in use" % name) + + vlan_id = self.get_vlan_id_by_tag(tag) + if vlan_id is not None: + raise InputError("VLAN tag %d is already in use" % int(tag)) + + try: + dt = datetime.datetime.now() + sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id" + data = (name, tag, is_base_vlan, dt) + self.cursor.execute(sql, data) + vlan_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + + return vlan_id + + # Create a new trunk in the database, linking two ports. Trunks + # are really simple objects for our use - they're just containers + # for 2 ports. + # + # Constraints: + # 1. Both ports listed must already exist. + # 2. Both ports must be in trunk mode. + # 3. Both must not be locked. + # 4. Both must not already be in a trunk. + def create_trunk(self, port_id1, port_id2): + + for port_id in (port_id1, port_id2): + port = self.get_port_by_id(int(port_id)) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + if not port['is_trunk']: + raise InputError("Port ID %d is not in trunk mode" % int(port_id)) + if port['is_locked']: + raise InputError("Port ID %d is locked" % int(port_id)) + if port['trunk_id'] != TRUNK_ID_NONE: + raise InputError("Port ID %d is already on trunk ID %d" % (int(port_id), int(port['trunk_id']))) + + try: + # Add the trunk itself + dt = datetime.datetime.now() + sql = "INSERT INTO trunk (creation_time) VALUES (%s) RETURNING trunk_id" + data = (dt, ) + self.cursor.execute(sql, data) + trunk_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + # And update the ports + for port_id in (port_id1, port_id2): + self._set_port_trunk(port_id, trunk_id) + except: + self.delete_trunk(trunk_id) + raise + + return trunk_id + + # Internal helper function + def _delete_row(self, table, field, value): + try: + sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s') + data = (value,) + self.cursor.execute(sql, data) + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + + # Delete the specified switch + # + # Constraints: + # 1. The switch must exist + # 2. The switch may not be referenced by any ports - + # delete them first! + def delete_switch(self, switch_id): + switch = self.get_switch_by_id(switch_id) + if switch is None: + raise NotFoundError("Switch ID %d does not exist" % int(switch_id)) + ports = self.get_ports_by_switch(switch_id) + if ports is not None: + raise InputError("Cannot delete switch ID %d when it still has %d ports" % + (int(switch_id), len(ports))) + self._delete_row("switch", "switch_id", switch_id) + return switch_id + + # Delete the specified port + # + # Constraints: + # 1. The port must exist + # 2. The port must not be locked + def delete_port(self, port_id): + port = self.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + if port['is_locked']: + raise InputError("Cannot delete port ID %d as it is locked" % int(port_id)) + self._delete_row("port", "port_id", port_id) + return port_id + + # Delete the specified VLAN + # + # Constraints: + # 1. The VLAN must exist + # 2. The VLAN may not contain any ports - move or delete them first! + def delete_vlan(self, vlan_id): + vlan = self.get_vlan_by_id(vlan_id) + if vlan is None: + raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) + ports = self.get_ports_by_current_vlan(vlan_id) + if ports is not None: + raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % + (int(vlan_id), len(ports))) + ports = self.get_ports_by_base_vlan(vlan_id) + if ports is not None: + raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % + (int(vlan_id), len(ports))) + self._delete_row("vlan", "vlan_id", vlan_id) + return vlan_id + + # Delete the specified trunk + # + # Constraints: + # 1. The trunk must exist + # + # Any ports attached will be detached (i.e. moved to trunk TRUNK_ID_NONE) + def delete_trunk(self, trunk_id): + trunk = self.get_trunk_by_id(trunk_id) + if trunk is None: + raise NotFoundError("Trunk ID %d does not exist" % int(trunk_id)) + ports = self.get_ports_by_trunk(trunk_id) + for port_id in ports: + self._set_port_trunk(port_id, TRUNK_ID_NONE) + self._delete_row("trunk", "trunk_id", trunk_id) + return trunk_id + + # Find the lowest unused VLAN tag and return it + # + # Constraints: + # None + def find_lowest_unused_vlan_tag(self): + sql = "SELECT tag FROM vlan ORDER BY tag ASC" + self.cursor.execute(sql,) + + # Walk through the list, looking for gaps + last = 1 + result = None + + for record in self.cursor: + if (record[0] - last) > 1: + result = last + 1 + break + last = record[0] + + if result is None: + result = last + 1 + + if result > 4093: + raise CriticalError("Can't find any VLAN tags remaining for allocation!") + + return result + + # Grab one column from one row of a query on one column; useful as + # a quick wrapper + def _get_element(self, select_field, table, compare_field, value): + + if value is None: + raise ValueError("Asked to look up using None as a key in %s" % compare_field) + + # We really want to use psycopg's type handling deal with the + # (potentially) user-supplied data in the value field, so we + # have to pass (sql,data) through to cursor.execute. However, + # we can't have psycopg do all the argument substitution here + # as it will quote all the params like the table name. That + # doesn't work. So, we substitute a "%s" for "%s" here so we + # keep it after python's own string substitution. + sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s") + + # Now, the next icky thing: we need to make sure that we're + # passing a dict so that psycopg2 can pick it apart properly + # for its own substitution code. We force this with the + # trailing comma here + data = (value, ) + self.cursor.execute(sql, data) + + if self.cursor.rowcount > 0: + return self.cursor.fetchone()[0] + else: + return None + + # Grab one column from one row of a query on 2 columns; useful as + # a quick wrapper + def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2): + + if value1 is None: + raise ValueError("Asked to look up using None as a key in %s" % compare_field1) + if value2 is None: + raise ValueError("Asked to look up using None as a key in %s" % compare_field2) + + # We really want to use psycopg's type handling deal with the + # (potentially) user-supplied data in the value field, so we + # have to pass (sql,data) through to cursor.execute. However, + # we can't have psycopg do all the argument substitution here + # as it will quote all the params like the table name. That + # doesn't work. So, we substitute a "%s" for "%s" here so we + # keep it after python's own string substitution. + sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s") + + data = (value1, value2) + self.cursor.execute(sql, data) + + if self.cursor.rowcount > 0: + return self.cursor.fetchone()[0] + else: + return None + + # Grab one column from multiple rows of a query; useful as a quick + # wrapper + def _get_multi_elements(self, select_field, table, compare_field, value, sort_field): + + if value is None: + raise ValueError("Asked to look up using None as a key in %s" % compare_field) + + # We really want to use psycopg's type handling deal with the + # (potentially) user-supplied data in the value field, so we + # have to pass (sql,data) through to cursor.execute. However, + # we can't have psycopg do all the argument substitution here + # as it will quote all the params like the table name. That + # doesn't work. So, we substitute a "%s" for "%s" here so we + # keep it after python's own string substitution. + sql = "SELECT %s FROM %s WHERE %s = %s ORDER BY %s ASC" % (select_field, table, compare_field, "%s", sort_field) + + # Now, the next icky thing: we need to make sure that we're + # passing a dict so that psycopg2 can pick it apart properly + # for its own substitution code. We force this with the + # trailing comma here + data = (value, ) + self.cursor.execute(sql, data) + + if self.cursor.rowcount > 0: + results = [] + for record in self.cursor: + results.append(record[0]) + return results + else: + return None + + # Grab one column from multiple rows of a 2-part query; useful as + # a wrapper + def _get_multi_elements2(self, select_field, table, compare_field1, value1, compare_field2, value2, sort_field): + + if value1 is None: + raise ValueError("Asked to look up using None as a key in %s" % compare_field1) + if value2 is None: + raise ValueError("Asked to look up using None as a key in %s" % compare_field2) + + # We really want to use psycopg's type handling deal with the + # (potentially) user-supplied data in the value field, so we + # have to pass (sql,data) through to cursor.execute. However, + # we can't have psycopg do all the argument substitution here + # as it will quote all the params like the table name. That + # doesn't work. So, we substitute a "%s" for "%s" here so we + # keep it after python's own string substitution. + sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s ORDER by %s ASC" % (select_field, table, compare_field1, "%s", compare_field2, "%s", sort_field) + + data = (value1, value2) + self.cursor.execute(sql, data) + + if self.cursor.rowcount > 0: + results = [] + for record in self.cursor: + results.append(record[0]) + return results + else: + return None + + # Simple lookup: look up a switch by ID, and return all the + # details of that switch. + # + # Returns None on failure. + def get_switch_by_id(self, switch_id): + return self._get_row("switch", "switch_id", int(switch_id)) + + # Simple lookup: look up a switch by name, and return the ID of + # that switch. + # + # Returns None on failure. + def get_switch_id_by_name(self, name): + return self._get_element("switch_id", "switch", "name", name) + + # Simple lookup: look up a switch by ID, and return the name of + # that switch. + # + # Returns None on failure. + def get_switch_name_by_id(self, switch_id): + return self._get_element("name", "switch", "switch_id", int(switch_id)) + + # Simple lookup: look up a port by ID, and return all the details + # of that port. + # + # Returns None on failure. + def get_port_by_id(self, port_id): + return self._get_row("port", "port_id", int(port_id)) + + # Simple lookup: look up a switch by ID, and return the IDs of all + # the ports on that switch. + # + # Returns None on failure. + def get_ports_by_switch(self, switch_id): + return self._get_multi_elements("port_id", "port", "switch_id", int(switch_id), "port_id") + + # More complex lookup: look up all the trunk ports on a switch by + # ID + # + # Returns None on failure. + def get_trunk_port_names_by_switch(self, switch_id): + return self._get_multi_elements2("name", "port", "switch_id", int(switch_id), "is_trunk", True, "port_id") + + # Simple lookup: look up a port by its name and its parent switch + # by ID, and return the ID of the port. + # + # Returns None on failure. + def get_port_by_switch_and_name(self, switch_id, name): + return self._get_element2("port_id", "port", "switch_id", int(switch_id), "name", name) + + # Simple lookup: look up a port by its external name and its + # parent switch by ID, and return the ID of the port. + # + # Returns None on failure. + def get_port_by_switch_and_number(self, switch_id, number): + return self._get_element2("port_id", "port", "switch_id", int(switch_id), "number", int(number)) + + # Simple lookup: look up a port by ID, and return the current VLAN + # id of that port. + # + # Returns None on failure. + def get_current_vlan_id_by_port(self, port_id): + return self._get_element("current_vlan_id", "port", "port_id", int(port_id)) + + # Simple lookup: look up a port by ID, and return the mode of that port. + # + # Returns None on failure. + def get_port_mode(self, port_id): + is_trunk = self._get_element("is_trunk", "port", "port_id", int(port_id)) + if is_trunk is not None: + if is_trunk: + return "trunk" + else: + return "access" + return None + + # Simple lookup: look up a port by ID, and return the base VLAN + # id of that port. + # + # Returns None on failure. + def get_base_vlan_id_by_port(self, port_id): + return self._get_element("base_vlan_id", "port", "port_id", int(port_id)) + + # Simple lookup: look up a current VLAN by ID, and return the IDs + # of all the ports on that VLAN. + # + # Returns None on failure. + def get_ports_by_current_vlan(self, vlan_id): + return self._get_multi_elements("port_id", "port", "current_vlan_id", int(vlan_id), "port_id") + + # Simple lookup: look up a base VLAN by ID, and return the IDs + # of all the ports on that VLAN. + # + # Returns None on failure. + def get_ports_by_base_vlan(self, vlan_id): + return self._get_multi_elements("port_id", "port", "base_vlan_id", int(vlan_id), "port_id") + + # Simple lookup: look up a trunk by ID, and return the IDs of the + # ports on both ends of that trunk. + # + # Returns None on failure. + def get_ports_by_trunk(self, trunk_id): + return self._get_multi_elements("port_id", "port", "trunk_id", int(trunk_id), "port_id") + + # Simple lookup: look up a VLAN by ID, and return all the details + # of that VLAN. + # + # Returns None on failure. + def get_vlan_by_id(self, vlan_id): + return self._get_row("vlan", "vlan_id", int(vlan_id)) + + # Simple lookup: look up a VLAN by name, and return the ID of that + # VLAN. + # + # Returns None on failure. + def get_vlan_id_by_name(self, name): + return self._get_element("vlan_id", "vlan", "name", name) + + # Simple lookup: look up a VLAN by tag, and return the ID of that + # VLAN. + # + # Returns None on failure. + def get_vlan_id_by_tag(self, tag): + return self._get_element("vlan_id", "vlan", "tag", int(tag)) + + # Simple lookup: look up a VLAN by ID, and return the name of that + # VLAN. + # + # Returns None on failure. + def get_vlan_name_by_id(self, vlan_id): + return self._get_element("name", "vlan", "vlan_id", int(vlan_id)) + + # Simple lookup: look up a VLAN by ID, and return the tag of that + # VLAN. + # + # Returns None on failure. + def get_vlan_tag_by_id(self, vlan_id): + return self._get_element("tag", "vlan", "vlan_id", int(vlan_id)) + + # Simple lookup: look up a trunk by ID, and return all the details + # of that trunk. + # + # Returns None on failure. + def get_trunk_by_id(self, trunk_id): + return self._get_row("trunk", "trunk_id", int(trunk_id)) + + # Get the last-modified time for the database + def get_last_modified_time(self): + sql = "SELECT last_modified FROM state" + self.cursor.execute(sql) + return self.cursor.fetchone()[0] + + # Grab one row of a query on one column; useful as a quick wrapper + def _get_row(self, table, field, value): + + # We really want to use psycopg's type handling deal with the + # (potentially) user-supplied data in the value field, so we + # have to pass (sql,data) through to cursor.execute. However, + # we can't have psycopg do all the argument substitution here + # as it will quote all the params like the table name. That + # doesn't work. So, we substitute a "%s" for "%s" here so we + # keep it after python's own string substitution. + sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s") + + # Now, the next icky thing: we need to make sure that we're + # passing a dict so that psycopg2 can pick it apart properly + # for its own substitution code. We force this with the + # trailing comma here + data = (value, ) + self.dictcursor.execute(sql, data) + return self.dictcursor.fetchone() + + # (Un)Lock a port in the database. This can only be done through + # the admin interface, and will stop API users from modifying + # settings on the port. Use this to lock down ports that are used + # for PDUs and other core infrastructure + def set_port_is_locked(self, port_id, is_locked, lock_reason=""): + port = self.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + try: + sql = "UPDATE port SET is_locked=%s, lock_reason=%s WHERE port_id=%s RETURNING port_id" + data = (is_locked, lock_reason, port_id) + self.cursor.execute(sql, data) + port_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise InputError("lock failed on Port ID %d" % int(port_id)) + return port_id + + # Set the mode of a port in the database. Valid values for mode + # are "trunk" and "access" + def set_port_mode(self, port_id, mode): + port = self.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + if mode == "access": + is_trunk = False + elif mode == "trunk": + is_trunk = True + else: + raise InputError("Port mode %s is not valid" % mode) + try: + sql = "UPDATE port SET is_trunk=%s WHERE port_id=%s RETURNING port_id" + data = (is_trunk, port_id) + self.cursor.execute(sql, data) + port_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + return port_id + + # Set the current vlan of a port in the database. The VLAN is + # passed by ID. + # + # Constraints: + # 1. The port must already exist + # 2. The port must not be a trunk port + # 3. The port must not be locked + # 1. The VLAN must already exist in the database + def set_current_vlan(self, port_id, vlan_id): + port = self.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + + if port['is_trunk'] or port['is_locked']: + raise CriticalError("The port is locked") + + vlan = self.get_vlan_by_id(vlan_id) + if vlan is None: + raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) + + try: + sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s RETURNING port_id" + data = (vlan_id, port_id) + self.cursor.execute(sql, data) + port_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + return port_id + + # Set the base vlan of a port in the database. The VLAN is + # passed by ID. + # + # Constraints: + # 1. The port must already exist + # 2. The port must not be a trunk port + # 3. The port must not be locked + # 4. The VLAN must already exist in the database + def set_base_vlan(self, port_id, vlan_id): + port = self.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + + if port['is_trunk'] or port['is_locked']: + raise CriticalError("The port is locked") + + vlan = self.get_vlan_by_id(vlan_id) + if vlan is None: + raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) + if not vlan['is_base_vlan']: + raise InputError("VLAN ID %d is not a base VLAN" % int(vlan_id)) + + try: + sql = "UPDATE port SET base_vlan_id=%s WHERE port_id=%s RETURNING port_id" + data = (vlan_id, port_id) + self.cursor.execute(sql, data) + port_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + return port_id + + # Internal function: Attach a port to a trunk in the database. + # + # Constraints: + # 1. The port must already exist + # 2. The port must not be locked + def _set_port_trunk(self, port_id, trunk_id): + port = self.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % int(port_id)) + if port['is_locked']: + raise CriticalError("The port is locked") + try: + sql = "UPDATE port SET trunk_id=%s WHERE port_id=%s RETURNING port_id" + data = (int(trunk_id), int(port_id)) + self.cursor.execute(sql, data) + port_id = self.cursor.fetchone()[0] + self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) + self.connection.commit() + except: + self.connection.rollback() + raise + return port_id + + # Trivial helper function to return all the rows in a given table + def _dump_table(self, table, order): + result = [] + self.dictcursor.execute("SELECT * FROM %s ORDER by %s ASC" % (table, order)) + record = self.dictcursor.fetchone() + while record != None: + result.append(record) + record = self.dictcursor.fetchone() + return result + + def all_switches(self): + return self._dump_table("switch", "switch_id") + + def all_ports(self): + return self._dump_table("port", "port_id") + + def all_vlans(self): + return self._dump_table("vlan", "vlan_id") + + def all_trunks(self): + return self._dump_table("trunk", "trunk_id") + +if __name__ == '__main__': + db = VlanDB() + s = db.all_switches() + print 'The DB knows about %d switch(es)' % len(s) + print s + p = db.all_ports() + print 'The DB knows about %d port(s)' % len(p) + print p + v = db.all_vlans() + print 'The DB knows about %d vlan(s)' % len(v) + print v + t = db.all_trunks() + print 'The DB knows about %d trunks(s)' % len(t) + print t + + print 'First free VLAN tag is %d' % db.find_lowest_unused_vlan_tag() diff --git a/Vland/db/init.doc b/Vland/db/init.doc new file mode 100644 index 0000000..91a3841 --- /dev/null +++ b/Vland/db/init.doc @@ -0,0 +1,5 @@ +create linux user vland with password vland +create database user vland with password vland +create tables +create vlan1 + diff --git a/Vland/db/setup_db.py b/Vland/db/setup_db.py new file mode 100755 index 0000000..99cfaf4 --- /dev/null +++ b/Vland/db/setup_db.py @@ -0,0 +1,56 @@ +#! /usr/bin/python + +# Copyright 2014-2018 Linaro Limited +# Authors: Dave Pigott , +# Steve McIntyre +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +# First of all, create the vland user +# Next - create the vland database + +# Create the switch, port, vlan, trunk and state tables + +import datetime +from psycopg2 import connect + +DATABASE_SCHEMA_VERSION = 1 + +conn = connect(database="postgres", user="postgres", password="postgres") + +cur = conn.cursor() +cur.execute("CREATE USER vland WITH SUPERUSER") +cur.execute("CREATE DATABASE vland WITH OWNER = vland PASSWORD 'vland'") +conn.close() + +conn = connect(database="vland", user="vland", password="vland") +cur = conn.cursor() + +cur.execute("CREATE TABLE switch (switch_id SERIAL, name VARCHAR(64))") +cur.execute("CREATE TABLE port (port_id SERIAL, name VARCHAR(64)," + "switch_id INTEGER, is_locked BOOLEAN," + "is_trunk BOOLEAN, base_vlan_id INTEGER," + "current_vlan_id INTEGER, number INTEGER, trunk_id INTEGER)") +cur.execute("CREATE TABLE vlan (vlan_id SERIAL, name VARCHAR(32)," + "tag INTEGER, is_base_vlan BOOLEAN, creation_time TIMESTAMP)") +cur.execute("CREATE TABLE trunk (trunk_id SERIAL," + "creation_time TIMESTAMP)") +cur.execute("CREATE TABLE state (last_modified TIMESTAMP, schema_version INTEGER)") +cur.execute("INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)" % (datetime.datetime.now(), DATABASE_SCHEMA_VERSION)) +cur.execute("COMMIT;") + +# Do not make any more changes here - the database code will cope with upgrades +# from this V1 database as they're needed. diff --git a/Vland/drivers/CiscoCatalyst.py b/Vland/drivers/CiscoCatalyst.py new file mode 100644 index 0000000..16f47db --- /dev/null +++ b/Vland/drivers/CiscoCatalyst.py @@ -0,0 +1,721 @@ +#! /usr/bin/python + +# Copyright 2014-2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import sys +import re +import pexpect + +if __name__ == '__main__': + import os + vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) + sys.path.insert(0, vlandpath) + sys.path.insert(0, "%s/.." % vlandpath) + +from errors import InputError, PExpectError +from drivers.common import SwitchDriver, SwitchErrors + +class CiscoCatalyst(SwitchDriver): + + connection = None + _username = None + _password = None + _enable_password = None + + _capabilities = [ + 'TrunkWildCardVlans' # Trunk ports are on all VLANs by + # default, so we shouldn't need to + # bugger with them + ] + + # Regexp of expected hardware information - fail if we don't see + # this + _expected_descr_re = re.compile(r'WS-C\S+-\d+P') + + def __init__(self, switch_hostname, switch_telnetport=23, debug = False): + SwitchDriver.__init__(self, switch_hostname, debug) + self._systemdata = [] + self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) + self.errors = SwitchErrors() + + ################################ + ### Switch-level API functions + ################################ + + # Save the current running config into flash - we want config to + # remain across reboots + def switch_save_running_config(self): + try: + self._cli("copy running-config startup-config") + self.connection.expect("startup-config") + self._cli("startup-config") + self.connection.expect("OK") + except (PExpectError, pexpect.EOF): + # recurse on error + self._switch_connect() + self.switch_save_running_config() + + # Restart the switch - we need to reload config to do a + # roll-back. Do NOT save running-config first if the switch asks - + # we're trying to dump recent changes, not save them. + # + # This will also implicitly cause a connection to be closed + def switch_restart(self): + self._cli("reload") + index = self.connection.expect(['has been modified', 'Proceed']) + if index == 0: + self._cli("n") # No, don't save + self.connection.expect("Proceed") + + # Fall through + self._cli("y") # Yes, continue to reset + self.connection.close(True) + + # List the capabilities of the switch (and driver) - some things + # make no sense to abstract. Returns a dict of strings, each one + # describing an extra feature that that higher levels may care + # about + def switch_get_capabilities(self): + return self._capabilities + + ################################ + ### VLAN API functions + ################################ + + # Create a VLAN with the specified tag + def vlan_create(self, tag): + logging.debug("Creating VLAN %d", tag) + try: + self._configure() + self._cli("vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + return + raise IOError("Failed to create VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_create(tag) + + # Destroy a VLAN with the specified tag + def vlan_destroy(self, tag): + logging.debug("Destroying VLAN %d", tag) + + try: + self._configure() + self._cli("no vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + raise IOError("Failed to destroy VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_destroy(tag) + + # Set the name of a VLAN + def vlan_set_name(self, tag, name): + logging.debug("Setting name of VLAN %d to %s", tag, name) + + try: + self._configure() + self._cli("vlan %d" % tag) + self._cli("name %s" % name) + self._end_configure() + + # Validate it happened + read_name = self.vlan_get_name(tag) + if read_name != name: + raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" + % (tag, read_name, name)) + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_set_name(tag, name) + + # Get a list of the VLAN tags currently registered on the switch + def vlan_get_list(self): + logging.debug("Grabbing list of VLANs") + + try: + vlans = [] + + regex = re.compile(r'^ *(\d+).*(active)') + + self._cli("show vlan brief") + for line in self._read_long_output("show vlan brief"): + match = regex.match(line) + if match: + vlans.append(int(match.group(1))) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_list() + + # For a given VLAN tag, ask the switch what the associated name is + def vlan_get_name(self, tag): + + try: + logging.debug("Grabbing the name of VLAN %d", tag) + name = None + regex = re.compile(r'^ *\d+\s+(\S+).*(active)') + self._cli("show vlan id %d" % tag) + for line in self._read_long_output("show vlan id"): + match = regex.match(line) + if match: + name = match.group(1) + name.strip() + return name + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_name(tag) + + ################################ + ### Port API functions + ################################ + + # Set the mode of a port: access or trunk + def port_set_mode(self, port, mode): + logging.debug("Setting port %s to %s", port, mode) + if not self._is_port_mode_valid(mode): + raise InputError("Port mode %s is not allowed" % mode) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + + try: + self._configure() + self._cli("interface %s" % port) + if mode == "trunk": + self._cli("switchport trunk encapsulation dot1q") + self._cli("switchport trunk native vlan 1") + self._cli("switchport mode %s" % mode) + self._end_configure() + + # Validate it happened + read_mode = self._port_get_mode(port) + + if read_mode != mode: + raise IOError("Failed to set mode for port %s" % port) + + # And cache the result + self._port_modes[port] = mode + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_mode(port, mode) + + # Set an access port to be in a specified VLAN (tag) + def port_set_access_vlan(self, port, tag): + logging.debug("Setting access port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport access vlan %d" % tag) + self._cli("no shutdown") + self._end_configure() + + # Finally, validate things worked + read_vlan = int(self.port_get_access_vlan(port)) + if read_vlan != tag: + raise IOError("Failed to move access port %d to VLAN %d - got VLAN %d instead" + % (port, tag, read_vlan)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_access_vlan(port, tag) + + # Add a trunk port to a specified VLAN (tag) + def port_add_trunk_to_vlan(self, port, tag): + logging.debug("Adding trunk port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport trunk allowed vlan add %d" % tag) + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag or vlan == "ALL": + return + raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_add_trunk_to_vlan(port, tag) + + # Remove a trunk port from a specified VLAN (tag) + def port_remove_trunk_from_vlan(self, port, tag): + logging.debug("Removing trunk port %s from VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport trunk allowed vlan remove %d" % tag) + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag: + raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_remove_trunk_from_vlan(port, tag) + + # Get the configured VLAN tag for an access port (tag) + def port_get_access_vlan(self, port): + logging.debug("Getting VLAN for access port %s", port) + vlan = 1 + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + regex = re.compile(r'Access Mode VLAN: (\d+)') + + try: + self._cli("show interfaces %s switchport" % port) + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + vlan = match.group(1) + return int(vlan) + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_access_vlan(port) + + # Get the list of configured VLAN tags for a trunk port + def port_get_trunk_vlan_list(self, port): + logging.debug("Getting VLANs for trunk port %s", port) + vlans = [ ] + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + regex_start = re.compile('Trunking VLANs Enabled: (.*)') + regex_continue = re.compile(r'\s*(\d.*)') + + try: + self._cli("show interfaces %s switchport" % port) + + # Horrible parsing work - VLAN list may extend over several lines + in_match = False + vlan_text = '' + + for line in self._read_long_output("show interfaces switchport"): + if in_match: + match = regex_continue.match(line) + if match: + vlan_text += match.group(1) + else: + in_match = False + else: + match = regex_start.match(line) + if match: + vlan_text += match.group(1) + in_match = True + + vlans = self._parse_vlan_list(vlan_text) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_trunk_vlan_list(port) + + ################################ + ### Internal functions + ################################ + + # Connect to the switch and log in + def _switch_connect(self): + + if not self.connection is None: + self.connection.close(True) + self.connection = None + + logging.debug("Connecting to Switch with: %s", self.exec_string) + self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) + self._login() + + # Avoid paged output + self._cli("terminal length 0") + + # And grab details about the switch. in case we need it + self._get_systemdata() + + # And also validate them - make sure we're driving a switch of + # the correct model! Also store the serial number + descr_regex = re.compile(r'^cisco\s+(\S+)') + sn_regex = re.compile(r'System serial number\s+:\s+(\S+)') + descr = "" + + for line in self._systemdata: + match = descr_regex.match(line) + if match: + descr = match.group(1) + match = sn_regex.match(line) + if match: + self.serial_number = match.group(1) + + logging.debug("serial number is %s", self.serial_number) + logging.debug("system description is %s", descr) + + if not self._expected_descr_re.match(descr): + raise IOError("Switch %s not recognised by this driver: abort" % descr) + + # Now build a list of our ports, for later sanity checking + self._ports = self._get_port_names() + if len(self._ports) < 4: + raise IOError("Not enough ports detected - problem!") + + def _login(self): + logging.debug("attempting login with username %s, password %s", self._username, self._password) + self.connection.expect('User Access Verification') + if self._username is not None: + self.connection.expect("User Name:") + self._cli("%s" % self._username) + if self._password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._password, False) + while True: + index = self.connection.expect(['User Name:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) + if index != 4: # Any other means: failed to log in! + logging.error("Login failure: index %d\n", index) + logging.error("Login failure: %s\n", self.connection.match.before) + raise IOError + + # else + self._prompt_name = re.escape(self.connection.match.group(1).strip()) + if self.connection.match.group(2) == ">": + # Need to enter "enable" mode too + self._cli("enable") + if self._enable_password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._enable_password, False) + index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) + if index != 3: # Any other means: failed to log in! + logging.error("Enable password failure: %s\n", self.connection.match) + raise IOError + return 0 + + def _logout(self): + logging.debug("Logging out") + self._cli("exit", False) + self.connection.close(True) + + def _configure(self): + self._cli("configure terminal") + + def _end_configure(self): + self._cli("end") + + def _read_long_output(self, text): + prompt = self._prompt_name + '#' + try: + self.connection.expect(prompt) + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_in(text) + raise PExpectError("_read_long_output failed") + except: + logging.error("prompt is \"%s\"", prompt) + raise + + longbuf = [] + for line in self.connection.before.split('\r\n'): + longbuf.append(line.strip()) + return longbuf + + def _get_port_names(self): + logging.debug("Grabbing list of ports") + interfaces = [] + + # Use "connect" to only identify lines in the output that + # match interfaces - it will match lines with "connected" or + # "notconnect". + regex = re.compile(r'^\s*([a-zA-Z0-9_/]*).*(connect)(.*)') + # Deliberately drop things marked as "routed", i.e. the + # management port + regex2 = re.compile('.*routed.*') + + try: + self._cli("show interfaces status") + for line in self._read_long_output("show interfaces status"): + match = regex.match(line) + if match: + interface = match.group(1) + junk = match.group(3) + match2 = regex2.match(junk) + if not match2: + interfaces.append(interface) + self._port_numbers[interface] = len(interfaces) + logging.debug(" found %d ports on the switch", len(interfaces)) + return interfaces + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_port_names() + + # Get the mode of a port: access or trunk + def _port_get_mode(self, port): + logging.debug("Getting mode of port %s", port) + mode = '' + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + regex = re.compile('Administrative Mode: (.*)') + + try: + self._cli("show interfaces %s switchport" % port) + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + mode = match.group(1) + if mode == 'static access': + return 'access' + if mode == 'trunk': + return 'trunk' + # Needs special handling - it's the default port + # mode on these switches, and it doesn't + # interoperate with some other vendors. Sigh. + if mode == 'dynamic auto': + return 'dynamic' + return mode + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_mode(port) + + def _show_config(self): + logging.debug("Grabbing config") + try: + self._cli("show running-config") + return self._read_long_output("show running-config") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_config() + + def _show_clock(self): + logging.debug("Grabbing time") + try: + self._cli("show clock") + return self._read_long_output("show clock") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_clock() + + def _get_systemdata(self): + logging.debug("Grabbing system sw and hw versions") + + try: + self._systemdata = [] + self._cli("show version") + for line in self._read_long_output("show version"): + self._systemdata.append(line) + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_systemdata() + + def _parse_vlan_list(self, inputdata): + vlans = [] + + if inputdata == "ALL": + return ["ALL"] + elif inputdata == "NONE": + return [] + else: + # Parse the complex list + groups = inputdata.split(',') + for group in groups: + subgroups = group.split('-') + if len(subgroups) == 1: + vlans.append(int(subgroups[0])) + elif len(subgroups) == 2: + for i in range (int(subgroups[0]), int(subgroups[1]) + 1): + vlans.append(i) + else: + logging.debug("Can't parse group \"" + group + "\"") + + return vlans + + # Wrapper around connection.send - by default, expect() the same + # text we've sent, to remove it from the output from the + # switch. For the few cases where we don't need that, override + # this using echo=False. + # Horrible, but seems to work. + def _cli(self, text, echo=True): + self.connection.send(text + '\r') + if echo: + try: + self.connection.expect(text) + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_out(text) + raise PExpectError("_cli failed on %s" % text) + except: + logging.error("Unexpected error: %s", sys.exc_info()[0]) + raise + +if __name__ == "__main__": + + # Simple test harness - exercise the main working functions above to verify + # they work. This does *NOT* test really disruptive things like "save + # running-config" and "reload" - test those by hand. + + import optparse + + switch = 'vlandswitch01' + parser = optparse.OptionParser() + parser.add_option("--switch", + dest = "switch", + action = "store", + nargs = 1, + type = "string", + help = "specify switch to connect to for testing", + metavar = "") + (opts, args) = parser.parse_args() + if opts.switch: + switch = opts.switch + + logging.basicConfig(level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s') + p = CiscoCatalyst(switch, 23, debug=True) + p.switch_connect(None, 'lngvirtual', 'lngenable') + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.vlan_get_name(2) + print "VLAN 2 is named \"%s\"" % buf + + print "Create VLAN 3" + p.vlan_create(3) + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "Set name of VLAN 3 to test333" + p.vlan_set_name(3, "test333") + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + print "Destroy VLAN 3" + p.vlan_destroy(3) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.port_get_mode("Gi1/0/10") + print "Port Gi1/0/10 is in %s mode" % buf + + buf = p.port_get_mode("Gi1/0/11") + print "Port Gi1/0/11 is in %s mode" % buf + + # Test access stuff + print "Set Gi1/0/9 to access mode" + p.port_set_mode("Gi1/0/9", "access") + + print "Move Gi1/0/9 to VLAN 4" + p.port_set_access_vlan("Gi1/0/9", 4) + + buf = p.port_get_access_vlan("Gi1/0/9") + print "Read from switch: Gi1/0/9 is on VLAN %s" % buf + + print "Move Gi1/0/9 back to VLAN 1" + p.port_set_access_vlan("Gi1/0/9", 1) + + # Test access stuff + print "Set Gi1/0/9 to trunk mode" + p.port_set_mode("Gi1/0/9", "trunk") + print "Read from switch: which VLANs is Gi1/0/9 on?" + buf = p.port_get_trunk_vlan_list("Gi1/0/9") + p.dump_list(buf) + print "Add Gi1/0/9 to VLAN 2" + p.port_add_trunk_to_vlan("Gi1/0/9", 2) + print "Add Gi1/0/9 to VLAN 3" + p.port_add_trunk_to_vlan("Gi1/0/9", 3) + print "Add Gi1/0/9 to VLAN 4" + p.port_add_trunk_to_vlan("Gi1/0/9", 4) + print "Read from switch: which VLANs is Gi1/0/9 on?" + buf = p.port_get_trunk_vlan_list("Gi1/0/9") + p.dump_list(buf) + + p.port_remove_trunk_from_vlan("Gi1/0/9", 3) + p.port_remove_trunk_from_vlan("Gi1/0/9", 3) + p.port_remove_trunk_from_vlan("Gi1/0/9", 4) + print "Read from switch: which VLANs is Gi1/0/9 on?" + buf = p.port_get_trunk_vlan_list("Gi1/0/9") + p.dump_list(buf) + +# print 'Restarting switch, to explicitly reset config' +# p.switch_restart() + +# p.switch_save_running_config() + +# p.switch_disconnect() +# p._show_config() diff --git a/Vland/drivers/CiscoSX300.py b/Vland/drivers/CiscoSX300.py new file mode 100644 index 0000000..a6f5446 --- /dev/null +++ b/Vland/drivers/CiscoSX300.py @@ -0,0 +1,697 @@ +#! /usr/bin/python + +# Copyright 2014-2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import sys +import re +import pexpect + +if __name__ == '__main__': + import os + vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) + sys.path.insert(0, vlandpath) + sys.path.insert(0, "%s/.." % vlandpath) + +from errors import InputError, PExpectError +from drivers.common import SwitchDriver, SwitchErrors + +class CiscoSX300(SwitchDriver): + + connection = None + _username = None + _password = None + _enable_password = None + + # No extra capabilities for this switch/driver yet + _capabilities = [ + ] + + # Regexp of expected hardware information - fail if we don't see + # this + _expected_descr_re = re.compile(r'.*\d+-Port.*Managed Switch.*') + + def __init__(self, switch_hostname, switch_telnetport=23, debug = False): + SwitchDriver.__init__(self, switch_hostname, debug) + self._systemdata = [] + self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) + self.errors = SwitchErrors() + + ################################ + ### Switch-level API functions + ################################ + + # Save the current running config into flash - we want config to + # remain across reboots + def switch_save_running_config(self): + try: + self._cli("copy running-config startup-config") + self.connection.expect("Y/N") + self._cli("y") + self.connection.expect("succeeded") + except (PExpectError, pexpect.EOF, pexpect.TIMEOUT): + # recurse on error + self._switch_connect() + self.switch_save_running_config() + + # Restart the switch - we need to reload config to do a + # roll-back. Do NOT save running-config first if the switch asks - + # we're trying to dump recent changes, not save them. + # + # This will also implicitly cause a connection to be closed + def switch_restart(self): + self._cli("reload") + index = self.connection.expect(['Are you sure', 'will reset']) + if index == 0: + self._cli("y") # Yes, continue without saving + self.connection.expect("reset the whole") + + # Fall through + self._cli("y") # Yes, continue to reset + self.connection.close(True) + + ################################ + ### VLAN API functions + ################################ + + # Create a VLAN with the specified tag + def vlan_create(self, tag): + logging.debug("Creating VLAN %d", tag) + + try: + self._configure() + self._cli("vlan database") + self._cli("vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + return + raise IOError("Failed to create VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_create(tag) + + # Destroy a VLAN with the specified tag + def vlan_destroy(self, tag): + logging.debug("Destroying VLAN %d", tag) + + try: + self._configure() + self._cli("no vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + raise IOError("Failed to destroy VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_destroy(tag) + + # Set the name of a VLAN + def vlan_set_name(self, tag, name): + logging.debug("Setting name of VLAN %d to %s", tag, name) + + try: + self._configure() + self._cli("vlan %d" % tag) + self._cli("interface vlan %d" % tag) + self._cli("name %s" % name) + self._end_configure() + + # Validate it happened + read_name = self.vlan_get_name(tag) + if read_name != name: + raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" + % (tag, read_name, name)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_set_name(tag, name) + + # Get a list of the VLAN tags currently registered on the switch + def vlan_get_list(self): + logging.debug("Grabbing list of VLANs") + + try: + vlans = [] + regex = re.compile(r'^ *(\d+).*(D|S|G|R)') + + self._cli("show vlan") + for line in self._read_long_output("show vlan"): + match = regex.match(line) + if match: + vlans.append(int(match.group(1))) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_list() + + # For a given VLAN tag, ask the switch what the associated name is + def vlan_get_name(self, tag): + logging.debug("Grabbing the name of VLAN %d", tag) + + try: + name = None + regex = re.compile(r'^ *\d+\s+(\S+).*(D|S|G|R)') + self._cli("show vlan tag %d" % tag) + for line in self._read_long_output("show vlan tag"): + match = regex.match(line) + if match: + name = match.group(1) + name.strip() + return name + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_name(tag) + + ################################ + ### Port API functions + ################################ + + # Set the mode of a port: access or trunk + def port_set_mode(self, port, mode): + logging.debug("Setting port %s to %s", port, mode) + if not self._is_port_mode_valid(mode): + raise InputError("Port mode %s is not allowed" % mode) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport mode %s" % mode) + self._end_configure() + + # Validate it happened + read_mode = self._port_get_mode(port) + if read_mode != mode: + raise IOError("Failed to set mode for port %s" % port) + + # And cache the result + self._port_modes[port] = mode + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_mode(port, mode) + + # Set an access port to be in a specified VLAN (tag) + def port_set_access_vlan(self, port, tag): + logging.debug("Setting access port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport access vlan %d" % tag) + self._end_configure() + + # Validate things worked + read_vlan = int(self.port_get_access_vlan(port)) + if read_vlan != tag: + raise IOError("Failed to move access port %s to VLAN %d - got VLAN %d instead" + % (port, tag, read_vlan)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_access_vlan(port, tag) + + # Add a trunk port to a specified VLAN (tag) + def port_add_trunk_to_vlan(self, port, tag): + logging.debug("Adding trunk port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport trunk allowed vlan add %d" % tag) + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag: + return + raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_add_trunk_to_vlan(port, tag) + + # Remove a trunk port from a specified VLAN (tag) + def port_remove_trunk_from_vlan(self, port, tag): + logging.debug("Removing trunk port %s from VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("switchport trunk allowed vlan remove %d" % tag) + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag: + raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_remove_trunk_from_vlan(port, tag) + + # Get the configured VLAN tag for an access port (tag) + def port_get_access_vlan(self, port): + logging.debug("Getting VLAN for access port %s", port) + vlan = 1 + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + regex = re.compile(r'(\d+)\s+\S+\s+Untagged\s+(D|S|G|R)') + + try: + self._cli("show interfaces switchport %s" % port) + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + vlan = match.group(1) + return int(vlan) + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_access_vlan(port) + + # Get the list of configured VLAN tags for a trunk port + def port_get_trunk_vlan_list(self, port): + logging.debug("Getting VLANs for trunk port %s", port) + vlans = [ ] + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + regex = re.compile(r'(\d+)\s+\S+\s+(Tagged|Untagged)\s+(D|S|G|R)') + + try: + self._cli("show interfaces switchport %s" % port) + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + vlans.append (int(match.group(1))) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_trunk_vlan_list(port) + + ################################ + ### Internal functions + ################################ + + # Connect to the switch and log in + def _switch_connect(self): + + if not self.connection is None: + self.connection.close(True) + self.connection = None + + logging.debug("Connecting to Switch with: %s", self.exec_string) + self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) + self._login() + + # Avoid paged output + self._cli("terminal datadump") + + # And grab details about the switch. in case we need it + self._get_systemdata() + + # And also validate them - make sure we're driving a switch of + # the correct model! Also store the serial number + descr_regex = re.compile(r'System Description:.\s+(.*)') + sn_regex = re.compile(r'SN:\s+(\S_)') + descr = "" + + for line in self._systemdata: + match = descr_regex.match(line) + if match: + descr = match.group(1) + match = sn_regex.match(line) + if match: + self.serial_number = match.group(1) + + if not self._expected_descr_re.match(descr): + raise IOError("Switch %s not recognised by this driver: abort" % descr) + + # Now build a list of our ports, for later sanity checking + self._ports = self._get_port_names() + if len(self._ports) < 4: + raise IOError("Not enough ports detected - problem!") + + def _login(self): + logging.debug("attempting login with username %s, password %s", self._username, self._password) + self._cli("") + self.connection.expect("User Name:") + self._cli("%s" % self._username) + self.connection.expect("Password:") + self._cli("%s" % self._password, False) + self.connection.expect(r"\*\*") + while True: + index = self.connection.expect(['User Name:', 'authentication failed', r'([^#]+)#', 'Password:', '.+']) + if index == 0 or index == 1: # Failed to log in! + logging.error("Login failure: %s\n", self.connection.match) + raise IOError + elif index == 2: + self._prompt_name = re.escape(self.connection.match.group(1).strip()) + # Horrible output from the switch at login time may + # confuse our pexpect stuff here. If we've somehow got + # multiple lines of output, clean up and just take the + # *last* line here. Anything before that is going to + # just be noise from the "***" password input, etc. + prompt_lines = self._prompt_name.split('\r\n') + if len(prompt_lines) > 1: + self._prompt_name = prompt_lines[-1] + logging.debug("Got prompt name %s", self._prompt_name) + return 0 + elif index == 3 or index == 4: + self._cli("", False) + + def _logout(self): + logging.debug("Logging out") + self._cli("exit", False) + self.connection.close(True) + + def _configure(self): + self._cli("configure terminal") + + def _end_configure(self): + self._cli("end") + + def _read_long_output(self, text): + prompt = self._prompt_name + '#' + try: + self.connection.expect(prompt) + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_in(text) + raise PExpectError("_read_long_output failed") + except: + logging.error("prompt is \"%s\"", prompt) + raise + + longbuf = [] + for line in self.connection.before.split('\r\n'): + longbuf.append(line.strip()) + return longbuf + + def _get_port_names(self): + logging.debug("Grabbing list of ports") + interfaces = [] + + # Use "Up" or "Down" to only identify lines in the output that + # match interfaces that exist + regex = re.compile(r'^(\w+).*(Up|Down)') + + try: + self._cli("show interfaces status detailed") + for line in self._read_long_output("show interfaces status detailed"): + match = regex.match(line) + if match: + interface = match.group(1) + interfaces.append(interface) + self._port_numbers[interface] = len(interfaces) + logging.debug(" found %d ports on the switch", len(interfaces)) + return interfaces + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_port_names() + + # Get the mode of a port: access or trunk + def _port_get_mode(self, port): + logging.debug("Getting mode of port %s", port) + mode = '' + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + regex = re.compile(r'Port Mode: (\S+)') + + try: + self._cli("show interfaces switchport %s" % port) + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + mode = match.group(1) + return mode.lower() + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_mode(port) + + def _show_config(self): + logging.debug("Grabbing config") + try: + self._cli("show running-config") + return self._read_long_output("show running-config") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_config() + + def _show_clock(self): + logging.debug("Grabbing time") + try: + self._cli("show clock") + return self._read_long_output("show clock") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_clock() + + def _get_systemdata(self): + logging.debug("Grabbing system data") + + try: + self._systemdata = [] + self._cli("show system") + for line in self._read_long_output("show system"): + self._systemdata.append(line) + + logging.debug("Grabbing system sw and hw versions") + self._cli("show version") + for line in self._read_long_output("show version"): + self._systemdata.append(line) + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_systemdata() + + ###################################### + # Internal port access helper methods + ###################################### + # N.B. No parameter checking here, for speed reasons - if you're + # calling this internal API then you should already have validated + # things yourself! Equally, no post-set checks in here - do that + # at the higher level. + ###################################### + + # Wrapper around connection.send - by default, expect() the same + # text we've sent, to remove it from the output from the + # switch. For the few cases where we don't need that, override + # this using echo=False. + # Horrible, but seems to work. + def _cli(self, text, echo=True): + self.connection.send(text + '\r') + if echo: + try: + self.connection.expect(text) + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_out(text) + raise PExpectError("_cli failed on %s" % text) + except: + logging.error("Unexpected error: %s", sys.exc_info()[0]) + raise + +if __name__ == "__main__": +# p = CiscoSX300('10.172.2.52', 23) + + # Simple test harness - exercise the main working functions above to verify + # they work. This does *NOT* test really disruptive things like "save + # running-config" and "reload" - test those by hand. + + import optparse + + switch = 'vlandswitch02' + parser = optparse.OptionParser() + parser.add_option("--switch", + dest = "switch", + action = "store", + nargs = 1, + type = "string", + help = "specify switch to connect to for testing", + metavar = "") + (opts, args) = parser.parse_args() + if opts.switch: + switch = opts.switch + + portname_base = "fa" + # Text to match if we're on a SG-series switch, ports all called gi + sys_descr_re = re.compile('System Description.*Gigabit') + + def _port_name(number): + return "%s%d" % (portname_base, number) + + logging.basicConfig(level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s') + p = CiscoSX300(switch, 23, debug = True) + p.switch_connect('cisco', 'cisco', None) + #buf = p._show_clock() + #print "%s" % buf + #buf = p._show_config() + #p.dump_list(buf) + + print "System data:" + p.dump_list(p._systemdata) + for l in p._systemdata: + m = sys_descr_re.match(l) + if m: + print 'Found an SG switch, using "gi" as port name prefix for testing' + portname_base = "gi" + + if portname_base == "fa": + print 'Found an SF switch, using "fa" as port name prefix for testing' + + print "Creating VLANs for testing:" + for i in [ 2, 3, 4, 5, 20 ]: + p.vlan_create(i) + p.vlan_set_name(i, "test%d" % i) + print " %d (test%d)" % (i, i) + + #print "And dump config\n" + #buf = p._show_config() + #print "%s" % buf + + #print "Destroying VLAN 2\n" + #p.vlan_destroy(2) + + #print "And dump config\n" + #buf = p._show_config() + #print "%s" % buf + + #print "Port names are:" + #buf = p.switch_get_port_names() + #p.dump_list(buf) + + #buf = p.vlan_get_name(25) + #print "VLAN with tag 25 is called \"%s\"" % buf + + #p.vlan_set_name(35, "foo") + #print "VLAN with tag 35 is called \"foo\"" + + #buf = p.port_get_mode(_port_name(12)) + #print "Port %s is in %s mode" % (_port_name(12), buf) + + # Test access stuff + print "Set %s to access mode" % _port_name(6) + p.port_set_mode(_port_name(6), "access") + print "Move %s to VLAN 2" % _port_name(6) + p.port_set_access_vlan(_port_name(6), 2) + buf = p.port_get_access_vlan(_port_name(6)) + print "Read from switch: %s is on VLAN %s" % (_port_name(6), buf) + print "Move %s back to default VLAN 1" % _port_name(6) + p.port_set_access_vlan(_port_name(6), 1) + #print "And move %s back to a trunk port" % _port_name(6) + #p.port_set_mode(_port_name(6), "trunk") + #buf = p.port_get_mode(_port_name(6)) + #print "Port %s is in %s mode" % (_port_name(6), buf) + + # Test trunk stuff + print "Set %s to trunk mode" % _port_name(2) + p.port_set_mode(_port_name(2), "trunk") + print "Add %s to VLAN 2" % _port_name(2) + p.port_add_trunk_to_vlan(_port_name(2), 2) + print "Add %s to VLAN 3" % _port_name(2) + p.port_add_trunk_to_vlan(_port_name(2), 3) + print "Add %s to VLAN 4" % _port_name(2) + p.port_add_trunk_to_vlan(_port_name(2), 4) + print "Read from switch: which VLANs is %s on?" % _port_name(2) + buf = p.port_get_trunk_vlan_list(_port_name(2)) + p.dump_list(buf) + + print "Remove %s from VLANs 3,3,4" % _port_name(2) + p.port_remove_trunk_from_vlan(_port_name(2), 3) + p.port_remove_trunk_from_vlan(_port_name(2), 3) + p.port_remove_trunk_from_vlan(_port_name(2), 4) + print "Read from switch: which VLANs is %s on?" % _port_name(2) + buf = p.port_get_trunk_vlan_list(_port_name(2)) + p.dump_list(buf) + + # print "Adding lots of ports to VLANs" + # p.port_add_trunk_to_vlan(_port_name(1), 2) + # p.port_add_trunk_to_vlan(_port_name(3), 2) + # p.port_add_trunk_to_vlan(_port_name(5), 2) + # p.port_add_trunk_to_vlan(_port_name(7), 2) + # p.port_add_trunk_to_vlan(_port_name(9), 2) + # p.port_add_trunk_to_vlan(_port_name(11), 2) + # p.port_add_trunk_to_vlan(_port_name(13), 2) + # p.port_add_trunk_to_vlan(_port_name(15), 2) + # p.port_add_trunk_to_vlan(_port_name(17), 2) + # p.port_add_trunk_to_vlan(_port_name(19), 2) + # p.port_add_trunk_to_vlan(_port_name(21), 2) + # p.port_add_trunk_to_vlan(_port_name(23), 2) + # p.port_add_trunk_to_vlan(_port_name(4, 2) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + +# print 'Restarting switch, to explicitly reset config' +# p.switch_restart() + +# p.switch_save_running_config() +# p._show_config() diff --git a/Vland/drivers/Dummy.py b/Vland/drivers/Dummy.py new file mode 100644 index 0000000..f630184 --- /dev/null +++ b/Vland/drivers/Dummy.py @@ -0,0 +1,361 @@ +#! /usr/bin/python + +# Copyright 2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import sys +import re +import pickle +import pexpect + +# Dummy switch driver, designed specifically for +# testing/validation. Just remembers what it's been told and gives the +# same data back on demand. +# +# To keep track of data in the dummy switch, this code will simply +# dump out and read back its internal state to/from a Python pickle +# file as needed. On first use, if no such file exists then the Dummy +# driver will simply generate a simple switch model: +# +# * N ports in access mode +# * 1 VLAN (tag 1) labelled DEFAULT +# +# The "hostname" given to the switch in VLANd is important, as it will +# determine both the number of ports allocated in this model and the +# name of the pickle file used for data storage. Call the switch +# "dummy-N" in your vland.cfg file to have N ports. If you want to use +# more than one dummy switch instance, ensure you give them different +# numbers, e.g. "dummy-25", "dummy-48", etc. + +if __name__ == '__main__': + import os + vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) + sys.path.insert(0, vlandpath) + sys.path.insert(0, "%s/.." % vlandpath) + +from errors import InputError, PExpectError +from drivers.common import SwitchDriver, SwitchErrors + +class Dummy(SwitchDriver): + + connection = None + _username = None + _password = None + _enable_password = None + _dummy_vlans = {} + _dummy_ports = {} + _state_file = None + + _capabilities = [ + ] + + def __init__(self, switch_hostname, switch_telnetport=23, debug = False): + SwitchDriver.__init__(self, switch_hostname, debug) + self._systemdata = [] + self.errors = SwitchErrors() + self._state_file = "%s.pk" % switch_hostname + + ################################ + ### Switch-level API functions + ################################ + + # Save the current running config - we want config to remain + # across reboots + def switch_save_running_config(self): + pass + + # Restart the switch - we need to reload config to do a + # roll-back. Do NOT save running-config first if the switch asks - + # we're trying to dump recent changes, not save them. + def switch_restart(self): + pass + + # List the capabilities of the switch (and driver) - some things + # make no sense to abstract. Returns a dict of strings, each one + # describing an extra feature that that higher levels may care + # about + def switch_get_capabilities(self): + return self._capabilities + + ################################ + ### VLAN API functions + ################################ + + # Create a VLAN with the specified tag + def vlan_create(self, tag): + logging.debug("Creating VLAN %d", tag) + if not tag in self._dummy_vlans: + self._dummy_vlans[tag] = "VLAN%s" % tag + else: + # It's not an error if it already exists, but log anyway + logging.debug("VLAN %d already exists, name %s", + tag, self._dummy_vlans[tag]) + + # Destroy a VLAN with the specified tag + def vlan_destroy(self, tag): + logging.debug("Destroying VLAN %d", tag) + if tag in self._dummy_vlans: + del self._dummy_vlans[tag] + else: + # It's not an error if it doesn't exist, but log anyway + logging.debug("VLAN %d did not exist", tag) + + # Set the name of a VLAN + def vlan_set_name(self, tag, name): + logging.debug("Setting name of VLAN %d to %s", tag, name) + if not tag in self._dummy_vlans: + raise InputError("Tag %d does not exist") + self._dummy_vlans[tag] = "VLAN%s" % tag + + # Get a list of the VLAN tags currently registered on the switch + def vlan_get_list(self): + logging.debug("Grabbing list of VLANs") + return sorted(self._dummy_vlans.keys()) + + # For a given VLAN tag, ask the switch what the associated name is + def vlan_get_name(self, tag): + logging.debug("Grabbing the name of VLAN %d", tag) + if not tag in self._dummy_vlans: + raise InputError("Tag %d does not exist") + return self._dummy_vlans[tag] + + ################################ + ### Port API functions + ################################ + + # Set the mode of a port: access or trunk + def port_set_mode(self, port, mode): + logging.debug("Setting port %s to %s mode", port, mode) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + self._dummy_ports[port]['mode'] = mode + + # Get the mode of a port: access or trunk + def port_get_mode(self, port): + logging.debug("Getting mode of port %s", port) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + return self._dummy_ports[port]['mode'] + + # Set an access port to be in a specified VLAN (tag) + def port_set_access_vlan(self, port, tag): + logging.debug("Setting access port %s to VLAN %d", port, tag) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + if not tag in self._dummy_vlans: + raise InputError("VLAN %d does not exist" % tag) + self._dummy_ports[port]['access_vlan'] = tag + + # Add a trunk port to a specified VLAN (tag) + def port_add_trunk_to_vlan(self, port, tag): + logging.debug("Adding trunk port %s to VLAN %d", port, tag) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + if not tag in self._dummy_vlans: + raise InputError("VLAN %d does not exist" % tag) + self._dummy_ports[port]['trunk_vlans'].append(tag) + + # Remove a trunk port from a specified VLAN (tag) + def port_remove_trunk_from_vlan(self, port, tag): + logging.debug("Removing trunk port %s from VLAN %d", port, tag) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + if not tag in self._dummy_vlans: + raise InputError("VLAN %d does not exist" % tag) + self._dummy_ports[port]['trunk_vlans'].remove(tag) + + # Get the configured VLAN tag for an access port (tag) + def port_get_access_vlan(self, port): + logging.debug("Getting VLAN for access port %s", port) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + return self._dummy_ports[port]['access_vlan'] + + # Get the list of configured VLAN tags for a trunk port + def port_get_trunk_vlan_list(self, port): + logging.debug("Getting VLAN(s) for trunk port %s", port) + if not port in self._dummy_ports: + raise InputError("Port %s does not exist" % port) + return sorted(self._dummy_ports[port]['trunk_vlans']) + + ################################ + ### Internal functions + ################################ + + # Connect to the switch and log in + def _switch_connect(self): + # Open data file if it exists, otherwise initialise + try: + pkl_file = open(self._state_file, 'rb') + self._dummy_vlans = pickle.load(pkl_file) + self._dummy_ports = pickle.load(pkl_file) + pkl_file.close() + except: + # Create data here + self._dummy_vlans = {1: 'DEFAULT'} + match = re.match(r'dummy-(\d+)', self.hostname) + if match: + num_ports = int(match.group(1)) + else: + raise InputError("Unable to determine number of ports from switch name") + for i in range(1, num_ports+1): + port_name = "dm%2.2d" % int(i) + self._dummy_ports[port_name] = {} + self._dummy_ports[port_name]['mode'] = 'access' + self._dummy_ports[port_name]['access_vlan'] = 1 + self._dummy_ports[port_name]['trunk_vlans'] = [] + + # Now build a list of our ports, for later sanity checking + self._ports = self._get_port_names() + if len(self._ports) < 4: + raise IOError("Not enough ports detected - problem!") + + def _logout(self): + pkl_file = open(self._state_file, 'wb') + pickle.dump(self._dummy_vlans, pkl_file) + pickle.dump(self._dummy_ports, pkl_file) + pkl_file.close() + + def _get_port_names(self): + logging.debug("Grabbing list of ports") + interfaces = [] + for interface in sorted(self._dummy_ports.keys()): + interfaces.append(interface) + self._port_numbers[interface] = len(interfaces) + return interfaces + +if __name__ == "__main__": + + # Simple test harness - exercise the main working functions above to verify + # they work. This does *NOT* test really disruptive things like "save + # running-config" and "reload" - test those by hand. + + import optparse + + switch = 'dummy-48' + parser = optparse.OptionParser() + parser.add_option("--switch", + dest = "switch", + action = "store", + nargs = 1, + type = "string", + help = "specify switch to connect to for testing", + metavar = "") + (opts, args) = parser.parse_args() + if opts.switch: + switch = opts.switch + + logging.basicConfig(level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s') + p = Dummy(switch, 23, debug=True) + p.switch_connect('admin', '', None) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.vlan_get_name(1) + print "VLAN 1 is named \"%s\"" % buf + + print "Create VLAN 3" + p.vlan_create(3) + + print "Create VLAN 4" + p.vlan_create(4) + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "Set name of VLAN 3 to test333" + p.vlan_set_name(3, "test333") + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + print "Destroy VLAN 3" + p.vlan_destroy(3) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.port_get_mode("dm10") + print "Port dm10 is in %s mode" % buf + + buf = p.port_get_mode("dm11") + print "Port dm11 is in %s mode" % buf + + # Test access stuff + print "Set dm09 to access mode" + p.port_set_mode("dm09", "access") + + print "Move dm9 to VLAN 4" + p.port_set_access_vlan("dm09", 4) + + buf = p.port_get_access_vlan("dm09") + print "Read from switch: dm09 is on VLAN %s" % buf + + print "Move dm09 back to VLAN 1" + p.port_set_access_vlan("dm09", 1) + + print "Create VLAN 2" + p.vlan_create(2) + + print "Create VLAN 3" + p.vlan_create(3) + + print "Create VLAN 4" + p.vlan_create(4) + + # Test access stuff + print "Set dm09 to trunk mode" + p.port_set_mode("dm09", "trunk") + print "Read from switch: which VLANs is dm09 on?" + buf = p.port_get_trunk_vlan_list("dm09") + p.dump_list(buf) + + # The adds below are NOOPs in effect on this switch - no filtering + # for "trunk" ports + print "Add dm09 to VLAN 2" + p.port_add_trunk_to_vlan("dm09", 2) + print "Add dm09 to VLAN 3" + p.port_add_trunk_to_vlan("dm09", 3) + print "Add dm09 to VLAN 4" + p.port_add_trunk_to_vlan("dm09", 4) + print "Read from switch: which VLANs is dm09 on?" + buf = p.port_get_trunk_vlan_list("dm09") + p.dump_list(buf) + + # And the same for removals here + p.port_remove_trunk_from_vlan("dm09", 3) + p.port_remove_trunk_from_vlan("dm09", 2) + p.port_remove_trunk_from_vlan("dm09", 4) + print "Read from switch: which VLANs is dm09 on?" + buf = p.port_get_trunk_vlan_list("dm09") + p.dump_list(buf) + + print 'Restarting switch, to explicitly reset config' + p.switch_restart() + + p.switch_save_running_config() + + p.switch_disconnect() diff --git a/Vland/drivers/Mellanox.py b/Vland/drivers/Mellanox.py new file mode 100644 index 0000000..ea74bf0 --- /dev/null +++ b/Vland/drivers/Mellanox.py @@ -0,0 +1,795 @@ +#! /usr/bin/python + +# Copyright 2018 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import sys +import re +import pexpect + +# Mellanox MLNX-OS driver +# Developed and tested against the SN2100 in the Linaro LAVA lab + +if __name__ == '__main__': + import os + vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) + sys.path.insert(0, vlandpath) + sys.path.insert(0, "%s/.." % vlandpath) + +from errors import InputError, PExpectError +from drivers.common import SwitchDriver, SwitchErrors + +class Mellanox(SwitchDriver): + + connection = None + _username = None + _password = None + _enable_password = None + + _capabilities = [ + 'TrunkWildCardVlans' # Trunk ports are on all VLANs by + # default, so we shouldn't need to + # bugger with them + ] + + # Regexp of expected hardware information - fail if we don't see + # this + _expected_descr_re = re.compile(r'MLNX-OS') + + def __init__(self, switch_hostname, switch_telnetport=23, debug = False): + SwitchDriver.__init__(self, switch_hostname, debug) + self._systemdata = [] + self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) + self.errors = SwitchErrors() + + ################################ + ### Switch-level API functions + ################################ + + # Save the current running config into flash - we want config to + # remain across reboots + def switch_save_running_config(self): + try: + self._configure() + self._cli("configuration write") + self._end_configure() + except (PExpectError, pexpect.EOF): + # recurse on error + self._switch_connect() + self.switch_save_running_config() + + # Restart the switch - we need to reload config to do a + # roll-back. Do NOT save running-config first if the switch asks - + # we're trying to dump recent changes, not save them. + # + # This will also implicitly cause a connection to be closed + def switch_restart(self): + self._cli("reload noconfirm") + + # List the capabilities of the switch (and driver) - some things + # make no sense to abstract. Returns a dict of strings, each one + # describing an extra feature that that higher levels may care + # about + def switch_get_capabilities(self): + return self._capabilities + + ################################ + ### VLAN API functions + ################################ + + # Create a VLAN with the specified tag + def vlan_create(self, tag): + logging.debug("Creating VLAN %d", tag) + try: + self._configure() + self._cli("vlan %d" % tag) + self._end_vlan() + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + return + raise IOError("Failed to create VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_create(tag) + + # Destroy a VLAN with the specified tag + def vlan_destroy(self, tag): + logging.debug("Destroying VLAN %d", tag) + + try: + self._configure() + self._cli("no vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + raise IOError("Failed to destroy VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_destroy(tag) + + # Set the name of a VLAN + def vlan_set_name(self, tag, name): + logging.debug("Setting name of VLAN %d to %s", tag, name) + + try: + self._configure() + self._cli("vlan %d name %s" % (tag, name)) + self._end_configure() + + # This switch *might* have problems if we drive it too quickly? At + # least one instance of set_name()/get_name() not working. This + # might help? + self._delay() + + # And retry around here + retries = 5 + read_name = None + while (retries > 0 and read_name is None): + # Validate it happened + read_name = self.vlan_get_name(tag) + retries -= 1 + + if read_name != name: + raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" + % (tag, read_name, name)) + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_set_name(tag, name) + + # Get a list of the VLAN tags currently registered on the switch + def vlan_get_list(self): + logging.debug("Grabbing list of VLANs") + + try: + vlans = [] + + regex = re.compile(r'^ *(\d+)') + + self._cli("show vlan") + for line in self._read_long_output("show vlan"): + match = regex.match(line) + if match: + vlans.append(int(match.group(1))) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_list() + + # For a given VLAN tag, ask the switch what the associated name is + def vlan_get_name(self, tag): + + try: + logging.debug("Grabbing the name of VLAN %d", tag) + name = None + + # Ugh, the output here is messy. VLAN names can include spaces, and + # there are no delimiters in the output, e.g.: + # VLAN Name Ports + # ---- ----------- -------------------------------------- + # 1 default Eth1/1/1, Eth1/1/2, Eth1/2, Eth1/3/1, Eth1/3/2, + # Eth1/4, Eth1/5, Eth1/6, Eth1/7, Eth1/8, + # Eth1/10, Eth1/12, Eth1/13, Eth1/14, Eth1/15, + # Eth1/16 + # 102 mdev testing + # 103 vpp 1 performance testing Eth1/1/3, Eth1/9 + # 104 vpp 2 performance testing Eth1/1/4, Eth1/11 + # + # Simplest strategy: + # 1. Match on a leading number and grab all the text after it + # 2. Drop anything starting with "Eth" to EOL + # 3. Strip leading and trailing whitespace + # + # Not perfect, but it'll have to do. Anybody including "Eth" in a + # VLAN name deserves to lose... + + regex = re.compile(r'^ *\d+\s+(.+)') + self._cli("show vlan id %d" % tag) + for line in self._read_long_output("show vlan id"): + match = regex.match(line) + if match: + name = re.sub(r'Eth.*$',"",match.group(1)).strip() + if name is None: + logging.debug("vlan_get_name: did not find a name") + return name + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_name(tag) + + + ################################ + ### Port API functions + ################################ + + # Set the mode of a port: access or trunk + def port_set_mode(self, port, mode): + logging.debug("Setting port %s to %s", port, mode) + if not self._is_port_mode_valid(mode): + raise InputError("Port mode %s is not allowed" % mode) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + + try: + self._configure() + self._cli("interface ethernet %s" % port) + self._cli("switchport mode %s" % mode) + if mode == "trunk": + # Put the new trunk port on all VLANs + self._cli("switchport trunk allowed-vlan all") + self._end_interface() + self._end_configure() + + # Validate it happened + read_mode = self._port_get_mode(port) + if read_mode != mode: + raise IOError("Failed to set mode for port %s" % port) + + # And cache the result + self._port_modes[port] = mode + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_mode(port, mode) + + # Set an access port to be in a specified VLAN (tag) + def port_set_access_vlan(self, port, tag): + logging.debug("Setting access port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + + try: + self._configure() + self._cli("interface ethernet %s" % port) + self._cli("switchport access vlan %d" % tag) + self._end_interface() + self._end_configure() + + # Validate things worked + read_vlan = int(self.port_get_access_vlan(port)) + if read_vlan != tag: + raise IOError("Failed to move access port %s to VLAN %d - got VLAN %d instead" + % (port, tag, read_vlan)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_access_vlan(port, tag) + + # Add a trunk port to a specified VLAN (tag) + def port_add_trunk_to_vlan(self, port, tag): + logging.debug("Adding trunk port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface ethernet %s" % port) + self._cli("switchport trunk allowed-vlan add %d" % tag) + self._end_interface() + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag or vlan == "ALL": + return + raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_add_trunk_to_vlan(port, tag) + + # Remove a trunk port from a specified VLAN (tag) + def port_remove_trunk_from_vlan(self, port, tag): + logging.debug("Removing trunk port %s from VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface ethernet %s" % port) + self._cli("switchport trunk allowed-vlan remove %d" % tag) + self._end_interface() + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag: + raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_remove_trunk_from_vlan(port, tag) + + # Get the configured VLAN tag for an access port (tag) + def port_get_access_vlan(self, port): + logging.debug("Getting VLAN for access port %s", port) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + + regex = re.compile(r'^Eth%s\s+access\s+(\d+)' % port) + + try: + self._cli("show interfaces switchport") + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + vlan = match.group(1) + return int(vlan) + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_access_vlan(port) + + # Get the list of configured VLAN tags for a trunk port + def port_get_trunk_vlan_list(self, port): + logging.debug("Getting VLANs for trunk port %s", port) + vlans = [] + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + regex_start = re.compile(r'^Eth%s\s+trunk\s+N/A\s+(.*)' % port) + regex_continue = re.compile(r'^(\d.*)') + + try: + self._cli("show interfaces switchport") + + # Complex parsing work - VLAN list may extend over several lines, e.g.: + # + # Eth1/16 trunk N/A 1, 102, 103, 104, 1000, 1001, 1002 + # 1003, 1004 + # + in_match = False + vlan_text = '' + + for line in self._read_long_output("show interfaces switchport"): + if in_match: + match = regex_continue.match(line) + if match: + vlan_text += ', ' # Make a consistently-formed list + vlan_text += match.group(1) + else: + in_match = False + if not in_match: + match = regex_start.match(line) + if match: + vlan_text += match.group(1) + in_match = True + + vlans = self._parse_vlan_list(vlan_text) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_trunk_vlan_list(port) + + ################################ + ### Internal functions + ################################ + + # Connect to the switch and log in + def _switch_connect(self): + + if not self.connection is None: + self.connection.close(True) + self.connection = None + + logging.debug("Connecting to Switch with: %s", self.exec_string) + self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) + self._login() + + # Avoid paged output as much as possible + self._cli("terminal length 999") + # Don't do silly things with ANSI codes + self._cli("terminal type dumb") + # and disable auto-logout after delay + self._cli("no cli session auto-logout") + + # And grab details about the switch. in case we need it + self._get_systemdata() + + # And also validate them - make sure we're driving a switch of + # the correct model! Also store the serial number + descr_regex = re.compile(r'Product name:\s*(\S+)') + sn_regex = re.compile(r'System serial num:\s*(\S+)') + descr = "" + + for line in self._systemdata: + match = descr_regex.match(line) + if match: + descr = match.group(1) + match = sn_regex.match(line) + if match: + self.serial_number = match.group(1) + + logging.debug("serial number is %s", self.serial_number) + logging.debug("system description is %s", descr) + + if not self._expected_descr_re.match(descr): + raise IOError("Switch %s not recognised by this driver: abort" % descr) + + # Now build a list of our ports, for later sanity checking + self._ports = self._get_port_names() + if len(self._ports) < 4: + raise IOError("Not enough ports detected - problem!") + + def _login(self): + logging.debug("attempting login with username %s, password %s", self._username, self._password) + if self._username is not None: + self.connection.expect("login:") + self._cli("%s" % self._username) + if self._password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._password, False) + while True: + index = self.connection.expect(['User:', 'Password:', 'Login incorrect', 'authentication failed', r'(.*?)(#|>)']) + if index != 4: # Any other means: failed to log in! + logging.error("Login failure: index %d\n", index) + logging.error("Login failure: %s\n", self.connection.match.before) + raise IOError + + # else + + # Add a couple of newlines to get past the "last login" etc. junk + self._cli("") + self._cli("") + self.connection.expect(r'^(.*?) (#|>)') + self._prompt_name = re.escape(self.connection.match.group(1).strip()) + logging.info("Got outer prompt \"%s\"", self._prompt_name) + if self.connection.match.group(2) == ">": + # Need to enter "enable" mode too + self._cli("enable") + if self._enable_password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._enable_password, False) + index = self.connection.expect(['Password:', 'Login incorrect', 'authentication failed', r'(.*) *(#|>)']) + if index != 3: # Any other means: failed to log in! + logging.error("Enable password failure: %s\n", self.connection.match) + raise IOError + self._cli("") + self._cli("") + self.connection.expect(r'^(.*?) (#|>)') + self._prompt_name = re.escape(self.connection.match.group(1).strip()) + logging.info("Got enable prompt \"%s\"", self._prompt_name) + return 0 + + def _logout(self): + logging.debug("Logging out") + self._cli("quit", False) + try: + self.connection.expect("Would you like to save them now") + self._cli("n") + except (pexpect.EOF): + pass + self.connection.close(True) + + def _configure(self): + self._cli("configure terminal") + + def _end_configure(self): + self._cli("exit") + + def _end_interface(self): + self._cli("exit") + + def _end_vlan(self): + self._cli("exit") + + def _read_long_output(self, text): + longbuf = [] + prompt = self._prompt_name + r'\s*#' + while True: + try: + index = self.connection.expect([r'lines \d+-\d+', prompt]) + if index == 0: # "lines 45-50" + for line in self.connection.before.split('\r\n'): + longbuf.append(line.strip()) + self._cli(' ', False) + elif index == 1: # Back to a prompt, says output is finished + break + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_in(text) + raise PExpectError("_read_long_output failed") + except: + logging.error("prompt is \"%s\"", prompt) + raise + + for line in self.connection.before.split('\r\n'): + longbuf.append(line.strip()) + return longbuf + + def _get_port_names(self): + logging.debug("Grabbing list of ports") + interfaces = [] + + # Look for "Eth1" at the beginning of the output lines to just + # match lines with interfaces - they have names like + # "Eth1/15". We do not care about Link Aggregation Groups (lag) + # here. + regex = re.compile(r'^Eth(\S+)') + + try: + self._cli("show interfaces switchport") + for line in self._read_long_output("show interfaces switchport"): + match = regex.match(line) + if match: + interface = match.group(1) + interfaces.append(interface) + self._port_numbers[interface] = len(interfaces) + logging.debug(" found %d ports on the switch", len(interfaces)) + return interfaces + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_port_names() + + # Get the mode of a port: access or trunk + def _port_get_mode(self, port): + logging.debug("Getting mode of port %s", port) + mode = '' + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + regex = re.compile('Switchport mode: (.*)') + + try: + self._cli("show interfaces ethernet %s" % port) + for line in self._read_long_output("show interfaces ethernet"): + match = regex.match(line) + if match: + mode = match.group(1) + if mode == 'access': + return 'access' + if mode == 'trunk': + return 'trunk' + return mode + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_mode(port) + + def _show_config(self): + logging.debug("Grabbing config") + try: + self._cli("show running-config") + return self._read_long_output("show running-config") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_config() + + def _show_clock(self): + logging.debug("Grabbing time") + try: + self._cli("show clock") + return self._read_long_output("show clock") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_clock() + + def _get_systemdata(self): + logging.debug("Grabbing system sw and hw versions") + + try: + self._systemdata = [] + self._cli("show version") + for line in self._read_long_output("show version"): + self._systemdata.append(line) + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_systemdata() + + # Borrowed from the Catalyst driver. Over-complex for our needs here, but + # it's already tested and will do the job. + def _parse_vlan_list(self, inputdata): + vlans = [] + + if inputdata == "ALL": + return ["ALL"] + elif inputdata == "NONE": + return [] + elif inputdata == "": + return [] + else: + # Parse the complex list + groups = inputdata.split(',') + for group in groups: + subgroups = group.split('-') + if len(subgroups) == 1: + vlans.append(int(subgroups[0])) + elif len(subgroups) == 2: + for i in range (int(subgroups[0]), int(subgroups[1]) + 1): + vlans.append(i) + else: + logging.debug("Can't parse group \"" + group + "\"") + + return vlans + + # Wrapper around connection.send - by default, expect() the same + # text we've sent, to remove it from the output from the + # switch. For the few cases where we don't need that, override + # this using echo=False. + # Horrible, but seems to work. + def _cli(self, text, echo=True): + self.connection.send(text + '\r') + if echo: + try: + self.connection.expect(text) + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_out(text) + raise PExpectError("_cli failed on %s" % text) + except: + logging.error("Unexpected error: %s", sys.exc_info()[0]) + raise + +if __name__ == "__main__": + + # Simple test harness - exercise the main working functions above to verify + # they work. This does *NOT* test really disruptive things like "save + # running-config" and "reload" - test those by hand. + + import optparse + + switch = '172.27.16.6' + parser = optparse.OptionParser() + parser.add_option("--switch", + dest = "switch", + action = "store", + nargs = 1, + type = "string", + help = "specify switch to connect to for testing", + metavar = "") + (opts, args) = parser.parse_args() + if opts.switch: + switch = opts.switch + + logging.basicConfig(level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s') + p = MlnxOS(switch, 23, debug=False) + p.switch_connect('admin', 'admin', None) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.vlan_get_name(102) + print "VLAN 102 is named \"%s\"" % buf + + print "Create VLAN 1003" + p.vlan_create(1003) + + buf = p.vlan_get_name(1003) + print "VLAN 1003 is named \"%s\"" % buf + + print "Set name of VLAN 1003 to test333" + p.vlan_set_name(1003, "test333") + + buf = p.vlan_get_name(1003) + print "VLAN 1003 is named \"%s\"" % buf + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + print "Destroy VLAN 1003" + p.vlan_destroy(1003) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.port_get_mode("1/15") + print "Port 1/15 is in %s mode" % buf + + buf = p.port_get_mode("1/16") + print "Port 1/16 is in %s mode" % buf + + # Test access stuff + print "Set 1/15 to access mode" + p.port_set_mode("1/15", "access") + + print "Move 1/15 to VLAN 4" + p.port_set_access_vlan("1/15", 4) + + buf = p.port_get_access_vlan("1/15") + print "Read from switch: 1/15 is on VLAN %s" % buf + + print "Move 1/15 back to VLAN 1" + p.port_set_access_vlan("1/15", 1) + + print "Create VLAN 1002" + p.vlan_create(1002) + + print "Create VLAN 1003" + p.vlan_create(1003) + + print "Create VLAN 1004" + p.vlan_create(1004) + + # Test access stuff + print "Set 1/15 to trunk mode" + p.port_set_mode("1/15", "trunk") + print "Read from switch: which VLANs is 1/15 on?" + buf = p.port_get_trunk_vlan_list("1/15") + p.dump_list(buf) + + # The adds below are NOOPs in effect on this switch - no filtering + # for "trunk" ports + print "Add 1/15 to VLAN 1002" + p.port_add_trunk_to_vlan("1/15", 1002) + print "Add 1/15 to VLAN 1003" + p.port_add_trunk_to_vlan("1/15", 1003) + print "Add 1/15 to VLAN 1004" + p.port_add_trunk_to_vlan("1/15", 1004) + print "Read from switch: which VLANs is 1/15 on?" + buf = p.port_get_trunk_vlan_list("1/15") + p.dump_list(buf) + + # And the same for removals here + p.port_remove_trunk_from_vlan("1/15", 1003) + p.port_remove_trunk_from_vlan("1/15", 1002) + p.port_remove_trunk_from_vlan("1/15", 1004) + print "Read from switch: which VLANs is 1/15 on?" + buf = p.port_get_trunk_vlan_list("1/15") + p.dump_list(buf) + +# print 'Restarting switch, to explicitly reset config' +# p.switch_restart() + +# p.switch_save_running_config() + +# p.switch_disconnect() +# p._show_config() diff --git a/Vland/drivers/NetgearXSM.py b/Vland/drivers/NetgearXSM.py new file mode 100644 index 0000000..f8ba8a5 --- /dev/null +++ b/Vland/drivers/NetgearXSM.py @@ -0,0 +1,782 @@ +#! /usr/bin/python + +# Copyright 2015-2018 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import sys +import re +import pexpect + +# Netgear XSM family driver +# Developed and tested against the XSM7224S in the Linaro LAVA lab + +if __name__ == '__main__': + import os + vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) + sys.path.insert(0, vlandpath) + sys.path.insert(0, "%s/.." % vlandpath) + +from errors import InputError, PExpectError +from drivers.common import SwitchDriver, SwitchErrors + +class NetgearXSM(SwitchDriver): + + connection = None + _username = None + _password = None + _enable_password = None + + _capabilities = [ + ] + + # Regexps of expected hardware information - fail if we don't see + # this + _expected_manuf = re.compile('^Netgear') + _expected_model = re.compile('^XSM') + + def __init__(self, switch_hostname, switch_telnetport=23, debug = False): + SwitchDriver.__init__(self, switch_hostname, debug) + self._systemdata = [] + self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) + self.errors = SwitchErrors() + + ################################ + ### Switch-level API functions + ################################ + + # Save the current running config into flash - we want config to + # remain across reboots + def switch_save_running_config(self): + try: + self._cli("save") + self.connection.expect("Are you sure") + self._cli("y") + self.connection.expect("Configuration Saved!") + except (PExpectError, pexpect.EOF): + # recurse on error + self._switch_connect() + self.switch_save_running_config() + + # Restart the switch - we need to reload config to do a + # roll-back. Do NOT save running-config first if the switch asks - + # we're trying to dump recent changes, not save them. + # + # This will also implicitly cause a connection to be closed + def switch_restart(self): + self._cli("reload") + self.connection.expect('Are you sure') + self._cli("y") # Yes, continue to reset + self.connection.close(True) + + # List the capabilities of the switch (and driver) - some things + # make no sense to abstract. Returns a dict of strings, each one + # describing an extra feature that that higher levels may care + # about + def switch_get_capabilities(self): + return self._capabilities + + ################################ + ### VLAN API functions + ################################ + + # Create a VLAN with the specified tag + def vlan_create(self, tag): + logging.debug("Creating VLAN %d", tag) + try: + self._cli("vlan database") + self._cli("vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + return + raise IOError("Failed to create VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_create(tag) + + # Destroy a VLAN with the specified tag + def vlan_destroy(self, tag): + logging.debug("Destroying VLAN %d", tag) + + try: + self._cli("vlan database") + self._cli("no vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + raise IOError("Failed to destroy VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_destroy(tag) + + # Set the name of a VLAN + def vlan_set_name(self, tag, name): + logging.debug("Setting name of VLAN %d to %s", tag, name) + + try: + self._cli("vlan database") + self._cli("vlan name %d %s" % (tag, name)) + self._end_configure() + + # Validate it happened + read_name = self.vlan_get_name(tag) + if read_name != name: + raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" + % (tag, read_name, name)) + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_set_name(tag, name) + + # Get a list of the VLAN tags currently registered on the switch + def vlan_get_list(self): + logging.debug("Grabbing list of VLANs") + + try: + vlans = [] + + regex = re.compile(r'^ *(\d+).*(Static)') + + self._cli("show vlan brief") + for line in self._read_long_output("show vlan brief"): + match = regex.match(line) + if match: + vlans.append(int(match.group(1))) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_list() + + # For a given VLAN tag, ask the switch what the associated name is + def vlan_get_name(self, tag): + + try: + logging.debug("Grabbing the name of VLAN %d", tag) + name = None + regex = re.compile('VLAN Name: (.*)') + self._cli("show vlan %d" % tag) + for line in self._read_long_output("show vlan"): + match = regex.match(line) + if match: + name = match.group(1) + name.strip() + return name + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_name(tag) + + ################################ + ### Port API functions + ################################ + + # Set the mode of a port: access or trunk + def port_set_mode(self, port, mode): + logging.debug("Setting port %s to %s mode", port, mode) + if not self._is_port_mode_valid(mode): + raise InputError("Port mode %s is not allowed" % mode) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + + # This switch does not support specific modes, so we can't + # actually change the mode directly. However, we can and + # should deal with the PVID and memberships of existing VLANs + # etc. + + try: + if mode == "trunk": + # We define a trunk port thus: + # * accept all frames on ingress + # * accept packets for all VLANs (no ingress filter) + # * tags frames on transmission (do that later when + # * adding VLANs to the port) + # * PVID should match the default VLAN (1). + self._configure() + self._cli("interface %s" % port) + self._cli("vlan acceptframe all") + self._cli("no vlan ingressfilter") + self._cli("vlan pvid 1") + self._end_interface() + self._end_configure() + + # We define an access port thus: + # * accept only untagged frames on ingress + # * accept packets for only desired VLANs (ingress filter) + # * exists on one VLAN only (1 by default) + # * do not tag frames on transmission (the devices + # we're talking to are expecting untagged frames) + # * PVID should match the VLAN it's on (1 by default, + # but don't do that here) + if mode == "access": + self._configure() + self._cli("interface %s" % port) + self._cli("vlan acceptframe admituntaggedonly") + self._cli("vlan ingressfilter") + self._cli("no vlan tagging 1-1023") + self._cli("no vlan tagging 1024-2047") + self._cli("no vlan tagging 2048-3071") + self._cli("no vlan tagging 3072-4093") + self._end_interface() + self._end_configure() + + # Validate it happened + read_mode = self._port_get_mode(port) + + if read_mode != mode: + raise IOError("Failed to set mode for port %s" % port) + + # And cache the result + self._port_modes[port] = mode + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_mode(port, mode) + + # Set an access port to be in a specified VLAN (tag) + def port_set_access_vlan(self, port, tag): + logging.debug("Setting access port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + + try: + current_vlans = self._get_port_vlans(port) + self._configure() + self._cli("interface %s" % port) + self._cli("vlan pvid %s" % tag) + # Find the list of VLANs we're currently on, and drop them + # all. "auto" mode is fine here, we won't be included + # unless we have GVRP configured, and we don't do + # that. + for current_vlan in current_vlans: + self._cli("vlan participation auto %s" % current_vlan) + # Now specifically include the VLAN we want + self._cli("vlan participation include %s" % tag) + self._cli("no shutdown") + self._end_interface() + self._end_configure() + + # Finally, validate things worked + read_vlan = int(self.port_get_access_vlan(port)) + if read_vlan != tag: + raise IOError("Failed to move access port %d to VLAN %d - got VLAN %d instead" + % (port, tag, read_vlan)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_access_vlan(port, tag) + + # Add a trunk port to a specified VLAN (tag) + def port_add_trunk_to_vlan(self, port, tag): + logging.debug("Adding trunk port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("vlan participation include %d" % tag) + self._cli("vlan tagging %d" % tag) + self._end_interface() + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag or vlan == "ALL": + return + raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_add_trunk_to_vlan(port, tag) + + # Remove a trunk port from a specified VLAN (tag) + def port_remove_trunk_from_vlan(self, port, tag): + logging.debug("Removing trunk port %s from VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + + try: + self._configure() + self._cli("interface %s" % port) + self._cli("vlan participation auto %d" % tag) + self._cli("no vlan tagging %d" % tag) + self._end_interface() + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag: + raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_remove_trunk_from_vlan(port, tag) + + # Get the configured VLAN tag for an access port (tag) + def port_get_access_vlan(self, port): + logging.debug("Getting VLAN for access port %s", port) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "access"): + raise InputError("Port %s not in access mode" % port) + vlans = self._get_port_vlans(port) + if (len(vlans) > 1): + raise IOError("More than one VLAN on access port %s" % port) + return vlans[0] + + # Get the list of configured VLAN tags for a trunk port + def port_get_trunk_vlan_list(self, port): + logging.debug("Getting VLAN(s) for trunk port %s", port) + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if not (self.port_get_mode(port) == "trunk"): + raise InputError("Port %s not in trunk mode" % port) + return self._get_port_vlans(port) + + ################################ + ### Internal functions + ################################ + + # Connect to the switch and log in + def _switch_connect(self): + + if not self.connection is None: + self.connection.close(True) + self.connection = None + + logging.debug("Connecting to Switch with: %s", self.exec_string) + self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) + self._login() + + # Avoid paged output + self._cli("terminal length 0") + + # And grab details about the switch. in case we need it + self._get_systemdata() + + # And also validate them - make sure we're driving a switch of + # the correct model! Also store the serial number + manuf_regex = re.compile(r'^Manufacturer([\.\s])+(\S+)') + model_regex = re.compile(r'^Machine Model([\.\s])+(\S+)') + sn_regex = re.compile(r'^Serial Number([\.\s])+(\S+)') + + for line in self._systemdata: + match1 = manuf_regex.match(line) + if match1: + manuf = match1.group(2) + + match2 = model_regex.match(line) + if match2: + model = match2.group(2) + + match3 = sn_regex.match(line) + if match3: + self.serial_number = match3.group(2) + + logging.debug("manufacturer is %s", manuf) + logging.debug("model is %s", model) + logging.debug("serial number is %s", self.serial_number) + + if not (self._expected_manuf.match(manuf) and self._expected_model.match(model)): + raise IOError("Switch %s %s not recognised by this driver: abort" % (manuf, model)) + + # Now build a list of our ports, for later sanity checking + self._ports = self._get_port_names() + if len(self._ports) < 4: + raise IOError("Not enough ports detected - problem!") + + def _login(self): + logging.debug("attempting login with username %s, password %s", self._username, self._password) + if self._username is not None: + self.connection.expect("User:") + self._cli("%s" % self._username) + if self._password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._password, False) + while True: + index = self.connection.expect(['User:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) + if index != 4: # Any other means: failed to log in! + logging.error("Login failure: index %d\n", index) + logging.error("Login failure: %s\n", self.connection.match.before) + raise IOError + + # else + self._prompt_name = re.escape(self.connection.match.group(1).strip()) + if self.connection.match.group(2) == ">": + # Need to enter "enable" mode too + self._cli("enable") + if self._enable_password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._enable_password, False) + index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*) *(#|>)']) + if index != 3: # Any other means: failed to log in! + logging.error("Enable password failure: %s\n", self.connection.match) + raise IOError + return 0 + + def _logout(self): + logging.debug("Logging out") + self._cli("quit", False) + try: + self.connection.expect("Would you like to save them now") + self._cli("n") + except (pexpect.EOF): + pass + self.connection.close(True) + + def _configure(self): + self._cli("configure") + + def _end_configure(self): + self._cli("exit") + + def _end_interface(self): + self._end_configure() + + def _read_long_output(self, text): + longbuf = [] + prompt = self._prompt_name + r'\s*#' + while True: + try: + index = self.connection.expect(['--More--', prompt]) + if index == 0: # "--More-- or (q)uit" + for line in self.connection.before.split('\r\n'): + line1 = re.sub('(\x08|\x0D)*', '', line.strip()) + longbuf.append(line1) + self._cli(' ', False) + elif index == 1: # Back to a prompt, says output is finished + break + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_in(text) + raise PExpectError("_read_long_output failed") + except: + logging.error("prompt is \"%s\"", prompt) + raise + + for line in self.connection.before.split('\r\n'): + longbuf.append(line.strip()) + return longbuf + + def _get_port_names(self): + logging.debug("Grabbing list of ports") + interfaces = [] + + # Look for "1" at the beginning of the output lines to just + # match lines with interfaces - they have names like + # "1/0/22". We do not care about Link Aggregation Groups (lag) + # here. + regex = re.compile(r'^(1\S+)') + + try: + self._cli("show port all") + for line in self._read_long_output("show port all"): + match = regex.match(line) + if match: + interface = match.group(1) + interfaces.append(interface) + self._port_numbers[interface] = len(interfaces) + logging.debug(" found %d ports on the switch", len(interfaces)) + return interfaces + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_port_names() + + # Get the mode of a port: access or trunk + def _port_get_mode(self, port): + logging.debug("Getting mode of port %s", port) + + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + acceptframe_re = re.compile('vlan acceptframe (.*)') + ingress_re = re.compile('vlan ingressfilter') + + acceptframe = None + ingressfilter = True + + try: + self._cli("show running-config interface %s" % port) + for line in self._read_long_output("show running-config interface"): + + match = acceptframe_re.match(line) + if match: + acceptframe = match.group(1) + + match = ingress_re.match(line) + if match: + ingressfilter = True + + # Simple classifier for now; may need to revisit later... + if (ingressfilter and acceptframe == "admituntaggedonly"): + return "access" + else: + return "trunk" + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_mode(port) + + def _show_config(self): + logging.debug("Grabbing config") + try: + self._cli("show running-config") + return self._read_long_output("show running-config") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_config() + + def _show_clock(self): + logging.debug("Grabbing time") + try: + self._cli("show clock") + return self._read_long_output("show clock") + except PExpectError: + # recurse on error + self._switch_connect() + return self._show_clock() + + def _get_systemdata(self): + logging.debug("Grabbing system sw and hw versions") + + try: + self._systemdata = [] + self._cli("show version") + for line in self._read_long_output("show version"): + self._systemdata.append(line) + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_systemdata() + + def _parse_vlan_list(self, inputdata): + vlans = [] + + if inputdata == "ALL": + return ["ALL"] + elif inputdata == "NONE": + return [] + else: + # Parse the complex list + groups = inputdata.split(',') + for group in groups: + subgroups = group.split('-') + if len(subgroups) == 1: + vlans.append(int(subgroups[0])) + elif len(subgroups) == 2: + for i in range (int(subgroups[0]), int(subgroups[1]) + 1): + vlans.append(i) + else: + logging.debug("Can't parse group \"" + group + "\"") + + return vlans + + def _get_port_vlans(self, port): + vlan_text = None + + vlan_part_re = re.compile('vlan participation include (.*)') + + try: + self._cli("show running-config interface %s" % port) + for line in self._read_long_output("show running-config interface"): + match = vlan_part_re.match(line) + if match: + if vlan_text != None: + vlan_text += "," + vlan_text += (match.group(1)) + else: + vlan_text = match.group(1) + + if vlan_text is None: + return [1] + else: + vlans = self._parse_vlan_list(vlan_text) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_port_vlans(port) + + # Wrapper around connection.send - by default, expect() the same + # text we've sent, to remove it from the output from the + # switch. For the few cases where we don't need that, override + # this using echo=False. + # Horrible, but seems to work. + def _cli(self, text, echo=True): + self.connection.send(text + '\r') + if echo: + try: + self.connection.expect(text) + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_out(text) + raise PExpectError("_cli failed on %s" % text) + except: + logging.error("Unexpected error: %s", sys.exc_info()[0]) + raise + +if __name__ == "__main__": + + # Simple test harness - exercise the main working functions above to verify + # they work. This does *NOT* test really disruptive things like "save + # running-config" and "reload" - test those by hand. + + import optparse + + switch = 'vlandswitch05' + parser = optparse.OptionParser() + parser.add_option("--switch", + dest = "switch", + action = "store", + nargs = 1, + type = "string", + help = "specify switch to connect to for testing", + metavar = "") + (opts, args) = parser.parse_args() + if opts.switch: + switch = opts.switch + + logging.basicConfig(level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s') + p = NetgearXSM(switch, 23, debug=True) + p.switch_connect('admin', '', None) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.vlan_get_name(2) + print "VLAN 2 is named \"%s\"" % buf + + print "Create VLAN 3" + p.vlan_create(3) + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "Set name of VLAN 3 to test333" + p.vlan_set_name(3, "test333") + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + print "Destroy VLAN 3" + p.vlan_destroy(3) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.port_get_mode("1/0/10") + print "Port 1/0/10 is in %s mode" % buf + + buf = p.port_get_mode("1/0/11") + print "Port 1/0/11 is in %s mode" % buf + + # Test access stuff + print "Set 1/0/9 to access mode" + p.port_set_mode("1/0/9", "access") + + print "Move 1/0/9 to VLAN 4" + p.port_set_access_vlan("1/0/9", 4) + + buf = p.port_get_access_vlan("1/0/9") + print "Read from switch: 1/0/9 is on VLAN %s" % buf + + print "Move 1/0/9 back to VLAN 1" + p.port_set_access_vlan("1/0/9", 1) + + print "Create VLAN 2" + p.vlan_create(2) + + print "Create VLAN 3" + p.vlan_create(3) + + print "Create VLAN 4" + p.vlan_create(4) + + # Test access stuff + print "Set 1/0/9 to trunk mode" + p.port_set_mode("1/0/9", "trunk") + print "Read from switch: which VLANs is 1/0/9 on?" + buf = p.port_get_trunk_vlan_list("1/0/9") + p.dump_list(buf) + + # The adds below are NOOPs in effect on this switch - no filtering + # for "trunk" ports + print "Add 1/0/9 to VLAN 2" + p.port_add_trunk_to_vlan("1/0/9", 2) + print "Add 1/0/9 to VLAN 3" + p.port_add_trunk_to_vlan("1/0/9", 3) + print "Add 1/0/9 to VLAN 4" + p.port_add_trunk_to_vlan("1/0/9", 4) + print "Read from switch: which VLANs is 1/0/9 on?" + buf = p.port_get_trunk_vlan_list("1/0/9") + p.dump_list(buf) + + # And the same for removals here + p.port_remove_trunk_from_vlan("1/0/9", 3) + p.port_remove_trunk_from_vlan("1/0/9", 2) + p.port_remove_trunk_from_vlan("1/0/9", 4) + print "Read from switch: which VLANs is 1/0/9 on?" + buf = p.port_get_trunk_vlan_list("1/0/9") + p.dump_list(buf) + +# print 'Restarting switch, to explicitly reset config' +# p.switch_restart() + +# p.switch_save_running_config() + +# p.switch_disconnect() +# p._show_config() diff --git a/Vland/drivers/TPLinkTLSG2XXX.py b/Vland/drivers/TPLinkTLSG2XXX.py new file mode 100644 index 0000000..5213d10 --- /dev/null +++ b/Vland/drivers/TPLinkTLSG2XXX.py @@ -0,0 +1,695 @@ +#! /usr/bin/python + +# Copyright 2014-2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import sys +import re +import pexpect + +if __name__ == '__main__': + import os + vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) + sys.path.insert(0, vlandpath) + sys.path.insert(0, "%s/.." % vlandpath) + +from errors import InputError, PExpectError +from drivers.common import SwitchDriver, SwitchErrors + +class TPLinkTLSG2XXX(SwitchDriver): + + connection = None + _username = None + _password = None + _enable_password = None + + _capabilities = [ + ] + + # Regexp of expected hardware information - fail if we don't see + # this + _expected_descr_re = re.compile(r'TL-SG2\d\d\d') + + def __init__(self, switch_hostname, switch_telnetport=23, debug=False): + SwitchDriver.__init__(self, switch_hostname, debug) + self._systemdata = [] + self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) + self.errors = SwitchErrors() + + ################################ + ### Switch-level API functions + ################################ + + # Save the current running config into flash - we want config to + # remain across reboots + def switch_save_running_config(self): + try: + self._cli("copy running-config startup-config") + self.connection.expect("OK") + except (PExpectError, pexpect.EOF): + # recurse on error + self._switch_connect() + self.switch_save_running_config() + + # Restart the switch - we need to reload config to do a + # roll-back. Do NOT save running-config first if the switch asks - + # we're trying to dump recent changes, not save them. + # + # This will also implicitly cause a connection to be closed + def switch_restart(self): + self._cli("reboot") + index = self.connection.expect(['Daving current', 'Continue?']) + if index == 0: + self._cli("n") # No, don't save + self.connection.expect("Continue?") + + # Fall through + self._cli("y") # Yes, continue to reset + self.connection.close(True) + + # List the capabilities of the switch (and driver) - some things + # make no sense to abstract. Returns a dict of strings, each one + # describing an extra feature that that higher levels may care + # about + def switch_get_capabilities(self): + return self._capabilities + + ################################ + ### VLAN API functions + ################################ + + # Create a VLAN with the specified tag + def vlan_create(self, tag): + logging.debug("Creating VLAN %d", tag) + + try: + self._configure() + self._cli("vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + return + raise IOError("Failed to create VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_create(tag) + + # Destroy a VLAN with the specified tag + def vlan_destroy(self, tag): + logging.debug("Destroying VLAN %d", tag) + + try: + self._configure() + self._cli("no vlan %d" % tag) + self._end_configure() + + # Validate it happened + vlans = self.vlan_get_list() + for vlan in vlans: + if vlan == tag: + raise IOError("Failed to destroy VLAN %d" % tag) + + except PExpectError: + # recurse on error + self._switch_connect() + self.vlan_destroy(tag) + + # Set the name of a VLAN + def vlan_set_name(self, tag, name): + logging.debug("Setting name of VLAN %d to %s", tag, name) + + try: + self._configure() + self._cli("vlan %d" % tag) + self._cli("name %s" % name) + self._end_configure() + + # Validate it happened + read_name = self.vlan_get_name(tag) + if read_name != name: + raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" + % (tag, read_name, name)) + + except (PExpectError, pexpect.EOF): + # recurse on error + self._switch_connect() + self.vlan_set_name(tag, name) + + # Get a list of the VLAN tags currently registered on the switch + def vlan_get_list(self): + logging.debug("Grabbing list of VLANs") + + try: + vlans = [] + regex = re.compile(r'^ *(\d+).*active') + + self._cli("show vlan brief") + for line in self._read_long_output("show vlan brief"): + match = regex.match(line) + if match: + vlans.append(int(match.group(1))) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_list() + + # For a given VLAN tag, ask the switch what the associated name is + def vlan_get_name(self, tag): + logging.debug("Grabbing the name of VLAN %d", tag) + + try: + name = None + regex = re.compile(r'^ *\d+\s+(\S+).*(active)') + self._cli("show vlan id %d" % tag) + for line in self._read_long_output("show vlan id"): + match = regex.match(line) + if match: + name = match.group(1) + name.strip() + return name + + except PExpectError: + # recurse on error + self._switch_connect() + return self.vlan_get_name(tag) + + ################################ + ### Port API functions + ################################ + + # Set the mode of a port: access or trunk + def port_set_mode(self, port, mode): + logging.debug("Setting port %s to %s", port, mode) + if not self._is_port_mode_valid(mode): + raise IndexError("Port mode %s is not allowed" % mode) + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + # This switch does not support specific modes, so we can't + # actually change the mode directly. However, we can and + # should deal with the PVID and memberships of existing VLANs + + try: + # We define a trunk to be on *all* VLANs on the switch in + # tagged mode, and PVID should match the default VLAN (1). + if mode == "trunk": + # Disconnect all the untagged ports + read_vlans = self._port_get_all_vlans(port, 'Untagged') + for vlan in read_vlans: + self._port_remove_general_vlan(port, vlan) + + # And move to VLAN 1 + self.port_add_trunk_to_vlan(port, 1) + self._set_pvid(port, 1) + + # And an access port should only be on one VLAN. Move to + # VLAN 1, untagged, and set PVID there. + if mode == "access": + # Disconnect all the ports + read_vlans = self._port_get_all_vlans(port, 'Untagged') + for vlan in read_vlans: + self._port_remove_general_vlan(port, vlan) + read_vlans = self._port_get_all_vlans(port, 'Tagged') + for vlan in read_vlans: + self._port_remove_general_vlan(port, vlan) + + # And move to VLAN 1 + self.port_set_access_vlan(port, 1) + self._set_pvid(port, 1) + + # Validate it happened + read_mode = self._port_get_mode(port) + if read_mode != mode: + raise IOError("Failed to set mode for port %s" % port) + + # And cache the result + self._port_modes[port] = mode + + except (PExpectError, pexpect.EOF): + # recurse on error + self._switch_connect() + self.port_set_mode(port, mode) + + # Set an access port to be in a specified VLAN (tag) + def port_set_access_vlan(self, port, tag): + logging.debug("Setting access port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + # Does the VLAN already exist? + vlan_list = self.vlan_get_list() + if not tag in vlan_list: + raise IndexError("VLAN tag %d not recognised" % tag) + + try: + # Add the new VLAN + self._configure() + self._cli("interface %s" % self._long_port_name(port)) + self._cli("switchport general allowed vlan %d untagged" % tag) + self._cli("no shutdown") + self._end_configure() + + self._set_pvid(port, tag) + + # Now drop all the other VLANs + read_vlans = self._port_get_all_vlans(port, 'Untagged') + for vlan in read_vlans: + if vlan != tag: + self._port_remove_general_vlan(port, vlan) + + # Finally, validate things worked + read_vlan = int(self.port_get_access_vlan(port)) + if read_vlan != tag: + raise IOError("Failed to move access port %s to VLAN %d - got VLAN %d instead" + % (port, tag, read_vlan)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_set_access_vlan(port, tag) + + # Add a trunk port to a specified VLAN (tag) + def port_add_trunk_to_vlan(self, port, tag): + logging.debug("Adding trunk port %s to VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + try: + self._configure() + self._cli("interface %s" % self._long_port_name(port)) + self._cli("switchport general allowed vlan %d tagged" % tag) + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag or vlan == "ALL": + return + raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_add_trunk_to_vlan(port, tag) + + # Remove a trunk port from a specified VLAN (tag) + def port_remove_trunk_from_vlan(self, port, tag): + logging.debug("Removing trunk port %s from VLAN %d", port, tag) + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + + try: + self._configure() + self._cli("interface %s" % self._long_port_name(port)) + self._cli("no switchport general allowed vlan %d" % tag) + self._end_configure() + + # Validate it happened + read_vlans = self.port_get_trunk_vlan_list(port) + for vlan in read_vlans: + if vlan == tag: + raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) + + except PExpectError: + # recurse on error + self._switch_connect() + self.port_remove_trunk_from_vlan(port, tag) + + # Get the configured VLAN tag for an access port (i.e. a port not + # configured for tagged egress) + def port_get_access_vlan(self, port): + logging.debug("Getting VLAN for access port %s", port) + vlan = 1 + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + regex = re.compile(r'(\d+)\s+.*Untagged') + + try: + self._cli("show interface switchport %s" % self._long_port_name(port)) + for line in self._read_long_output("show interface switchport"): + match = regex.match(line) + if match: + vlan = match.group(1) + return int(vlan) + + except PExpectError: + # recurse on error + self._switch_connect() + return self.port_get_access_vlan(port) + + # Get the list of configured VLAN tags for a trunk port + def port_get_trunk_vlan_list(self, port): + logging.debug("Getting VLANs for trunk port %s", port) + + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + + return self._port_get_all_vlans(port, 'Tagged') + + ################################ + ### Internal functions + ################################ + + # Connect to the switch and log in + def _switch_connect(self): + + if not self.connection is None: + self.connection.close(True) + self.connection = None + + logging.debug("Connecting to Switch with: %s", self.exec_string) + self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) + self._login() + + # No way to avoid paged output on this switch AFAICS + + # And grab details about the switch. in case we need it + self._get_systemdata() + + # And also validate them - make sure we're driving a switch of + # the correct model! Also store the serial number + descr_regex = re.compile(r'Hardware Version\s+ - (.*)') + descr = "" + + for line in self._systemdata: + match = descr_regex.match(line) + if match: + descr = match.group(1) + + logging.debug("system description is %s", descr) + + if not self._expected_descr_re.match(descr): + raise IOError("Switch %s not recognised by this driver: abort" % descr) + + # Now build a list of our ports, for later sanity checking + self._ports = self._get_port_names() + if len(self._ports) < 4: + raise IOError("Not enough ports detected - problem!") + + def _login(self): + logging.debug("attempting login with username \"%s\", password \"%s\", enable_password \"%s\"", self._username, self._password, self._enable_password) + self.connection.expect('User Access Login') + if self._username is not None: + self.connection.expect("User:") + self._cli("%s" % self._username) + if self._password is not None: + self.connection.expect("Password:") + self._cli("%s" % self._password, False) + while True: + index = self.connection.expect(['User Name:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) + if index != 4: # Any other means: failed to log in! + logging.error("Login failure: index %d\n", index) + logging.error("Login failure: %s\n", self.connection.match.before) + raise IOError + + # else + self._prompt_name = re.escape(self.connection.match.group(1).strip()) + if self.connection.match.group(2) == ">": + # Need to enter "enable" mode too + self._cli("") + self._cli("enable") + if self._enable_password is not None and len(self._enable_password) > 0: + self.connection.expect("Password:") + self._cli("%s" % self._enable_password, False) + index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) + if index != 3: # Any other means: failed to log in! + logging.error("Enable password failure: %s\n", self.connection.match) + raise IOError + return 0 + + def _logout(self): + logging.debug("Logging out") + self._cli("exit", False) + self.connection.close(True) + + def _configure(self): + self._cli("configure") + + def _end_configure(self): + self._cli("end") + + def _read_long_output(self, text): + longbuf = [] + prompt = self._prompt_name + '#' + while True: + try: + index = self.connection.expect(['^Press any key to continue', prompt]) + if index == 0: # "Press any key to continue (Q to quit)" + for line in self.connection.before.split('\r\n'): + line1 = re.sub('(\x08|\x0D)*', '', line.strip()) + longbuf.append(line1) + self._cli(' ', False) + elif index == 1: # Back to a prompt, says output is finished + break + except (pexpect.EOF, pexpect.TIMEOUT): + # Something went wrong; logout, log in and try again! + logging.error("PEXPECT FAILURE, RECONNECT") + self.errors.log_error_in(text) + raise PExpectError("_read_long_output failed") + except: + logging.error("prompt is \"%s\"", prompt) + raise + + for line in self.connection.before.split('\r\n'): + line1 = re.sub('(\x08|\x0D)*', '', line.strip()) + longbuf.append(line1) + + return longbuf + + def _long_port_name(self, port): + return re.sub('Gi', 'gigabitEthernet ', port) + + def _set_pvid(self, port, pvid): + # Set a port's PVID + self._configure() + self._cli("interface %s" % self._long_port_name(port)) + self._cli("switchport pvid %d" % pvid) + self._end_configure() + + def _port_get_all_vlans(self, port, port_type): + vlans = [] + regex = re.compile(r'(\d+)\s+.*' + port_type) + + try: + self._cli("show interface switchport %s" % self._long_port_name(port)) + for line in self._read_long_output("show interface switchport"): + match = regex.match(line) + if match: + vlan = match.group(1) + vlans.append(int(vlan)) + return vlans + + except PExpectError: + # recurse on error + self._switch_connect() + return self._port_get_all_vlans(port, port_type) + + def _port_remove_general_vlan(self, port, tag): + try: + self._configure() + self._cli("interface %s" % self._long_port_name(port)) + self._cli("no switchport general allowed vlan %d" % tag) + self._end_configure() + + except PExpectError: + # recurse on error + self._switch_connect() + return self._port_remove_general_vlan(port, tag) + + def _get_port_names(self): + logging.debug("Grabbing list of ports") + interfaces = [] + + # Use "Link" to only identify lines in the output that match + # interfaces that exist - it'll match "LinkUp" and "LinkDown" + regex = re.compile(r'^\s*([a-zA-Z0-9_/]*).*Link') + + try: + self._cli("show interface status") + for line in self._read_long_output("show interface status"): + match = regex.match(line) + if match: + interface = match.group(1) + interfaces.append(interface) + self._port_numbers[interface] = len(interfaces) + logging.debug(" found %d ports on the switch", len(interfaces)) + return interfaces + + except PExpectError: + # recurse on error + self._switch_connect() + return self._get_port_names() + + # Get the mode of a port: access or trunk + def _port_get_mode(self, port): + logging.debug("Getting mode of port %s", port) + + if not self._is_port_name_valid(port): + raise IndexError("Port name %s not recognised" % port) + + # This switch does not support specific modes, so we have to + # make stuff up here. We define trunk ports to be on (1 or + # many) tagged VLANs, anything not tagged to be access. + read_vlans = self._port_get_all_vlans(port, 'Tagged') + if len(read_vlans) > 0: + return "trunk" + else: + return "access" + + def _show_config(self): + logging.debug("Grabbing config") + self._cli("show running-config") + return self._read_long_output("show running-config") + + def _show_clock(self): + logging.debug("Grabbing time") + self._cli("show system-time") + return self._read_long_output("show system-time") + + def _get_systemdata(self): + logging.debug("Grabbing system sw and hw versions") + + self._cli("show system-info") + self._systemdata = [] + for line in self._read_long_output("show system-info"): + self._systemdata.append(line) + + # Wrapper around connection.send - by default, expect() the same + # text we've sent, to remove it from the output from the + # switch. For the few cases where we don't need that, override + # this using echo=False. + # Horrible, but seems to work. + def _cli(self, text, echo=True): + self.connection.send(text + '\r') + if echo: + self.connection.expect(text) + +if __name__ == "__main__": + + # Simple test harness - exercise the main working functions above to verify + # they work. This does *NOT* test really disruptive things like "save + # running-config" and "reload" - test those by hand. + + import optparse + + switch = '10.172.2.50' + parser = optparse.OptionParser() + parser.add_option("--switch", + dest = "switch", + action = "store", + nargs = 1, + type = "string", + help = "specify switch to connect to for testing", + metavar = "") + (opts, args) = parser.parse_args() + if opts.switch: + switch = opts.switch + + logging.basicConfig(level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s') + p = TPLinkTLSG2XXX(switch, 23, debug = False) + p.switch_connect('admin', 'admin', None) + + print "Ports are:" + buf = p.switch_get_port_names() + p.dump_list(buf) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.vlan_get_name(2) + print "VLAN 2 is named \"%s\"" % buf + + print "Create VLAN 3" + p.vlan_create(3) + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "Set name of VLAN 3 to test333" + p.vlan_set_name(3, "test333") + + buf = p.vlan_get_name(3) + print "VLAN 3 is named \"%s\"" % buf + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + print "Destroy VLAN 3" + p.vlan_destroy(3) + + print "VLANs are:" + buf = p.vlan_get_list() + p.dump_list(buf) + + buf = p.port_get_mode("Gi1/0/10") + print "Port Gi1/0/10 is in %s mode" % buf + + buf = p.port_get_mode("Gi1/0/11") + print "Port Gi1/0/11 is in %s mode" % buf + + # Test access stuff + buf = p.port_get_mode("Gi1/0/9") + print "Port Gi1/0/9 is in %s mode" % buf + + print "Set Gi1/0/9 to access mode" + p.port_set_mode("Gi1/0/9", "access") + + print "Move Gi1/0/9 to VLAN 4" + p.port_set_access_vlan("Gi1/0/9", 4) + + buf = p.port_get_access_vlan("Gi1/0/9") + print "Read from switch: Gi1/0/9 is on VLAN %s" % buf + + print "Move Gi1/0/9 back to VLAN 1" + p.port_set_access_vlan("Gi1/0/9", 1) + + # Test access stuff + print "Set Gi1/0/9 to trunk mode" + p.port_set_mode("Gi1/0/9", "trunk") + print "Read from switch: which VLANs is Gi1/0/9 on?" + buf = p.port_get_trunk_vlan_list("Gi1/0/9") + p.dump_list(buf) + print "Add Gi1/0/9 to VLAN 2" + p.port_add_trunk_to_vlan("Gi1/0/9", 2) + print "Add Gi1/0/9 to VLAN 3" + p.port_add_trunk_to_vlan("Gi1/0/9", 3) + print "Add Gi1/0/9 to VLAN 4" + p.port_add_trunk_to_vlan("Gi1/0/9", 4) + print "Read from switch: which VLANs is Gi1/0/9 on?" + buf = p.port_get_trunk_vlan_list("Gi1/0/9") + p.dump_list(buf) + + p.port_remove_trunk_from_vlan("Gi1/0/9", 3) + p.port_remove_trunk_from_vlan("Gi1/0/9", 3) + p.port_remove_trunk_from_vlan("Gi1/0/9", 4) + print "Read from switch: which VLANs is Gi1/0/9 on?" + buf = p.port_get_trunk_vlan_list("Gi1/0/9") + p.dump_list(buf) + + + p.switch_save_running_config() + + p.switch_disconnect() +# p._show_config() diff --git a/Vland/drivers/__init__.py b/Vland/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Vland/drivers/common.py b/Vland/drivers/common.py new file mode 100644 index 0000000..e564c9e --- /dev/null +++ b/Vland/drivers/common.py @@ -0,0 +1,167 @@ +#! /usr/bin/python + +# Copyright 2014-2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import time +import logging + +from errors import InputError, PExpectError + +class SwitchErrors: + """ Error logging and statistics class """ + + def __init__(self): + self.errors_in = 0 + self.errors_out = 0 + + def __repr__(self): + return "" % (self.errors_in, self.errors_out) + + # For now, just count the error. Later on we might add stats and + # analysis + def log_error_in(self, text): + self.errors_in += 1 + + # For now, just count the error. Later on we might add stats and + # analysis + def log_error_out(self, text): + self.errors_out += 1 + +class SwitchDriver(object): + + connection = None + hostname = "" + serial_number = '' + + _allowed_port_modes = [ "trunk", "access" ] + _ports = [] + _port_modes = {} + _port_numbers = {} + _prompt_name = '' + _username = '' + _password = '' + _enable_password = '' + _systemdata = [] + + def __init__ (self, switch_hostname, debug): + + if debug: + # Configure logging for pexpect output if we have debug + # enabled + + # get the logger + self.logger = logging.getLogger(switch_hostname) + + # give the logger the methods required by pexpect + self.logger.write = self._log_write + self.logger.flush = self._log_do_nothing + + else: + self.logger = None + + self.hostname = switch_hostname + + # Connect to the switch and log in + def switch_connect(self, username, password, enablepassword): + self._username = username + self._password = password + self._enable_password = enablepassword + self._switch_connect() + + # Log out of the switch and drop the connection and all state + def switch_disconnect(self): + self._logout() + logging.debug("Closing connection to %s", self.hostname) + self._ports = [] + self._port_modes.clear() + self._port_numbers.clear() + self._prompt_name = '' + self._systemdata = [] + del(self) + + def dump_list(self, data): + i = 0 + for line in data: + print "%d: \"%s\"" % (i, line) + i += 1 + + def _delay(self): + time.sleep(0.5) + + # List the capabilities of the switch (and driver) - some things + # make no sense to abstract. Returns a dict of strings, each one + # describing an extra feature that that higher levels may care + # about + def switch_get_capabilities(self): + return self._capabilities + + # List the names of all the ports on the switch + def switch_get_port_names(self): + return self._ports + + def _is_port_name_valid(self, name): + for port in self._ports: + if name == port: + return True + return False + + def _is_port_mode_valid(self, mode): + for allowed in self._allowed_port_modes: + if allowed == mode: + return True + return False + + # Try to look up a port mode in our cache. If not there, go ask + # the switch and cache the result + def port_get_mode(self, port): + if not self._is_port_name_valid(port): + raise InputError("Port name %s not recognised" % port) + if port in self._port_modes: + logging.debug("port_get_mode: returning mode %s from cache for port %s", self._port_modes[port], port) + return self._port_modes[port] + else: + mode = self._port_get_mode(port) + self._port_modes[port] = mode + logging.debug("port_get_mode: found mode %s for port %s, adding to cache", self._port_modes[port], port) + return mode + + def port_map_name_to_number(self, port_name): + if not self._is_port_name_valid(port_name): + raise InputError("Port name %s not recognised" % port_name) + logging.debug("port_map_name_to_number: returning %d for port_name %s", self._port_numbers[port_name], port_name) + return self._port_numbers[port_name] + + # Wrappers to adapt logging for pexpect when we've configured on a + # switch. + # This will be the method called by the pexpect object to write a + # log message + def _log_write(self, *args, **kwargs): + # ignore other parameters, pexpect only uses one arg + content = args[0] + + if content in [' ', '', '\n', '\r', '\r\n']: + return # don't log empty lines + + # Split the output into multiple lines so we get a + # well-formatted logfile + for line in content.split('\r\n'): + logging.info(line) + + # This is the flush method for pexpect + def _log_do_nothing(self): + pass diff --git a/Vland/errors.py b/Vland/errors.py new file mode 100644 index 0000000..6759e50 --- /dev/null +++ b/Vland/errors.py @@ -0,0 +1,59 @@ +# Copyright 2014-2016 Linaro Limited +# Authors: Dave Pigott , +# Steve McIntyre +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +class VlandError(Exception): + """ + Base exception and error class for the vlan daemon + """ + + +class CriticalError(VlandError): + """ + The critical error + """ + +class NotFoundError(VlandError): + """ + Couldn't find object + """ + +class InputError(VlandError): + """ + Invalid input + """ + +class ConfigError(VlandError): + """ + Invalid configuration + """ + +class SocketError(VlandError): + """ + Socket connection failure + """ + +class PExpectError(VlandError): + """ + CLI communication failure + """ + +class Error: + OK = 0 + FAILED = 1 + NOTFOUND = 2 diff --git a/Vland/ipc/__init__.py b/Vland/ipc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Vland/ipc/client-new.py b/Vland/ipc/client-new.py new file mode 100644 index 0000000..8834f9d --- /dev/null +++ b/Vland/ipc/client-new.py @@ -0,0 +1,18 @@ +import socket +import time +import json +from ipc import VlanIpc + +host = 'localhost' # The remote host +port = 3080 # The same port as used by the server + +s = VlanIpc() +s.client_connect(host, port) +msg = {"group": "group1", "client_name": "client1", "request": "lava_sync", "message": "bye bye world"} +print "Sending to server:" +print msg +s.client_send(msg) +ret = s.client_recv_and_close() +print "Server said in reply: " +print ret + diff --git a/Vland/ipc/ipc.py b/Vland/ipc/ipc.py new file mode 100644 index 0000000..5961ed1 --- /dev/null +++ b/Vland/ipc/ipc.py @@ -0,0 +1,177 @@ +# Copyright 2014-2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Simple VLANd IPC module + +import socket +import json +import time +import datetime +import os +import sys +import logging + +from Vland.errors import CriticalError, InputError, ConfigError, SocketError + +class VlanIpc: + """VLANd IPC class""" + + def __init__(self): + self.conn = None + self.socket = None + + def server_init(self, host, port): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.conn = None + + while True: + try: + self.socket.bind((host, port)) + break + except socket.error as e: + print "Can't bind to port %d: %s" % (port, e) + time.sleep(1) + + def server_listen(self): + if self.socket is None: + raise SocketError("Server can't receive data: no socket") + self.socket.listen(1) + + def server_recv(self): + if self.socket is None: + raise SocketError("Server can't receive data: no socket") + + self.conn, addr = self.socket.accept() + logging.debug("server: Connection from") + logging.debug(addr) + data = self.conn.recv(8) # 32bit limit + count = int(data, 16) + c = 0 + data = '' + while c < count: + data += self.conn.recv(1) + c += 1 + try: + json_data = json.loads(data) + except ValueError: + self.conn.close() + self.conn = None + raise SocketError("Server unable to decode receieved data: corrupt?") + + if 'client_name' not in json_data: + self.conn.close() + self.conn = None + raise SocketError("Server unable to detect client name: corrupt packet?") + + return json_data + + def server_reply(self, json_data): + if self.conn is None: + raise SocketError("Server can't send data: no connection") + + data = self._format_message(json_data) + if not data: + self.conn.close() + self.conn = None + raise SocketError("Server unable to format reply data") + + try: + # send the actual number of bytes to read. + self.conn.send(data[0]) + # now send the bytes. + self.conn.send(data[1]) + except socket.error as e: + logging.error("Can't send response to client: %s", e) + logging.error("Was trying to send data:") + logging.error(data) + + def server_close(self): + if self.conn is not None: + self.conn.shutdown(socket.SHUT_RDWR) + self.conn.close() + + def client_connect(self, host, port): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + ret = self.socket.connect_ex((host, port)) + if ret: + self.socket.close() + self.socket = None + raise SocketError("Client can't send connect: %s" % ret) + else: + break + except socket.error: + time.sleep(1) + return True + + def client_send(self, json_data): + if self.socket is None: + raise SocketError("Client can't send data: no socket") + + data = self._format_message(json_data) + if not data: + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() + self.socket = None + raise SocketError("Client unable to send data") + + # send the actual number of bytes to read. + self.socket.send(data[0]) + # now send the bytes. + self.socket.send(data[1]) + + def client_recv_and_close(self): + if self.socket is None: + raise SocketError("Client can't receieve data: no socket") + + data = self.socket.recv(8) # 32bit limit + count = int(data, 16) + c = 0 + data = '' + while c < count: + data += self.socket.recv(1) + c += 1 + try: + json_data = json.loads(data) + except ValueError: + self.socket.close() + self.socket = None + raise SocketError("Client unable to decode receieved data: corrupt?") + + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() + + return json_data + + # The default JSON serialiser code can't deal with datetime + # objects by default, so let's tell it how to. + def _json_serial(self, obj): + """JSON serializer for objects not serialisable by default json code""" + if isinstance(obj, datetime.datetime): + serial = obj.isoformat() + return serial + + def _format_message(self, json_data): + try: + msgstr = json.dumps(json_data, default=self._json_serial) + except ValueError: + return None + # "header" calculation + msglen = "%08X" % len(msgstr) + return (msglen, msgstr) diff --git a/Vland/ipc/server-new.py b/Vland/ipc/server-new.py new file mode 100644 index 0000000..e398dcc --- /dev/null +++ b/Vland/ipc/server-new.py @@ -0,0 +1,24 @@ +# Echo server test program +import socket +import time +import json +from ipc import VlanIpc + +host = 'localhost' # Symbolic name meaning the local host +port = 3080 # Arbitrary non-privileged port + +s = VlanIpc() +s.server_init(host, port) + +while True: + s.server_listen() + json_data = s.server_recv() + + print "client sent us:" + print json_data + + response = {'response': 'ack'} + print "sending reply:" + print response + + s.server_reply(response) diff --git a/Vland/util.py b/Vland/util.py new file mode 100644 index 0000000..738eb00 --- /dev/null +++ b/Vland/util.py @@ -0,0 +1,871 @@ +# Copyright 2014-2018 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Utility routines, including handling of API functions +# + +import logging +import time +from errors import CriticalError, NotFoundError, InputError, ConfigError, SocketError + +class VlanUtil: + """VLANd utility functions""" + + def set_logging_level(self, level): + loglevel = logging.CRITICAL + if level == "ERROR": + loglevel = logging.ERROR + elif level == "WARNING": + loglevel = logging.WARNING + elif level == "INFO": + loglevel = logging.INFO + elif level == "DEBUG": + loglevel = logging.DEBUG + return loglevel + + def get_switch_driver(self, switch_name, config): + logging.debug("Trying to find a driver for %s", switch_name) + driver = config.switches[switch_name].driver + logging.debug("Driver: %s", driver) + module = __import__("drivers.%s" % driver, fromlist=[driver]) + class_ = getattr(module, driver) + return class_(switch_name, debug = config.switches[switch_name].debug) + + def probe_switches(self, state): + config = state.config + ret = {} + for switch_name in sorted(config.switches): + logging.debug("Found switch %s:", switch_name) + logging.debug(" Probing...") + + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + ret[switch_name] = 'Found %d ports: ' % len(s.switch_get_port_names()) + for name in s.switch_get_port_names(): + ret[switch_name] += '%s ' % name + s.switch_disconnect() + del s + return ret + + # Simple helper wrapper for all the read-only database queries + def perform_db_query(self, state, command, data): + logging.debug('perform_db_query') + logging.debug(command) + logging.debug(data) + ret = {} + db = state.db + try: + if command == 'db.all_switches': + ret = db.all_switches() + elif command == 'db.all_ports': + ret = db.all_ports() + elif command == 'db.all_vlans': + ret = db.all_vlans() + elif command == 'db.all_trunks': + ret = db.all_trunks() + elif command == 'db.get_switch_by_id': + ret = db.get_switch_by_id(data['switch_id']) + elif command == 'db.get_switch_id_by_name': + ret = db.get_switch_id_by_name(data['name']) + elif command == 'db.get_switch_name_by_id': + ret = db.get_switch_name_by_id(data['switch_id']) + elif command == 'db.get_port_by_id': + ret = db.get_port_by_id(data['port_id']) + elif command == 'db.get_ports_by_switch': + ret = db.get_ports_by_switch(data['switch_id']) + elif command == 'db.get_port_by_switch_and_name': + ret = db.get_port_by_switch_and_name(data['switch_id'], data['name']) + elif command == 'db.get_port_by_switch_and_number': + ret = db.get_port_by_switch_and_number(data['switch_id'], int(data['number'])) + elif command == 'db.get_current_vlan_id_by_port': + ret = db.get_current_vlan_id_by_port(data['port_id']) + elif command == 'db.get_base_vlan_id_by_port': + ret = db.get_base_vlan_id_by_port(data['port_id']) + elif command == 'db.get_ports_by_current_vlan': + ret = db.get_ports_by_current_vlan(data['vlan_id']) + elif command == 'db.get_ports_by_base_vlan': + ret = db.get_ports_by_base_vlan(data['vlan_id']) + elif command == 'db.get_port_mode': + ret = db.get_port_mode(data['port_id']) + elif command == 'db.get_ports_by_trunk': + ret = db.get_ports_by_trunk(data['trunk_id']) + elif command == 'db.get_vlan_by_id': + ret = db.get_vlan_by_id(data['vlan_id']) + elif command == 'db.get_vlan_tag_by_id': + ret = db.get_vlan_tag_by_id(data['vlan_id']) + elif command == 'db.get_vlan_id_by_name': + ret = db.get_vlan_id_by_name(data['name']) + elif command == 'db.get_vlan_id_by_tag': + ret = db.get_vlan_id_by_tag(data['tag']) + elif command == 'db.get_vlan_name_by_id': + ret = db.get_vlan_name_by_id(data['vlan_id']) + elif command == 'db.get_trunk_by_id': + ret = db.get_trunk_by_id(data['trunk_id']) + else: + raise InputError("Unknown db_query command \"%s\"" % command) + + except (InputError, NotFoundError) as e: + logging.error('perform_db_query(%s) got error %s', command, e) + raise + except ValueError as e: + logging.error('perform_db_query(%s) got error %s', command, e) + raise InputError("Invalid value in API call argument: %s" % e) + except TypeError as e: + logging.error('perform_db_query(%s) got error %s', command, e) + raise InputError("Invalid type in API call argument: %s" % e) + + return ret + + # Simple helper wrapper for all the read-only daemon state queries + def perform_daemon_query(self, state, command, data): + logging.debug('perform_daemon_query') + logging.debug(command) + logging.debug(data) + ret = {} + try: + if command == 'daemon.status': + # data ignored + ret['running'] = 'ok' + ret['last_modified'] = state.db.get_last_modified_time() + elif command == 'daemon.version': + # data ignored + ret['version'] = state.version + elif command == 'daemon.statistics': + ret['uptime'] = time.time() - state.starttime + elif command == 'daemon.probe_switches': + ret = self.probe_switches(state) + elif command == 'daemon.shutdown': + # data ignored + ret['shutdown'] = 'Shutting down' + state.running = False + else: + raise InputError("Unknown daemon_query command \"%s\"" % command) + + except (InputError, NotFoundError) as e: + logging.error('perform_daemon_query(%s) got error %s', command, e) + raise + except ValueError as e: + logging.error('perform_daemon_query(%s) got error %s', command, e) + raise InputError("Invalid value in API call argument: %s" % e) + except TypeError as e: + logging.error('perform_daemon_query(%s) got error %s', command, e) + raise InputError("Invalid type in API call argument: %s" % e) + + return ret + + # Helper wrapper for API functions modifying database state only + def perform_db_update(self, state, command, data): + logging.debug('perform_db_update') + logging.debug(command) + logging.debug(data) + ret = {} + db = state.db + try: + if command == 'db.create_switch': + ret = db.create_switch(data['name']) + elif command == 'db.create_port': + try: + number = int(data['number']) + except ValueError: + raise InputError("Invalid value for port number (%s) - must be numeric only!" % data['number']) + ret = db.create_port(data['switch_id'], data['name'], + number, + state.default_vlan_id, + state.default_vlan_id) + elif command == 'db.create_trunk': + ret = db.create_trunk(data['port_id1'], data['port_id2']) + elif command == 'db.delete_switch': + ret = db.delete_switch(data['switch_id']) + elif command == 'db.delete_port': + ret = db.delete_port(data['port_id']) + elif command == 'db.set_port_is_locked': + ret = db.set_port_is_locked(data['port_id'], + data['is_locked'], + data['lock_reason']) + elif command == 'db.set_base_vlan': + ret = db.set_base_vlan(data['port_id'], data['base_vlan_id']) + elif command == 'db.delete_trunk': + ret = db.delete_trunk(data['trunk_id']) + else: + raise InputError("Unknown db_update command \"%s\"" % command) + + except (InputError, NotFoundError) as e: + logging.error('perform_db_update(%s) got error %s', command, e) + raise + except ValueError as e: + logging.error('perform_db_update(%s) got error %s', command, e) + raise InputError("Invalid value in API call argument: %s" % e) + except TypeError as e: + logging.error('perform_db_update(%s) got error %s', command, e) + raise InputError("Invalid type in API call argument: %s" % e) + + return ret + + # Helper wrapper for API functions that modify both database state + # and on-switch VLAN state + def perform_vlan_update(self, state, command, data): + logging.debug('perform_vlan_update') + logging.debug(command) + logging.debug(data) + ret = {} + + try: + # All of these are complex commands, so call helpers + # rather than inline the code here + if command == 'api.create_vlan': + ret = self.create_vlan(state, data['name'], int(data['tag']), data['is_base_vlan']) + elif command == 'api.delete_vlan': + ret = self.delete_vlan(state, int(data['vlan_id'])) + elif command == 'api.set_port_mode': + ret = self.set_port_mode(state, int(data['port_id']), data['mode']) + elif command == 'api.set_current_vlan': + ret = self.set_current_vlan(state, int(data['port_id']), int(data['vlan_id'])) + elif command == 'api.restore_base_vlan': + ret = self.restore_base_vlan(state, int(data['port_id'])) + elif command == 'api.auto_import_switch': + ret = self.auto_import_switch(state, data['switch']) + else: + raise InputError("Unknown query command \"%s\"" % command) + + except (InputError, NotFoundError) as e: + logging.error('perform_vlan_update(%s) got error %s', command, e) + raise + except ValueError as e: + logging.error('perform_vlan_update(%s) got error %s', command, e) + raise InputError("Invalid value in API call argument: %s" % e) + except TypeError as e: + logging.error('perform_vlan_update(%s) got error %s', command, e) + raise InputError("Invalid type in API call argument: %s" % e) + + return ret + + + # Complex call + # 1. create the VLAN in the DB + # 2. Iterate through all switches: + # a. Create the VLAN + # b. Add the VLAN to all trunk ports (if needed) + # 3. If all went OK, save config on all the switches + # + # The VLAN may already exist on some of the switches, that's + # fine. If things fail, we attempt to roll back by rebooting + # switches then removing the VLAN in the DB. + def create_vlan(self, state, name, tag, is_base_vlan): + + logging.debug('create_vlan') + db = state.db + config = state.config + + # Check for tag == -1, i.e. use the next available tag + if tag == -1: + tag = db.find_lowest_unused_vlan_tag() + logging.debug('create_vlan called with a tag of -1, found first unused tag %d', tag) + + # 1. Database record first + try: + logging.debug('Adding DB record first: name %s, tag %d, is_base_vlan %d', name, tag, is_base_vlan) + vlan_id = db.create_vlan(name, tag, is_base_vlan) + logging.debug('Added VLAN tag %d, name %s to the database, created VLAN ID %d', tag, name, vlan_id) + except (InputError, NotFoundError): + logging.debug('DB creation failed') + raise + + # Keep track of which switches we've configured, for later use + switches_done = [] + + # 2. Now the switches + try: + for switch in db.all_switches(): + trunk_ports = [] + switch_name = switch['name'] + try: + logging.debug('Adding new VLAN to switch %s', switch_name) + # Get the right driver + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + + # Mark this switch as one we've touched, for + # either config saving or rollback below + switches_done.append(switch_name) + + # 2a. Create the VLAN on the switch + s.vlan_create(tag) + s.vlan_set_name(tag, name) + logging.debug('Added VLAN tag %d, name %s to switch %s', tag, name, switch_name) + + # 2b. Do we need to worry about trunk ports on this switch? + if 'TrunkWildCardVlans' in s.switch_get_capabilities(): + logging.debug('This switch does not need special trunk port handling') + else: + logging.debug('This switch needs special trunk port handling') + trunk_ports = db.get_trunk_port_names_by_switch(switch['switch_id']) + if trunk_ports is None: + logging.debug("But it has no trunk ports defined") + trunk_ports = [] + else: + logging.debug('Found %d trunk_ports that need adjusting', len(trunk_ports)) + + # Modify any trunk ports as needed + for port in trunk_ports: + logging.debug('Adding VLAN tag %d, name %s to switch %s port %s', tag, name, switch_name, port) + s.port_add_trunk_to_vlan(port, tag) + + # And now we're done with this switch + s.switch_disconnect() + del s + + except IOError as e: + logging.error('Failed to add VLAN %d to switch ID %d (%s): %s', tag, switch['switch_id'], switch['name'], e) + raise + + except IOError: + # Bugger. Looks like one of the switch calls above + # failed. To undo the changes safely, we'll need to reset + # all the switches we managed to configure. This could + # take some time! + logging.error('create_vlan failed, resetting all switches to recover') + for switch_name in switches_done: + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + s.switch_restart() # Will implicitly also close the connection + del s + + # Undo the database change + logging.debug('Switch access failed. Deleting the new VLAN entry in the database') + db.delete_vlan(vlan_id) + raise + + # If we've got this far, things were successful. Save config + # on all the switches so it will persist across reboots + for switch_name in switches_done: + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + s.switch_save_running_config() + s.switch_disconnect() + del s + + return (vlan_id, tag) # If we're successful + + # Complex call + # 1. Check in the DB if there are any ports on the VLAN. Bail if so + # 2. Iterate through all switches: + # a. Remove the VLAN from all trunk ports (if needed) + # b. Remove the VLAN + # 3. If all went OK, save config on the switches + # 4. Remove the VLAN in the DB + # + # If things fail, we attempt to roll back by rebooting switches. + def delete_vlan(self, state, vlan_id): + + logging.debug('delete_vlan') + db = state.db + config = state.config + + # 1. Check for database records first + logging.debug('Checking for ports using VLAN ID %d', vlan_id) + vlan = db.get_vlan_by_id(vlan_id) + if vlan is None: + raise NotFoundError("VLAN ID %d does not exist" % vlan_id) + vlan_tag = vlan['tag'] + ports = db.get_ports_by_current_vlan(vlan_id) + if ports is not None: + raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % + (vlan_id, len(ports))) + ports = db.get_ports_by_base_vlan(vlan_id) + if ports is not None: + raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % + (vlan_id, len(ports))) + + # Keep track of which switches we've configured, for later use + switches_done = [] + + # 2. Now the switches + try: + for switch in db.all_switches(): + switch_name = switch['name'] + trunk_ports = [] + try: + # Get the right driver + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + + # Mark this switch as one we've touched, for + # either config saving or rollback below + switches_done.append(switch_name) + + # 2a. Do we need to worry about trunk ports on this switch? + if 'TrunkWildCardVlans' in s.switch_get_capabilities(): + logging.debug('This switch does not need special trunk port handling') + else: + logging.debug('This switch needs special trunk port handling') + trunk_ports = db.get_trunk_port_names_by_switch(switch['switch_id']) + if trunk_ports is None: + logging.debug("But it has no trunk ports defined") + trunk_ports = [] + else: + logging.debug('Found %d trunk_ports that need adjusting', len(trunk_ports)) + + # Modify any trunk ports as needed + for port in trunk_ports: + s.port_remove_trunk_from_vlan(port, vlan_tag) + logging.debug('Removed VLAN tag %d from switch %s port %s', vlan_tag, switch_name, port) + + # 2b. Remove the VLAN from the switch + logging.debug('Removing VLAN tag %d from switch %s', vlan_tag, switch_name) + s.vlan_destroy(vlan_tag) + logging.debug('Removed VLAN tag %d from switch %s', vlan_tag, switch_name) + + # And now we're done with this switch + s.switch_disconnect() + del s + + except IOError: + raise + + except IOError: + # Bugger. Looks like one of the switch calls above + # failed. To undo the changes safely, we'll need to reset + # all the switches we managed to configure. This could + # take some time! + logging.error('delete_vlan failed, resetting all switches to recover') + for switch_name in switches_done: + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + s.switch_restart() # Will implicitly also close the connection + del s + raise + + # 3. If we've got this far, things were successful. Save + # config on all the switches so it will persist across reboots + for switch_name in switches_done: + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + s.switch_save_running_config() + s.switch_disconnect() + del s + + # 4. Finally, remove the VLAN in the DB + try: + logging.debug('Removing DB record: VLAN ID %d', vlan_id) + vlan_id = db.delete_vlan(vlan_id) + logging.debug('Removed VLAN ID %d from the database OK', vlan_id) + except (InputError, NotFoundError): + logging.debug('DB deletion failed') + raise + + return vlan_id # If we're successful + + # Complex call, depends on existing state a lot + # 1. Check validity of inputs + # 2. Switch mode and other config on the port. + # a. If switching trunk->access, remove all trunk VLANs from it + # (if needed) and switch back to the base VLAN for the + # port. Next, switch to access mode. + # b. If switching access->trunk, switch back to the base VLAN + # for the port. Next, switch mode. Then add all trunk VLANs + # to it (if needed) + # 3. If all went OK, save config on the switch + # 4. Change details of the port in the DB + # + # If things fail, we attempt to roll back by rebooting the switch + def set_port_mode(self, state, port_id, mode): + + logging.debug('set_port_mode') + db = state.db + config = state.config + + # 1. Sanity-check inputs + if mode != 'access' and mode != 'trunk': + raise InputError("Port mode '%s' is not a valid option: try 'access' or 'trunk'" % mode) + port = db.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % port_id) + if port['is_locked']: + raise InputError("Port ID %d is locked" % port_id) + if mode == 'trunk' and port['is_trunk']: + raise InputError("Port ID %d is already in trunk mode" % port_id) + if mode == 'access' and not port['is_trunk']: + raise InputError("Port ID %d is already in access mode" % port_id) + base_vlan_tag = db.get_vlan_tag_by_id(port['base_vlan_id']) + + # Get the right driver + switch_name = db.get_switch_name_by_id(port['switch_id']) + s = self.get_switch_driver(switch_name, config) + + # 2. Now start configuring the switch + try: + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + except: + logging.debug('Failed to talk to switch %s!', switch_name) + raise + + try: + if port['is_trunk']: + # 2a. We're going from a trunk port to an access port + if 'TrunkWildCardVlans' in s.switch_get_capabilities(): + logging.debug('This switch does not need special trunk port handling') + else: + logging.debug('This switch needs special trunk port handling') + vlans = s.port_get_trunk_vlan_list(port['name']) + if vlans is None: + logging.debug("But it has no VLANs defined on port %s", port['name']) + vlans = [] + else: + logging.debug('Found %d vlans that may need dropping on port %s', len(vlans), port['name']) + + for vlan in vlans: + if vlan != state.config.vland.default_vlan_tag: + s.port_remove_trunk_from_vlan(port['name'], vlan) + + s.port_set_mode(port['name'], "access") + + else: + # 2b. We're going from an access port to a trunk port + s.port_set_access_vlan(port['name'], base_vlan_tag) + s.port_set_mode(port['name'], "trunk") + if 'TrunkWildCardVlans' in s.switch_get_capabilities(): + logging.debug('This switch does not need special trunk port handling') + else: + vlans = db.all_vlans() + for vlan in vlans: + if vlan['tag'] != state.config.vland.default_vlan_tag: + s.port_add_trunk_to_vlan(port['name'], vlan['tag']) + + except IOError: + logging.error('set_port_mode failed, resetting switch to recover') + # Bugger. Looks like one of the switch calls above + # failed. To undo the changes safely, we'll need to reset + # all the config on this switch + s.switch_restart() # Will implicitly also close the connection + del s + raise + + # 3. All seems to have worked so far! + s.switch_save_running_config() + s.switch_disconnect() + del s + + # 4. And update the DB + db.set_port_mode(port_id, mode) + + return port_id # If we're successful + + # Complex call, updating both DB and switch state + # 1. Check validity of inputs + # 2. Update the port config on the switch + # 3. If all went OK, save config on the switch + # 4. Change details of the port in the DB + # + # If things fail, we attempt to roll back by rebooting the switch + def set_current_vlan(self, state, port_id, vlan_id): + + logging.debug('set_current_vlan') + db = state.db + config = state.config + + # 1. Sanity checks! + port = db.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % port_id) + if port['is_locked']: + raise InputError("Port ID %d is locked" % port_id) + if port['is_trunk']: + raise InputError("Port ID %d is not an access port" % port_id) + + vlan = db.get_vlan_by_id(vlan_id) + if vlan is None: + raise NotFoundError("VLAN ID %d does not exist" % vlan_id) + + # Get the right driver + switch_name = db.get_switch_name_by_id(port['switch_id']) + s = self.get_switch_driver(switch_name, config) + + # 2. Now start configuring the switch + try: + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + except: + logging.debug('Failed to talk to switch %s!', switch_name) + raise + + try: + s.port_set_access_vlan(port['name'], vlan['tag']) + except IOError: + logging.error('set_current_vlan failed, resetting switch to recover') + # Bugger. Looks like one of the switch calls above + # failed. To undo the changes safely, we'll need to reset + # all the config on this switch + s.switch_restart() # Will implicitly also close the connection + del s + raise + + # 3. All seems to have worked so far! + s.switch_save_running_config() + s.switch_disconnect() + del s + + # 4. And update the DB + db.set_current_vlan(port_id, vlan_id) + + return port_id # If we're successful + + # Complex call, updating both DB and switch state + # 1. Check validity of input + # 2. Update the port config on the switch + # 3. If all went OK, save config on the switch + # 4. Change details of the port in the DB + # + # If things fail, we attempt to roll back by rebooting the switch + def restore_base_vlan(self, state, port_id): + + logging.debug('restore_base_vlan') + db = state.db + config = state.config + + # 1. Sanity checks! + port = db.get_port_by_id(port_id) + if port is None: + raise NotFoundError("Port ID %d does not exist" % port_id) + if port['is_trunk']: + raise InputError("Port ID %d is not an access port" % port_id) + if port['is_locked']: + raise InputError("Port ID %d is locked" % port_id) + + # Bail out early if we're *already* on the base VLAN. This is + # not an error + if port['current_vlan_id'] == port['base_vlan_id']: + return port_id + + vlan = db.get_vlan_by_id(port['base_vlan_id']) + + # Get the right driver + switch_name = db.get_switch_name_by_id(port['switch_id']) + s = self.get_switch_driver(switch_name, config) + + # 2. Now start configuring the switch + try: + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + except: + logging.debug('Failed to talk to switch %s!', switch_name) + raise + + try: + s.port_set_access_vlan(port['name'], vlan['tag']) + except IOError: + logging.error('restore_base_vlan failed, resetting switch to recover') + # Bugger. Looks like one of the switch calls above + # failed. To undo the changes safely, we'll need to reset + # all the config on this switch + s.switch_restart() # Will implicitly also close the connection + del s + raise + + # 3. All seems to have worked so far! + s.switch_save_running_config() + s.switch_disconnect() + del s + + # 4. And update the DB + db.set_current_vlan(port_id, port['base_vlan_id']) + + return port_id # If we're successful + + # Complex call, updating both DB and switch state + # * Check validity of input + # * Read all the config from the switch (switch, ports, VLANs) + # * Create initial DB entries to match each of those + # * Merge VLANs across all switches + # * Set up ports appropriately + # + def auto_import_switch(self, state, switch_name): + + logging.debug('auto_import_switch') + db = state.db + config = state.config + + port_vlans = {} + + # 1. Sanity checks! + switch_id = db.get_switch_id_by_name(switch_name) + if switch_id is not None: + raise InputError("Switch name %s already exists in the DB (ID %d)" % (switch_name, switch_id)) + + if not switch_name in config.switches: + raise NotFoundError("Switch name %s not defined in config" % switch_name) + + # 2. Now start reading config from the switch + try: + s = self.get_switch_driver(switch_name, config) + s.switch_connect(config.switches[switch_name].username, + config.switches[switch_name].password, + config.switches[switch_name].enable_password) + except: + logging.debug('Failed to talk to switch %s!', switch_name) + raise + + # DON'T create the switch record in the DB first - we'll want + # to create VLANs on *other* switches, and it's easier to do + # that before we've added our new switch + + new_vlan_tags = [] + + # Grab the VLANs defined on this switch + vlan_tags = s.vlan_get_list() + + logging.debug(' found %d vlans on the switch', len(vlan_tags)) + + for vlan_tag in vlan_tags: + vlan_name = s.vlan_get_name(vlan_tag) + + # If a VLAN is already in the database, then that's easy - + # we can just ignore it. However, we have to check that + # there is not a different name for the existing VLAN tag + # - bail out if so... UNLESS we're looking at the default + # VLAN + # + # If this VLAN tag is not already in the DB, we'll need to + # add it there and to all the other switches (and their + # trunk ports!) too. + vlan_id = db.get_vlan_id_by_tag(vlan_tag) + if vlan_id != state.default_vlan_id: + if vlan_id is not None: + vlan_db_name = db.get_vlan_name_by_id(vlan_id) + if vlan_name != vlan_db_name: + raise InputError("Can't add VLAN tag %d (name %s) for this switch - VLAN tag %d already exists in the database, but with a different name (%s)" % (vlan_tag, vlan_name, vlan_tag, vlan_db_name)) + + else: + # OK, we'll need to set up the new VLAN now. It can't + # be a base VLAN - switches don't have such a concept! + # Rather than create individually here, add to a + # list. *Only* once we've worked through all the + # switch's VLANs successfully (checking for existing + # records and possible clashes!) should we start + # committing changes + new_vlan_tags.append(vlan_tag) + + # Now create the VLAN DB entries + for vlan_tag in new_vlan_tags: + vlan_name = s.vlan_get_name(vlan_tag) + vlan_id = self.create_vlan(state, vlan_name, vlan_tag, False) + + # *Now* add this switch itself to the database, after we've + # worked on all the other switches + switch_id = db.create_switch(switch_name) + + # And now the ports + trunk_ports = [] + ports = s.switch_get_port_names() + logging.debug(' found %d ports on the switch', len(ports)) + for port_name in ports: + logging.debug(' trying to import port %s', port_name) + port_id = None + port_mode = s.port_get_mode(port_name) + port_number = s.port_map_name_to_number(port_name) + if port_mode == 'access': + # Access ports are easy - just create the port, and + # set both the current and base VLANs to the current + # VLAN on the switch. We'll end up changing this after + # import if needed. + port_vlans[port_name] = (s.port_get_access_vlan(port_name),) + port_vlan_id = db.get_vlan_id_by_tag(port_vlans[port_name][0]) + port_id = db.create_port(switch_id, port_name, port_number, + port_vlan_id, port_vlan_id) + logging.debug(' access port, VLAN %d', int(port_vlans[port_name][0])) + # Nothing further needed + elif port_mode == 'trunk': + logging.debug(' trunk port, VLANs:') + # Trunk ports are a little more involved. First, + # create the port in the DB, setting the VLANs to the + # first VLAN found on the trunk port. This will *also* + # be in access mode by default, and unlocked. + port_vlans[port_name] = s.port_get_trunk_vlan_list(port_name) + logging.debug(port_vlans[port_name]) + if port_vlans[port_name] == [] or port_vlans[port_name] is None or 'ALL' in port_vlans[port_name]: + port_vlans[port_name] = (state.config.vland.default_vlan_tag,) + port_vlan_id = db.get_vlan_id_by_tag(port_vlans[port_name][0]) + port_id = db.create_port(switch_id, port_name, port_number, + port_vlan_id, port_vlan_id) + # Append to a list of trunk ports that we will need to + # modify once we're done + trunk_ports.append(port_id) + else: + # We've found a port mode we don't want, e.g. the + # "dynamic auto" on a Cisco Catalyst. Handle that here + # - tell the switch to set that port to access and + # handle accordingly. + s.port_set_mode(port_name, 'access') + port_vlans[port_name] = (s.port_get_access_vlan(port_name),) + port_vlan_id = db.get_vlan_id_by_tag(port_vlans[port_name][0]) + port_id = db.create_port(switch_id, port_name, port_number, + port_vlan_id, port_vlan_id) + logging.debug(' Found port in %s mode', port_mode) + logging.debug(' Forcing to access mode, VLAN %d', int(port_vlans[port_name][0])) + port_mode = "access" + + logging.debug(" Added port %s, got port ID %d", port_name, port_id) + + db.set_port_mode(port_id, port_mode) + + # Make sure this switch has all the VLANs we need + for vlan in db.all_vlans(): + if vlan['tag'] != state.config.vland.default_vlan_tag: + if not vlan['tag'] in vlan_tags: + logging.debug("Adding VLAN tag %d to this switch", vlan['tag']) + s.vlan_create(vlan['tag']) + s.vlan_set_name(vlan['tag'], vlan['name']) + + # Now, on each trunk port on the switch, we need to add all + # the VLANs already configured across our system + if not 'TrunkWildCardVlans' in s.switch_get_capabilities(): + for port_id in trunk_ports: + port = db.get_port_by_id(port_id) + + for vlan in db.all_vlans(): + if vlan['vlan_id'] != state.default_vlan_id: + if not vlan['tag'] in port_vlans[port['name']]: + logging.debug("Adding allowed VLAN tag %d to trunk port %s", vlan['tag'], port['name']) + s.port_add_trunk_to_vlan(port['name'], vlan['tag']) + + # Done with this switch \o/ + s.switch_save_running_config() + s.switch_disconnect() + del s + + ret = {} + ret['switch_id'] = switch_id + ret['num_ports_added'] = len(ports) + ret['num_vlans_added'] = len(new_vlan_tags) + return ret # If we're successful diff --git a/Vland/visualisation/__init__.py b/Vland/visualisation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Vland/visualisation/graphics.py b/Vland/visualisation/graphics.py new file mode 100644 index 0000000..84083d0 --- /dev/null +++ b/Vland/visualisation/graphics.py @@ -0,0 +1,484 @@ +#! /usr/bin/python + +# Copyright 2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Visualisation graphics module for VLANd +# +# This code uses python-gd to generate graphics ready for insertion +# into our web interface. Example code in the self-test at the +# bottom. + +import gd, os, sys + +from Vland.errors import InputError + +class Graphics: + """ Code and config for the visualisation graphics module """ + + font = None + + # Default font size for the small labels + small_font_size = 12 + + # And the size for the top-level label + label_font_size = 24 + + # Size in pixels of that font, calculated later + twocharwidth = 0 + charheight = 0 + + # How big a gap to leave between trunk connections + trunk_gap = 8 + + # Details of the legend + legend_width = 0 + legend_height = 0 + legend_text_width = 0 + legend_text_height = 0 + legend_total_width = 0 + legend_box_width = 0 + legend_box_height = 0 + + # Basic colour definitions used later + colour_defs = {} + colour_defs['black'] = (0, 0, 0) + colour_defs['white'] = (255, 255, 255) + colour_defs['purple'] = (255, 0, 255) + colour_defs['blue'] = (0, 0, 255) + colour_defs['darkgrey'] = (60, 60, 60) + colour_defs['yellow'] = (255, 255, 0) + colour_defs['red'] = (255, 0, 0) + colour_defs['aqua'] = (0, 255, 255) + + pallette = {} + + # colours for the background + pallette['bg_colour'] = 'purple' + pallette['transparent_colour'] = 'purple' + pallette['graphic_label_colour'] = 'black' + + # switch colours + pallette['switch_outline_colour'] = 'black' + pallette['switch_fill_colour'] = 'darkgrey' + pallette['switch_label_colour'] = 'white' + + # verious sets of port colours, matching the 'highlight' options in + # draw_port() + port_pallette = {} + port_pallette['normal'] = {} + port_pallette['normal']['port_box'] = 'white' + port_pallette['normal']['port_bg'] = 'black' + port_pallette['normal']['port_label'] = 'white' + port_pallette['normal']['trace'] = 'black' + + port_pallette['trunk'] = {} + port_pallette['trunk']['port_box'] = 'white' + port_pallette['trunk']['port_bg'] = 'blue' + port_pallette['trunk']['port_label'] = 'yellow' + port_pallette['trunk']['trace'] = 'blue' + + port_pallette['locked'] = {} + port_pallette['locked']['port_box'] = 'white' + port_pallette['locked']['port_bg'] = 'red' + port_pallette['locked']['port_label'] = 'yellow' + port_pallette['locked']['trace'] = 'red' + + port_pallette['VLAN'] = {} + port_pallette['VLAN']['port_box'] = 'white' + port_pallette['VLAN']['port_bg'] = 'aqua' + port_pallette['VLAN']['port_label'] = 'black' + port_pallette['VLAN']['trace'] = 'aqua' + + im = None + + # TODO: make colours configurable, add maybe parsing for + # /etc/X11/rgb.txt to allow people to use arbitrary names? + + # Choose a font for our graphics to use. Pass in a list of fonts + # to be tried, in priority order. + def set_font(self, fontlist): + for font in fontlist: + if os.path.exists(font): + self.font = os.path.abspath(font) + break + + # Work out how big we need to be for the biggest possible text + # in a 2-digit number. Grotty, but we need to know this later. + for value in range (0, 100): + (width, height) = self.get_label_size(repr(value), self.small_font_size) + self.twocharwidth = max(self.twocharwidth, width) + self.charheight = max(self.charheight, height) + + # Now we can also calulate other stuff + self._calc_legend_size() + + # Create a canvas and set things up ready for use + def create_canvas(self, x, y): + im = gd.image((x, y)) + + # Allocate our colours in the image's colour map + for key in self.colour_defs.iterkeys(): + im.colorAllocate((self.colour_defs[key][0], + self.colour_defs[key][1], + self.colour_defs[key][2])) + + im.fill((0,0), im.colorExact(self.colour_defs[self.pallette['bg_colour']])) + im.colorTransparent(im.colorExact(self.colour_defs[self.pallette['transparent_colour']])) + im.interlace(0) + self.im = im + + # Using our selected font, what dimensions will a particular piece + # of text take? + def get_label_size(self, label, font_size): + tmp_im = gd.image((200, 200)) + (llx, lly, lrx, lry, urx, ury, ulx, uly) = tmp_im.get_bounding_rect(self.font, + font_size, + 0.0, + (10, 100), label) + width = max(lrx, urx) - min(llx, ulx) + height = max(lly, lry) - min(uly, ury) + return (width, height) + + # Draw a trunk connection between two ports + # + # Ports are defined as (ulx,uly),(lrx,lry), top): x, y + # co-ordinates of UL and LR corners, and whether the port is on + # the top or bottom row of a switch, i.e. does the wire come up or + # down when it leaves the port. + def draw_trunk(self, trunknum, node1, node2, colour): + for node in (node1, node2): + ((ulx,uly),(lrx,lry),top) = node + + # Work out the co-ordinates for a line vertically up or + # down from the edge of the port + x1 = int((ulx + lrx) / 2) + x2 = x1 + if (top): + y1 = uly + y2 = y1 - (self.trunk_gap * (trunknum + 1)) + else: + y1 = lry + y2 = y1 + (self.trunk_gap * (trunknum + 1)) + # Quick hack - use 2-pixel wide rectangles as thick lines :-) + # First line, vertically up/down from the port + self.im.rectangle((x1-1,y1), (x2,y2), self.im.colorExact(self.colour_defs[colour])) + # Now draw horizontally across to the left margin space + x3 = self.trunk_gap * (trunknum + 1) + self.im.rectangle((x3, y2), (x2,y2+1), self.im.colorExact(self.colour_defs[colour])) + + # Now join up the trunks vertically + ((ulx1,uly1),(lrx1,lry1),top1) = node1 + if (top1): + y1 = uly1 - self.trunk_gap * (trunknum + 1) + else: + y1 = lry1 + self.trunk_gap * (trunknum + 1) + ((ulx2,uly2),(lrx2,lry2),top2) = node2 + if (top2): + y2 = uly2 - self.trunk_gap * (trunknum + 1) + else: + y2 = lry2 + self.trunk_gap * (trunknum + 1) + x1 = self.trunk_gap * (trunknum + 1) + self.im.rectangle((x1, y1), (x1+1,y2), self.im.colorExact(self.colour_defs[colour])) + + # How big is the legend? + def _calc_legend_size(self): + max_width = 0 + max_height = 0 + + for value in self.port_pallette.iterkeys(): + (width, height) = self.get_label_size(value, self.small_font_size) + max_width = max(max_width, width) + max_height = max(max_height, height) + + (width, height) = self.get_label_size('##', self.small_font_size) + self.legend_box_width = width + 6 + self.legend_box_height = height + 6 + self.legend_width = max_width + self.legend_box_width + 10 + self.legend_height = 3 + self.legend_box_height + 3 + self.legend_text_width = max_width + self.legend_text_height = max_height + self.legend_total_width = 6 + (len(self.port_pallette) * self.legend_width) + + # Return the legend dimensions + def get_legend_dimensions(self): + return (self.legend_total_width, self.legend_height) + + # Draw the legend using (left, top) as the top left corner + def draw_legend(self, left, top): + lrx = left + self.legend_total_width - 1 + lry = top + self.legend_height - 1 + self.im.rectangle((left, top), (lrx, lry), + self.im.colorExact(self.colour_defs[self.pallette['switch_outline_colour']]), + self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']])) + curr_x = left + 3 + curr_y = top + 3 + + for value in sorted(self.port_pallette): + box_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_box']]) + box_bg_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_bg']]) + text_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_label']]) + lrx = curr_x + self.legend_box_width - 1 + lry = curr_y + self.legend_box_height - 1 + self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour) + + llx = curr_x + 4 + lly = curr_y + self.legend_box_height - 4 + self.im.string_ttf(self.font, self.small_font_size, 0.0, (llx, lly), '##', text_colour) + curr_x += self.legend_box_width + self.im.string_ttf(self.font, self.small_font_size, 0.0, (curr_x + 3, lly), value, + self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']])) + curr_x += self.legend_text_width + 10 + + # Draw the graphic's label using (left, top) as the top left + # corner with a box around + def draw_label(self, left, top, label, gap): + box_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']]) + box_bg_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']]) + text_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']]) + (width, height) = self.get_label_size(label, self.label_font_size) + curr_x = left + curr_y = top + lrx = curr_x + width + gap + lry = curr_y + height + 20 + self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour) + curr_x = left + 10 + curr_y = top + height + 6 + self.im.string_ttf(self.font, self.label_font_size, 0.0, (curr_x, curr_y), label, text_colour) + + +class Switch: + """ Code and config for dealing with a switch """ + port_width = 0 + port_height = 0 + text_width = 0 + text_height = 0 + label_left = 0 + label_bot = 0 + total_width = 0 + total_height = 0 + num_ports = 0 + left = None + top = None + name = None + + # Set up a new switch instance; calculate all the sizes so we can + # size our canvas + def __init__(self, g, num_ports, name): + self.num_ports = num_ports + self.name = name + self._calc_port_size(g) + self._calc_switch_size(g) + + # How big is a port and the text within it? + def _calc_port_size(self, g): + self.text_width = g.twocharwidth + self.text_height = g.charheight + # Leave enough space around the text for a nice clear box + self.port_width = self.text_width + 6 + self.port_height = self.text_height + 6 + + # How big is the full switch, including all the ports and the + # switch name label? + def _calc_switch_size(self, g): + (label_width, label_height) = g.get_label_size(self.name, g.small_font_size) + num_ports = self.num_ports + # Make sure we have an even number for 2 rows + if (self.num_ports & 1): + num_ports += 1 + self.label_left = 3 + (num_ports * self.port_width / 2) + 3 + self.label_bot = self.port_height - 2 + self.total_width = self.label_left + label_width + 3 + self.total_height = 3 + max(label_height, (2 * self.port_height)) + 3 + + # Return the switch dimensions + def get_dimensions(self): + return (self.total_width, self.total_height) + + # Draw the basic switch outline and label using (left, top) as the + # top left corner. The switch object will remember this origin for + # later use when drawing ports. + def draw_switch(self, g, left, top): + self.left = left + self.top = top + lrx = left + self.total_width -1 + lry = top + self.total_height - 1 + g.im.rectangle((left, top), (lrx, lry), + g.im.colorExact(g.colour_defs[g.pallette['switch_outline_colour']]), + g.im.colorExact(g.colour_defs[g.pallette['switch_fill_colour']])) + llx = left + self.label_left + lly = top + self.label_bot + g.im.string_ttf(g.font, g.small_font_size, 0.0, (llx, lly), self.name, + g.im.colorExact(g.colour_defs[g.pallette['switch_label_colour']])) + + # Draw a port inside the switch, using a specified colour scheme + # to denote its type. The switch outline must have been drawn + # first, for its origin to be set. + def draw_port(self, g, portnum, highlight): + if portnum < 1 or portnum > self.num_ports: + raise InputError('port number out of range') + if not self.left or not self.top: + raise InputError('cannot draw ports before switch is drawn') + if highlight not in g.port_pallette.iterkeys(): + raise InputError('unknown highlight type \"%s\"' % highlight) + + box_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_box']]) + box_bg_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_bg']]) + text_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_label']]) + + if (portnum & 1): # odd port number, so top row + ulx = self.left + 3 + ((portnum-1) * self.port_width / 2) + uly = self.top + 3 + else: # bottom row + ulx = self.left + 3 + ((portnum-2) * self.port_width / 2) + uly = self.top + 3 + self.port_height + lrx = ulx + self.port_width - 1 + lry = uly + self.port_height - 1 + g.im.rectangle((ulx,uly), (lrx,lry), box_colour, box_bg_colour) + + # centre the text + (width, height) = g.get_label_size(repr(portnum), g.small_font_size) + llx = ulx + 3 + (self.text_width - width) / 2 + lly = uly + max(height, self.text_height) + 1 + g.im.string_ttf(g.font, g.small_font_size, + 0.0, (llx, lly), repr(portnum), text_colour) + + # Quick helper: draw all the ports for a switch in the default + # colour scheme. + def draw_default_ports(self, g): + for portnum in range(1, self.num_ports + 1): + self.draw_port(g, portnum, 'normal') + + # Get the (x,y) co-ordinates of the UL and LR edges of the port + # box, and if it's upper row. This lets us so useful things such + # as draw a connection to that point for a trunk. + def get_port_location(self, portnum): + if portnum > self.num_ports: + raise InputError('port number out of range') + + if (portnum & 1): # odd port number, so top row + ulx = self.left + 3 + ((portnum-1) * self.port_width / 2) + uly = self.top + lrx = ulx + self.port_width + lry = uly + self.port_height + return ((ulx,uly), (lrx,lry), True) + else: # bottom row + ulx = self.left + 3 + ((portnum-2) * self.port_width / 2) + uly = self.top + 3 + self.port_height + lrx = ulx + self.port_width + lry = uly + self.port_height + return ((ulx,uly), (lrx,lry), False) + + # Debug: print some of the state of the switch object + def dump_state(self): + print 'port_width %d' % self.port_width + print 'port_height %d' % self.port_height + print 'text_width %d' % self.text_width + print 'text_height %d' % self.text_height + print 'label_left %d' % self.label_left + print 'label_bot %d' % self.label_bot + print 'total_width %d' % self.total_width + print 'total_height %d' % self.total_height + +# Test harness - generate a PNG using fake data +if __name__ == '__main__': + gim = Graphics() + gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf', + '/usr/share/fonts/truetype/freefont/FreeMono.ttf']) + try: + gim.font + except NameError: + print 'no fonts found' + sys.exit(1) + + switch = {} + size_x = {} + size_y = {} + switch[0] = Switch(gim, 48, 'lngswitch01') + switch[1] = Switch(gim, 24, 'lngswitch02') + switch[2] = Switch(gim, 52, 'lngswitch03') + label = "VLAN 4jj" + + # Need to set gaps big enough for the number of trunks, at least. + num_trunks = 3 + y_gap = max(20, 15 * num_trunks) + x_gap = max(20, 15 * num_trunks) + + x = 0 + y = y_gap + + for i in range (0, 3): + (size_x[i], size_y[i]) = switch[i].get_dimensions() + x = max(x, size_x[i]) + y += size_y[i] + y_gap + + # Add space for the legend and the label + (legend_width, legend_height) = gim.get_legend_dimensions() + (label_width, label_height) = gim.get_label_size(label, gim.label_font_size) + + x = max(x, legend_width + 2*x_gap + label_width) + x = x_gap + x + x_gap + y = y + max(legend_height + y_gap, label_height) + + gim.create_canvas(x, y) + + curr_y = y_gap + switch[0].draw_switch(gim, x_gap, curr_y) + switch[0].draw_default_ports(gim) + switch[0].draw_port(gim, 2, 'VLAN') + switch[0].draw_port(gim, 5, 'locked') + switch[0].draw_port(gim, 11, 'trunk') + switch[0].draw_port(gim, 44, 'trunk') + curr_y += size_y[0] + y_gap + + switch[1].draw_switch(gim, x_gap, curr_y) + switch[1].draw_default_ports(gim) + switch[1].draw_port(gim, 5, 'VLAN') + switch[1].draw_port(gim, 8, 'locked') + switch[1].draw_port(gim, 13, 'trunk') + switch[1].draw_port(gim, 16, 'trunk') + curr_y += size_y[2] + y_gap + + switch[2].draw_switch(gim, x_gap, curr_y) + switch[2].draw_default_ports(gim) + switch[2].draw_port(gim, 1, 'trunk') + switch[2].draw_port(gim, 2, 'locked') + switch[2].draw_port(gim, 14, 'trunk') + switch[2].draw_port(gim, 19, 'VLAN') + curr_y += size_y[2] + y_gap + + # Now let's try and draw some trunks! + gim.draw_trunk(0, + switch[0].get_port_location(11), + switch[1].get_port_location(16), + gim.port_pallette['trunk']['trace']) + gim.draw_trunk(1, + switch[1].get_port_location(13), + switch[2].get_port_location(1), + gim.port_pallette['trunk']['trace']) + gim.draw_trunk(2, + switch[0].get_port_location(44), + switch[2].get_port_location(14), + gim.port_pallette['trunk']['trace']) + + gim.draw_legend(x_gap, curr_y) + gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2)) + + f=open('xx.png','w') + gim.im.writePng(f) + f.close() + print 'Test graphic written to xx.png' diff --git a/Vland/visualisation/visualisation.py b/Vland/visualisation/visualisation.py new file mode 100644 index 0000000..603835d --- /dev/null +++ b/Vland/visualisation/visualisation.py @@ -0,0 +1,498 @@ +#! /usr/bin/python + +# Copyright 2015 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Visualisation module for VLANd. Fork a trivial webserver +# implementation on an extra, and generate a simple set of pages and +# graphics on demand. +# + +import os, sys, logging, time, datetime, re, signal +from multiprocessing import Process +from BaseHTTPServer import BaseHTTPRequestHandler +from BaseHTTPServer import HTTPServer +import urlparse +import cStringIO + +from Vland.errors import InputError +from Vland.db.db import VlanDB +from Vland.config.config import VlanConfig +from Vland.visualisation.graphics import Graphics,Switch +from Vland.util import VlanUtil +class VlandHTTPServer(HTTPServer): + """ Trivial wrapper for HTTPServer so we can include our own state. """ + def __init__(self, server_address, handler, state): + HTTPServer.__init__(self, server_address, handler) + self.state = state + +class GraphicsCache(object): + """ Cache for graphics state, to avoid having to recalculate every + query too many times. """ + last_update = None + graphics = {} + + def __init__(self): + # Pick an epoch older than any sensible use + self.last_update = datetime.datetime(2000, 01, 01) + +class Visualisation(object): + """ Code and config for the visualisation graphics module. """ + + state = None + p = None + + # Fork a new process for the visualisation webserver + def __init__(self, state): + self.state = state + self.p = Process(target=self.visloop, args=()) + self.p.start() + + def _receive_signal(self, signum, stack): + if signum == signal.SIGUSR1: + self.state.db_ok = True + + # The main loop for the visualisation webserver + def visloop(self): + self.state.db_ok = False + self.state.cache = GraphicsCache() + + loglevel = VlanUtil().set_logging_level(self.state.config.logging.level) + + # Should we log to stderr? + if self.state.config.logging.filename is None: + logging.basicConfig(level = loglevel, + format = '%(asctime)s %(levelname)-8s %(message)s') + else: + logging.basicConfig(level = loglevel, + format = '%(asctime)s %(levelname)-8s VIS %(message)s', + datefmt = '%Y-%m-%d %H:%M:%S %Z', + filename = self.state.config.logging.filename, + filemode = 'a') + logging.info('%s visualisation starting up', self.state.banner) + + # Wait for main process to signal to us that it's finished with any + # database upgrades and we can open it without any problems. + signal.signal(signal.SIGUSR1, self._receive_signal) + while not self.state.db_ok: + logging.info('%s visualisation waiting for db_ok signal', self.state.banner) + time.sleep(1) + logging.info('%s visualisation received db_ok signal', self.state.banner) + + self.state.db = VlanDB(db_name=self.state.config.database.dbname, + username=self.state.config.database.username, + readonly=True) + + server = VlandHTTPServer(('', self.state.config.visualisation.port), + GetHandler, self.state) + server.serve_forever() + + # Kill the webserver + def shutdown(self): + self.p.terminate() + + # Kill the webserver + def signal_db_ok(self): + os.kill(self.p.pid, signal.SIGUSR1) + +class GetHandler(BaseHTTPRequestHandler): + """ Methods to generate and serve the pages """ + + parsed_path = None + + # Trivial top-level page. Link to images for each of the VLANs we + # know about. + def send_index(self): + self.send_response(200) + self.wfile.write('Content-type: text/html\r\n') + self.end_headers() + config = self.server.state.config.visualisation + cache = self.server.state.cache + db = self.server.state.db + switches = db.all_switches() + vlans = db.all_vlans() + vlan_tags = {} + + for vlan in vlans: + vlan_tags[vlan['vlan_id']] = vlan['tag'] + + if cache.last_update < self.server.state.db.get_last_modified_time(): + logging.debug('Cache is out of date') + # Fill the cache with all the information we need: + # * the graphics themselves + # * the data to match each graphic, so we can generate imagemap/tooltips + cache.graphics = {} + if len(switches) > 0: + for vlan in vlans: + cache.graphics[vlan['vlan_id']] = self.generate_graphic(vlan['vlan_id']) + cache.last_update = datetime.datetime.utcnow() + + page = [] + page.append('') + page.append('') + page.append('') + page.append('') + page.append('VLANd visualisation') + page.append('') + if config.refresh and config.refresh > 0: + page.append('' % config.refresh) + page.append('') + page.append('') + + # Generate left-hand menu with links to each VLAN diagram + page.append('') + + # Now the main content area with the graphics + page.append('
') + page.append('

VLANd visualisation

') + + # Bail early if we have nothing to show! + if len(switches) == 0: + page.append('

No switches found in the database, nothing to show...

') + page.append('
') + page.append('') + self.wfile.write('\r\n'.join(page)) + return + + # Trivial javascript helpers for tooltip control + page.append('') + + # For each VLAN, add a graphic + for vlan in vlans: + this_image = cache.graphics[vlan['vlan_id']] + page.append('' % vlan['vlan_id']) + page.append('

VLAN ID %d, Tag %d, name %s

' % (vlan['vlan_id'], vlan['tag'], vlan['name'])) + + # Link to an image we generate from our data + page.append('

' % (vlan['vlan_id'],vlan['vlan_id'])) + + # Generate an imagemap describing all the ports, with + # javascript hooks to pop up/down a tooltip box based on + # later data. + page.append('' % vlan['vlan_id']) + for switch in this_image['ports'].keys(): + for portnum in this_image['ports'][switch].keys(): + this_port = this_image['ports'][switch][portnum] + port = this_port['db'] + ((ulx,uly),(lrx,lry),upper) = this_port['location'] + page.append('' % (vlan['vlan_id'], port['port_id'], vlan['vlan_id'], port['port_id'])) + page.append('

') + page.append('
') + page.append('') # End of normal content, all the VLAN graphics shown + + # Now generate the tooltip boxes for the ports. Each is + # fully-formed but invisible, ready for our javascript helper + # to pop visible on demand. + for vlan in vlans: + this_image = cache.graphics[vlan['vlan_id']] + for switch in this_image['ports'].keys(): + for portnum in this_image['ports'][switch].keys(): + this_port = this_image['ports'][switch][portnum] + port = this_port['db'] + page.append('
' % (vlan['vlan_id'], port['port_id'])) + page.append('Port ID: %d
' % port['port_id']) + page.append('Port Number: %d
' % port['number']) + page.append('Port Name: %s
' % port['name']) + if port['is_locked']: + page.append('Locked - ') + if (port['lock_reason'] is not None + and len(port['lock_reason']) > 1): + page.append(port['lock_reason']) + else: + page.append('unknown reason') + page.append('
') + if port['is_trunk']: + page.append('Trunk') + if port['trunk_id'] != -1: + page.append(' (Trunk ID %d)' % port['trunk_id']) + page.append('
') + else: + page.append('Current VLAN ID: %d (Tag %d)
' % (port['current_vlan_id'], vlan_tags[port['current_vlan_id']])) + page.append('Base VLAN ID: %d (Tag %d)
' % (port['base_vlan_id'], vlan_tags[port['base_vlan_id']])) + page.append('
') + + page.append('') + self.wfile.write('\r\n'.join(page)) + + # Simple-ish style sheet + def send_style(self): + self.send_response(200) + self.wfile.write('Content-type: text/css\r\n') + self.end_headers() + cache = self.server.state.cache + page = [] + page.append('body {') + page.append(' background: white;') + page.append(' color: black;') + page.append(' font-size: 12pt;') + page.append('}') + page.append('') + page.append('.menu {') + page.append(' position:fixed;') + page.append(' float:left;') + page.append(' font-family: arial, Helvetica, sans-serif;') + page.append(' width:20%;') + page.append(' height:100%;') + page.append(' font-size: 10pt;') + page.append(' padding-top: 10px;') + page.append('}') + page.append('') + page.append('.content {') + page.append(' position:relative;') + page.append(' padding-top: 10px;') + page.append(' width: 80%;') + page.append(' max-width:80%;') + page.append(' margin-left: 21%;') + page.append(' margin-top: 50px;') + page.append(' height:100%;') + page.append('}') + page.append('') + page.append('h1,h2,h3 {') + page.append(' font-family: arial, Helvetica, sans-serif;') + page.append(' padding-right:3pt;') + page.append(' padding-top:2pt;') + page.append(' padding-bottom:2pt;') + page.append(' margin-top:8pt;') + page.append(' margin-bottom:8pt;') + page.append(' border-style:none;') + page.append(' border-width:thin;') + page.append('}') + page.append('A:link { text-decoration: none; }') + page.append('A:visited { text-decoration: none}') + page.append('h1 { font-size: 18pt; }') + page.append('h2 { font-size: 14pt; }') + page.append('h3 { font-size: 12pt; }') + page.append('dl,ul { margin-top: 1pt; text-indent: 0 }') + page.append('ol { margin-top: 1pt; text-indent: 0 }') + page.append('div.date { font-size: 8pt; }') + page.append('div.sig { font-size: 8pt; }') + page.append('div.port {') + page.append(' display: block;') + page.append(' position: fixed;') + page.append(' left: 0px;') + page.append(' bottom: 0px;') + page.append(' z-index: 99;') + page.append(' background: #FFFF00;') + page.append(' border-style: solid;') + page.append(' border-width: 3pt;') + page.append(' border-color: #3B3B3B;') + page.append(' margin: 1;') + page.append(' padding: 5px;') + page.append(' font-size: 10pt;') + page.append(' font-family: Courier,monotype;') + page.append(' visibility: hidden;') + page.append('}') + self.wfile.write('\r\n'.join(page)) + + # Generate a PNG showing the layout of switches/port/trunks for a + # specific VLAN + def send_graphic(self): + vlan_id = 0 + vlan_re = re.compile(r'^/images/vlan/(\d+).png$') + match = vlan_re.match(self.parsed_path.path) + if match: + vlan_id = int(match.group(1)) + cache = self.server.state.cache + + # Do we have a graphic for this VLAN ID? + if not vlan_id in cache.graphics.keys(): + logging.debug('asked for vlan_id %s', vlan_id) + logging.debug(cache.graphics.keys()) + self.send_response(404) + self.wfile.write('Content-type: text/plain\r\n') + self.end_headers() + self.wfile.write('404 Not Found\r\n') + self.wfile.write('%s' % self.parsed_path.path) + logging.error('VLAN graphic not found - asked for %s', self.parsed_path.path) + return + + # Yes - just send it from the cache + self.send_response(200) + self.wfile.write('Content-type: image/png\r\n') + self.end_headers() + self.wfile.write(cache.graphics[vlan_id]['image']['png'].getvalue()) + return + + # Generate a PNG showing the layout of switches/port/trunks for a + # specific VLAN, and return that PNG along with geometry details + def generate_graphic(self, vlan_id): + db = self.server.state.db + vlan = db.get_vlan_by_id(vlan_id) + # We've been asked for a VLAN that doesn't exist + if vlan is None: + return None + + data = {} + data['image'] = {} + data['ports'] = {} + + gim = Graphics() + + # Pick fonts. TODO: Make these configurable? + gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf', + '/usr/share/fonts/truetype/freefont/FreeMono.ttf']) + try: + gim.font + # If we can't get the font we need, fail + except NameError: + self.send_response(500) + self.wfile.write('Content-type: text/plain\r\n') + self.end_headers() + self.wfile.write('500 Internal Server Error\r\n') + logging.error('Unable to generate graphic, no fonts found - asked for %s', + self.parsed_path.path) + return + + switch = {} + size_x = {} + size_y = {} + + switches = db.all_switches() + + # Need to set gaps big enough for the number of trunks, at least. + trunks = db.all_trunks() + y_gap = max(20, 15 * len(trunks)) + x_gap = max(20, 15 * len(trunks)) + + x = 0 + y = y_gap + + # Work out how much space we need for the switches + for i in range(0, len(switches)): + ports = db.get_ports_by_switch(switches[i]['switch_id']) + switch[i] = Switch(gim, len(ports), switches[i]['name']) + (size_x[i], size_y[i]) = switch[i].get_dimensions() + x = max(x, size_x[i]) + y += size_y[i] + y_gap + + # Add space for the legend and the label + label = "VLAN %d - %s" % (vlan['tag'], vlan['name']) + (legend_width, legend_height) = gim.get_legend_dimensions() + (label_width, label_height) = gim.get_label_size(label, gim.label_font_size) + x = max(x, legend_width + 2*x_gap + label_width) + x = x_gap + x + x_gap + y = y + max(legend_height + y_gap, label_height) + + # Create a canvas of the right size + gim.create_canvas(x, y) + + # Draw the switches and ports in it + curr_y = y_gap + for i in range(0, len(switches)): + switch[i].draw_switch(gim, x_gap, curr_y) + ports = db.get_ports_by_switch(switches[i]['switch_id']) + data['ports'][i] = {} + for port_id in ports: + port = db.get_port_by_id(port_id) + port_location = switch[i].get_port_location(port['number']) + data['ports'][i][port['number']] = {} + data['ports'][i][port['number']]['db'] = port + data['ports'][i][port['number']]['location'] = port_location + if port['is_locked']: + switch[i].draw_port(gim, port['number'], 'locked') + elif port['is_trunk']: + switch[i].draw_port(gim, port['number'], 'trunk') + elif port['current_vlan_id'] == int(vlan_id): + switch[i].draw_port(gim, port['number'], 'VLAN') + else: + switch[i].draw_port(gim, port['number'], 'normal') + curr_y += size_y[i] + y_gap + + # Now add the trunks + for i in range(0, len(trunks)): + ports = db.get_ports_by_trunk(trunks[i]['trunk_id']) + port1 = db.get_port_by_id(ports[0]) + port2 = db.get_port_by_id(ports[1]) + for s in range(0, len(switches)): + if switches[s]['switch_id'] == port1['switch_id']: + switch1 = s + if switches[s]['switch_id'] == port2['switch_id']: + switch2 = s + gim.draw_trunk(i, + switch[switch1].get_port_location(port1['number']), + switch[switch2].get_port_location(port2['number']), + gim.port_pallette['trunk']['trace']) + + # And the legend and label + gim.draw_legend(x_gap, curr_y) + gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2)) + + # All done - push the image file into the cache for this vlan + data['image']['png'] = cStringIO.StringIO() + gim.im.writePng(data['image']['png']) + data['image']['width'] = x + data['image']['height'] = y + return data + + # Implement an HTTP GET handler for the HTTPServer instance + def do_GET(self): + # Compare the URL path to any of the names we recognise and + # call the right generator function if we get a match + self.parsed_path = urlparse.urlparse(self.path) + for url in self.functionMap: + match = re.match(url['re'], self.parsed_path.path) + if match: + return url['fn'](self) + + # Fall-through for any files we don't recognise + self.send_response(404) + self.wfile.write('Content-type: text/plain\r\n') + self.end_headers() + self.wfile.write('404 Not Found') + self.wfile.write('%s' % self.parsed_path.path) + logging.error('File not supported - asked for %s', self.parsed_path.path) + return + + # Override the BaseHTTPRequestHandler log_message() method so we + # can log requests properly + def log_message(self, fmt, *args): + """Log an arbitrary message. """ + logging.info('%s %s', self.client_address[0], fmt%args) + + functionMap = ( + {'re': r'^/$', 'fn': send_index}, + {'re': r'^/style.css$', 'fn': send_style}, + {'re': r'^/images/vlan/(\d+).png$', 'fn': send_graphic} + ) diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100644 index 89e40d5..0000000 --- a/config/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# VLANd configuration module -# -# Set the defaults here, over-ride later -# diff --git a/config/config.py b/config/config.py deleted file mode 100644 index 67d3bfa..0000000 --- a/config/config.py +++ /dev/null @@ -1,275 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2015 Linaro Limited -# Author: Steve McIntyre -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# VLANd simple config parser -# - -import ConfigParser -import os, sys, re - -if __name__ == '__main__': - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import ConfigError - -def is_positive(text): - valid_true = ('1', 'y', 'yes', 't', 'true') - valid_false = ('0', 'n', 'no', 'f', 'false') - - if str(text) in valid_true or str(text).lower() in valid_true: - return True - elif str(text) in valid_false or str(text).lower() in valid_false: - return False - -def is_valid_logging_level(text): - valid = ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG') - if text in valid: - return True - return False - -class DaemonConfigClass: - """ Simple container for stuff to make for nicer syntax """ - - def __repr__(self): - return "" % (self.port) - -class DBConfigClass: - """ Simple container for stuff to make for nicer syntax """ - - def __repr__(self): - return "" % (self.server, self.port, self.dbname, self.username, self.password) - -class LoggingConfigClass: - """ Simple container for stuff to make for nicer syntax """ - - def __repr__(self): - return "" % (self.level, self.filename) - -class VisualisationConfigClass: - """ Simple container for stuff to make for nicer syntax """ - def __repr__(self): - return "" % (self.enabled, self.port) - -class SwitchConfigClass: - """ Simple container for stuff to make for nicer syntax """ - def __repr__(self): - return "" % (self.name, self.section, self.driver, self.username, self.password, self.enable_password) - -class VlanConfig: - """VLANd config class""" - def __init__(self, filenames): - - config = ConfigParser.RawConfigParser({ - # Set default values - 'dbname': None, - 'debug': False, - 'driver': None, - 'enable_password': None, - 'enabled': False, - 'name': None, - 'password': None, - 'port': None, - 'refresh': None, - 'server': None, - 'username': None, - }) - - config.read(filenames) - - # Parse out the config file - # Must have a [database] section - # May have a [vland] section - # May have a [logging] section - # May have multiple [switch 'foo'] sections - if not config.has_section('database'): - raise ConfigError('No database configuration section found') - - # No DB-specific defaults to set - self.database = DBConfigClass() - - # Set defaults logging details - self.logging = LoggingConfigClass() - self.logging.level = 'CRITICAL' - self.logging.filename = None - - # Set default port number and VLAN tag - self.vland = DaemonConfigClass() - self.vland.port = 3080 - self.vland.default_vlan_tag = 1 - - # Visualisation is disabled by default - self.visualisation = VisualisationConfigClass() - self.visualisation.port = 3081 - self.visualisation.enabled = False - - # No switch-specific defaults to set - self.switches = {} - - sw_regex = re.compile(r'(switch)\ (.*)', flags=re.I) - for section in config.sections(): - if section == 'database': - try: - self.database.server = config.get(section, 'server') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid database configuration (server)') - - try: - port = config.get(section, 'port') - if port is not None: - self.database.port = config.getint(section, 'port') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid database configuration (port)') - - try: - self.database.dbname = config.get(section, 'dbname') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid database configuration (dbname)') - - try: - self.database.username = config.get(section, 'username') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid database configuration (username)') - - try: - self.database.password = config.get(section, 'password') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid database configuration (password)') - - # Other database config options are optional, but these are not - if self.database.dbname is None or self.database.username is None: - raise ConfigError('Database configuration section incomplete') - - elif section == 'logging': - try: - self.logging.level = config.get(section, 'level') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid logging configuration (level)') - self.logging.level = self.logging.level.upper() - if not is_valid_logging_level(self.logging.level): - raise ConfigError('Invalid logging configuration (level)') - - try: - self.logging.filename = config.get(section, 'filename') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid logging configuration (filename)') - - elif section == 'vland': - try: - self.vland.port = config.getint(section, 'port') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid vland configuration (port)') - - try: - self.vland.default_vlan_tag = config.getint(section, 'default_vlan_tag') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid vland configuration (default_vlan_tag)') - - elif section == 'visualisation': - try: - self.visualisation.port = config.getint(section, 'port') - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid visualisation configuration (port)') - - try: - self.visualisation.enabled = config.get(section, 'enabled') - if not is_positive(self.visualisation.enabled): - self.visualisation.enabled = False - elif is_positive(self.visualisation.enabled): - self.visualisation.enabled = True - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid visualisation configuration (enabled)') - - try: - self.visualisation.refresh = config.get(section, 'refresh') - if self.visualisation.refresh is not None: - if not is_positive(self.visualisation.refresh): - self.visualisation.refresh = None - else: - self.visualisation.refresh = int(self.visualisation.refresh) - except ConfigParser.NoOptionError: - pass - except: - raise ConfigError('Invalid visualisation configuration (refresh)') - - else: - match = sw_regex.match(section) - if match: - # Constraint: switch names must be unique! See if - # there's already a switch with this name - name = config.get(section, 'name') - for key in self.switches.keys(): - if name == key: - raise ConfigError('Found switches with the same name (%s)' % name) - self.switches[name] = SwitchConfigClass() - self.switches[name].name = name - self.switches[name].section = section - self.switches[name].driver = config.get(section, 'driver') - self.switches[name].username = config.get(section, 'username') - self.switches[name].password = config.get(section, 'password') - self.switches[name].enable_password = config.get(section, 'enable_password') - self.switches[name].debug = config.get(section, 'debug') - if not is_positive(self.switches[name].debug): - self.switches[name].debug = False - elif is_positive(self.switches[name].debug): - self.switches[name].debug = True - else: - raise ConfigError('Invalid vland configuration (switch "%s", debug "%s"' % (name, self.switches[name].debug)) - else: - raise ConfigError('Unrecognised config section %s' % section) - - # Generic checking for config values - if self.visualisation.enabled: - if self.visualisation.port == self.vland.port: - raise ConfigError('Invalid configuration: VLANd and the visualisation service must use distinct port numbers') - - def __del__(self): - pass - -if __name__ == '__main__': - c = VlanConfig(filenames=('./vland.cfg',)) - print c.database - print c.vland - for switch in c.switches: - print c.switches[switch] - diff --git a/config/test-clashing-ports.cfg b/config/test-clashing-ports.cfg deleted file mode 100644 index 46b2f3e..0000000 --- a/config/test-clashing-ports.cfg +++ /dev/null @@ -1,33 +0,0 @@ -[database] -server = foo -port = 123 -dbname = vland -username = vland -password = vland - -[switch foo] -name = 10.172.2.51 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch bar] -name = 10.172.2.52 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch baz] -name = baz -driver = CiscoSX300 -username = cisco -password = cisco - -[vland] -port = 245 - -[visualisation] -enabled = yes -port = 245 diff --git a/config/test-invalid-DB.cfg b/config/test-invalid-DB.cfg deleted file mode 100644 index a80fcbc..0000000 --- a/config/test-invalid-DB.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[database] -port = bar -dbname = vland -username = vland - diff --git a/config/test-invalid-logging-level.cfg b/config/test-invalid-logging-level.cfg deleted file mode 100644 index d2de278..0000000 --- a/config/test-invalid-logging-level.cfg +++ /dev/null @@ -1,30 +0,0 @@ -[database] -server = foo -port = 123 -dbname = vland -username = vland -password = vland - -[switch foo] -name = 10.172.2.51 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch bar] -name = 10.172.2.52 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch baz] -name = baz -driver = CiscoSX300 -username = cisco -password = cisco - -[logging] -level = bibble - diff --git a/config/test-invalid-vland.cfg b/config/test-invalid-vland.cfg deleted file mode 100644 index 4478e7a..0000000 --- a/config/test-invalid-vland.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[database] -server = foo -port = 123 -dbname = vland -username = vland -password = vland - -[vland] -port = foo -default_vlan_tag = bar - diff --git a/config/test-known-good.cfg b/config/test-known-good.cfg deleted file mode 100644 index bd060db..0000000 --- a/config/test-known-good.cfg +++ /dev/null @@ -1,35 +0,0 @@ -# Example config for VLANd - -[database] -server=foo -port=123 -dbname = vland -username = user -password= pass - -[vland] -port = 9997 -default_vlan_tag = 42 - -[switch foo] -name = 10.172.2.51 -driver = CiscoSX300 -username = cisco_user -password = cisco_pass -enable_password = foobar - -[switch bar] -name = 10.172.2.52 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch baz] -name = baz -driver = CiscoSX300 -username = cisco -password = cisco - -[logging] - diff --git a/config/test-missing-db-username.cfg b/config/test-missing-db-username.cfg deleted file mode 100644 index 160934c..0000000 --- a/config/test-missing-db-username.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[database] -server = foo -port = 123 -dbname = vland -password = vland diff --git a/config/test-missing-dbname.cfg b/config/test-missing-dbname.cfg deleted file mode 100644 index ddc7168..0000000 --- a/config/test-missing-dbname.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[database] -server = foo -port = 123 -username = vland -password = vland - diff --git a/config/test-reused-switch-names.cfg b/config/test-reused-switch-names.cfg deleted file mode 100644 index 2d8485a..0000000 --- a/config/test-reused-switch-names.cfg +++ /dev/null @@ -1,26 +0,0 @@ -[database] -server = foo -port = 123 -dbname = vland -username = vland -password = vland - -[switch foo] -name = 10.172.2.51 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch bar] -name = 10.172.2.51 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch baz] -name = baz -driver = CiscoSX300 -username = cisco -password = cisco diff --git a/config/test-unknown-section.cfg b/config/test-unknown-section.cfg deleted file mode 100644 index 446e0a5..0000000 --- a/config/test-unknown-section.cfg +++ /dev/null @@ -1,29 +0,0 @@ -[database] -server = foo -port = 123 -dbname = vland -username = vland -password = vland - -[switch foo] -name = 10.172.2.51 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch bar] -name = 10.172.2.52 -driver = CiscoSX300 -username = cisco -password = cisco -#enable_password = - -[switch baz] -name = baz -driver = CiscoSX300 -username = cisco -password = cisco - -[bibble] -foo = 1 diff --git a/config/test.py b/config/test.py deleted file mode 100644 index 0ce11f6..0000000 --- a/config/test.py +++ /dev/null @@ -1,101 +0,0 @@ -import unittest, os, sys -vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) -sys.path.insert(0, vlandpath) -sys.path.insert(0, "%s/.." % vlandpath) - -os.chdir(vlandpath) - -from config.config import VlanConfig -from errors import ConfigError - -class MyTest(unittest.TestCase): - - # Check that we raise on missing database section - def test_missing_database_section(self): - with self.assertRaisesRegexp(ConfigError, 'No database'): - config = VlanConfig(filenames=("/dev/null",)) - del config - - # Check that we raise on broken database config values - def test_missing_database_config(self): - with self.assertRaisesRegexp(ConfigError, 'Invalid database'): - config = VlanConfig(filenames=("test-invalid-DB.cfg",)) - del config - - # Check that we raise on missing database config values - def test_missing_dbname(self): - with self.assertRaisesRegexp(ConfigError, 'Database.*incomplete'): - config = VlanConfig(filenames=("test-missing-dbname.cfg",)) - del config - - # Check that we raise on missing database config values - def test_missing_db_username(self): - with self.assertRaisesRegexp(ConfigError, 'Database.*incomplete'): - config = VlanConfig(filenames=("test-missing-db-username.cfg",)) - del config - - # Check that we raise on broken vland config values - def test_missing_vlan_config(self): - with self.assertRaisesRegexp(ConfigError, 'Invalid vland'): - config = VlanConfig(filenames=("test-invalid-vland.cfg",)) - del config - - # Check that we raise on broken logging level - def test_invalid_logging_level(self): - with self.assertRaisesRegexp(ConfigError, 'Invalid logging.*level'): - config = VlanConfig(filenames=("test-invalid-logging-level.cfg",)) - del config - - # Check that we raise when VLANd and visn are configured for the - # same port - def test_clashing_ports(self): - with self.assertRaisesRegexp(ConfigError, 'Invalid.*distinct port'): - config = VlanConfig(filenames=("test-clashing-ports.cfg",)) - del config - - # Check that we raise on repeated switch names - def test_missing_repeated_switch_names(self): - with self.assertRaisesRegexp(ConfigError, 'same name'): - config = VlanConfig(filenames=("test-reused-switch-names.cfg",)) - del config - - # Check that we raise on unknown config section - def test_unknown_config(self): - with self.assertRaisesRegexp(ConfigError, 'Unrecognised config'): - config = VlanConfig(filenames=("test-unknown-section.cfg",)) - del config - - # Check we get expected values on a known-good config - def test_known_good(self): - config = VlanConfig(filenames=("test-known-good.cfg",)) - self.assertEqual(config.database.server, 'foo') - self.assertEqual(config.database.port, 123) - self.assertEqual(config.database.dbname, 'vland') - self.assertEqual(config.database.username, 'user') - self.assertEqual(config.database.password, 'pass') - - self.assertEqual(config.vland.port, 9997) - self.assertEqual(config.vland.default_vlan_tag, 42) - - self.assertEqual(len(config.switches), 3) - self.assertEqual(config.switches["10.172.2.51"].name, '10.172.2.51') - self.assertEqual(config.switches["10.172.2.51"].driver, 'CiscoSX300') - self.assertEqual(config.switches["10.172.2.51"].username, 'cisco_user') - self.assertEqual(config.switches["10.172.2.51"].password, 'cisco_pass') - self.assertEqual(config.switches["10.172.2.51"].enable_password, 'foobar') - self.assertEqual(config.switches["10.172.2.51"].driver, 'CiscoSX300') - - self.assertEqual(config.switches["10.172.2.52"].name, '10.172.2.52') - self.assertEqual(config.switches["10.172.2.52"].driver, 'CiscoSX300') - self.assertEqual(config.switches["10.172.2.52"].username, 'cisco') - self.assertEqual(config.switches["10.172.2.52"].password, 'cisco') - self.assertEqual(config.switches["10.172.2.52"].enable_password, None) - self.assertEqual(config.switches["10.172.2.52"].driver, 'CiscoSX300') - - self.assertEqual(config.switches["baz"].name, 'baz') - self.assertEqual(config.switches["baz"].driver, 'CiscoSX300') - self.assertEqual(config.switches["baz"].username, 'cisco') - self.assertEqual(config.switches["baz"].password, 'cisco') - -if __name__ == '__main__': - unittest.main() diff --git a/db/__init__.py b/db/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/db/db.py b/db/db.py deleted file mode 100644 index 4fc9444..0000000 --- a/db/db.py +++ /dev/null @@ -1,830 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2018 Linaro Limited -# Authors: Dave Pigott , -# Steve McIntyre -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import psycopg2 -import psycopg2.extras -import datetime, os, sys -import logging - -TRUNK_ID_NONE = -1 - -# The schema version that this code expects. If it finds an older version (or -# no version!) at startup, it will auto-migrate to the latest version -# -# Version 0: Base, no version found -# -# Version 1: No changes, except adding the version and coping with upgrade -# -# Version 2: Add "lock_reason" field in the port table, and code to deal with -# it -DATABASE_SCHEMA_VERSION = 2 - -if __name__ == '__main__': - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import CriticalError, InputError, NotFoundError - -class VlanDB: - def __init__(self, db_name="vland", username="vland", readonly=True): - try: - self.connection = psycopg2.connect(database=db_name, user=username) - # Create first cursor for normal usage - returns tuples - self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor) - # Create second cursor for full-row lookups - returns a dict - # instead, much more useful in the admin interface - self.dictcursor = self.connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - if not readonly: - self._init_state() - except Exception as e: - logging.error("Failed to access database: %s", e) - raise - - def __del__(self): - self.cursor.close() - self.dictcursor.close() - self.connection.close() - - # Create the state table (if needed) and add its only record - # - # Use the stored record of the expected database schema to track what - # version the on-disk database is, and upgrade it to match the current code - # if necessary. - def _init_state(self): - found_db = False - current_db_version = 0 - try: - sql = "SELECT * FROM state" - self.cursor.execute(sql) - found_db = True - except psycopg2.ProgrammingError: - self.connection.commit() # state doesn't exist; clear error - sql = "CREATE TABLE state (last_modified TIMESTAMP, schema_version INTEGER)" - self.cursor.execute(sql) - # We've just created a version 1 database - current_db_version = 1 - - if found_db: - # Grab the version of the database we have - try: - sql = "SELECT schema_version FROM state" - self.cursor.execute(sql) - current_db_version = self.cursor.fetchone()[0] - # No version found ==> we have "version 0" - except psycopg2.ProgrammingError: - self.connection.commit() # state doesn't exist; clear error - current_db_version = 0 - - # Now delete the existing state record, we'll write a new one in a - # moment - self.cursor.execute('DELETE FROM state') - logging.info("Found a database, version %d", current_db_version) - - # Apply upgrades here! - if current_db_version < 1: - logging.info("Upgrading database to match schema version 1") - sql = "ALTER TABLE state ADD schema_version INTEGER" - self.cursor.execute(sql) - logging.info("Schema version 1 upgrade successful") - - if current_db_version < 2: - logging.info("Upgrading database to match schema version 2") - sql = "ALTER TABLE port ADD lock_reason VARCHAR(64)" - self.cursor.execute(sql) - logging.info("Schema version 2 upgrade successful") - - sql = "INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)" - data = (datetime.datetime.now(), DATABASE_SCHEMA_VERSION) - self.cursor.execute(sql, data) - self.connection.commit() - - # Create a new switch in the database. Switches are really simple - # devices - they're just containers for ports. - # - # Constraints: - # Switches must be uniquely named - def create_switch(self, name): - - switch_id = self.get_switch_id_by_name(name) - if switch_id is not None: - raise InputError("Switch name %s already exists" % name) - - try: - sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id" - data = (name, ) - self.cursor.execute(sql, data) - switch_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - - return switch_id - - # Create a new port in the database. Three of the fields are - # created with default values (is_locked, is_trunk, trunk_id) - # here, and should be updated separately if desired. For the - # current_vlan_id and base_vlan_id fields, *BE CAREFUL* that you - # have already looked up the correct VLAN_ID for each. This is - # *NOT* the same as the VLAN tag (likely to be 1). You Have Been - # Warned! - # - # Constraints: - # 1. The switch referred to must already exist - # 2. The VLANs mentioned here must already exist - # 3. (Switch/name) must be unique - # 4. (Switch/number) must be unique - def create_port(self, switch_id, name, number, current_vlan_id, base_vlan_id): - - switch = self.get_switch_by_id(switch_id) - if switch is None: - raise NotFoundError("Switch ID %d does not exist" % int(switch_id)) - - for vlan_id in (current_vlan_id, base_vlan_id): - vlan = self.get_vlan_by_id(vlan_id) - if vlan is None: - raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) - - port_id = self.get_port_by_switch_and_name(switch_id, name) - if port_id is not None: - raise InputError("Already have a port %s on switch ID %d" % (name, int(switch_id))) - - port_id = self.get_port_by_switch_and_number(switch_id, int(number)) - if port_id is not None: - raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id))) - - try: - sql = "INSERT INTO port (name, number, switch_id, is_locked, lock_reason, is_trunk, current_vlan_id, base_vlan_id, trunk_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING port_id" - data = (name, number, switch_id, - False, "", - False, - current_vlan_id, base_vlan_id, TRUNK_ID_NONE) - self.cursor.execute(sql, data) - port_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - - return port_id - - # Create a new vlan in the database. We locally add a creation - # timestamp, for debug purposes. If vlans seems to be sticking - # around, we'll be able to see when they were created. - # - # Constraints: - # Names and tags must be unique - # Tags must be in the range 1-4095 (802.1q spec) - # Names can be any free-form text, length 1-32 characters - def create_vlan(self, name, tag, is_base_vlan): - - if int(tag) < 1 or int(tag) > 4095: - raise InputError("VLAN tag %d is outside of the valid range (1-4095)" % int(tag)) - - if (len(name) < 1) or (len(name) > 32): - raise InputError("VLAN name %s is invalid (must be 1-32 chars)" % name) - - vlan_id = self.get_vlan_id_by_name(name) - if vlan_id is not None: - raise InputError("VLAN name %s is already in use" % name) - - vlan_id = self.get_vlan_id_by_tag(tag) - if vlan_id is not None: - raise InputError("VLAN tag %d is already in use" % int(tag)) - - try: - dt = datetime.datetime.now() - sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id" - data = (name, tag, is_base_vlan, dt) - self.cursor.execute(sql, data) - vlan_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - - return vlan_id - - # Create a new trunk in the database, linking two ports. Trunks - # are really simple objects for our use - they're just containers - # for 2 ports. - # - # Constraints: - # 1. Both ports listed must already exist. - # 2. Both ports must be in trunk mode. - # 3. Both must not be locked. - # 4. Both must not already be in a trunk. - def create_trunk(self, port_id1, port_id2): - - for port_id in (port_id1, port_id2): - port = self.get_port_by_id(int(port_id)) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - if not port['is_trunk']: - raise InputError("Port ID %d is not in trunk mode" % int(port_id)) - if port['is_locked']: - raise InputError("Port ID %d is locked" % int(port_id)) - if port['trunk_id'] != TRUNK_ID_NONE: - raise InputError("Port ID %d is already on trunk ID %d" % (int(port_id), int(port['trunk_id']))) - - try: - # Add the trunk itself - dt = datetime.datetime.now() - sql = "INSERT INTO trunk (creation_time) VALUES (%s) RETURNING trunk_id" - data = (dt, ) - self.cursor.execute(sql, data) - trunk_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - # And update the ports - for port_id in (port_id1, port_id2): - self._set_port_trunk(port_id, trunk_id) - except: - self.delete_trunk(trunk_id) - raise - - return trunk_id - - # Internal helper function - def _delete_row(self, table, field, value): - try: - sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s') - data = (value,) - self.cursor.execute(sql, data) - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - - # Delete the specified switch - # - # Constraints: - # 1. The switch must exist - # 2. The switch may not be referenced by any ports - - # delete them first! - def delete_switch(self, switch_id): - switch = self.get_switch_by_id(switch_id) - if switch is None: - raise NotFoundError("Switch ID %d does not exist" % int(switch_id)) - ports = self.get_ports_by_switch(switch_id) - if ports is not None: - raise InputError("Cannot delete switch ID %d when it still has %d ports" % - (int(switch_id), len(ports))) - self._delete_row("switch", "switch_id", switch_id) - return switch_id - - # Delete the specified port - # - # Constraints: - # 1. The port must exist - # 2. The port must not be locked - def delete_port(self, port_id): - port = self.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - if port['is_locked']: - raise InputError("Cannot delete port ID %d as it is locked" % int(port_id)) - self._delete_row("port", "port_id", port_id) - return port_id - - # Delete the specified VLAN - # - # Constraints: - # 1. The VLAN must exist - # 2. The VLAN may not contain any ports - move or delete them first! - def delete_vlan(self, vlan_id): - vlan = self.get_vlan_by_id(vlan_id) - if vlan is None: - raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) - ports = self.get_ports_by_current_vlan(vlan_id) - if ports is not None: - raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % - (int(vlan_id), len(ports))) - ports = self.get_ports_by_base_vlan(vlan_id) - if ports is not None: - raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % - (int(vlan_id), len(ports))) - self._delete_row("vlan", "vlan_id", vlan_id) - return vlan_id - - # Delete the specified trunk - # - # Constraints: - # 1. The trunk must exist - # - # Any ports attached will be detached (i.e. moved to trunk TRUNK_ID_NONE) - def delete_trunk(self, trunk_id): - trunk = self.get_trunk_by_id(trunk_id) - if trunk is None: - raise NotFoundError("Trunk ID %d does not exist" % int(trunk_id)) - ports = self.get_ports_by_trunk(trunk_id) - for port_id in ports: - self._set_port_trunk(port_id, TRUNK_ID_NONE) - self._delete_row("trunk", "trunk_id", trunk_id) - return trunk_id - - # Find the lowest unused VLAN tag and return it - # - # Constraints: - # None - def find_lowest_unused_vlan_tag(self): - sql = "SELECT tag FROM vlan ORDER BY tag ASC" - self.cursor.execute(sql,) - - # Walk through the list, looking for gaps - last = 1 - result = None - - for record in self.cursor: - if (record[0] - last) > 1: - result = last + 1 - break - last = record[0] - - if result is None: - result = last + 1 - - if result > 4093: - raise CriticalError("Can't find any VLAN tags remaining for allocation!") - - return result - - # Grab one column from one row of a query on one column; useful as - # a quick wrapper - def _get_element(self, select_field, table, compare_field, value): - - if value is None: - raise ValueError("Asked to look up using None as a key in %s" % compare_field) - - # We really want to use psycopg's type handling deal with the - # (potentially) user-supplied data in the value field, so we - # have to pass (sql,data) through to cursor.execute. However, - # we can't have psycopg do all the argument substitution here - # as it will quote all the params like the table name. That - # doesn't work. So, we substitute a "%s" for "%s" here so we - # keep it after python's own string substitution. - sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s") - - # Now, the next icky thing: we need to make sure that we're - # passing a dict so that psycopg2 can pick it apart properly - # for its own substitution code. We force this with the - # trailing comma here - data = (value, ) - self.cursor.execute(sql, data) - - if self.cursor.rowcount > 0: - return self.cursor.fetchone()[0] - else: - return None - - # Grab one column from one row of a query on 2 columns; useful as - # a quick wrapper - def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2): - - if value1 is None: - raise ValueError("Asked to look up using None as a key in %s" % compare_field1) - if value2 is None: - raise ValueError("Asked to look up using None as a key in %s" % compare_field2) - - # We really want to use psycopg's type handling deal with the - # (potentially) user-supplied data in the value field, so we - # have to pass (sql,data) through to cursor.execute. However, - # we can't have psycopg do all the argument substitution here - # as it will quote all the params like the table name. That - # doesn't work. So, we substitute a "%s" for "%s" here so we - # keep it after python's own string substitution. - sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s") - - data = (value1, value2) - self.cursor.execute(sql, data) - - if self.cursor.rowcount > 0: - return self.cursor.fetchone()[0] - else: - return None - - # Grab one column from multiple rows of a query; useful as a quick - # wrapper - def _get_multi_elements(self, select_field, table, compare_field, value, sort_field): - - if value is None: - raise ValueError("Asked to look up using None as a key in %s" % compare_field) - - # We really want to use psycopg's type handling deal with the - # (potentially) user-supplied data in the value field, so we - # have to pass (sql,data) through to cursor.execute. However, - # we can't have psycopg do all the argument substitution here - # as it will quote all the params like the table name. That - # doesn't work. So, we substitute a "%s" for "%s" here so we - # keep it after python's own string substitution. - sql = "SELECT %s FROM %s WHERE %s = %s ORDER BY %s ASC" % (select_field, table, compare_field, "%s", sort_field) - - # Now, the next icky thing: we need to make sure that we're - # passing a dict so that psycopg2 can pick it apart properly - # for its own substitution code. We force this with the - # trailing comma here - data = (value, ) - self.cursor.execute(sql, data) - - if self.cursor.rowcount > 0: - results = [] - for record in self.cursor: - results.append(record[0]) - return results - else: - return None - - # Grab one column from multiple rows of a 2-part query; useful as - # a wrapper - def _get_multi_elements2(self, select_field, table, compare_field1, value1, compare_field2, value2, sort_field): - - if value1 is None: - raise ValueError("Asked to look up using None as a key in %s" % compare_field1) - if value2 is None: - raise ValueError("Asked to look up using None as a key in %s" % compare_field2) - - # We really want to use psycopg's type handling deal with the - # (potentially) user-supplied data in the value field, so we - # have to pass (sql,data) through to cursor.execute. However, - # we can't have psycopg do all the argument substitution here - # as it will quote all the params like the table name. That - # doesn't work. So, we substitute a "%s" for "%s" here so we - # keep it after python's own string substitution. - sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s ORDER by %s ASC" % (select_field, table, compare_field1, "%s", compare_field2, "%s", sort_field) - - data = (value1, value2) - self.cursor.execute(sql, data) - - if self.cursor.rowcount > 0: - results = [] - for record in self.cursor: - results.append(record[0]) - return results - else: - return None - - # Simple lookup: look up a switch by ID, and return all the - # details of that switch. - # - # Returns None on failure. - def get_switch_by_id(self, switch_id): - return self._get_row("switch", "switch_id", int(switch_id)) - - # Simple lookup: look up a switch by name, and return the ID of - # that switch. - # - # Returns None on failure. - def get_switch_id_by_name(self, name): - return self._get_element("switch_id", "switch", "name", name) - - # Simple lookup: look up a switch by ID, and return the name of - # that switch. - # - # Returns None on failure. - def get_switch_name_by_id(self, switch_id): - return self._get_element("name", "switch", "switch_id", int(switch_id)) - - # Simple lookup: look up a port by ID, and return all the details - # of that port. - # - # Returns None on failure. - def get_port_by_id(self, port_id): - return self._get_row("port", "port_id", int(port_id)) - - # Simple lookup: look up a switch by ID, and return the IDs of all - # the ports on that switch. - # - # Returns None on failure. - def get_ports_by_switch(self, switch_id): - return self._get_multi_elements("port_id", "port", "switch_id", int(switch_id), "port_id") - - # More complex lookup: look up all the trunk ports on a switch by - # ID - # - # Returns None on failure. - def get_trunk_port_names_by_switch(self, switch_id): - return self._get_multi_elements2("name", "port", "switch_id", int(switch_id), "is_trunk", True, "port_id") - - # Simple lookup: look up a port by its name and its parent switch - # by ID, and return the ID of the port. - # - # Returns None on failure. - def get_port_by_switch_and_name(self, switch_id, name): - return self._get_element2("port_id", "port", "switch_id", int(switch_id), "name", name) - - # Simple lookup: look up a port by its external name and its - # parent switch by ID, and return the ID of the port. - # - # Returns None on failure. - def get_port_by_switch_and_number(self, switch_id, number): - return self._get_element2("port_id", "port", "switch_id", int(switch_id), "number", int(number)) - - # Simple lookup: look up a port by ID, and return the current VLAN - # id of that port. - # - # Returns None on failure. - def get_current_vlan_id_by_port(self, port_id): - return self._get_element("current_vlan_id", "port", "port_id", int(port_id)) - - # Simple lookup: look up a port by ID, and return the mode of that port. - # - # Returns None on failure. - def get_port_mode(self, port_id): - is_trunk = self._get_element("is_trunk", "port", "port_id", int(port_id)) - if is_trunk is not None: - if is_trunk: - return "trunk" - else: - return "access" - return None - - # Simple lookup: look up a port by ID, and return the base VLAN - # id of that port. - # - # Returns None on failure. - def get_base_vlan_id_by_port(self, port_id): - return self._get_element("base_vlan_id", "port", "port_id", int(port_id)) - - # Simple lookup: look up a current VLAN by ID, and return the IDs - # of all the ports on that VLAN. - # - # Returns None on failure. - def get_ports_by_current_vlan(self, vlan_id): - return self._get_multi_elements("port_id", "port", "current_vlan_id", int(vlan_id), "port_id") - - # Simple lookup: look up a base VLAN by ID, and return the IDs - # of all the ports on that VLAN. - # - # Returns None on failure. - def get_ports_by_base_vlan(self, vlan_id): - return self._get_multi_elements("port_id", "port", "base_vlan_id", int(vlan_id), "port_id") - - # Simple lookup: look up a trunk by ID, and return the IDs of the - # ports on both ends of that trunk. - # - # Returns None on failure. - def get_ports_by_trunk(self, trunk_id): - return self._get_multi_elements("port_id", "port", "trunk_id", int(trunk_id), "port_id") - - # Simple lookup: look up a VLAN by ID, and return all the details - # of that VLAN. - # - # Returns None on failure. - def get_vlan_by_id(self, vlan_id): - return self._get_row("vlan", "vlan_id", int(vlan_id)) - - # Simple lookup: look up a VLAN by name, and return the ID of that - # VLAN. - # - # Returns None on failure. - def get_vlan_id_by_name(self, name): - return self._get_element("vlan_id", "vlan", "name", name) - - # Simple lookup: look up a VLAN by tag, and return the ID of that - # VLAN. - # - # Returns None on failure. - def get_vlan_id_by_tag(self, tag): - return self._get_element("vlan_id", "vlan", "tag", int(tag)) - - # Simple lookup: look up a VLAN by ID, and return the name of that - # VLAN. - # - # Returns None on failure. - def get_vlan_name_by_id(self, vlan_id): - return self._get_element("name", "vlan", "vlan_id", int(vlan_id)) - - # Simple lookup: look up a VLAN by ID, and return the tag of that - # VLAN. - # - # Returns None on failure. - def get_vlan_tag_by_id(self, vlan_id): - return self._get_element("tag", "vlan", "vlan_id", int(vlan_id)) - - # Simple lookup: look up a trunk by ID, and return all the details - # of that trunk. - # - # Returns None on failure. - def get_trunk_by_id(self, trunk_id): - return self._get_row("trunk", "trunk_id", int(trunk_id)) - - # Get the last-modified time for the database - def get_last_modified_time(self): - sql = "SELECT last_modified FROM state" - self.cursor.execute(sql) - return self.cursor.fetchone()[0] - - # Grab one row of a query on one column; useful as a quick wrapper - def _get_row(self, table, field, value): - - # We really want to use psycopg's type handling deal with the - # (potentially) user-supplied data in the value field, so we - # have to pass (sql,data) through to cursor.execute. However, - # we can't have psycopg do all the argument substitution here - # as it will quote all the params like the table name. That - # doesn't work. So, we substitute a "%s" for "%s" here so we - # keep it after python's own string substitution. - sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s") - - # Now, the next icky thing: we need to make sure that we're - # passing a dict so that psycopg2 can pick it apart properly - # for its own substitution code. We force this with the - # trailing comma here - data = (value, ) - self.dictcursor.execute(sql, data) - return self.dictcursor.fetchone() - - # (Un)Lock a port in the database. This can only be done through - # the admin interface, and will stop API users from modifying - # settings on the port. Use this to lock down ports that are used - # for PDUs and other core infrastructure - def set_port_is_locked(self, port_id, is_locked, lock_reason=""): - port = self.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - try: - sql = "UPDATE port SET is_locked=%s, lock_reason=%s WHERE port_id=%s RETURNING port_id" - data = (is_locked, lock_reason, port_id) - self.cursor.execute(sql, data) - port_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise InputError("lock failed on Port ID %d" % int(port_id)) - return port_id - - # Set the mode of a port in the database. Valid values for mode - # are "trunk" and "access" - def set_port_mode(self, port_id, mode): - port = self.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - if mode == "access": - is_trunk = False - elif mode == "trunk": - is_trunk = True - else: - raise InputError("Port mode %s is not valid" % mode) - try: - sql = "UPDATE port SET is_trunk=%s WHERE port_id=%s RETURNING port_id" - data = (is_trunk, port_id) - self.cursor.execute(sql, data) - port_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - return port_id - - # Set the current vlan of a port in the database. The VLAN is - # passed by ID. - # - # Constraints: - # 1. The port must already exist - # 2. The port must not be a trunk port - # 3. The port must not be locked - # 1. The VLAN must already exist in the database - def set_current_vlan(self, port_id, vlan_id): - port = self.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - - if port['is_trunk'] or port['is_locked']: - raise CriticalError("The port is locked") - - vlan = self.get_vlan_by_id(vlan_id) - if vlan is None: - raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) - - try: - sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s RETURNING port_id" - data = (vlan_id, port_id) - self.cursor.execute(sql, data) - port_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - return port_id - - # Set the base vlan of a port in the database. The VLAN is - # passed by ID. - # - # Constraints: - # 1. The port must already exist - # 2. The port must not be a trunk port - # 3. The port must not be locked - # 4. The VLAN must already exist in the database - def set_base_vlan(self, port_id, vlan_id): - port = self.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - - if port['is_trunk'] or port['is_locked']: - raise CriticalError("The port is locked") - - vlan = self.get_vlan_by_id(vlan_id) - if vlan is None: - raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id)) - if not vlan['is_base_vlan']: - raise InputError("VLAN ID %d is not a base VLAN" % int(vlan_id)) - - try: - sql = "UPDATE port SET base_vlan_id=%s WHERE port_id=%s RETURNING port_id" - data = (vlan_id, port_id) - self.cursor.execute(sql, data) - port_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - return port_id - - # Internal function: Attach a port to a trunk in the database. - # - # Constraints: - # 1. The port must already exist - # 2. The port must not be locked - def _set_port_trunk(self, port_id, trunk_id): - port = self.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % int(port_id)) - if port['is_locked']: - raise CriticalError("The port is locked") - try: - sql = "UPDATE port SET trunk_id=%s WHERE port_id=%s RETURNING port_id" - data = (int(trunk_id), int(port_id)) - self.cursor.execute(sql, data) - port_id = self.cursor.fetchone()[0] - self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),)) - self.connection.commit() - except: - self.connection.rollback() - raise - return port_id - - # Trivial helper function to return all the rows in a given table - def _dump_table(self, table, order): - result = [] - self.dictcursor.execute("SELECT * FROM %s ORDER by %s ASC" % (table, order)) - record = self.dictcursor.fetchone() - while record != None: - result.append(record) - record = self.dictcursor.fetchone() - return result - - def all_switches(self): - return self._dump_table("switch", "switch_id") - - def all_ports(self): - return self._dump_table("port", "port_id") - - def all_vlans(self): - return self._dump_table("vlan", "vlan_id") - - def all_trunks(self): - return self._dump_table("trunk", "trunk_id") - -if __name__ == '__main__': - db = VlanDB() - s = db.all_switches() - print 'The DB knows about %d switch(es)' % len(s) - print s - p = db.all_ports() - print 'The DB knows about %d port(s)' % len(p) - print p - v = db.all_vlans() - print 'The DB knows about %d vlan(s)' % len(v) - print v - t = db.all_trunks() - print 'The DB knows about %d trunks(s)' % len(t) - print t - - print 'First free VLAN tag is %d' % db.find_lowest_unused_vlan_tag() diff --git a/db/init.doc b/db/init.doc deleted file mode 100644 index 91a3841..0000000 --- a/db/init.doc +++ /dev/null @@ -1,5 +0,0 @@ -create linux user vland with password vland -create database user vland with password vland -create tables -create vlan1 - diff --git a/db/setup_db.py b/db/setup_db.py deleted file mode 100755 index 99cfaf4..0000000 --- a/db/setup_db.py +++ /dev/null @@ -1,56 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2018 Linaro Limited -# Authors: Dave Pigott , -# Steve McIntyre -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -# First of all, create the vland user -# Next - create the vland database - -# Create the switch, port, vlan, trunk and state tables - -import datetime -from psycopg2 import connect - -DATABASE_SCHEMA_VERSION = 1 - -conn = connect(database="postgres", user="postgres", password="postgres") - -cur = conn.cursor() -cur.execute("CREATE USER vland WITH SUPERUSER") -cur.execute("CREATE DATABASE vland WITH OWNER = vland PASSWORD 'vland'") -conn.close() - -conn = connect(database="vland", user="vland", password="vland") -cur = conn.cursor() - -cur.execute("CREATE TABLE switch (switch_id SERIAL, name VARCHAR(64))") -cur.execute("CREATE TABLE port (port_id SERIAL, name VARCHAR(64)," - "switch_id INTEGER, is_locked BOOLEAN," - "is_trunk BOOLEAN, base_vlan_id INTEGER," - "current_vlan_id INTEGER, number INTEGER, trunk_id INTEGER)") -cur.execute("CREATE TABLE vlan (vlan_id SERIAL, name VARCHAR(32)," - "tag INTEGER, is_base_vlan BOOLEAN, creation_time TIMESTAMP)") -cur.execute("CREATE TABLE trunk (trunk_id SERIAL," - "creation_time TIMESTAMP)") -cur.execute("CREATE TABLE state (last_modified TIMESTAMP, schema_version INTEGER)") -cur.execute("INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)" % (datetime.datetime.now(), DATABASE_SCHEMA_VERSION)) -cur.execute("COMMIT;") - -# Do not make any more changes here - the database code will cope with upgrades -# from this V1 database as they're needed. diff --git a/drivers/CiscoCatalyst.py b/drivers/CiscoCatalyst.py deleted file mode 100644 index 16f47db..0000000 --- a/drivers/CiscoCatalyst.py +++ /dev/null @@ -1,721 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import logging -import sys -import re -import pexpect - -if __name__ == '__main__': - import os - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError, PExpectError -from drivers.common import SwitchDriver, SwitchErrors - -class CiscoCatalyst(SwitchDriver): - - connection = None - _username = None - _password = None - _enable_password = None - - _capabilities = [ - 'TrunkWildCardVlans' # Trunk ports are on all VLANs by - # default, so we shouldn't need to - # bugger with them - ] - - # Regexp of expected hardware information - fail if we don't see - # this - _expected_descr_re = re.compile(r'WS-C\S+-\d+P') - - def __init__(self, switch_hostname, switch_telnetport=23, debug = False): - SwitchDriver.__init__(self, switch_hostname, debug) - self._systemdata = [] - self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) - self.errors = SwitchErrors() - - ################################ - ### Switch-level API functions - ################################ - - # Save the current running config into flash - we want config to - # remain across reboots - def switch_save_running_config(self): - try: - self._cli("copy running-config startup-config") - self.connection.expect("startup-config") - self._cli("startup-config") - self.connection.expect("OK") - except (PExpectError, pexpect.EOF): - # recurse on error - self._switch_connect() - self.switch_save_running_config() - - # Restart the switch - we need to reload config to do a - # roll-back. Do NOT save running-config first if the switch asks - - # we're trying to dump recent changes, not save them. - # - # This will also implicitly cause a connection to be closed - def switch_restart(self): - self._cli("reload") - index = self.connection.expect(['has been modified', 'Proceed']) - if index == 0: - self._cli("n") # No, don't save - self.connection.expect("Proceed") - - # Fall through - self._cli("y") # Yes, continue to reset - self.connection.close(True) - - # List the capabilities of the switch (and driver) - some things - # make no sense to abstract. Returns a dict of strings, each one - # describing an extra feature that that higher levels may care - # about - def switch_get_capabilities(self): - return self._capabilities - - ################################ - ### VLAN API functions - ################################ - - # Create a VLAN with the specified tag - def vlan_create(self, tag): - logging.debug("Creating VLAN %d", tag) - try: - self._configure() - self._cli("vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - return - raise IOError("Failed to create VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_create(tag) - - # Destroy a VLAN with the specified tag - def vlan_destroy(self, tag): - logging.debug("Destroying VLAN %d", tag) - - try: - self._configure() - self._cli("no vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - raise IOError("Failed to destroy VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_destroy(tag) - - # Set the name of a VLAN - def vlan_set_name(self, tag, name): - logging.debug("Setting name of VLAN %d to %s", tag, name) - - try: - self._configure() - self._cli("vlan %d" % tag) - self._cli("name %s" % name) - self._end_configure() - - # Validate it happened - read_name = self.vlan_get_name(tag) - if read_name != name: - raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" - % (tag, read_name, name)) - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_set_name(tag, name) - - # Get a list of the VLAN tags currently registered on the switch - def vlan_get_list(self): - logging.debug("Grabbing list of VLANs") - - try: - vlans = [] - - regex = re.compile(r'^ *(\d+).*(active)') - - self._cli("show vlan brief") - for line in self._read_long_output("show vlan brief"): - match = regex.match(line) - if match: - vlans.append(int(match.group(1))) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_list() - - # For a given VLAN tag, ask the switch what the associated name is - def vlan_get_name(self, tag): - - try: - logging.debug("Grabbing the name of VLAN %d", tag) - name = None - regex = re.compile(r'^ *\d+\s+(\S+).*(active)') - self._cli("show vlan id %d" % tag) - for line in self._read_long_output("show vlan id"): - match = regex.match(line) - if match: - name = match.group(1) - name.strip() - return name - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_name(tag) - - ################################ - ### Port API functions - ################################ - - # Set the mode of a port: access or trunk - def port_set_mode(self, port, mode): - logging.debug("Setting port %s to %s", port, mode) - if not self._is_port_mode_valid(mode): - raise InputError("Port mode %s is not allowed" % mode) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - - try: - self._configure() - self._cli("interface %s" % port) - if mode == "trunk": - self._cli("switchport trunk encapsulation dot1q") - self._cli("switchport trunk native vlan 1") - self._cli("switchport mode %s" % mode) - self._end_configure() - - # Validate it happened - read_mode = self._port_get_mode(port) - - if read_mode != mode: - raise IOError("Failed to set mode for port %s" % port) - - # And cache the result - self._port_modes[port] = mode - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_mode(port, mode) - - # Set an access port to be in a specified VLAN (tag) - def port_set_access_vlan(self, port, tag): - logging.debug("Setting access port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport access vlan %d" % tag) - self._cli("no shutdown") - self._end_configure() - - # Finally, validate things worked - read_vlan = int(self.port_get_access_vlan(port)) - if read_vlan != tag: - raise IOError("Failed to move access port %d to VLAN %d - got VLAN %d instead" - % (port, tag, read_vlan)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_access_vlan(port, tag) - - # Add a trunk port to a specified VLAN (tag) - def port_add_trunk_to_vlan(self, port, tag): - logging.debug("Adding trunk port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport trunk allowed vlan add %d" % tag) - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag or vlan == "ALL": - return - raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_add_trunk_to_vlan(port, tag) - - # Remove a trunk port from a specified VLAN (tag) - def port_remove_trunk_from_vlan(self, port, tag): - logging.debug("Removing trunk port %s from VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport trunk allowed vlan remove %d" % tag) - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag: - raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_remove_trunk_from_vlan(port, tag) - - # Get the configured VLAN tag for an access port (tag) - def port_get_access_vlan(self, port): - logging.debug("Getting VLAN for access port %s", port) - vlan = 1 - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - regex = re.compile(r'Access Mode VLAN: (\d+)') - - try: - self._cli("show interfaces %s switchport" % port) - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - vlan = match.group(1) - return int(vlan) - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_access_vlan(port) - - # Get the list of configured VLAN tags for a trunk port - def port_get_trunk_vlan_list(self, port): - logging.debug("Getting VLANs for trunk port %s", port) - vlans = [ ] - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - regex_start = re.compile('Trunking VLANs Enabled: (.*)') - regex_continue = re.compile(r'\s*(\d.*)') - - try: - self._cli("show interfaces %s switchport" % port) - - # Horrible parsing work - VLAN list may extend over several lines - in_match = False - vlan_text = '' - - for line in self._read_long_output("show interfaces switchport"): - if in_match: - match = regex_continue.match(line) - if match: - vlan_text += match.group(1) - else: - in_match = False - else: - match = regex_start.match(line) - if match: - vlan_text += match.group(1) - in_match = True - - vlans = self._parse_vlan_list(vlan_text) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_trunk_vlan_list(port) - - ################################ - ### Internal functions - ################################ - - # Connect to the switch and log in - def _switch_connect(self): - - if not self.connection is None: - self.connection.close(True) - self.connection = None - - logging.debug("Connecting to Switch with: %s", self.exec_string) - self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) - self._login() - - # Avoid paged output - self._cli("terminal length 0") - - # And grab details about the switch. in case we need it - self._get_systemdata() - - # And also validate them - make sure we're driving a switch of - # the correct model! Also store the serial number - descr_regex = re.compile(r'^cisco\s+(\S+)') - sn_regex = re.compile(r'System serial number\s+:\s+(\S+)') - descr = "" - - for line in self._systemdata: - match = descr_regex.match(line) - if match: - descr = match.group(1) - match = sn_regex.match(line) - if match: - self.serial_number = match.group(1) - - logging.debug("serial number is %s", self.serial_number) - logging.debug("system description is %s", descr) - - if not self._expected_descr_re.match(descr): - raise IOError("Switch %s not recognised by this driver: abort" % descr) - - # Now build a list of our ports, for later sanity checking - self._ports = self._get_port_names() - if len(self._ports) < 4: - raise IOError("Not enough ports detected - problem!") - - def _login(self): - logging.debug("attempting login with username %s, password %s", self._username, self._password) - self.connection.expect('User Access Verification') - if self._username is not None: - self.connection.expect("User Name:") - self._cli("%s" % self._username) - if self._password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._password, False) - while True: - index = self.connection.expect(['User Name:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) - if index != 4: # Any other means: failed to log in! - logging.error("Login failure: index %d\n", index) - logging.error("Login failure: %s\n", self.connection.match.before) - raise IOError - - # else - self._prompt_name = re.escape(self.connection.match.group(1).strip()) - if self.connection.match.group(2) == ">": - # Need to enter "enable" mode too - self._cli("enable") - if self._enable_password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._enable_password, False) - index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) - if index != 3: # Any other means: failed to log in! - logging.error("Enable password failure: %s\n", self.connection.match) - raise IOError - return 0 - - def _logout(self): - logging.debug("Logging out") - self._cli("exit", False) - self.connection.close(True) - - def _configure(self): - self._cli("configure terminal") - - def _end_configure(self): - self._cli("end") - - def _read_long_output(self, text): - prompt = self._prompt_name + '#' - try: - self.connection.expect(prompt) - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_in(text) - raise PExpectError("_read_long_output failed") - except: - logging.error("prompt is \"%s\"", prompt) - raise - - longbuf = [] - for line in self.connection.before.split('\r\n'): - longbuf.append(line.strip()) - return longbuf - - def _get_port_names(self): - logging.debug("Grabbing list of ports") - interfaces = [] - - # Use "connect" to only identify lines in the output that - # match interfaces - it will match lines with "connected" or - # "notconnect". - regex = re.compile(r'^\s*([a-zA-Z0-9_/]*).*(connect)(.*)') - # Deliberately drop things marked as "routed", i.e. the - # management port - regex2 = re.compile('.*routed.*') - - try: - self._cli("show interfaces status") - for line in self._read_long_output("show interfaces status"): - match = regex.match(line) - if match: - interface = match.group(1) - junk = match.group(3) - match2 = regex2.match(junk) - if not match2: - interfaces.append(interface) - self._port_numbers[interface] = len(interfaces) - logging.debug(" found %d ports on the switch", len(interfaces)) - return interfaces - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_port_names() - - # Get the mode of a port: access or trunk - def _port_get_mode(self, port): - logging.debug("Getting mode of port %s", port) - mode = '' - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - regex = re.compile('Administrative Mode: (.*)') - - try: - self._cli("show interfaces %s switchport" % port) - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - mode = match.group(1) - if mode == 'static access': - return 'access' - if mode == 'trunk': - return 'trunk' - # Needs special handling - it's the default port - # mode on these switches, and it doesn't - # interoperate with some other vendors. Sigh. - if mode == 'dynamic auto': - return 'dynamic' - return mode - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_mode(port) - - def _show_config(self): - logging.debug("Grabbing config") - try: - self._cli("show running-config") - return self._read_long_output("show running-config") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_config() - - def _show_clock(self): - logging.debug("Grabbing time") - try: - self._cli("show clock") - return self._read_long_output("show clock") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_clock() - - def _get_systemdata(self): - logging.debug("Grabbing system sw and hw versions") - - try: - self._systemdata = [] - self._cli("show version") - for line in self._read_long_output("show version"): - self._systemdata.append(line) - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_systemdata() - - def _parse_vlan_list(self, inputdata): - vlans = [] - - if inputdata == "ALL": - return ["ALL"] - elif inputdata == "NONE": - return [] - else: - # Parse the complex list - groups = inputdata.split(',') - for group in groups: - subgroups = group.split('-') - if len(subgroups) == 1: - vlans.append(int(subgroups[0])) - elif len(subgroups) == 2: - for i in range (int(subgroups[0]), int(subgroups[1]) + 1): - vlans.append(i) - else: - logging.debug("Can't parse group \"" + group + "\"") - - return vlans - - # Wrapper around connection.send - by default, expect() the same - # text we've sent, to remove it from the output from the - # switch. For the few cases where we don't need that, override - # this using echo=False. - # Horrible, but seems to work. - def _cli(self, text, echo=True): - self.connection.send(text + '\r') - if echo: - try: - self.connection.expect(text) - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_out(text) - raise PExpectError("_cli failed on %s" % text) - except: - logging.error("Unexpected error: %s", sys.exc_info()[0]) - raise - -if __name__ == "__main__": - - # Simple test harness - exercise the main working functions above to verify - # they work. This does *NOT* test really disruptive things like "save - # running-config" and "reload" - test those by hand. - - import optparse - - switch = 'vlandswitch01' - parser = optparse.OptionParser() - parser.add_option("--switch", - dest = "switch", - action = "store", - nargs = 1, - type = "string", - help = "specify switch to connect to for testing", - metavar = "") - (opts, args) = parser.parse_args() - if opts.switch: - switch = opts.switch - - logging.basicConfig(level = logging.DEBUG, - format = '%(asctime)s %(levelname)-8s %(message)s') - p = CiscoCatalyst(switch, 23, debug=True) - p.switch_connect(None, 'lngvirtual', 'lngenable') - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.vlan_get_name(2) - print "VLAN 2 is named \"%s\"" % buf - - print "Create VLAN 3" - p.vlan_create(3) - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "Set name of VLAN 3 to test333" - p.vlan_set_name(3, "test333") - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - print "Destroy VLAN 3" - p.vlan_destroy(3) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.port_get_mode("Gi1/0/10") - print "Port Gi1/0/10 is in %s mode" % buf - - buf = p.port_get_mode("Gi1/0/11") - print "Port Gi1/0/11 is in %s mode" % buf - - # Test access stuff - print "Set Gi1/0/9 to access mode" - p.port_set_mode("Gi1/0/9", "access") - - print "Move Gi1/0/9 to VLAN 4" - p.port_set_access_vlan("Gi1/0/9", 4) - - buf = p.port_get_access_vlan("Gi1/0/9") - print "Read from switch: Gi1/0/9 is on VLAN %s" % buf - - print "Move Gi1/0/9 back to VLAN 1" - p.port_set_access_vlan("Gi1/0/9", 1) - - # Test access stuff - print "Set Gi1/0/9 to trunk mode" - p.port_set_mode("Gi1/0/9", "trunk") - print "Read from switch: which VLANs is Gi1/0/9 on?" - buf = p.port_get_trunk_vlan_list("Gi1/0/9") - p.dump_list(buf) - print "Add Gi1/0/9 to VLAN 2" - p.port_add_trunk_to_vlan("Gi1/0/9", 2) - print "Add Gi1/0/9 to VLAN 3" - p.port_add_trunk_to_vlan("Gi1/0/9", 3) - print "Add Gi1/0/9 to VLAN 4" - p.port_add_trunk_to_vlan("Gi1/0/9", 4) - print "Read from switch: which VLANs is Gi1/0/9 on?" - buf = p.port_get_trunk_vlan_list("Gi1/0/9") - p.dump_list(buf) - - p.port_remove_trunk_from_vlan("Gi1/0/9", 3) - p.port_remove_trunk_from_vlan("Gi1/0/9", 3) - p.port_remove_trunk_from_vlan("Gi1/0/9", 4) - print "Read from switch: which VLANs is Gi1/0/9 on?" - buf = p.port_get_trunk_vlan_list("Gi1/0/9") - p.dump_list(buf) - -# print 'Restarting switch, to explicitly reset config' -# p.switch_restart() - -# p.switch_save_running_config() - -# p.switch_disconnect() -# p._show_config() diff --git a/drivers/CiscoSX300.py b/drivers/CiscoSX300.py deleted file mode 100644 index a6f5446..0000000 --- a/drivers/CiscoSX300.py +++ /dev/null @@ -1,697 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import logging -import sys -import re -import pexpect - -if __name__ == '__main__': - import os - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError, PExpectError -from drivers.common import SwitchDriver, SwitchErrors - -class CiscoSX300(SwitchDriver): - - connection = None - _username = None - _password = None - _enable_password = None - - # No extra capabilities for this switch/driver yet - _capabilities = [ - ] - - # Regexp of expected hardware information - fail if we don't see - # this - _expected_descr_re = re.compile(r'.*\d+-Port.*Managed Switch.*') - - def __init__(self, switch_hostname, switch_telnetport=23, debug = False): - SwitchDriver.__init__(self, switch_hostname, debug) - self._systemdata = [] - self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) - self.errors = SwitchErrors() - - ################################ - ### Switch-level API functions - ################################ - - # Save the current running config into flash - we want config to - # remain across reboots - def switch_save_running_config(self): - try: - self._cli("copy running-config startup-config") - self.connection.expect("Y/N") - self._cli("y") - self.connection.expect("succeeded") - except (PExpectError, pexpect.EOF, pexpect.TIMEOUT): - # recurse on error - self._switch_connect() - self.switch_save_running_config() - - # Restart the switch - we need to reload config to do a - # roll-back. Do NOT save running-config first if the switch asks - - # we're trying to dump recent changes, not save them. - # - # This will also implicitly cause a connection to be closed - def switch_restart(self): - self._cli("reload") - index = self.connection.expect(['Are you sure', 'will reset']) - if index == 0: - self._cli("y") # Yes, continue without saving - self.connection.expect("reset the whole") - - # Fall through - self._cli("y") # Yes, continue to reset - self.connection.close(True) - - ################################ - ### VLAN API functions - ################################ - - # Create a VLAN with the specified tag - def vlan_create(self, tag): - logging.debug("Creating VLAN %d", tag) - - try: - self._configure() - self._cli("vlan database") - self._cli("vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - return - raise IOError("Failed to create VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_create(tag) - - # Destroy a VLAN with the specified tag - def vlan_destroy(self, tag): - logging.debug("Destroying VLAN %d", tag) - - try: - self._configure() - self._cli("no vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - raise IOError("Failed to destroy VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_destroy(tag) - - # Set the name of a VLAN - def vlan_set_name(self, tag, name): - logging.debug("Setting name of VLAN %d to %s", tag, name) - - try: - self._configure() - self._cli("vlan %d" % tag) - self._cli("interface vlan %d" % tag) - self._cli("name %s" % name) - self._end_configure() - - # Validate it happened - read_name = self.vlan_get_name(tag) - if read_name != name: - raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" - % (tag, read_name, name)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_set_name(tag, name) - - # Get a list of the VLAN tags currently registered on the switch - def vlan_get_list(self): - logging.debug("Grabbing list of VLANs") - - try: - vlans = [] - regex = re.compile(r'^ *(\d+).*(D|S|G|R)') - - self._cli("show vlan") - for line in self._read_long_output("show vlan"): - match = regex.match(line) - if match: - vlans.append(int(match.group(1))) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_list() - - # For a given VLAN tag, ask the switch what the associated name is - def vlan_get_name(self, tag): - logging.debug("Grabbing the name of VLAN %d", tag) - - try: - name = None - regex = re.compile(r'^ *\d+\s+(\S+).*(D|S|G|R)') - self._cli("show vlan tag %d" % tag) - for line in self._read_long_output("show vlan tag"): - match = regex.match(line) - if match: - name = match.group(1) - name.strip() - return name - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_name(tag) - - ################################ - ### Port API functions - ################################ - - # Set the mode of a port: access or trunk - def port_set_mode(self, port, mode): - logging.debug("Setting port %s to %s", port, mode) - if not self._is_port_mode_valid(mode): - raise InputError("Port mode %s is not allowed" % mode) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport mode %s" % mode) - self._end_configure() - - # Validate it happened - read_mode = self._port_get_mode(port) - if read_mode != mode: - raise IOError("Failed to set mode for port %s" % port) - - # And cache the result - self._port_modes[port] = mode - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_mode(port, mode) - - # Set an access port to be in a specified VLAN (tag) - def port_set_access_vlan(self, port, tag): - logging.debug("Setting access port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport access vlan %d" % tag) - self._end_configure() - - # Validate things worked - read_vlan = int(self.port_get_access_vlan(port)) - if read_vlan != tag: - raise IOError("Failed to move access port %s to VLAN %d - got VLAN %d instead" - % (port, tag, read_vlan)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_access_vlan(port, tag) - - # Add a trunk port to a specified VLAN (tag) - def port_add_trunk_to_vlan(self, port, tag): - logging.debug("Adding trunk port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport trunk allowed vlan add %d" % tag) - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag: - return - raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_add_trunk_to_vlan(port, tag) - - # Remove a trunk port from a specified VLAN (tag) - def port_remove_trunk_from_vlan(self, port, tag): - logging.debug("Removing trunk port %s from VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("switchport trunk allowed vlan remove %d" % tag) - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag: - raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_remove_trunk_from_vlan(port, tag) - - # Get the configured VLAN tag for an access port (tag) - def port_get_access_vlan(self, port): - logging.debug("Getting VLAN for access port %s", port) - vlan = 1 - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - regex = re.compile(r'(\d+)\s+\S+\s+Untagged\s+(D|S|G|R)') - - try: - self._cli("show interfaces switchport %s" % port) - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - vlan = match.group(1) - return int(vlan) - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_access_vlan(port) - - # Get the list of configured VLAN tags for a trunk port - def port_get_trunk_vlan_list(self, port): - logging.debug("Getting VLANs for trunk port %s", port) - vlans = [ ] - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - regex = re.compile(r'(\d+)\s+\S+\s+(Tagged|Untagged)\s+(D|S|G|R)') - - try: - self._cli("show interfaces switchport %s" % port) - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - vlans.append (int(match.group(1))) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_trunk_vlan_list(port) - - ################################ - ### Internal functions - ################################ - - # Connect to the switch and log in - def _switch_connect(self): - - if not self.connection is None: - self.connection.close(True) - self.connection = None - - logging.debug("Connecting to Switch with: %s", self.exec_string) - self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) - self._login() - - # Avoid paged output - self._cli("terminal datadump") - - # And grab details about the switch. in case we need it - self._get_systemdata() - - # And also validate them - make sure we're driving a switch of - # the correct model! Also store the serial number - descr_regex = re.compile(r'System Description:.\s+(.*)') - sn_regex = re.compile(r'SN:\s+(\S_)') - descr = "" - - for line in self._systemdata: - match = descr_regex.match(line) - if match: - descr = match.group(1) - match = sn_regex.match(line) - if match: - self.serial_number = match.group(1) - - if not self._expected_descr_re.match(descr): - raise IOError("Switch %s not recognised by this driver: abort" % descr) - - # Now build a list of our ports, for later sanity checking - self._ports = self._get_port_names() - if len(self._ports) < 4: - raise IOError("Not enough ports detected - problem!") - - def _login(self): - logging.debug("attempting login with username %s, password %s", self._username, self._password) - self._cli("") - self.connection.expect("User Name:") - self._cli("%s" % self._username) - self.connection.expect("Password:") - self._cli("%s" % self._password, False) - self.connection.expect(r"\*\*") - while True: - index = self.connection.expect(['User Name:', 'authentication failed', r'([^#]+)#', 'Password:', '.+']) - if index == 0 or index == 1: # Failed to log in! - logging.error("Login failure: %s\n", self.connection.match) - raise IOError - elif index == 2: - self._prompt_name = re.escape(self.connection.match.group(1).strip()) - # Horrible output from the switch at login time may - # confuse our pexpect stuff here. If we've somehow got - # multiple lines of output, clean up and just take the - # *last* line here. Anything before that is going to - # just be noise from the "***" password input, etc. - prompt_lines = self._prompt_name.split('\r\n') - if len(prompt_lines) > 1: - self._prompt_name = prompt_lines[-1] - logging.debug("Got prompt name %s", self._prompt_name) - return 0 - elif index == 3 or index == 4: - self._cli("", False) - - def _logout(self): - logging.debug("Logging out") - self._cli("exit", False) - self.connection.close(True) - - def _configure(self): - self._cli("configure terminal") - - def _end_configure(self): - self._cli("end") - - def _read_long_output(self, text): - prompt = self._prompt_name + '#' - try: - self.connection.expect(prompt) - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_in(text) - raise PExpectError("_read_long_output failed") - except: - logging.error("prompt is \"%s\"", prompt) - raise - - longbuf = [] - for line in self.connection.before.split('\r\n'): - longbuf.append(line.strip()) - return longbuf - - def _get_port_names(self): - logging.debug("Grabbing list of ports") - interfaces = [] - - # Use "Up" or "Down" to only identify lines in the output that - # match interfaces that exist - regex = re.compile(r'^(\w+).*(Up|Down)') - - try: - self._cli("show interfaces status detailed") - for line in self._read_long_output("show interfaces status detailed"): - match = regex.match(line) - if match: - interface = match.group(1) - interfaces.append(interface) - self._port_numbers[interface] = len(interfaces) - logging.debug(" found %d ports on the switch", len(interfaces)) - return interfaces - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_port_names() - - # Get the mode of a port: access or trunk - def _port_get_mode(self, port): - logging.debug("Getting mode of port %s", port) - mode = '' - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - regex = re.compile(r'Port Mode: (\S+)') - - try: - self._cli("show interfaces switchport %s" % port) - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - mode = match.group(1) - return mode.lower() - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_mode(port) - - def _show_config(self): - logging.debug("Grabbing config") - try: - self._cli("show running-config") - return self._read_long_output("show running-config") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_config() - - def _show_clock(self): - logging.debug("Grabbing time") - try: - self._cli("show clock") - return self._read_long_output("show clock") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_clock() - - def _get_systemdata(self): - logging.debug("Grabbing system data") - - try: - self._systemdata = [] - self._cli("show system") - for line in self._read_long_output("show system"): - self._systemdata.append(line) - - logging.debug("Grabbing system sw and hw versions") - self._cli("show version") - for line in self._read_long_output("show version"): - self._systemdata.append(line) - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_systemdata() - - ###################################### - # Internal port access helper methods - ###################################### - # N.B. No parameter checking here, for speed reasons - if you're - # calling this internal API then you should already have validated - # things yourself! Equally, no post-set checks in here - do that - # at the higher level. - ###################################### - - # Wrapper around connection.send - by default, expect() the same - # text we've sent, to remove it from the output from the - # switch. For the few cases where we don't need that, override - # this using echo=False. - # Horrible, but seems to work. - def _cli(self, text, echo=True): - self.connection.send(text + '\r') - if echo: - try: - self.connection.expect(text) - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_out(text) - raise PExpectError("_cli failed on %s" % text) - except: - logging.error("Unexpected error: %s", sys.exc_info()[0]) - raise - -if __name__ == "__main__": -# p = CiscoSX300('10.172.2.52', 23) - - # Simple test harness - exercise the main working functions above to verify - # they work. This does *NOT* test really disruptive things like "save - # running-config" and "reload" - test those by hand. - - import optparse - - switch = 'vlandswitch02' - parser = optparse.OptionParser() - parser.add_option("--switch", - dest = "switch", - action = "store", - nargs = 1, - type = "string", - help = "specify switch to connect to for testing", - metavar = "") - (opts, args) = parser.parse_args() - if opts.switch: - switch = opts.switch - - portname_base = "fa" - # Text to match if we're on a SG-series switch, ports all called gi - sys_descr_re = re.compile('System Description.*Gigabit') - - def _port_name(number): - return "%s%d" % (portname_base, number) - - logging.basicConfig(level = logging.DEBUG, - format = '%(asctime)s %(levelname)-8s %(message)s') - p = CiscoSX300(switch, 23, debug = True) - p.switch_connect('cisco', 'cisco', None) - #buf = p._show_clock() - #print "%s" % buf - #buf = p._show_config() - #p.dump_list(buf) - - print "System data:" - p.dump_list(p._systemdata) - for l in p._systemdata: - m = sys_descr_re.match(l) - if m: - print 'Found an SG switch, using "gi" as port name prefix for testing' - portname_base = "gi" - - if portname_base == "fa": - print 'Found an SF switch, using "fa" as port name prefix for testing' - - print "Creating VLANs for testing:" - for i in [ 2, 3, 4, 5, 20 ]: - p.vlan_create(i) - p.vlan_set_name(i, "test%d" % i) - print " %d (test%d)" % (i, i) - - #print "And dump config\n" - #buf = p._show_config() - #print "%s" % buf - - #print "Destroying VLAN 2\n" - #p.vlan_destroy(2) - - #print "And dump config\n" - #buf = p._show_config() - #print "%s" % buf - - #print "Port names are:" - #buf = p.switch_get_port_names() - #p.dump_list(buf) - - #buf = p.vlan_get_name(25) - #print "VLAN with tag 25 is called \"%s\"" % buf - - #p.vlan_set_name(35, "foo") - #print "VLAN with tag 35 is called \"foo\"" - - #buf = p.port_get_mode(_port_name(12)) - #print "Port %s is in %s mode" % (_port_name(12), buf) - - # Test access stuff - print "Set %s to access mode" % _port_name(6) - p.port_set_mode(_port_name(6), "access") - print "Move %s to VLAN 2" % _port_name(6) - p.port_set_access_vlan(_port_name(6), 2) - buf = p.port_get_access_vlan(_port_name(6)) - print "Read from switch: %s is on VLAN %s" % (_port_name(6), buf) - print "Move %s back to default VLAN 1" % _port_name(6) - p.port_set_access_vlan(_port_name(6), 1) - #print "And move %s back to a trunk port" % _port_name(6) - #p.port_set_mode(_port_name(6), "trunk") - #buf = p.port_get_mode(_port_name(6)) - #print "Port %s is in %s mode" % (_port_name(6), buf) - - # Test trunk stuff - print "Set %s to trunk mode" % _port_name(2) - p.port_set_mode(_port_name(2), "trunk") - print "Add %s to VLAN 2" % _port_name(2) - p.port_add_trunk_to_vlan(_port_name(2), 2) - print "Add %s to VLAN 3" % _port_name(2) - p.port_add_trunk_to_vlan(_port_name(2), 3) - print "Add %s to VLAN 4" % _port_name(2) - p.port_add_trunk_to_vlan(_port_name(2), 4) - print "Read from switch: which VLANs is %s on?" % _port_name(2) - buf = p.port_get_trunk_vlan_list(_port_name(2)) - p.dump_list(buf) - - print "Remove %s from VLANs 3,3,4" % _port_name(2) - p.port_remove_trunk_from_vlan(_port_name(2), 3) - p.port_remove_trunk_from_vlan(_port_name(2), 3) - p.port_remove_trunk_from_vlan(_port_name(2), 4) - print "Read from switch: which VLANs is %s on?" % _port_name(2) - buf = p.port_get_trunk_vlan_list(_port_name(2)) - p.dump_list(buf) - - # print "Adding lots of ports to VLANs" - # p.port_add_trunk_to_vlan(_port_name(1), 2) - # p.port_add_trunk_to_vlan(_port_name(3), 2) - # p.port_add_trunk_to_vlan(_port_name(5), 2) - # p.port_add_trunk_to_vlan(_port_name(7), 2) - # p.port_add_trunk_to_vlan(_port_name(9), 2) - # p.port_add_trunk_to_vlan(_port_name(11), 2) - # p.port_add_trunk_to_vlan(_port_name(13), 2) - # p.port_add_trunk_to_vlan(_port_name(15), 2) - # p.port_add_trunk_to_vlan(_port_name(17), 2) - # p.port_add_trunk_to_vlan(_port_name(19), 2) - # p.port_add_trunk_to_vlan(_port_name(21), 2) - # p.port_add_trunk_to_vlan(_port_name(23), 2) - # p.port_add_trunk_to_vlan(_port_name(4, 2) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - -# print 'Restarting switch, to explicitly reset config' -# p.switch_restart() - -# p.switch_save_running_config() -# p._show_config() diff --git a/drivers/Dummy.py b/drivers/Dummy.py deleted file mode 100644 index f630184..0000000 --- a/drivers/Dummy.py +++ /dev/null @@ -1,361 +0,0 @@ -#! /usr/bin/python - -# Copyright 2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import logging -import sys -import re -import pickle -import pexpect - -# Dummy switch driver, designed specifically for -# testing/validation. Just remembers what it's been told and gives the -# same data back on demand. -# -# To keep track of data in the dummy switch, this code will simply -# dump out and read back its internal state to/from a Python pickle -# file as needed. On first use, if no such file exists then the Dummy -# driver will simply generate a simple switch model: -# -# * N ports in access mode -# * 1 VLAN (tag 1) labelled DEFAULT -# -# The "hostname" given to the switch in VLANd is important, as it will -# determine both the number of ports allocated in this model and the -# name of the pickle file used for data storage. Call the switch -# "dummy-N" in your vland.cfg file to have N ports. If you want to use -# more than one dummy switch instance, ensure you give them different -# numbers, e.g. "dummy-25", "dummy-48", etc. - -if __name__ == '__main__': - import os - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError, PExpectError -from drivers.common import SwitchDriver, SwitchErrors - -class Dummy(SwitchDriver): - - connection = None - _username = None - _password = None - _enable_password = None - _dummy_vlans = {} - _dummy_ports = {} - _state_file = None - - _capabilities = [ - ] - - def __init__(self, switch_hostname, switch_telnetport=23, debug = False): - SwitchDriver.__init__(self, switch_hostname, debug) - self._systemdata = [] - self.errors = SwitchErrors() - self._state_file = "%s.pk" % switch_hostname - - ################################ - ### Switch-level API functions - ################################ - - # Save the current running config - we want config to remain - # across reboots - def switch_save_running_config(self): - pass - - # Restart the switch - we need to reload config to do a - # roll-back. Do NOT save running-config first if the switch asks - - # we're trying to dump recent changes, not save them. - def switch_restart(self): - pass - - # List the capabilities of the switch (and driver) - some things - # make no sense to abstract. Returns a dict of strings, each one - # describing an extra feature that that higher levels may care - # about - def switch_get_capabilities(self): - return self._capabilities - - ################################ - ### VLAN API functions - ################################ - - # Create a VLAN with the specified tag - def vlan_create(self, tag): - logging.debug("Creating VLAN %d", tag) - if not tag in self._dummy_vlans: - self._dummy_vlans[tag] = "VLAN%s" % tag - else: - # It's not an error if it already exists, but log anyway - logging.debug("VLAN %d already exists, name %s", - tag, self._dummy_vlans[tag]) - - # Destroy a VLAN with the specified tag - def vlan_destroy(self, tag): - logging.debug("Destroying VLAN %d", tag) - if tag in self._dummy_vlans: - del self._dummy_vlans[tag] - else: - # It's not an error if it doesn't exist, but log anyway - logging.debug("VLAN %d did not exist", tag) - - # Set the name of a VLAN - def vlan_set_name(self, tag, name): - logging.debug("Setting name of VLAN %d to %s", tag, name) - if not tag in self._dummy_vlans: - raise InputError("Tag %d does not exist") - self._dummy_vlans[tag] = "VLAN%s" % tag - - # Get a list of the VLAN tags currently registered on the switch - def vlan_get_list(self): - logging.debug("Grabbing list of VLANs") - return sorted(self._dummy_vlans.keys()) - - # For a given VLAN tag, ask the switch what the associated name is - def vlan_get_name(self, tag): - logging.debug("Grabbing the name of VLAN %d", tag) - if not tag in self._dummy_vlans: - raise InputError("Tag %d does not exist") - return self._dummy_vlans[tag] - - ################################ - ### Port API functions - ################################ - - # Set the mode of a port: access or trunk - def port_set_mode(self, port, mode): - logging.debug("Setting port %s to %s mode", port, mode) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - self._dummy_ports[port]['mode'] = mode - - # Get the mode of a port: access or trunk - def port_get_mode(self, port): - logging.debug("Getting mode of port %s", port) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - return self._dummy_ports[port]['mode'] - - # Set an access port to be in a specified VLAN (tag) - def port_set_access_vlan(self, port, tag): - logging.debug("Setting access port %s to VLAN %d", port, tag) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - if not tag in self._dummy_vlans: - raise InputError("VLAN %d does not exist" % tag) - self._dummy_ports[port]['access_vlan'] = tag - - # Add a trunk port to a specified VLAN (tag) - def port_add_trunk_to_vlan(self, port, tag): - logging.debug("Adding trunk port %s to VLAN %d", port, tag) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - if not tag in self._dummy_vlans: - raise InputError("VLAN %d does not exist" % tag) - self._dummy_ports[port]['trunk_vlans'].append(tag) - - # Remove a trunk port from a specified VLAN (tag) - def port_remove_trunk_from_vlan(self, port, tag): - logging.debug("Removing trunk port %s from VLAN %d", port, tag) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - if not tag in self._dummy_vlans: - raise InputError("VLAN %d does not exist" % tag) - self._dummy_ports[port]['trunk_vlans'].remove(tag) - - # Get the configured VLAN tag for an access port (tag) - def port_get_access_vlan(self, port): - logging.debug("Getting VLAN for access port %s", port) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - return self._dummy_ports[port]['access_vlan'] - - # Get the list of configured VLAN tags for a trunk port - def port_get_trunk_vlan_list(self, port): - logging.debug("Getting VLAN(s) for trunk port %s", port) - if not port in self._dummy_ports: - raise InputError("Port %s does not exist" % port) - return sorted(self._dummy_ports[port]['trunk_vlans']) - - ################################ - ### Internal functions - ################################ - - # Connect to the switch and log in - def _switch_connect(self): - # Open data file if it exists, otherwise initialise - try: - pkl_file = open(self._state_file, 'rb') - self._dummy_vlans = pickle.load(pkl_file) - self._dummy_ports = pickle.load(pkl_file) - pkl_file.close() - except: - # Create data here - self._dummy_vlans = {1: 'DEFAULT'} - match = re.match(r'dummy-(\d+)', self.hostname) - if match: - num_ports = int(match.group(1)) - else: - raise InputError("Unable to determine number of ports from switch name") - for i in range(1, num_ports+1): - port_name = "dm%2.2d" % int(i) - self._dummy_ports[port_name] = {} - self._dummy_ports[port_name]['mode'] = 'access' - self._dummy_ports[port_name]['access_vlan'] = 1 - self._dummy_ports[port_name]['trunk_vlans'] = [] - - # Now build a list of our ports, for later sanity checking - self._ports = self._get_port_names() - if len(self._ports) < 4: - raise IOError("Not enough ports detected - problem!") - - def _logout(self): - pkl_file = open(self._state_file, 'wb') - pickle.dump(self._dummy_vlans, pkl_file) - pickle.dump(self._dummy_ports, pkl_file) - pkl_file.close() - - def _get_port_names(self): - logging.debug("Grabbing list of ports") - interfaces = [] - for interface in sorted(self._dummy_ports.keys()): - interfaces.append(interface) - self._port_numbers[interface] = len(interfaces) - return interfaces - -if __name__ == "__main__": - - # Simple test harness - exercise the main working functions above to verify - # they work. This does *NOT* test really disruptive things like "save - # running-config" and "reload" - test those by hand. - - import optparse - - switch = 'dummy-48' - parser = optparse.OptionParser() - parser.add_option("--switch", - dest = "switch", - action = "store", - nargs = 1, - type = "string", - help = "specify switch to connect to for testing", - metavar = "") - (opts, args) = parser.parse_args() - if opts.switch: - switch = opts.switch - - logging.basicConfig(level = logging.DEBUG, - format = '%(asctime)s %(levelname)-8s %(message)s') - p = Dummy(switch, 23, debug=True) - p.switch_connect('admin', '', None) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.vlan_get_name(1) - print "VLAN 1 is named \"%s\"" % buf - - print "Create VLAN 3" - p.vlan_create(3) - - print "Create VLAN 4" - p.vlan_create(4) - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "Set name of VLAN 3 to test333" - p.vlan_set_name(3, "test333") - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - print "Destroy VLAN 3" - p.vlan_destroy(3) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.port_get_mode("dm10") - print "Port dm10 is in %s mode" % buf - - buf = p.port_get_mode("dm11") - print "Port dm11 is in %s mode" % buf - - # Test access stuff - print "Set dm09 to access mode" - p.port_set_mode("dm09", "access") - - print "Move dm9 to VLAN 4" - p.port_set_access_vlan("dm09", 4) - - buf = p.port_get_access_vlan("dm09") - print "Read from switch: dm09 is on VLAN %s" % buf - - print "Move dm09 back to VLAN 1" - p.port_set_access_vlan("dm09", 1) - - print "Create VLAN 2" - p.vlan_create(2) - - print "Create VLAN 3" - p.vlan_create(3) - - print "Create VLAN 4" - p.vlan_create(4) - - # Test access stuff - print "Set dm09 to trunk mode" - p.port_set_mode("dm09", "trunk") - print "Read from switch: which VLANs is dm09 on?" - buf = p.port_get_trunk_vlan_list("dm09") - p.dump_list(buf) - - # The adds below are NOOPs in effect on this switch - no filtering - # for "trunk" ports - print "Add dm09 to VLAN 2" - p.port_add_trunk_to_vlan("dm09", 2) - print "Add dm09 to VLAN 3" - p.port_add_trunk_to_vlan("dm09", 3) - print "Add dm09 to VLAN 4" - p.port_add_trunk_to_vlan("dm09", 4) - print "Read from switch: which VLANs is dm09 on?" - buf = p.port_get_trunk_vlan_list("dm09") - p.dump_list(buf) - - # And the same for removals here - p.port_remove_trunk_from_vlan("dm09", 3) - p.port_remove_trunk_from_vlan("dm09", 2) - p.port_remove_trunk_from_vlan("dm09", 4) - print "Read from switch: which VLANs is dm09 on?" - buf = p.port_get_trunk_vlan_list("dm09") - p.dump_list(buf) - - print 'Restarting switch, to explicitly reset config' - p.switch_restart() - - p.switch_save_running_config() - - p.switch_disconnect() diff --git a/drivers/Mellanox.py b/drivers/Mellanox.py deleted file mode 100644 index ea74bf0..0000000 --- a/drivers/Mellanox.py +++ /dev/null @@ -1,795 +0,0 @@ -#! /usr/bin/python - -# Copyright 2018 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import logging -import sys -import re -import pexpect - -# Mellanox MLNX-OS driver -# Developed and tested against the SN2100 in the Linaro LAVA lab - -if __name__ == '__main__': - import os - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError, PExpectError -from drivers.common import SwitchDriver, SwitchErrors - -class Mellanox(SwitchDriver): - - connection = None - _username = None - _password = None - _enable_password = None - - _capabilities = [ - 'TrunkWildCardVlans' # Trunk ports are on all VLANs by - # default, so we shouldn't need to - # bugger with them - ] - - # Regexp of expected hardware information - fail if we don't see - # this - _expected_descr_re = re.compile(r'MLNX-OS') - - def __init__(self, switch_hostname, switch_telnetport=23, debug = False): - SwitchDriver.__init__(self, switch_hostname, debug) - self._systemdata = [] - self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) - self.errors = SwitchErrors() - - ################################ - ### Switch-level API functions - ################################ - - # Save the current running config into flash - we want config to - # remain across reboots - def switch_save_running_config(self): - try: - self._configure() - self._cli("configuration write") - self._end_configure() - except (PExpectError, pexpect.EOF): - # recurse on error - self._switch_connect() - self.switch_save_running_config() - - # Restart the switch - we need to reload config to do a - # roll-back. Do NOT save running-config first if the switch asks - - # we're trying to dump recent changes, not save them. - # - # This will also implicitly cause a connection to be closed - def switch_restart(self): - self._cli("reload noconfirm") - - # List the capabilities of the switch (and driver) - some things - # make no sense to abstract. Returns a dict of strings, each one - # describing an extra feature that that higher levels may care - # about - def switch_get_capabilities(self): - return self._capabilities - - ################################ - ### VLAN API functions - ################################ - - # Create a VLAN with the specified tag - def vlan_create(self, tag): - logging.debug("Creating VLAN %d", tag) - try: - self._configure() - self._cli("vlan %d" % tag) - self._end_vlan() - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - return - raise IOError("Failed to create VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_create(tag) - - # Destroy a VLAN with the specified tag - def vlan_destroy(self, tag): - logging.debug("Destroying VLAN %d", tag) - - try: - self._configure() - self._cli("no vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - raise IOError("Failed to destroy VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_destroy(tag) - - # Set the name of a VLAN - def vlan_set_name(self, tag, name): - logging.debug("Setting name of VLAN %d to %s", tag, name) - - try: - self._configure() - self._cli("vlan %d name %s" % (tag, name)) - self._end_configure() - - # This switch *might* have problems if we drive it too quickly? At - # least one instance of set_name()/get_name() not working. This - # might help? - self._delay() - - # And retry around here - retries = 5 - read_name = None - while (retries > 0 and read_name is None): - # Validate it happened - read_name = self.vlan_get_name(tag) - retries -= 1 - - if read_name != name: - raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" - % (tag, read_name, name)) - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_set_name(tag, name) - - # Get a list of the VLAN tags currently registered on the switch - def vlan_get_list(self): - logging.debug("Grabbing list of VLANs") - - try: - vlans = [] - - regex = re.compile(r'^ *(\d+)') - - self._cli("show vlan") - for line in self._read_long_output("show vlan"): - match = regex.match(line) - if match: - vlans.append(int(match.group(1))) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_list() - - # For a given VLAN tag, ask the switch what the associated name is - def vlan_get_name(self, tag): - - try: - logging.debug("Grabbing the name of VLAN %d", tag) - name = None - - # Ugh, the output here is messy. VLAN names can include spaces, and - # there are no delimiters in the output, e.g.: - # VLAN Name Ports - # ---- ----------- -------------------------------------- - # 1 default Eth1/1/1, Eth1/1/2, Eth1/2, Eth1/3/1, Eth1/3/2, - # Eth1/4, Eth1/5, Eth1/6, Eth1/7, Eth1/8, - # Eth1/10, Eth1/12, Eth1/13, Eth1/14, Eth1/15, - # Eth1/16 - # 102 mdev testing - # 103 vpp 1 performance testing Eth1/1/3, Eth1/9 - # 104 vpp 2 performance testing Eth1/1/4, Eth1/11 - # - # Simplest strategy: - # 1. Match on a leading number and grab all the text after it - # 2. Drop anything starting with "Eth" to EOL - # 3. Strip leading and trailing whitespace - # - # Not perfect, but it'll have to do. Anybody including "Eth" in a - # VLAN name deserves to lose... - - regex = re.compile(r'^ *\d+\s+(.+)') - self._cli("show vlan id %d" % tag) - for line in self._read_long_output("show vlan id"): - match = regex.match(line) - if match: - name = re.sub(r'Eth.*$',"",match.group(1)).strip() - if name is None: - logging.debug("vlan_get_name: did not find a name") - return name - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_name(tag) - - - ################################ - ### Port API functions - ################################ - - # Set the mode of a port: access or trunk - def port_set_mode(self, port, mode): - logging.debug("Setting port %s to %s", port, mode) - if not self._is_port_mode_valid(mode): - raise InputError("Port mode %s is not allowed" % mode) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - - try: - self._configure() - self._cli("interface ethernet %s" % port) - self._cli("switchport mode %s" % mode) - if mode == "trunk": - # Put the new trunk port on all VLANs - self._cli("switchport trunk allowed-vlan all") - self._end_interface() - self._end_configure() - - # Validate it happened - read_mode = self._port_get_mode(port) - if read_mode != mode: - raise IOError("Failed to set mode for port %s" % port) - - # And cache the result - self._port_modes[port] = mode - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_mode(port, mode) - - # Set an access port to be in a specified VLAN (tag) - def port_set_access_vlan(self, port, tag): - logging.debug("Setting access port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - - try: - self._configure() - self._cli("interface ethernet %s" % port) - self._cli("switchport access vlan %d" % tag) - self._end_interface() - self._end_configure() - - # Validate things worked - read_vlan = int(self.port_get_access_vlan(port)) - if read_vlan != tag: - raise IOError("Failed to move access port %s to VLAN %d - got VLAN %d instead" - % (port, tag, read_vlan)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_access_vlan(port, tag) - - # Add a trunk port to a specified VLAN (tag) - def port_add_trunk_to_vlan(self, port, tag): - logging.debug("Adding trunk port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface ethernet %s" % port) - self._cli("switchport trunk allowed-vlan add %d" % tag) - self._end_interface() - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag or vlan == "ALL": - return - raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_add_trunk_to_vlan(port, tag) - - # Remove a trunk port from a specified VLAN (tag) - def port_remove_trunk_from_vlan(self, port, tag): - logging.debug("Removing trunk port %s from VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface ethernet %s" % port) - self._cli("switchport trunk allowed-vlan remove %d" % tag) - self._end_interface() - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag: - raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_remove_trunk_from_vlan(port, tag) - - # Get the configured VLAN tag for an access port (tag) - def port_get_access_vlan(self, port): - logging.debug("Getting VLAN for access port %s", port) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - - regex = re.compile(r'^Eth%s\s+access\s+(\d+)' % port) - - try: - self._cli("show interfaces switchport") - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - vlan = match.group(1) - return int(vlan) - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_access_vlan(port) - - # Get the list of configured VLAN tags for a trunk port - def port_get_trunk_vlan_list(self, port): - logging.debug("Getting VLANs for trunk port %s", port) - vlans = [] - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - regex_start = re.compile(r'^Eth%s\s+trunk\s+N/A\s+(.*)' % port) - regex_continue = re.compile(r'^(\d.*)') - - try: - self._cli("show interfaces switchport") - - # Complex parsing work - VLAN list may extend over several lines, e.g.: - # - # Eth1/16 trunk N/A 1, 102, 103, 104, 1000, 1001, 1002 - # 1003, 1004 - # - in_match = False - vlan_text = '' - - for line in self._read_long_output("show interfaces switchport"): - if in_match: - match = regex_continue.match(line) - if match: - vlan_text += ', ' # Make a consistently-formed list - vlan_text += match.group(1) - else: - in_match = False - if not in_match: - match = regex_start.match(line) - if match: - vlan_text += match.group(1) - in_match = True - - vlans = self._parse_vlan_list(vlan_text) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_trunk_vlan_list(port) - - ################################ - ### Internal functions - ################################ - - # Connect to the switch and log in - def _switch_connect(self): - - if not self.connection is None: - self.connection.close(True) - self.connection = None - - logging.debug("Connecting to Switch with: %s", self.exec_string) - self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) - self._login() - - # Avoid paged output as much as possible - self._cli("terminal length 999") - # Don't do silly things with ANSI codes - self._cli("terminal type dumb") - # and disable auto-logout after delay - self._cli("no cli session auto-logout") - - # And grab details about the switch. in case we need it - self._get_systemdata() - - # And also validate them - make sure we're driving a switch of - # the correct model! Also store the serial number - descr_regex = re.compile(r'Product name:\s*(\S+)') - sn_regex = re.compile(r'System serial num:\s*(\S+)') - descr = "" - - for line in self._systemdata: - match = descr_regex.match(line) - if match: - descr = match.group(1) - match = sn_regex.match(line) - if match: - self.serial_number = match.group(1) - - logging.debug("serial number is %s", self.serial_number) - logging.debug("system description is %s", descr) - - if not self._expected_descr_re.match(descr): - raise IOError("Switch %s not recognised by this driver: abort" % descr) - - # Now build a list of our ports, for later sanity checking - self._ports = self._get_port_names() - if len(self._ports) < 4: - raise IOError("Not enough ports detected - problem!") - - def _login(self): - logging.debug("attempting login with username %s, password %s", self._username, self._password) - if self._username is not None: - self.connection.expect("login:") - self._cli("%s" % self._username) - if self._password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._password, False) - while True: - index = self.connection.expect(['User:', 'Password:', 'Login incorrect', 'authentication failed', r'(.*?)(#|>)']) - if index != 4: # Any other means: failed to log in! - logging.error("Login failure: index %d\n", index) - logging.error("Login failure: %s\n", self.connection.match.before) - raise IOError - - # else - - # Add a couple of newlines to get past the "last login" etc. junk - self._cli("") - self._cli("") - self.connection.expect(r'^(.*?) (#|>)') - self._prompt_name = re.escape(self.connection.match.group(1).strip()) - logging.info("Got outer prompt \"%s\"", self._prompt_name) - if self.connection.match.group(2) == ">": - # Need to enter "enable" mode too - self._cli("enable") - if self._enable_password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._enable_password, False) - index = self.connection.expect(['Password:', 'Login incorrect', 'authentication failed', r'(.*) *(#|>)']) - if index != 3: # Any other means: failed to log in! - logging.error("Enable password failure: %s\n", self.connection.match) - raise IOError - self._cli("") - self._cli("") - self.connection.expect(r'^(.*?) (#|>)') - self._prompt_name = re.escape(self.connection.match.group(1).strip()) - logging.info("Got enable prompt \"%s\"", self._prompt_name) - return 0 - - def _logout(self): - logging.debug("Logging out") - self._cli("quit", False) - try: - self.connection.expect("Would you like to save them now") - self._cli("n") - except (pexpect.EOF): - pass - self.connection.close(True) - - def _configure(self): - self._cli("configure terminal") - - def _end_configure(self): - self._cli("exit") - - def _end_interface(self): - self._cli("exit") - - def _end_vlan(self): - self._cli("exit") - - def _read_long_output(self, text): - longbuf = [] - prompt = self._prompt_name + r'\s*#' - while True: - try: - index = self.connection.expect([r'lines \d+-\d+', prompt]) - if index == 0: # "lines 45-50" - for line in self.connection.before.split('\r\n'): - longbuf.append(line.strip()) - self._cli(' ', False) - elif index == 1: # Back to a prompt, says output is finished - break - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_in(text) - raise PExpectError("_read_long_output failed") - except: - logging.error("prompt is \"%s\"", prompt) - raise - - for line in self.connection.before.split('\r\n'): - longbuf.append(line.strip()) - return longbuf - - def _get_port_names(self): - logging.debug("Grabbing list of ports") - interfaces = [] - - # Look for "Eth1" at the beginning of the output lines to just - # match lines with interfaces - they have names like - # "Eth1/15". We do not care about Link Aggregation Groups (lag) - # here. - regex = re.compile(r'^Eth(\S+)') - - try: - self._cli("show interfaces switchport") - for line in self._read_long_output("show interfaces switchport"): - match = regex.match(line) - if match: - interface = match.group(1) - interfaces.append(interface) - self._port_numbers[interface] = len(interfaces) - logging.debug(" found %d ports on the switch", len(interfaces)) - return interfaces - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_port_names() - - # Get the mode of a port: access or trunk - def _port_get_mode(self, port): - logging.debug("Getting mode of port %s", port) - mode = '' - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - regex = re.compile('Switchport mode: (.*)') - - try: - self._cli("show interfaces ethernet %s" % port) - for line in self._read_long_output("show interfaces ethernet"): - match = regex.match(line) - if match: - mode = match.group(1) - if mode == 'access': - return 'access' - if mode == 'trunk': - return 'trunk' - return mode - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_mode(port) - - def _show_config(self): - logging.debug("Grabbing config") - try: - self._cli("show running-config") - return self._read_long_output("show running-config") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_config() - - def _show_clock(self): - logging.debug("Grabbing time") - try: - self._cli("show clock") - return self._read_long_output("show clock") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_clock() - - def _get_systemdata(self): - logging.debug("Grabbing system sw and hw versions") - - try: - self._systemdata = [] - self._cli("show version") - for line in self._read_long_output("show version"): - self._systemdata.append(line) - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_systemdata() - - # Borrowed from the Catalyst driver. Over-complex for our needs here, but - # it's already tested and will do the job. - def _parse_vlan_list(self, inputdata): - vlans = [] - - if inputdata == "ALL": - return ["ALL"] - elif inputdata == "NONE": - return [] - elif inputdata == "": - return [] - else: - # Parse the complex list - groups = inputdata.split(',') - for group in groups: - subgroups = group.split('-') - if len(subgroups) == 1: - vlans.append(int(subgroups[0])) - elif len(subgroups) == 2: - for i in range (int(subgroups[0]), int(subgroups[1]) + 1): - vlans.append(i) - else: - logging.debug("Can't parse group \"" + group + "\"") - - return vlans - - # Wrapper around connection.send - by default, expect() the same - # text we've sent, to remove it from the output from the - # switch. For the few cases where we don't need that, override - # this using echo=False. - # Horrible, but seems to work. - def _cli(self, text, echo=True): - self.connection.send(text + '\r') - if echo: - try: - self.connection.expect(text) - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_out(text) - raise PExpectError("_cli failed on %s" % text) - except: - logging.error("Unexpected error: %s", sys.exc_info()[0]) - raise - -if __name__ == "__main__": - - # Simple test harness - exercise the main working functions above to verify - # they work. This does *NOT* test really disruptive things like "save - # running-config" and "reload" - test those by hand. - - import optparse - - switch = '172.27.16.6' - parser = optparse.OptionParser() - parser.add_option("--switch", - dest = "switch", - action = "store", - nargs = 1, - type = "string", - help = "specify switch to connect to for testing", - metavar = "") - (opts, args) = parser.parse_args() - if opts.switch: - switch = opts.switch - - logging.basicConfig(level = logging.DEBUG, - format = '%(asctime)s %(levelname)-8s %(message)s') - p = MlnxOS(switch, 23, debug=False) - p.switch_connect('admin', 'admin', None) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.vlan_get_name(102) - print "VLAN 102 is named \"%s\"" % buf - - print "Create VLAN 1003" - p.vlan_create(1003) - - buf = p.vlan_get_name(1003) - print "VLAN 1003 is named \"%s\"" % buf - - print "Set name of VLAN 1003 to test333" - p.vlan_set_name(1003, "test333") - - buf = p.vlan_get_name(1003) - print "VLAN 1003 is named \"%s\"" % buf - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - print "Destroy VLAN 1003" - p.vlan_destroy(1003) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.port_get_mode("1/15") - print "Port 1/15 is in %s mode" % buf - - buf = p.port_get_mode("1/16") - print "Port 1/16 is in %s mode" % buf - - # Test access stuff - print "Set 1/15 to access mode" - p.port_set_mode("1/15", "access") - - print "Move 1/15 to VLAN 4" - p.port_set_access_vlan("1/15", 4) - - buf = p.port_get_access_vlan("1/15") - print "Read from switch: 1/15 is on VLAN %s" % buf - - print "Move 1/15 back to VLAN 1" - p.port_set_access_vlan("1/15", 1) - - print "Create VLAN 1002" - p.vlan_create(1002) - - print "Create VLAN 1003" - p.vlan_create(1003) - - print "Create VLAN 1004" - p.vlan_create(1004) - - # Test access stuff - print "Set 1/15 to trunk mode" - p.port_set_mode("1/15", "trunk") - print "Read from switch: which VLANs is 1/15 on?" - buf = p.port_get_trunk_vlan_list("1/15") - p.dump_list(buf) - - # The adds below are NOOPs in effect on this switch - no filtering - # for "trunk" ports - print "Add 1/15 to VLAN 1002" - p.port_add_trunk_to_vlan("1/15", 1002) - print "Add 1/15 to VLAN 1003" - p.port_add_trunk_to_vlan("1/15", 1003) - print "Add 1/15 to VLAN 1004" - p.port_add_trunk_to_vlan("1/15", 1004) - print "Read from switch: which VLANs is 1/15 on?" - buf = p.port_get_trunk_vlan_list("1/15") - p.dump_list(buf) - - # And the same for removals here - p.port_remove_trunk_from_vlan("1/15", 1003) - p.port_remove_trunk_from_vlan("1/15", 1002) - p.port_remove_trunk_from_vlan("1/15", 1004) - print "Read from switch: which VLANs is 1/15 on?" - buf = p.port_get_trunk_vlan_list("1/15") - p.dump_list(buf) - -# print 'Restarting switch, to explicitly reset config' -# p.switch_restart() - -# p.switch_save_running_config() - -# p.switch_disconnect() -# p._show_config() diff --git a/drivers/NetgearXSM.py b/drivers/NetgearXSM.py deleted file mode 100644 index f8ba8a5..0000000 --- a/drivers/NetgearXSM.py +++ /dev/null @@ -1,782 +0,0 @@ -#! /usr/bin/python - -# Copyright 2015-2018 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import logging -import sys -import re -import pexpect - -# Netgear XSM family driver -# Developed and tested against the XSM7224S in the Linaro LAVA lab - -if __name__ == '__main__': - import os - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError, PExpectError -from drivers.common import SwitchDriver, SwitchErrors - -class NetgearXSM(SwitchDriver): - - connection = None - _username = None - _password = None - _enable_password = None - - _capabilities = [ - ] - - # Regexps of expected hardware information - fail if we don't see - # this - _expected_manuf = re.compile('^Netgear') - _expected_model = re.compile('^XSM') - - def __init__(self, switch_hostname, switch_telnetport=23, debug = False): - SwitchDriver.__init__(self, switch_hostname, debug) - self._systemdata = [] - self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) - self.errors = SwitchErrors() - - ################################ - ### Switch-level API functions - ################################ - - # Save the current running config into flash - we want config to - # remain across reboots - def switch_save_running_config(self): - try: - self._cli("save") - self.connection.expect("Are you sure") - self._cli("y") - self.connection.expect("Configuration Saved!") - except (PExpectError, pexpect.EOF): - # recurse on error - self._switch_connect() - self.switch_save_running_config() - - # Restart the switch - we need to reload config to do a - # roll-back. Do NOT save running-config first if the switch asks - - # we're trying to dump recent changes, not save them. - # - # This will also implicitly cause a connection to be closed - def switch_restart(self): - self._cli("reload") - self.connection.expect('Are you sure') - self._cli("y") # Yes, continue to reset - self.connection.close(True) - - # List the capabilities of the switch (and driver) - some things - # make no sense to abstract. Returns a dict of strings, each one - # describing an extra feature that that higher levels may care - # about - def switch_get_capabilities(self): - return self._capabilities - - ################################ - ### VLAN API functions - ################################ - - # Create a VLAN with the specified tag - def vlan_create(self, tag): - logging.debug("Creating VLAN %d", tag) - try: - self._cli("vlan database") - self._cli("vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - return - raise IOError("Failed to create VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_create(tag) - - # Destroy a VLAN with the specified tag - def vlan_destroy(self, tag): - logging.debug("Destroying VLAN %d", tag) - - try: - self._cli("vlan database") - self._cli("no vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - raise IOError("Failed to destroy VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_destroy(tag) - - # Set the name of a VLAN - def vlan_set_name(self, tag, name): - logging.debug("Setting name of VLAN %d to %s", tag, name) - - try: - self._cli("vlan database") - self._cli("vlan name %d %s" % (tag, name)) - self._end_configure() - - # Validate it happened - read_name = self.vlan_get_name(tag) - if read_name != name: - raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" - % (tag, read_name, name)) - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_set_name(tag, name) - - # Get a list of the VLAN tags currently registered on the switch - def vlan_get_list(self): - logging.debug("Grabbing list of VLANs") - - try: - vlans = [] - - regex = re.compile(r'^ *(\d+).*(Static)') - - self._cli("show vlan brief") - for line in self._read_long_output("show vlan brief"): - match = regex.match(line) - if match: - vlans.append(int(match.group(1))) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_list() - - # For a given VLAN tag, ask the switch what the associated name is - def vlan_get_name(self, tag): - - try: - logging.debug("Grabbing the name of VLAN %d", tag) - name = None - regex = re.compile('VLAN Name: (.*)') - self._cli("show vlan %d" % tag) - for line in self._read_long_output("show vlan"): - match = regex.match(line) - if match: - name = match.group(1) - name.strip() - return name - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_name(tag) - - ################################ - ### Port API functions - ################################ - - # Set the mode of a port: access or trunk - def port_set_mode(self, port, mode): - logging.debug("Setting port %s to %s mode", port, mode) - if not self._is_port_mode_valid(mode): - raise InputError("Port mode %s is not allowed" % mode) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - - # This switch does not support specific modes, so we can't - # actually change the mode directly. However, we can and - # should deal with the PVID and memberships of existing VLANs - # etc. - - try: - if mode == "trunk": - # We define a trunk port thus: - # * accept all frames on ingress - # * accept packets for all VLANs (no ingress filter) - # * tags frames on transmission (do that later when - # * adding VLANs to the port) - # * PVID should match the default VLAN (1). - self._configure() - self._cli("interface %s" % port) - self._cli("vlan acceptframe all") - self._cli("no vlan ingressfilter") - self._cli("vlan pvid 1") - self._end_interface() - self._end_configure() - - # We define an access port thus: - # * accept only untagged frames on ingress - # * accept packets for only desired VLANs (ingress filter) - # * exists on one VLAN only (1 by default) - # * do not tag frames on transmission (the devices - # we're talking to are expecting untagged frames) - # * PVID should match the VLAN it's on (1 by default, - # but don't do that here) - if mode == "access": - self._configure() - self._cli("interface %s" % port) - self._cli("vlan acceptframe admituntaggedonly") - self._cli("vlan ingressfilter") - self._cli("no vlan tagging 1-1023") - self._cli("no vlan tagging 1024-2047") - self._cli("no vlan tagging 2048-3071") - self._cli("no vlan tagging 3072-4093") - self._end_interface() - self._end_configure() - - # Validate it happened - read_mode = self._port_get_mode(port) - - if read_mode != mode: - raise IOError("Failed to set mode for port %s" % port) - - # And cache the result - self._port_modes[port] = mode - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_mode(port, mode) - - # Set an access port to be in a specified VLAN (tag) - def port_set_access_vlan(self, port, tag): - logging.debug("Setting access port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - - try: - current_vlans = self._get_port_vlans(port) - self._configure() - self._cli("interface %s" % port) - self._cli("vlan pvid %s" % tag) - # Find the list of VLANs we're currently on, and drop them - # all. "auto" mode is fine here, we won't be included - # unless we have GVRP configured, and we don't do - # that. - for current_vlan in current_vlans: - self._cli("vlan participation auto %s" % current_vlan) - # Now specifically include the VLAN we want - self._cli("vlan participation include %s" % tag) - self._cli("no shutdown") - self._end_interface() - self._end_configure() - - # Finally, validate things worked - read_vlan = int(self.port_get_access_vlan(port)) - if read_vlan != tag: - raise IOError("Failed to move access port %d to VLAN %d - got VLAN %d instead" - % (port, tag, read_vlan)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_access_vlan(port, tag) - - # Add a trunk port to a specified VLAN (tag) - def port_add_trunk_to_vlan(self, port, tag): - logging.debug("Adding trunk port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("vlan participation include %d" % tag) - self._cli("vlan tagging %d" % tag) - self._end_interface() - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag or vlan == "ALL": - return - raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_add_trunk_to_vlan(port, tag) - - # Remove a trunk port from a specified VLAN (tag) - def port_remove_trunk_from_vlan(self, port, tag): - logging.debug("Removing trunk port %s from VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - - try: - self._configure() - self._cli("interface %s" % port) - self._cli("vlan participation auto %d" % tag) - self._cli("no vlan tagging %d" % tag) - self._end_interface() - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag: - raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_remove_trunk_from_vlan(port, tag) - - # Get the configured VLAN tag for an access port (tag) - def port_get_access_vlan(self, port): - logging.debug("Getting VLAN for access port %s", port) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "access"): - raise InputError("Port %s not in access mode" % port) - vlans = self._get_port_vlans(port) - if (len(vlans) > 1): - raise IOError("More than one VLAN on access port %s" % port) - return vlans[0] - - # Get the list of configured VLAN tags for a trunk port - def port_get_trunk_vlan_list(self, port): - logging.debug("Getting VLAN(s) for trunk port %s", port) - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if not (self.port_get_mode(port) == "trunk"): - raise InputError("Port %s not in trunk mode" % port) - return self._get_port_vlans(port) - - ################################ - ### Internal functions - ################################ - - # Connect to the switch and log in - def _switch_connect(self): - - if not self.connection is None: - self.connection.close(True) - self.connection = None - - logging.debug("Connecting to Switch with: %s", self.exec_string) - self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) - self._login() - - # Avoid paged output - self._cli("terminal length 0") - - # And grab details about the switch. in case we need it - self._get_systemdata() - - # And also validate them - make sure we're driving a switch of - # the correct model! Also store the serial number - manuf_regex = re.compile(r'^Manufacturer([\.\s])+(\S+)') - model_regex = re.compile(r'^Machine Model([\.\s])+(\S+)') - sn_regex = re.compile(r'^Serial Number([\.\s])+(\S+)') - - for line in self._systemdata: - match1 = manuf_regex.match(line) - if match1: - manuf = match1.group(2) - - match2 = model_regex.match(line) - if match2: - model = match2.group(2) - - match3 = sn_regex.match(line) - if match3: - self.serial_number = match3.group(2) - - logging.debug("manufacturer is %s", manuf) - logging.debug("model is %s", model) - logging.debug("serial number is %s", self.serial_number) - - if not (self._expected_manuf.match(manuf) and self._expected_model.match(model)): - raise IOError("Switch %s %s not recognised by this driver: abort" % (manuf, model)) - - # Now build a list of our ports, for later sanity checking - self._ports = self._get_port_names() - if len(self._ports) < 4: - raise IOError("Not enough ports detected - problem!") - - def _login(self): - logging.debug("attempting login with username %s, password %s", self._username, self._password) - if self._username is not None: - self.connection.expect("User:") - self._cli("%s" % self._username) - if self._password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._password, False) - while True: - index = self.connection.expect(['User:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) - if index != 4: # Any other means: failed to log in! - logging.error("Login failure: index %d\n", index) - logging.error("Login failure: %s\n", self.connection.match.before) - raise IOError - - # else - self._prompt_name = re.escape(self.connection.match.group(1).strip()) - if self.connection.match.group(2) == ">": - # Need to enter "enable" mode too - self._cli("enable") - if self._enable_password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._enable_password, False) - index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*) *(#|>)']) - if index != 3: # Any other means: failed to log in! - logging.error("Enable password failure: %s\n", self.connection.match) - raise IOError - return 0 - - def _logout(self): - logging.debug("Logging out") - self._cli("quit", False) - try: - self.connection.expect("Would you like to save them now") - self._cli("n") - except (pexpect.EOF): - pass - self.connection.close(True) - - def _configure(self): - self._cli("configure") - - def _end_configure(self): - self._cli("exit") - - def _end_interface(self): - self._end_configure() - - def _read_long_output(self, text): - longbuf = [] - prompt = self._prompt_name + r'\s*#' - while True: - try: - index = self.connection.expect(['--More--', prompt]) - if index == 0: # "--More-- or (q)uit" - for line in self.connection.before.split('\r\n'): - line1 = re.sub('(\x08|\x0D)*', '', line.strip()) - longbuf.append(line1) - self._cli(' ', False) - elif index == 1: # Back to a prompt, says output is finished - break - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_in(text) - raise PExpectError("_read_long_output failed") - except: - logging.error("prompt is \"%s\"", prompt) - raise - - for line in self.connection.before.split('\r\n'): - longbuf.append(line.strip()) - return longbuf - - def _get_port_names(self): - logging.debug("Grabbing list of ports") - interfaces = [] - - # Look for "1" at the beginning of the output lines to just - # match lines with interfaces - they have names like - # "1/0/22". We do not care about Link Aggregation Groups (lag) - # here. - regex = re.compile(r'^(1\S+)') - - try: - self._cli("show port all") - for line in self._read_long_output("show port all"): - match = regex.match(line) - if match: - interface = match.group(1) - interfaces.append(interface) - self._port_numbers[interface] = len(interfaces) - logging.debug(" found %d ports on the switch", len(interfaces)) - return interfaces - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_port_names() - - # Get the mode of a port: access or trunk - def _port_get_mode(self, port): - logging.debug("Getting mode of port %s", port) - - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - acceptframe_re = re.compile('vlan acceptframe (.*)') - ingress_re = re.compile('vlan ingressfilter') - - acceptframe = None - ingressfilter = True - - try: - self._cli("show running-config interface %s" % port) - for line in self._read_long_output("show running-config interface"): - - match = acceptframe_re.match(line) - if match: - acceptframe = match.group(1) - - match = ingress_re.match(line) - if match: - ingressfilter = True - - # Simple classifier for now; may need to revisit later... - if (ingressfilter and acceptframe == "admituntaggedonly"): - return "access" - else: - return "trunk" - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_mode(port) - - def _show_config(self): - logging.debug("Grabbing config") - try: - self._cli("show running-config") - return self._read_long_output("show running-config") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_config() - - def _show_clock(self): - logging.debug("Grabbing time") - try: - self._cli("show clock") - return self._read_long_output("show clock") - except PExpectError: - # recurse on error - self._switch_connect() - return self._show_clock() - - def _get_systemdata(self): - logging.debug("Grabbing system sw and hw versions") - - try: - self._systemdata = [] - self._cli("show version") - for line in self._read_long_output("show version"): - self._systemdata.append(line) - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_systemdata() - - def _parse_vlan_list(self, inputdata): - vlans = [] - - if inputdata == "ALL": - return ["ALL"] - elif inputdata == "NONE": - return [] - else: - # Parse the complex list - groups = inputdata.split(',') - for group in groups: - subgroups = group.split('-') - if len(subgroups) == 1: - vlans.append(int(subgroups[0])) - elif len(subgroups) == 2: - for i in range (int(subgroups[0]), int(subgroups[1]) + 1): - vlans.append(i) - else: - logging.debug("Can't parse group \"" + group + "\"") - - return vlans - - def _get_port_vlans(self, port): - vlan_text = None - - vlan_part_re = re.compile('vlan participation include (.*)') - - try: - self._cli("show running-config interface %s" % port) - for line in self._read_long_output("show running-config interface"): - match = vlan_part_re.match(line) - if match: - if vlan_text != None: - vlan_text += "," - vlan_text += (match.group(1)) - else: - vlan_text = match.group(1) - - if vlan_text is None: - return [1] - else: - vlans = self._parse_vlan_list(vlan_text) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_port_vlans(port) - - # Wrapper around connection.send - by default, expect() the same - # text we've sent, to remove it from the output from the - # switch. For the few cases where we don't need that, override - # this using echo=False. - # Horrible, but seems to work. - def _cli(self, text, echo=True): - self.connection.send(text + '\r') - if echo: - try: - self.connection.expect(text) - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_out(text) - raise PExpectError("_cli failed on %s" % text) - except: - logging.error("Unexpected error: %s", sys.exc_info()[0]) - raise - -if __name__ == "__main__": - - # Simple test harness - exercise the main working functions above to verify - # they work. This does *NOT* test really disruptive things like "save - # running-config" and "reload" - test those by hand. - - import optparse - - switch = 'vlandswitch05' - parser = optparse.OptionParser() - parser.add_option("--switch", - dest = "switch", - action = "store", - nargs = 1, - type = "string", - help = "specify switch to connect to for testing", - metavar = "") - (opts, args) = parser.parse_args() - if opts.switch: - switch = opts.switch - - logging.basicConfig(level = logging.DEBUG, - format = '%(asctime)s %(levelname)-8s %(message)s') - p = NetgearXSM(switch, 23, debug=True) - p.switch_connect('admin', '', None) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.vlan_get_name(2) - print "VLAN 2 is named \"%s\"" % buf - - print "Create VLAN 3" - p.vlan_create(3) - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "Set name of VLAN 3 to test333" - p.vlan_set_name(3, "test333") - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - print "Destroy VLAN 3" - p.vlan_destroy(3) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.port_get_mode("1/0/10") - print "Port 1/0/10 is in %s mode" % buf - - buf = p.port_get_mode("1/0/11") - print "Port 1/0/11 is in %s mode" % buf - - # Test access stuff - print "Set 1/0/9 to access mode" - p.port_set_mode("1/0/9", "access") - - print "Move 1/0/9 to VLAN 4" - p.port_set_access_vlan("1/0/9", 4) - - buf = p.port_get_access_vlan("1/0/9") - print "Read from switch: 1/0/9 is on VLAN %s" % buf - - print "Move 1/0/9 back to VLAN 1" - p.port_set_access_vlan("1/0/9", 1) - - print "Create VLAN 2" - p.vlan_create(2) - - print "Create VLAN 3" - p.vlan_create(3) - - print "Create VLAN 4" - p.vlan_create(4) - - # Test access stuff - print "Set 1/0/9 to trunk mode" - p.port_set_mode("1/0/9", "trunk") - print "Read from switch: which VLANs is 1/0/9 on?" - buf = p.port_get_trunk_vlan_list("1/0/9") - p.dump_list(buf) - - # The adds below are NOOPs in effect on this switch - no filtering - # for "trunk" ports - print "Add 1/0/9 to VLAN 2" - p.port_add_trunk_to_vlan("1/0/9", 2) - print "Add 1/0/9 to VLAN 3" - p.port_add_trunk_to_vlan("1/0/9", 3) - print "Add 1/0/9 to VLAN 4" - p.port_add_trunk_to_vlan("1/0/9", 4) - print "Read from switch: which VLANs is 1/0/9 on?" - buf = p.port_get_trunk_vlan_list("1/0/9") - p.dump_list(buf) - - # And the same for removals here - p.port_remove_trunk_from_vlan("1/0/9", 3) - p.port_remove_trunk_from_vlan("1/0/9", 2) - p.port_remove_trunk_from_vlan("1/0/9", 4) - print "Read from switch: which VLANs is 1/0/9 on?" - buf = p.port_get_trunk_vlan_list("1/0/9") - p.dump_list(buf) - -# print 'Restarting switch, to explicitly reset config' -# p.switch_restart() - -# p.switch_save_running_config() - -# p.switch_disconnect() -# p._show_config() diff --git a/drivers/TPLinkTLSG2XXX.py b/drivers/TPLinkTLSG2XXX.py deleted file mode 100644 index 5213d10..0000000 --- a/drivers/TPLinkTLSG2XXX.py +++ /dev/null @@ -1,695 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import logging -import sys -import re -import pexpect - -if __name__ == '__main__': - import os - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError, PExpectError -from drivers.common import SwitchDriver, SwitchErrors - -class TPLinkTLSG2XXX(SwitchDriver): - - connection = None - _username = None - _password = None - _enable_password = None - - _capabilities = [ - ] - - # Regexp of expected hardware information - fail if we don't see - # this - _expected_descr_re = re.compile(r'TL-SG2\d\d\d') - - def __init__(self, switch_hostname, switch_telnetport=23, debug=False): - SwitchDriver.__init__(self, switch_hostname, debug) - self._systemdata = [] - self.exec_string = "/usr/bin/telnet %s %d" % (switch_hostname, switch_telnetport) - self.errors = SwitchErrors() - - ################################ - ### Switch-level API functions - ################################ - - # Save the current running config into flash - we want config to - # remain across reboots - def switch_save_running_config(self): - try: - self._cli("copy running-config startup-config") - self.connection.expect("OK") - except (PExpectError, pexpect.EOF): - # recurse on error - self._switch_connect() - self.switch_save_running_config() - - # Restart the switch - we need to reload config to do a - # roll-back. Do NOT save running-config first if the switch asks - - # we're trying to dump recent changes, not save them. - # - # This will also implicitly cause a connection to be closed - def switch_restart(self): - self._cli("reboot") - index = self.connection.expect(['Daving current', 'Continue?']) - if index == 0: - self._cli("n") # No, don't save - self.connection.expect("Continue?") - - # Fall through - self._cli("y") # Yes, continue to reset - self.connection.close(True) - - # List the capabilities of the switch (and driver) - some things - # make no sense to abstract. Returns a dict of strings, each one - # describing an extra feature that that higher levels may care - # about - def switch_get_capabilities(self): - return self._capabilities - - ################################ - ### VLAN API functions - ################################ - - # Create a VLAN with the specified tag - def vlan_create(self, tag): - logging.debug("Creating VLAN %d", tag) - - try: - self._configure() - self._cli("vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - return - raise IOError("Failed to create VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_create(tag) - - # Destroy a VLAN with the specified tag - def vlan_destroy(self, tag): - logging.debug("Destroying VLAN %d", tag) - - try: - self._configure() - self._cli("no vlan %d" % tag) - self._end_configure() - - # Validate it happened - vlans = self.vlan_get_list() - for vlan in vlans: - if vlan == tag: - raise IOError("Failed to destroy VLAN %d" % tag) - - except PExpectError: - # recurse on error - self._switch_connect() - self.vlan_destroy(tag) - - # Set the name of a VLAN - def vlan_set_name(self, tag, name): - logging.debug("Setting name of VLAN %d to %s", tag, name) - - try: - self._configure() - self._cli("vlan %d" % tag) - self._cli("name %s" % name) - self._end_configure() - - # Validate it happened - read_name = self.vlan_get_name(tag) - if read_name != name: - raise IOError("Failed to set name for VLAN %d (name found is \"%s\", not \"%s\")" - % (tag, read_name, name)) - - except (PExpectError, pexpect.EOF): - # recurse on error - self._switch_connect() - self.vlan_set_name(tag, name) - - # Get a list of the VLAN tags currently registered on the switch - def vlan_get_list(self): - logging.debug("Grabbing list of VLANs") - - try: - vlans = [] - regex = re.compile(r'^ *(\d+).*active') - - self._cli("show vlan brief") - for line in self._read_long_output("show vlan brief"): - match = regex.match(line) - if match: - vlans.append(int(match.group(1))) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_list() - - # For a given VLAN tag, ask the switch what the associated name is - def vlan_get_name(self, tag): - logging.debug("Grabbing the name of VLAN %d", tag) - - try: - name = None - regex = re.compile(r'^ *\d+\s+(\S+).*(active)') - self._cli("show vlan id %d" % tag) - for line in self._read_long_output("show vlan id"): - match = regex.match(line) - if match: - name = match.group(1) - name.strip() - return name - - except PExpectError: - # recurse on error - self._switch_connect() - return self.vlan_get_name(tag) - - ################################ - ### Port API functions - ################################ - - # Set the mode of a port: access or trunk - def port_set_mode(self, port, mode): - logging.debug("Setting port %s to %s", port, mode) - if not self._is_port_mode_valid(mode): - raise IndexError("Port mode %s is not allowed" % mode) - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - # This switch does not support specific modes, so we can't - # actually change the mode directly. However, we can and - # should deal with the PVID and memberships of existing VLANs - - try: - # We define a trunk to be on *all* VLANs on the switch in - # tagged mode, and PVID should match the default VLAN (1). - if mode == "trunk": - # Disconnect all the untagged ports - read_vlans = self._port_get_all_vlans(port, 'Untagged') - for vlan in read_vlans: - self._port_remove_general_vlan(port, vlan) - - # And move to VLAN 1 - self.port_add_trunk_to_vlan(port, 1) - self._set_pvid(port, 1) - - # And an access port should only be on one VLAN. Move to - # VLAN 1, untagged, and set PVID there. - if mode == "access": - # Disconnect all the ports - read_vlans = self._port_get_all_vlans(port, 'Untagged') - for vlan in read_vlans: - self._port_remove_general_vlan(port, vlan) - read_vlans = self._port_get_all_vlans(port, 'Tagged') - for vlan in read_vlans: - self._port_remove_general_vlan(port, vlan) - - # And move to VLAN 1 - self.port_set_access_vlan(port, 1) - self._set_pvid(port, 1) - - # Validate it happened - read_mode = self._port_get_mode(port) - if read_mode != mode: - raise IOError("Failed to set mode for port %s" % port) - - # And cache the result - self._port_modes[port] = mode - - except (PExpectError, pexpect.EOF): - # recurse on error - self._switch_connect() - self.port_set_mode(port, mode) - - # Set an access port to be in a specified VLAN (tag) - def port_set_access_vlan(self, port, tag): - logging.debug("Setting access port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - # Does the VLAN already exist? - vlan_list = self.vlan_get_list() - if not tag in vlan_list: - raise IndexError("VLAN tag %d not recognised" % tag) - - try: - # Add the new VLAN - self._configure() - self._cli("interface %s" % self._long_port_name(port)) - self._cli("switchport general allowed vlan %d untagged" % tag) - self._cli("no shutdown") - self._end_configure() - - self._set_pvid(port, tag) - - # Now drop all the other VLANs - read_vlans = self._port_get_all_vlans(port, 'Untagged') - for vlan in read_vlans: - if vlan != tag: - self._port_remove_general_vlan(port, vlan) - - # Finally, validate things worked - read_vlan = int(self.port_get_access_vlan(port)) - if read_vlan != tag: - raise IOError("Failed to move access port %s to VLAN %d - got VLAN %d instead" - % (port, tag, read_vlan)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_set_access_vlan(port, tag) - - # Add a trunk port to a specified VLAN (tag) - def port_add_trunk_to_vlan(self, port, tag): - logging.debug("Adding trunk port %s to VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - try: - self._configure() - self._cli("interface %s" % self._long_port_name(port)) - self._cli("switchport general allowed vlan %d tagged" % tag) - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag or vlan == "ALL": - return - raise IOError("Failed to add trunk port %s to VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_add_trunk_to_vlan(port, tag) - - # Remove a trunk port from a specified VLAN (tag) - def port_remove_trunk_from_vlan(self, port, tag): - logging.debug("Removing trunk port %s from VLAN %d", port, tag) - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - - try: - self._configure() - self._cli("interface %s" % self._long_port_name(port)) - self._cli("no switchport general allowed vlan %d" % tag) - self._end_configure() - - # Validate it happened - read_vlans = self.port_get_trunk_vlan_list(port) - for vlan in read_vlans: - if vlan == tag: - raise IOError("Failed to remove trunk port %s from VLAN %d" % (port, tag)) - - except PExpectError: - # recurse on error - self._switch_connect() - self.port_remove_trunk_from_vlan(port, tag) - - # Get the configured VLAN tag for an access port (i.e. a port not - # configured for tagged egress) - def port_get_access_vlan(self, port): - logging.debug("Getting VLAN for access port %s", port) - vlan = 1 - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - regex = re.compile(r'(\d+)\s+.*Untagged') - - try: - self._cli("show interface switchport %s" % self._long_port_name(port)) - for line in self._read_long_output("show interface switchport"): - match = regex.match(line) - if match: - vlan = match.group(1) - return int(vlan) - - except PExpectError: - # recurse on error - self._switch_connect() - return self.port_get_access_vlan(port) - - # Get the list of configured VLAN tags for a trunk port - def port_get_trunk_vlan_list(self, port): - logging.debug("Getting VLANs for trunk port %s", port) - - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - - return self._port_get_all_vlans(port, 'Tagged') - - ################################ - ### Internal functions - ################################ - - # Connect to the switch and log in - def _switch_connect(self): - - if not self.connection is None: - self.connection.close(True) - self.connection = None - - logging.debug("Connecting to Switch with: %s", self.exec_string) - self.connection = pexpect.spawn(self.exec_string, logfile=self.logger) - self._login() - - # No way to avoid paged output on this switch AFAICS - - # And grab details about the switch. in case we need it - self._get_systemdata() - - # And also validate them - make sure we're driving a switch of - # the correct model! Also store the serial number - descr_regex = re.compile(r'Hardware Version\s+ - (.*)') - descr = "" - - for line in self._systemdata: - match = descr_regex.match(line) - if match: - descr = match.group(1) - - logging.debug("system description is %s", descr) - - if not self._expected_descr_re.match(descr): - raise IOError("Switch %s not recognised by this driver: abort" % descr) - - # Now build a list of our ports, for later sanity checking - self._ports = self._get_port_names() - if len(self._ports) < 4: - raise IOError("Not enough ports detected - problem!") - - def _login(self): - logging.debug("attempting login with username \"%s\", password \"%s\", enable_password \"%s\"", self._username, self._password, self._enable_password) - self.connection.expect('User Access Login') - if self._username is not None: - self.connection.expect("User:") - self._cli("%s" % self._username) - if self._password is not None: - self.connection.expect("Password:") - self._cli("%s" % self._password, False) - while True: - index = self.connection.expect(['User Name:', 'Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) - if index != 4: # Any other means: failed to log in! - logging.error("Login failure: index %d\n", index) - logging.error("Login failure: %s\n", self.connection.match.before) - raise IOError - - # else - self._prompt_name = re.escape(self.connection.match.group(1).strip()) - if self.connection.match.group(2) == ">": - # Need to enter "enable" mode too - self._cli("") - self._cli("enable") - if self._enable_password is not None and len(self._enable_password) > 0: - self.connection.expect("Password:") - self._cli("%s" % self._enable_password, False) - index = self.connection.expect(['Password:', 'Bad passwords', 'authentication failed', r'(.*)(#|>)']) - if index != 3: # Any other means: failed to log in! - logging.error("Enable password failure: %s\n", self.connection.match) - raise IOError - return 0 - - def _logout(self): - logging.debug("Logging out") - self._cli("exit", False) - self.connection.close(True) - - def _configure(self): - self._cli("configure") - - def _end_configure(self): - self._cli("end") - - def _read_long_output(self, text): - longbuf = [] - prompt = self._prompt_name + '#' - while True: - try: - index = self.connection.expect(['^Press any key to continue', prompt]) - if index == 0: # "Press any key to continue (Q to quit)" - for line in self.connection.before.split('\r\n'): - line1 = re.sub('(\x08|\x0D)*', '', line.strip()) - longbuf.append(line1) - self._cli(' ', False) - elif index == 1: # Back to a prompt, says output is finished - break - except (pexpect.EOF, pexpect.TIMEOUT): - # Something went wrong; logout, log in and try again! - logging.error("PEXPECT FAILURE, RECONNECT") - self.errors.log_error_in(text) - raise PExpectError("_read_long_output failed") - except: - logging.error("prompt is \"%s\"", prompt) - raise - - for line in self.connection.before.split('\r\n'): - line1 = re.sub('(\x08|\x0D)*', '', line.strip()) - longbuf.append(line1) - - return longbuf - - def _long_port_name(self, port): - return re.sub('Gi', 'gigabitEthernet ', port) - - def _set_pvid(self, port, pvid): - # Set a port's PVID - self._configure() - self._cli("interface %s" % self._long_port_name(port)) - self._cli("switchport pvid %d" % pvid) - self._end_configure() - - def _port_get_all_vlans(self, port, port_type): - vlans = [] - regex = re.compile(r'(\d+)\s+.*' + port_type) - - try: - self._cli("show interface switchport %s" % self._long_port_name(port)) - for line in self._read_long_output("show interface switchport"): - match = regex.match(line) - if match: - vlan = match.group(1) - vlans.append(int(vlan)) - return vlans - - except PExpectError: - # recurse on error - self._switch_connect() - return self._port_get_all_vlans(port, port_type) - - def _port_remove_general_vlan(self, port, tag): - try: - self._configure() - self._cli("interface %s" % self._long_port_name(port)) - self._cli("no switchport general allowed vlan %d" % tag) - self._end_configure() - - except PExpectError: - # recurse on error - self._switch_connect() - return self._port_remove_general_vlan(port, tag) - - def _get_port_names(self): - logging.debug("Grabbing list of ports") - interfaces = [] - - # Use "Link" to only identify lines in the output that match - # interfaces that exist - it'll match "LinkUp" and "LinkDown" - regex = re.compile(r'^\s*([a-zA-Z0-9_/]*).*Link') - - try: - self._cli("show interface status") - for line in self._read_long_output("show interface status"): - match = regex.match(line) - if match: - interface = match.group(1) - interfaces.append(interface) - self._port_numbers[interface] = len(interfaces) - logging.debug(" found %d ports on the switch", len(interfaces)) - return interfaces - - except PExpectError: - # recurse on error - self._switch_connect() - return self._get_port_names() - - # Get the mode of a port: access or trunk - def _port_get_mode(self, port): - logging.debug("Getting mode of port %s", port) - - if not self._is_port_name_valid(port): - raise IndexError("Port name %s not recognised" % port) - - # This switch does not support specific modes, so we have to - # make stuff up here. We define trunk ports to be on (1 or - # many) tagged VLANs, anything not tagged to be access. - read_vlans = self._port_get_all_vlans(port, 'Tagged') - if len(read_vlans) > 0: - return "trunk" - else: - return "access" - - def _show_config(self): - logging.debug("Grabbing config") - self._cli("show running-config") - return self._read_long_output("show running-config") - - def _show_clock(self): - logging.debug("Grabbing time") - self._cli("show system-time") - return self._read_long_output("show system-time") - - def _get_systemdata(self): - logging.debug("Grabbing system sw and hw versions") - - self._cli("show system-info") - self._systemdata = [] - for line in self._read_long_output("show system-info"): - self._systemdata.append(line) - - # Wrapper around connection.send - by default, expect() the same - # text we've sent, to remove it from the output from the - # switch. For the few cases where we don't need that, override - # this using echo=False. - # Horrible, but seems to work. - def _cli(self, text, echo=True): - self.connection.send(text + '\r') - if echo: - self.connection.expect(text) - -if __name__ == "__main__": - - # Simple test harness - exercise the main working functions above to verify - # they work. This does *NOT* test really disruptive things like "save - # running-config" and "reload" - test those by hand. - - import optparse - - switch = '10.172.2.50' - parser = optparse.OptionParser() - parser.add_option("--switch", - dest = "switch", - action = "store", - nargs = 1, - type = "string", - help = "specify switch to connect to for testing", - metavar = "") - (opts, args) = parser.parse_args() - if opts.switch: - switch = opts.switch - - logging.basicConfig(level = logging.DEBUG, - format = '%(asctime)s %(levelname)-8s %(message)s') - p = TPLinkTLSG2XXX(switch, 23, debug = False) - p.switch_connect('admin', 'admin', None) - - print "Ports are:" - buf = p.switch_get_port_names() - p.dump_list(buf) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.vlan_get_name(2) - print "VLAN 2 is named \"%s\"" % buf - - print "Create VLAN 3" - p.vlan_create(3) - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "Set name of VLAN 3 to test333" - p.vlan_set_name(3, "test333") - - buf = p.vlan_get_name(3) - print "VLAN 3 is named \"%s\"" % buf - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - print "Destroy VLAN 3" - p.vlan_destroy(3) - - print "VLANs are:" - buf = p.vlan_get_list() - p.dump_list(buf) - - buf = p.port_get_mode("Gi1/0/10") - print "Port Gi1/0/10 is in %s mode" % buf - - buf = p.port_get_mode("Gi1/0/11") - print "Port Gi1/0/11 is in %s mode" % buf - - # Test access stuff - buf = p.port_get_mode("Gi1/0/9") - print "Port Gi1/0/9 is in %s mode" % buf - - print "Set Gi1/0/9 to access mode" - p.port_set_mode("Gi1/0/9", "access") - - print "Move Gi1/0/9 to VLAN 4" - p.port_set_access_vlan("Gi1/0/9", 4) - - buf = p.port_get_access_vlan("Gi1/0/9") - print "Read from switch: Gi1/0/9 is on VLAN %s" % buf - - print "Move Gi1/0/9 back to VLAN 1" - p.port_set_access_vlan("Gi1/0/9", 1) - - # Test access stuff - print "Set Gi1/0/9 to trunk mode" - p.port_set_mode("Gi1/0/9", "trunk") - print "Read from switch: which VLANs is Gi1/0/9 on?" - buf = p.port_get_trunk_vlan_list("Gi1/0/9") - p.dump_list(buf) - print "Add Gi1/0/9 to VLAN 2" - p.port_add_trunk_to_vlan("Gi1/0/9", 2) - print "Add Gi1/0/9 to VLAN 3" - p.port_add_trunk_to_vlan("Gi1/0/9", 3) - print "Add Gi1/0/9 to VLAN 4" - p.port_add_trunk_to_vlan("Gi1/0/9", 4) - print "Read from switch: which VLANs is Gi1/0/9 on?" - buf = p.port_get_trunk_vlan_list("Gi1/0/9") - p.dump_list(buf) - - p.port_remove_trunk_from_vlan("Gi1/0/9", 3) - p.port_remove_trunk_from_vlan("Gi1/0/9", 3) - p.port_remove_trunk_from_vlan("Gi1/0/9", 4) - print "Read from switch: which VLANs is Gi1/0/9 on?" - buf = p.port_get_trunk_vlan_list("Gi1/0/9") - p.dump_list(buf) - - - p.switch_save_running_config() - - p.switch_disconnect() -# p._show_config() diff --git a/drivers/__init__.py b/drivers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/drivers/common.py b/drivers/common.py deleted file mode 100644 index e564c9e..0000000 --- a/drivers/common.py +++ /dev/null @@ -1,167 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -import time -import logging - -from errors import InputError, PExpectError - -class SwitchErrors: - """ Error logging and statistics class """ - - def __init__(self): - self.errors_in = 0 - self.errors_out = 0 - - def __repr__(self): - return "" % (self.errors_in, self.errors_out) - - # For now, just count the error. Later on we might add stats and - # analysis - def log_error_in(self, text): - self.errors_in += 1 - - # For now, just count the error. Later on we might add stats and - # analysis - def log_error_out(self, text): - self.errors_out += 1 - -class SwitchDriver(object): - - connection = None - hostname = "" - serial_number = '' - - _allowed_port_modes = [ "trunk", "access" ] - _ports = [] - _port_modes = {} - _port_numbers = {} - _prompt_name = '' - _username = '' - _password = '' - _enable_password = '' - _systemdata = [] - - def __init__ (self, switch_hostname, debug): - - if debug: - # Configure logging for pexpect output if we have debug - # enabled - - # get the logger - self.logger = logging.getLogger(switch_hostname) - - # give the logger the methods required by pexpect - self.logger.write = self._log_write - self.logger.flush = self._log_do_nothing - - else: - self.logger = None - - self.hostname = switch_hostname - - # Connect to the switch and log in - def switch_connect(self, username, password, enablepassword): - self._username = username - self._password = password - self._enable_password = enablepassword - self._switch_connect() - - # Log out of the switch and drop the connection and all state - def switch_disconnect(self): - self._logout() - logging.debug("Closing connection to %s", self.hostname) - self._ports = [] - self._port_modes.clear() - self._port_numbers.clear() - self._prompt_name = '' - self._systemdata = [] - del(self) - - def dump_list(self, data): - i = 0 - for line in data: - print "%d: \"%s\"" % (i, line) - i += 1 - - def _delay(self): - time.sleep(0.5) - - # List the capabilities of the switch (and driver) - some things - # make no sense to abstract. Returns a dict of strings, each one - # describing an extra feature that that higher levels may care - # about - def switch_get_capabilities(self): - return self._capabilities - - # List the names of all the ports on the switch - def switch_get_port_names(self): - return self._ports - - def _is_port_name_valid(self, name): - for port in self._ports: - if name == port: - return True - return False - - def _is_port_mode_valid(self, mode): - for allowed in self._allowed_port_modes: - if allowed == mode: - return True - return False - - # Try to look up a port mode in our cache. If not there, go ask - # the switch and cache the result - def port_get_mode(self, port): - if not self._is_port_name_valid(port): - raise InputError("Port name %s not recognised" % port) - if port in self._port_modes: - logging.debug("port_get_mode: returning mode %s from cache for port %s", self._port_modes[port], port) - return self._port_modes[port] - else: - mode = self._port_get_mode(port) - self._port_modes[port] = mode - logging.debug("port_get_mode: found mode %s for port %s, adding to cache", self._port_modes[port], port) - return mode - - def port_map_name_to_number(self, port_name): - if not self._is_port_name_valid(port_name): - raise InputError("Port name %s not recognised" % port_name) - logging.debug("port_map_name_to_number: returning %d for port_name %s", self._port_numbers[port_name], port_name) - return self._port_numbers[port_name] - - # Wrappers to adapt logging for pexpect when we've configured on a - # switch. - # This will be the method called by the pexpect object to write a - # log message - def _log_write(self, *args, **kwargs): - # ignore other parameters, pexpect only uses one arg - content = args[0] - - if content in [' ', '', '\n', '\r', '\r\n']: - return # don't log empty lines - - # Split the output into multiple lines so we get a - # well-formatted logfile - for line in content.split('\r\n'): - logging.info(line) - - # This is the flush method for pexpect - def _log_do_nothing(self): - pass diff --git a/errors.py b/errors.py deleted file mode 100644 index 6759e50..0000000 --- a/errors.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2014-2016 Linaro Limited -# Authors: Dave Pigott , -# Steve McIntyre -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -class VlandError(Exception): - """ - Base exception and error class for the vlan daemon - """ - - -class CriticalError(VlandError): - """ - The critical error - """ - -class NotFoundError(VlandError): - """ - Couldn't find object - """ - -class InputError(VlandError): - """ - Invalid input - """ - -class ConfigError(VlandError): - """ - Invalid configuration - """ - -class SocketError(VlandError): - """ - Socket connection failure - """ - -class PExpectError(VlandError): - """ - CLI communication failure - """ - -class Error: - OK = 0 - FAILED = 1 - NOTFOUND = 2 diff --git a/ipc/__init__.py b/ipc/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ipc/client-new.py b/ipc/client-new.py deleted file mode 100644 index 8834f9d..0000000 --- a/ipc/client-new.py +++ /dev/null @@ -1,18 +0,0 @@ -import socket -import time -import json -from ipc import VlanIpc - -host = 'localhost' # The remote host -port = 3080 # The same port as used by the server - -s = VlanIpc() -s.client_connect(host, port) -msg = {"group": "group1", "client_name": "client1", "request": "lava_sync", "message": "bye bye world"} -print "Sending to server:" -print msg -s.client_send(msg) -ret = s.client_recv_and_close() -print "Server said in reply: " -print ret - diff --git a/ipc/ipc.py b/ipc/ipc.py deleted file mode 100644 index 8df1219..0000000 --- a/ipc/ipc.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright 2014-2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# Simple VLANd IPC module - -import socket -import json -import time -import datetime -import os -import sys -import logging - -vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) -sys.path.insert(0, vlandpath) -from errors import CriticalError, InputError, ConfigError, SocketError - -class VlanIpc: - """VLANd IPC class""" - - def __init__(self): - self.conn = None - self.socket = None - - def server_init(self, host, port): - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.conn = None - - while True: - try: - self.socket.bind((host, port)) - break - except socket.error as e: - print "Can't bind to port %d: %s" % (port, e) - time.sleep(1) - - def server_listen(self): - if self.socket is None: - raise SocketError("Server can't receive data: no socket") - self.socket.listen(1) - - def server_recv(self): - if self.socket is None: - raise SocketError("Server can't receive data: no socket") - - self.conn, addr = self.socket.accept() - logging.debug("server: Connection from") - logging.debug(addr) - data = self.conn.recv(8) # 32bit limit - count = int(data, 16) - c = 0 - data = '' - while c < count: - data += self.conn.recv(1) - c += 1 - try: - json_data = json.loads(data) - except ValueError: - self.conn.close() - self.conn = None - raise SocketError("Server unable to decode receieved data: corrupt?") - - if 'client_name' not in json_data: - self.conn.close() - self.conn = None - raise SocketError("Server unable to detect client name: corrupt packet?") - - return json_data - - def server_reply(self, json_data): - if self.conn is None: - raise SocketError("Server can't send data: no connection") - - data = self._format_message(json_data) - if not data: - self.conn.close() - self.conn = None - raise SocketError("Server unable to format reply data") - - try: - # send the actual number of bytes to read. - self.conn.send(data[0]) - # now send the bytes. - self.conn.send(data[1]) - except socket.error as e: - logging.error("Can't send response to client: %s", e) - logging.error("Was trying to send data:") - logging.error(data) - - def server_close(self): - if self.conn is not None: - self.conn.shutdown(socket.SHUT_RDWR) - self.conn.close() - - def client_connect(self, host, port): - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - while True: - try: - ret = self.socket.connect_ex((host, port)) - if ret: - self.socket.close() - self.socket = None - raise SocketError("Client can't send connect: %s" % ret) - else: - break - except socket.error: - time.sleep(1) - return True - - def client_send(self, json_data): - if self.socket is None: - raise SocketError("Client can't send data: no socket") - - data = self._format_message(json_data) - if not data: - self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() - self.socket = None - raise SocketError("Client unable to send data") - - # send the actual number of bytes to read. - self.socket.send(data[0]) - # now send the bytes. - self.socket.send(data[1]) - - def client_recv_and_close(self): - if self.socket is None: - raise SocketError("Client can't receieve data: no socket") - - data = self.socket.recv(8) # 32bit limit - count = int(data, 16) - c = 0 - data = '' - while c < count: - data += self.socket.recv(1) - c += 1 - try: - json_data = json.loads(data) - except ValueError: - self.socket.close() - self.socket = None - raise SocketError("Client unable to decode receieved data: corrupt?") - - self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() - - return json_data - - # The default JSON serialiser code can't deal with datetime - # objects by default, so let's tell it how to. - def _json_serial(self, obj): - """JSON serializer for objects not serialisable by default json code""" - if isinstance(obj, datetime.datetime): - serial = obj.isoformat() - return serial - - def _format_message(self, json_data): - try: - msgstr = json.dumps(json_data, default=self._json_serial) - except ValueError: - return None - # "header" calculation - msglen = "%08X" % len(msgstr) - return (msglen, msgstr) diff --git a/ipc/server-new.py b/ipc/server-new.py deleted file mode 100644 index e398dcc..0000000 --- a/ipc/server-new.py +++ /dev/null @@ -1,24 +0,0 @@ -# Echo server test program -import socket -import time -import json -from ipc import VlanIpc - -host = 'localhost' # Symbolic name meaning the local host -port = 3080 # Arbitrary non-privileged port - -s = VlanIpc() -s.server_init(host, port) - -while True: - s.server_listen() - json_data = s.server_recv() - - print "client sent us:" - print json_data - - response = {'response': 'ack'} - print "sending reply:" - print response - - s.server_reply(response) diff --git a/util.py b/util.py deleted file mode 100644 index 738eb00..0000000 --- a/util.py +++ /dev/null @@ -1,871 +0,0 @@ -# Copyright 2014-2018 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# Utility routines, including handling of API functions -# - -import logging -import time -from errors import CriticalError, NotFoundError, InputError, ConfigError, SocketError - -class VlanUtil: - """VLANd utility functions""" - - def set_logging_level(self, level): - loglevel = logging.CRITICAL - if level == "ERROR": - loglevel = logging.ERROR - elif level == "WARNING": - loglevel = logging.WARNING - elif level == "INFO": - loglevel = logging.INFO - elif level == "DEBUG": - loglevel = logging.DEBUG - return loglevel - - def get_switch_driver(self, switch_name, config): - logging.debug("Trying to find a driver for %s", switch_name) - driver = config.switches[switch_name].driver - logging.debug("Driver: %s", driver) - module = __import__("drivers.%s" % driver, fromlist=[driver]) - class_ = getattr(module, driver) - return class_(switch_name, debug = config.switches[switch_name].debug) - - def probe_switches(self, state): - config = state.config - ret = {} - for switch_name in sorted(config.switches): - logging.debug("Found switch %s:", switch_name) - logging.debug(" Probing...") - - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - ret[switch_name] = 'Found %d ports: ' % len(s.switch_get_port_names()) - for name in s.switch_get_port_names(): - ret[switch_name] += '%s ' % name - s.switch_disconnect() - del s - return ret - - # Simple helper wrapper for all the read-only database queries - def perform_db_query(self, state, command, data): - logging.debug('perform_db_query') - logging.debug(command) - logging.debug(data) - ret = {} - db = state.db - try: - if command == 'db.all_switches': - ret = db.all_switches() - elif command == 'db.all_ports': - ret = db.all_ports() - elif command == 'db.all_vlans': - ret = db.all_vlans() - elif command == 'db.all_trunks': - ret = db.all_trunks() - elif command == 'db.get_switch_by_id': - ret = db.get_switch_by_id(data['switch_id']) - elif command == 'db.get_switch_id_by_name': - ret = db.get_switch_id_by_name(data['name']) - elif command == 'db.get_switch_name_by_id': - ret = db.get_switch_name_by_id(data['switch_id']) - elif command == 'db.get_port_by_id': - ret = db.get_port_by_id(data['port_id']) - elif command == 'db.get_ports_by_switch': - ret = db.get_ports_by_switch(data['switch_id']) - elif command == 'db.get_port_by_switch_and_name': - ret = db.get_port_by_switch_and_name(data['switch_id'], data['name']) - elif command == 'db.get_port_by_switch_and_number': - ret = db.get_port_by_switch_and_number(data['switch_id'], int(data['number'])) - elif command == 'db.get_current_vlan_id_by_port': - ret = db.get_current_vlan_id_by_port(data['port_id']) - elif command == 'db.get_base_vlan_id_by_port': - ret = db.get_base_vlan_id_by_port(data['port_id']) - elif command == 'db.get_ports_by_current_vlan': - ret = db.get_ports_by_current_vlan(data['vlan_id']) - elif command == 'db.get_ports_by_base_vlan': - ret = db.get_ports_by_base_vlan(data['vlan_id']) - elif command == 'db.get_port_mode': - ret = db.get_port_mode(data['port_id']) - elif command == 'db.get_ports_by_trunk': - ret = db.get_ports_by_trunk(data['trunk_id']) - elif command == 'db.get_vlan_by_id': - ret = db.get_vlan_by_id(data['vlan_id']) - elif command == 'db.get_vlan_tag_by_id': - ret = db.get_vlan_tag_by_id(data['vlan_id']) - elif command == 'db.get_vlan_id_by_name': - ret = db.get_vlan_id_by_name(data['name']) - elif command == 'db.get_vlan_id_by_tag': - ret = db.get_vlan_id_by_tag(data['tag']) - elif command == 'db.get_vlan_name_by_id': - ret = db.get_vlan_name_by_id(data['vlan_id']) - elif command == 'db.get_trunk_by_id': - ret = db.get_trunk_by_id(data['trunk_id']) - else: - raise InputError("Unknown db_query command \"%s\"" % command) - - except (InputError, NotFoundError) as e: - logging.error('perform_db_query(%s) got error %s', command, e) - raise - except ValueError as e: - logging.error('perform_db_query(%s) got error %s', command, e) - raise InputError("Invalid value in API call argument: %s" % e) - except TypeError as e: - logging.error('perform_db_query(%s) got error %s', command, e) - raise InputError("Invalid type in API call argument: %s" % e) - - return ret - - # Simple helper wrapper for all the read-only daemon state queries - def perform_daemon_query(self, state, command, data): - logging.debug('perform_daemon_query') - logging.debug(command) - logging.debug(data) - ret = {} - try: - if command == 'daemon.status': - # data ignored - ret['running'] = 'ok' - ret['last_modified'] = state.db.get_last_modified_time() - elif command == 'daemon.version': - # data ignored - ret['version'] = state.version - elif command == 'daemon.statistics': - ret['uptime'] = time.time() - state.starttime - elif command == 'daemon.probe_switches': - ret = self.probe_switches(state) - elif command == 'daemon.shutdown': - # data ignored - ret['shutdown'] = 'Shutting down' - state.running = False - else: - raise InputError("Unknown daemon_query command \"%s\"" % command) - - except (InputError, NotFoundError) as e: - logging.error('perform_daemon_query(%s) got error %s', command, e) - raise - except ValueError as e: - logging.error('perform_daemon_query(%s) got error %s', command, e) - raise InputError("Invalid value in API call argument: %s" % e) - except TypeError as e: - logging.error('perform_daemon_query(%s) got error %s', command, e) - raise InputError("Invalid type in API call argument: %s" % e) - - return ret - - # Helper wrapper for API functions modifying database state only - def perform_db_update(self, state, command, data): - logging.debug('perform_db_update') - logging.debug(command) - logging.debug(data) - ret = {} - db = state.db - try: - if command == 'db.create_switch': - ret = db.create_switch(data['name']) - elif command == 'db.create_port': - try: - number = int(data['number']) - except ValueError: - raise InputError("Invalid value for port number (%s) - must be numeric only!" % data['number']) - ret = db.create_port(data['switch_id'], data['name'], - number, - state.default_vlan_id, - state.default_vlan_id) - elif command == 'db.create_trunk': - ret = db.create_trunk(data['port_id1'], data['port_id2']) - elif command == 'db.delete_switch': - ret = db.delete_switch(data['switch_id']) - elif command == 'db.delete_port': - ret = db.delete_port(data['port_id']) - elif command == 'db.set_port_is_locked': - ret = db.set_port_is_locked(data['port_id'], - data['is_locked'], - data['lock_reason']) - elif command == 'db.set_base_vlan': - ret = db.set_base_vlan(data['port_id'], data['base_vlan_id']) - elif command == 'db.delete_trunk': - ret = db.delete_trunk(data['trunk_id']) - else: - raise InputError("Unknown db_update command \"%s\"" % command) - - except (InputError, NotFoundError) as e: - logging.error('perform_db_update(%s) got error %s', command, e) - raise - except ValueError as e: - logging.error('perform_db_update(%s) got error %s', command, e) - raise InputError("Invalid value in API call argument: %s" % e) - except TypeError as e: - logging.error('perform_db_update(%s) got error %s', command, e) - raise InputError("Invalid type in API call argument: %s" % e) - - return ret - - # Helper wrapper for API functions that modify both database state - # and on-switch VLAN state - def perform_vlan_update(self, state, command, data): - logging.debug('perform_vlan_update') - logging.debug(command) - logging.debug(data) - ret = {} - - try: - # All of these are complex commands, so call helpers - # rather than inline the code here - if command == 'api.create_vlan': - ret = self.create_vlan(state, data['name'], int(data['tag']), data['is_base_vlan']) - elif command == 'api.delete_vlan': - ret = self.delete_vlan(state, int(data['vlan_id'])) - elif command == 'api.set_port_mode': - ret = self.set_port_mode(state, int(data['port_id']), data['mode']) - elif command == 'api.set_current_vlan': - ret = self.set_current_vlan(state, int(data['port_id']), int(data['vlan_id'])) - elif command == 'api.restore_base_vlan': - ret = self.restore_base_vlan(state, int(data['port_id'])) - elif command == 'api.auto_import_switch': - ret = self.auto_import_switch(state, data['switch']) - else: - raise InputError("Unknown query command \"%s\"" % command) - - except (InputError, NotFoundError) as e: - logging.error('perform_vlan_update(%s) got error %s', command, e) - raise - except ValueError as e: - logging.error('perform_vlan_update(%s) got error %s', command, e) - raise InputError("Invalid value in API call argument: %s" % e) - except TypeError as e: - logging.error('perform_vlan_update(%s) got error %s', command, e) - raise InputError("Invalid type in API call argument: %s" % e) - - return ret - - - # Complex call - # 1. create the VLAN in the DB - # 2. Iterate through all switches: - # a. Create the VLAN - # b. Add the VLAN to all trunk ports (if needed) - # 3. If all went OK, save config on all the switches - # - # The VLAN may already exist on some of the switches, that's - # fine. If things fail, we attempt to roll back by rebooting - # switches then removing the VLAN in the DB. - def create_vlan(self, state, name, tag, is_base_vlan): - - logging.debug('create_vlan') - db = state.db - config = state.config - - # Check for tag == -1, i.e. use the next available tag - if tag == -1: - tag = db.find_lowest_unused_vlan_tag() - logging.debug('create_vlan called with a tag of -1, found first unused tag %d', tag) - - # 1. Database record first - try: - logging.debug('Adding DB record first: name %s, tag %d, is_base_vlan %d', name, tag, is_base_vlan) - vlan_id = db.create_vlan(name, tag, is_base_vlan) - logging.debug('Added VLAN tag %d, name %s to the database, created VLAN ID %d', tag, name, vlan_id) - except (InputError, NotFoundError): - logging.debug('DB creation failed') - raise - - # Keep track of which switches we've configured, for later use - switches_done = [] - - # 2. Now the switches - try: - for switch in db.all_switches(): - trunk_ports = [] - switch_name = switch['name'] - try: - logging.debug('Adding new VLAN to switch %s', switch_name) - # Get the right driver - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - - # Mark this switch as one we've touched, for - # either config saving or rollback below - switches_done.append(switch_name) - - # 2a. Create the VLAN on the switch - s.vlan_create(tag) - s.vlan_set_name(tag, name) - logging.debug('Added VLAN tag %d, name %s to switch %s', tag, name, switch_name) - - # 2b. Do we need to worry about trunk ports on this switch? - if 'TrunkWildCardVlans' in s.switch_get_capabilities(): - logging.debug('This switch does not need special trunk port handling') - else: - logging.debug('This switch needs special trunk port handling') - trunk_ports = db.get_trunk_port_names_by_switch(switch['switch_id']) - if trunk_ports is None: - logging.debug("But it has no trunk ports defined") - trunk_ports = [] - else: - logging.debug('Found %d trunk_ports that need adjusting', len(trunk_ports)) - - # Modify any trunk ports as needed - for port in trunk_ports: - logging.debug('Adding VLAN tag %d, name %s to switch %s port %s', tag, name, switch_name, port) - s.port_add_trunk_to_vlan(port, tag) - - # And now we're done with this switch - s.switch_disconnect() - del s - - except IOError as e: - logging.error('Failed to add VLAN %d to switch ID %d (%s): %s', tag, switch['switch_id'], switch['name'], e) - raise - - except IOError: - # Bugger. Looks like one of the switch calls above - # failed. To undo the changes safely, we'll need to reset - # all the switches we managed to configure. This could - # take some time! - logging.error('create_vlan failed, resetting all switches to recover') - for switch_name in switches_done: - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - s.switch_restart() # Will implicitly also close the connection - del s - - # Undo the database change - logging.debug('Switch access failed. Deleting the new VLAN entry in the database') - db.delete_vlan(vlan_id) - raise - - # If we've got this far, things were successful. Save config - # on all the switches so it will persist across reboots - for switch_name in switches_done: - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - s.switch_save_running_config() - s.switch_disconnect() - del s - - return (vlan_id, tag) # If we're successful - - # Complex call - # 1. Check in the DB if there are any ports on the VLAN. Bail if so - # 2. Iterate through all switches: - # a. Remove the VLAN from all trunk ports (if needed) - # b. Remove the VLAN - # 3. If all went OK, save config on the switches - # 4. Remove the VLAN in the DB - # - # If things fail, we attempt to roll back by rebooting switches. - def delete_vlan(self, state, vlan_id): - - logging.debug('delete_vlan') - db = state.db - config = state.config - - # 1. Check for database records first - logging.debug('Checking for ports using VLAN ID %d', vlan_id) - vlan = db.get_vlan_by_id(vlan_id) - if vlan is None: - raise NotFoundError("VLAN ID %d does not exist" % vlan_id) - vlan_tag = vlan['tag'] - ports = db.get_ports_by_current_vlan(vlan_id) - if ports is not None: - raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % - (vlan_id, len(ports))) - ports = db.get_ports_by_base_vlan(vlan_id) - if ports is not None: - raise InputError("Cannot delete VLAN ID %d when it still has %d ports" % - (vlan_id, len(ports))) - - # Keep track of which switches we've configured, for later use - switches_done = [] - - # 2. Now the switches - try: - for switch in db.all_switches(): - switch_name = switch['name'] - trunk_ports = [] - try: - # Get the right driver - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - - # Mark this switch as one we've touched, for - # either config saving or rollback below - switches_done.append(switch_name) - - # 2a. Do we need to worry about trunk ports on this switch? - if 'TrunkWildCardVlans' in s.switch_get_capabilities(): - logging.debug('This switch does not need special trunk port handling') - else: - logging.debug('This switch needs special trunk port handling') - trunk_ports = db.get_trunk_port_names_by_switch(switch['switch_id']) - if trunk_ports is None: - logging.debug("But it has no trunk ports defined") - trunk_ports = [] - else: - logging.debug('Found %d trunk_ports that need adjusting', len(trunk_ports)) - - # Modify any trunk ports as needed - for port in trunk_ports: - s.port_remove_trunk_from_vlan(port, vlan_tag) - logging.debug('Removed VLAN tag %d from switch %s port %s', vlan_tag, switch_name, port) - - # 2b. Remove the VLAN from the switch - logging.debug('Removing VLAN tag %d from switch %s', vlan_tag, switch_name) - s.vlan_destroy(vlan_tag) - logging.debug('Removed VLAN tag %d from switch %s', vlan_tag, switch_name) - - # And now we're done with this switch - s.switch_disconnect() - del s - - except IOError: - raise - - except IOError: - # Bugger. Looks like one of the switch calls above - # failed. To undo the changes safely, we'll need to reset - # all the switches we managed to configure. This could - # take some time! - logging.error('delete_vlan failed, resetting all switches to recover') - for switch_name in switches_done: - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - s.switch_restart() # Will implicitly also close the connection - del s - raise - - # 3. If we've got this far, things were successful. Save - # config on all the switches so it will persist across reboots - for switch_name in switches_done: - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - s.switch_save_running_config() - s.switch_disconnect() - del s - - # 4. Finally, remove the VLAN in the DB - try: - logging.debug('Removing DB record: VLAN ID %d', vlan_id) - vlan_id = db.delete_vlan(vlan_id) - logging.debug('Removed VLAN ID %d from the database OK', vlan_id) - except (InputError, NotFoundError): - logging.debug('DB deletion failed') - raise - - return vlan_id # If we're successful - - # Complex call, depends on existing state a lot - # 1. Check validity of inputs - # 2. Switch mode and other config on the port. - # a. If switching trunk->access, remove all trunk VLANs from it - # (if needed) and switch back to the base VLAN for the - # port. Next, switch to access mode. - # b. If switching access->trunk, switch back to the base VLAN - # for the port. Next, switch mode. Then add all trunk VLANs - # to it (if needed) - # 3. If all went OK, save config on the switch - # 4. Change details of the port in the DB - # - # If things fail, we attempt to roll back by rebooting the switch - def set_port_mode(self, state, port_id, mode): - - logging.debug('set_port_mode') - db = state.db - config = state.config - - # 1. Sanity-check inputs - if mode != 'access' and mode != 'trunk': - raise InputError("Port mode '%s' is not a valid option: try 'access' or 'trunk'" % mode) - port = db.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % port_id) - if port['is_locked']: - raise InputError("Port ID %d is locked" % port_id) - if mode == 'trunk' and port['is_trunk']: - raise InputError("Port ID %d is already in trunk mode" % port_id) - if mode == 'access' and not port['is_trunk']: - raise InputError("Port ID %d is already in access mode" % port_id) - base_vlan_tag = db.get_vlan_tag_by_id(port['base_vlan_id']) - - # Get the right driver - switch_name = db.get_switch_name_by_id(port['switch_id']) - s = self.get_switch_driver(switch_name, config) - - # 2. Now start configuring the switch - try: - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - except: - logging.debug('Failed to talk to switch %s!', switch_name) - raise - - try: - if port['is_trunk']: - # 2a. We're going from a trunk port to an access port - if 'TrunkWildCardVlans' in s.switch_get_capabilities(): - logging.debug('This switch does not need special trunk port handling') - else: - logging.debug('This switch needs special trunk port handling') - vlans = s.port_get_trunk_vlan_list(port['name']) - if vlans is None: - logging.debug("But it has no VLANs defined on port %s", port['name']) - vlans = [] - else: - logging.debug('Found %d vlans that may need dropping on port %s', len(vlans), port['name']) - - for vlan in vlans: - if vlan != state.config.vland.default_vlan_tag: - s.port_remove_trunk_from_vlan(port['name'], vlan) - - s.port_set_mode(port['name'], "access") - - else: - # 2b. We're going from an access port to a trunk port - s.port_set_access_vlan(port['name'], base_vlan_tag) - s.port_set_mode(port['name'], "trunk") - if 'TrunkWildCardVlans' in s.switch_get_capabilities(): - logging.debug('This switch does not need special trunk port handling') - else: - vlans = db.all_vlans() - for vlan in vlans: - if vlan['tag'] != state.config.vland.default_vlan_tag: - s.port_add_trunk_to_vlan(port['name'], vlan['tag']) - - except IOError: - logging.error('set_port_mode failed, resetting switch to recover') - # Bugger. Looks like one of the switch calls above - # failed. To undo the changes safely, we'll need to reset - # all the config on this switch - s.switch_restart() # Will implicitly also close the connection - del s - raise - - # 3. All seems to have worked so far! - s.switch_save_running_config() - s.switch_disconnect() - del s - - # 4. And update the DB - db.set_port_mode(port_id, mode) - - return port_id # If we're successful - - # Complex call, updating both DB and switch state - # 1. Check validity of inputs - # 2. Update the port config on the switch - # 3. If all went OK, save config on the switch - # 4. Change details of the port in the DB - # - # If things fail, we attempt to roll back by rebooting the switch - def set_current_vlan(self, state, port_id, vlan_id): - - logging.debug('set_current_vlan') - db = state.db - config = state.config - - # 1. Sanity checks! - port = db.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % port_id) - if port['is_locked']: - raise InputError("Port ID %d is locked" % port_id) - if port['is_trunk']: - raise InputError("Port ID %d is not an access port" % port_id) - - vlan = db.get_vlan_by_id(vlan_id) - if vlan is None: - raise NotFoundError("VLAN ID %d does not exist" % vlan_id) - - # Get the right driver - switch_name = db.get_switch_name_by_id(port['switch_id']) - s = self.get_switch_driver(switch_name, config) - - # 2. Now start configuring the switch - try: - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - except: - logging.debug('Failed to talk to switch %s!', switch_name) - raise - - try: - s.port_set_access_vlan(port['name'], vlan['tag']) - except IOError: - logging.error('set_current_vlan failed, resetting switch to recover') - # Bugger. Looks like one of the switch calls above - # failed. To undo the changes safely, we'll need to reset - # all the config on this switch - s.switch_restart() # Will implicitly also close the connection - del s - raise - - # 3. All seems to have worked so far! - s.switch_save_running_config() - s.switch_disconnect() - del s - - # 4. And update the DB - db.set_current_vlan(port_id, vlan_id) - - return port_id # If we're successful - - # Complex call, updating both DB and switch state - # 1. Check validity of input - # 2. Update the port config on the switch - # 3. If all went OK, save config on the switch - # 4. Change details of the port in the DB - # - # If things fail, we attempt to roll back by rebooting the switch - def restore_base_vlan(self, state, port_id): - - logging.debug('restore_base_vlan') - db = state.db - config = state.config - - # 1. Sanity checks! - port = db.get_port_by_id(port_id) - if port is None: - raise NotFoundError("Port ID %d does not exist" % port_id) - if port['is_trunk']: - raise InputError("Port ID %d is not an access port" % port_id) - if port['is_locked']: - raise InputError("Port ID %d is locked" % port_id) - - # Bail out early if we're *already* on the base VLAN. This is - # not an error - if port['current_vlan_id'] == port['base_vlan_id']: - return port_id - - vlan = db.get_vlan_by_id(port['base_vlan_id']) - - # Get the right driver - switch_name = db.get_switch_name_by_id(port['switch_id']) - s = self.get_switch_driver(switch_name, config) - - # 2. Now start configuring the switch - try: - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - except: - logging.debug('Failed to talk to switch %s!', switch_name) - raise - - try: - s.port_set_access_vlan(port['name'], vlan['tag']) - except IOError: - logging.error('restore_base_vlan failed, resetting switch to recover') - # Bugger. Looks like one of the switch calls above - # failed. To undo the changes safely, we'll need to reset - # all the config on this switch - s.switch_restart() # Will implicitly also close the connection - del s - raise - - # 3. All seems to have worked so far! - s.switch_save_running_config() - s.switch_disconnect() - del s - - # 4. And update the DB - db.set_current_vlan(port_id, port['base_vlan_id']) - - return port_id # If we're successful - - # Complex call, updating both DB and switch state - # * Check validity of input - # * Read all the config from the switch (switch, ports, VLANs) - # * Create initial DB entries to match each of those - # * Merge VLANs across all switches - # * Set up ports appropriately - # - def auto_import_switch(self, state, switch_name): - - logging.debug('auto_import_switch') - db = state.db - config = state.config - - port_vlans = {} - - # 1. Sanity checks! - switch_id = db.get_switch_id_by_name(switch_name) - if switch_id is not None: - raise InputError("Switch name %s already exists in the DB (ID %d)" % (switch_name, switch_id)) - - if not switch_name in config.switches: - raise NotFoundError("Switch name %s not defined in config" % switch_name) - - # 2. Now start reading config from the switch - try: - s = self.get_switch_driver(switch_name, config) - s.switch_connect(config.switches[switch_name].username, - config.switches[switch_name].password, - config.switches[switch_name].enable_password) - except: - logging.debug('Failed to talk to switch %s!', switch_name) - raise - - # DON'T create the switch record in the DB first - we'll want - # to create VLANs on *other* switches, and it's easier to do - # that before we've added our new switch - - new_vlan_tags = [] - - # Grab the VLANs defined on this switch - vlan_tags = s.vlan_get_list() - - logging.debug(' found %d vlans on the switch', len(vlan_tags)) - - for vlan_tag in vlan_tags: - vlan_name = s.vlan_get_name(vlan_tag) - - # If a VLAN is already in the database, then that's easy - - # we can just ignore it. However, we have to check that - # there is not a different name for the existing VLAN tag - # - bail out if so... UNLESS we're looking at the default - # VLAN - # - # If this VLAN tag is not already in the DB, we'll need to - # add it there and to all the other switches (and their - # trunk ports!) too. - vlan_id = db.get_vlan_id_by_tag(vlan_tag) - if vlan_id != state.default_vlan_id: - if vlan_id is not None: - vlan_db_name = db.get_vlan_name_by_id(vlan_id) - if vlan_name != vlan_db_name: - raise InputError("Can't add VLAN tag %d (name %s) for this switch - VLAN tag %d already exists in the database, but with a different name (%s)" % (vlan_tag, vlan_name, vlan_tag, vlan_db_name)) - - else: - # OK, we'll need to set up the new VLAN now. It can't - # be a base VLAN - switches don't have such a concept! - # Rather than create individually here, add to a - # list. *Only* once we've worked through all the - # switch's VLANs successfully (checking for existing - # records and possible clashes!) should we start - # committing changes - new_vlan_tags.append(vlan_tag) - - # Now create the VLAN DB entries - for vlan_tag in new_vlan_tags: - vlan_name = s.vlan_get_name(vlan_tag) - vlan_id = self.create_vlan(state, vlan_name, vlan_tag, False) - - # *Now* add this switch itself to the database, after we've - # worked on all the other switches - switch_id = db.create_switch(switch_name) - - # And now the ports - trunk_ports = [] - ports = s.switch_get_port_names() - logging.debug(' found %d ports on the switch', len(ports)) - for port_name in ports: - logging.debug(' trying to import port %s', port_name) - port_id = None - port_mode = s.port_get_mode(port_name) - port_number = s.port_map_name_to_number(port_name) - if port_mode == 'access': - # Access ports are easy - just create the port, and - # set both the current and base VLANs to the current - # VLAN on the switch. We'll end up changing this after - # import if needed. - port_vlans[port_name] = (s.port_get_access_vlan(port_name),) - port_vlan_id = db.get_vlan_id_by_tag(port_vlans[port_name][0]) - port_id = db.create_port(switch_id, port_name, port_number, - port_vlan_id, port_vlan_id) - logging.debug(' access port, VLAN %d', int(port_vlans[port_name][0])) - # Nothing further needed - elif port_mode == 'trunk': - logging.debug(' trunk port, VLANs:') - # Trunk ports are a little more involved. First, - # create the port in the DB, setting the VLANs to the - # first VLAN found on the trunk port. This will *also* - # be in access mode by default, and unlocked. - port_vlans[port_name] = s.port_get_trunk_vlan_list(port_name) - logging.debug(port_vlans[port_name]) - if port_vlans[port_name] == [] or port_vlans[port_name] is None or 'ALL' in port_vlans[port_name]: - port_vlans[port_name] = (state.config.vland.default_vlan_tag,) - port_vlan_id = db.get_vlan_id_by_tag(port_vlans[port_name][0]) - port_id = db.create_port(switch_id, port_name, port_number, - port_vlan_id, port_vlan_id) - # Append to a list of trunk ports that we will need to - # modify once we're done - trunk_ports.append(port_id) - else: - # We've found a port mode we don't want, e.g. the - # "dynamic auto" on a Cisco Catalyst. Handle that here - # - tell the switch to set that port to access and - # handle accordingly. - s.port_set_mode(port_name, 'access') - port_vlans[port_name] = (s.port_get_access_vlan(port_name),) - port_vlan_id = db.get_vlan_id_by_tag(port_vlans[port_name][0]) - port_id = db.create_port(switch_id, port_name, port_number, - port_vlan_id, port_vlan_id) - logging.debug(' Found port in %s mode', port_mode) - logging.debug(' Forcing to access mode, VLAN %d', int(port_vlans[port_name][0])) - port_mode = "access" - - logging.debug(" Added port %s, got port ID %d", port_name, port_id) - - db.set_port_mode(port_id, port_mode) - - # Make sure this switch has all the VLANs we need - for vlan in db.all_vlans(): - if vlan['tag'] != state.config.vland.default_vlan_tag: - if not vlan['tag'] in vlan_tags: - logging.debug("Adding VLAN tag %d to this switch", vlan['tag']) - s.vlan_create(vlan['tag']) - s.vlan_set_name(vlan['tag'], vlan['name']) - - # Now, on each trunk port on the switch, we need to add all - # the VLANs already configured across our system - if not 'TrunkWildCardVlans' in s.switch_get_capabilities(): - for port_id in trunk_ports: - port = db.get_port_by_id(port_id) - - for vlan in db.all_vlans(): - if vlan['vlan_id'] != state.default_vlan_id: - if not vlan['tag'] in port_vlans[port['name']]: - logging.debug("Adding allowed VLAN tag %d to trunk port %s", vlan['tag'], port['name']) - s.port_add_trunk_to_vlan(port['name'], vlan['tag']) - - # Done with this switch \o/ - s.switch_save_running_config() - s.switch_disconnect() - del s - - ret = {} - ret['switch_id'] = switch_id - ret['num_ports_added'] = len(ports) - ret['num_vlans_added'] = len(new_vlan_tags) - return ret # If we're successful diff --git a/visualisation/__init__.py b/visualisation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/visualisation/graphics.py b/visualisation/graphics.py deleted file mode 100644 index 55067d8..0000000 --- a/visualisation/graphics.py +++ /dev/null @@ -1,489 +0,0 @@ -#! /usr/bin/python - -# Copyright 2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# Visualisation graphics module for VLANd -# -# This code uses python-gd to generate graphics ready for insertion -# into our web interface. Example code in the self-test at the -# bottom. - -import gd, os, sys - -if __name__ == '__main__': - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError - -class Graphics: - """ Code and config for the visualisation graphics module """ - - font = None - - # Default font size for the small labels - small_font_size = 12 - - # And the size for the top-level label - label_font_size = 24 - - # Size in pixels of that font, calculated later - twocharwidth = 0 - charheight = 0 - - # How big a gap to leave between trunk connections - trunk_gap = 8 - - # Details of the legend - legend_width = 0 - legend_height = 0 - legend_text_width = 0 - legend_text_height = 0 - legend_total_width = 0 - legend_box_width = 0 - legend_box_height = 0 - - # Basic colour definitions used later - colour_defs = {} - colour_defs['black'] = (0, 0, 0) - colour_defs['white'] = (255, 255, 255) - colour_defs['purple'] = (255, 0, 255) - colour_defs['blue'] = (0, 0, 255) - colour_defs['darkgrey'] = (60, 60, 60) - colour_defs['yellow'] = (255, 255, 0) - colour_defs['red'] = (255, 0, 0) - colour_defs['aqua'] = (0, 255, 255) - - pallette = {} - - # colours for the background - pallette['bg_colour'] = 'purple' - pallette['transparent_colour'] = 'purple' - pallette['graphic_label_colour'] = 'black' - - # switch colours - pallette['switch_outline_colour'] = 'black' - pallette['switch_fill_colour'] = 'darkgrey' - pallette['switch_label_colour'] = 'white' - - # verious sets of port colours, matching the 'highlight' options in - # draw_port() - port_pallette = {} - port_pallette['normal'] = {} - port_pallette['normal']['port_box'] = 'white' - port_pallette['normal']['port_bg'] = 'black' - port_pallette['normal']['port_label'] = 'white' - port_pallette['normal']['trace'] = 'black' - - port_pallette['trunk'] = {} - port_pallette['trunk']['port_box'] = 'white' - port_pallette['trunk']['port_bg'] = 'blue' - port_pallette['trunk']['port_label'] = 'yellow' - port_pallette['trunk']['trace'] = 'blue' - - port_pallette['locked'] = {} - port_pallette['locked']['port_box'] = 'white' - port_pallette['locked']['port_bg'] = 'red' - port_pallette['locked']['port_label'] = 'yellow' - port_pallette['locked']['trace'] = 'red' - - port_pallette['VLAN'] = {} - port_pallette['VLAN']['port_box'] = 'white' - port_pallette['VLAN']['port_bg'] = 'aqua' - port_pallette['VLAN']['port_label'] = 'black' - port_pallette['VLAN']['trace'] = 'aqua' - - im = None - - # TODO: make colours configurable, add maybe parsing for - # /etc/X11/rgb.txt to allow people to use arbitrary names? - - # Choose a font for our graphics to use. Pass in a list of fonts - # to be tried, in priority order. - def set_font(self, fontlist): - for font in fontlist: - if os.path.exists(font): - self.font = os.path.abspath(font) - break - - # Work out how big we need to be for the biggest possible text - # in a 2-digit number. Grotty, but we need to know this later. - for value in range (0, 100): - (width, height) = self.get_label_size(repr(value), self.small_font_size) - self.twocharwidth = max(self.twocharwidth, width) - self.charheight = max(self.charheight, height) - - # Now we can also calulate other stuff - self._calc_legend_size() - - # Create a canvas and set things up ready for use - def create_canvas(self, x, y): - im = gd.image((x, y)) - - # Allocate our colours in the image's colour map - for key in self.colour_defs.iterkeys(): - im.colorAllocate((self.colour_defs[key][0], - self.colour_defs[key][1], - self.colour_defs[key][2])) - - im.fill((0,0), im.colorExact(self.colour_defs[self.pallette['bg_colour']])) - im.colorTransparent(im.colorExact(self.colour_defs[self.pallette['transparent_colour']])) - im.interlace(0) - self.im = im - - # Using our selected font, what dimensions will a particular piece - # of text take? - def get_label_size(self, label, font_size): - tmp_im = gd.image((200, 200)) - (llx, lly, lrx, lry, urx, ury, ulx, uly) = tmp_im.get_bounding_rect(self.font, - font_size, - 0.0, - (10, 100), label) - width = max(lrx, urx) - min(llx, ulx) - height = max(lly, lry) - min(uly, ury) - return (width, height) - - # Draw a trunk connection between two ports - # - # Ports are defined as (ulx,uly),(lrx,lry), top): x, y - # co-ordinates of UL and LR corners, and whether the port is on - # the top or bottom row of a switch, i.e. does the wire come up or - # down when it leaves the port. - def draw_trunk(self, trunknum, node1, node2, colour): - for node in (node1, node2): - ((ulx,uly),(lrx,lry),top) = node - - # Work out the co-ordinates for a line vertically up or - # down from the edge of the port - x1 = int((ulx + lrx) / 2) - x2 = x1 - if (top): - y1 = uly - y2 = y1 - (self.trunk_gap * (trunknum + 1)) - else: - y1 = lry - y2 = y1 + (self.trunk_gap * (trunknum + 1)) - # Quick hack - use 2-pixel wide rectangles as thick lines :-) - # First line, vertically up/down from the port - self.im.rectangle((x1-1,y1), (x2,y2), self.im.colorExact(self.colour_defs[colour])) - # Now draw horizontally across to the left margin space - x3 = self.trunk_gap * (trunknum + 1) - self.im.rectangle((x3, y2), (x2,y2+1), self.im.colorExact(self.colour_defs[colour])) - - # Now join up the trunks vertically - ((ulx1,uly1),(lrx1,lry1),top1) = node1 - if (top1): - y1 = uly1 - self.trunk_gap * (trunknum + 1) - else: - y1 = lry1 + self.trunk_gap * (trunknum + 1) - ((ulx2,uly2),(lrx2,lry2),top2) = node2 - if (top2): - y2 = uly2 - self.trunk_gap * (trunknum + 1) - else: - y2 = lry2 + self.trunk_gap * (trunknum + 1) - x1 = self.trunk_gap * (trunknum + 1) - self.im.rectangle((x1, y1), (x1+1,y2), self.im.colorExact(self.colour_defs[colour])) - - # How big is the legend? - def _calc_legend_size(self): - max_width = 0 - max_height = 0 - - for value in self.port_pallette.iterkeys(): - (width, height) = self.get_label_size(value, self.small_font_size) - max_width = max(max_width, width) - max_height = max(max_height, height) - - (width, height) = self.get_label_size('##', self.small_font_size) - self.legend_box_width = width + 6 - self.legend_box_height = height + 6 - self.legend_width = max_width + self.legend_box_width + 10 - self.legend_height = 3 + self.legend_box_height + 3 - self.legend_text_width = max_width - self.legend_text_height = max_height - self.legend_total_width = 6 + (len(self.port_pallette) * self.legend_width) - - # Return the legend dimensions - def get_legend_dimensions(self): - return (self.legend_total_width, self.legend_height) - - # Draw the legend using (left, top) as the top left corner - def draw_legend(self, left, top): - lrx = left + self.legend_total_width - 1 - lry = top + self.legend_height - 1 - self.im.rectangle((left, top), (lrx, lry), - self.im.colorExact(self.colour_defs[self.pallette['switch_outline_colour']]), - self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']])) - curr_x = left + 3 - curr_y = top + 3 - - for value in sorted(self.port_pallette): - box_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_box']]) - box_bg_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_bg']]) - text_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_label']]) - lrx = curr_x + self.legend_box_width - 1 - lry = curr_y + self.legend_box_height - 1 - self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour) - - llx = curr_x + 4 - lly = curr_y + self.legend_box_height - 4 - self.im.string_ttf(self.font, self.small_font_size, 0.0, (llx, lly), '##', text_colour) - curr_x += self.legend_box_width - self.im.string_ttf(self.font, self.small_font_size, 0.0, (curr_x + 3, lly), value, - self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']])) - curr_x += self.legend_text_width + 10 - - # Draw the graphic's label using (left, top) as the top left - # corner with a box around - def draw_label(self, left, top, label, gap): - box_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']]) - box_bg_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']]) - text_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']]) - (width, height) = self.get_label_size(label, self.label_font_size) - curr_x = left - curr_y = top - lrx = curr_x + width + gap - lry = curr_y + height + 20 - self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour) - curr_x = left + 10 - curr_y = top + height + 6 - self.im.string_ttf(self.font, self.label_font_size, 0.0, (curr_x, curr_y), label, text_colour) - - -class Switch: - """ Code and config for dealing with a switch """ - port_width = 0 - port_height = 0 - text_width = 0 - text_height = 0 - label_left = 0 - label_bot = 0 - total_width = 0 - total_height = 0 - num_ports = 0 - left = None - top = None - name = None - - # Set up a new switch instance; calculate all the sizes so we can - # size our canvas - def __init__(self, g, num_ports, name): - self.num_ports = num_ports - self.name = name - self._calc_port_size(g) - self._calc_switch_size(g) - - # How big is a port and the text within it? - def _calc_port_size(self, g): - self.text_width = g.twocharwidth - self.text_height = g.charheight - # Leave enough space around the text for a nice clear box - self.port_width = self.text_width + 6 - self.port_height = self.text_height + 6 - - # How big is the full switch, including all the ports and the - # switch name label? - def _calc_switch_size(self, g): - (label_width, label_height) = g.get_label_size(self.name, g.small_font_size) - num_ports = self.num_ports - # Make sure we have an even number for 2 rows - if (self.num_ports & 1): - num_ports += 1 - self.label_left = 3 + (num_ports * self.port_width / 2) + 3 - self.label_bot = self.port_height - 2 - self.total_width = self.label_left + label_width + 3 - self.total_height = 3 + max(label_height, (2 * self.port_height)) + 3 - - # Return the switch dimensions - def get_dimensions(self): - return (self.total_width, self.total_height) - - # Draw the basic switch outline and label using (left, top) as the - # top left corner. The switch object will remember this origin for - # later use when drawing ports. - def draw_switch(self, g, left, top): - self.left = left - self.top = top - lrx = left + self.total_width -1 - lry = top + self.total_height - 1 - g.im.rectangle((left, top), (lrx, lry), - g.im.colorExact(g.colour_defs[g.pallette['switch_outline_colour']]), - g.im.colorExact(g.colour_defs[g.pallette['switch_fill_colour']])) - llx = left + self.label_left - lly = top + self.label_bot - g.im.string_ttf(g.font, g.small_font_size, 0.0, (llx, lly), self.name, - g.im.colorExact(g.colour_defs[g.pallette['switch_label_colour']])) - - # Draw a port inside the switch, using a specified colour scheme - # to denote its type. The switch outline must have been drawn - # first, for its origin to be set. - def draw_port(self, g, portnum, highlight): - if portnum < 1 or portnum > self.num_ports: - raise InputError('port number out of range') - if not self.left or not self.top: - raise InputError('cannot draw ports before switch is drawn') - if highlight not in g.port_pallette.iterkeys(): - raise InputError('unknown highlight type \"%s\"' % highlight) - - box_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_box']]) - box_bg_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_bg']]) - text_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_label']]) - - if (portnum & 1): # odd port number, so top row - ulx = self.left + 3 + ((portnum-1) * self.port_width / 2) - uly = self.top + 3 - else: # bottom row - ulx = self.left + 3 + ((portnum-2) * self.port_width / 2) - uly = self.top + 3 + self.port_height - lrx = ulx + self.port_width - 1 - lry = uly + self.port_height - 1 - g.im.rectangle((ulx,uly), (lrx,lry), box_colour, box_bg_colour) - - # centre the text - (width, height) = g.get_label_size(repr(portnum), g.small_font_size) - llx = ulx + 3 + (self.text_width - width) / 2 - lly = uly + max(height, self.text_height) + 1 - g.im.string_ttf(g.font, g.small_font_size, - 0.0, (llx, lly), repr(portnum), text_colour) - - # Quick helper: draw all the ports for a switch in the default - # colour scheme. - def draw_default_ports(self, g): - for portnum in range(1, self.num_ports + 1): - self.draw_port(g, portnum, 'normal') - - # Get the (x,y) co-ordinates of the UL and LR edges of the port - # box, and if it's upper row. This lets us so useful things such - # as draw a connection to that point for a trunk. - def get_port_location(self, portnum): - if portnum > self.num_ports: - raise InputError('port number out of range') - - if (portnum & 1): # odd port number, so top row - ulx = self.left + 3 + ((portnum-1) * self.port_width / 2) - uly = self.top - lrx = ulx + self.port_width - lry = uly + self.port_height - return ((ulx,uly), (lrx,lry), True) - else: # bottom row - ulx = self.left + 3 + ((portnum-2) * self.port_width / 2) - uly = self.top + 3 + self.port_height - lrx = ulx + self.port_width - lry = uly + self.port_height - return ((ulx,uly), (lrx,lry), False) - - # Debug: print some of the state of the switch object - def dump_state(self): - print 'port_width %d' % self.port_width - print 'port_height %d' % self.port_height - print 'text_width %d' % self.text_width - print 'text_height %d' % self.text_height - print 'label_left %d' % self.label_left - print 'label_bot %d' % self.label_bot - print 'total_width %d' % self.total_width - print 'total_height %d' % self.total_height - -# Test harness - generate a PNG using fake data -if __name__ == '__main__': - gim = Graphics() - gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf', - '/usr/share/fonts/truetype/freefont/FreeMono.ttf']) - try: - gim.font - except NameError: - print 'no fonts found' - sys.exit(1) - - switch = {} - size_x = {} - size_y = {} - switch[0] = Switch(gim, 48, 'lngswitch01') - switch[1] = Switch(gim, 24, 'lngswitch02') - switch[2] = Switch(gim, 52, 'lngswitch03') - label = "VLAN 4jj" - - # Need to set gaps big enough for the number of trunks, at least. - num_trunks = 3 - y_gap = max(20, 15 * num_trunks) - x_gap = max(20, 15 * num_trunks) - - x = 0 - y = y_gap - - for i in range (0, 3): - (size_x[i], size_y[i]) = switch[i].get_dimensions() - x = max(x, size_x[i]) - y += size_y[i] + y_gap - - # Add space for the legend and the label - (legend_width, legend_height) = gim.get_legend_dimensions() - (label_width, label_height) = gim.get_label_size(label, gim.label_font_size) - - x = max(x, legend_width + 2*x_gap + label_width) - x = x_gap + x + x_gap - y = y + max(legend_height + y_gap, label_height) - - gim.create_canvas(x, y) - - curr_y = y_gap - switch[0].draw_switch(gim, x_gap, curr_y) - switch[0].draw_default_ports(gim) - switch[0].draw_port(gim, 2, 'VLAN') - switch[0].draw_port(gim, 5, 'locked') - switch[0].draw_port(gim, 11, 'trunk') - switch[0].draw_port(gim, 44, 'trunk') - curr_y += size_y[0] + y_gap - - switch[1].draw_switch(gim, x_gap, curr_y) - switch[1].draw_default_ports(gim) - switch[1].draw_port(gim, 5, 'VLAN') - switch[1].draw_port(gim, 8, 'locked') - switch[1].draw_port(gim, 13, 'trunk') - switch[1].draw_port(gim, 16, 'trunk') - curr_y += size_y[2] + y_gap - - switch[2].draw_switch(gim, x_gap, curr_y) - switch[2].draw_default_ports(gim) - switch[2].draw_port(gim, 1, 'trunk') - switch[2].draw_port(gim, 2, 'locked') - switch[2].draw_port(gim, 14, 'trunk') - switch[2].draw_port(gim, 19, 'VLAN') - curr_y += size_y[2] + y_gap - - # Now let's try and draw some trunks! - gim.draw_trunk(0, - switch[0].get_port_location(11), - switch[1].get_port_location(16), - gim.port_pallette['trunk']['trace']) - gim.draw_trunk(1, - switch[1].get_port_location(13), - switch[2].get_port_location(1), - gim.port_pallette['trunk']['trace']) - gim.draw_trunk(2, - switch[0].get_port_location(44), - switch[2].get_port_location(14), - gim.port_pallette['trunk']['trace']) - - gim.draw_legend(x_gap, curr_y) - gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2)) - - f=open('xx.png','w') - gim.im.writePng(f) - f.close() - print 'Test graphic written to xx.png' diff --git a/visualisation/visualisation.py b/visualisation/visualisation.py deleted file mode 100644 index eb1edcd..0000000 --- a/visualisation/visualisation.py +++ /dev/null @@ -1,503 +0,0 @@ -#! /usr/bin/python - -# Copyright 2015 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# Visualisation module for VLANd. Fork a trivial webserver -# implementation on an extra, and generate a simple set of pages and -# graphics on demand. -# - -import os, sys, logging, time, datetime, re, signal -from multiprocessing import Process -from BaseHTTPServer import BaseHTTPRequestHandler -from BaseHTTPServer import HTTPServer -import urlparse -import cStringIO - -if __name__ == '__main__': - vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) - sys.path.insert(0, vlandpath) - sys.path.insert(0, "%s/.." % vlandpath) - -from errors import InputError -from db.db import VlanDB -from config.config import VlanConfig -from graphics import Graphics,Switch -from util import VlanUtil -class VlandHTTPServer(HTTPServer): - """ Trivial wrapper for HTTPServer so we can include our own state. """ - def __init__(self, server_address, handler, state): - HTTPServer.__init__(self, server_address, handler) - self.state = state - -class GraphicsCache(object): - """ Cache for graphics state, to avoid having to recalculate every - query too many times. """ - last_update = None - graphics = {} - - def __init__(self): - # Pick an epoch older than any sensible use - self.last_update = datetime.datetime(2000, 01, 01) - -class Visualisation(object): - """ Code and config for the visualisation graphics module. """ - - state = None - p = None - - # Fork a new process for the visualisation webserver - def __init__(self, state): - self.state = state - self.p = Process(target=self.visloop, args=()) - self.p.start() - - def _receive_signal(self, signum, stack): - if signum == signal.SIGUSR1: - self.state.db_ok = True - - # The main loop for the visualisation webserver - def visloop(self): - self.state.db_ok = False - self.state.cache = GraphicsCache() - - loglevel = VlanUtil().set_logging_level(self.state.config.logging.level) - - # Should we log to stderr? - if self.state.config.logging.filename is None: - logging.basicConfig(level = loglevel, - format = '%(asctime)s %(levelname)-8s %(message)s') - else: - logging.basicConfig(level = loglevel, - format = '%(asctime)s %(levelname)-8s VIS %(message)s', - datefmt = '%Y-%m-%d %H:%M:%S %Z', - filename = self.state.config.logging.filename, - filemode = 'a') - logging.info('%s visualisation starting up', self.state.banner) - - # Wait for main process to signal to us that it's finished with any - # database upgrades and we can open it without any problems. - signal.signal(signal.SIGUSR1, self._receive_signal) - while not self.state.db_ok: - logging.info('%s visualisation waiting for db_ok signal', self.state.banner) - time.sleep(1) - logging.info('%s visualisation received db_ok signal', self.state.banner) - - self.state.db = VlanDB(db_name=self.state.config.database.dbname, - username=self.state.config.database.username, - readonly=True) - - server = VlandHTTPServer(('', self.state.config.visualisation.port), - GetHandler, self.state) - server.serve_forever() - - # Kill the webserver - def shutdown(self): - self.p.terminate() - - # Kill the webserver - def signal_db_ok(self): - os.kill(self.p.pid, signal.SIGUSR1) - -class GetHandler(BaseHTTPRequestHandler): - """ Methods to generate and serve the pages """ - - parsed_path = None - - # Trivial top-level page. Link to images for each of the VLANs we - # know about. - def send_index(self): - self.send_response(200) - self.wfile.write('Content-type: text/html\r\n') - self.end_headers() - config = self.server.state.config.visualisation - cache = self.server.state.cache - db = self.server.state.db - switches = db.all_switches() - vlans = db.all_vlans() - vlan_tags = {} - - for vlan in vlans: - vlan_tags[vlan['vlan_id']] = vlan['tag'] - - if cache.last_update < self.server.state.db.get_last_modified_time(): - logging.debug('Cache is out of date') - # Fill the cache with all the information we need: - # * the graphics themselves - # * the data to match each graphic, so we can generate imagemap/tooltips - cache.graphics = {} - if len(switches) > 0: - for vlan in vlans: - cache.graphics[vlan['vlan_id']] = self.generate_graphic(vlan['vlan_id']) - cache.last_update = datetime.datetime.utcnow() - - page = [] - page.append('') - page.append('') - page.append('') - page.append('') - page.append('VLANd visualisation') - page.append('') - if config.refresh and config.refresh > 0: - page.append('' % config.refresh) - page.append('') - page.append('') - - # Generate left-hand menu with links to each VLAN diagram - page.append('') - - # Now the main content area with the graphics - page.append('
') - page.append('

VLANd visualisation

') - - # Bail early if we have nothing to show! - if len(switches) == 0: - page.append('

No switches found in the database, nothing to show...

') - page.append('
') - page.append('') - self.wfile.write('\r\n'.join(page)) - return - - # Trivial javascript helpers for tooltip control - page.append('') - - # For each VLAN, add a graphic - for vlan in vlans: - this_image = cache.graphics[vlan['vlan_id']] - page.append('' % vlan['vlan_id']) - page.append('

VLAN ID %d, Tag %d, name %s

' % (vlan['vlan_id'], vlan['tag'], vlan['name'])) - - # Link to an image we generate from our data - page.append('

' % (vlan['vlan_id'],vlan['vlan_id'])) - - # Generate an imagemap describing all the ports, with - # javascript hooks to pop up/down a tooltip box based on - # later data. - page.append('' % vlan['vlan_id']) - for switch in this_image['ports'].keys(): - for portnum in this_image['ports'][switch].keys(): - this_port = this_image['ports'][switch][portnum] - port = this_port['db'] - ((ulx,uly),(lrx,lry),upper) = this_port['location'] - page.append('' % (vlan['vlan_id'], port['port_id'], vlan['vlan_id'], port['port_id'])) - page.append('

') - page.append('
') - page.append('') # End of normal content, all the VLAN graphics shown - - # Now generate the tooltip boxes for the ports. Each is - # fully-formed but invisible, ready for our javascript helper - # to pop visible on demand. - for vlan in vlans: - this_image = cache.graphics[vlan['vlan_id']] - for switch in this_image['ports'].keys(): - for portnum in this_image['ports'][switch].keys(): - this_port = this_image['ports'][switch][portnum] - port = this_port['db'] - page.append('
' % (vlan['vlan_id'], port['port_id'])) - page.append('Port ID: %d
' % port['port_id']) - page.append('Port Number: %d
' % port['number']) - page.append('Port Name: %s
' % port['name']) - if port['is_locked']: - page.append('Locked - ') - if (port['lock_reason'] is not None - and len(port['lock_reason']) > 1): - page.append(port['lock_reason']) - else: - page.append('unknown reason') - page.append('
') - if port['is_trunk']: - page.append('Trunk') - if port['trunk_id'] != -1: - page.append(' (Trunk ID %d)' % port['trunk_id']) - page.append('
') - else: - page.append('Current VLAN ID: %d (Tag %d)
' % (port['current_vlan_id'], vlan_tags[port['current_vlan_id']])) - page.append('Base VLAN ID: %d (Tag %d)
' % (port['base_vlan_id'], vlan_tags[port['base_vlan_id']])) - page.append('
') - - page.append('') - self.wfile.write('\r\n'.join(page)) - - # Simple-ish style sheet - def send_style(self): - self.send_response(200) - self.wfile.write('Content-type: text/css\r\n') - self.end_headers() - cache = self.server.state.cache - page = [] - page.append('body {') - page.append(' background: white;') - page.append(' color: black;') - page.append(' font-size: 12pt;') - page.append('}') - page.append('') - page.append('.menu {') - page.append(' position:fixed;') - page.append(' float:left;') - page.append(' font-family: arial, Helvetica, sans-serif;') - page.append(' width:20%;') - page.append(' height:100%;') - page.append(' font-size: 10pt;') - page.append(' padding-top: 10px;') - page.append('}') - page.append('') - page.append('.content {') - page.append(' position:relative;') - page.append(' padding-top: 10px;') - page.append(' width: 80%;') - page.append(' max-width:80%;') - page.append(' margin-left: 21%;') - page.append(' margin-top: 50px;') - page.append(' height:100%;') - page.append('}') - page.append('') - page.append('h1,h2,h3 {') - page.append(' font-family: arial, Helvetica, sans-serif;') - page.append(' padding-right:3pt;') - page.append(' padding-top:2pt;') - page.append(' padding-bottom:2pt;') - page.append(' margin-top:8pt;') - page.append(' margin-bottom:8pt;') - page.append(' border-style:none;') - page.append(' border-width:thin;') - page.append('}') - page.append('A:link { text-decoration: none; }') - page.append('A:visited { text-decoration: none}') - page.append('h1 { font-size: 18pt; }') - page.append('h2 { font-size: 14pt; }') - page.append('h3 { font-size: 12pt; }') - page.append('dl,ul { margin-top: 1pt; text-indent: 0 }') - page.append('ol { margin-top: 1pt; text-indent: 0 }') - page.append('div.date { font-size: 8pt; }') - page.append('div.sig { font-size: 8pt; }') - page.append('div.port {') - page.append(' display: block;') - page.append(' position: fixed;') - page.append(' left: 0px;') - page.append(' bottom: 0px;') - page.append(' z-index: 99;') - page.append(' background: #FFFF00;') - page.append(' border-style: solid;') - page.append(' border-width: 3pt;') - page.append(' border-color: #3B3B3B;') - page.append(' margin: 1;') - page.append(' padding: 5px;') - page.append(' font-size: 10pt;') - page.append(' font-family: Courier,monotype;') - page.append(' visibility: hidden;') - page.append('}') - self.wfile.write('\r\n'.join(page)) - - # Generate a PNG showing the layout of switches/port/trunks for a - # specific VLAN - def send_graphic(self): - vlan_id = 0 - vlan_re = re.compile(r'^/images/vlan/(\d+).png$') - match = vlan_re.match(self.parsed_path.path) - if match: - vlan_id = int(match.group(1)) - cache = self.server.state.cache - - # Do we have a graphic for this VLAN ID? - if not vlan_id in cache.graphics.keys(): - logging.debug('asked for vlan_id %s', vlan_id) - logging.debug(cache.graphics.keys()) - self.send_response(404) - self.wfile.write('Content-type: text/plain\r\n') - self.end_headers() - self.wfile.write('404 Not Found\r\n') - self.wfile.write('%s' % self.parsed_path.path) - logging.error('VLAN graphic not found - asked for %s', self.parsed_path.path) - return - - # Yes - just send it from the cache - self.send_response(200) - self.wfile.write('Content-type: image/png\r\n') - self.end_headers() - self.wfile.write(cache.graphics[vlan_id]['image']['png'].getvalue()) - return - - # Generate a PNG showing the layout of switches/port/trunks for a - # specific VLAN, and return that PNG along with geometry details - def generate_graphic(self, vlan_id): - db = self.server.state.db - vlan = db.get_vlan_by_id(vlan_id) - # We've been asked for a VLAN that doesn't exist - if vlan is None: - return None - - data = {} - data['image'] = {} - data['ports'] = {} - - gim = Graphics() - - # Pick fonts. TODO: Make these configurable? - gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf', - '/usr/share/fonts/truetype/freefont/FreeMono.ttf']) - try: - gim.font - # If we can't get the font we need, fail - except NameError: - self.send_response(500) - self.wfile.write('Content-type: text/plain\r\n') - self.end_headers() - self.wfile.write('500 Internal Server Error\r\n') - logging.error('Unable to generate graphic, no fonts found - asked for %s', - self.parsed_path.path) - return - - switch = {} - size_x = {} - size_y = {} - - switches = db.all_switches() - - # Need to set gaps big enough for the number of trunks, at least. - trunks = db.all_trunks() - y_gap = max(20, 15 * len(trunks)) - x_gap = max(20, 15 * len(trunks)) - - x = 0 - y = y_gap - - # Work out how much space we need for the switches - for i in range(0, len(switches)): - ports = db.get_ports_by_switch(switches[i]['switch_id']) - switch[i] = Switch(gim, len(ports), switches[i]['name']) - (size_x[i], size_y[i]) = switch[i].get_dimensions() - x = max(x, size_x[i]) - y += size_y[i] + y_gap - - # Add space for the legend and the label - label = "VLAN %d - %s" % (vlan['tag'], vlan['name']) - (legend_width, legend_height) = gim.get_legend_dimensions() - (label_width, label_height) = gim.get_label_size(label, gim.label_font_size) - x = max(x, legend_width + 2*x_gap + label_width) - x = x_gap + x + x_gap - y = y + max(legend_height + y_gap, label_height) - - # Create a canvas of the right size - gim.create_canvas(x, y) - - # Draw the switches and ports in it - curr_y = y_gap - for i in range(0, len(switches)): - switch[i].draw_switch(gim, x_gap, curr_y) - ports = db.get_ports_by_switch(switches[i]['switch_id']) - data['ports'][i] = {} - for port_id in ports: - port = db.get_port_by_id(port_id) - port_location = switch[i].get_port_location(port['number']) - data['ports'][i][port['number']] = {} - data['ports'][i][port['number']]['db'] = port - data['ports'][i][port['number']]['location'] = port_location - if port['is_locked']: - switch[i].draw_port(gim, port['number'], 'locked') - elif port['is_trunk']: - switch[i].draw_port(gim, port['number'], 'trunk') - elif port['current_vlan_id'] == int(vlan_id): - switch[i].draw_port(gim, port['number'], 'VLAN') - else: - switch[i].draw_port(gim, port['number'], 'normal') - curr_y += size_y[i] + y_gap - - # Now add the trunks - for i in range(0, len(trunks)): - ports = db.get_ports_by_trunk(trunks[i]['trunk_id']) - port1 = db.get_port_by_id(ports[0]) - port2 = db.get_port_by_id(ports[1]) - for s in range(0, len(switches)): - if switches[s]['switch_id'] == port1['switch_id']: - switch1 = s - if switches[s]['switch_id'] == port2['switch_id']: - switch2 = s - gim.draw_trunk(i, - switch[switch1].get_port_location(port1['number']), - switch[switch2].get_port_location(port2['number']), - gim.port_pallette['trunk']['trace']) - - # And the legend and label - gim.draw_legend(x_gap, curr_y) - gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2)) - - # All done - push the image file into the cache for this vlan - data['image']['png'] = cStringIO.StringIO() - gim.im.writePng(data['image']['png']) - data['image']['width'] = x - data['image']['height'] = y - return data - - # Implement an HTTP GET handler for the HTTPServer instance - def do_GET(self): - # Compare the URL path to any of the names we recognise and - # call the right generator function if we get a match - self.parsed_path = urlparse.urlparse(self.path) - for url in self.functionMap: - match = re.match(url['re'], self.parsed_path.path) - if match: - return url['fn'](self) - - # Fall-through for any files we don't recognise - self.send_response(404) - self.wfile.write('Content-type: text/plain\r\n') - self.end_headers() - self.wfile.write('404 Not Found') - self.wfile.write('%s' % self.parsed_path.path) - logging.error('File not supported - asked for %s', self.parsed_path.path) - return - - # Override the BaseHTTPRequestHandler log_message() method so we - # can log requests properly - def log_message(self, fmt, *args): - """Log an arbitrary message. """ - logging.info('%s %s', self.client_address[0], fmt%args) - - functionMap = ( - {'re': r'^/$', 'fn': send_index}, - {'re': r'^/style.css$', 'fn': send_style}, - {'re': r'^/images/vlan/(\d+).png$', 'fn': send_graphic} - ) diff --git a/vland b/vland new file mode 100755 index 0000000..7365a73 --- /dev/null +++ b/vland @@ -0,0 +1,232 @@ +#! /usr/bin/python + +# Copyright 2014-2018 Linaro Limited +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Top-level VLANd daemon +# + +import os, sys +import time +import logging + +from Vland.config.config import VlanConfig +from Vland.db.db import VlanDB +from Vland.ipc.ipc import VlanIpc +from Vland.errors import InputError, NotFoundError, SocketError +from Vland.util import VlanUtil +from Vland.visualisation.visualisation import Visualisation + +class DaemonState: + """ Simple container for stuff to make for nicer syntax """ + +state = DaemonState() +state.version = "0.7" +state.banner = "Linaro VLANd version %s" % state.version +state.starttime = time.time() +state.vis = None + +os.environ['TZ'] = 'UTC' +time.tzset() + +print '%s' % state.banner + +print 'Parsing Config...' +state.config = VlanConfig(filenames=('./vland.cfg',)) +print ' Config knows about %d switches' % len(state.config.switches) + +if state.config.visualisation.enabled: + state.vis = Visualisation(state) + +util = VlanUtil() + +loglevel = util.set_logging_level(state.config.logging.level) + +# Should we log to stderr? +if state.config.logging.filename is None: + logging.basicConfig(level = loglevel, + format = '%(asctime)s %(levelname)-8s %(message)s') +else: + print "Logging to %s" % state.config.logging.filename + logging.basicConfig(level = loglevel, + format = '%(asctime)s %(levelname)-8s MAIN %(message)s', + datefmt = '%Y-%m-%d %H:%M:%S %Z', + filename = state.config.logging.filename, + filemode = 'a') + +logging.info('%s main daemon starting up', state.banner) +logging.info('Connecting to DB...') +state.db = VlanDB(db_name=state.config.database.dbname, + username=state.config.database.username, readonly=False) + +switches = state.db.all_switches() +logging.info('DB knows about %d switches', len(switches)) +ports = state.db.all_ports() +logging.info('DB knows about %d ports', len(ports)) +vlans = state.db.all_vlans() +logging.info('DB knows about %d vlans', len(vlans)) +trunks = state.db.all_trunks() +logging.info('DB knows about %d trunks', len(trunks)) + +# Initial startup sanity chacking + +# For sanity, we need to know the vlan_id for the default vlan (tag +# 1). Make sure we know that before anybody attempts to create things +# that depend on it. +state.default_vlan_id = state.db.get_vlan_id_by_tag(state.config.vland.default_vlan_tag) +if state.default_vlan_id is None: + # It doesn't exist - create it and try again + state.default_vlan_id = state.db.create_vlan("DEFAULT", + state.config.vland.default_vlan_tag, + True) + +if len(switches) != len(state.config.switches): + print 'You have configured access details for %d switch(es), ' % len(state.config.switches) + print 'but have %d switch(es) registered in your database.' % len(switches) + print 'You must fix this difference for VLANd to work sensibly.' + print 'HINT: Running vland-admin auto_import_switch --name ' + print 'for each of your switches may help!' + print + +# Now we're done starting up, let the visualisation code talk to the database +if state.config.visualisation.enabled: + logging.info('sending db_ok signal') + state.vis.signal_db_ok() + +# Now start up the core loop. Listen for command connections and +# process them +state.running = True +ipc = VlanIpc() +ipc.server_init('localhost', state.config.vland.port) +while state.running: + try: + ipc.server_listen() + json_data = ipc.server_recv() + except SocketError as e: + print e + logging.debug('Caught IPC error, ignoring') + continue + except: + ipc.server_close() + raise + + logging.debug("client %s sent us:", json_data['client_name']) + logging.debug(json_data) + + response = {} + + # Several types of IPC message here, with potentially different + # access control and safety + + # First - simple queries to the database only. Should be safe! + if json_data['type'] == 'db_query': + response['type'] = 'response' + try: + response['data'] = util.perform_db_query(state, json_data['command'], json_data['data']) + response['response'] = 'ACK' + except InputError as e: + print e + response['response'] = 'ERROR' + response['error'] = e.__str__() + except NotFoundError as e: + print e + response['response'] = 'NOTFOUND' + response['error'] = e.__str__() + + # Next - simple queries about daemon state only. Should be safe! + if json_data['type'] == 'daemon_query': + response['type'] = 'response' + try: + response['data'] = util.perform_daemon_query(state, json_data['command'], json_data['data']) + response['response'] = 'ACK' + except InputError as e: + print e + response['response'] = 'ERROR' + response['error'] = e.__str__() + except NotFoundError as e: + print e + response['response'] = 'NOTFOUND' + response['error'] = e.__str__() + + # Next, calls that manipulate objects in the database only + # (switches and ports). These are safe and don't need actual + # co-ordinating with hardware directly. + # + # As/when/if we add authentication, use of this function will need + # it. + if json_data['type'] == 'db_update': + response['type'] = 'response' + try: + response['data'] = util.perform_db_update(state, json_data['command'], json_data['data']) + response['response'] = 'ACK' + except InputError as e: + print e + response['response'] = 'ERROR' + response['error'] = e.__str__() + except NotFoundError as e: + print e + response['response'] = 'NOTFOUND' + response['error'] = e.__str__() + + # Next, calls that may manipulate switch state *as well* as state + # in the database - changes to VLAN setup. + # + # As/when/if we add authentication, use of this function will need + # it. + if json_data['type'] == 'vlan_update': + response['type'] = 'response' + try: + response['data'] = util.perform_vlan_update(state, json_data['command'], json_data['data']) + response['response'] = 'ACK' + except InputError as e: + print e + response['response'] = 'ERROR' + response['error'] = e.__str__() + except NotFoundError as e: + print e + response['response'] = 'NOTFOUND' + response['error'] = e.__str__() + + # Finally, IPC interface for more complex API calls. + # NOT IMPLEMENTED YET + if json_data['type'] == 'vland_api': + response['type'] = 'response' + response['response'] = 'ERROR' + response['error'] = 'VLANd API not yet implemented...' + + logging.debug("sending reply:") + logging.debug(response) + + ipc.server_reply(response) + +# We've been asked to shut down. Do that as cleanly as we can +ipc.server_close() + +if state.config.visualisation.enabled: + state.vis.shutdown() + +logging.info('%s shutting down', state.banner) +switches = state.db.all_switches() +logging.info(' DB knows about %d switches', len(switches)) +ports = state.db.all_ports() +logging.info(' DB knows about %d ports', len(ports)) +vlans = state.db.all_vlans() +logging.info(' DB knows about %d vlans', len(vlans)) +trunks = state.db.all_trunks() +logging.info(' DB knows about %d trunks', len(trunks)) + +logging.shutdown() diff --git a/vland-admin b/vland-admin index f635699..ced26eb 100755 --- a/vland-admin +++ b/vland-admin @@ -24,12 +24,9 @@ import os, sys import argparse import datetime, time -vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) -sys.path.insert(0, vlandpath) - -from errors import InputError, SocketError, NotFoundError, Error -from config.config import VlanConfig -from ipc.ipc import VlanIpc +from Vland.errors import InputError, SocketError, NotFoundError, Error +from Vland.config.config import VlanConfig +from Vland.ipc.ipc import VlanIpc prog = "vland-admin" version = "0.7" diff --git a/vland.py b/vland.py deleted file mode 100755 index 633e10f..0000000 --- a/vland.py +++ /dev/null @@ -1,235 +0,0 @@ -#! /usr/bin/python - -# Copyright 2014-2018 Linaro Limited -# -# 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 2 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, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -# Main VLANd module -# - -import os, sys -import time -import logging - -vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0]))) -sys.path.insert(0, vlandpath) - -from config.config import VlanConfig -from db.db import VlanDB -from ipc.ipc import VlanIpc -from errors import InputError, NotFoundError, SocketError -from util import VlanUtil -from visualisation.visualisation import Visualisation - -class DaemonState: - """ Simple container for stuff to make for nicer syntax """ - -state = DaemonState() -state.version = "0.7" -state.banner = "Linaro VLANd version %s" % state.version -state.starttime = time.time() -state.vis = None - -os.environ['TZ'] = 'UTC' -time.tzset() - -print '%s' % state.banner - -print 'Parsing Config...' -state.config = VlanConfig(filenames=('./vland.cfg',)) -print ' Config knows about %d switches' % len(state.config.switches) - -if state.config.visualisation.enabled: - state.vis = Visualisation(state) - -util = VlanUtil() - -loglevel = util.set_logging_level(state.config.logging.level) - -# Should we log to stderr? -if state.config.logging.filename is None: - logging.basicConfig(level = loglevel, - format = '%(asctime)s %(levelname)-8s %(message)s') -else: - print "Logging to %s" % state.config.logging.filename - logging.basicConfig(level = loglevel, - format = '%(asctime)s %(levelname)-8s MAIN %(message)s', - datefmt = '%Y-%m-%d %H:%M:%S %Z', - filename = state.config.logging.filename, - filemode = 'a') - -logging.info('%s main daemon starting up', state.banner) -logging.info('Connecting to DB...') -state.db = VlanDB(db_name=state.config.database.dbname, - username=state.config.database.username, readonly=False) - -switches = state.db.all_switches() -logging.info('DB knows about %d switches', len(switches)) -ports = state.db.all_ports() -logging.info('DB knows about %d ports', len(ports)) -vlans = state.db.all_vlans() -logging.info('DB knows about %d vlans', len(vlans)) -trunks = state.db.all_trunks() -logging.info('DB knows about %d trunks', len(trunks)) - -# Initial startup sanity chacking - -# For sanity, we need to know the vlan_id for the default vlan (tag -# 1). Make sure we know that before anybody attempts to create things -# that depend on it. -state.default_vlan_id = state.db.get_vlan_id_by_tag(state.config.vland.default_vlan_tag) -if state.default_vlan_id is None: - # It doesn't exist - create it and try again - state.default_vlan_id = state.db.create_vlan("DEFAULT", - state.config.vland.default_vlan_tag, - True) - -if len(switches) != len(state.config.switches): - print 'You have configured access details for %d switch(es), ' % len(state.config.switches) - print 'but have %d switch(es) registered in your database.' % len(switches) - print 'You must fix this difference for VLANd to work sensibly.' - print 'HINT: Running vland-admin auto_import_switch --name ' - print 'for each of your switches may help!' - print - -# Now we're done starting up, let the visualisation code talk to the database -if state.config.visualisation.enabled: - logging.info('sending db_ok signal') - state.vis.signal_db_ok() - -# Now start up the core loop. Listen for command connections and -# process them -state.running = True -ipc = VlanIpc() -ipc.server_init('localhost', state.config.vland.port) -while state.running: - try: - ipc.server_listen() - json_data = ipc.server_recv() - except SocketError as e: - print e - logging.debug('Caught IPC error, ignoring') - continue - except: - ipc.server_close() - raise - - logging.debug("client %s sent us:", json_data['client_name']) - logging.debug(json_data) - - response = {} - - # Several types of IPC message here, with potentially different - # access control and safety - - # First - simple queries to the database only. Should be safe! - if json_data['type'] == 'db_query': - response['type'] = 'response' - try: - response['data'] = util.perform_db_query(state, json_data['command'], json_data['data']) - response['response'] = 'ACK' - except InputError as e: - print e - response['response'] = 'ERROR' - response['error'] = e.__str__() - except NotFoundError as e: - print e - response['response'] = 'NOTFOUND' - response['error'] = e.__str__() - - # Next - simple queries about daemon state only. Should be safe! - if json_data['type'] == 'daemon_query': - response['type'] = 'response' - try: - response['data'] = util.perform_daemon_query(state, json_data['command'], json_data['data']) - response['response'] = 'ACK' - except InputError as e: - print e - response['response'] = 'ERROR' - response['error'] = e.__str__() - except NotFoundError as e: - print e - response['response'] = 'NOTFOUND' - response['error'] = e.__str__() - - # Next, calls that manipulate objects in the database only - # (switches and ports). These are safe and don't need actual - # co-ordinating with hardware directly. - # - # As/when/if we add authentication, use of this function will need - # it. - if json_data['type'] == 'db_update': - response['type'] = 'response' - try: - response['data'] = util.perform_db_update(state, json_data['command'], json_data['data']) - response['response'] = 'ACK' - except InputError as e: - print e - response['response'] = 'ERROR' - response['error'] = e.__str__() - except NotFoundError as e: - print e - response['response'] = 'NOTFOUND' - response['error'] = e.__str__() - - # Next, calls that may manipulate switch state *as well* as state - # in the database - changes to VLAN setup. - # - # As/when/if we add authentication, use of this function will need - # it. - if json_data['type'] == 'vlan_update': - response['type'] = 'response' - try: - response['data'] = util.perform_vlan_update(state, json_data['command'], json_data['data']) - response['response'] = 'ACK' - except InputError as e: - print e - response['response'] = 'ERROR' - response['error'] = e.__str__() - except NotFoundError as e: - print e - response['response'] = 'NOTFOUND' - response['error'] = e.__str__() - - # Finally, IPC interface for more complex API calls. - # NOT IMPLEMENTED YET - if json_data['type'] == 'vland_api': - response['type'] = 'response' - response['response'] = 'ERROR' - response['error'] = 'VLANd API not yet implemented...' - - logging.debug("sending reply:") - logging.debug(response) - - ipc.server_reply(response) - -# We've been asked to shut down. Do that as cleanly as we can -ipc.server_close() - -if state.config.visualisation.enabled: - state.vis.shutdown() - -logging.info('%s shutting down', state.banner) -switches = state.db.all_switches() -logging.info(' DB knows about %d switches', len(switches)) -ports = state.db.all_ports() -logging.info(' DB knows about %d ports', len(ports)) -vlans = state.db.all_vlans() -logging.info(' DB knows about %d vlans', len(vlans)) -trunks = state.db.all_trunks() -logging.info(' DB knows about %d trunks', len(trunks)) - -logging.shutdown() diff --git a/vland.service b/vland.service index 83397c9..e998dba 100644 --- a/vland.service +++ b/vland.service @@ -6,7 +6,7 @@ Requires=postgresql.service [Service] Type=simple -ExecStart=/home/vland/vland/vland.py +ExecStart=/home/vland/vland/vland ExecStop=/home/vland/vland/vland-admin shutdown WorkingDirectory=/home/vland/vland User=vland -- cgit v1.2.3