blob: 9c172a5cfcda378507032dca0a602312529ca83f [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)
30 self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
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
Dave Pigott281203e2014-09-17 23:45:02 +010058 return switch_id
59
Steve McIntyre90a4a972014-11-28 16:50:56 +000060 # Create a new port in the database. Two of the fields are created
61 # with default values (is_locked, is_trunk) here, and should be
62 # updated separately if desired. For the current_vlan_id and
63 # base_vlan_id fields, *BE CAREFUL* that you have already looked
64 # up the correct VLAN_ID for each. This is *NOT* the same as the
65 # VLAN tag (likely to be 1).
66 # You Have Been Warned!
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000067 #
68 # Constraints:
69 # 1. The switch referred to must already exist
70 # 2. The VLANs mentioned here must already exist
Steve McIntyre90a4a972014-11-28 16:50:56 +000071 def create_port(self, name, switch_id, current_vlan_id, base_vlan_id):
Steve McIntyrecb42ebf2014-12-02 12:36:45 +000072
73 switch = self.get_switch(switch_id)
74 if switch is None:
75 raise InputError("Switch id %s does not exist" % switch_id)
76
77 for vlan_id in (current_vlan_id, base_vlan_id):
78 vlan = self.get_vlan(vlan_id)
79 if vlan is None:
80 raise InputError("VLAN id %s does not exist" % vlan_id)
81
Dave Pigott2649a1a2014-09-18 00:04:49 +010082 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +000083 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 +000084 data = (name, switch_id,
85 False, False,
86 current_vlan_id, base_vlan_id)
Steve McIntyredbd7fe52014-11-27 16:54:29 +000087 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +010088 port_id = self.cursor.fetchone()[0]
89 self.connection.commit()
90 except:
91 self.connection.rollback()
92 raise
Dave Pigott281203e2014-09-17 23:45:02 +010093 return port_id
94
Steve McIntyreb005a2f2014-11-28 18:23:05 +000095 # Create a new vlan in the database. We locally add a creation
96 # timestamp, for debug purposes. If vlans seems to be sticking
97 # around, we'll be able to see when they were created.
Steve McIntyredbd7fe52014-11-27 16:54:29 +000098 def create_vlan(self, name, tag, is_base_vlan):
Dave Pigott2649a1a2014-09-18 00:04:49 +010099 try:
Steve McIntyred74d97c2014-11-28 14:44:39 +0000100 dt = datetime.datetime.now()
101 sql = "INSERT INTO vlan (name, tag, is_base_vlan, creation_time) VALUES (%s, %s, %s, %s) RETURNING vlan_id"
102 data = (name, tag, is_base_vlan, dt)
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000103 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100104 vlan_id = self.cursor.fetchone()[0]
105 self.connection.commit()
106 except:
107 self.connection.rollback()
108 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100109 return vlan_id
110
111 def _delete_row(self, table, field, value):
Dave Pigott2649a1a2014-09-18 00:04:49 +0100112 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000113 sql = "DELETE FROM %s WHERE %s = %s"
114 data = (table, field, value)
115 self.cursor.execute(sql, data)
Dave Pigott2649a1a2014-09-18 00:04:49 +0100116 self.connection.commit()
117 except:
118 self.connection.rollback()
119 raise
Dave Pigott281203e2014-09-17 23:45:02 +0100120
121 def delete_switch(self, switch_id):
122 self._delete_row("switch", "switch_id", switch_id)
123
124 def delete_port(self, port_id):
125 self._delete_row("port", "port_id", port_id)
126
127 def delete_vlan(self, vlan_id):
128 self._delete_row("vlan", "vlan_id", vlan_id)
129
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100130 def _get_element(self, select_field, table, compare_field, value):
Steve McIntyre95614c22014-11-28 17:02:44 +0000131
132 # We really want to use psycopg's type handling deal with the
133 # (potentially) user-supplied data in the value field, so we
134 # have to pass (sql,data) through to cursor.execute. However,
135 # we can't have psycopg do all the argument substitution here
136 # as it will quote all the params like the table name. That
137 # doesn't work. So, we substitute a "%s" for "%s" here so we
138 # keep it after python's own string substitution.
139 sql = "SELECT %s FROM %s WHERE %s = %s" % (select_field, table, compare_field, "%s")
140
141 # Now, the next icky thing: we need to make sure that we're
142 # passing a dict so that psycopg2 can pick it apart properly
143 # for its own substitution code. We force this with the
144 # trailing comma here
145 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000146 self.cursor.execute(sql, data)
Steve McIntyre95614c22014-11-28 17:02:44 +0000147
148 # Will raise an exception here if there are no rows that
149 # match. That's OK - the caller needs to deal with that.
Dave Pigott281203e2014-09-17 23:45:02 +0100150 return self.cursor.fetchone()[0]
151
152 def get_switch_id(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100153 return self._get_element("switch_id", "switch", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100154
155 def get_port_id(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100156 return self._get_element("port_id", "port", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100157
Steve McIntyre9f403e82014-11-28 18:10:09 +0000158 def get_vlan_id_from_name(self, name):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100159 return self._get_element("vlan_id", "vlan", "name", name)
Dave Pigott281203e2014-09-17 23:45:02 +0100160
Steve McIntyre9f403e82014-11-28 18:10:09 +0000161 def get_vlan_id_from_tag(self, tag):
162 return self._get_element("vlan_id", "vlan", "tag", tag)
163
Dave Pigott281203e2014-09-17 23:45:02 +0100164 def get_switch_name(self, switch_id):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100165 return self._get_element("name", "switch", "switch_id", switch_id)
Dave Pigott281203e2014-09-17 23:45:02 +0100166
167 def get_port_name(self, port_id):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100168 return self._get_element("port_name", "port", "port_id", port_id)
Dave Pigott281203e2014-09-17 23:45:02 +0100169
170 def get_vlan_name(self, vlan_id):
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100171 return self._get_element("vlan_name", "vlan", "vlan_id", vlan_id)
172
173 def _get_row(self, table, field, value):
Steve McIntyree0b842a2014-11-28 18:23:47 +0000174
175 # We really want to use psycopg's type handling deal with the
176 # (potentially) user-supplied data in the value field, so we
177 # have to pass (sql,data) through to cursor.execute. However,
178 # we can't have psycopg do all the argument substitution here
179 # as it will quote all the params like the table name. That
180 # doesn't work. So, we substitute a "%s" for "%s" here so we
181 # keep it after python's own string substitution.
182 sql = "SELECT * FROM %s WHERE %s = %s" % (table, field, "%s")
183
184 # Now, the next icky thing: we need to make sure that we're
185 # passing a dict so that psycopg2 can pick it apart properly
186 # for its own substitution code. We force this with the
187 # trailing comma here
188 data = (value, )
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000189 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100190 return self.cursor.fetchone()
191
192 def get_switch(self, switch_id):
193 return self._get_row("switch", "switch_id", switch_id)
194
195 def get_port(self, port_id):
196 return self._get_row("port", "port_id", port_id)
197
198 def get_vlan(self, vlan_id):
199 return self._get_row("vlan", "vlan_id", vlan_id)
200
Steve McIntyre3330f4b2014-11-28 18:11:02 +0000201 # (Un)Lock a port in the database. This can only be done through
202 # the admin interface, and will stop API users from modifying
203 # settings on the port. Use this to lock down ports that are used
204 # for PDUs and other core infrastructure
205 def set_port_is_locked(self, port_id, is_locked):
206 try:
207 sql = "UPDATE port SET is_locked=%s WHERE port_id=%s"
208 data = (is_locked, port_id)
209 self.cursor.execute(sql, data)
210 port_id = self.cursor.fetchone()[0]
211 self.connection.commit()
212 except:
213 self.connection.rollback()
214 raise
215 return port_id
216
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100217 def set_vlan(self, port_id, vlan_id):
218 port = self.get_port(port_id)
219 if port == None:
220 raise("Port %s does not exist" % port_id)
221
222 if port["is_trunk"] or port["is_locked"]:
223 raise CriticalError("The port is locked")
224
225 vlan = self.get_vlan(vlan_id)
226 if vlan == None:
227 raise CriticalError("VLAN %s does not exist" % vlan_id)
228
229 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000230 sql = "UPDATE port SET current_vlan_id=%s WHERE port_id=%s"
231 data = (vlan_id, port_id)
232 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100233 except:
234 self.connection.rollback()
235 raise
236
237 def restore_default_vlan(self, port_id):
238 port = self.get_port(port_id)
239 if port == None:
240 raise CriticalError("Port %s does not exist")
241
242 if port["is_trunk"] or port["is_locked"]:
243 raise CriticalError("The port is locked")
244
245 try:
Steve McIntyredbd7fe52014-11-27 16:54:29 +0000246 sql = "UPDATE port SET current_vlan_id=base_vlan_id WHERE port_id=%d"
247 data = port_id
248 self.cursor.execute(sql, data)
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100249 except:
250 self.connection.rollback()
251 raise
252
Dave Pigott281203e2014-09-17 23:45:02 +0100253 def _dump_table(self, table):
254 result = []
255 self.cursor.execute("SELECT * FROM %s" % table)
Dave Pigott281203e2014-09-17 23:45:02 +0100256 record = self.cursor.fetchone()
257 while record != None:
Steve McIntyree73eb122014-11-27 15:18:47 +0000258 result.append(record)
Dave Pigott281203e2014-09-17 23:45:02 +0100259 record = self.cursor.fetchone()
260 return result
261
262 def all_switches(self):
263 return self._dump_table("switch")
264
265 def all_ports(self):
266 return self._dump_table("port")
267
268 def all_vlans(self):
269 return self._dump_table("vlan")
Dave Pigott9b73f3a2014-09-18 22:55:42 +0100270