aboutsummaryrefslogtreecommitdiff
path: root/Vland
diff options
context:
space:
mode:
Diffstat (limited to 'Vland')
-rw-r--r--Vland/__init__.py0
-rw-r--r--Vland/config/__init__.py23
-rw-r--r--Vland/config/config.py270
-rw-r--r--Vland/config/test-clashing-ports.cfg33
-rw-r--r--Vland/config/test-invalid-DB.cfg5
-rw-r--r--Vland/config/test-invalid-logging-level.cfg30
-rw-r--r--Vland/config/test-invalid-vland.cfg11
-rw-r--r--Vland/config/test-known-good.cfg35
-rw-r--r--Vland/config/test-missing-db-username.cfg5
-rw-r--r--Vland/config/test-missing-dbname.cfg6
-rw-r--r--Vland/config/test-reused-switch-names.cfg26
-rw-r--r--Vland/config/test-unknown-section.cfg29
-rw-r--r--Vland/config/test.py101
-rw-r--r--Vland/db/__init__.py0
-rw-r--r--Vland/db/db.py825
-rw-r--r--Vland/db/init.doc5
-rwxr-xr-xVland/db/setup_db.py56
-rw-r--r--Vland/drivers/CiscoCatalyst.py721
-rw-r--r--Vland/drivers/CiscoSX300.py697
-rw-r--r--Vland/drivers/Dummy.py361
-rw-r--r--Vland/drivers/Mellanox.py795
-rw-r--r--Vland/drivers/NetgearXSM.py782
-rw-r--r--Vland/drivers/TPLinkTLSG2XXX.py695
-rw-r--r--Vland/drivers/__init__.py0
-rw-r--r--Vland/drivers/common.py167
-rw-r--r--Vland/errors.py59
-rw-r--r--Vland/ipc/__init__.py0
-rw-r--r--Vland/ipc/client-new.py18
-rw-r--r--Vland/ipc/ipc.py177
-rw-r--r--Vland/ipc/server-new.py24
-rw-r--r--Vland/util.py871
-rw-r--r--Vland/visualisation/__init__.py0
-rw-r--r--Vland/visualisation/graphics.py484
-rw-r--r--Vland/visualisation/visualisation.py498
34 files changed, 7809 insertions, 0 deletions
diff --git a/Vland/__init__.py b/Vland/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Vland/__init__.py
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 <steve.mcintyre@linaro.org>
+#
+# 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 "<DaemonConfig: port: %s>" % (self.port)
+
+class DBConfigClass:
+ """ Simple container for stuff to make for nicer syntax """
+
+ def __repr__(self):
+ return "<DBConfig: server: %s, port: %s, dbname: %s, username: %s, password: %s>" % (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 "<LoggingConfig: level: %s, filename: %s>" % (self.level, self.filename)
+
+class VisualisationConfigClass:
+ """ Simple container for stuff to make for nicer syntax """
+ def __repr__(self):
+ return "<VisualisationConfig: enabled: %s, port: %s>" % (self.enabled, self.port)
+
+class SwitchConfigClass:
+ """ Simple container for stuff to make for nicer syntax """
+ def __repr__(self):
+ return "<SwitchConfig: name: %s, section: %s, driver: %s, username: %s, password: %s, enable_password: %s>" % (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
--- /dev/null
+++ b/Vland/db/__init__.py
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 <dave.pigott@linaro.org>,
+# Steve McIntyre <steve.mcintyre@linaro.org>
+#
+# 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 <dave.pigot@linaro.org>,
+# Steve McIntyre <steve.mcintyre@linaro.org>
+#
+# 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 = "<switch>")
+ (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 = "<switch>")
+ (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<number>
+ 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 = "<switch>")
+ (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 = "<switch>")
+ (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 = "<switch>")
+ (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 = "<switch>")
+ (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
--- /dev/null
+++ b/Vland/drivers/__init__.py
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 "<SwitchErrors: errors_in: %d, errors_out: %d>" % (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 <dave.pigot@linaro.org>,
+# Steve McIntyre <steve.mcintyre@linaro.org>
+#
+# 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
--- /dev/null
+++ b/Vland/ipc/__init__.py
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
--- /dev/null
+++ b/Vland/visualisation/__init__.py
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('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">')
+ page.append('<html>')
+ page.append('<head>')
+ page.append('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">')
+ page.append('<TITLE>VLANd visualisation</TITLE>')
+ page.append('<link rel="stylesheet" type="text/css" href="style.css">')
+ if config.refresh and config.refresh > 0:
+ page.append('<meta http-equiv="refresh" content="%d">' % config.refresh)
+ page.append('</HEAD>')
+ page.append('<body>')
+
+ # Generate left-hand menu with links to each VLAN diagram
+ page.append('<div class="menu">')
+ if len(switches) > 0:
+ page.append('<h2>Menu</h2>')
+ page.append('<p>VLANs: %d</p>' % len(vlans))
+ page.append('<ul>')
+ for vlan in vlans:
+ page.append('<li><a href="./#vlan%d">VLAN ID %d, Tag %d<br>(%s)</a>' % (vlan['vlan_id'], vlan['vlan_id'], vlan['tag'], vlan['name']))
+ page.append('</ul>')
+ page.append('<div class="date"><p>Current time: %s</p>' % datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"))
+ page.append('<p>version %s</p>' % self.server.state.version)
+ page.append('</div>')
+ page.append('</div>')
+
+ # Now the main content area with the graphics
+ page.append('<div class="content">')
+ page.append('<h1>VLANd visualisation</h1>')
+
+ # Bail early if we have nothing to show!
+ if len(switches) == 0:
+ page.append('<p>No switches found in the database, nothing to show...</p>')
+ page.append('</div>')
+ page.append('</body>')
+ self.wfile.write('\r\n'.join(page))
+ return
+
+ # Trivial javascript helpers for tooltip control
+ page.append('<SCRIPT LANGUAGE="javascript">')
+ page.append('function popup(v,p) {')
+ page.append('a=v.toString();')
+ page.append('b=p.toString();')
+ page.append('id="port".concat("",a).concat(".",b);')
+ page.append('document.getElementById(id).style.visibility="visible";')
+ page.append('}')
+ page.append('function popdown(v,p) {')
+ page.append('a=v.toString();')
+ page.append('b=p.toString();')
+ page.append('id="port".concat("",a).concat(".",b);')
+ page.append('document.getElementById(id).style.visibility="hidden";')
+ page.append('}')
+ page.append('</SCRIPT>')
+
+ # For each VLAN, add a graphic
+ for vlan in vlans:
+ this_image = cache.graphics[vlan['vlan_id']]
+ page.append('<a name="vlan%d"></a>' % vlan['vlan_id'])
+ page.append('<h3>VLAN ID %d, Tag %d, name %s</h3>' % (vlan['vlan_id'], vlan['tag'], vlan['name']))
+
+ # Link to an image we generate from our data
+ page.append('<p><img src="images/vlan/%d.png" ' % vlan['vlan_id'])
+ page.append('width="%d" height="%d" ' % ( this_image['image']['width'], this_image['image']['height']))
+ page.append('alt="VLAN %d diagram" usemap="#MAPVLAN%d">' % (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('<map name="MAPVLAN%d">' % 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('<area shape="rect" ')
+ page.append('coords="%d,%d,%d,%d" ' % (ulx,uly,lrx,lry))
+ page.append('onMouseOver="popup(%d,%d)" onMouseOut="popdown(%d,%d)">' % (vlan['vlan_id'], port['port_id'], vlan['vlan_id'], port['port_id']))
+ page.append('</map></p>')
+ page.append('<hr>')
+ page.append('</div>') # 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('<div class="port" id="port%d.%d">' % (vlan['vlan_id'], port['port_id']))
+ page.append('Port ID: %d<br>' % port['port_id'])
+ page.append('Port Number: %d<br>' % port['number'])
+ page.append('Port Name: %s<br>' % 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('<br>')
+ if port['is_trunk']:
+ page.append('Trunk')
+ if port['trunk_id'] != -1:
+ page.append(' (Trunk ID %d)' % port['trunk_id'])
+ page.append('<br>')
+ else:
+ page.append('Current VLAN ID: %d (Tag %d)<br>' % (port['current_vlan_id'], vlan_tags[port['current_vlan_id']]))
+ page.append('Base VLAN ID: %d (Tag %d)<br>' % (port['base_vlan_id'], vlan_tags[port['base_vlan_id']]))
+ page.append('</div>')
+
+ page.append('</body>')
+ 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}
+ )