blob: 4fc9444d8ce283e602e98eae76ac50dd05005e35 [file] [log] [blame]
Dave Pigott281203e2014-09-17 23:45:02 +01001#! /usr/bin/python
2
Steve McIntyre8fed4912018-01-31 17:21:31 +00003# Copyright 2014-2018 Linaro Limited
Steve McIntyrec4890132015-08-07 15:19:11 +01004# Authors: Dave Pigott <dave.pigott@linaro.org>,
5# Steve McIntyre <steve.mcintyre@linaro.org>
Dave Pigott281203e2014-09-17 23:45:02 +01006#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20# MA 02110-1301, USA.
21
22import psycopg2
23import psycopg2.extras
Steve McIntyre6a618162014-12-10 16:47:07 +000024import datetime, os, sys
Steve McIntyre7cf80982015-02-12 07:03:40 +000025import logging
Steve McIntyre6a618162014-12-10 16:47:07 +000026
Steve McIntyrec4890132015-08-07 15:19:11 +010027TRUNK_ID_NONE = -1
28
Steve McIntyre8fed4912018-01-31 17:21:31 +000029# The schema version that this code expects. If it finds an older version (or
30# no version!) at startup, it will auto-migrate to the latest version
31#
32# Version 0: Base, no version found
Steve McIntyre7d219202018-02-01 16:53:25 +000033#
Steve McIntyre8fed4912018-01-31 17:21:31 +000034# Version 1: No changes, except adding the version and coping with upgrade
Steve McIntyre7d219202018-02-01 16:53:25 +000035#
36# Version 2: Add "lock_reason" field in the port table, and code to deal with
37# it
38DATABASE_SCHEMA_VERSION = 2
Steve McIntyre8fed4912018-01-31 17:21:31 +000039
Steve McIntyre6a618162014-12-10 16:47:07 +000040if __name__ == '__main__':
41 vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
42 sys.path.insert(0, vlandpath)
43 sys.path.insert(0, "%s/.." % vlandpath)
44
Steve McIntyreb01959f2016-03-22 17:02:39 +000045from errors import CriticalError, InputError, NotFoundError
Dave Pigott281203e2014-09-17 23:45:02 +010046
47class VlanDB:
Steve McIntyreea343aa2015-10-23 17:46:17 +010048 def __init__(self, db_name="vland", username="vland", readonly=True):
Dave Pigott281203e2014-09-17 23:45:02 +010049 try:
Steve McIntyree38f6222014-11-27 15:09:49 +000050 self.connection = psycopg2.connect(database=db_name, user=username)
Steve McIntyre89932392018-02-02 10:15:53 +000051 # Create first cursor for normal usage - returns tuples
Steve McIntyreb09ed282014-12-02 17:59:35 +000052 self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
Steve McIntyre89932392018-02-02 10:15:53 +000053 # Create second cursor for full-row lookups - returns a dict
54 # instead, much more useful in the admin interface
55 self.dictcursor = self.connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
Steve McIntyreea343aa2015-10-23 17:46:17 +010056 if not readonly:
57 self._init_state()
Dave Pigott281203e2014-09-17 23:45:02 +010058 except Exception as e:
Steve McIntyre5fa22652015-04-01 18:01:45 +010059 logging.error("Failed to access database: %s", e)
Steve McIntyre7cf80982015-02-12 07:03:40 +000060 raise
Dave Pigott281203e2014-09-17 23:45:02 +010061
62 def __del__(self):
63 self.cursor.close()
Steve McIntyre89932392018-02-02 10:15:53 +000064 self.dictcursor.close()
Dave Pigott281203e2014-09-17 23:45:02 +010065 self.connection.close()
66
Steve McIntyreea343aa2015-10-23 17:46:17 +010067 # Create the state table (if needed) and add its only record
Steve McIntyre8fed4912018-01-31 17:21:31 +000068 #
69 # Use the stored record of the expected database schema to track what
70 # version the on-disk database is, and upgrade it to match the current code
71 # if necessary.
Steve McIntyreea343aa2015-10-23 17:46:17 +010072 def _init_state(self):
Steve McIntyre8fed4912018-01-31 17:21:31 +000073 found_db = False
74 current_db_version = 0
Steve McIntyreea343aa2015-10-23 17:46:17 +010075 try:
76 sql = "SELECT * FROM state"
77 self.cursor.execute(sql)
Steve McIntyre8fed4912018-01-31 17:21:31 +000078 found_db = True
Steve McIntyreea343aa2015-10-23 17:46:17 +010079 except psycopg2.ProgrammingError:
80 self.connection.commit() # state doesn't exist; clear error
Steve McIntyre8fed4912018-01-31 17:21:31 +000081 sql = "CREATE TABLE state (last_modified TIMESTAMP, schema_version INTEGER)"
Steve McIntyreea343aa2015-10-23 17:46:17 +010082 self.cursor.execute(sql)
Steve McIntyre8fed4912018-01-31 17:21:31 +000083 # We've just created a version 1 database
84 current_db_version = 1
85
86 if found_db:
87 # Grab the version of the database we have
88 try:
89 sql = "SELECT schema_version FROM state"
90 self.cursor.execute(sql)
91 current_db_version = self.cursor.fetchone()[0]
92 # No version found ==> we have "version 0"
93 except psycopg2.ProgrammingError:
94 self.connection.commit() # state doesn't exist; clear error
95 current_db_version = 0
96
97 # Now delete the existing state record, we'll write a new one in a
98 # moment
99 self.cursor.execute('DELETE FROM state')
100 logging.info("Found a database, version %d", current_db_version)
101
102 # Apply upgrades here!
103 if current_db_version < 1:
104 logging.info("Upgrading database to match schema version 1")
105 sql = "ALTER TABLE state ADD schema_version INTEGER"
106 self.cursor.execute(sql)
Steve McIntyre7d219202018-02-01 16:53:25 +0000107 logging.info("Schema version 1 upgrade successful")
108
109 if current_db_version < 2:
110 logging.info("Upgrading database to match schema version 2")
111 sql = "ALTER TABLE port ADD lock_reason VARCHAR(64)"
112 self.cursor.execute(sql)
113 logging.info("Schema version 2 upgrade successful")
Steve McIntyre8fed4912018-01-31 17:21:31 +0000114
115 sql = "INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)"
116 data = (datetime.datetime.now(), DATABASE_SCHEMA_VERSION)
Steve McIntyreea343aa2015-10-23 17:46:17 +0100117 self.cursor.execute(sql, data)
118 self.connection.commit()
119
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000120 # Create a new switch in the database. Switches are really simple
121 # devices - they're just containers for ports.
122 #
123 # Constraints:
124 # Switches must be uniquely named
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000125 def create_switch(self, name):
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000126
Steve McIntyre549435f2014-12-05 15:42:46 +0000127 switch_id = self.get_switch_id_by_name(name)
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000128 if switch_id is not None:
129 raise InputError("Switch name %s already exists" % name)
130
Dave Pigott2649a1a2014-09-18 00:04:49 +0100131 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000132 sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id"
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000133 data = (name, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000134 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100135 switch_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100136 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100137 self.connection.commit()
138 except:
139 self.connection.rollback()
140 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000141
Dave Pigott281203e2014-09-17 23:45:02 +0100142 return switch_id
143
Steve McIntyrec4890132015-08-07 15:19:11 +0100144 # Create a new port in the database. Three of the fields are
145 # created with default values (is_locked, is_trunk, trunk_id)
146 # here, and should be updated separately if desired. For the
147 # current_vlan_id and base_vlan_id fields, *BE CAREFUL* that you
148 # have already looked up the correct VLAN_ID for each. This is
149 # *NOT* the same as the VLAN tag (likely to be 1). You Have Been
150 # Warned!
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000151 #
152 # Constraints:
153 # 1. The switch referred to must already exist
154 # 2. The VLANs mentioned here must already exist
Steve McIntyre6a7fdb22014-12-05 15:17:30 +0000155 # 3. (Switch/name) must be unique
Steve McIntyreea753972015-08-05 13:52:48 +0100156 # 4. (Switch/number) must be unique
157 def create_port(self, switch_id, name, number, current_vlan_id, base_vlan_id):
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000158
Steve McIntyre549435f2014-12-05 15:42:46 +0000159 switch = self.get_switch_by_id(switch_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000160 if switch is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000161 raise NotFoundError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000162
163 for vlan_id in (current_vlan_id, base_vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000164 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000165 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000166 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +0000167
168 port_id = self.get_port_by_switch_and_name(switch_id, name)
169 if port_id is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000170 raise InputError("Already have a port %s on switch ID %d" % (name, int(switch_id)))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +0000171
Steve McIntyreea753972015-08-05 13:52:48 +0100172 port_id = self.get_port_by_switch_and_number(switch_id, int(number))
173 if port_id is not None:
174 raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id)))
175
Dave Pigott2649a1a2014-09-18 00:04:49 +0100176 try:
Steve McIntyre7d219202018-02-01 16:53:25 +0000177 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"
Steve McIntyreea753972015-08-05 13:52:48 +0100178 data = (name, number, switch_id,
Steve McIntyre7d219202018-02-01 16:53:25 +0000179 False, "",
180 False,
Steve McIntyrec4890132015-08-07 15:19:11 +0100181 current_vlan_id, base_vlan_id, TRUNK_ID_NONE)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000182 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100183 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100184 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100185 self.connection.commit()
186 except:
187 self.connection.rollback()
188 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000189
Dave Pigott281203e2014-09-17 23:45:02 +0100190 return port_id
191
Steve McIntyreb005a2f2014-11-28 18:23:05 +0000192 # Create a new vlan in the database. We locally add a creation
193 # timestamp, for debug purposes. If vlans seems to be sticking
194 # around, we'll be able to see when they were created.
Steve McIntyre31b2df52014-12-02 12:37:54 +0000195 #
196 # Constraints:
197 # Names and tags must be unique
Steve McIntyre57f45912014-12-08 14:43:00 +0000198 # Tags must be in the range 1-4095 (802.1q spec)
Steve McIntyre49777e72014-12-08 16:00:46 +0000199 # Names can be any free-form text, length 1-32 characters
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000200 def create_vlan(self, name, tag, is_base_vlan):
Steve McIntyre31b2df52014-12-02 12:37:54 +0000201
Steve McIntyre57f45912014-12-08 14:43:00 +0000202 if int(tag) < 1 or int(tag) > 4095:
Steve McIntyre49777e72014-12-08 16:00:46 +0000203 raise InputError("VLAN tag %d is outside of the valid range (1-4095)" % int(tag))
204
205 if (len(name) < 1) or (len(name) > 32):
206 raise InputError("VLAN name %s is invalid (must be 1-32 chars)" % name)
Steve McIntyre57f45912014-12-08 14:43:00 +0000207
Steve McIntyrea34c1812014-12-05 15:27:55 +0000208 vlan_id = self.get_vlan_id_by_name(name)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000209 if vlan_id is not None:
210 raise InputError("VLAN name %s is already in use" % name)
211
Steve McIntyre50eb0602014-12-05 15:29:04 +0000212 vlan_id = self.get_vlan_id_by_tag(tag)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000213 if vlan_id is not None:
214 raise InputError("VLAN tag %d is already in use" % int(tag))
215
Dave Pigott2649a1a2014-09-18 00:04:49 +0100216 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +0000217 dt = datetime.datetime.now()
Steve McIntyre4b918132014-12-05 17:04:46 +0000218 sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id"
219 data = (name, tag, is_base_vlan, dt)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000220 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100221 vlan_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100222 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100223 self.connection.commit()
224 except:
225 self.connection.rollback()
226 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000227
Dave Pigott281203e2014-09-17 23:45:02 +0100228 return vlan_id
229
Steve McIntyrec4890132015-08-07 15:19:11 +0100230 # Create a new trunk in the database, linking two ports. Trunks
231 # are really simple objects for our use - they're just containers
232 # for 2 ports.
233 #
234 # Constraints:
235 # 1. Both ports listed must already exist.
236 # 2. Both ports must be in trunk mode.
237 # 3. Both must not be locked.
238 # 4. Both must not already be in a trunk.
239 def create_trunk(self, port_id1, port_id2):
240
241 for port_id in (port_id1, port_id2):
242 port = self.get_port_by_id(int(port_id))
243 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000244 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyreca115d22018-02-07 00:03:49 +0000245 if not port['is_trunk']:
Steve McIntyrec4890132015-08-07 15:19:11 +0100246 raise InputError("Port ID %d is not in trunk mode" % int(port_id))
Steve McIntyreca115d22018-02-07 00:03:49 +0000247 if port['is_locked']:
Steve McIntyrec4890132015-08-07 15:19:11 +0100248 raise InputError("Port ID %d is locked" % int(port_id))
Steve McIntyreca115d22018-02-07 00:03:49 +0000249 if port['trunk_id'] != TRUNK_ID_NONE:
250 raise InputError("Port ID %d is already on trunk ID %d" % (int(port_id), int(port['trunk_id'])))
Steve McIntyrec4890132015-08-07 15:19:11 +0100251
252 try:
253 # Add the trunk itself
254 dt = datetime.datetime.now()
255 sql = "INSERT INTO trunk (creation_time) VALUES (%s) RETURNING trunk_id"
256 data = (dt, )
257 self.cursor.execute(sql, data)
258 trunk_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100259 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyrec4890132015-08-07 15:19:11 +0100260 self.connection.commit()
261 # And update the ports
262 for port_id in (port_id1, port_id2):
263 self._set_port_trunk(port_id, trunk_id)
264 except:
265 self.delete_trunk(trunk_id)
266 raise
267
268 return trunk_id
269
Steve McIntyre2d685c72014-12-08 15:24:12 +0000270 # Internal helper function
Dave Pigott281203e2014-09-17 23:45:02 +0100271 def _delete_row(self, table, field, value):
Dave Pigott2649a1a2014-09-18 00:04:49 +0100272 try:
Steve McIntyree03de002014-12-02 17:14:14 +0000273 sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s')
274 data = (value,)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000275 self.cursor.execute(sql, data)
Steve McIntyreea343aa2015-10-23 17:46:17 +0100276 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100277 self.connection.commit()
278 except:
279 self.connection.rollback()
280 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100281
Steve McIntyre388f0e22014-12-02 17:19:04 +0000282 # Delete the specified switch
283 #
284 # Constraints:
285 # 1. The switch must exist
286 # 2. The switch may not be referenced by any ports -
287 # delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100288 def delete_switch(self, switch_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000289 switch = self.get_switch_by_id(switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000290 if switch is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000291 raise NotFoundError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyre388f0e22014-12-02 17:19:04 +0000292 ports = self.get_ports_by_switch(switch_id)
293 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000294 raise InputError("Cannot delete switch ID %d when it still has %d ports" %
295 (int(switch_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100296 self._delete_row("switch", "switch_id", switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000297 return switch_id
Dave Pigott281203e2014-09-17 23:45:02 +0100298
Steve McIntyre6a968622014-12-02 18:01:41 +0000299 # Delete the specified port
300 #
301 # Constraints:
302 # 1. The port must exist
303 # 2. The port must not be locked
Dave Pigott281203e2014-09-17 23:45:02 +0100304 def delete_port(self, port_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000305 port = self.get_port_by_id(port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000306 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000307 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyreca115d22018-02-07 00:03:49 +0000308 if port['is_locked']:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000309 raise InputError("Cannot delete port ID %d as it is locked" % int(port_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100310 self._delete_row("port", "port_id", port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000311 return port_id
Dave Pigott281203e2014-09-17 23:45:02 +0100312
Steve McIntyre14552ac2014-12-05 15:23:57 +0000313 # Delete the specified VLAN
314 #
315 # Constraints:
Steve McIntyre2a5df972015-08-07 15:19:40 +0100316 # 1. The VLAN must exist
Steve McIntyre14552ac2014-12-05 15:23:57 +0000317 # 2. The VLAN may not contain any ports - move or delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100318 def delete_vlan(self, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000319 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000320 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000321 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000322 ports = self.get_ports_by_current_vlan(vlan_id)
323 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000324 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
325 (int(vlan_id), len(ports)))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000326 ports = self.get_ports_by_base_vlan(vlan_id)
327 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000328 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
329 (int(vlan_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100330 self._delete_row("vlan", "vlan_id", vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000331 return vlan_id
Dave Pigott281203e2014-09-17 23:45:02 +0100332
Steve McIntyrec4890132015-08-07 15:19:11 +0100333 # Delete the specified trunk
334 #
335 # Constraints:
336 # 1. The trunk must exist
337 #
338 # Any ports attached will be detached (i.e. moved to trunk TRUNK_ID_NONE)
339 def delete_trunk(self, trunk_id):
340 trunk = self.get_trunk_by_id(trunk_id)
341 if trunk is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000342 raise NotFoundError("Trunk ID %d does not exist" % int(trunk_id))
Steve McIntyrec4890132015-08-07 15:19:11 +0100343 ports = self.get_ports_by_trunk(trunk_id)
344 for port_id in ports:
345 self._set_port_trunk(port_id, TRUNK_ID_NONE)
346 self._delete_row("trunk", "trunk_id", trunk_id)
347 return trunk_id
348
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100349 # Find the lowest unused VLAN tag and return it
350 #
351 # Constraints:
352 # None
353 def find_lowest_unused_vlan_tag(self):
354 sql = "SELECT tag FROM vlan ORDER BY tag ASC"
355 self.cursor.execute(sql,)
356
357 # Walk through the list, looking for gaps
358 last = 1
359 result = None
360
361 for record in self.cursor:
362 if (record[0] - last) > 1:
363 result = last + 1
364 break
365 last = record[0]
366
367 if result is None:
368 result = last + 1
369
370 if result > 4093:
371 raise CriticalError("Can't find any VLAN tags remaining for allocation!")
372
373 return result
374
Steve McIntyre2d685c72014-12-08 15:24:12 +0000375 # Grab one column from one row of a query on one column; useful as
376 # a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100377 def _get_element(self, select_field, table, compare_field, value):
Steve McIntyre95614c22014-11-28 17:02:44 +0000378
Steve McIntyredc173072016-08-25 18:29:10 +0100379 if value is None:
380 raise ValueError("Asked to look up using None as a key in %s" % compare_field)
381
Steve McIntyre95614c22014-11-28 17:02:44 +0000382 # We really want to use psycopg's type handling deal with the
383 # (potentially) user-supplied data in the value field, so we
384 # have to pass (sql,data) through to cursor.execute. However,
385 # we can't have psycopg do all the argument substitution here
386 # as it will quote all the params like the table name. That
387 # doesn't work. So, we substitute a "%s" for "%s" here so we
388 # keep it after python's own string substitution.
389 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
390
391 # Now, the next icky thing: we need to make sure that we're
392 # passing a dict so that psycopg2 can pick it apart properly
393 # for its own substitution code. We force this with the
394 # trailing comma here
395 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000396 self.cursor.execute(sql, data)
Steve McIntyre95614c22014-11-28 17:02:44 +0000397
Steve McIntyre58b57a42014-12-02 13:09:21 +0000398 if self.cursor.rowcount > 0:
399 return self.cursor.fetchone()[0]
400 else:
Steve McIntyrec831f9c2014-12-02 12:38:54 +0000401 return None
Dave Pigott281203e2014-09-17 23:45:02 +0100402
Steve McIntyre2d685c72014-12-08 15:24:12 +0000403 # Grab one column from one row of a query on 2 columns; useful as
404 # a quick wrapper
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000405 def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2):
406
Steve McIntyredc173072016-08-25 18:29:10 +0100407 if value1 is None:
408 raise ValueError("Asked to look up using None as a key in %s" % compare_field1)
409 if value2 is None:
410 raise ValueError("Asked to look up using None as a key in %s" % compare_field2)
411
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000412 # We really want to use psycopg's type handling deal with the
413 # (potentially) user-supplied data in the value field, so we
414 # have to pass (sql,data) through to cursor.execute. However,
415 # we can't have psycopg do all the argument substitution here
416 # as it will quote all the params like the table name. That
417 # doesn't work. So, we substitute a "%s" for "%s" here so we
418 # keep it after python's own string substitution.
419 sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s")
420
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000421 data = (value1, value2)
422 self.cursor.execute(sql, data)
423
424 if self.cursor.rowcount > 0:
425 return self.cursor.fetchone()[0]
426 else:
427 return None
428
Steve McIntyre2d685c72014-12-08 15:24:12 +0000429 # Grab one column from multiple rows of a query; useful as a quick
430 # wrapper
Steve McIntyre05e3e622015-09-25 01:29:18 +0100431 def _get_multi_elements(self, select_field, table, compare_field, value, sort_field):
Steve McIntyree9da15e2014-12-05 15:22:41 +0000432
Steve McIntyredc173072016-08-25 18:29:10 +0100433 if value is None:
434 raise ValueError("Asked to look up using None as a key in %s" % compare_field)
435
Steve McIntyree9da15e2014-12-05 15:22:41 +0000436 # We really want to use psycopg's type handling deal with the
437 # (potentially) user-supplied data in the value field, so we
438 # have to pass (sql,data) through to cursor.execute. However,
439 # we can't have psycopg do all the argument substitution here
440 # as it will quote all the params like the table name. That
441 # doesn't work. So, we substitute a "%s" for "%s" here so we
442 # keep it after python's own string substitution.
Steve McIntyre05e3e622015-09-25 01:29:18 +0100443 sql = "SELECT %s FROM %s WHERE %s = %s ORDER BY %s ASC" % (select_field, table, compare_field, "%s", sort_field)
Steve McIntyree9da15e2014-12-05 15:22:41 +0000444
445 # Now, the next icky thing: we need to make sure that we're
446 # passing a dict so that psycopg2 can pick it apart properly
447 # for its own substitution code. We force this with the
448 # trailing comma here
449 data = (value, )
450 self.cursor.execute(sql, data)
451
452 if self.cursor.rowcount > 0:
453 results = []
454 for record in self.cursor:
Steve McIntyre52509622014-12-02 17:13:15 +0000455 results.append(record[0])
Steve McIntyree9da15e2014-12-05 15:22:41 +0000456 return results
Steve McIntyre52509622014-12-02 17:13:15 +0000457 else:
458 return None
459
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000460 # Grab one column from multiple rows of a 2-part query; useful as
461 # a wrapper
Steve McIntyre05e3e622015-09-25 01:29:18 +0100462 def _get_multi_elements2(self, select_field, table, compare_field1, value1, compare_field2, value2, sort_field):
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000463
Steve McIntyredc173072016-08-25 18:29:10 +0100464 if value1 is None:
465 raise ValueError("Asked to look up using None as a key in %s" % compare_field1)
466 if value2 is None:
467 raise ValueError("Asked to look up using None as a key in %s" % compare_field2)
468
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000469 # We really want to use psycopg's type handling deal with the
470 # (potentially) user-supplied data in the value field, so we
471 # have to pass (sql,data) through to cursor.execute. However,
472 # we can't have psycopg do all the argument substitution here
473 # as it will quote all the params like the table name. That
474 # doesn't work. So, we substitute a "%s" for "%s" here so we
475 # keep it after python's own string substitution.
Steve McIntyre05e3e622015-09-25 01:29:18 +0100476 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)
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000477
478 data = (value1, value2)
479 self.cursor.execute(sql, data)
480
481 if self.cursor.rowcount > 0:
482 results = []
483 for record in self.cursor:
484 results.append(record[0])
485 return results
486 else:
487 return None
488
Steve McIntyre2d685c72014-12-08 15:24:12 +0000489 # Simple lookup: look up a switch by ID, and return all the
490 # details of that switch.
491 #
492 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000493 def get_switch_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000494 return self._get_row("switch", "switch_id", int(switch_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000495
Steve McIntyre2d685c72014-12-08 15:24:12 +0000496 # Simple lookup: look up a switch by name, and return the ID of
497 # that switch.
498 #
499 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000500 def get_switch_id_by_name(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100501 return self._get_element("switch_id", "switch", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100502
Steve McIntyre2d685c72014-12-08 15:24:12 +0000503 # Simple lookup: look up a switch by ID, and return the name of
504 # that switch.
505 #
506 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000507 def get_switch_name_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000508 return self._get_element("name", "switch", "switch_id", int(switch_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100509
Steve McIntyre2d685c72014-12-08 15:24:12 +0000510 # Simple lookup: look up a port by ID, and return all the details
511 # of that port.
512 #
513 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000514 def get_port_by_id(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000515 return self._get_row("port", "port_id", int(port_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000516
Steve McIntyre2d685c72014-12-08 15:24:12 +0000517 # Simple lookup: look up a switch by ID, and return the IDs of all
518 # the ports on that switch.
519 #
520 # Returns None on failure.
Steve McIntyreb67f3912014-12-02 17:14:36 +0000521 def get_ports_by_switch(self, switch_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100522 return self._get_multi_elements("port_id", "port", "switch_id", int(switch_id), "port_id")
Steve McIntyreb67f3912014-12-02 17:14:36 +0000523
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000524 # More complex lookup: look up all the trunk ports on a switch by
525 # ID
526 #
527 # Returns None on failure.
528 def get_trunk_port_names_by_switch(self, switch_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100529 return self._get_multi_elements2("name", "port", "switch_id", int(switch_id), "is_trunk", True, "port_id")
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000530
Steve McIntyre2d685c72014-12-08 15:24:12 +0000531 # Simple lookup: look up a port by its name and its parent switch
532 # by ID, and return the ID of the port.
533 #
534 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000535 def get_port_by_switch_and_name(self, switch_id, name):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000536 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "name", name)
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000537
Steve McIntyre45f55012015-08-05 13:55:15 +0100538 # Simple lookup: look up a port by its external name and its
539 # parent switch by ID, and return the ID of the port.
540 #
541 # Returns None on failure.
542 def get_port_by_switch_and_number(self, switch_id, number):
543 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "number", int(number))
544
Steve McIntyre2d685c72014-12-08 15:24:12 +0000545 # Simple lookup: look up a port by ID, and return the current VLAN
546 # id of that port.
547 #
548 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000549 def get_current_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000550 return self._get_element("current_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000551
Steve McIntyredfeac092018-02-07 00:57:49 +0000552 # Simple lookup: look up a port by ID, and return the mode of that port.
553 #
554 # Returns None on failure.
555 def get_port_mode(self, port_id):
556 is_trunk = self._get_element("is_trunk", "port", "port_id", int(port_id))
557 if is_trunk is not None:
558 if is_trunk:
559 return "trunk"
560 else:
561 return "access"
562 return None
563
Steve McIntyre2d685c72014-12-08 15:24:12 +0000564 # Simple lookup: look up a port by ID, and return the base VLAN
565 # id of that port.
566 #
567 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000568 def get_base_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000569 return self._get_element("base_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000570
Steve McIntyre2d685c72014-12-08 15:24:12 +0000571 # Simple lookup: look up a current VLAN by ID, and return the IDs
572 # of all the ports on that VLAN.
573 #
574 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000575 def get_ports_by_current_vlan(self, vlan_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100576 return self._get_multi_elements("port_id", "port", "current_vlan_id", int(vlan_id), "port_id")
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000577
Steve McIntyre2d685c72014-12-08 15:24:12 +0000578 # Simple lookup: look up a base VLAN by ID, and return the IDs
579 # of all the ports on that VLAN.
580 #
581 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000582 def get_ports_by_base_vlan(self, vlan_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100583 return self._get_multi_elements("port_id", "port", "base_vlan_id", int(vlan_id), "port_id")
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000584
Steve McIntyrec4890132015-08-07 15:19:11 +0100585 # Simple lookup: look up a trunk by ID, and return the IDs of the
586 # ports on both ends of that trunk.
587 #
588 # Returns None on failure.
589 def get_ports_by_trunk(self, trunk_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100590 return self._get_multi_elements("port_id", "port", "trunk_id", int(trunk_id), "port_id")
Steve McIntyrec4890132015-08-07 15:19:11 +0100591
Steve McIntyre2d685c72014-12-08 15:24:12 +0000592 # Simple lookup: look up a VLAN by ID, and return all the details
593 # of that VLAN.
594 #
595 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000596 def get_vlan_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000597 return self._get_row("vlan", "vlan_id", int(vlan_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000598
Steve McIntyre2d685c72014-12-08 15:24:12 +0000599 # Simple lookup: look up a VLAN by name, and return the ID of that
600 # VLAN.
601 #
602 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000603 def get_vlan_id_by_name(self, name):
604 return self._get_element("vlan_id", "vlan", "name", name)
605
Steve McIntyre2d685c72014-12-08 15:24:12 +0000606 # Simple lookup: look up a VLAN by tag, and return the ID of that
607 # VLAN.
608 #
609 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000610 def get_vlan_id_by_tag(self, tag):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000611 return self._get_element("vlan_id", "vlan", "tag", int(tag))
Steve McIntyref3655062014-12-05 15:34:39 +0000612
Steve McIntyre2d685c72014-12-08 15:24:12 +0000613 # Simple lookup: look up a VLAN by ID, and return the name of that
614 # VLAN.
615 #
616 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000617 def get_vlan_name_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000618 return self._get_element("name", "vlan", "vlan_id", int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100619
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000620 # Simple lookup: look up a VLAN by ID, and return the tag of that
621 # VLAN.
622 #
623 # Returns None on failure.
624 def get_vlan_tag_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000625 return self._get_element("tag", "vlan", "vlan_id", int(vlan_id))
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000626
Steve McIntyrec4890132015-08-07 15:19:11 +0100627 # Simple lookup: look up a trunk by ID, and return all the details
628 # of that trunk.
629 #
630 # Returns None on failure.
631 def get_trunk_by_id(self, trunk_id):
632 return self._get_row("trunk", "trunk_id", int(trunk_id))
633
Steve McIntyreea343aa2015-10-23 17:46:17 +0100634 # Get the last-modified time for the database
635 def get_last_modified_time(self):
636 sql = "SELECT last_modified FROM state"
637 self.cursor.execute(sql)
Steve McIntyreaf24aaa2015-10-23 17:59:04 +0100638 return self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100639
Steve McIntyre2d685c72014-12-08 15:24:12 +0000640 # Grab one row of a query on one column; useful as a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100641 def _get_row(self, table, field, value):
Steve McIntyree0b842a2014-11-28 18:23:47 +0000642
643 # We really want to use psycopg's type handling deal with the
644 # (potentially) user-supplied data in the value field, so we
645 # have to pass (sql,data) through to cursor.execute. However,
646 # we can't have psycopg do all the argument substitution here
647 # as it will quote all the params like the table name. That
648 # doesn't work. So, we substitute a "%s" for "%s" here so we
649 # keep it after python's own string substitution.
650 sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s")
651
652 # Now, the next icky thing: we need to make sure that we're
653 # passing a dict so that psycopg2 can pick it apart properly
654 # for its own substitution code. We force this with the
655 # trailing comma here
656 data = (value, )
Steve McIntyre89932392018-02-02 10:15:53 +0000657 self.dictcursor.execute(sql, data)
658 return self.dictcursor.fetchone()
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100659
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000660 # (Un)Lock a port in the database. This can only be done through
661 # the admin interface, and will stop API users from modifying
662 # settings on the port. Use this to lock down ports that are used
663 # for PDUs and other core infrastructure
Steve McIntyre7d219202018-02-01 16:53:25 +0000664 def set_port_is_locked(self, port_id, is_locked, lock_reason=""):
Steve McIntyre8c64d952014-12-05 16:22:44 +0000665 port = self.get_port_by_id(port_id)
666 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000667 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000668 try:
Steve McIntyre7d219202018-02-01 16:53:25 +0000669 sql = "UPDATE port SET is_locked=%s, lock_reason=%s WHERE port_id=%s RETURNING port_id"
670 data = (is_locked, lock_reason, port_id)
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000671 self.cursor.execute(sql, data)
672 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100673 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000674 self.connection.commit()
675 except:
676 self.connection.rollback()
Steve McIntyre7d219202018-02-01 16:53:25 +0000677 raise InputError("lock failed on Port ID %d" % int(port_id))
Steve McIntyre1c8a3212015-07-14 17:07:31 +0100678 return port_id
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000679
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000680 # Set the mode of a port in the database. Valid values for mode
681 # are "trunk" and "access"
682 def set_port_mode(self, port_id, mode):
683 port = self.get_port_by_id(port_id)
684 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000685 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000686 if mode == "access":
687 is_trunk = False
688 elif mode == "trunk":
689 is_trunk = True
690 else:
691 raise InputError("Port mode %s is not valid" % mode)
692 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000693 sql = "UPDATE port SET is_trunk=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000694 data = (is_trunk, port_id)
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000695 self.cursor.execute(sql, data)
696 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100697 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000698 self.connection.commit()
699 except:
700 self.connection.rollback()
701 raise
702 return port_id
703
Steve McIntyre2d685c72014-12-08 15:24:12 +0000704 # Set the current vlan of a port in the database. The VLAN is
705 # passed by ID.
706 #
707 # Constraints:
708 # 1. The port must already exist
709 # 2. The port must not be a trunk port
710 # 3. The port must not be locked
711 # 1. The VLAN must already exist in the database
Steve McIntyre9eb78652014-12-05 17:51:53 +0000712 def set_current_vlan(self, port_id, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000713 port = self.get_port_by_id(port_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000714 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000715 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100716
Steve McIntyreca115d22018-02-07 00:03:49 +0000717 if port['is_trunk'] or port['is_locked']:
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100718 raise CriticalError("The port is locked")
719
Steve McIntyre549435f2014-12-05 15:42:46 +0000720 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000721 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000722 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100723
724 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000725 sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000726 data = (vlan_id, port_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000727 self.cursor.execute(sql, data)
Steve McIntyree1371102014-12-05 17:17:09 +0000728 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100729 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyree1371102014-12-05 17:17:09 +0000730 self.connection.commit()
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100731 except:
732 self.connection.rollback()
733 raise
Steve McIntyree1371102014-12-05 17:17:09 +0000734 return port_id
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100735
Steve McIntyre2d685c72014-12-08 15:24:12 +0000736 # Set the base vlan of a port in the database. The VLAN is
737 # passed by ID.
738 #
739 # Constraints:
740 # 1. The port must already exist
741 # 2. The port must not be a trunk port
742 # 3. The port must not be locked
Steve McIntyree653d172015-08-06 16:51:18 +0100743 # 4. The VLAN must already exist in the database
Steve McIntyredaae5502014-12-05 17:55:18 +0000744 def set_base_vlan(self, port_id, vlan_id):
745 port = self.get_port_by_id(port_id)
746 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000747 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000748
Steve McIntyreca115d22018-02-07 00:03:49 +0000749 if port['is_trunk'] or port['is_locked']:
Steve McIntyredaae5502014-12-05 17:55:18 +0000750 raise CriticalError("The port is locked")
751
752 vlan = self.get_vlan_by_id(vlan_id)
753 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000754 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyreca115d22018-02-07 00:03:49 +0000755 if not vlan['is_base_vlan']:
Steve McIntyredaae5502014-12-05 17:55:18 +0000756 raise InputError("VLAN ID %d is not a base VLAN" % int(vlan_id))
757
758 try:
759 sql = "UPDATE port SET base_vlan_id=%s WHERE port_id=%s RETURNING port_id"
760 data = (vlan_id, port_id)
761 self.cursor.execute(sql, data)
762 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100763 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyredaae5502014-12-05 17:55:18 +0000764 self.connection.commit()
765 except:
766 self.connection.rollback()
767 raise
768 return port_id
769
Steve McIntyrec4890132015-08-07 15:19:11 +0100770 # Internal function: Attach a port to a trunk in the database.
771 #
772 # Constraints:
773 # 1. The port must already exist
774 # 2. The port must not be locked
775 def _set_port_trunk(self, port_id, trunk_id):
776 port = self.get_port_by_id(port_id)
777 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000778 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyreca115d22018-02-07 00:03:49 +0000779 if port['is_locked']:
Steve McIntyrec4890132015-08-07 15:19:11 +0100780 raise CriticalError("The port is locked")
781 try:
782 sql = "UPDATE port SET trunk_id=%s WHERE port_id=%s RETURNING port_id"
783 data = (int(trunk_id), int(port_id))
784 self.cursor.execute(sql, data)
785 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100786 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyrec4890132015-08-07 15:19:11 +0100787 self.connection.commit()
788 except:
789 self.connection.rollback()
790 raise
791 return port_id
792
Steve McIntyre2d685c72014-12-08 15:24:12 +0000793 # Trivial helper function to return all the rows in a given table
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100794 def _dump_table(self, table, order):
Dave Pigott281203e2014-09-17 23:45:02 +0100795 result = []
Steve McIntyre89932392018-02-02 10:15:53 +0000796 self.dictcursor.execute("SELECT * FROM %s ORDER by %s ASC" % (table, order))
797 record = self.dictcursor.fetchone()
Dave Pigott281203e2014-09-17 23:45:02 +0100798 while record != None:
Steve McIntyree73eb122014-11-27 15:18:47 +0000799 result.append(record)
Steve McIntyre89932392018-02-02 10:15:53 +0000800 record = self.dictcursor.fetchone()
Dave Pigott281203e2014-09-17 23:45:02 +0100801 return result
802
803 def all_switches(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100804 return self._dump_table("switch", "switch_id")
Dave Pigott281203e2014-09-17 23:45:02 +0100805
806 def all_ports(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100807 return self._dump_table("port", "port_id")
Dave Pigott281203e2014-09-17 23:45:02 +0100808
809 def all_vlans(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100810 return self._dump_table("vlan", "vlan_id")
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100811
Steve McIntyrec4890132015-08-07 15:19:11 +0100812 def all_trunks(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100813 return self._dump_table("trunk", "trunk_id")
Steve McIntyrec4890132015-08-07 15:19:11 +0100814
Steve McIntyre6a618162014-12-10 16:47:07 +0000815if __name__ == '__main__':
816 db = VlanDB()
Steve McIntyre6d84ec12014-12-18 16:56:56 +0000817 s = db.all_switches()
818 print 'The DB knows about %d switch(es)' % len(s)
819 print s
820 p = db.all_ports()
821 print 'The DB knows about %d port(s)' % len(p)
822 print p
823 v = db.all_vlans()
824 print 'The DB knows about %d vlan(s)' % len(v)
825 print v
Steve McIntyrec4890132015-08-07 15:19:11 +0100826 t = db.all_trunks()
827 print 'The DB knows about %d trunks(s)' % len(t)
828 print t
Steve McIntyre6a618162014-12-10 16:47:07 +0000829
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100830 print 'First free VLAN tag is %d' % db.find_lowest_unused_vlan_tag()