Skip to main content

oceanctl — Ocean CLI

터미널에서 Ocean 에 작업을 제출하고 상태를 확인한다. AI 에이전트가 쓰는 것을 상한으로 설계했다.

라이선스: 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/credentials0600 으로 저장된다.
  • 토큰을 환경변수나 플래그로 주지 않는다. 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 instance delete --pod-uid UID --yes   인스턴스 삭제(본인 것만 · 1.2.0+ — 아래 참조)
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 는 1.2.0 에서 열렸다(D4 2026-07-31 개정 — Running 인스턴스는 할당량을 계속 점유하는데 정리가 웹뿐이면 에이전트 루프가 닫히지 않아서다). 본인 것만 지워진다 (남의 podUid 는 404). --pod-uidinstance create --json 요약 또는 instance list --jsonpodUid 그 값이다. 확인 게이트: 비대화형/--json--yes 필수(없으면 confirmation_required 봉투), TTY 는 y/N(취소 = delete_cancelled · exit 1 · 요청 0건).

stop 은 없고, connect/ssh 래퍼도 없다(D5) — instance listsshCommand 를 그대로 보여주고 거기서 멈춘다. 감싸면 포트포워딩·키·에이전트 전달까지 떠안게 되고, 그건 ssh 클라이언트가 이미 하는 일이다. job delete·cancel 도 여전히 없다(D4 는 잡에 그대로) — 끝난 잡은 할당량을 먹지 않고 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 을 보내면 403 cli_token_show_all_forbidden). 웹 관리자 화면은 그대로 조직 전체를 본다. 능력이 아니라 자격증명의 수명 때문이다: 웹 세션은 24시간 쿠키인데 이 토큰은 만료가 없고 파일에 있어, 유출 시 반경을 그 사람 것으로 묶어 두는 편이 안전하다.

★단서 하나(P6 실측, [[OD-113]]): job logs·job whyshowAll 이 아니라 조직 역할로 범위가 정해진다 — 조직 관리자 토큰은 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 에 넣을 값이다.
  • TYPEPUBLIC 은 모두가 쓰는 것, 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 listWRITE=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 quotaFREE 를 먼저 보라.
  • --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 whyinsufficient_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 는 --jsonerror.details.podUids 에 온다 — 사람용 출력은 메시지와 코드만 낸다. oceanctl job listPOD 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 whynull 을 줄 수 있다 — 파드가 대기 중이 아니면(이미 스케줄됐거나 끝났으면) 에러가 아니라 {"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·email 이 최상위 → user.name·email). whoamiGET /api/users/me 대신 토큰 검증 라우트를 쓰게 되면서다. 사용자가 owner 뿐이라 그때는 무해했지만, 다음 변경은 계약 변경이다.

--json서버 응답 원형이다. 사람용 표의 정렬·단위·- 채움은 봉투에 들어가지 않으므로, 사람용 출력을 손봐도 에이전트 파서가 깨지지 않는다.

  • 필수 플래그가 빠지면 exit 1 + 봉투다(missing_required_flags) — 어떤 플래그가 빠졌는지 메시지에 이름으로 들어 있다. 예전에는 argparse 사용법이 exit 2 로 나갔는데, -f 와 양자택일이 되면서 검사를 코드로 옮겼고 그 편이 "exit code 는 0/1" 계약에도 맞다.
  • 파싱 오류도 exit 1 + 봉투다(invalid_arguments, 1.1.1+) — 모르는 플래그(--nope), 숫자여야 하는 자리에 문자(--machine-type abc), 없는 서브커맨드. 메시지가 oceanctl job submit: … 처럼 어느 명령의 인자가 틀렸는지를 접두로 알려 준다. (1.1.0 까지는 이 부류만 argparse 가 exit 2 + stderr 로 죽었다 — 외부 에이전트 도그푸딩이 잡은 세 번째 실패 모양이었고, 이제 실패 모양은 봉투 하나다. --help/--version 은 그대로 exit 0.)
  • 파괴적 명령의 확인 게이트도 봉투다(1.2.0+): instance delete--yes 없이 --json/비대화형으로 부르면 confirmation_required(요청은 서버에 안 나감 — --yes 붙여 재시도), TTY 에서 취소하면 delete_cancelled(역시 요청 0건 · exit 1 — 아무것도 안 지웠는데 0 이면 스크립트가 성공으로 읽기 때문).
  • 성공/실패는 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

oceanctl-1.2.1.tar.gz (85.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oceanctl-1.2.1-py3-none-any.whl (62.2 kB view details)

Uploaded Python 3

File details

Details for the file oceanctl-1.2.1.tar.gz.

File metadata

  • Download URL: oceanctl-1.2.1.tar.gz
  • Upload date:
  • Size: 85.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for oceanctl-1.2.1.tar.gz
Algorithm Hash digest
SHA256 e64e5a16e3354d6ddb33c17ac3b03028298324028bfe4624739434c92aeac7ee
MD5 a7a29df4dedb40d9dd5264cc1d656a4c
BLAKE2b-256 ee616e3a300fffe8d4364a03574129a6e07c414eaca991d2febe4660dd354c2b

See more details on using hashes here.

Provenance

The following attestation bundles were made for oceanctl-1.2.1.tar.gz:

Publisher: release-please.yml on AI-Ocean/ocean-all

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oceanctl-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: oceanctl-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 62.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for oceanctl-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9d81fce052fd5cb3950f628e633deaca9cdfac6e1859a9b0d6a5328895548faa
MD5 ffa004aeff1c9e0cc8387725436fc61e
BLAKE2b-256 3401a68736563a35b5504c6506a8804e7add7020de0b83573fc58c7387123fc5

See more details on using hashes here.

Provenance

The following attestation bundles were made for oceanctl-1.2.1-py3-none-any.whl:

Publisher: release-please.yml on AI-Ocean/ocean-all

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

4.1.1

2 files

4.1.0

2 files

4.0.1

2 files

4.0.0

2 files

3.0.1

2 files

3.0.0

2 files

2.0.1

2 files

2.0.0

2 files

1.2.2

2 files

This release

1.2.1 This release

2 files

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page