Add support for storing and using lock_reason

lock_reason is simple optional text for admins to use to store why
they've locked a port in VLANd. Helpful as reminders later on!

Changes:

 * Added database schema V2, with upgrade code to go with it. Adds the
   "lock_reason" field in the port table.
 * Deal with changes to all the database methods dealing with the
   port table. set_port_is_locked() now takes an extra parameter for
   lock_reason.
 * API call db.set_port_is_locked() updated to match
 * admin.py updated to match - allow optional setting of lock_reason
   when locking a port, and print the reason when displaying port
   details too
 * In the web interface when hovering over a locked port, now also
   display lock_reason if it's available, or "unknown reason"
   otherwise

Change-Id: I31abc7308fd95e264729abc32ee855ac327a9abd
diff --git a/admin.py b/admin.py
index ca13a0d..706d24b 100755
--- a/admin.py
+++ b/admin.py
@@ -51,7 +51,7 @@
         switch[1])
 
 def dump_port(port):
-    print "port_id:%d name:%s switch_id:%d locked:%s mode:%s base_vlan_id:%d current_vlan_id:%d number:%d trunk_id:%s" % (
+    print "port_id:%d name:%s switch_id:%d locked:%s mode:%s base_vlan_id:%d current_vlan_id:%d number:%d trunk_id:%s lock_reason:%s" % (
         int(port[0]),
         port[1],
         int(port[2]),
@@ -60,7 +60,8 @@
         int(port[5]),
         int(port[6]),
         int(port[7]),
-        'None' if (TRUNK_ID_NONE == port[8]) else port[8])
+        'None' if (TRUNK_ID_NONE == port[8]) else port[8],
+        port[9])
 
 def dump_vlan(vlan):
     print "vlan_id:%d name:%s tag:%d is_base_vlan:%s, creation_time:%s" % (
@@ -249,6 +250,9 @@
 p_lock_port.add_argument('--port_id',
                 required = True,
                 help = "The ID of the port to lock")
+p_lock_port.add_argument('--reason',
+                required = False,
+                help = "(Optional) The reason for locking the port")
 p_unlock_port = sp.add_parser("unlock_port",
                 help = "Unlock the settings on a port")
 p_unlock_port.set_defaults(which = "unlock_port")
@@ -714,7 +718,8 @@
                              {'command':'db.set_port_is_locked',
                               'data':
                               {'port_id': args.port_id,
-                               'is_locked': True}})
+                               'is_locked': True,
+                               'lock_reason': args.reason}})
         print "Locked port_id %d" % port_id
     except InputError as inst:
         print 'Failed: %s' % inst
@@ -728,7 +733,8 @@
                              {'command':'db.set_port_is_locked',
                               'data':
                               {'port_id': args.port_id,
-                               'is_locked': False}})
+                               'is_locked': False,
+                               'lock_reason': ""}})
         print "Unlocked port_id %d" % port_id
     except InputError as inst:
         print 'Failed: %s' % inst
diff --git a/db/db.py b/db/db.py
index ec8e1f4..003f973 100644
--- a/db/db.py
+++ b/db/db.py
@@ -30,8 +30,12 @@
 # no version!) at startup, it will auto-migrate to the latest version
 #
 # Version 0: Base, no version found
+#
 # Version 1: No changes, except adding the version and coping with upgrade
-DATABASE_SCHEMA_VERSION = 1
+#
+# Version 2: Add "lock_reason" field in the port table, and code to deal with
+#            it
+DATABASE_SCHEMA_VERSION = 2
 
 if __name__ == '__main__':
     vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
@@ -95,7 +99,13 @@
             logging.info("Upgrading database to match schema version 1")
             sql = "ALTER TABLE state ADD schema_version INTEGER"
             self.cursor.execute(sql)
-            logging.info("Sschema version 1 upgrade successful")
+            logging.info("Schema version 1 upgrade successful")
+
+        if current_db_version < 2:
+            logging.info("Upgrading database to match schema version 2")
+            sql = "ALTER TABLE port ADD lock_reason VARCHAR(64)"
+            self.cursor.execute(sql)
+            logging.info("Schema version 2 upgrade successful")
 
         sql = "INSERT INTO state (last_modified, schema_version) VALUES (%s, %s)"
         data = (datetime.datetime.now(), DATABASE_SCHEMA_VERSION)
@@ -159,9 +169,10 @@
             raise InputError("Already have a port %d on switch ID %d" % (int(number), int(switch_id)))
 
         try:
-            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"
+            sql = "INSERT INTO port (name, number, switch_id, is_locked, lock_reason, is_trunk, current_vlan_id, base_vlan_id, trunk_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING port_id"
             data = (name, number, switch_id,
-                    False, False,
+                    False, "",
+                    False,
                     current_vlan_id, base_vlan_id, TRUNK_ID_NONE)
             self.cursor.execute(sql, data)
             port_id = self.cursor.fetchone()[0]
@@ -633,20 +644,20 @@
     # the admin interface, and will stop API users from modifying
     # settings on the port. Use this to lock down ports that are used
     # for PDUs and other core infrastructure
-    def set_port_is_locked(self, port_id, is_locked):
+    def set_port_is_locked(self, port_id, is_locked, lock_reason=""):
         port = self.get_port_by_id(port_id)
         if port is None:
             raise NotFoundError("Port ID %d does not exist" % int(port_id))
         try:
-            sql = "UPDATE port SET is_locked=%s WHERE port_id=%s RETURNING port_id"
-            data = (is_locked, port_id)
+            sql = "UPDATE port SET is_locked=%s, lock_reason=%s WHERE port_id=%s RETURNING port_id"
+            data = (is_locked, lock_reason, port_id)
             self.cursor.execute(sql, data)
             port_id = self.cursor.fetchone()[0]
             self.cursor.execute("UPDATE state SET last_modified=%s", (datetime.datetime.now(),))
             self.connection.commit()
         except:
             self.connection.rollback()
-            raise
+            raise InputError("lock failed on Port ID %d" % int(port_id))
         return port_id
 
     # Set the mode of a port in the database. Valid values for mode
diff --git a/util.py b/util.py
index 090e06e..9474211 100644
--- a/util.py
+++ b/util.py
@@ -193,7 +193,9 @@
             elif command == 'db.delete_port':
                 ret = db.delete_port(data['port_id'])
             elif command == 'db.set_port_is_locked':
-                ret = db.set_port_is_locked(data['port_id'], data['is_locked'])
+                ret = db.set_port_is_locked(data['port_id'],
+                                            data['is_locked'],
+                                            data['lock_reason'])
             elif command == 'db.set_base_vlan':
                 ret = db.set_base_vlan(data['port_id'], data['base_vlan_id'])
             elif command == 'db.delete_trunk':
diff --git a/visualisation/visualisation.py b/visualisation/visualisation.py
index e73c860..fd24435 100644
--- a/visualisation/visualisation.py
+++ b/visualisation/visualisation.py
@@ -239,7 +239,13 @@
                     page.append('<div class="port" id="port%d.%d">' % (vlan.vlan_id, port.port_id))
                     page.append('Port ID: %d    Port number: %d<br>' % (port.port_id, port.number))
                     if port.is_locked:
-                        page.append('Locked<br>')
+                        page.append('Locked - ')
+                        if (port.lock_reason is not None
+                                and len(port.lock_reason) > 1):
+                            page.append(port.lock_reason)
+                        else:
+                            page.append('unknown reason')
+                        page.append('<br>')
                     if port.is_trunk:
                         page.append('Trunk')
                         if port.trunk_id != -1: