blob: 003f973ab7edbe55cc5b8d2c891f1a42fd1a9217 [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 McIntyreb09ed282014-12-02 17:59:35 +000051 self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
Steve McIntyreea343aa2015-10-23 17:46:17 +010052 if not readonly:
53 self._init_state()
Dave Pigott281203e2014-09-17 23:45:02 +010054 except Exception as e:
Steve McIntyre5fa22652015-04-01 18:01:45 +010055 logging.error("Failed to access database: %s", e)
Steve McIntyre7cf80982015-02-12 07:03:40 +000056 raise
Dave Pigott281203e2014-09-17 23:45:02 +010057
58 def __del__(self):
59 self.cursor.close()
60 self.connection.close()
61
Steve McIntyreea343aa2015-10-23 17:46:17 +010062 # Create the state table (if needed) and add its only record
Steve McIntyre8fed4912018-01-31 17:21:31 +000063 #
64 # Use the stored record of the expected database schema to track what
65 # version the on-disk database is, and upgrade it to match the current code
66 # if necessary.
Steve McIntyreea343aa2015-10-23 17:46:17 +010067 def _init_state(self):
Steve McIntyre8fed4912018-01-31 17:21:31 +000068 found_db = False
69 current_db_version = 0
Steve McIntyreea343aa2015-10-23 17:46:17 +010070 try:
71 sql = "SELECT * FROM state"
72 self.cursor.execute(sql)
Steve McIntyre8fed4912018-01-31 17:21:31 +000073 found_db = True
Steve McIntyreea343aa2015-10-23 17:46:17 +010074 except psycopg2.ProgrammingError:
75 self.connection.commit() # state doesn't exist; clear error
Steve McIntyre8fed4912018-01-31 17:21:31 +000076 sql = "CREATE TABLE state (last_modified TIMESTAMP, schema_version INTEGER)"
Steve McIntyreea343aa2015-10-23 17:46:17 +010077 self.cursor.execute(sql)
Steve McIntyre8fed4912018-01-31 17:21:31 +000078 # We've just created a version 1 database
79 current_db_version = 1
80
81 if found_db:
82 # Grab the version of the database we have
83 try:
84 sql = "SELECT schema_version FROM state"
85 self.cursor.execute(sql)
86 current_db_version = self.cursor.fetchone()[0]
87 # No version found ==> we have "version 0"
88 except psycopg2.ProgrammingError:
89 self.connection.commit() # state doesn't exist; clear error
90 current_db_version = 0
91
92 # Now delete the existing state record, we'll write a new one in a
93 # moment
94 self.cursor.execute('DELETE FROM state')
95 logging.info("Found a database, version %d", current_db_version)
96
97 # Apply upgrades here!
98 if current_db_version < 1:
99 logging.info("Upgrading database to match schema version 1")
100 sql = "ALTER TABLE state ADD schema_version INTEGER"
101 self.cursor.execute(sql)
Steve McIntyre7d219202018-02-01 16:53:25 +0000102 logging.info("Schema version 1 upgrade successful")
103
104 if current_db_version < 2:
105 logging.info("Upgrading database to match schema version 2")
106 sql = "ALTER TABLE port ADD lock_reason VARCHAR(64)"
107 self.cursor.execute(sql)
108 logging.info("Schema version 2 upgrade successful")
Steve McIntyre8fed4912018-01-31 17:21:31 +0000109
110 sql = "INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)"
111 data = (datetime.datetime.now(), DATABASE_SCHEMA_VERSION)
Steve McIntyreea343aa2015-10-23 17:46:17 +0100112 self.cursor.execute(sql, data)
113 self.connection.commit()
114
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000115 # Create a new switch in the database. Switches are really simple
116 # devices - they're just containers for ports.
117 #
118 # Constraints:
119 # Switches must be uniquely named
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000120 def create_switch(self, name):
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000121
Steve McIntyre549435f2014-12-05 15:42:46 +0000122 switch_id = self.get_switch_id_by_name(name)
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000123 if switch_id is not None:
124 raise InputError("Switch name %s already exists" % name)
125
Dave Pigott2649a1a2014-09-18 00:04:49 +0100126 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000127 sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id"
Steve McIntyre31d6dfa2014-12-02 12:35:56 +0000128 data = (name, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000129 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100130 switch_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100131 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100132 self.connection.commit()
133 except:
134 self.connection.rollback()
135 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000136
Dave Pigott281203e2014-09-17 23:45:02 +0100137 return switch_id
138
Steve McIntyrec4890132015-08-07 15:19:11 +0100139 # Create a new port in the database. Three of the fields are
140 # created with default values (is_locked, is_trunk, trunk_id)
141 # here, and should be updated separately if desired. For the
142 # current_vlan_id and base_vlan_id fields, *BE CAREFUL* that you
143 # have already looked up the correct VLAN_ID for each. This is
144 # *NOT* the same as the VLAN tag (likely to be 1). You Have Been
145 # Warned!
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000146 #
147 # Constraints:
148 # 1. The switch referred to must already exist
149 # 2. The VLANs mentioned here must already exist
Steve McIntyre6a7fdb22014-12-05 15:17:30 +0000150 # 3. (Switch/name) must be unique
Steve McIntyreea753972015-08-05 13:52:48 +0100151 # 4. (Switch/number) must be unique
152 def create_port(self, switch_id, name, number, current_vlan_id, base_vlan_id):
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000153
Steve McIntyre549435f2014-12-05 15:42:46 +0000154 switch = self.get_switch_by_id(switch_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000155 if switch is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000156 raise NotFoundError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000157
158 for vlan_id in (current_vlan_id, base_vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000159 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +0000160 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000161 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +0000162
163 port_id = self.get_port_by_switch_and_name(switch_id, name)
164 if port_id is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000165 raise InputError("Already have a port %s on switch ID %d" % (name, int(switch_id)))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +0000166
Steve McIntyreea753972015-08-05 13:52:48 +0100167 port_id = self.get_port_by_switch_and_number(switch_id, int(number))
168 if port_id is not None:
169 raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id)))
170
Dave Pigott2649a1a2014-09-18 00:04:49 +0100171 try:
Steve McIntyre7d219202018-02-01 16:53:25 +0000172 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 +0100173 data = (name, number, switch_id,
Steve McIntyre7d219202018-02-01 16:53:25 +0000174 False, "",
175 False,
Steve McIntyrec4890132015-08-07 15:19:11 +0100176 current_vlan_id, base_vlan_id, TRUNK_ID_NONE)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000177 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100178 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100179 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100180 self.connection.commit()
181 except:
182 self.connection.rollback()
183 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000184
Dave Pigott281203e2014-09-17 23:45:02 +0100185 return port_id
186
Steve McIntyreb005a2f2014-11-28 18:23:05 +0000187 # Create a new vlan in the database. We locally add a creation
188 # timestamp, for debug purposes. If vlans seems to be sticking
189 # around, we'll be able to see when they were created.
Steve McIntyre31b2df52014-12-02 12:37:54 +0000190 #
191 # Constraints:
192 # Names and tags must be unique
Steve McIntyre57f45912014-12-08 14:43:00 +0000193 # Tags must be in the range 1-4095 (802.1q spec)
Steve McIntyre49777e72014-12-08 16:00:46 +0000194 # Names can be any free-form text, length 1-32 characters
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000195 def create_vlan(self, name, tag, is_base_vlan):
Steve McIntyre31b2df52014-12-02 12:37:54 +0000196
Steve McIntyre57f45912014-12-08 14:43:00 +0000197 if int(tag) < 1 or int(tag) > 4095:
Steve McIntyre49777e72014-12-08 16:00:46 +0000198 raise InputError("VLAN tag %d is outside of the valid range (1-4095)" % int(tag))
199
200 if (len(name) < 1) or (len(name) > 32):
201 raise InputError("VLAN name %s is invalid (must be 1-32 chars)" % name)
Steve McIntyre57f45912014-12-08 14:43:00 +0000202
Steve McIntyrea34c1812014-12-05 15:27:55 +0000203 vlan_id = self.get_vlan_id_by_name(name)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000204 if vlan_id is not None:
205 raise InputError("VLAN name %s is already in use" % name)
206
Steve McIntyre50eb0602014-12-05 15:29:04 +0000207 vlan_id = self.get_vlan_id_by_tag(tag)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000208 if vlan_id is not None:
209 raise InputError("VLAN tag %d is already in use" % int(tag))
210
Dave Pigott2649a1a2014-09-18 00:04:49 +0100211 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +0000212 dt = datetime.datetime.now()
Steve McIntyre4b918132014-12-05 17:04:46 +0000213 sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id"
214 data = (name, tag, is_base_vlan, dt)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000215 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100216 vlan_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100217 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100218 self.connection.commit()
219 except:
220 self.connection.rollback()
221 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000222
Dave Pigott281203e2014-09-17 23:45:02 +0100223 return vlan_id
224
Steve McIntyrec4890132015-08-07 15:19:11 +0100225 # Create a new trunk in the database, linking two ports. Trunks
226 # are really simple objects for our use - they're just containers
227 # for 2 ports.
228 #
229 # Constraints:
230 # 1. Both ports listed must already exist.
231 # 2. Both ports must be in trunk mode.
232 # 3. Both must not be locked.
233 # 4. Both must not already be in a trunk.
234 def create_trunk(self, port_id1, port_id2):
235
236 for port_id in (port_id1, port_id2):
237 port = self.get_port_by_id(int(port_id))
238 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000239 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyrec4890132015-08-07 15:19:11 +0100240 if not port.is_trunk:
241 raise InputError("Port ID %d is not in trunk mode" % int(port_id))
242 if port.is_locked:
243 raise InputError("Port ID %d is locked" % int(port_id))
244 if port.trunk_id != TRUNK_ID_NONE:
245 raise InputError("Port ID %d is already on trunk ID %d" % (int(port_id), int(port.trunk_id)))
246
247 try:
248 # Add the trunk itself
249 dt = datetime.datetime.now()
250 sql = "INSERT INTO trunk (creation_time) VALUES (%s) RETURNING trunk_id"
251 data = (dt, )
252 self.cursor.execute(sql, data)
253 trunk_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100254 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyrec4890132015-08-07 15:19:11 +0100255 self.connection.commit()
256 # And update the ports
257 for port_id in (port_id1, port_id2):
258 self._set_port_trunk(port_id, trunk_id)
259 except:
260 self.delete_trunk(trunk_id)
261 raise
262
263 return trunk_id
264
Steve McIntyre2d685c72014-12-08 15:24:12 +0000265 # Internal helper function
Dave Pigott281203e2014-09-17 23:45:02 +0100266 def _delete_row(self, table, field, value):
Dave Pigott2649a1a2014-09-18 00:04:49 +0100267 try:
Steve McIntyree03de002014-12-02 17:14:14 +0000268 sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s')
269 data = (value,)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000270 self.cursor.execute(sql, data)
Steve McIntyreea343aa2015-10-23 17:46:17 +0100271 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Dave Pigott2649a1a2014-09-18 00:04:49 +0100272 self.connection.commit()
273 except:
274 self.connection.rollback()
275 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100276
Steve McIntyre388f0e22014-12-02 17:19:04 +0000277 # Delete the specified switch
278 #
279 # Constraints:
280 # 1. The switch must exist
281 # 2. The switch may not be referenced by any ports -
282 # delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100283 def delete_switch(self, switch_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000284 switch = self.get_switch_by_id(switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000285 if switch is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000286 raise NotFoundError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyre388f0e22014-12-02 17:19:04 +0000287 ports = self.get_ports_by_switch(switch_id)
288 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000289 raise InputError("Cannot delete switch ID %d when it still has %d ports" %
290 (int(switch_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100291 self._delete_row("switch", "switch_id", switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000292 return switch_id
Dave Pigott281203e2014-09-17 23:45:02 +0100293
Steve McIntyre6a968622014-12-02 18:01:41 +0000294 # Delete the specified port
295 #
296 # Constraints:
297 # 1. The port must exist
298 # 2. The port must not be locked
Dave Pigott281203e2014-09-17 23:45:02 +0100299 def delete_port(self, port_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000300 port = self.get_port_by_id(port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000301 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000302 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyre6a968622014-12-02 18:01:41 +0000303 if port.is_locked:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000304 raise InputError("Cannot delete port ID %d as it is locked" % int(port_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100305 self._delete_row("port", "port_id", port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000306 return port_id
Dave Pigott281203e2014-09-17 23:45:02 +0100307
Steve McIntyre14552ac2014-12-05 15:23:57 +0000308 # Delete the specified VLAN
309 #
310 # Constraints:
Steve McIntyre2a5df972015-08-07 15:19:40 +0100311 # 1. The VLAN must exist
Steve McIntyre14552ac2014-12-05 15:23:57 +0000312 # 2. The VLAN may not contain any ports - move or delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100313 def delete_vlan(self, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000314 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000315 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000316 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000317 ports = self.get_ports_by_current_vlan(vlan_id)
318 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000319 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
320 (int(vlan_id), len(ports)))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000321 ports = self.get_ports_by_base_vlan(vlan_id)
322 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000323 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
324 (int(vlan_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100325 self._delete_row("vlan", "vlan_id", vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000326 return vlan_id
Dave Pigott281203e2014-09-17 23:45:02 +0100327
Steve McIntyrec4890132015-08-07 15:19:11 +0100328 # Delete the specified trunk
329 #
330 # Constraints:
331 # 1. The trunk must exist
332 #
333 # Any ports attached will be detached (i.e. moved to trunk TRUNK_ID_NONE)
334 def delete_trunk(self, trunk_id):
335 trunk = self.get_trunk_by_id(trunk_id)
336 if trunk is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000337 raise NotFoundError("Trunk ID %d does not exist" % int(trunk_id))
Steve McIntyrec4890132015-08-07 15:19:11 +0100338 ports = self.get_ports_by_trunk(trunk_id)
339 for port_id in ports:
340 self._set_port_trunk(port_id, TRUNK_ID_NONE)
341 self._delete_row("trunk", "trunk_id", trunk_id)
342 return trunk_id
343
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100344 # Find the lowest unused VLAN tag and return it
345 #
346 # Constraints:
347 # None
348 def find_lowest_unused_vlan_tag(self):
349 sql = "SELECT tag FROM vlan ORDER BY tag ASC"
350 self.cursor.execute(sql,)
351
352 # Walk through the list, looking for gaps
353 last = 1
354 result = None
355
356 for record in self.cursor:
357 if (record[0] - last) > 1:
358 result = last + 1
359 break
360 last = record[0]
361
362 if result is None:
363 result = last + 1
364
365 if result > 4093:
366 raise CriticalError("Can't find any VLAN tags remaining for allocation!")
367
368 return result
369
Steve McIntyre2d685c72014-12-08 15:24:12 +0000370 # Grab one column from one row of a query on one column; useful as
371 # a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100372 def _get_element(self, select_field, table, compare_field, value):
Steve McIntyre95614c22014-11-28 17:02:44 +0000373
Steve McIntyredc173072016-08-25 18:29:10 +0100374 if value is None:
375 raise ValueError("Asked to look up using None as a key in %s" % compare_field)
376
Steve McIntyre95614c22014-11-28 17:02:44 +0000377 # We really want to use psycopg's type handling deal with the
378 # (potentially) user-supplied data in the value field, so we
379 # have to pass (sql,data) through to cursor.execute. However,
380 # we can't have psycopg do all the argument substitution here
381 # as it will quote all the params like the table name. That
382 # doesn't work. So, we substitute a "%s" for "%s" here so we
383 # keep it after python's own string substitution.
384 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
385
386 # Now, the next icky thing: we need to make sure that we're
387 # passing a dict so that psycopg2 can pick it apart properly
388 # for its own substitution code. We force this with the
389 # trailing comma here
390 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000391 self.cursor.execute(sql, data)
Steve McIntyre95614c22014-11-28 17:02:44 +0000392
Steve McIntyre58b57a42014-12-02 13:09:21 +0000393 if self.cursor.rowcount > 0:
394 return self.cursor.fetchone()[0]
395 else:
Steve McIntyrec831f9c2014-12-02 12:38:54 +0000396 return None
Dave Pigott281203e2014-09-17 23:45:02 +0100397
Steve McIntyre2d685c72014-12-08 15:24:12 +0000398 # Grab one column from one row of a query on 2 columns; useful as
399 # a quick wrapper
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000400 def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2):
401
Steve McIntyredc173072016-08-25 18:29:10 +0100402 if value1 is None:
403 raise ValueError("Asked to look up using None as a key in %s" % compare_field1)
404 if value2 is None:
405 raise ValueError("Asked to look up using None as a key in %s" % compare_field2)
406
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000407 # We really want to use psycopg's type handling deal with the
408 # (potentially) user-supplied data in the value field, so we
409 # have to pass (sql,data) through to cursor.execute. However,
410 # we can't have psycopg do all the argument substitution here
411 # as it will quote all the params like the table name. That
412 # doesn't work. So, we substitute a "%s" for "%s" here so we
413 # keep it after python's own string substitution.
414 sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s")
415
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000416 data = (value1, value2)
417 self.cursor.execute(sql, data)
418
419 if self.cursor.rowcount > 0:
420 return self.cursor.fetchone()[0]
421 else:
422 return None
423
Steve McIntyre2d685c72014-12-08 15:24:12 +0000424 # Grab one column from multiple rows of a query; useful as a quick
425 # wrapper
Steve McIntyre05e3e622015-09-25 01:29:18 +0100426 def _get_multi_elements(self, select_field, table, compare_field, value, sort_field):
Steve McIntyree9da15e2014-12-05 15:22:41 +0000427
Steve McIntyredc173072016-08-25 18:29:10 +0100428 if value is None:
429 raise ValueError("Asked to look up using None as a key in %s" % compare_field)
430
Steve McIntyree9da15e2014-12-05 15:22:41 +0000431 # We really want to use psycopg's type handling deal with the
432 # (potentially) user-supplied data in the value field, so we
433 # have to pass (sql,data) through to cursor.execute. However,
434 # we can't have psycopg do all the argument substitution here
435 # as it will quote all the params like the table name. That
436 # doesn't work. So, we substitute a "%s" for "%s" here so we
437 # keep it after python's own string substitution.
Steve McIntyre05e3e622015-09-25 01:29:18 +0100438 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 +0000439
440 # Now, the next icky thing: we need to make sure that we're
441 # passing a dict so that psycopg2 can pick it apart properly
442 # for its own substitution code. We force this with the
443 # trailing comma here
444 data = (value, )
445 self.cursor.execute(sql, data)
446
447 if self.cursor.rowcount > 0:
448 results = []
449 for record in self.cursor:
Steve McIntyre52509622014-12-02 17:13:15 +0000450 results.append(record[0])
Steve McIntyree9da15e2014-12-05 15:22:41 +0000451 return results
Steve McIntyre52509622014-12-02 17:13:15 +0000452 else:
453 return None
454
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000455 # Grab one column from multiple rows of a 2-part query; useful as
456 # a wrapper
Steve McIntyre05e3e622015-09-25 01:29:18 +0100457 def _get_multi_elements2(self, select_field, table, compare_field1, value1, compare_field2, value2, sort_field):
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000458
Steve McIntyredc173072016-08-25 18:29:10 +0100459 if value1 is None:
460 raise ValueError("Asked to look up using None as a key in %s" % compare_field1)
461 if value2 is None:
462 raise ValueError("Asked to look up using None as a key in %s" % compare_field2)
463
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000464 # We really want to use psycopg's type handling deal with the
465 # (potentially) user-supplied data in the value field, so we
466 # have to pass (sql,data) through to cursor.execute. However,
467 # we can't have psycopg do all the argument substitution here
468 # as it will quote all the params like the table name. That
469 # doesn't work. So, we substitute a "%s" for "%s" here so we
470 # keep it after python's own string substitution.
Steve McIntyre05e3e622015-09-25 01:29:18 +0100471 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 +0000472
473 data = (value1, value2)
474 self.cursor.execute(sql, data)
475
476 if self.cursor.rowcount > 0:
477 results = []
478 for record in self.cursor:
479 results.append(record[0])
480 return results
481 else:
482 return None
483
Steve McIntyre2d685c72014-12-08 15:24:12 +0000484 # Simple lookup: look up a switch by ID, and return all the
485 # details of that switch.
486 #
487 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000488 def get_switch_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000489 return self._get_row("switch", "switch_id", int(switch_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000490
Steve McIntyre2d685c72014-12-08 15:24:12 +0000491 # Simple lookup: look up a switch by name, and return the ID of
492 # that switch.
493 #
494 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000495 def get_switch_id_by_name(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100496 return self._get_element("switch_id", "switch", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100497
Steve McIntyre2d685c72014-12-08 15:24:12 +0000498 # Simple lookup: look up a switch by ID, and return the name of
499 # that switch.
500 #
501 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000502 def get_switch_name_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000503 return self._get_element("name", "switch", "switch_id", int(switch_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100504
Steve McIntyre2d685c72014-12-08 15:24:12 +0000505 # Simple lookup: look up a port by ID, and return all the details
506 # of that port.
507 #
508 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000509 def get_port_by_id(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000510 return self._get_row("port", "port_id", int(port_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000511
Steve McIntyre2d685c72014-12-08 15:24:12 +0000512 # Simple lookup: look up a switch by ID, and return the IDs of all
513 # the ports on that switch.
514 #
515 # Returns None on failure.
Steve McIntyreb67f3912014-12-02 17:14:36 +0000516 def get_ports_by_switch(self, switch_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100517 return self._get_multi_elements("port_id", "port", "switch_id", int(switch_id), "port_id")
Steve McIntyreb67f3912014-12-02 17:14:36 +0000518
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000519 # More complex lookup: look up all the trunk ports on a switch by
520 # ID
521 #
522 # Returns None on failure.
523 def get_trunk_port_names_by_switch(self, switch_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100524 return self._get_multi_elements2("name", "port", "switch_id", int(switch_id), "is_trunk", True, "port_id")
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000525
Steve McIntyre2d685c72014-12-08 15:24:12 +0000526 # Simple lookup: look up a port by its name and its parent switch
527 # by ID, and return the ID of the port.
528 #
529 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000530 def get_port_by_switch_and_name(self, switch_id, name):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000531 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "name", name)
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000532
Steve McIntyre45f55012015-08-05 13:55:15 +0100533 # Simple lookup: look up a port by its external name and its
534 # parent switch by ID, and return the ID of the port.
535 #
536 # Returns None on failure.
537 def get_port_by_switch_and_number(self, switch_id, number):
538 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "number", int(number))
539
Steve McIntyre2d685c72014-12-08 15:24:12 +0000540 # Simple lookup: look up a port by ID, and return the current VLAN
541 # id of that port.
542 #
543 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000544 def get_current_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000545 return self._get_element("current_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000546
Steve McIntyre2d685c72014-12-08 15:24:12 +0000547 # Simple lookup: look up a port by ID, and return the base VLAN
548 # id of that port.
549 #
550 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000551 def get_base_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000552 return self._get_element("base_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000553
Steve McIntyre2d685c72014-12-08 15:24:12 +0000554 # Simple lookup: look up a current VLAN by ID, and return the IDs
555 # of all the ports on that VLAN.
556 #
557 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000558 def get_ports_by_current_vlan(self, vlan_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100559 return self._get_multi_elements("port_id", "port", "current_vlan_id", int(vlan_id), "port_id")
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000560
Steve McIntyre2d685c72014-12-08 15:24:12 +0000561 # Simple lookup: look up a base VLAN by ID, and return the IDs
562 # of all the ports on that VLAN.
563 #
564 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000565 def get_ports_by_base_vlan(self, vlan_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100566 return self._get_multi_elements("port_id", "port", "base_vlan_id", int(vlan_id), "port_id")
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000567
Steve McIntyrec4890132015-08-07 15:19:11 +0100568 # Simple lookup: look up a trunk by ID, and return the IDs of the
569 # ports on both ends of that trunk.
570 #
571 # Returns None on failure.
572 def get_ports_by_trunk(self, trunk_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100573 return self._get_multi_elements("port_id", "port", "trunk_id", int(trunk_id), "port_id")
Steve McIntyrec4890132015-08-07 15:19:11 +0100574
Steve McIntyre2d685c72014-12-08 15:24:12 +0000575 # Simple lookup: look up a VLAN by ID, and return all the details
576 # of that VLAN.
577 #
578 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000579 def get_vlan_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000580 return self._get_row("vlan", "vlan_id", int(vlan_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000581
Steve McIntyre2d685c72014-12-08 15:24:12 +0000582 # Simple lookup: look up a VLAN by name, and return the ID of that
583 # VLAN.
584 #
585 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000586 def get_vlan_id_by_name(self, name):
587 return self._get_element("vlan_id", "vlan", "name", name)
588
Steve McIntyre2d685c72014-12-08 15:24:12 +0000589 # Simple lookup: look up a VLAN by tag, and return the ID of that
590 # VLAN.
591 #
592 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000593 def get_vlan_id_by_tag(self, tag):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000594 return self._get_element("vlan_id", "vlan", "tag", int(tag))
Steve McIntyref3655062014-12-05 15:34:39 +0000595
Steve McIntyre2d685c72014-12-08 15:24:12 +0000596 # Simple lookup: look up a VLAN by ID, and return the name of that
597 # VLAN.
598 #
599 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000600 def get_vlan_name_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000601 return self._get_element("name", "vlan", "vlan_id", int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100602
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000603 # Simple lookup: look up a VLAN by ID, and return the tag of that
604 # VLAN.
605 #
606 # Returns None on failure.
607 def get_vlan_tag_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000608 return self._get_element("tag", "vlan", "vlan_id", int(vlan_id))
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000609
Steve McIntyrec4890132015-08-07 15:19:11 +0100610 # Simple lookup: look up a trunk by ID, and return all the details
611 # of that trunk.
612 #
613 # Returns None on failure.
614 def get_trunk_by_id(self, trunk_id):
615 return self._get_row("trunk", "trunk_id", int(trunk_id))
616
Steve McIntyreea343aa2015-10-23 17:46:17 +0100617 # Get the last-modified time for the database
618 def get_last_modified_time(self):
619 sql = "SELECT last_modified FROM state"
620 self.cursor.execute(sql)
Steve McIntyreaf24aaa2015-10-23 17:59:04 +0100621 return self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100622
Steve McIntyre2d685c72014-12-08 15:24:12 +0000623 # Grab one row of a query on one column; useful as a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100624 def _get_row(self, table, field, value):
Steve McIntyree0b842a2014-11-28 18:23:47 +0000625
626 # We really want to use psycopg's type handling deal with the
627 # (potentially) user-supplied data in the value field, so we
628 # have to pass (sql,data) through to cursor.execute. However,
629 # we can't have psycopg do all the argument substitution here
630 # as it will quote all the params like the table name. That
631 # doesn't work. So, we substitute a "%s" for "%s" here so we
632 # keep it after python's own string substitution.
633 sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s")
634
635 # Now, the next icky thing: we need to make sure that we're
636 # passing a dict so that psycopg2 can pick it apart properly
637 # for its own substitution code. We force this with the
638 # trailing comma here
639 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000640 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100641 return self.cursor.fetchone()
642
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000643 # (Un)Lock a port in the database. This can only be done through
644 # the admin interface, and will stop API users from modifying
645 # settings on the port. Use this to lock down ports that are used
646 # for PDUs and other core infrastructure
Steve McIntyre7d219202018-02-01 16:53:25 +0000647 def set_port_is_locked(self, port_id, is_locked, lock_reason=""):
Steve McIntyre8c64d952014-12-05 16:22:44 +0000648 port = self.get_port_by_id(port_id)
649 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000650 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000651 try:
Steve McIntyre7d219202018-02-01 16:53:25 +0000652 sql = "UPDATE port SET is_locked=%s, lock_reason=%s WHERE port_id=%s RETURNING port_id"
653 data = (is_locked, lock_reason, port_id)
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000654 self.cursor.execute(sql, data)
655 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100656 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000657 self.connection.commit()
658 except:
659 self.connection.rollback()
Steve McIntyre7d219202018-02-01 16:53:25 +0000660 raise InputError("lock failed on Port ID %d" % int(port_id))
Steve McIntyre1c8a3212015-07-14 17:07:31 +0100661 return port_id
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000662
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000663 # Set the mode of a port in the database. Valid values for mode
664 # are "trunk" and "access"
665 def set_port_mode(self, port_id, mode):
666 port = self.get_port_by_id(port_id)
667 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000668 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000669 if mode == "access":
670 is_trunk = False
671 elif mode == "trunk":
672 is_trunk = True
673 else:
674 raise InputError("Port mode %s is not valid" % mode)
675 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000676 sql = "UPDATE port SET is_trunk=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000677 data = (is_trunk, port_id)
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000678 self.cursor.execute(sql, data)
679 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100680 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000681 self.connection.commit()
682 except:
683 self.connection.rollback()
684 raise
685 return port_id
686
Steve McIntyre2d685c72014-12-08 15:24:12 +0000687 # Set the current vlan of a port in the database. The VLAN is
688 # passed by ID.
689 #
690 # Constraints:
691 # 1. The port must already exist
692 # 2. The port must not be a trunk port
693 # 3. The port must not be locked
694 # 1. The VLAN must already exist in the database
Steve McIntyre9eb78652014-12-05 17:51:53 +0000695 def set_current_vlan(self, port_id, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000696 port = self.get_port_by_id(port_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000697 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000698 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100699
Steve McIntyre6dd00be2014-12-05 17:29:35 +0000700 if port.is_trunk or port.is_locked:
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100701 raise CriticalError("The port is locked")
702
Steve McIntyre549435f2014-12-05 15:42:46 +0000703 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000704 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000705 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100706
707 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000708 sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000709 data = (vlan_id, port_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000710 self.cursor.execute(sql, data)
Steve McIntyree1371102014-12-05 17:17:09 +0000711 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100712 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyree1371102014-12-05 17:17:09 +0000713 self.connection.commit()
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100714 except:
715 self.connection.rollback()
716 raise
Steve McIntyree1371102014-12-05 17:17:09 +0000717 return port_id
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100718
Steve McIntyre2d685c72014-12-08 15:24:12 +0000719 # Set the base vlan of a port in the database. The VLAN is
720 # passed by ID.
721 #
722 # Constraints:
723 # 1. The port must already exist
724 # 2. The port must not be a trunk port
725 # 3. The port must not be locked
Steve McIntyree653d172015-08-06 16:51:18 +0100726 # 4. The VLAN must already exist in the database
Steve McIntyredaae5502014-12-05 17:55:18 +0000727 def set_base_vlan(self, port_id, vlan_id):
728 port = self.get_port_by_id(port_id)
729 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000730 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000731
732 if port.is_trunk or port.is_locked:
733 raise CriticalError("The port is locked")
734
735 vlan = self.get_vlan_by_id(vlan_id)
736 if vlan is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000737 raise NotFoundError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000738 if not vlan.is_base_vlan:
739 raise InputError("VLAN ID %d is not a base VLAN" % int(vlan_id))
740
741 try:
742 sql = "UPDATE port SET base_vlan_id=%s WHERE port_id=%s RETURNING port_id"
743 data = (vlan_id, port_id)
744 self.cursor.execute(sql, data)
745 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100746 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyredaae5502014-12-05 17:55:18 +0000747 self.connection.commit()
748 except:
749 self.connection.rollback()
750 raise
751 return port_id
752
Steve McIntyrec4890132015-08-07 15:19:11 +0100753 # Internal function: Attach a port to a trunk in the database.
754 #
755 # Constraints:
756 # 1. The port must already exist
757 # 2. The port must not be locked
758 def _set_port_trunk(self, port_id, trunk_id):
759 port = self.get_port_by_id(port_id)
760 if port is None:
Steve McIntyreb01959f2016-03-22 17:02:39 +0000761 raise NotFoundError("Port ID %d does not exist" % int(port_id))
Steve McIntyrec4890132015-08-07 15:19:11 +0100762 if port.is_locked:
763 raise CriticalError("The port is locked")
764 try:
765 sql = "UPDATE port SET trunk_id=%s WHERE port_id=%s RETURNING port_id"
766 data = (int(trunk_id), int(port_id))
767 self.cursor.execute(sql, data)
768 port_id = self.cursor.fetchone()[0]
Steve McIntyreea343aa2015-10-23 17:46:17 +0100769 self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
Steve McIntyrec4890132015-08-07 15:19:11 +0100770 self.connection.commit()
771 except:
772 self.connection.rollback()
773 raise
774 return port_id
775
Steve McIntyre2d685c72014-12-08 15:24:12 +0000776 # Trivial helper function to return all the rows in a given table
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100777 def _dump_table(self, table, order):
Dave Pigott281203e2014-09-17 23:45:02 +0100778 result = []
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100779 self.cursor.execute("SELECT * FROM %s ORDER by %s ASC" % (table, order))
Dave Pigott281203e2014-09-17 23:45:02 +0100780 record = self.cursor.fetchone()
781 while record != None:
Steve McIntyree73eb122014-11-27 15:18:47 +0000782 result.append(record)
Dave Pigott281203e2014-09-17 23:45:02 +0100783 record = self.cursor.fetchone()
784 return result
785
786 def all_switches(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100787 return self._dump_table("switch", "switch_id")
Dave Pigott281203e2014-09-17 23:45:02 +0100788
789 def all_ports(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100790 return self._dump_table("port", "port_id")
Dave Pigott281203e2014-09-17 23:45:02 +0100791
792 def all_vlans(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100793 return self._dump_table("vlan", "vlan_id")
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100794
Steve McIntyrec4890132015-08-07 15:19:11 +0100795 def all_trunks(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100796 return self._dump_table("trunk", "trunk_id")
Steve McIntyrec4890132015-08-07 15:19:11 +0100797
Steve McIntyre6a618162014-12-10 16:47:07 +0000798if __name__ == '__main__':
799 db = VlanDB()
Steve McIntyre6d84ec12014-12-18 16:56:56 +0000800 s = db.all_switches()
801 print 'The DB knows about %d switch(es)' % len(s)
802 print s
803 p = db.all_ports()
804 print 'The DB knows about %d port(s)' % len(p)
805 print p
806 v = db.all_vlans()
807 print 'The DB knows about %d vlan(s)' % len(v)
808 print v
Steve McIntyrec4890132015-08-07 15:19:11 +0100809 t = db.all_trunks()
810 print 'The DB knows about %d trunks(s)' % len(t)
811 print t
Steve McIntyre6a618162014-12-10 16:47:07 +0000812
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100813 print 'First free VLAN tag is %d' % db.find_lowest_unused_vlan_tag()