blob: 0708cb6de0115409de1f154819510598b3194193 [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 McIntyred74d97c2014-11-28 14:44:39 +000023import datetime
Steve McIntyre6b013652014-12-02 12:35:18 +000024from errors import CriticalError, InputError
Dave Pigott281203e2014-09-17 23:45:02 +010025
26class VlanDB:
27 def __init__(self, db_name="vland", username="vland"):
28 try:
Steve McIntyree38f6222014-11-27 15:09:49 +000029 self.connection = psycopg2.connect(database=db_name, user=username)
Steve McIntyreb09ed282014-12-02 17:59:35 +000030 self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
Dave Pigott281203e2014-09-17 23:45:02 +010031 except Exception as e:
32 print "Failed to access database: %s" % e
33
34 def __del__(self):
35 self.cursor.close()
36 self.connection.close()
37
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000038 # Create a new switch in the database. Switches are really simple
39 # devices - they're just containers for ports.
40 #
41 # Constraints:
42 # Switches must be uniquely named
Steve McIntyredbd7fe52014-11-27 16:54:29 +000043 def create_switch(self, name):
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000044
45 switch_id = self.get_switch_id(name)
46 if switch_id is not None:
47 raise InputError("Switch name %s already exists" % name)
48
Dave Pigott2649a1a2014-09-18 00:04:49 +010049 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +000050 sql = "INSERT INTO switch (name) VALUES (%s) RETURNING switch_id"
Steve McIntyre31d6dfa2014-12-02 12:35:56 +000051 data = (name, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +000052 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +010053 switch_id = self.cursor.fetchone()[0]
54 self.connection.commit()
55 except:
56 self.connection.rollback()
57 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +000058
Dave Pigott281203e2014-09-17 23:45:02 +010059 return switch_id
60
Steve McIntyre90a4a972014-11-28 16:50:56 +000061 # Create a new port in the database. Two of the fields are created
62 # with default values (is_locked, is_trunk) here, and should be
63 # updated separately if desired. For the current_vlan_id and
64 # base_vlan_id fields, *BE CAREFUL* that you have already looked
65 # up the correct VLAN_ID for each. This is *NOT* the same as the
66 # VLAN tag (likely to be 1).
67 # You Have Been Warned!
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000068 #
69 # Constraints:
70 # 1. The switch referred to must already exist
71 # 2. The VLANs mentioned here must already exist
Steve McIntyre1d10dbe2014-12-02 18:23:36 +000072 def create_port(self, switch_id, name, current_vlan_id, base_vlan_id):
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000073
74 switch = self.get_switch(switch_id)
75 if switch is None:
76 raise InputError("Switch id %s does not exist" % switch_id)
77
78 for vlan_id in (current_vlan_id, base_vlan_id):
79 vlan = self.get_vlan(vlan_id)
80 if vlan is None:
81 raise InputError("VLAN id %s does not exist" % vlan_id)
82
Dave Pigott2649a1a2014-09-18 00:04:49 +010083 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +000084 sql = "INSERT INTO port (name, switch_id, is_locked, is_trunk, current_vlan_id, base_vlan_id) VALUES (%s, %s, %s, %s, %s, %s) RETURNING port_id"
Steve McIntyre90a4a972014-11-28 16:50:56 +000085 data = (name, switch_id,
86 False, False,
87 current_vlan_id, base_vlan_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +000088 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +010089 port_id = self.cursor.fetchone()[0]
90 self.connection.commit()
91 except:
92 self.connection.rollback()
93 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +000094
Dave Pigott281203e2014-09-17 23:45:02 +010095 return port_id
96
Steve McIntyreb005a2f2014-11-28 18:23:05 +000097 # Create a new vlan in the database. We locally add a creation
98 # timestamp, for debug purposes. If vlans seems to be sticking
99 # around, we'll be able to see when they were created.
Steve McIntyre31b2df52014-12-02 12:37:54 +0000100 #
101 # Constraints:
102 # Names and tags must be unique
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000103 def create_vlan(self, name, tag, is_base_vlan):
Steve McIntyre31b2df52014-12-02 12:37:54 +0000104
105 vlan_id = self.get_vlan_id_from_name(name)
106 if vlan_id is not None:
107 raise InputError("VLAN name %s is already in use" % name)
108
109 vlan_id = self.get_vlan_id_from_tag(tag)
110 if vlan_id is not None:
111 raise InputError("VLAN tag %d is already in use" % int(tag))
112
Dave Pigott2649a1a2014-09-18 00:04:49 +0100113 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +0000114 dt = datetime.datetime.now()
115 sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id"
116 data = (name, tag, is_base_vlan, dt)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000117 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100118 vlan_id = self.cursor.fetchone()[0]
119 self.connection.commit()
120 except:
121 self.connection.rollback()
122 raise
Steve McIntyree1febdb2014-12-02 12:39:14 +0000123
Dave Pigott281203e2014-09-17 23:45:02 +0100124 return vlan_id
125
126 def _delete_row(self, table, field, value):
Dave Pigott2649a1a2014-09-18 00:04:49 +0100127 try:
Steve McIntyree03de002014-12-02 17:14:14 +0000128 sql = "DELETE FROM %s WHERE %s = %s" % (table, field, '%s')
129 data = (value,)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000130 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100131 self.connection.commit()
132 except:
133 self.connection.rollback()
134 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100135
Steve McIntyre388f0e22014-12-02 17:19:04 +0000136 # Delete the specified switch
137 #
138 # Constraints:
139 # 1. The switch must exist
140 # 2. The switch may not be referenced by any ports -
141 # delete them first!
Dave Pigott281203e2014-09-17 23:45:02 +0100142 def delete_switch(self, switch_id):
Steve McIntyre388f0e22014-12-02 17:19:04 +0000143 switch = self.get_switch(switch_id)
144 if switch is None:
Steve McIntyrecaa40a62014-12-02 17:59:54 +0000145 raise InputError("Switch ID %s does not exist" % switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000146 ports = self.get_ports_by_switch(switch_id)
147 if ports is not None:
148 raise InputError("Cannot delete switch ID %s when it still has %d ports" %
149 (switch_id, len(ports)))
Dave Pigott281203e2014-09-17 23:45:02 +0100150 self._delete_row("switch", "switch_id", switch_id)
Steve McIntyre388f0e22014-12-02 17:19:04 +0000151 return switch_id
Dave Pigott281203e2014-09-17 23:45:02 +0100152
Steve McIntyre6a968622014-12-02 18:01:41 +0000153 # Delete the specified port
154 #
155 # Constraints:
156 # 1. The port must exist
157 # 2. The port must not be locked
Dave Pigott281203e2014-09-17 23:45:02 +0100158 def delete_port(self, port_id):
Steve McIntyre6a968622014-12-02 18:01:41 +0000159 port = self.get_port(port_id)
160 if port is None:
161 raise InputError("Port ID %s does not exist" % port_id)
162 if port.is_locked:
163 raise InputError("Cannot delete port ID %s as it is locked" % port_id)
Dave Pigott281203e2014-09-17 23:45:02 +0100164 self._delete_row("port", "port_id", port_id)
Steve McIntyre6a968622014-12-02 18:01:41 +0000165 return port_id
Dave Pigott281203e2014-09-17 23:45:02 +0100166
167 def delete_vlan(self, vlan_id):
168 self._delete_row("vlan", "vlan_id", vlan_id)
169
Steve McIntyrec3b2ae42014-12-02 17:13:36 +0000170 # Grab one column from one row of a query; useful as a quick wrapper
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100171 def _get_element(self, select_field, table, compare_field, value):
Steve McIntyre95614c22014-11-28 17:02:44 +0000172
173 # We really want to use psycopg's type handling deal with the
174 # (potentially) user-supplied data in the value field, so we
175 # have to pass (sql,data) through to cursor.execute. However,
176 # we can't have psycopg do all the argument substitution here
177 # as it will quote all the params like the table name. That
178 # doesn't work. So, we substitute a "%s" for "%s" here so we
179 # keep it after python's own string substitution.
180 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
181
182 # Now, the next icky thing: we need to make sure that we're
183 # passing a dict so that psycopg2 can pick it apart properly
184 # for its own substitution code. We force this with the
185 # trailing comma here
186 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000187 self.cursor.execute(sql, data)
Steve McIntyre95614c22014-11-28 17:02:44 +0000188
Steve McIntyre58b57a42014-12-02 13:09:21 +0000189 if self.cursor.rowcount > 0:
190 return self.cursor.fetchone()[0]
191 else:
Steve McIntyrec831f9c2014-12-02 12:38:54 +0000192 return None
Dave Pigott281203e2014-09-17 23:45:02 +0100193
Steve McIntyre52509622014-12-02 17:13:15 +0000194 # Grab one column from multiple rows of a query; useful as a quick wrapper
195 def _get_multi_elements(self, select_field, table, compare_field, value):
196
197 # We really want to use psycopg's type handling deal with the
198 # (potentially) user-supplied data in the value field, so we
199 # have to pass (sql,data) through to cursor.execute. However,
200 # we can't have psycopg do all the argument substitution here
201 # as it will quote all the params like the table name. That
202 # doesn't work. So, we substitute a "%s" for "%s" here so we
203 # keep it after python's own string substitution.
204 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
205
206 # Now, the next icky thing: we need to make sure that we're
207 # passing a dict so that psycopg2 can pick it apart properly
208 # for its own substitution code. We force this with the
209 # trailing comma here
210 data = (value, )
211 self.cursor.execute(sql, data)
212
213 if self.cursor.rowcount > 0:
214 results = []
215 for record in self.cursor:
Steve McIntyrea74c7fe2014-12-02 18:49:38 +0000216 # Grab one column from one row of a query on 2 columns; useful as a quick wrapper
217 def _get_element2(self, select_field, table, compare_field1, value1, compare_field2, value2):
218
219 # We really want to use psycopg's type handling deal with the
220 # (potentially) user-supplied data in the value field, so we
221 # have to pass (sql,data) through to cursor.execute. However,
222 # we can't have psycopg do all the argument substitution here
223 # as it will quote all the params like the table name. That
224 # doesn't work. So, we substitute a "%s" for "%s" here so we
225 # keep it after python's own string substitution.
226 sql = "SELECT %s FROM %s WHERE %s = %s AND %s = %s" % (select_field, table, compare_field1, "%s", compare_field2, "%s")
227
228 # Now, the next icky thing: we need to make sure that we're
229 # passing a dict so that psycopg2 can pick it apart properly
230 # for its own substitution code. We force this with the
231 # trailing comma here
232 data = (value1, value2)
233 self.cursor.execute(sql, data)
234
235 if self.cursor.rowcount > 0:
236 return self.cursor.fetchone()[0]
237 else:
238 return None
239
Steve McIntyre52509622014-12-02 17:13:15 +0000240 results.append(record[0])
241 else:
242 return None
243
Dave Pigott281203e2014-09-17 23:45:02 +0100244 def get_switch_id(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100245 return self._get_element("switch_id", "switch", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100246
247 def get_port_id(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100248 return self._get_element("port_id", "port", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100249
Steve McIntyre9f403e82014-11-28 18:10:09 +0000250 def get_vlan_id_from_name(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100251 return self._get_element("vlan_id", "vlan", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100252
Steve McIntyre9f403e82014-11-28 18:10:09 +0000253 def get_vlan_id_from_tag(self, tag):
254 return self._get_element("vlan_id", "vlan", "tag", tag)
255
Dave Pigott281203e2014-09-17 23:45:02 +0100256 def get_switch_name(self, switch_id):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100257 return self._get_element("name", "switch", "switch_id", switch_id)
Dave Pigott281203e2014-09-17 23:45:02 +0100258
259 def get_port_name(self, port_id):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100260 return self._get_element("port_name", "port", "port_id", port_id)
Dave Pigott281203e2014-09-17 23:45:02 +0100261
Steve McIntyreb67f3912014-12-02 17:14:36 +0000262 def get_ports_by_switch(self, switch_id):
263 return self._get_multi_elements("port_id", "port", "switch_id", switch_id)
264
Dave Pigott281203e2014-09-17 23:45:02 +0100265 def get_vlan_name(self, vlan_id):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100266 return self._get_element("vlan_name", "vlan", "vlan_id", vlan_id)
267
268 def _get_row(self, table, field, value):
Steve McIntyree0b842a2014-11-28 18:23:47 +0000269
270 # We really want to use psycopg's type handling deal with the
271 # (potentially) user-supplied data in the value field, so we
272 # have to pass (sql,data) through to cursor.execute. However,
273 # we can't have psycopg do all the argument substitution here
274 # as it will quote all the params like the table name. That
275 # doesn't work. So, we substitute a "%s" for "%s" here so we
276 # keep it after python's own string substitution.
277 sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s")
278
279 # Now, the next icky thing: we need to make sure that we're
280 # passing a dict so that psycopg2 can pick it apart properly
281 # for its own substitution code. We force this with the
282 # trailing comma here
283 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000284 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100285 return self.cursor.fetchone()
286
287 def get_switch(self, switch_id):
288 return self._get_row("switch", "switch_id", switch_id)
289
290 def get_port(self, port_id):
291 return self._get_row("port", "port_id", port_id)
292
293 def get_vlan(self, vlan_id):
294 return self._get_row("vlan", "vlan_id", vlan_id)
295
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000296 # (Un)Lock a port in the database. This can only be done through
297 # the admin interface, and will stop API users from modifying
298 # settings on the port. Use this to lock down ports that are used
299 # for PDUs and other core infrastructure
300 def set_port_is_locked(self, port_id, is_locked):
301 try:
302 sql = "UPDATE port SET is_locked=%s WHERE port_id=%s"
303 data = (is_locked, port_id)
304 self.cursor.execute(sql, data)
305 port_id = self.cursor.fetchone()[0]
306 self.connection.commit()
307 except:
308 self.connection.rollback()
309 raise
310 return port_id
311
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100312 def set_vlan(self, port_id, vlan_id):
313 port = self.get_port(port_id)
314 if port == None:
315 raise("Port %s does not exist" % port_id)
316
317 if port["is_trunk"] or port["is_locked"]:
318 raise CriticalError("The port is locked")
319
320 vlan = self.get_vlan(vlan_id)
321 if vlan == None:
322 raise CriticalError("VLAN %s does not exist" % vlan_id)
323
324 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000325 sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s"
326 data = (vlan_id, port_id)
327 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100328 except:
329 self.connection.rollback()
330 raise
331
332 def restore_default_vlan(self, port_id):
333 port = self.get_port(port_id)
334 if port == None:
335 raise CriticalError("Port %s does not exist")
336
337 if port["is_trunk"] or port["is_locked"]:
338 raise CriticalError("The port is locked")
339
340 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000341 sql = "UPDATE port SET current_vlan_id=base_vlan_id WHERE port_id=%d"
342 data = port_id
343 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100344 except:
345 self.connection.rollback()
346 raise
347
Dave Pigott281203e2014-09-17 23:45:02 +0100348 def _dump_table(self, table):
349 result = []
350 self.cursor.execute("SELECT * FROM %s" % table)
Dave Pigott281203e2014-09-17 23:45:02 +0100351 record = self.cursor.fetchone()
352 while record != None:
Steve McIntyree73eb122014-11-27 15:18:47 +0000353 result.append(record)
Dave Pigott281203e2014-09-17 23:45:02 +0100354 record = self.cursor.fetchone()
355 return result
356
357 def all_switches(self):
358 return self._dump_table("switch")
359
360 def all_ports(self):
361 return self._dump_table("port")
362
363 def all_vlans(self):
364 return self._dump_table("vlan")
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100365