diff --git a/backend/app/services/scheduler/schedule_account.py b/backend/app/services/scheduler/schedule_account.py index 445bb09..f32d581 100644 --- a/backend/app/services/scheduler/schedule_account.py +++ b/backend/app/services/scheduler/schedule_account.py @@ -77,106 +77,98 @@ def run_account_check(schedule_log_id: int, db: Session): result.mailbox_done_at = now_tw() fail_reasons.append(f"mailbox: {e}") - # [3] NC user + Mail(manager 租戶無 NC container,跳過) - if tenant.is_manager: - result.nc_result = True + # [3] NC user check + try: + from app.core.config import settings as _cfg + nc = NextcloudClient(tenant.domain, _cfg.NC_ADMIN_USER, _cfg.NC_ADMIN_PASSWORD) + nc_exists = nc.user_exists(account.sso_account) + if nc_exists: + result.nc_result = True + else: + created = nc.create_user(account.sso_account, account.default_password, account.quota_limit) + result.nc_result = created + if result.nc_result and account.quota_limit: + nc.set_user_quota(account.sso_account, account.quota_limit) result.nc_done_at = now_tw() - result.nc_mail_result = True - result.nc_mail_done_at = now_tw() - else: - # [3] NC user check + except Exception as e: + result.nc_result = False + result.nc_done_at = now_tw() + fail_reasons.append(f"nc: {e}") + + # [3.5] NC 台灣國定假日行事曆訂閱 + TW_HOLIDAYS_ICS_URL = "https://www.officeholidays.com/ics-clean/taiwan" + if result.nc_result: try: + cal_ok = nc.subscribe_calendar( + account.sso_account, + "taiwan-public-holidays", + "台灣國定假日", + TW_HOLIDAYS_ICS_URL, + "#e9322d", + ) + if cal_ok: + logger.info(f"NC calendar subscribed [{account.sso_account}]: taiwan-public-holidays") + else: + logger.warning(f"NC calendar subscribe failed [{account.sso_account}]") + except Exception as e: + logger.warning(f"NC calendar subscribe error [{account.sso_account}]: {e}") + + # [4] NC Mail 帳號設定 + try: + if result.nc_result and result.mailbox_result: + import paramiko from app.core.config import settings as _cfg - nc = NextcloudClient(tenant.domain, _cfg.NC_ADMIN_USER, _cfg.NC_ADMIN_PASSWORD) - nc_exists = nc.user_exists(account.sso_account) - if nc_exists: - result.nc_result = True + is_active = tenant.status == "active" + nc_container = f"nc-{tenant.code}" if is_active else f"nc-{tenant.code}-test" + email = account.email or f"{account.sso_account}@{tenant.domain}" + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(_cfg.DOCKER_SSH_HOST, username=_cfg.DOCKER_SSH_USER, timeout=15) + + def _ssh_run(cmd, timeout=60): + _, stdout, _ = ssh.exec_command(cmd) + stdout.channel.settimeout(timeout) + try: + out = stdout.read().decode().strip() + except Exception: + out = "" + return out + + export_out = _ssh_run( + f"docker exec -u www-data {nc_container} " + f"php /var/www/html/occ mail:account:export {account.sso_account} 2>/dev/null" + ) + already_set = len(export_out) > 10 and "imap" in export_out.lower() + + if already_set: + result.nc_mail_result = True else: - created = nc.create_user(account.sso_account, account.default_password, account.quota_limit) - result.nc_result = created - # 確保 quota 設定正確(無論新建或已存在) - if result.nc_result and account.quota_limit: - nc.set_user_quota(account.sso_account, account.quota_limit) - result.nc_done_at = now_tw() - except Exception as e: - result.nc_result = False - result.nc_done_at = now_tw() - fail_reasons.append(f"nc: {e}") - - # [3.5] NC 台灣國定假日行事曆訂閱 - TW_HOLIDAYS_ICS_URL = "https://www.officeholidays.com/ics-clean/taiwan" - if result.nc_result: - try: - cal_ok = nc.subscribe_calendar( - account.sso_account, - "taiwan-public-holidays", - "台灣國定假日", - TW_HOLIDAYS_ICS_URL, - "#e9322d", - ) - if cal_ok: - logger.info(f"NC calendar subscribed [{account.sso_account}]: taiwan-public-holidays") - else: - logger.warning(f"NC calendar subscribe failed [{account.sso_account}]") - except Exception as e: - logger.warning(f"NC calendar subscribe error [{account.sso_account}]: {e}") - - # [4] NC Mail 帳號設定 - try: - if result.nc_result and result.mailbox_result: - import paramiko - from app.core.config import settings as _cfg - is_active = tenant.status == "active" - nc_container = f"nc-{tenant.code}" if is_active else f"nc-{tenant.code}-test" - email = account.email or f"{account.sso_account}@{tenant.domain}" - - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(_cfg.DOCKER_SSH_HOST, username=_cfg.DOCKER_SSH_USER, timeout=15) - - def _ssh_run(cmd, timeout=60): - _, stdout, _ = ssh.exec_command(cmd) - stdout.channel.settimeout(timeout) - try: - out = stdout.read().decode().strip() - except Exception: - out = "" - return out - - export_out = _ssh_run( + display = account.legal_name or account.sso_account + create_cmd = ( f"docker exec -u www-data {nc_container} " - f"php /var/www/html/occ mail:account:export {account.sso_account} 2>/dev/null" + f"php /var/www/html/occ mail:account:create " + f"'{account.sso_account}' '{display}' '{email}' " + f"10.1.0.254 143 none '{email}' '{account.default_password}' " + f"10.1.0.254 587 none '{email}' '{account.default_password}' 2>&1" ) - already_set = len(export_out) > 10 and "imap" in export_out.lower() - - if already_set: - result.nc_mail_result = True - else: - display = account.legal_name or account.sso_account - create_cmd = ( - f"docker exec -u www-data {nc_container} " - f"php /var/www/html/occ mail:account:create " - f"'{account.sso_account}' '{display}' '{email}' " - f"10.1.0.254 143 none '{email}' '{account.default_password}' " - f"10.1.0.254 587 none '{email}' '{account.default_password}' 2>&1" - ) - out_text = _ssh_run(create_cmd) - logger.info(f"NC mail:account:create [{account.sso_account}]: {out_text}") - result.nc_mail_result = ( - "error" not in out_text.lower() and "exception" not in out_text.lower() - ) - - ssh.close() - else: - result.nc_mail_result = False - fail_reasons.append( - f"nc_mail: skipped (nc={result.nc_result}, mailbox={result.mailbox_result})" + out_text = _ssh_run(create_cmd) + logger.info(f"NC mail:account:create [{account.sso_account}]: {out_text}") + result.nc_mail_result = ( + "error" not in out_text.lower() and "exception" not in out_text.lower() ) - result.nc_mail_done_at = now_tw() - except Exception as e: + + ssh.close() + else: result.nc_mail_result = False - result.nc_mail_done_at = now_tw() - fail_reasons.append(f"nc_mail: {e}") + fail_reasons.append( + f"nc_mail: skipped (nc={result.nc_result}, mailbox={result.mailbox_result})" + ) + result.nc_mail_done_at = now_tw() + except Exception as e: + result.nc_mail_result = False + result.nc_mail_done_at = now_tw() + fail_reasons.append(f"nc_mail: {e}") # [5] Quota try: diff --git a/backend/app/services/scheduler/schedule_tenant.py b/backend/app/services/scheduler/schedule_tenant.py index 4a3f4b1..63701b1 100644 --- a/backend/app/services/scheduler/schedule_tenant.py +++ b/backend/app/services/scheduler/schedule_tenant.py @@ -946,93 +946,82 @@ def run_tenant_check(schedule_log_id: int, db: Session): # ── [4] NC container + DB 驗證 + OIDC 設定 ───────────────────────── pg_db = f"nc_{tenant.code}_db" - if tenant.is_manager: - # Manager 租戶無需 NC/OO 容器,直接標記成功 - result.nc_result = True - result.nc_done_at = now_tw() - result.office_result = True - result.office_done_at = now_tw() - else: - try: - nc_state = docker.check_container_ssh(nc_name) - if nc_state is None: - # 容器不存在 → 確保 docker-compose.yml + DB + 部署 - logger.info(f"NC {nc_name}: not found, ensuring compose/DB and deploying") - _ensure_tenant_compose(tenant, is_active) - _ensure_nc_db(pg_host, pg_db, pg_port) - ok = docker.ssh_compose_up(tenant.code) - result.nc_result = True if ok else False - if not ok: - fail_reasons.append("nc: deploy failed") - else: - # 部署成功後驗證 NC 是否正確使用 PostgreSQL - if not _nc_db_check(nc_name, pg_host, pg_db, tenant.domain, pg_port): - result.nc_result = False - fail_reasons.append("nc: installed but not using pgsql") - elif nc_state is False: - # 容器存在但已停止 → 重啟 - logger.info(f"NC {nc_name}: stopped, restarting") - ok = docker.ssh_compose_up(tenant.code) - result.nc_result = True if ok else False - if not ok: - fail_reasons.append("nc: start failed") + try: + nc_state = docker.check_container_ssh(nc_name) + if nc_state is None: + # 容器不存在 → 確保 docker-compose.yml + DB + 部署 + logger.info(f"NC {nc_name}: not found, ensuring compose/DB and deploying") + _ensure_tenant_compose(tenant, is_active) + _ensure_nc_db(pg_host, pg_db, pg_port) + ok = docker.ssh_compose_up(tenant.code) + result.nc_result = True if ok else False + if not ok: + fail_reasons.append("nc: deploy failed") else: - # 容器正常運行 → 驗證 DB 類型(防止 sqlite3 殘留問題) - db_ok = _nc_db_check(nc_name, pg_host, pg_db, tenant.domain, pg_port) - if not db_ok: + if not _nc_db_check(nc_name, pg_host, pg_db, tenant.domain, pg_port): result.nc_result = False - fail_reasons.append("nc: DB check failed (possible sqlite3 issue)") - else: - result.nc_result = True - if kc_drive_secret: - if not _nc_initialized(nc_name): - # 首次初始化:語言 + Apps + OIDC + SSO 強制模式 + OO 整合 - oo_url = (f"https://office-{tenant.code}.ease.taipei" if is_active - else f"https://office-{tenant.code}.lab.taipei") - ok = _nc_initialize(nc_name, kc_host, realm, kc_drive_secret, tenant.domain, - oo_name, oo_url, is_active) - if not ok: - fail_reasons.append("nc: initialization failed") - else: - # 已初始化:僅同步 OIDC secret(確保與 KC 一致) - ok = _configure_nc_oidc(nc_name, kc_host, realm, kc_drive_secret, tenant.domain) - if not ok: - fail_reasons.append("nc: OIDC sync failed") - result.nc_done_at = now_tw() - except Exception as e: - result.nc_result = False - result.nc_done_at = now_tw() - fail_reasons.append(f"nc: {e}") - - # ── [5] OO container ───────────────────────────────────────────────── - try: - oo_state = docker.check_container_ssh(oo_name) - if oo_state is None: - ok = docker.ssh_compose_up(tenant.code) - result.office_result = True if ok else False - if not ok: - fail_reasons.append("oo: deploy failed") - elif oo_state is False: - ok = docker.ssh_compose_up(tenant.code) - result.office_result = True if ok else False - if not ok: - fail_reasons.append("oo: start failed") + fail_reasons.append("nc: installed but not using pgsql") + elif nc_state is False: + # 容器存在但已停止 → 重啟 + logger.info(f"NC {nc_name}: stopped, restarting") + ok = docker.ssh_compose_up(tenant.code) + result.nc_result = True if ok else False + if not ok: + fail_reasons.append("nc: start failed") + else: + # 容器正常運行 → 驗證 DB 類型 + db_ok = _nc_db_check(nc_name, pg_host, pg_db, tenant.domain, pg_port) + if not db_ok: + result.nc_result = False + fail_reasons.append("nc: DB check failed (possible sqlite3 issue)") else: - result.office_result = True - result.office_done_at = now_tw() - except Exception as e: - result.office_result = False - result.office_done_at = now_tw() - fail_reasons.append(f"oo: {e}") + result.nc_result = True + if kc_drive_secret: + if not _nc_initialized(nc_name): + oo_url = (f"https://office-{tenant.code}.ease.taipei" if is_active + else f"https://office-{tenant.code}.lab.taipei") + ok = _nc_initialize(nc_name, kc_host, realm, kc_drive_secret, tenant.domain, + oo_name, oo_url, is_active) + if not ok: + fail_reasons.append("nc: initialization failed") + else: + ok = _configure_nc_oidc(nc_name, kc_host, realm, kc_drive_secret, tenant.domain) + if not ok: + fail_reasons.append("nc: OIDC sync failed") + result.nc_done_at = now_tw() + except Exception as e: + result.nc_result = False + result.nc_done_at = now_tw() + fail_reasons.append(f"nc: {e}") + + # ── [5] OO container ───────────────────────────────────────────────── + try: + oo_state = docker.check_container_ssh(oo_name) + if oo_state is None: + ok = docker.ssh_compose_up(tenant.code) + result.office_result = True if ok else False + if not ok: + fail_reasons.append("oo: deploy failed") + elif oo_state is False: + ok = docker.ssh_compose_up(tenant.code) + result.office_result = True if ok else False + if not ok: + fail_reasons.append("oo: start failed") + else: + result.office_result = True + result.office_done_at = now_tw() + except Exception as e: + result.office_result = False + result.office_done_at = now_tw() + fail_reasons.append(f"oo: {e}") # ── [6] Quota (OO disk + PG DB size) ──────────────────────────────── - if not tenant.is_manager: - try: - oo_gb = docker.get_oo_disk_usage_gb(oo_name) or 0.0 - pg_gb = _get_pg_db_size_gb(pg_host, pg_db, pg_port) or 0.0 - result.quota_usage = round(oo_gb + pg_gb, 3) - except Exception as e: - logger.warning(f"Quota check failed for {tenant.code}: {e}") + try: + oo_gb = docker.get_oo_disk_usage_gb(oo_name) or 0.0 + pg_gb = _get_pg_db_size_gb(pg_host, pg_db, pg_port) or 0.0 + result.quota_usage = round(oo_gb + pg_gb, 3) + except Exception as e: + logger.warning(f"Quota check failed for {tenant.code}: {e}") if fail_reasons: result.fail_reason = "; ".join(fail_reasons)