diff options
Diffstat (limited to 'Vland')
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} + ) |