runv-server

server tooling for runv.club
Log | Files | Refs | README

skel.py (11511B)


      1 #!/usr/bin/env python3
      2 """
      3 Prepara /etc/skel para novos usuários do runv.club (Debian).
      4 Executar como root. Não cria usuários, não altera Apache nem SSH.
      5 
      6 Versão 0.02 — runv.club
      7 """
      8 
      9 from __future__ import annotations
     10 
     11 import argparse
     12 import os
     13 import sys
     14 from pathlib import Path
     15 from typing import Final
     16 
     17 _SCRIPT_DIR = Path(__file__).resolve().parent
     18 if str(_SCRIPT_DIR) not in sys.path:
     19     sys.path.insert(0, str(_SCRIPT_DIR))
     20 
     21 from admin_guard import ensure_admin_cli
     22 
     23 # ---------------------------------------------------------------------------
     24 # Constantes
     25 # ---------------------------------------------------------------------------
     26 
     27 SKEL_ROOT: Final[Path] = Path("/etc/skel")
     28 PUBLIC_HTML_DIR: Final[Path] = SKEL_ROOT / "public_html"
     29 INDEX_HTML: Final[Path] = PUBLIC_HTML_DIR / "index.html"
     30 README_MD: Final[Path] = SKEL_ROOT / "README.md"
     31 
     32 VERSION: Final[str] = "0.02"
     33 
     34 EXIT_OK: Final[int] = 0
     35 EXIT_ERROR: Final[int] = 1
     36 EXIT_PRIVILEGE: Final[int] = 2
     37 
     38 
     39 # ---------------------------------------------------------------------------
     40 # Validação de privilégios
     41 # ---------------------------------------------------------------------------
     42 
     43 
     44 def validate_privileges() -> None:
     45     """Exige UID 0 (root) para alterar /etc/skel."""
     46     if os.geteuid() != 0:
     47         print(
     48             "Erro: este script precisa ser executado como root (sudo).",
     49             file=sys.stderr,
     50         )
     51         raise SystemExit(EXIT_PRIVILEGE)
     52 
     53 
     54 # ---------------------------------------------------------------------------
     55 # Geração de conteúdo
     56 # ---------------------------------------------------------------------------
     57 
     58 
     59 def render_index_html() -> str:
     60     """HTML estático com CSS embutido; visual simples e textual, sem dependências externas."""
     61     return """<!DOCTYPE html>
     62 <html lang="pt-BR">
     63 <head>
     64   <meta charset="utf-8">
     65   <meta name="viewport" content="width=device-width, initial-scale=1">
     66   <title>A sua página no runv.club</title>
     67   <style>
     68     :root {
     69       --bg: #f4f0e8;
     70       --fg: #1a1a12;
     71       --muted: #5c5a52;
     72       --accent: #2d6a4f;
     73       --rule: #c4c0b8;
     74     }
     75     * { box-sizing: border-box; }
     76     body {
     77       margin: 0;
     78       padding: 1.5rem 1rem 3rem;
     79       font-family: "Georgia", "Times New Roman", serif;
     80       font-size: 1rem;
     81       line-height: 1.55;
     82       color: var(--fg);
     83       background: var(--bg);
     84       max-width: 38rem;
     85       margin-left: auto;
     86       margin-right: auto;
     87     }
     88     h1 {
     89       font-size: 1.35rem;
     90       font-weight: normal;
     91       letter-spacing: 0.02em;
     92       border-bottom: 1px solid var(--rule);
     93       padding-bottom: 0.5rem;
     94       margin-top: 0;
     95     }
     96     .tagline {
     97       font-style: italic;
     98       color: var(--muted);
     99       margin: 0.25rem 0 1.25rem;
    100       font-size: 0.95rem;
    101     }
    102     pre, code {
    103       font-family: ui-monospace, "Cascadia Mono", "Consolas", monospace;
    104       font-size: 0.88rem;
    105     }
    106     pre {
    107       background: #e8e4dc;
    108       border: 1px solid var(--rule);
    109       padding: 0.75rem 1rem;
    110       overflow-x: auto;
    111       margin: 0.75rem 0;
    112     }
    113     section {
    114       margin: 1.5rem 0;
    115     }
    116     h2 {
    117       font-size: 1.05rem;
    118       font-weight: normal;
    119       margin: 0 0 0.5rem;
    120       color: var(--accent);
    121     }
    122     .url-box {
    123       border-left: 3px solid var(--accent);
    124       padding-left: 0.75rem;
    125       margin: 0.75rem 0;
    126     }
    127     footer {
    128       margin-top: 2rem;
    129       padding-top: 0.75rem;
    130       border-top: 1px solid var(--rule);
    131       font-size: 0.85rem;
    132       color: var(--muted);
    133     }
    134   </style>
    135 </head>
    136 <body>
    137   <h1>Bem-vindo ao runv.club</h1>
    138   <p class="tagline">Um cantinho na rede — pubnix runv.club.</p>
    139 
    140   <p>
    141     Esta página foi gerada automaticamente quando sua conta foi criada.
    142     Você pode editá-la quando quiser: o arquivo fica em
    143     <code>~/public_html/index.html</code>.
    144   </p>
    145 
    146   <section>
    147     <h2>Próximos passos</h2>
    148     <ol>
    149       <li>Entrar no servidor por SSH.</li>
    150       <li>Ir para a pasta do site pessoal.</li>
    151       <li>Editar este HTML com um editor de texto.</li>
    152       <li>Salvar e recarregar a página no navegador.</li>
    153     </ol>
    154   </section>
    155 
    156   <section>
    157     <h2>Comandos úteis</h2>
    158     <pre>cd ~/public_html
    159 nano index.html
    160 ls -la</pre>
    161   </section>
    162 
    163   <section>
    164     <h2>Sua URL</h2>
    165     <p>Quando estiver no ar, seu site costuma aparecer em:</p>
    166     <div class="url-box">
    167       <code>http://runv.club/~SEU_USUARIO/</code>
    168     </div>
    169     <p>Substitua <code>SEU_USUARIO</code> pelo seu nome de usuário Unix.</p>
    170   </section>
    171 
    172   <footer>
    173     runv.club — servidor multiusuário. Edite esta página à vontade.
    174   </footer>
    175 </body>
    176 </html>
    177 """
    178 
    179 
    180 def render_readme_md() -> str:
    181     """README em Markdown para a home inicial (copiado de /etc/skel)."""
    182     return """# Bem-vindo ao runv.club
    183 
    184 O **runv.club** é um servidor multiutilizador: cada pessoa tem uma conta Unix e um
    185 site pessoal servido pelo Apache.
    186 
    187 ## Onde fica o seu site
    188 
    189 - **Pasta:** `~/public_html/`
    190 - **Arquivo principal:** `~/public_html/index.html` — edite este primeiro.
    191 
    192 ## URL pública
    193 
    194 Depois de publicar, seu site costuma ficar em:
    195 
    196 ```text
    197 http://runv.club/~SEU_USUARIO/
    198 ```
    199 
    200 Troque `SEU_USUARIO` pelo seu nome de usuário Unix (o mesmo do login).
    201 
    202 ## Permissões (referência)
    203 
    204 Após a criação da conta, costuma ser assim para o site aparecer:
    205 
    206 | Caminho | Permissão típica |
    207 |---------|------------------|
    208 | `~` (home) | `755` |
    209 | `~/public_html` | `755` |
    210 | `~/public_html/index.html` | `644` |
    211 
    212 Se algo não carregar no navegador, peça ajuda a um admin e mencione estas pastas.
    213 
    214 ## Comandos básicos
    215 
    216 ```bash
    217 cd ~/public_html
    218 nano index.html
    219 ls -la
    220 ```
    221 
    222 ## Servidor multiusuário
    223 
    224 - Muitas pessoas usam a mesma máquina. **Não guarde segredos** em arquivos dentro de
    225   `public_html` ou em qualquer lugar que o site possa expor.
    226 - O que está em `public_html` é pensado para ser **público na web**.
    227 
    228 ## Dúvidas
    229 
    230 Leia também a documentação do projeto ou fale com a equipe no canal indicado pelo runv.club.
    231 
    232 — Equipe runv.club
    233 """
    234 
    235 
    236 # ---------------------------------------------------------------------------
    237 # Diretórios e ficheiros
    238 # ---------------------------------------------------------------------------
    239 
    240 
    241 def ensure_directories(
    242     dry_run: bool,
    243     verbose: bool,
    244 ) -> tuple[list[Path], list[Path]]:
    245     """
    246     Garante que os diretórios necessários existem.
    247     Retorna (criados, já existentes).
    248     """
    249     created: list[Path] = []
    250     existed: list[Path] = []
    251     for d in (SKEL_ROOT, PUBLIC_HTML_DIR):
    252         if d.is_dir():
    253             existed.append(d)
    254             if verbose:
    255                 print(f"  [dir] já existe: {d}")
    256             continue
    257         if dry_run:
    258             created.append(d)
    259             print(f"  [dry-run] criaria diretório: {d}")
    260             continue
    261         d.mkdir(parents=True, exist_ok=True)
    262         created.append(d)
    263         print(f"  [dir] criado: {d}")
    264     return created, existed
    265 
    266 
    267 def apply_permissions(paths: list[Path], verbose: bool) -> None:
    268     """Aplica modos 755 para diretórios e 644 para ficheiros."""
    269     for p in paths:
    270         if not p.exists():
    271             continue
    272         if p.is_dir():
    273             mode = 0o755
    274         else:
    275             mode = 0o644
    276         if verbose:
    277             print(f"  [chmod] {oct(mode)} {p}")
    278         try:
    279             p.chmod(mode)
    280         except OSError as e:
    281             print(f"Erro ao definir permissões em {p}: {e}", file=sys.stderr)
    282             raise SystemExit(EXIT_ERROR) from e
    283 
    284 
    285 def write_file_safe(
    286     path: Path,
    287     content: str,
    288     *,
    289     force: bool,
    290     dry_run: bool,
    291     verbose: bool,
    292 ) -> str:
    293     """
    294     Escreve conteúdo se o ficheiro não existir ou se force=True.
    295     Retorna: 'created' | 'updated' | 'preserved'
    296     """
    297     existed_before = path.is_file()
    298 
    299     if dry_run:
    300         if existed_before and not force:
    301             print(f"  [dry-run] preservaria (sem alterar; use --force para regenerar): {path}")
    302             return "preserved"
    303         verb = "atualizaria" if existed_before else "criaria"
    304         print(f"  [dry-run] {verb} arquivo: {path}")
    305         return "updated" if existed_before else "created"
    306 
    307     if existed_before and not force:
    308         hint = " (use --force para sobrescrever)" if verbose else ""
    309         print(f"  [file] preservado{hint}: {path}")
    310         return "preserved"
    311 
    312     path.parent.mkdir(parents=True, exist_ok=True)
    313     path.write_text(content, encoding="utf-8")
    314     label = "atualizado" if existed_before else "criado"
    315     print(f"  [file] {label}: {path}")
    316     return "updated" if existed_before else "created"
    317 
    318 
    319 def run_dry_run(verbose: bool) -> int:
    320     """Mostra o plano sem escrever em disco (não exige root)."""
    321     print("Modo dry-run — nenhuma alteração em disco.\n")
    322     ensure_directories(dry_run=True, verbose=verbose)
    323     write_file_safe(
    324         INDEX_HTML, render_index_html(), force=False, dry_run=True, verbose=verbose
    325     )
    326     write_file_safe(
    327         README_MD, render_readme_md(), force=False, dry_run=True, verbose=verbose
    328     )
    329     print("\nResumo: nada foi gravado. Execute sem --dry-run como root para aplicar.")
    330     return EXIT_OK
    331 
    332 
    333 def main() -> int:
    334     parser = argparse.ArgumentParser(
    335         description="Prepara /etc/skel para novos usuários do runv.club (Debian).",
    336     )
    337     parser.add_argument(
    338         "--dry-run",
    339         action="store_true",
    340         help="mostra o que seria feito sem alterar arquivos",
    341     )
    342     parser.add_argument(
    343         "--verbose",
    344         action="store_true",
    345         help="mais detalhes na saída",
    346     )
    347     parser.add_argument(
    348         "--force",
    349         action="store_true",
    350         help="sobrescreve index.html e README.md se já existirem",
    351     )
    352     parser.add_argument(
    353         "--version",
    354         action="version",
    355         version=f"%(prog)s {VERSION} — runv.club",
    356     )
    357     args = parser.parse_args()
    358     ensure_admin_cli(
    359         script_name=Path(__file__).name,
    360         dry_run=bool(args.dry_run),
    361     )
    362 
    363     if args.dry_run:
    364         return run_dry_run(args.verbose)
    365 
    366     validate_privileges()
    367 
    368     print("skel.py — preparando /etc/skel para runv.club\n")
    369 
    370     dirs_created, _dirs_existed = ensure_directories(dry_run=False, verbose=args.verbose)
    371 
    372     results: dict[str, str] = {}
    373     for label, path, content in (
    374         ("index.html", INDEX_HTML, render_index_html()),
    375         ("README.md", README_MD, render_readme_md()),
    376     ):
    377         results[label] = write_file_safe(
    378             path,
    379             content,
    380             force=args.force,
    381             dry_run=False,
    382             verbose=args.verbose,
    383         )
    384 
    385     # Permissões
    386     to_chmod = [PUBLIC_HTML_DIR, INDEX_HTML, README_MD]
    387     print("\nAplicando permissões...")
    388     apply_permissions(to_chmod, verbose=args.verbose)
    389 
    390     # Resumo
    391     print("\n--- Resumo ---")
    392     print(f"  Diretórios criados agora: {len(dirs_created)}")
    393     if dirs_created:
    394         for d in dirs_created:
    395             print(f"    - {d}")
    396     print(f"  index.html: {results.get('index.html', '?')}")
    397     print(f"  README.md:  {results.get('README.md', '?')}")
    398     print("  Permissões: public_html → 755; index.html e README.md → 644")
    399 
    400     print("\n--- Próximos passos sugeridos ---")
    401     print("  1. Crie um usuário de teste: sudo adduser --disabled-password testuser")
    402     print("  2. Verifique se a home copiou de /etc/skel:")
    403     print("       ls -la ~/  (como esse usuário)")
    404     print("       ls -la ~/public_html/")
    405     print("  3. Teste no navegador: http://runv.club/~testuser/ (ajuste DNS/host)")
    406 
    407     return EXIT_OK
    408 
    409 
    410 if __name__ == "__main__":
    411     raise SystemExit(main())