blob: 2f69f1085c1cc47dc42015a5fa735332e2056994 [file] [log] [blame]
Dave Pigott281203e2014-09-17 23:45:02 +01001#! /usr/bin/python
2
3# Copyright 2014 Linaro Limited
Steve McIntyre663dc062014-10-20 11:11:47 +01004# Author: Dave Pigott <dave.pigott@linaro.org>
Dave Pigott281203e2014-09-17 23:45:02 +01005#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19# MA 02110-1301, USA.
20
21import psycopg2
22import psycopg2.extras
Steve McIntyre6a618162014-12-10 16:47:07 +000023import datetime, os, sys
Steve McIntyre7cf80982015-02-12 07:03:40 +000024import logging
Steve McIntyre6a618162014-12-10 16:47:07 +000025
26if __name__ == '__main__':
27 vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
28 sys.path.insert(0, vlandpath)
29 sys.path.insert(0, "%s/.." % vlandpath)
30
Steve McIntyre6b013652014-12-02 12:35:18 +000031from errors import CriticalError, InputError
Dave Pigott281203e2014-09-17 23:45:02 +010032
33class VlanDB:
34 def __init__(self, db_name="vland", username="vland"):
35 try:
Steve McIntyree38f6222014-11-27 15:09:49 +000036 self.connection = psycopg2.connect(database=db_name, user=username)
Steve McIntyreb09ed282014-12-02 17:59:35 +000037 self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
Dave Pigott281203e2014-09-17 23:45:02 +010038 except Exception as e:
Steve McIntyre5fa22652015-04-01 18:01:45 +010039 logging.error("Failed to access database: %s", e)
Steve McIntyre7cf80982015-02-12 07:03:40 +000040 raise
Dave Pigott281203e2014-09-17 23:45:02 +010041
42 def __del__(self):
43 self.cursor.close()
44 self.connection.close()
45
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000046 # Create a new switch in the database. Switches are really simple
47 # devices - they're just containers for ports.
48 #
49 # Constraints:
50 # Switches must be uniquely named
Steve McIntyredbd7fe52014-11-27 16:54:29 +000051 def create_switch(self, name):
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000052
Steve McIntyre549435f2014-12-05 15:42:46 +000053 switch_id = self.get_switch_id_by_name(name)
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000054 if switch_id is not None:
55 raise InputError("Switch name %s already exists" % name)
56
Dave Pigott2649a1a2014-09-18 00:04:49 +010057 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +000058 sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id"
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000059 data = (name, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +000060 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +010061 switch_id = self.cursor.fetchone()[0]
62 self.connection.commit()
63 except:
64 self.connection.rollback()
65 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +000066
Dave Pigott281203e2014-09-17 23:45:02 +010067 return switch_id
68
Steve McIntyre90a4a972014-11-28 16:50:56 +000069 # Create a new port in the database. Two of the fields are created
70 # with default values (is_locked, is_trunk) here, and should be
71 # updated separately if desired. For the current_vlan_id and
72 # base_vlan_id fields, *BE CAREFUL* that you have already looked
73 # up the correct VLAN_ID for each. This is *NOT* the same as the
74 # VLAN tag (likely to be 1).
75 # You Have Been Warned!
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000076 #
77 # Constraints:
78 # 1. The switch referred to must already exist
79 # 2. The VLANs mentioned here must already exist
Steve McIntyre6a7fdb22014-12-05 15:17:30 +000080 # 3. (Switch/name) must be unique
Steve McIntyreea753972015-08-05 13:52:48 +010081 # 4. (Switch/number) must be unique
82 def create_port(self, switch_id, name, number, current_vlan_id, base_vlan_id):
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000083
Steve McIntyre549435f2014-12-05 15:42:46 +000084 switch = self.get_switch_by_id(switch_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000085 if switch is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +000086 raise InputError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000087
88 for vlan_id in (current_vlan_id, base_vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +000089 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000090 if vlan is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +000091 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +000092
93 port_id = self.get_port_by_switch_and_name(switch_id, name)
94 if port_id is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +000095 raise InputError("Already have a port %s on switch ID %d" % (name, int(switch_id)))
Steve McIntyre6a7fdb22014-12-05 15:17:30 +000096
Steve McIntyreea753972015-08-05 13:52:48 +010097 port_id = self.get_port_by_switch_and_number(switch_id, int(number))
98 if port_id is not None:
99 raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id)))
100
Dave Pigott2649a1a2014-09-18 00:04:49 +0100101 try:
Steve McIntyreea753972015-08-05 13:52:48 +0100102 sql = "INSERT INTO port (name, number, switch_id, is_locked, is_trunk, current_vlan_id, base_vlan_id) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING port_id"
103 data = (name, number, switch_id,
Steve McIntyre90a4a972014-11-28 16:50:56 +0000104 False, False,
Steve McIntyre4b918132014-12-05 17:04:46 +0000105 current_vlan_id, base_vlan_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000106 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100107 port_id = self.cursor.fetchone()[0]
108 self.connection.commit()
109 except:
110 self.connection.rollback()
111 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000112
Dave Pigott281203e2014-09-17 23:45:02 +0100113 return port_id
114
Steve McIntyreb005a2f2014-11-28 18:23:05 +0000115 # Create a new vlan in the database. We locally add a creation
116 # timestamp, for debug purposes. If vlans seems to be sticking
117 # around, we'll be able to see when they were created.
Steve McIntyre31b2df52014-12-02 12:37:54 +0000118 #
119 # Constraints:
120 # Names and tags must be unique
Steve McIntyre57f45912014-12-08 14:43:00 +0000121 # Tags must be in the range 1-4095 (802.1q spec)
Steve McIntyre49777e72014-12-08 16:00:46 +0000122 # Names can be any free-form text, length 1-32 characters
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000123 def create_vlan(self, name, tag, is_base_vlan):
Steve McIntyre31b2df52014-12-02 12:37:54 +0000124
Steve McIntyre57f45912014-12-08 14:43:00 +0000125 if int(tag) < 1 or int(tag) > 4095:
Steve McIntyre49777e72014-12-08 16:00:46 +0000126 raise InputError("VLAN tag %d is outside of the valid range (1-4095)" % int(tag))
127
128 if (len(name) < 1) or (len(name) > 32):
129 raise InputError("VLAN name %s is invalid (must be 1-32 chars)" % name)
Steve McIntyre57f45912014-12-08 14:43:00 +0000130
Steve McIntyrea34c1812014-12-05 15:27:55 +0000131 vlan_id = self.get_vlan_id_by_name(name)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000132 if vlan_id is not None:
133 raise InputError("VLAN name %s is already in use" % name)
134
Steve McIntyre50eb0602014-12-05 15:29:04 +0000135 vlan_id = self.get_vlan_id_by_tag(tag)
Steve McIntyre31b2df52014-12-02 12:37:54 +0000136 if vlan_id is not None:
137 raise InputError("VLAN tag %d is already in use" % int(tag))
138
Dave Pigott2649a1a2014-09-18 00:04:49 +0100139 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +0000140 dt = datetime.datetime.now()
Steve McIntyre4b918132014-12-05 17:04:46 +0000141 sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id"
142 data = (name, tag, is_base_vlan, dt)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000143 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100144 vlan_id = self.cursor.fetchone()[0]
145 self.connection.commit()
146 except:
147 self.connection.rollback()
148 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000149
Dave Pigott281203e2014-09-17 23:45:02 +0100150 return vlan_id
151
Steve McIntyre2d685c72014-12-08 15:24:12 +0000152 # Internal helper function
Dave Pigott281203e2014-09-17 23:45:02 +0100153 def _delete_row(self, table, field, value):
Dave Pigott2649a1a2014-09-18 00:04:49 +0100154 try:
Steve McIntyree03de002014-12-02 17:14:14 +0000155 sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s')
156 data = (value,)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000157 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100158 self.connection.commit()
159 except:
160 self.connection.rollback()
161 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100162
Steve McIntyre388f0e22014-12-02 17:19:04 +0000163 # Delete the specified switch
164 #
165 # Constraints:
166 # 1. The switch must exist
167 # 2. The switch may not be referenced by any ports -
168 # delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100169 def delete_switch(self, switch_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000170 switch = self.get_switch_by_id(switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000171 if switch is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000172 raise InputError("Switch ID %d does not exist" % int(switch_id))
Steve McIntyre388f0e22014-12-02 17:19:04 +0000173 ports = self.get_ports_by_switch(switch_id)
174 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000175 raise InputError("Cannot delete switch ID %d when it still has %d ports" %
176 (int(switch_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100177 self._delete_row("switch", "switch_id", switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000178 return switch_id
Dave Pigott281203e2014-09-17 23:45:02 +0100179
Steve McIntyre6a968622014-12-02 18:01:41 +0000180 # Delete the specified port
181 #
182 # Constraints:
183 # 1. The port must exist
184 # 2. The port must not be locked
Dave Pigott281203e2014-09-17 23:45:02 +0100185 def delete_port(self, port_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000186 port = self.get_port_by_id(port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000187 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000188 raise InputError("Port ID %d does not exist" % int(port_id))
Steve McIntyre6a968622014-12-02 18:01:41 +0000189 if port.is_locked:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000190 raise InputError("Cannot delete port ID %d as it is locked" % int(port_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100191 self._delete_row("port", "port_id", port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000192 return port_id
Dave Pigott281203e2014-09-17 23:45:02 +0100193
Steve McIntyre14552ac2014-12-05 15:23:57 +0000194 # Delete the specified VLAN
195 #
196 # Constraints:
197 # 1. The VLAN
198 # 2. The VLAN may not contain any ports - move or delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100199 def delete_vlan(self, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000200 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000201 if vlan is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000202 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000203 ports = self.get_ports_by_current_vlan(vlan_id)
204 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000205 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
206 (int(vlan_id), len(ports)))
Steve McIntyre14552ac2014-12-05 15:23:57 +0000207 ports = self.get_ports_by_base_vlan(vlan_id)
208 if ports is not None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000209 raise InputError("Cannot delete VLAN ID %d when it still has %d ports" %
210 (int(vlan_id), len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100211 self._delete_row("vlan", "vlan_id", vlan_id)
Steve McIntyre14552ac2014-12-05 15:23:57 +0000212 return vlan_id
Dave Pigott281203e2014-09-17 23:45:02 +0100213
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100214 # Find the lowest unused VLAN tag and return it
215 #
216 # Constraints:
217 # None
218 def find_lowest_unused_vlan_tag(self):
219 sql = "SELECT tag FROM vlan ORDER BY tag ASC"
220 self.cursor.execute(sql,)
221
222 # Walk through the list, looking for gaps
223 last = 1
224 result = None
225
226 for record in self.cursor:
227 if (record[0] - last) > 1:
228 result = last + 1
229 break
230 last = record[0]
231
232 if result is None:
233 result = last + 1
234
235 if result > 4093:
236 raise CriticalError("Can't find any VLAN tags remaining for allocation!")
237
238 return result
239
Steve McIntyre2d685c72014-12-08 15:24:12 +0000240 # Grab one column from one row of a query on one column; useful as
241 # a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100242 def _get_element(self, select_field, table, compare_field, value):
Steve McIntyre95614c22014-11-28 17:02:44 +0000243
244 # We really want to use psycopg's type handling deal with the
245 # (potentially) user-supplied data in the value field, so we
246 # have to pass (sql,data) through to cursor.execute. However,
247 # we can't have psycopg do all the argument substitution here
248 # as it will quote all the params like the table name. That
249 # doesn't work. So, we substitute a "%s" for "%s" here so we
250 # keep it after python's own string substitution.
251 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
252
253 # Now, the next icky thing: we need to make sure that we're
254 # passing a dict so that psycopg2 can pick it apart properly
255 # for its own substitution code. We force this with the
256 # trailing comma here
257 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000258 self.cursor.execute(sql, data)
Steve McIntyre95614c22014-11-28 17:02:44 +0000259
Steve McIntyre58b57a42014-12-02 13:09:21 +0000260 if self.cursor.rowcount > 0:
261 return self.cursor.fetchone()[0]
262 else:
Steve McIntyrec831f9c2014-12-02 12:38:54 +0000263 return None
Dave Pigott281203e2014-09-17 23:45:02 +0100264
Steve McIntyre2d685c72014-12-08 15:24:12 +0000265 # Grab one column from one row of a query on 2 columns; useful as
266 # a quick wrapper
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000267 def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2):
268
269 # We really want to use psycopg's type handling deal with the
270 # (potentially) user-supplied data in the value field, so we
271 # have to pass (sql,data) through to cursor.execute. However,
272 # we can't have psycopg do all the argument substitution here
273 # as it will quote all the params like the table name. That
274 # doesn't work. So, we substitute a "%s" for "%s" here so we
275 # keep it after python's own string substitution.
276 sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s")
277
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000278 data = (value1, value2)
279 self.cursor.execute(sql, data)
280
281 if self.cursor.rowcount > 0:
282 return self.cursor.fetchone()[0]
283 else:
284 return None
285
Steve McIntyre2d685c72014-12-08 15:24:12 +0000286 # Grab one column from multiple rows of a query; useful as a quick
287 # wrapper
Steve McIntyree9da15e2014-12-05 15:22:41 +0000288 def _get_multi_elements(self, select_field, table, compare_field, value):
289
290 # We really want to use psycopg's type handling deal with the
291 # (potentially) user-supplied data in the value field, so we
292 # have to pass (sql,data) through to cursor.execute. However,
293 # we can't have psycopg do all the argument substitution here
294 # as it will quote all the params like the table name. That
295 # doesn't work. So, we substitute a "%s" for "%s" here so we
296 # keep it after python's own string substitution.
297 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
298
299 # Now, the next icky thing: we need to make sure that we're
300 # passing a dict so that psycopg2 can pick it apart properly
301 # for its own substitution code. We force this with the
302 # trailing comma here
303 data = (value, )
304 self.cursor.execute(sql, data)
305
306 if self.cursor.rowcount > 0:
307 results = []
308 for record in self.cursor:
Steve McIntyre52509622014-12-02 17:13:15 +0000309 results.append(record[0])
Steve McIntyree9da15e2014-12-05 15:22:41 +0000310 return results
Steve McIntyre52509622014-12-02 17:13:15 +0000311 else:
312 return None
313
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000314 # Grab one column from multiple rows of a 2-part query; useful as
315 # a wrapper
316 def _get_multi_elements2(self, select_field, table, compare_field1, value1, compare_field2, value2):
317
318 # We really want to use psycopg's type handling deal with the
319 # (potentially) user-supplied data in the value field, so we
320 # have to pass (sql,data) through to cursor.execute. However,
321 # we can't have psycopg do all the argument substitution here
322 # as it will quote all the params like the table name. That
323 # doesn't work. So, we substitute a "%s" for "%s" here so we
324 # keep it after python's own string substitution.
325 sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s")
326
327 data = (value1, value2)
328 self.cursor.execute(sql, data)
329
330 if self.cursor.rowcount > 0:
331 results = []
332 for record in self.cursor:
333 results.append(record[0])
334 return results
335 else:
336 return None
337
Steve McIntyre2d685c72014-12-08 15:24:12 +0000338 # Simple lookup: look up a switch by ID, and return all the
339 # details of that switch.
340 #
341 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000342 def get_switch_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000343 return self._get_row("switch", "switch_id", int(switch_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000344
Steve McIntyre2d685c72014-12-08 15:24:12 +0000345 # Simple lookup: look up a switch by name, and return the ID of
346 # that switch.
347 #
348 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000349 def get_switch_id_by_name(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100350 return self._get_element("switch_id", "switch", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100351
Steve McIntyre2d685c72014-12-08 15:24:12 +0000352 # Simple lookup: look up a switch by ID, and return the name of
353 # that switch.
354 #
355 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000356 def get_switch_name_by_id(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000357 return self._get_element("name", "switch", "switch_id", int(switch_id))
Dave Pigott281203e2014-09-17 23:45:02 +0100358
Steve McIntyre2d685c72014-12-08 15:24:12 +0000359 # Simple lookup: look up a port by ID, and return all the details
360 # of that port.
361 #
362 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000363 def get_port_by_id(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000364 return self._get_row("port", "port_id", int(port_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000365
Steve McIntyre2d685c72014-12-08 15:24:12 +0000366 # Simple lookup: look up a switch by ID, and return the IDs of all
367 # the ports on that switch.
368 #
369 # Returns None on failure.
Steve McIntyreb67f3912014-12-02 17:14:36 +0000370 def get_ports_by_switch(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000371 return self._get_multi_elements("port_id", "port", "switch_id", int(switch_id))
Steve McIntyreb67f3912014-12-02 17:14:36 +0000372
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000373 # More complex lookup: look up all the trunk ports on a switch by
374 # ID
375 #
376 # Returns None on failure.
377 def get_trunk_port_names_by_switch(self, switch_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000378 return self._get_multi_elements2("name", "port", "switch_id", int(switch_id), "is_trunk", True)
Steve McIntyre7201c9b2014-12-17 17:33:51 +0000379
Steve McIntyre2d685c72014-12-08 15:24:12 +0000380 # Simple lookup: look up a port by its name and its parent switch
381 # by ID, and return the ID of the port.
382 #
383 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000384 def get_port_by_switch_and_name(self, switch_id, name):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000385 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "name", name)
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000386
Steve McIntyre45f55012015-08-05 13:55:15 +0100387 # Simple lookup: look up a port by its external name and its
388 # parent switch by ID, and return the ID of the port.
389 #
390 # Returns None on failure.
391 def get_port_by_switch_and_number(self, switch_id, number):
392 return self._get_element2("port_id", "port", "switch_id", int(switch_id), "number", int(number))
393
Steve McIntyre2d685c72014-12-08 15:24:12 +0000394 # Simple lookup: look up a port by ID, and return the current VLAN
395 # id of that port.
396 #
397 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000398 def get_current_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000399 return self._get_element("current_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000400
Steve McIntyre2d685c72014-12-08 15:24:12 +0000401 # Simple lookup: look up a port by ID, and return the base VLAN
402 # id of that port.
403 #
404 # Returns None on failure.
Steve McIntyredaae5502014-12-05 17:55:18 +0000405 def get_base_vlan_id_by_port(self, port_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000406 return self._get_element("base_vlan_id", "port", "port_id", int(port_id))
Steve McIntyredaae5502014-12-05 17:55:18 +0000407
Steve McIntyre2d685c72014-12-08 15:24:12 +0000408 # Simple lookup: look up a current VLAN by ID, and return the IDs
409 # of all the ports on that VLAN.
410 #
411 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000412 def get_ports_by_current_vlan(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000413 return self._get_multi_elements("port_id", "port", "current_vlan_id", int(vlan_id))
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000414
Steve McIntyre2d685c72014-12-08 15:24:12 +0000415 # Simple lookup: look up a base VLAN by ID, and return the IDs
416 # of all the ports on that VLAN.
417 #
418 # Returns None on failure.
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000419 def get_ports_by_base_vlan(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000420 return self._get_multi_elements("port_id", "port", "base_vlan_id", int(vlan_id))
Steve McIntyre53a7bc82014-12-05 15:23:34 +0000421
Steve McIntyre2d685c72014-12-08 15:24:12 +0000422 # Simple lookup: look up a VLAN by ID, and return all the details
423 # of that VLAN.
424 #
425 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000426 def get_vlan_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000427 return self._get_row("vlan", "vlan_id", int(vlan_id))
Steve McIntyref3655062014-12-05 15:34:39 +0000428
Steve McIntyre2d685c72014-12-08 15:24:12 +0000429 # Simple lookup: look up a VLAN by name, and return the ID of that
430 # VLAN.
431 #
432 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000433 def get_vlan_id_by_name(self, name):
434 return self._get_element("vlan_id", "vlan", "name", name)
435
Steve McIntyre2d685c72014-12-08 15:24:12 +0000436 # Simple lookup: look up a VLAN by tag, and return the ID of that
437 # VLAN.
438 #
439 # Returns None on failure.
Steve McIntyref3655062014-12-05 15:34:39 +0000440 def get_vlan_id_by_tag(self, tag):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000441 return self._get_element("vlan_id", "vlan", "tag", int(tag))
Steve McIntyref3655062014-12-05 15:34:39 +0000442
Steve McIntyre2d685c72014-12-08 15:24:12 +0000443 # Simple lookup: look up a VLAN by ID, and return the name of that
444 # VLAN.
445 #
446 # Returns None on failure.
Steve McIntyre549435f2014-12-05 15:42:46 +0000447 def get_vlan_name_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000448 return self._get_element("name", "vlan", "vlan_id", int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100449
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000450 # Simple lookup: look up a VLAN by ID, and return the tag of that
451 # VLAN.
452 #
453 # Returns None on failure.
454 def get_vlan_tag_by_id(self, vlan_id):
Steve McIntyre32e3a892015-01-23 17:47:46 +0000455 return self._get_element("tag", "vlan", "vlan_id", int(vlan_id))
Steve McIntyreb9b0aa52014-12-21 23:31:12 +0000456
Steve McIntyre2d685c72014-12-08 15:24:12 +0000457 # Grab one row of a query on one column; useful as a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100458 def _get_row(self, table, field, value):
Steve McIntyree0b842a2014-11-28 18:23:47 +0000459
460 # We really want to use psycopg's type handling deal with the
461 # (potentially) user-supplied data in the value field, so we
462 # have to pass (sql,data) through to cursor.execute. However,
463 # we can't have psycopg do all the argument substitution here
464 # as it will quote all the params like the table name. That
465 # doesn't work. So, we substitute a "%s" for "%s" here so we
466 # keep it after python's own string substitution.
467 sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s")
468
469 # Now, the next icky thing: we need to make sure that we're
470 # passing a dict so that psycopg2 can pick it apart properly
471 # for its own substitution code. We force this with the
472 # trailing comma here
473 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000474 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100475 return self.cursor.fetchone()
476
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000477 # (Un)Lock a port in the database. This can only be done through
478 # the admin interface, and will stop API users from modifying
479 # settings on the port. Use this to lock down ports that are used
480 # for PDUs and other core infrastructure
481 def set_port_is_locked(self, port_id, is_locked):
Steve McIntyre8c64d952014-12-05 16:22:44 +0000482 port = self.get_port_by_id(port_id)
483 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000484 raise InputError("Port ID %d does not exist" % int(port_id))
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000485 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000486 sql = "UPDATE port SET is_locked=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000487 data = (is_locked, port_id)
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000488 self.cursor.execute(sql, data)
489 port_id = self.cursor.fetchone()[0]
490 self.connection.commit()
491 except:
492 self.connection.rollback()
493 raise
Steve McIntyre1c8a3212015-07-14 17:07:31 +0100494 return port_id
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000495
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000496 # Set the mode of a port in the database. Valid values for mode
497 # are "trunk" and "access"
498 def set_port_mode(self, port_id, mode):
499 port = self.get_port_by_id(port_id)
500 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000501 raise InputError("Port ID %d does not exist" % int(port_id))
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000502 if mode == "access":
503 is_trunk = False
504 elif mode == "trunk":
505 is_trunk = True
506 else:
507 raise InputError("Port mode %s is not valid" % mode)
508 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000509 sql = "UPDATE port SET is_trunk=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000510 data = (is_trunk, port_id)
Steve McIntyre4204d0d2014-12-05 16:24:10 +0000511 self.cursor.execute(sql, data)
512 port_id = self.cursor.fetchone()[0]
513 self.connection.commit()
514 except:
515 self.connection.rollback()
516 raise
517 return port_id
518
Steve McIntyre2d685c72014-12-08 15:24:12 +0000519 # Set the current vlan of a port in the database. The VLAN is
520 # passed by ID.
521 #
522 # Constraints:
523 # 1. The port must already exist
524 # 2. The port must not be a trunk port
525 # 3. The port must not be locked
526 # 1. The VLAN must already exist in the database
Steve McIntyre9eb78652014-12-05 17:51:53 +0000527 def set_current_vlan(self, port_id, vlan_id):
Steve McIntyre549435f2014-12-05 15:42:46 +0000528 port = self.get_port_by_id(port_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000529 if port is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000530 raise InputError("Port ID %d does not exist" % int(port_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100531
Steve McIntyre6dd00be2014-12-05 17:29:35 +0000532 if port.is_trunk or port.is_locked:
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100533 raise CriticalError("The port is locked")
534
Steve McIntyre549435f2014-12-05 15:42:46 +0000535 vlan = self.get_vlan_by_id(vlan_id)
Steve McIntyre028b3cc2014-12-05 16:24:46 +0000536 if vlan is None:
Steve McIntyrea1c75222014-12-05 16:57:13 +0000537 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100538
539 try:
Steve McIntyree1371102014-12-05 17:17:09 +0000540 sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s RETURNING port_id"
Steve McIntyre4b918132014-12-05 17:04:46 +0000541 data = (vlan_id, port_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000542 self.cursor.execute(sql, data)
Steve McIntyree1371102014-12-05 17:17:09 +0000543 port_id = self.cursor.fetchone()[0]
544 self.connection.commit()
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100545 except:
546 self.connection.rollback()
547 raise
Steve McIntyree1371102014-12-05 17:17:09 +0000548 return port_id
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100549
Steve McIntyre2d685c72014-12-08 15:24:12 +0000550 # Set the base vlan of a port in the database. The VLAN is
551 # passed by ID.
552 #
553 # Constraints:
554 # 1. The port must already exist
555 # 2. The port must not be a trunk port
556 # 3. The port must not be locked
557 # 1. The VLAN must already exist in the database
Steve McIntyredaae5502014-12-05 17:55:18 +0000558 def set_base_vlan(self, port_id, vlan_id):
559 port = self.get_port_by_id(port_id)
560 if port is None:
561 raise InputError("Port ID %d does not exist" % int(port_id))
562
563 if port.is_trunk or port.is_locked:
564 raise CriticalError("The port is locked")
565
566 vlan = self.get_vlan_by_id(vlan_id)
567 if vlan is None:
568 raise InputError("VLAN ID %d does not exist" % int(vlan_id))
569 if not vlan.is_base_vlan:
570 raise InputError("VLAN ID %d is not a base VLAN" % int(vlan_id))
571
572 try:
573 sql = "UPDATE port SET base_vlan_id=%s WHERE port_id=%s RETURNING port_id"
574 data = (vlan_id, port_id)
575 self.cursor.execute(sql, data)
576 port_id = self.cursor.fetchone()[0]
577 self.connection.commit()
578 except:
579 self.connection.rollback()
580 raise
581 return port_id
582
Steve McIntyre2d685c72014-12-08 15:24:12 +0000583 # Trivial helper function to return all the rows in a given table
Dave Pigott281203e2014-09-17 23:45:02 +0100584 def _dump_table(self, table):
585 result = []
586 self.cursor.execute("SELECT * FROM %s" % table)
Dave Pigott281203e2014-09-17 23:45:02 +0100587 record = self.cursor.fetchone()
588 while record != None:
Steve McIntyree73eb122014-11-27 15:18:47 +0000589 result.append(record)
Dave Pigott281203e2014-09-17 23:45:02 +0100590 record = self.cursor.fetchone()
591 return result
592
593 def all_switches(self):
594 return self._dump_table("switch")
595
596 def all_ports(self):
597 return self._dump_table("port")
598
599 def all_vlans(self):
600 return self._dump_table("vlan")
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100601
Steve McIntyre6a618162014-12-10 16:47:07 +0000602if __name__ == '__main__':
603 db = VlanDB()
Steve McIntyre6d84ec12014-12-18 16:56:56 +0000604 s = db.all_switches()
605 print 'The DB knows about %d switch(es)' % len(s)
606 print s
607 p = db.all_ports()
608 print 'The DB knows about %d port(s)' % len(p)
609 print p
610 v = db.all_vlans()
611 print 'The DB knows about %d vlan(s)' % len(v)
612 print v
Steve McIntyre6a618162014-12-10 16:47:07 +0000613
Steve McIntyre6c4f33f2015-08-03 19:25:07 +0100614 print 'First free VLAN tag is %d' % db.find_lowest_unused_vlan_tag()