Add shutdown option for zuul_console

This should look for the process holding open the port, and then delete
all of the remaining files.

Co-Authored-By: Clark Boylan <clark.boylan@gmail.com>
Change-Id: Iba3eda63e84c4357a121a1782b97a12232e1b8ce
diff --git a/zuul/ansible/library/zuul_console.py b/zuul/ansible/library/zuul_console.py
index 7f8a1b6..42f41f0 100644
--- a/zuul/ansible/library/zuul_console.py
+++ b/zuul/ansible/library/zuul_console.py
@@ -15,10 +15,12 @@
 # You should have received a copy of the GNU General Public License
 # along with this software.  If not, see <http://www.gnu.org/licenses/>.
 
+import glob
 import os
 import sys
 import select
 import socket
+import subprocess
 import threading
 import time
 
@@ -196,6 +198,53 @@
                 pass
 
 
+def get_inode(port_number=19885):
+    for netfile in ('/proc/net/tcp6', '/proc/net/tcp'):
+        if not os.path.exists(netfile):
+            continue
+        with open(netfile) as f:
+            # discard header line
+            f.readline()
+            for line in f:
+                # sl local_address rem_address st tx_queue:rx_queue tr:tm->when
+                # retrnsmt   uid  timeout inode
+                fields = line.split()
+                # Format is localaddr:localport in hex
+                port = int(fields[1].split(':')[1], base=16)
+                if port == port_number:
+                    return fields[9]
+
+
+def get_pid_from_inode(inode):
+    my_euid = os.geteuid()
+    exceptions = []
+    for d in os.listdir('/proc'):
+        try:
+            try:
+                int(d)
+            except Exception as e:
+                continue
+            d_abs_path = os.path.join('/proc', d)
+            if os.stat(d_abs_path).st_uid != my_euid:
+                continue
+            fd_dir = os.path.join(d_abs_path, 'fd')
+            if os.path.exists(fd_dir):
+                if os.stat(fd_dir).st_uid != my_euid:
+                    continue
+                for fd in os.listdir(fd_dir):
+                    try:
+                        fd_path = os.path.join(fd_dir, fd)
+                        if os.path.islink(fd_path):
+                            target = os.readlink(fd_path)
+                            if '[' + inode + ']' in target:
+                                return d, exceptions
+                    except Exception as e:
+                        exceptions.append(e)
+        except Exception as e:
+            exceptions.append(e)
+    return None, exceptions
+
+
 def test():
     s = Server(LOG_STREAM_FILE, LOG_STREAM_PORT)
     s.run()
@@ -206,19 +255,54 @@
         argument_spec=dict(
             path=dict(default=LOG_STREAM_FILE),
             port=dict(default=LOG_STREAM_PORT, type='int'),
+            state=dict(default='present', choices=['absent', 'present']),
         )
     )
 
     p = module.params
     path = p['path']
     port = p['port']
+    state = p['state']
 
-    if daemonize():
+    if state == 'present':
+        if daemonize():
+            module.exit_json()
+
+        s = Server(path, port)
+        s.run()
+    else:
+        pid = None
+        exceptions = []
+        inode = get_inode()
+        if not inode:
+            module.fail_json(
+                "Could not find inode for port",
+                exceptions=[])
+
+        pid, exceptions = get_pid_from_inode(inode)
+        if not pid:
+            except_strings = [str(e) for e in exceptions]
+            module.fail_json(
+                msg="Could not find zuul_console process for inode",
+                exceptions=except_strings)
+
+        try:
+            subprocess.check_output(['kill', pid])
+        except subprocess.CalledProcessError as e:
+            module.fail_json(
+                msg="Could not kill zuul_console pid",
+                exceptions=[str(e)])
+
+        for fn in glob.glob(LOG_STREAM_FILE.format(log_uuid='*')):
+            try:
+                os.unlink(fn)
+            except Exception as e:
+                module.fail_json(
+                    msg="Could not remove logfile {fn}".format(fn=fn),
+                    exceptions=[str(e)])
+
         module.exit_json()
 
-    s = Server(path, port)
-    s.run()
-
 from ansible.module_utils.basic import *  # noqa
 from ansible.module_utils.basic import AnsibleModule