blob: 71e458d1ad1138d348b4f3973493fa596e003d15 [file] [log] [blame]
Dave Pigott281203e2014-09-17 23:45:02 +01001#! /usr/bin/python
2
Steve McIntyrec4890132015-08-07 15:19:11 +01003# Copyright 2014-2015 Linaro Limited
4# 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 McIntyre6a618162014-12-10 16:47:07 +000029if __name__ == '__main__':
30 vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
31 sys.path.insert(0, vlandpath)
32 sys.path.insert(0, "%s/.." % vlandpath)
33
Steve McIntyre6b013652014-12-02 12:35:18 +000034from errors import CriticalError, InputError
Dave Pigott281203e2014-09-17 23:45:02 +010035
36class VlanDB:
37 def __init__(self, db_name="vland", username="vland"):
38 try:
Steve McIntyree38f6222014-11-27 15:09:49 +000039 self.connection = psycopg2.connect(database=db_name, user=username)
Steve McIntyreb09ed282014-12-02 17:59:35 +000040 self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
Dave Pigott281203e2014-09-17 23:45:02 +010041 except Exception as e:
Steve McIntyre5fa22652015-04-01 18:01:45 +010042 logging.error("Failed to access database: %s", e)
Steve McIntyre7cf80982015-02-12 07:03:40 +000043 raise
Dave Pigott281203e2014-09-17 23:45:02 +010044
45 def __del__(self):
46 self.cursor.close()
47 self.connection.close()
48
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000049 # Create a new switch in the database. Switches are really simple
50 # devices - they're just containers for ports.
51 #
52 # Constraints:
53 # Switches must be uniquely named
Steve McIntyredbd7fe52014-11-27 16:54:29 +000054 def create_switch(self, name):
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000055
Steve McIntyre549435f2014-12-05 15:42:46 +000056 switch_id = self.get_switch_id_by_name(name)
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000057 if switch_id is not None:
58 raise InputError("Switch name %s already exists" % name)
59
Dave Pigott2649a1a2014-09-18 00:04:49 +010060 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +000061 sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id"
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000062 data = (name, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +000063 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +010064 switch_id = self.cursor.fetchone()[0]
65 self.connection.commit()
66 except:
67 self.connection.rollback()
68 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +000069
Dave Pigott281203e2014-09-17 23:45:02 +010070 return switch_id
71
Steve McIntyrec4890132015-08-07 15:19:11 +010072 # Create a new port in the database. Three of the fields are
73 # created with default values (is_locked, is_trunk, trunk_id)
74 # here, and should be updated separately if desired. For the
75 # current_vlan_id and base_vlan_id fields, *BE CAREFUL* that you
76 # have already looked up the correct VLAN_ID for each. This is
77 # *NOT* the same as the VLAN tag (likely to be 1). You Have Been
78 # Warned!
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000079 #
80 # Constraints:
81 # 1. The switch referred to must already exist
82 # 2. The VLANs mentioned here must already exist
Steve McIntyre6a7fdb22014-12-05 15:17:30 +000083 # 3. (Switch/name) must be unique
Steve McIntyreea753972015-08-05 13:52:48 +010084 # 4. (Switch/number) must be unique
85 def create_port(self, switch_id, name, number, current_vlan_id, base_vlan_id):
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000086
Steve McIntyre549435f2014-12-05 15:42:46 +000087 switch = self.get_switch_by_id(switch_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000088 if switch is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +000089 raise InputError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000090
91 for vlan_id in (current_vlan_id, base_vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +000092 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000093 if vlan is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +000094 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +000095
96 port_id = self.get_port_by_switch_and_name(switch_id, name)
97 if port_id is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +000098 raise InputError("Already have a port %s on switch ID %d" % (name, int(switch_id)))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +000099
Steve McIntyreea753972015-08-05 13:52:48 +0100100 port_id = self.get_port_by_switch_and_number(switch_id, int(number))
101 if port_id is not None:
102 raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id)))
103
Dave Pigott2649a1a2014-09-18 00:04:49 +0100104 try:
Steve McIntyrec4890132015-08-07 15:19:11 +0100105 sql = "INSERT INTO port (name, number, switch_id, is_locked, is_trunk, current_vlan_id, base_vlan_id, trunk_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING port_id"
Steve McIntyreea753972015-08-05 13:52:48 +0100106 data = (name, number, switch_id,
Steve McIntyre90a4a972014-11-28 16:50:56 +0000107 False, False,
Steve McIntyrec4890132015-08-07 15:19:11 +0100108 current_vlan_id, base_vlan_id, TRUNK_ID_NONE)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000109 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100110 port_id = self.cursor.fetchone()[0]
111 self.connection.commit()
112 except:
113 self.connection.rollback()
114 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000115
Dave Pigott281203e2014-09-17 23:45:02 +0100116 return port_id
117
Steve McIntyreb005a2f2014-11-28 18:23:05 +0000118 # Create a new vlan in the database. We locally add a creation
119 # timestamp, for debug purposes. If vlans seems to be sticking
120 # around, we'll be able to see when they were created.
Steve McIntyre31b2df52014-12-02 12:37:54 +0000121 #
122 # Constraints:
123 # Names and tags must be unique
Steve McIntyre57f45912014-12-08 14:43:00 +0000124 # Tags must be in the range 1-4095 (802.1q spec)
Steve McIntyre49777e72014-12-08 16:00:46 +0000125 # Names can be any free-form text, length 1-32 characters
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000126 def create_vlan(self, name, tag, is_base_vlan):
Steve McIntyre31b2df52014-12-02 12:37:54 +0000127
Steve McIntyre57f45912014-12-08 14:43:00 +0000128 if int(tag) < 1 or int(tag) > 4095:
Steve McIntyre49777e72014-12-08 16:00:46 +0000129 raise InputError("VLAN tag %d is outside of the valid range (1-4095)" % int(tag))
130
131 if (len(name) < 1) or (len(name) > 32):
132 raise InputError("VLAN name %s is invalid (must be 1-32 chars)" % name)
Steve McIntyre57f45912014-12-08 14:43:00 +0000133
Steve McIntyrea34c1812014-12-05 15:27:55 +0000134 vlan_id = self.get_vlan_id_by_name(name)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000135 if vlan_id is not None:
136 raise InputError("VLAN name %s is already in use" % name)
137
Steve McIntyre50eb0602014-12-05 15:29:04 +0000138 vlan_id = self.get_vlan_id_by_tag(tag)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000139 if vlan_id is not None:
140 raise InputError("VLAN tag %d is already in use" % int(tag))
141
Dave Pigott2649a1a2014-09-18 00:04:49 +0100142 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +0000143 dt = datetime.datetime.now()
Steve McIntyre4b918132014-12-05 17:04:46 +0000144 sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id"
145 data = (name, tag, is_base_vlan, dt)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000146 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100147 vlan_id = self.cursor.fetchone()[0]
148 self.connection.commit()
149 except:
150 self.connection.rollback()
151 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000152
Dave Pigott281203e2014-09-17 23:45:02 +0100153 return vlan_id
154
Steve McIntyrec4890132015-08-07 15:19:11 +0100155 # Create a new trunk in the database, linking two ports. Trunks
156 # are really simple objects for our use - they're just containers
157 # for 2 ports.
158 #
159 # Constraints:
160 # 1. Both ports listed must already exist.
161 # 2. Both ports must be in trunk mode.
162 # 3. Both must not be locked.
163 # 4. Both must not already be in a trunk.
164 def create_trunk(self, port_id1, port_id2):
165
166 for port_id in (port_id1, port_id2):
167 port = self.get_port_by_id(int(port_id))
168 if port is None:
169 raise InputError("Port ID %d does not exist" % int(port_id))
170 if not port.is_trunk:
171 raise InputError("Port ID %d is not in trunk mode" % int(port_id))
172 if port.is_locked:
173 raise InputError("Port ID %d is locked" % int(port_id))
174 if port.trunk_id != TRUNK_ID_NONE:
175 raise InputError("Port ID %d is already on trunk ID %d" % (int(port_id), int(port.trunk_id)))
176
177 try:
178 # Add the trunk itself
179 dt = datetime.datetime.now()
180 sql = "INSERT INTO trunk (creation_time) VALUES (%s) RETURNING trunk_id"
181 data = (dt, )
182 self.cursor.execute(sql, data)
183 trunk_id = self.cursor.fetchone()[0]
184 self.connection.commit()
185 # And update the ports
186 for port_id in (port_id1, port_id2):
187 self._set_port_trunk(port_id, trunk_id)
188 except:
189 self.delete_trunk(trunk_id)
190 raise
191
192 return trunk_id
193
Steve McIntyre2d685c72014-12-08 15:24:12 +0000194 # Internal helper function
Dave Pigott281203e2014-09-17 23:45:02 +0100195 def _delete_row(self, table, field, value):
Dave Pigott2649a1a2014-09-18 00:04:49 +0100196 try:
Steve McIntyree03de002014-12-02 17:14:14 +0000197 sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s')
198 data = (value,)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000199 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100200 self.connection.commit()
201 except:
202 self.connection.rollback()
203 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100204
Steve McIntyre388f0e22014-12-02 17:19:04 +0000205 # Delete the specified switch
206 #
207 # Constraints:
208 # 1. The switch must exist
209 # 2. The switch may not be referenced by any ports -
210 # delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100211 def delete_switch(self, switch_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000212 switch = self.get_switch_by_id(switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000213 if switch is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000214 raise InputError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyre388f0e22014-12-02 17:19:04 +0000215 ports = self.get_ports_by_switch(switch_id)
216 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000217 raise InputError("Cannot delete switch ID %d when it still has %d ports" %
218 (int(switch_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100219 self._delete_row("switch", "switch_id", switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000220 return switch_id
Dave Pigott281203e2014-09-17 23:45:02 +0100221
Steve McIntyre6a968622014-12-02 18:01:41 +0000222 # Delete the specified port
223 #
224 # Constraints:
225 # 1. The port must exist
226 # 2. The port must not be locked
Dave Pigott281203e2014-09-17 23:45:02 +0100227 def delete_port(self, port_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000228 port = self.get_port_by_id(port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000229 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000230 raise InputError("Port ID %d does not exist" % int(port_id))
Steve McIntyre6a968622014-12-02 18:01:41 +0000231 if port.is_locked:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000232 raise InputError("Cannot delete port ID %d as it is locked" % int(port_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100233 self._delete_row("port", "port_id", port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000234 return port_id
Dave Pigott281203e2014-09-17 23:45:02 +0100235
Steve McIntyre14552ac2014-12-05 15:23:57 +0000236 # Delete the specified VLAN
237 #
238 # Constraints:
Steve McIntyre2a5df972015-08-07 15:19:40 +0100239 # 1. The VLAN must exist
Steve McIntyre14552ac2014-12-05 15:23:57 +0000240 # 2. The VLAN may not contain any ports - move or delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100241 def delete_vlan(self, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000242 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000243 if vlan is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000244 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000245 ports = self.get_ports_by_current_vlan(vlan_id)
246 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000247 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
248 (int(vlan_id), len(ports)))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000249 ports = self.get_ports_by_base_vlan(vlan_id)
250 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000251 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
252 (int(vlan_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100253 self._delete_row("vlan", "vlan_id", vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000254 return vlan_id
Dave Pigott281203e2014-09-17 23:45:02 +0100255
Steve McIntyrec4890132015-08-07 15:19:11 +0100256 # Delete the specified trunk
257 #
258 # Constraints:
259 # 1. The trunk must exist
260 #
261 # Any ports attached will be detached (i.e. moved to trunk TRUNK_ID_NONE)
262 def delete_trunk(self, trunk_id):
263 trunk = self.get_trunk_by_id(trunk_id)
264 if trunk is None:
265 raise InputError("Trunk ID %d does not exist" % int(trunk_id))
266 ports = self.get_ports_by_trunk(trunk_id)
267 for port_id in ports:
268 self._set_port_trunk(port_id, TRUNK_ID_NONE)
269 self._delete_row("trunk", "trunk_id", trunk_id)
270 return trunk_id
271
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100272 # Find the lowest unused VLAN tag and return it
273 #
274 # Constraints:
275 # None
276 def find_lowest_unused_vlan_tag(self):
277 sql = "SELECT tag FROM vlan ORDER BY tag ASC"
278 self.cursor.execute(sql,)
279
280 # Walk through the list, looking for gaps
281 last = 1
282 result = None
283
284 for record in self.cursor:
285 if (record[0] - last) > 1:
286 result = last + 1
287 break
288 last = record[0]
289
290 if result is None:
291 result = last + 1
292
293 if result > 4093:
294 raise CriticalError("Can't find any VLAN tags remaining for allocation!")
295
296 return result
297
Steve McIntyre2d685c72014-12-08 15:24:12 +0000298 # Grab one column from one row of a query on one column; useful as
299 # a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100300 def _get_element(self, select_field, table, compare_field, value):
Steve McIntyre95614c22014-11-28 17:02:44 +0000301
302 # We really want to use psycopg's type handling deal with the
303 # (potentially) user-supplied data in the value field, so we
304 # have to pass (sql,data) through to cursor.execute. However,
305 # we can't have psycopg do all the argument substitution here
306 # as it will quote all the params like the table name. That
307 # doesn't work. So, we substitute a "%s" for "%s" here so we
308 # keep it after python's own string substitution.
309 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
310
311 # Now, the next icky thing: we need to make sure that we're
312 # passing a dict so that psycopg2 can pick it apart properly
313 # for its own substitution code. We force this with the
314 # trailing comma here
315 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000316 self.cursor.execute(sql, data)
Steve McIntyre95614c22014-11-28 17:02:44 +0000317
Steve McIntyre58b57a42014-12-02 13:09:21 +0000318 if self.cursor.rowcount > 0:
319 return self.cursor.fetchone()[0]
320 else:
Steve McIntyrec831f9c2014-12-02 12:38:54 +0000321 return None
Dave Pigott281203e2014-09-17 23:45:02 +0100322
Steve McIntyre2d685c72014-12-08 15:24:12 +0000323 # Grab one column from one row of a query on 2 columns; useful as
324 # a quick wrapper
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000325 def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2):
326
327 # We really want to use psycopg's type handling deal with the
328 # (potentially) user-supplied data in the value field, so we
329 # have to pass (sql,data) through to cursor.execute. However,
330 # we can't have psycopg do all the argument substitution here
331 # as it will quote all the params like the table name. That
332 # doesn't work. So, we substitute a "%s" for "%s" here so we
333 # keep it after python's own string substitution.
334 sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s")
335
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000336 data = (value1, value2)
337 self.cursor.execute(sql, data)
338
339 if self.cursor.rowcount > 0:
340 return self.cursor.fetchone()[0]
341 else:
342 return None
343
Steve McIntyre2d685c72014-12-08 15:24:12 +0000344 # Grab one column from multiple rows of a query; useful as a quick
345 # wrapper
Steve McIntyre05e3e622015-09-25 01:29:18 +0100346 def _get_multi_elements(self, select_field, table, compare_field, value, sort_field):
Steve McIntyree9da15e2014-12-05 15:22:41 +0000347
348 # We really want to use psycopg's type handling deal with the
349 # (potentially) user-supplied data in the value field, so we
350 # have to pass (sql,data) through to cursor.execute. However,
351 # we can't have psycopg do all the argument substitution here
352 # as it will quote all the params like the table name. That
353 # doesn't work. So, we substitute a "%s" for "%s" here so we
354 # keep it after python's own string substitution.
Steve McIntyre05e3e622015-09-25 01:29:18 +0100355 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 +0000356
357 # Now, the next icky thing: we need to make sure that we're
358 # passing a dict so that psycopg2 can pick it apart properly
359 # for its own substitution code. We force this with the
360 # trailing comma here
361 data = (value, )
362 self.cursor.execute(sql, data)
363
364 if self.cursor.rowcount > 0:
365 results = []
366 for record in self.cursor:
Steve McIntyre52509622014-12-02 17:13:15 +0000367 results.append(record[0])
Steve McIntyree9da15e2014-12-05 15:22:41 +0000368 return results
Steve McIntyre52509622014-12-02 17:13:15 +0000369 else:
370 return None
371
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000372 # Grab one column from multiple rows of a 2-part query; useful as
373 # a wrapper
Steve McIntyre05e3e622015-09-25 01:29:18 +0100374 def _get_multi_elements2(self, select_field, table, compare_field1, value1, compare_field2, value2, sort_field):
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000375
376 # We really want to use psycopg's type handling deal with the
377 # (potentially) user-supplied data in the value field, so we
378 # have to pass (sql,data) through to cursor.execute. However,
379 # we can't have psycopg do all the argument substitution here
380 # as it will quote all the params like the table name. That
381 # doesn't work. So, we substitute a "%s" for "%s" here so we
382 # keep it after python's own string substitution.
Steve McIntyre05e3e622015-09-25 01:29:18 +0100383 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 +0000384
385 data = (value1, value2)
386 self.cursor.execute(sql, data)
387
388 if self.cursor.rowcount > 0:
389 results = []
390 for record in self.cursor:
391 results.append(record[0])
392 return results
393 else:
394 return None
395
Steve McIntyre2d685c72014-12-08 15:24:12 +0000396 # Simple lookup: look up a switch by ID, and return all the
397 # details of that switch.
398 #
399 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000400 def get_switch_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000401 return self._get_row("switch", "switch_id", int(switch_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000402
Steve McIntyre2d685c72014-12-08 15:24:12 +0000403 # Simple lookup: look up a switch by name, and return the ID of
404 # that switch.
405 #
406 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000407 def get_switch_id_by_name(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100408 return self._get_element("switch_id", "switch", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100409
Steve McIntyre2d685c72014-12-08 15:24:12 +0000410 # Simple lookup: look up a switch by ID, and return the name of
411 # that switch.
412 #
413 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000414 def get_switch_name_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000415 return self._get_element("name", "switch", "switch_id", int(switch_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100416
Steve McIntyre2d685c72014-12-08 15:24:12 +0000417 # Simple lookup: look up a port by ID, and return all the details
418 # of that port.
419 #
420 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000421 def get_port_by_id(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000422 return self._get_row("port", "port_id", int(port_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000423
Steve McIntyre2d685c72014-12-08 15:24:12 +0000424 # Simple lookup: look up a switch by ID, and return the IDs of all
425 # the ports on that switch.
426 #
427 # Returns None on failure.
Steve McIntyreb67f3912014-12-02 17:14:36 +0000428 def get_ports_by_switch(self, switch_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100429 return self._get_multi_elements("port_id", "port", "switch_id", int(switch_id), "port_id")
Steve McIntyreb67f3912014-12-02 17:14:36 +0000430
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000431 # More complex lookup: look up all the trunk ports on a switch by
432 # ID
433 #
434 # Returns None on failure.
435 def get_trunk_port_names_by_switch(self, switch_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100436 return self._get_multi_elements2("name", "port", "switch_id", int(switch_id), "is_trunk", True, "port_id")
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000437
Steve McIntyre2d685c72014-12-08 15:24:12 +0000438 # Simple lookup: look up a port by its name and its parent switch
439 # by ID, and return the ID of the port.
440 #
441 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000442 def get_port_by_switch_and_name(self, switch_id, name):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000443 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "name", name)
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000444
Steve McIntyre45f55012015-08-05 13:55:15 +0100445 # Simple lookup: look up a port by its external name and its
446 # parent switch by ID, and return the ID of the port.
447 #
448 # Returns None on failure.
449 def get_port_by_switch_and_number(self, switch_id, number):
450 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "number", int(number))
451
Steve McIntyre2d685c72014-12-08 15:24:12 +0000452 # Simple lookup: look up a port by ID, and return the current VLAN
453 # id of that port.
454 #
455 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000456 def get_current_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000457 return self._get_element("current_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000458
Steve McIntyre2d685c72014-12-08 15:24:12 +0000459 # Simple lookup: look up a port by ID, and return the base VLAN
460 # id of that port.
461 #
462 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000463 def get_base_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000464 return self._get_element("base_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000465
Steve McIntyre2d685c72014-12-08 15:24:12 +0000466 # Simple lookup: look up a current VLAN by ID, and return the IDs
467 # of all the ports on that VLAN.
468 #
469 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000470 def get_ports_by_current_vlan(self, vlan_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100471 return self._get_multi_elements("port_id", "port", "current_vlan_id", int(vlan_id), "port_id")
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000472
Steve McIntyre2d685c72014-12-08 15:24:12 +0000473 # Simple lookup: look up a base VLAN by ID, and return the IDs
474 # of all the ports on that VLAN.
475 #
476 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000477 def get_ports_by_base_vlan(self, vlan_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100478 return self._get_multi_elements("port_id", "port", "base_vlan_id", int(vlan_id), "port_id")
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000479
Steve McIntyrec4890132015-08-07 15:19:11 +0100480 # Simple lookup: look up a trunk by ID, and return the IDs of the
481 # ports on both ends of that trunk.
482 #
483 # Returns None on failure.
484 def get_ports_by_trunk(self, trunk_id):
Steve McIntyre05e3e622015-09-25 01:29:18 +0100485 return self._get_multi_elements("port_id", "port", "trunk_id", int(trunk_id), "port_id")
Steve McIntyrec4890132015-08-07 15:19:11 +0100486
Steve McIntyre2d685c72014-12-08 15:24:12 +0000487 # Simple lookup: look up a VLAN by ID, and return all the details
488 # of that VLAN.
489 #
490 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000491 def get_vlan_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000492 return self._get_row("vlan", "vlan_id", int(vlan_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000493
Steve McIntyre2d685c72014-12-08 15:24:12 +0000494 # Simple lookup: look up a VLAN by name, and return the ID of that
495 # VLAN.
496 #
497 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000498 def get_vlan_id_by_name(self, name):
499 return self._get_element("vlan_id", "vlan", "name", name)
500
Steve McIntyre2d685c72014-12-08 15:24:12 +0000501 # Simple lookup: look up a VLAN by tag, and return the ID of that
502 # VLAN.
503 #
504 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000505 def get_vlan_id_by_tag(self, tag):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000506 return self._get_element("vlan_id", "vlan", "tag", int(tag))
Steve McIntyref3655062014-12-05 15:34:39 +0000507
Steve McIntyre2d685c72014-12-08 15:24:12 +0000508 # Simple lookup: look up a VLAN by ID, and return the name of that
509 # VLAN.
510 #
511 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000512 def get_vlan_name_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000513 return self._get_element("name", "vlan", "vlan_id", int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100514
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000515 # Simple lookup: look up a VLAN by ID, and return the tag of that
516 # VLAN.
517 #
518 # Returns None on failure.
519 def get_vlan_tag_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000520 return self._get_element("tag", "vlan", "vlan_id", int(vlan_id))
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000521
Steve McIntyrec4890132015-08-07 15:19:11 +0100522 # Simple lookup: look up a trunk by ID, and return all the details
523 # of that trunk.
524 #
525 # Returns None on failure.
526 def get_trunk_by_id(self, trunk_id):
527 return self._get_row("trunk", "trunk_id", int(trunk_id))
528
Steve McIntyre2d685c72014-12-08 15:24:12 +0000529 # Grab one row of a query on one column; useful as a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100530 def _get_row(self, table, field, value):
Steve McIntyree0b842a2014-11-28 18:23:47 +0000531
532 # We really want to use psycopg's type handling deal with the
533 # (potentially) user-supplied data in the value field, so we
534 # have to pass (sql,data) through to cursor.execute. However,
535 # we can't have psycopg do all the argument substitution here
536 # as it will quote all the params like the table name. That
537 # doesn't work. So, we substitute a "%s" for "%s" here so we
538 # keep it after python's own string substitution.
539 sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s")
540
541 # Now, the next icky thing: we need to make sure that we're
542 # passing a dict so that psycopg2 can pick it apart properly
543 # for its own substitution code. We force this with the
544 # trailing comma here
545 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000546 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100547 return self.cursor.fetchone()
548
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000549 # (Un)Lock a port in the database. This can only be done through
550 # the admin interface, and will stop API users from modifying
551 # settings on the port. Use this to lock down ports that are used
552 # for PDUs and other core infrastructure
553 def set_port_is_locked(self, port_id, is_locked):
Steve McIntyre8c64d952014-12-05 16:22:44 +0000554 port = self.get_port_by_id(port_id)
555 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000556 raise InputError("Port ID %d does not exist" % int(port_id))
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000557 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000558 sql = "UPDATE port SET is_locked=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000559 data = (is_locked, port_id)
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000560 self.cursor.execute(sql, data)
561 port_id = self.cursor.fetchone()[0]
562 self.connection.commit()
563 except:
564 self.connection.rollback()
565 raise
Steve McIntyre1c8a3212015-07-14 17:07:31 +0100566 return port_id
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000567
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000568 # Set the mode of a port in the database. Valid values for mode
569 # are "trunk" and "access"
570 def set_port_mode(self, port_id, mode):
571 port = self.get_port_by_id(port_id)
572 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000573 raise InputError("Port ID %d does not exist" % int(port_id))
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000574 if mode == "access":
575 is_trunk = False
576 elif mode == "trunk":
577 is_trunk = True
578 else:
579 raise InputError("Port mode %s is not valid" % mode)
580 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000581 sql = "UPDATE port SET is_trunk=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000582 data = (is_trunk, port_id)
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000583 self.cursor.execute(sql, data)
584 port_id = self.cursor.fetchone()[0]
585 self.connection.commit()
586 except:
587 self.connection.rollback()
588 raise
589 return port_id
590
Steve McIntyre2d685c72014-12-08 15:24:12 +0000591 # Set the current vlan of a port in the database. The VLAN is
592 # passed by ID.
593 #
594 # Constraints:
595 # 1. The port must already exist
596 # 2. The port must not be a trunk port
597 # 3. The port must not be locked
598 # 1. The VLAN must already exist in the database
Steve McIntyre9eb78652014-12-05 17:51:53 +0000599 def set_current_vlan(self, port_id, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000600 port = self.get_port_by_id(port_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000601 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000602 raise InputError("Port ID %d does not exist" % int(port_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100603
Steve McIntyre6dd00be2014-12-05 17:29:35 +0000604 if port.is_trunk or port.is_locked:
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100605 raise CriticalError("The port is locked")
606
Steve McIntyre549435f2014-12-05 15:42:46 +0000607 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000608 if vlan is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000609 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100610
611 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000612 sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000613 data = (vlan_id, port_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000614 self.cursor.execute(sql, data)
Steve McIntyree1371102014-12-05 17:17:09 +0000615 port_id = self.cursor.fetchone()[0]
616 self.connection.commit()
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100617 except:
618 self.connection.rollback()
619 raise
Steve McIntyree1371102014-12-05 17:17:09 +0000620 return port_id
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100621
Steve McIntyre2d685c72014-12-08 15:24:12 +0000622 # Set the base vlan of a port in the database. The VLAN is
623 # passed by ID.
624 #
625 # Constraints:
626 # 1. The port must already exist
627 # 2. The port must not be a trunk port
628 # 3. The port must not be locked
Steve McIntyree653d172015-08-06 16:51:18 +0100629 # 4. The VLAN must already exist in the database
Steve McIntyredaae5502014-12-05 17:55:18 +0000630 def set_base_vlan(self, port_id, vlan_id):
631 port = self.get_port_by_id(port_id)
632 if port is None:
633 raise InputError("Port ID %d does not exist" % int(port_id))
634
635 if port.is_trunk or port.is_locked:
636 raise CriticalError("The port is locked")
637
638 vlan = self.get_vlan_by_id(vlan_id)
639 if vlan is None:
640 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
641 if not vlan.is_base_vlan:
642 raise InputError("VLAN ID %d is not a base VLAN" % int(vlan_id))
643
644 try:
645 sql = "UPDATE port SET base_vlan_id=%s WHERE port_id=%s RETURNING port_id"
646 data = (vlan_id, port_id)
647 self.cursor.execute(sql, data)
648 port_id = self.cursor.fetchone()[0]
649 self.connection.commit()
650 except:
651 self.connection.rollback()
652 raise
653 return port_id
654
Steve McIntyrec4890132015-08-07 15:19:11 +0100655 # Internal function: Attach a port to a trunk in the database.
656 #
657 # Constraints:
658 # 1. The port must already exist
659 # 2. The port must not be locked
660 def _set_port_trunk(self, port_id, trunk_id):
661 port = self.get_port_by_id(port_id)
662 if port is None:
663 raise InputError("Port ID %d does not exist" % int(port_id))
664 if port.is_locked:
665 raise CriticalError("The port is locked")
666 try:
667 sql = "UPDATE port SET trunk_id=%s WHERE port_id=%s RETURNING port_id"
668 data = (int(trunk_id), int(port_id))
669 self.cursor.execute(sql, data)
670 port_id = self.cursor.fetchone()[0]
671 self.connection.commit()
672 except:
673 self.connection.rollback()
674 raise
675 return port_id
676
Steve McIntyre2d685c72014-12-08 15:24:12 +0000677 # Trivial helper function to return all the rows in a given table
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100678 def _dump_table(self, table, order):
Dave Pigott281203e2014-09-17 23:45:02 +0100679 result = []
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100680 self.cursor.execute("SELECT * FROM %s ORDER by %s ASC" % (table, order))
Dave Pigott281203e2014-09-17 23:45:02 +0100681 record = self.cursor.fetchone()
682 while record != None:
Steve McIntyree73eb122014-11-27 15:18:47 +0000683 result.append(record)
Dave Pigott281203e2014-09-17 23:45:02 +0100684 record = self.cursor.fetchone()
685 return result
686
687 def all_switches(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100688 return self._dump_table("switch", "switch_id")
Dave Pigott281203e2014-09-17 23:45:02 +0100689
690 def all_ports(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100691 return self._dump_table("port", "port_id")
Dave Pigott281203e2014-09-17 23:45:02 +0100692
693 def all_vlans(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100694 return self._dump_table("vlan", "vlan_id")
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100695
Steve McIntyrec4890132015-08-07 15:19:11 +0100696 def all_trunks(self):
Steve McIntyree3fb49a2015-09-23 00:04:12 +0100697 return self._dump_table("trunk", "trunk_id")
Steve McIntyrec4890132015-08-07 15:19:11 +0100698
Steve McIntyre6a618162014-12-10 16:47:07 +0000699if __name__ == '__main__':
700 db = VlanDB()
Steve McIntyre6d84ec12014-12-18 16:56:56 +0000701 s = db.all_switches()
702 print 'The DB knows about %d switch(es)' % len(s)
703 print s
704 p = db.all_ports()
705 print 'The DB knows about %d port(s)' % len(p)
706 print p
707 v = db.all_vlans()
708 print 'The DB knows about %d vlan(s)' % len(v)
709 print v
Steve McIntyrec4890132015-08-07 15:19:11 +0100710 t = db.all_trunks()
711 print 'The DB knows about %d trunks(s)' % len(t)
712 print t
Steve McIntyre6a618162014-12-10 16:47:07 +0000713
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100714 print 'First free VLAN tag is %d' % db.find_lowest_unused_vlan_tag()