""" 按组挂载 Git 资源(只读) + 自动建用户加组 资源路径 /home/zhangyi/jupyter-collection 控制文件 /home/zhangyi/jupyter-collection/.hub/resource_map.ini """ import os, grp, pwd, shutil, subprocess, configparser from pathlib import Path from jupyterhub.spawner import Spawner # ---------- 路径常量 ---------- SHARED_ROOT = Path("/home/zhangyi/jupyter-collection") # Git 资源仓库 USER_HOME = Path("/home/jupyter-{username}") # 用户家目录模板 CTRL_FILE = SHARED_ROOT / ".hub" / "resource_map.ini" # 组-资源-用户映射 # ---------- 工具:读控制表 ---------- def load_ctrl(): """返回 (组->资源, 组->用户) 两个 dict""" cfg = configparser.ConfigParser() cfg.read(CTRL_FILE) res_map = {g: [r.strip() for r in v.split(",")] for g, v in cfg.items("map")} usr_map = {g: [u.strip() for u in v.split(",")] for g, v in cfg.items("users")} return res_map, usr_map # ========== 最早钩子:预创建家目录 ========== def ensure_homedir(spawner): """systemd 启动前保证目录存在且属主正确""" username = spawner.user.name user_dir = Path(str(USER_HOME).format(username=username)) if not user_dir.exists(): user_dir.mkdir(mode=0o700, parents=True) shutil.chown(user_dir, user=username, group=username) c.Spawner.pre_spawn_hook = ensure_homedir # ========== 认证后钩子:自动建用户+加组 ========== def ensure_user_and_groups(authenticator, handler, authentication): username = authentication['name'] res_map, usr_map = load_ctrl() wanted_groups = [g for g, users in usr_map.items() if username in users] if not wanted_groups: return authentication for g in wanted_groups: # 确保组存在 subprocess.run(["groupadd", "-f", g], check=False) if subprocess.run(["id", "-u", username], capture_output=True).returncode != 0: subprocess.run([ "useradd", "-m", "-s", "/bin/bash", "-d", str(USER_HOME).format(username=username), username ], check=False) for g in wanted_groups: # 加组 subprocess.run(["usermod", "-aG", g, username], check=False) return authentication c.Authenticator.post_auth_hook = ensure_user_and_groups # ========== spawn 钩子:清 shared + 只读挂载 ========== def prepare_user(spawner): username = spawner.user.name user_dir = Path(str(USER_HOME).format(username=username)) # ① 确保家目录 700 + 属主正确(首次/后续都适用) user_dir.mkdir(mode=0o700, parents=True, exist_ok=True) shutil.chown(user_dir, user=username, group=username) # ② 只清 shared,保留用户其它文件 shared = user_dir / "shared" if shared.exists(): shutil.rmtree(shared) shared.mkdir(parents=True, exist_ok=True) shutil.chown(shared, user=username, group=username) # 读取控制表 res_map, _ = load_ctrl() # 获取用户系统组 try: pwnam = pwd.getpwnam(username) gid_list = os.getgrouplist(username, pwnam.pw_gid) except (KeyError, OSError): return user_groups = {grp.getgrgid(g).gr_name for g in gid_list} # 组装 bind_mounts(SystemdSpawner 语法) bind_list = [] for grp_name in user_groups: for res in res_map.get(grp_name, []): res_path = SHARED_ROOT / res if res_path.is_dir(): bind_list.append({ 'source': str(res_path), 'target': str(shared / res), 'read-only': True, }) spawner.bind_mounts = bind_list c.Spawner.pre_spawn_hook = prepare_user