|
21 | 21 | SWAPFILE = "/swapfile" |
22 | 22 | SWAP_SIZE_GB = 4 |
23 | 23 | SWAP_SWAPPINESS = 10 |
| 24 | +LOWRAM_JOURNALD_DROPIN = "/etc/systemd/journald.conf.d/99-lowram.conf" |
| 25 | +LOWRAM_SYSCTL = "/etc/sysctl.d/99-lowram.conf" |
| 26 | +LOWRAM_ZRAM_DEFAULTS = {"algo": "zstd", "percent": 25} |
| 27 | +LOWRAM_DISABLE_SERVICES = [ |
| 28 | + "avahi-daemon", |
| 29 | + "ModemManager", |
| 30 | + "bluetooth", |
| 31 | + "cups", |
| 32 | + "packagekit", |
| 33 | + "rsyslog", |
| 34 | +] |
| 35 | +LOWRAM_MASK_TARGETS = [ |
| 36 | + "sleep.target", |
| 37 | + "suspend.target", |
| 38 | + "hibernate.target", |
| 39 | + "hybrid-sleep.target", |
| 40 | +] |
24 | 41 |
|
25 | 42 |
|
26 | 43 | def encryption_config_has_keys(path: str) -> bool: |
@@ -117,6 +134,168 @@ def run( |
117 | 134 | return result |
118 | 135 |
|
119 | 136 |
|
| 137 | +def _write_file(path: str, content: str, mode: int = 0o644) -> None: |
| 138 | + os.makedirs(os.path.dirname(path), exist_ok=True) |
| 139 | + with open(path, "w", encoding="utf-8") as fh: |
| 140 | + fh.write(content) |
| 141 | + os.chmod(path, mode) |
| 142 | + |
| 143 | + |
| 144 | +def read_os_release(path: str = "/etc/os-release") -> dict[str, str]: |
| 145 | + data: dict[str, str] = {} |
| 146 | + try: |
| 147 | + with open(path, "r", encoding="utf-8") as fh: |
| 148 | + for line in fh: |
| 149 | + line = line.strip() |
| 150 | + if not line or line.startswith("#") or "=" not in line: |
| 151 | + continue |
| 152 | + key, value = line.split("=", 1) |
| 153 | + data[key] = value.strip().strip('"') |
| 154 | + except OSError: |
| 155 | + return data |
| 156 | + return data |
| 157 | + |
| 158 | + |
| 159 | +def is_debian_like(os_info: dict[str, str]) -> bool: |
| 160 | + if os_info.get("ID") == "debian": |
| 161 | + return True |
| 162 | + like = os_info.get("ID_LIKE", "") |
| 163 | + return "debian" in like.split() or "debian" in like |
| 164 | + |
| 165 | + |
| 166 | +def service_exists(service: str) -> bool: |
| 167 | + unit = f"{service}.service" |
| 168 | + for base in ("/etc/systemd/system", "/lib/systemd/system", "/usr/lib/systemd/system"): |
| 169 | + if os.path.exists(os.path.join(base, unit)): |
| 170 | + return True |
| 171 | + return False |
| 172 | + |
| 173 | + |
| 174 | +def command_exists(command: str) -> bool: |
| 175 | + return shutil.which(command) is not None |
| 176 | + |
| 177 | + |
| 178 | +def disable_services(services: Sequence[str]) -> None: |
| 179 | + for service in services: |
| 180 | + run(f"systemctl disable --now {service}", check=False, quiet=True, log_ok=False) |
| 181 | + |
| 182 | + |
| 183 | +def mask_targets(targets: Sequence[str]) -> None: |
| 184 | + if targets: |
| 185 | + joined = " ".join(shlex.quote(target) for target in targets) |
| 186 | + run(f"systemctl mask {joined}", check=False, quiet=True, log_ok=False) |
| 187 | + |
| 188 | + |
| 189 | +def disable_swapfile(swapfile: str) -> None: |
| 190 | + run(f"swapoff {swapfile}", check=False, quiet=True, log_ok=False) |
| 191 | + try: |
| 192 | + with open("/etc/fstab", "r", encoding="utf-8") as fh: |
| 193 | + lines = fh.readlines() |
| 194 | + except OSError: |
| 195 | + lines = [] |
| 196 | + kept = [line for line in lines if swapfile not in line] |
| 197 | + if kept != lines: |
| 198 | + with open("/etc/fstab", "w", encoding="utf-8") as fh: |
| 199 | + fh.writelines(kept) |
| 200 | + if os.path.exists(swapfile): |
| 201 | + try: |
| 202 | + os.remove(swapfile) |
| 203 | + except OSError: |
| 204 | + pass |
| 205 | + if os.path.exists("/etc/sysctl.d/99-swap.conf"): |
| 206 | + try: |
| 207 | + os.remove("/etc/sysctl.d/99-swap.conf") |
| 208 | + except OSError: |
| 209 | + pass |
| 210 | + |
| 211 | + |
| 212 | +def ensure_lowram_tuning(os_info: dict[str, str], swap_swappiness: int) -> bool: |
| 213 | + setting = os.environ.get("LOWRAM_TUNE", "auto").lower() |
| 214 | + if setting in ("0", "false", "no", "off"): |
| 215 | + log("[lowram] disabled by env", level="INFO") |
| 216 | + return False |
| 217 | + if setting in ("1", "true", "yes", "on"): |
| 218 | + enabled = True |
| 219 | + else: |
| 220 | + enabled = os_info.get("ID") == "debian" |
| 221 | + if not enabled: |
| 222 | + return False |
| 223 | + |
| 224 | + if not is_debian_like(os_info): |
| 225 | + log("[lowram] requested but OS is not Debian-like; skipping", level="WARN") |
| 226 | + return False |
| 227 | + |
| 228 | + zram_setting = os.environ.get("LOWRAM_ZRAM_ENABLED", "1").lower() |
| 229 | + zram_enabled = zram_setting not in ("0", "false", "no", "off") |
| 230 | + zram_algo = os.environ.get("LOWRAM_ZRAM_ALGO", LOWRAM_ZRAM_DEFAULTS["algo"]) |
| 231 | + zram_percent_raw = os.environ.get("LOWRAM_ZRAM_PERCENT", str(LOWRAM_ZRAM_DEFAULTS["percent"])) |
| 232 | + try: |
| 233 | + zram_percent = int(zram_percent_raw) |
| 234 | + except ValueError: |
| 235 | + log(f"[lowram] invalid LOWRAM_ZRAM_PERCENT={zram_percent_raw!r}; using default", level="WARN") |
| 236 | + zram_percent = LOWRAM_ZRAM_DEFAULTS["percent"] |
| 237 | + |
| 238 | + swappiness_value = 180 if zram_enabled else swap_swappiness |
| 239 | + |
| 240 | + _write_file( |
| 241 | + LOWRAM_JOURNALD_DROPIN, |
| 242 | + "[Journal]\nStorage=volatile\nRuntimeMaxUse=16M\nSystemMaxUse=0\n", |
| 243 | + ) |
| 244 | + _write_file( |
| 245 | + LOWRAM_SYSCTL, |
| 246 | + textwrap.dedent( |
| 247 | + f"""\ |
| 248 | + vm.swappiness={swappiness_value} |
| 249 | + vm.vfs_cache_pressure=200 |
| 250 | + vm.dirty_ratio=5 |
| 251 | + vm.dirty_background_ratio=3 |
| 252 | + vm.min_free_kbytes=65536 |
| 253 | + """ |
| 254 | + ), |
| 255 | + ) |
| 256 | + run("sysctl --system", check=False, quiet=True, log_ok=False) |
| 257 | + |
| 258 | + disable_services(LOWRAM_DISABLE_SERVICES) |
| 259 | + mask_targets(LOWRAM_MASK_TARGETS) |
| 260 | + |
| 261 | + if zram_enabled: |
| 262 | + run("apt-get install -y zram-tools", check=False, quiet=True, log_ok=False) |
| 263 | + _write_file( |
| 264 | + "/etc/default/zramswap", |
| 265 | + f"ALGO={zram_algo}\nPERCENT={zram_percent}\n", |
| 266 | + ) |
| 267 | + if service_exists("zramswap"): |
| 268 | + run("systemctl restart zramswap", check=False, quiet=True, log_ok=False) |
| 269 | + |
| 270 | + if service_exists("systemd-journald"): |
| 271 | + run("systemctl restart systemd-journald", check=False, quiet=True, log_ok=False) |
| 272 | + |
| 273 | + if service_exists("systemd-oomd"): |
| 274 | + run("systemctl enable --now systemd-oomd", check=False, quiet=True, log_ok=False) |
| 275 | + |
| 276 | + _write_file( |
| 277 | + "/etc/docker/daemon.json", |
| 278 | + textwrap.dedent( |
| 279 | + """\ |
| 280 | + { |
| 281 | + "log-driver": "local", |
| 282 | + "log-opts": { |
| 283 | + "max-size": "10m", |
| 284 | + "max-file": "3" |
| 285 | + }, |
| 286 | + "live-restore": true, |
| 287 | + "userland-proxy": false |
| 288 | + } |
| 289 | + """ |
| 290 | + ), |
| 291 | + ) |
| 292 | + if service_exists("docker") or command_exists("dockerd") or command_exists("docker"): |
| 293 | + run("systemctl restart docker", check=False, quiet=True, log_ok=False) |
| 294 | + |
| 295 | + run("apt-get purge -y snapd", check=False, quiet=True, log_ok=False) |
| 296 | + return zram_enabled |
| 297 | + |
| 298 | + |
120 | 299 | def wait_for_k8s(): |
121 | 300 | for _ in range(60): |
122 | 301 | if subprocess.run("kubectl get nodes", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: |
@@ -465,7 +644,12 @@ def main(): |
465 | 644 | os.environ["KUBECONFIG"] = "/etc/rancher/k3s/k3s.yaml" |
466 | 645 | allow_fresh_env = os.environ.get("ALLOW_FRESH_BOOTSTRAP", "").lower() in ("1", "true", "yes") |
467 | 646 | ensure_data_mount(env_name, luks_key_url, luks_key_access, luks_key_secret) |
468 | | - if swap_size_gb <= 0: |
| 647 | + os_info = read_os_release() |
| 648 | + zram_enabled = ensure_lowram_tuning(os_info, swap_swappiness) |
| 649 | + if zram_enabled: |
| 650 | + log("[swap] zram enabled; skipping swapfile setup", level="INFO") |
| 651 | + disable_swapfile(swap_file) |
| 652 | + elif swap_size_gb <= 0: |
469 | 653 | log("SWAP_SIZE_GB <= 0; skipping swap setup", level="WARN") |
470 | 654 | else: |
471 | 655 | ensure_swap(swap_file, swap_size_gb, swap_swappiness) |
|
0 commit comments