oceanctl — Ocean CLI
터미널에서 Ocean 에 작업을 제출하고 상태를 확인한다. AI 에이전트가 쓰는 것을 상한으로 설계했다.
- 설계 = projects/ocean-cli/design.md
- 구현 계획 = projects/ocean-cli/implementation-plan.md
- 런타임 의존성 0 — 표준 라이브러리만 쓴다. 어디서든 설치가 가볍다.
라이선스: MIT(이 디렉터리 한정) · 배포: PyPI
oceanctl
설치
최근 Debian/Ubuntu 는 시스템 Python 에 직접 설치하는 것을 막는다(PEP 668,
externally-managed-environment). CLI 애플리케이션이므로 격리 설치가 맞다.
권장 — uv
uv tool install oceanctl # pipx 와 같은 격리 설치, 더 빠르다
oceanctl --version # PATH 에 자동 등록된다
uvx oceanctl whoami # 설치 없이 일회성 실행도 된다
★uv 는 파이썬이 없는 머신에서도 파이썬을 알아서 받아 깐다 — "파이썬부터 깔아야 하나" 가
없는 유일한 경로라 권장 기본이다. 버전 고정은 uv tool install oceanctl==1.0.0.
pipx 도 그대로 된다
sudo apt install pipx # 또는 python3 -m pip install --user pipx
pipx install oceanctl
레포 체크아웃에서 개발판을 깔려면 pipx install /path/to/ocean-all/ocean-cli
(uv 는 uv tool install /path/to/ocean-all/ocean-cli).
단일 파일 — .pyz (kubectl 식)
릴리스마다 실행 가능한 zipapp 하나가 첨부된다. 런타임 의존성이 0 이라 가능한 형태다 — 파이썬 3.10+ 만 있으면 어떤 설치 도구도 없이 돈다:
gh release download oceanctl-v1.0.0 -p oceanctl.pyz -R AI-Ocean/ocean-all
chmod +x oceanctl.pyz && ./oceanctl.pyz --version # 또는 python3 oceanctl.pyz
- ★저장소가 private 인 동안 릴리스 자산도 private 다 — 레포 접근 권한이 있는 사람만 받을 수
있다(외부 사용자는 PyPI 경로를 쓴다). 저장소가 공개되면
curl -LO https://github.com/AI-Ocean/ocean-all/releases/download/oceanctl-v<버전>/oceanctl.pyz가 된다. - ★모노레포라
releases/latest는 다른 컴포넌트(agent·SM) 릴리스를 가리킬 수 있다 — 태그를 명시한 URL 만 쓴다.
pipx 가 없으면 — venv
python3 -m venv ~/.venvs/oceanctl
~/.venvs/oceanctl/bin/pip install /path/to/ocean-all/ocean-cli
# PATH 에 올리려면 (하나만 골라서)
ln -s ~/.venvs/oceanctl/bin/oceanctl ~/.local/bin/oceanctl
# 또는
echo 'alias oceanctl="$HOME/.venvs/oceanctl/bin/oceanctl"' >> ~/.bashrc
pip install --break-system-packages는 쓰지 않는다 — 시스템 Python 을 깨뜨릴 수 있고, 이 도구는 격리해도 잃을 게 없다(의존성이 0이다).
설정
토큰은 웹에서 발급한다 — 사이드바 CLI 토큰 → 이름 입력 → 발급. 평문은 그때 한 번만 보인다(서버는 해시만 저장한다).
oceanctl configure --api-url https://api.aiocean.click
# CLI token: ← 붙여넣기 (화면에 찍히지 않는다)
# ✓ 저장됨 ~/.ocean/credentials (0600)
# ✓ 확인 오션어드민 <lee@example.com> · 고려대 / korea
# └ 어느 조직/클러스터에 붙었는지 그 자리에서 보인다
oceanctl configure --show
# API URL https://api.aiocean.click
# 토큰 ocn_3f9a7c21… (전체는 표시하지 않음)
# 파일 ~/.ocean/credentials
- 자격증명은
~/.ocean/credentials에 0600 으로 저장된다. - ★토큰을 환경변수나 플래그로 주지 않는다. env 는 에이전트 프로세스에 상속돼
env출력·로그·에러 리포트로 새고, 플래그는 셸 히스토리와ps에 남는다. 에이전트는oceanctl ...을 실행할 뿐 토큰 원문을 보지 않는다. - ★토큰은 조직 + 클러스터에 묶인다. 발급할 때 클러스터를 고르고, 그 토큰은 그 클러스터에서만
쓴다. 컨텍스트가 곧 자격증명이라 CLI 에
--cluster플래그가 없다 — 대상을 바꾸려면 그 클러스터용 토큰으로oceanctl configure를 다시 한다(kubectl config use-context대신 토큰 교체). 여러 토큰을 등록해 두고 이름으로 전환하는 프로파일은 뒤에 붙일 수 있다(계약을 넓히는 방향이라 안전하다).
명령
oceanctl configure [--api-url URL] [--show] 자격증명·API URL 설정 / 현재 설정 보기
oceanctl whoami 이 토큰이 묶인 유저·조직·클러스터
oceanctl machine-type list 고를 수 있는 머신타입
oceanctl quota 남은 할당량
oceanctl project list 프로젝트 목록
oceanctl image list 쓸 수 있는 컨테이너 이미지(create 에 넣을 이름)
oceanctl volume list 마운트할 수 있는 볼륨(create 에 넣을 이름)
oceanctl node list --machine-type ID --for job|instance
지금 쓸 수 있는 노드 후보(--node 에 넣을 이름)
oceanctl instance list 인스턴스 목록 + 접속 정보(SSH·VSCode)
oceanctl instance create --name N --image I --machine-type ID --volume V[:PATH[:ro]]
인스턴스 생성 (`-f spec.json` 도 가능)
oceanctl job submit --name N --image I --machine-type ID --volume V[:PATH[:ro]] --command CMD
잡 제출 (+ [--repeat N] [--project ID] [--node NAME])
oceanctl job submit -f spec.json 잡 제출 (스펙 파일 · `-f -` 는 stdin)
oceanctl job list 잡 목록 · 상태 · uid
oceanctl job wait --job-uid UID 잡 종결 대기(성공 0 · 실패/초과 1)
oceanctl job logs --job-uid U [--pod-uid U] 잡 로그(raw 스트림 — 아래 참조)
oceanctl job why --pod-uid U 그 파드가 왜 대기 중인가
★
instance delete·stop은 없다(설계 D4). 정리는 웹 UI 에서 한다.connect/ssh 래퍼도 없다(D5) —instance list가sshCommand를 그대로 보여주고 거기서 멈춘다. 감싸면 포트포워딩·키·에이전트 전달까지 떠안게 되고, 그건 ssh 클라이언트가 이미 하는 일이다.job delete·cancel도 같은 이유로 없다 — 끝난 잡은 할당량을 먹지 않고 60일 뒤 자동으로 사라지므로 반복 실험이 그대로 돈다.
옵션(붙는 명령은 괄호 안에):
| 옵션 | 설명 |
|---|---|
--json |
모든 명령 — {ok, data} / {ok, error} 봉투(에이전트용). exit code 는 0/1. ★job logs 는 예외 — 아래 |
--project <id> |
대상 프로젝트(quota·job submit·instance create — 생략 = 기본 프로젝트) |
--limit <N> |
가져올 개수(instance list·project list·job list — 생략 시 서버 기본 50, 상한 200) |
--node <name> |
이 노드에 올린다(job submit·instance create — 생략 = 스케줄러 선택) |
organizationId·clusterId 는 주지 않는다 — 토큰이 담고 서버 미들웨어가 채운다.
토큰이 고정한 것과 다른 값을 굳이 보내면 403 이다(조용히 덮지 않는다).
★CLI 토큰은 개인 작업용이다(owner 2026-07-29). 목록은 자기 자원만 보여준다 — 조직 관리자라 해도 CLI 로는 조직 전체를 볼 수 없다(
showAll을 보내면 403cli_token_show_all_forbidden). 웹 관리자 화면은 그대로 조직 전체를 본다. 능력이 아니라 자격증명의 수명 때문이다: 웹 세션은 24시간 쿠키인데 이 토큰은 만료가 없고 파일에 있어, 유출 시 반경을 그 사람 것으로 묶어 두는 편이 안전하다.★단서 하나(P6 실측, [[OD-113]]):
job logs·job why는showAll이 아니라 조직 역할로 범위가 정해진다 — 조직 관리자 토큰은 uid 를 이미 알고 있는 남의 잡 로그를 읽을 수 있다. 웹 관리자와 같은 범위이고, uid 를 얻는 CLI 경로(job list·volume list)는 전부 자기 것만 주므로 목록으로 훑을 수는 없다.
★
cluster list는 없다(2026-07-28 제거). 토큰이 클러스터를 고정하므로 목록을 봐도 CLI 로 할 수 있는 일이 없다 — "지금 어느 클러스터에 붙어 있나" 는whoami가 답한다. 부수로 그 라우트(GET /api/organization/kubernetes)가 토큰 수용 목록에서 빠졌다: 클러스터 API 주소· prometheus 주소·조직 구성원 전원 명단을 실어 보내는데 CLI 는 id·name·status 만 쓰고 있었다.
출력
목록은 헤더가 있는 표다. 값이 없으면 빈칸이 아니라 - 로 채운다.
$ oceanctl whoami
오션어드민 <lee@example.com>
조직 고려대 (3)
클러스터 korea (11)
토큰 p5-dev · ocn_ab12cd34
API https://api.aiocean.click
$ oceanctl machine-type list
ID NAME CPU MEM GPU GPU TYPE
34 32CPU-100G-2GPU 32 100Gi 2 NVIDIA-RTX-A6000
46 default-cpu 1 4Gi 0 -
$ oceanctl quota
범위 legacy-5-3 (기본)
인스턴스
MACHINE TYPE ID NAME CPU MEM GPU GPU TYPE USED QUOTA FREE
48 mid-gpu 16 64Gi 2 NVIDIA-RTX-A6000 1 1 0
49 mid-cpu 16 64Gi 0 - 0 2 2
잡
MACHINE TYPE ID NAME CPU MEM GPU GPU TYPE USED QUOTA FREE
48 mid-gpu 16 64Gi 2 NVIDIA-RTX-A6000 0 2 2
$ oceanctl instance create --name devbox --image busybox:1.36 --machine-type 48 --volume data
✓ 생성 요청됨 devbox (Pending)
접속 정보(SSH·VSCode)는 `oceanctl instance list` 에서 확인하세요(파드가 Running 이 되면 접속됩니다).
$ oceanctl instance list
NAME STATUS MACHINE TYPE NODE SSH VSCODE
devbox Running mid-gpu gpu-01 ssh ocean@10.0.0.5 -p 30022 http://10.0.0.5:30080
총 1개
★목록은 조용히 잘리지 않는다. 서버 기본 limit 이 50 이므로 인스턴스·프로젝트가 그보다 많으면
표 뒤에 그 사실이 뜬다:
★총 120개 중 50개만 표시됐다 — `--limit 120` 로 전부 본다
total(잘리기 전 총계)은 서버가 원래 주고 있던 값이다 — CLI 가 그걸 버려서 51번째부터
있는데 없는 것처럼 보이고 있었다. 상한(200)을 넘으면 안 되는 값을 권하지 않고 웹으로 안내한다.
$ oceanctl image list
NAME TYPE SIZE
aicoean/custom:v1 PRIVATE 0.04Gi
myaiocean/pytorch:2.1.2-cuda12.1-cudnn8-runtime PUBLIC 12.35Gi
NAME이 곧instance create --image에 넣을 값이다.TYPE—PUBLIC은 모두가 쓰는 것,PRIVATE은 이 조직이 올린 것. 남의 조직 PRIVATE 은 서버가 애초에 안 준다.- 크기 단위는
Gi다 — 백엔드가 레지스트리 bytes 를1024³으로 나눠 저장하므로 실제로는 GiB 다(웹 화면은 "GB" 로 찍는데 그건 표기가 틀린 것이다). - ★이미지 등록·삭제 명령은 없다(볼륨과 같은 논리). 목록만 있는 이유는
--image에 넣을 이름을 CLI 안에서 알 수 있어야 하기 때문이다. - 이 목록은 페이지네이션이 없어
--limit이 없다(machine-type list와 같다).
$ oceanctl volume list
NAME CAPACITY STATUS SHARED WRITE IN USE
workspace 100Gi Bound - - 1
datasets 2Ti Bound yes ro 0
-
NAME이 곧instance create --volume에 넣을 값이다. 백엔드는 claim 이름·표시명·라벨· PV 이름을 전부 별칭으로 받지만(volumes/mount.py), 사람이 웹에서 보는 것과 같은 값이 이것이다. -
IN USE= 그 볼륨을 마운트한 인스턴스 + 잡 개수. 서버는 객체를 통째로 주지만 표에는 개수만 낸다(원문은--json). -
WRITE는 공유 볼륨에만 의미가 있다.ro인 공유 볼륨을 rw 로 마운트하려 하면 422 로 거부된다. -
★볼륨 생성·삭제 명령은 없다(D4 와 같은 논리). 목록만 있는 이유는
--volume에 넣을 이름을 CLI 안에서 알 수 있어야 하기 때문이다 — 웹을 봐야 아는 상태면 에이전트는 쓸 수 없다. -
★
CAPACITY는 할당량이지 사용량이 아니다. 실제로 얼마나 썼는지는oceanctl volume usage NAME이 답한다([[OD-112]]) — 할당/실사용을 함께 보여주고, 서버가 백그라운드로 계산 중이면 "계산 중" 이라고 말한다(잠시 후 재실행). ★현재 이 명령은 org ADMIN 전용이다(서버 라우트 게이트) — 일반 사용자 개방은 owner=self 강제 설계가 필요한 별건으로 남아 있다. 같은 이름의 볼륨이 여럿이면--owner USER_ID로 좁힌다. -
★
--volume은 필수다. 빼면 PVC 가 하나도 안 붙은 파드가 떠서 재시작·evict·수명 만료에 작업물이 사라진다. 웹은 그 상태를 아예 만들 수 없다(볼륨 0개면 생성 패널이 안 열린다) — 백엔드가 빈 목록을 받아줄 뿐 제품 계약에는 없는 상태다. 여러 개면 반복한다:--volume a --volume b. -
마운트 경로와 모드를 줄 수 있다 —
--volume NAME[:PATH[:ro]](도커-v관례):--volume data # /volume/data (서버가 정한다 — 지금까지와 같다) --volume data:/volume/mydata # 경로 지정 --volume datasets:/volume/ds:ro # 경로 + 읽기전용 --volume datasets:ro # 경로는 서버 기본, 읽기전용만 지정
-
필드는 최대 셋이고 경로에
:를 쓸 수 없다. 두 번째 필드가/로 시작하면 경로, 아니면 모드다. 모르는 모드는 거부한다(:readonly·:r등) — 조용히 경로로 삼으면 요청한 읽기전용이 사라진다. 대소문자는 가리지 않는다(:RO=:ro). -
명시
:rw는 아무것도 보내지 않는다(서버 기본값과 같다) — 서버가 기본을 바꾸면 CLI 가 그걸 덮어쓰지 않게 하기 위해서다. -
경로는
/volume/…아래 또는/home/ocean·/home/linuxbrew/.linuxbrew만 된다. 예약 경로(/root/dataset·/dev/shm)·루트·상위 이동(..)·중복은 서버가 422 로 막는다. CLI 는 규칙을 따라 검사하지 않는다(두 곳이 어긋나면 더 나쁘다) — 서버가 진실이다. -
★읽기전용 공유 볼륨(
volume list의WRITE=ro)은:ro를 줘야 붙는다. 안 주면 서버가 422 로 거부한다(조용히 읽기전용으로 낮추지 않는다). 그전에는 CLI 로 아예 못 붙였다.
-
-
★생성 응답에는 접속 정보가 없다.
POST /api/instances는 만들어진 k8s 파드·서비스 객체를 주는데,sshCommand·vscodeAddress는 조회 시점에 계산되는 값이다 — 그래서create는 다음 행동을 알려주고 멈추고, 접속 정보는instance list가 답한다. 주소 자체는 바로 나온다(재료인 nodePort 가 Service 에서 오고 Service 는 생성 시점에 생긴다). 실제로 붙는 것은 파드가Running이 된 뒤다. -
instance list는 17개 응답 필드 중 6개만 표에 넣는다(podUid·image·limits·volumes등은 폭 때문에 뺐다).--json에는 전부 들어 있다 — 뺀 이유는oceanctl/cli.py의 표에 적혀 있다. -
SSH 칸이
-면 그 클러스터가 SSH 를 NodePort 로 노출하지 않는 것이다. 가장 흔한 원인은 Istio 모드(그때는 VSCODE 칸에 게이트웨이 URL 이 온다)지만 유일하지는 않다 — 클러스터 노출 주소(cluster.address)가 비어 있어도 같은 결과다. 즉-는 "이 경로로는 못 붙는다" 이지 "Istio 다" 가 아니다. -
메모리 단위는
Gi다 — 백엔드가 파드 스펙에 그대로 넣는 값이다({memory}Gi). -
FREE=QUOTA - USED. 제출 전에 보는 명령이라 남은 개수가 먼저다. -
MACHINE TYPE ID가 곧 제출에 넣을machineTypeId다 —machine-type list를 다시 칠 일이 없다. -
★
quota의 기본 범위는 "기본 프로젝트" 다 — 전체 합산이 아니다. 제출(instance create·job submit)이projectId를 생략하면 서버가 기본 프로젝트로 넣으므로, quota 가 보여주는 숫자가 곧 그 제출을 막을 숫자여야 한다. 두 기본값이 어긋나 있으면 프로젝트가 여러 개일 때 quota 는 FREE 2 라고 하는데 제출은 거부되는 일이 생긴다. 다른 프로젝트를 보려면--project <id>(id 는oceanctl project list).- 프로젝트는 있는데 기본이 없으면 전체 합산으로 떨어지지 않고 에러다(
default_project_not_found) — 틀린 숫자보다 낫다. - 프로젝트가 아예 없으면(할당량 승인 전 신규 유저) 에러가 아니라
범위 프로젝트 없음으로 표시한다 — 프로젝트가 0개면 틀릴 숫자가 없고, 이때 죽이면 안내(project list)가 실행 불가다.
- 프로젝트는 있는데 기본이 없으면 전체 합산으로 떨어지지 않고 에러다(
잡
제출 → 목록에서 uid 확인 → 로그. 이 셋이 한 흐름이다.
$ oceanctl job submit --name exp --image nvcr.io/nvidia/pytorch:24.01-py3 \
--machine-type 48 --volume data --command "python train.py"
✓ 제출됨 exp (잡 1개)
상태와 uid 는 `oceanctl job list`, 안 뜨는 이유는 `oceanctl job why` 로 봅니다.
$ oceanctl job list
NAME STATUS NODE JOB UID POD UID
exp-0 Running gpu-01 7d0a1c9e-3f2b-4f7a-9c11-8e5a1b2c3d4e 1b9f8e7d-6c5b-4a39-8271-0f1e2d3c4b5a
총 1개 잡 그룹
$ oceanctl job logs --job-uid 7d0a1c9e-…
2026-07-29T01:00:11.123Z Epoch 1/10 loss=2.31
2026-07-29T01:00:39.884Z Epoch 2/10 loss=1.87
$ oceanctl job why --pod-uid 1b9f8e7d-…
사유 insufficient_gpu
설명 0/3 nodes are available: 3 Insufficient nvidia.com/gpu.
--volume은 여기서도 필수다. 잡은 인스턴스보다 더 아프다 — 끝나면 파드가 사라지므로 결과물을 PVC 에 안 썼으면 회수할 방법이 없다(웹은 볼륨 0개인 잡을 아예 만들 수 없다).--repeat은 기본 1 이다(웹 생성 폼과 같은 기본값). N 을 주면 같은 잡이 N개 제출되고 할당량을 N개 먹는다 —oceanctl quota의FREE를 먼저 보라.--project <id>로 프로젝트를 고른다(생략 = 기본 프로젝트, 지금까지와 같다). id 는oceanctl project list.quota --project와 같은 범위를 가리키므로, 제출 전에 본 숫자가 곧 그 제출을 막을 숫자다.--node <name>으로 노드를 지정한다(생략 = 스케줄러가 고른다). 후보는oceanctl node list. ★핀은 honored 다 — 그 노드에 자리가 없으면 거부가 아니라 pending 이고(--repeat은 모든 반복이 같은 노드를 겨냥한다), 이유는oceanctl job why가 답한다.- ★없는 노드 이름은 서버가 막는다(404
node_not_found, [[OD-117]]). 예전에는 그대로 받아 파드가 영원히 pending 이었고 그동안 할당량을 먹었다 — 웹은 목록에서 고르게 해 그 실수가 불가능했지만 CLI 는 자유 입력이라 열려 있었다. 지금은 아무것도 만들지 않고 거부한다. - 노드가 있는데 자리가 없으면 그건 pending 이 맞다(honored 핀의 설계) —
oceanctl job why가insufficient_gpu·selector_mismatch로 답한다.
$ oceanctl node list --machine-type 48 --for job
범위 머신타입 48 · job · 요청 2 GPU
NODE FREE GPU FITS OWNER GROUP SHARING MINE
gpusystem 2 yes 물리학과 PUBLIC -
ds02 0 no 내 랩 EXCLUSIVE yes
-
★
FREE GPU는 노드별이다 — 총합이 아니라 노드별이라야 "이 노드에 핀하면 지금 뜨는가" 를 안다. 여유가 0인 노드도 숨기지 않는다(그것도 판단 재료다). -
★
FITS가 그 판단을 대신 해 준다. 전에는 "후보"가 GPU 종류·컴퓨팅타입이 맞는 노드를 뜻할 뿐 지금 들어갈 수 있는 노드가 아니어서, 2 GPU 를 요청했는데 여유 1인 노드도 올라왔다. 그걸--node로 핀하면 영영 pending 이다(2026-07-29 도그푸딩에서 실제로 그랬다). 전부no면 표 아래에 그렇게 적는다. 목록에서 지우지는 않는다 —--node를 생략한 자동 배치는 여전히 가능하다. -
★판정은 서버가 한다(
fits). CLI 는 읽어서 그릴 뿐이다 — 잠깐 웹과 CLI 가 같은 규칙을 각각 갖고 있었는데, 서버 필터가 바뀌면 둘 다 따로 어긋나는 모양이라 한 곳으로 모았다. 네 값이 각각 다르다:값 뜻 yes서버가 막을 이유를 못 찾았다. "확실히 뜬다"가 아니다 — 서버는 GPU 만 판정한다 no서버가 부족을 확인했다 -이 머신타입은 GPU 를 안 쓴다 — 판정 대상이 아니라는 뜻이지 "뜬다"가 아니다 ?서버가 판정을 안 줬다(구버전). no와 구분해서 읽어야 한다 -
★서버가 CPU·메모리는 판정하지 않는다. 사용량 지표가 Ocean namespace 파드만 세어 다른 namespace(kube-system 등)가 잡아 둔 몫이 빠지기 때문이다 — 판정하면 거짓
yes가 된다. 그래서yes를 받고도 CPU 가 모자라 pending 일 수 있고, 그때 이유는oceanctl job why가 답한다. -
--for는 필수다. 서버가 이 값으로 후보를 거르므로(노드마다 잡용/인스턴스용 라벨이 있다) 생략하면 조용히 틀린 목록이 된다. -
OWNER GROUP이 내 랩이 아니어도 고를 수는 있다(소유는 advisory).SHARING이 그 노드의 공유 정책이다 —EXCLUSIVE·SHARED_RECLAIMABLE·PUBLIC. -
★노드 변경 명령은 없다 — 소유·타입 지정은 ADMIN 운영 작업이다. 목록만 있는 이유는
--node에 넣을 이름을 CLI 안에서 알 수 있어야 하기 때문이다(볼륨·이미지와 같은 논리). -
job list는 파드마다 한 행이다.--repeat 3으로 만든 잡 그룹은 세 행으로 보인다 — 로그·대기사유가podUid를 받으므로 행 단위가 파드여야 한다. 그래서 표 뒤의 총계는 잡 그룹 수(서버가 세는 단위)라고 밝혀 둔다. -
JOB UID·POD UID를 그대로 복사해job logs·job why에 넣는다. 이름으로는 못 찾는다 — 같은 이름으로 여러 번 제출할 수 있어서 CLI 가 이름→uid 규칙을 발명해야 하기 때문이다. -
★
job logs는--job-uid하나면 된다. 잡의 파드가 하나면(오늘의 모든 잡) 서버가 그것을 고른다.--pod-uid는 파드가 여럿일 때만 필요하고, 그때 서버가pod_selection_required(400)로 후보 uid 를details.podUids에 담아 알려준다.--job-uid를 남긴 이유는 나중에 멀티노드 잡이 와도 잡 uid 는 그대로 정체성이기 때문이다. (후보 uid 는--json의error.details.podUids에 온다 — 사람용 출력은 메시지와 코드만 낸다.oceanctl job list의POD UID컬럼에서도 같은 값을 볼 수 있다.) -
★끝난 잡의 로그는 사라진다. 파드가 정리되면(수명 제한 초과·GC)
job logs는 "파드가 남아 있지 않다" 는 에러(job_pod_not_found)로 답한다 — 기다려도 안 오는 것을 "준비 중" 이라고 하면 에이전트가 무한히 폴링한다. -
★
job logs는--json봉투를 씌우지 않는다(설계 §5 가 정한 명시적 예외). 서버가text/plain스트림으로 주기 때문에, 봉투를 씌우려면 전부 모았다가 한 번에 내야 해서 스트리밍이 아니게 된다.--json을 줘도 본문은 원문 그대로이고 다음만 달라진다:언제 어디로 모양 로그 본문 stdout 원문 그대로 스트림이 시작되기 전 에러(404·403·연결 실패) --json이면 stdout, 아니면 stderr평소 봉투 스트림 도중 끊김 항상 stderr + exit 1 ✗ …한 줄마지막 줄이 핵심이다 — 이미 로그가 stdout 에 흐른 뒤 봉투를 stdout 에 얹으면 둘 다 못 읽는다.
-
로그는 현재 로그(마지막 50줄) 를 흘리고 끝난다 —
tail -f가 아니다(서버가follow=False). 실행 중인 잡에서도 멈추지 않으므로 에이전트가 매달릴 일이 없다. -
파이프로 잘라 써도 된다:
oceanctl job logs … | head -20처럼 읽는 쪽이 먼저 닫으면 조용히 끝난다(exit 0 — 원하는 만큼 받고 닫은 것이지 실패가 아니다). -
★
job why는null을 줄 수 있다 — 파드가 대기 중이 아니면(이미 스케줄됐거나 끝났으면) 에러가 아니라{"ok": true, "data": null}이다. 에이전트는 이걸 "기다릴 이유가 없다" 로 읽는다.category는 안정적인 값이라 분기 축으로 쓴다:insufficient_gpu·selector_mismatch·node_unavailable·insufficient_cpu_mem·other. -
설명은 쿠버네티스 스케줄러가 남긴 원문이다(영문). 서버가 요약하지 않고 그대로 싣고 ("진실 은폐 금지")category만 얹는다 — 분류에 안 걸리면other+ 원문이다.
스펙 파일로 제출하기 (-f)
repeat를 생략하면 플래그 경로와 같은 1 이다([[OD-127]]) — "스펙=서버 body 원형" 계약의 유일한 예외.
플래그가 늘면 한 줄이 길어진다. -f 로 JSON 을 주면 된다 — 스키마는 서버가 받는 요청 본문
그대로다(CLI 전용 스키마가 없다).
★주의:
--json출력은 서버 응답이라 이 파일과 모양이 다르다(제출 응답은 만들어진 잡 정보다). "--json으로 나온 것을 그대로-f에 넣는다" 는 성립하지 않는다 — 처음 이 문서에 그렇게 적었다가 무맥락 리뷰가 잡았다. 재사용하려면 보낸 스펙 파일을 보관하면 된다.
$ cat job.json
{
"name": "exp-42",
"image": "myaiocean/pytorch:2.1.2-cuda12.1-cudnn8-runtime",
"machineTypeId": 48,
"command": "python train.py",
"repeat": 1,
"projectId": 7,
"nodeName": "gpusystem",
"purpose": "lr 3e-4 재현",
"volumes": [
{"volumeName": "test0521", "mountPath": "/volume/data"},
{"volumeName": "datasets", "readOnly": true}
]
}
$ oceanctl job submit -f job.json
$ cat job.json | oceanctl job submit -f - # stdin
-f와 다른 플래그를 함께 쓸 수 없다(v1). 병합 규칙(무엇이 이기나)을 지금 정하면 검증하지 않은 규칙이 계약이 되기 때문이다 — 필요가 증명되면 넓힌다(넓히기는 안전하다).- ★모르는 필드는 CLI 가 거부한다. 백엔드는 모르는 필드를 조용히 무시하므로
"comand"오타가 "command 누락" 422 로 둔갑하거나, 선택 필드 오타는 아무 말 없이 사라진다. instance create도 같다. 단 스펙에command·repeat은 없다(잡 전용) — 넣으면 거부한다.purpose는 자유 텍스트 메모(200자)다. 목록에 보이지만 검색은 안 된다 — 실행 조건을 적어 두는 용도이고, 플래그로는 안 받는다(파일에만 있다).- YAML 은 지원하지 않는다 — 이 CLI 는 런타임 의존성 0이 계약이고 YAML 은 새 의존성이 필요하다.
에이전트가 쓸 때
oceanctl quota --json
# {"ok": true, "data": {"instances": [...], "jobs": [...]}}
oceanctl project list --json
# {"ok": false, "error": {"errorCode": "invalid_cli_token", "message": "..."}} # exit 1
★job list --json 의 모양도 계약으로 박아 둔다([[OD-126]] — 에이전트 루프 실증에서 첫
폴러가 이 스키마를 몰라 헛돌았다). 상태는 3단 깊이에 있고, 잡 키는 uid 인데 job logs
파라미터는 --job-uid 다(같은 값):
{"ok": true, "data": {"items": [
{"name": "exp", // 잡 그룹(제출 단위) — 그룹 레벨 status 는 없다
"jobs": [
{"name": "exp-0", "uid": "…", // ← job logs --job-uid · job wait --job-uid 에 넣는 값
"jobPodInfos": [
{"uid": "…", // ← job why --pod-uid 에 넣는 값
"status": "Running", "node": "…"}]}]}
], "total": 1}}
종결만 기다릴 거면 이 스키마를 직접 파싱할 필요가 없다 — job wait 가 폴링을 안으로 접는다
([[OD-128]]): oceanctl job wait --job-uid UID --json → 성공 종결 exit 0 · 실패/시간초과 exit 1.
whoami --json 의 모양은 계약이므로 여기 박아 둔다 — 바꾸면 에이전트 스크립트가 깨진다:
{"ok": true, "data": {
"user": {"id": 5, "name": "오션어드민"},
"email": "lee@example.com",
"organization": {"id": 3, "name": "고려대"},
"cluster": {"id": 11, "name": "korea"},
"token": {"id": 7, "name": "p5-dev", "tokenPrefix": "ocn_ab12cd34",
"createdAt": "2026-07-28T00:00:00Z"},
"apiUrl": "https://api.aiocean.click"
}}
★2026-07-28 에 이 모양이 한 번 바뀌었다(
name·user.name·whoami가GET /api/users/me대신 토큰 검증 라우트를 쓰게 되면서다. 사용자가 owner 뿐이라 그때는 무해했지만, 다음 변경은 계약 변경이다.
★--json 은 서버 응답 원형이다. 사람용 표의 정렬·단위·- 채움은 봉투에 들어가지 않으므로,
사람용 출력을 손봐도 에이전트 파서가 깨지지 않는다.
- ★필수 플래그가 빠지면 exit 1 + 봉투다(
missing_required_flags) — 어떤 플래그가 빠졌는지 메시지에 이름으로 들어 있다. 예전에는 argparse 사용법이 exit 2 로 나갔는데,-f와 양자택일이 되면서 검사를 코드로 옮겼고 그 편이 "exit code 는 0/1" 계약에도 맞다. 단 argparse 가 먼저 잡는 것은 여전히 exit 2 다 — 모르는 플래그(--nope), 숫자여야 하는 자리에 문자(--machine-type abc), 없는 서브커맨드. 그것들은 파서가 인자를 해석하기도 전에 죽는 자리라 CLI 코드가 손댈 수 없다. - 성공/실패는 exit code 0/1 로 먼저 갈린다. 세부 분기는
error.errorCode로 한다 (서버가 정의한 안정적인 snake_case 코드 —cluster_id_required·invalid_cli_token등). --json은 성공·실패 모두 stdout 하나로 나간다.- CLI 는 재시도하지 않는다. 재시도 여부는 호출자가
errorCode를 보고 판단한다 (예:cluster_tunnel_disconnected는 잠시 뒤 다시,permission_forbidden은 재시도 무의미).
개발
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/pytest
.venv/bin/ruff check .
CI 게이트는 .github/workflows/python-services.yml(Tier 0 — ruff hard · pytest hard).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file oceanctl-1.1.0.tar.gz.
File metadata
- Download URL: oceanctl-1.1.0.tar.gz
- Upload date:
- Size: 80.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06ae99f8848352fb3cabc4a34cd1af23ee834b0f3a8fe992f9e4db2dfcf0b470
|
|
| MD5 |
24ae056d256d7bcbdd9fb21f46b1ca77
|
|
| BLAKE2b-256 |
77df4361186780ea2486b4e9baedf797c7e94601223b7f25bf6c9d563f7a964d
|
Provenance
The following attestation bundles were made for oceanctl-1.1.0.tar.gz:
Publisher:
release-please.yml on AI-Ocean/ocean-all
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oceanctl-1.1.0.tar.gz -
Subject digest:
06ae99f8848352fb3cabc4a34cd1af23ee834b0f3a8fe992f9e4db2dfcf0b470 - Sigstore transparency entry: 2291125032
- Sigstore integration time:
-
Permalink:
AI-Ocean/ocean-all@5a2569a6c1bc950340646af2f02f126d89e441e6 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/AI-Ocean
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
release-please.yml@5a2569a6c1bc950340646af2f02f126d89e441e6 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file oceanctl-1.1.0-py3-none-any.whl.
File metadata
- Download URL: oceanctl-1.1.0-py3-none-any.whl
- Upload date:
- Size: 58.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
18628f62e11a8f5c7f782e7999d31b44f8220ba9d14037df5b225683901cead9
|
|
| MD5 |
05649d6e64cab71cc77c40267db7dbd6
|
|
| BLAKE2b-256 |
1f238c5dce660b93acfa4d456507dd36bc2e025bc22bd39fd5c1a0fdbd6d41c5
|
Provenance
The following attestation bundles were made for oceanctl-1.1.0-py3-none-any.whl:
Publisher:
release-please.yml on AI-Ocean/ocean-all
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oceanctl-1.1.0-py3-none-any.whl -
Subject digest:
18628f62e11a8f5c7f782e7999d31b44f8220ba9d14037df5b225683901cead9 - Sigstore transparency entry: 2291125287
- Sigstore integration time:
-
Permalink:
AI-Ocean/ocean-all@5a2569a6c1bc950340646af2f02f126d89e441e6 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/AI-Ocean
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
release-please.yml@5a2569a6c1bc950340646af2f02f126d89e441e6 -
Trigger Event:
workflow_dispatch
-
Statement type: