|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +""" |
| 5 | +What: |
| 6 | + There have been cases, where disk turns Read-only due to kernel bug. |
| 7 | + In Read-only state, system blocks new remote user login via TACACS. |
| 8 | + This utility is to check & make transient recovery as needed. |
| 9 | +
|
| 10 | +How: |
| 11 | + check for Read-Write permission. If Read-only, create writable overlay using tmpfs. |
| 12 | +
|
| 13 | + By default "/etc" & "/home" are checked and if in Read-only state, make them Read-Write |
| 14 | + using overlay on top of tmpfs. |
| 15 | +
|
| 16 | + Making /etc & /home as writable lets successful new remote user login. |
| 17 | +
|
| 18 | + If in Read-only state or in Read-Write state with the help of tmpfs overlay, |
| 19 | + syslog ERR messages are written, to help raise alerts. |
| 20 | +
|
| 21 | + Monit may be used to invoke it periodically, to help scan & fix and |
| 22 | + report via syslog. |
| 23 | +
|
| 24 | +Tidbit: |
| 25 | + If you would like to test this script, you could simulate a RO disk |
| 26 | + with the following command. Reboot will revert the effect. |
| 27 | + sudo bash -c "echo u > /proc/sysrq-trigger" |
| 28 | +
|
| 29 | +""" |
| 30 | + |
| 31 | +import argparse |
| 32 | +import os |
| 33 | +import sys |
| 34 | +import syslog |
| 35 | +import subprocess |
| 36 | + |
| 37 | +UPPER_DIR = "/run/mount/upper" |
| 38 | +WORK_DIR = "/run/mount/work" |
| 39 | +MOUNTS_FILE = "/proc/mounts" |
| 40 | + |
| 41 | +chk_log_level = syslog.LOG_ERR |
| 42 | + |
| 43 | +def _log_msg(lvl, pfx, msg): |
| 44 | + if lvl <= chk_log_level: |
| 45 | + print("{}: {}".format(pfx, msg)) |
| 46 | + syslog.syslog(lvl, msg) |
| 47 | + |
| 48 | +def log_err(m): |
| 49 | + _log_msg(syslog.LOG_ERR, "Err", m) |
| 50 | + |
| 51 | + |
| 52 | +def log_info(m): |
| 53 | + _log_msg(syslog.LOG_INFO, "Info", m) |
| 54 | + |
| 55 | + |
| 56 | +def log_debug(m): |
| 57 | + _log_msg(syslog.LOG_DEBUG, "Debug", m) |
| 58 | + |
| 59 | + |
| 60 | +def test_writable(dirs): |
| 61 | + for d in dirs: |
| 62 | + rw = os.access(d, os.W_OK) |
| 63 | + if not rw: |
| 64 | + log_err("{} is not read-write".format(d)) |
| 65 | + return False |
| 66 | + else: |
| 67 | + log_debug("{} is Read-Write".format(d)) |
| 68 | + return True |
| 69 | + |
| 70 | + |
| 71 | +def run_cmd(cmd): |
| 72 | + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) |
| 73 | + (output, err) = p.communicate() |
| 74 | + ## Wait for end of command. Get return returncode ## |
| 75 | + ret = p.wait() |
| 76 | + if ret: |
| 77 | + log_err("failed: ret={} cmd={}".format(ret, cmd)) |
| 78 | + else: |
| 79 | + log_info("ret={} cmd: {}".format(ret, cmd)) |
| 80 | + |
| 81 | + if output: |
| 82 | + log_info("stdout: {}".format(output.decode("utf-8"))) |
| 83 | + if err: |
| 84 | + log_info("stderr: {}".format(err.decode("utf-8"))) |
| 85 | + |
| 86 | + return ret |
| 87 | + |
| 88 | + |
| 89 | +def get_dname(path_name): |
| 90 | + return os.path.basename(os.path.normpath(path_name)) |
| 91 | + |
| 92 | + |
| 93 | +def do_mnt(dirs): |
| 94 | + if os.path.exists(UPPER_DIR): |
| 95 | + log_err("Already mounted") |
| 96 | + return 1 |
| 97 | + |
| 98 | + for i in (UPPER_DIR, WORK_DIR): |
| 99 | + try: |
| 100 | + os.mkdir(i) |
| 101 | + except OSError as error: |
| 102 | + log_err("Failed to create {}".format(i)) |
| 103 | + return 1 |
| 104 | + |
| 105 | + for d in dirs: |
| 106 | + d_name = get_dname(d) |
| 107 | + d_upper = os.path.join(UPPER_DIR, d_name) |
| 108 | + d_work = os.path.join(WORK_DIR, d_name) |
| 109 | + os.mkdir(d_upper) |
| 110 | + os.mkdir(d_work) |
| 111 | + |
| 112 | + ret = run_cmd("mount -t overlay overlay_{} -o lowerdir={}," |
| 113 | + "upperdir={},workdir={} {}".format( |
| 114 | + d_name, d, d_upper, d_work, d)) |
| 115 | + if ret: |
| 116 | + break |
| 117 | + |
| 118 | + if ret: |
| 119 | + for i in (UPPER_DIR, WORK_DIR): |
| 120 | + if os.path.exists(i): |
| 121 | + ret = run_cmd("rm -rf {}".format(i)) |
| 122 | + if ret: |
| 123 | + log_err("Failed to remove {}".format(i)) |
| 124 | + |
| 125 | + log_err("Failed to mount {} as Read-Write".format(dirs)) |
| 126 | + else: |
| 127 | + log_info("{} are mounted as Read-Write".format(dirs)) |
| 128 | + return ret |
| 129 | + |
| 130 | + |
| 131 | +def is_mounted(dirs): |
| 132 | + if not os.path.exists(UPPER_DIR): |
| 133 | + return False |
| 134 | + |
| 135 | + onames = set() |
| 136 | + for d in dirs: |
| 137 | + onames.add("overlay_{}".format(get_dname(d))) |
| 138 | + |
| 139 | + with open(MOUNTS_FILE, "r") as s: |
| 140 | + for ln in s.readlines(): |
| 141 | + n = ln.strip().split()[0] |
| 142 | + if n in onames: |
| 143 | + log_debug("Mount exists for {}".format(n)) |
| 144 | + return True |
| 145 | + return False |
| 146 | + |
| 147 | + |
| 148 | +def do_check(skip_mount, dirs): |
| 149 | + ret = 0 |
| 150 | + if not test_writable(dirs): |
| 151 | + if not skip_mount: |
| 152 | + ret = do_mnt(dirs) |
| 153 | + |
| 154 | + # Check if mounted |
| 155 | + if (not ret) and is_mounted(dirs): |
| 156 | + log_err("READ-ONLY: Mounted {} to make Read-Write".format(dirs)) |
| 157 | + |
| 158 | + return ret |
| 159 | + |
| 160 | + |
| 161 | +def main(): |
| 162 | + global chk_log_level |
| 163 | + |
| 164 | + parser=argparse.ArgumentParser( |
| 165 | + description="check disk for Read-Write and mount etc & home as Read-Write") |
| 166 | + parser.add_argument('-s', "--skip-mount", action='store_true', default=False, |
| 167 | + help="Skip mounting /etc & /home as Read-Write") |
| 168 | + parser.add_argument('-d', "--dirs", default="/etc,/home", |
| 169 | + help="dirs to mount") |
| 170 | + parser.add_argument('-l', "--loglvl", default=syslog.LOG_ERR, type=int, |
| 171 | + help="log level") |
| 172 | + args = parser.parse_args() |
| 173 | + |
| 174 | + chk_log_level = args.loglvl |
| 175 | + ret = do_check(args.skip_mount, args.dirs.split(",")) |
| 176 | + return ret |
| 177 | + |
| 178 | + |
| 179 | +if __name__ == "__main__": |
| 180 | + sys.exit(main()) |
0 commit comments