diff --git a/app/artifact_retention.py b/app/artifact_retention.py index 2b0de5f..db94f15 100644 --- a/app/artifact_retention.py +++ b/app/artifact_retention.py @@ -86,7 +86,7 @@ def maintain(store, *, max_jobs=16, budget_seconds=5): accounted = storage['observed_bytes']+storage['reserved_bytes']+RESERVATION target = storage['retention_target'] if accounted > store.config.cap and not target: - target = store.config.cap*9//10 + target = store.config.cleanup_target or store.config.cap*9//10 free = shutil.disk_usage(root).free-storage['reserved_bytes']-RESERVATION-store.config.external_reserve needed = (bool(target) and accounted>target) or free cfg['max_bytes'] - target = int(cfg['max_bytes']*.9) if excess else cfg['max_bytes'] + target = cfg.get('cleanup_target_bytes', cfg['max_bytes']*9//10) if excess else cfg['max_bytes'] + if not cfg.get('retention_enabled', True): + return used + reserve <= cfg['max_bytes'] and free - reserve >= cfg['min_free_bytes'] rows = db.execute("SELECT id,path FROM jobs WHERE state='READY' ORDER BY created").fetchall() for job_id, relative in rows: if used + reserve <= target and free - reserve >= cfg['min_free_bytes']: diff --git a/docs/deployment/CT_PORTABLE.ko.md b/docs/deployment/CT_PORTABLE.ko.md index 1d11397..95a7b56 100644 --- a/docs/deployment/CT_PORTABLE.ko.md +++ b/docs/deployment/CT_PORTABLE.ko.md @@ -1,5 +1,8 @@ # 서버별 ct 설치와 재사용 +2026-10-07: [자료 정리·초기화·보관 정책](CT_DATA_MAINTENANCE.ko.md)을 추가했다. +AI2/test 도구 설치 완료, 실제 삭제와 새 APP Runtime 활성화는 사용자 후속이다. 아래는 이전 설치 기준이다. + Last reviewed: `2026-10-01 18:26:23 KST`. 계약: CTG-01~13. test 도구 설치·격리 검증 완료. 이 문서의 test 수용 대기는 당시 상태이며 이후 서버별 배포 결과와 구분한다. 추가 서버의 ct 설치·인증·서비스/모델 검증은 diff --git a/docs/deployment/CT_REUSE_HANDOFF.ko.md b/docs/deployment/CT_REUSE_HANDOFF.ko.md index 07ca197..78dbf2a 100644 --- a/docs/deployment/CT_REUSE_HANDOFF.ko.md +++ b/docs/deployment/CT_REUSE_HANDOFF.ko.md @@ -1,5 +1,11 @@ # ct 재사용·오프라인 후속 개발 인계 +## 최신: 자료 관리 도구 (2026-10-07) + +[CT_DATA_MAINTENANCE.ko.md](CT_DATA_MAINTENANCE.ko.md)에 명령·삭제 경계·권한·설정 원본과 +설치 복구 위치를 정리했다. AI2/test 도구 반영 완료, AI2 정지/test 기존 실행 유지다. +청계산은 접속·패키지 생성 없이 후속 호환 검토만 남겼다. 아래 상태는 각 기록 시점의 사실이다. + ## 최신: test 공통 도구 설치 (2026-10-01) [CT_PORTABLE.ko.md](CT_PORTABLE.ko.md)가 새 설치 정의·Compose adapter·패치 계약·test 경로를 소유한다. diff --git a/docs/deployment/ICERTI_OFFLINE_MAINTENANCE.ko.md b/docs/deployment/ICERTI_OFFLINE_MAINTENANCE.ko.md index be3b6ed..59cf315 100644 --- a/docs/deployment/ICERTI_OFFLINE_MAINTENANCE.ko.md +++ b/docs/deployment/ICERTI_OFFLINE_MAINTENANCE.ko.md @@ -1,5 +1,8 @@ # 아이서티 오프라인 유지보수 준비 +2026-10-07 추가: [portable 자료 관리](CT_DATA_MAINTENANCE.ko.md)는 AI 서버2/test에만 설치했다. +청계산 현장판에 같은 명령이 설치됐다고 가정하지 않는다. 기존 현장 PDF는 이번에 변경하지 않았다. + 2026-10-01 추가: [서버별 ct/test 설치](CT_PORTABLE.ko.md). 아래 si·현장 경로/자동부팅 설명은 기존 현장판 기준이다. test는 root·Compose adapter이며 `ct server`/`ct profile paths`의 실제 값을 우선한다. 이번에 아이서티 설치본을 교체하지 않았다. diff --git a/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md b/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md index 1d00d88..1e144c0 100644 --- a/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md +++ b/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md @@ -1,9 +1,22 @@ # Runtime Lifecycle -Status: `AI2 DEPLOYED · LIVE OBSERVATION PASS · OPERATOR CLOSEOUT WAIT` +Status: `CT DATA TOOLS INSTALLED · AI2 STOPPED · RUNTIME ACTIVATION / USER ACCEPTANCE WAIT` Owner: `(test, ltb1021, work-session)` on `task/runtime-lifecycle-refactor` +## ct 자료 관리·권한 보완 + +Last reviewed: `2026-10-07 11:40:00 KST`. 기준 HEAD `654c35d1994318d401408319b62fc100c2ce45fe`. +[CTCLR 결정](decision/20261007T114000+0900-decision-ct-data-maintenance.md), +[설치·필수 검증](validation/20261007T114000+0900-validation-ct-data-maintenance.md), +[사용·복구 가이드](../../../../deployment/CT_DATA_MAINTENANCE.ko.md)를 우선한다. +si launcher 권한과 재발 원인을 고치고 clean/reset/storage 도구를 AI2와 test에 반영했다. +실자료 삭제·초기화 없음. AI2 5서비스 exited, test 기존5서비스 running 유지. +새 APP image는 다음 명시적 기동 대상으로만 준비했고 새 Runtime 적용/사용자 수용은 보류한다. +최종13파일 설치 지문 일치·si sudo/menu와 격리 필수 검증 완료. 청계산/101 접근 없음. +재개점은 영구 evidence `2026-10-07_ct-storage-maintenance_r01/checkpoint.ko.md`다. +아래의 AI2 최종 Stop 대기는 과거 기록이며 현재 정지 상태로 갱신한다. + ## AI 서버2 배포 재개 Last reviewed: `2026-10-06 19:58:03 KST`. 기준 source `4f0bd4eddd5b6e600b947eee5444fd34e7ee8a54`. diff --git a/tools/icerti_offline/install_portable.py b/tools/icerti_offline/install_portable.py index 6d2492b..a5d7569 100644 --- a/tools/icerti_offline/install_portable.py +++ b/tools/icerti_offline/install_portable.py @@ -96,6 +96,7 @@ def install(d,p,original,sealed,rows): for f in sorted((REPO/'tools/icerti_offline/maintenance').glob('*.py')): if not f.name.startswith('test_'):sources[f.name]=f sources['stop_channels.py']=REPO/'tools/icerti_offline/stop_channels.py' + sources['storage_policy.py']=REPO/'app/storage_policy.py' for module in ('deployment_kit','control_sidecar'): for f in sorted((REPO/module).rglob('*.py')): if '__pycache__' in f.parts:continue @@ -110,6 +111,8 @@ def install(d,p,original,sealed,rows): fp.atomic(prefix/'bridge_tools/tools/__init__.py',b'',mode=0o644) for directory in prefix.rglob('*'): if directory.is_dir():directory.chmod(0o755) + from tool_permissions import tool_access + tool_access(prefix) inventory={str(f.relative_to(prefix)):hashlib.sha256(f.read_bytes()).hexdigest() for f in prefix.rglob('*.py')} fp.atomic(prefix/'installed-files.json',inventory,mode=0o644) for folder in (fp.STATE,fp.RUNTIME/'state',Path(d['paths']['data'])/'site',fp.SETUP/'profiles',fp.SETUP/'inbox',fp.SETUP/'outbox'): @@ -154,8 +157,10 @@ def finish_install(d,p,original,sealed,rows,inventory): connection.verify_ca(ca) fp.atomic(fp.ca_path(p),ca.read_bytes(),user=True) # No credential copy: the Web password changed and the user will provide it privately. - launcher=('#!/bin/sh\nexec /usr/bin/python3 '+str(prefix/'ct.py')+' "$@"\n').encode() + from tool_permissions import launcher as build_launcher + launcher=build_launcher(prefix) server.CONFIG.parent.mkdir(parents=True,exist_ok=True,mode=0o755) + server.CONFIG.parent.chmod(0o755) fp.atomic(server.CONFIG,d,mode=0o644) fp.atomic('/usr/local/bin/ct',launcher,mode=0o755) receipt={'status':'TOOLS_INSTALLED_RUNTIME_UNCHANGED','server_definition':str(server.CONFIG),'prefix':str(prefix), diff --git a/tools/icerti_offline/maintenance/compose_backend.py b/tools/icerti_offline/maintenance/compose_backend.py index 745f618..a7627b8 100644 --- a/tools/icerti_offline/maintenance/compose_backend.py +++ b/tools/icerti_offline/maintenance/compose_backend.py @@ -286,6 +286,12 @@ def restore_config(): def runtime(action): + from data_safety import lock + with lock(fp.STATE/'data-maintenance.lock'): + return _runtime(action) + + +def _runtime(action): from maintenance_process import run import field_runtime as shared if action=='stop': diff --git a/tools/icerti_offline/maintenance/ct.py b/tools/icerti_offline/maintenance/ct.py index b4fadc9..9cf9895 100644 --- a/tools/icerti_offline/maintenance/ct.py +++ b/tools/icerti_offline/maintenance/ct.py @@ -18,9 +18,9 @@ def command(args, timeout=30, **kwargs): return run(args, check=True, timeout=timeout, **kwargs) -def admin(args): +def admin(args, *, fresh=False): if os.geteuid() != 0: - os.execvp("sudo", ["sudo", "--", "/usr/local/bin/ct", *args]) + os.execvp("sudo", ["sudo", *(['-k'] if fresh else []), "--", "/usr/local/bin/ct", *args]) def runtime(action): @@ -94,7 +94,7 @@ def user_command(args, timeout=3600): def menu(): - admin([]) + admin([], fresh=True) dispatch={'1':['status'],'2':['profile','select'],'3':['profile','edit'],'4':['profile','credentials'], '5':['ca'],'6':['net','status'],'7':['net','apply'],'8':['config','prepare'], '9':['config','connect'],'10':['config','apply'],'11':['start'],'12':['stop'], @@ -102,7 +102,8 @@ def menu(): '17':['profile','paths'],'18':['profile','file'],'19':['config','export'],'20':['apply'], '23':['config','confirm'],'24':['config','note'],'25':['profile','web'], '26':['config','version'],'27':['resources'],'28':['lang'],'29':['net','arp'],'30':['vscode'], - '31':['usb']} + '31':['usb'],'32':['data','clean'],'33':['db','reset'], + '34':['camera','reset'],'35':['redis','clean'],'36':['storage']} while True: print('\nContextra 현장 관리 / field maintenance (0 Exit)') print('1 상태/status 2 환경선택/profile 3 설정/edit 4 계정저장/credentials 5 CA 파일') @@ -116,6 +117,7 @@ def menu(): print('26 Connection version 27 CPU/RAM resources 28 Language 29 ARP status') print('30 VSCode safety status (settings only; no automatic restart)') print('31 USB status (ct usb mount / ct usb unmount)') + print('32 Data clean 33 DB reset 34 Camera reset 35 Redis clean 36 Storage policy') choice=input('번호 [Enter=메뉴 유지]: ').strip() if choice=='0':return try: @@ -134,12 +136,16 @@ def menu(): def main(argv=None): parser = argparse.ArgumentParser(description=text("Contextra field maintenance. status/stop/start/profile/config/resources/usb/lang")) - parser.add_argument("action", nargs="?", choices=("status","stop","start","observe","work","connection","apply","edit","logs","access","auth","profile","config","net","ca","emergency","diagnose","resources","lang","vscode","usb","patch","server")) + parser.add_argument("action", nargs="?", choices=("status","stop","start","observe","work","connection","apply","edit","logs","access","auth","profile","config","net","ca","emergency","diagnose","resources","lang","vscode","usb","patch","server","data","artifact","training","db","camera","redis","storage")) parser.add_argument("arguments", nargs=argparse.REMAINDER) args = parser.parse_args(argv) readonly = args.action in (None,'status','server','logs','resources','lang','vscode','usb','diagnose','patch') or (args.action in ('profile','config','net') and (not args.arguments or args.arguments[0] in ('status','paths','list','version','scheduled'))) if server.CURRENT and not readonly:server.verify_tools() - if args.action=='server': + if args.action in ('data','artifact','training','db','camera','redis','storage'): + admin([args.action,*args.arguments]) + import field_data + field_data.main(args.action,args.arguments) + elif args.action=='server': server.banner() if server.CURRENT:print('Capabilities:',', '.join(server.CURRENT['capabilities'])) elif args.action=='patch': diff --git a/tools/icerti_offline/maintenance/field_ui.py b/tools/icerti_offline/maintenance/field_ui.py index cb40696..25c2f08 100644 --- a/tools/icerti_offline/maintenance/field_ui.py +++ b/tools/icerti_offline/maintenance/field_ui.py @@ -160,6 +160,7 @@ KOREAN_MESSAGES = { 'Web certificate signing failed. Ask the Web operator to check CA/key validity, matching and file permissions.':'Web 인증서 서명 실패입니다. Web 담당자에게 CA/키 유효성·일치·읽기 권한 확인을 요청하세요.', 'Preserve this stage and session. Do not cancel, change the connection version or create a new CA. Retry once after the Web fix.':'준비본과 세션을 보존하세요. cancel·버전 변경·새 CA 생성을 하지 말고 Web 조치 후 한 번 재시도하세요.', '26 Connection version 27 CPU/RAM resources 28 Language 29 ARP status':'26 연결 설정 버전 27 CPU/RAM 진단 28 언어 29 ARP 상태', + '32 Data clean 33 DB reset 34 Camera reset 35 Redis clean 36 Storage policy':'32 저장자료 정리 33 DB 기록 초기화 34 카메라 초기화 35 Redis 정리 36 보관 설정', '30 VSCode safety status (settings only; no automatic restart)':'30 VSCode 안전 설정 상태 (자동 재시작 없음)', '31 USB status (ct usb mount / ct usb unmount)':'31 USB 상태 (ct usb mount / ct usb unmount)', 'Prepared connectionConfigVersion:':'준비한 연결 설정 버전:',