@bifos/nhncloud-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -195,6 +195,16 @@ nhncloud instance create \
195
195
  --boot-volume-size 30 \
196
196
  --wait
197
197
 
198
+ # cloud-init user-data 주입 (부팅 시 자동 셋업 — NVIDIA 드라이버·docker 등)
199
+ nhncloud instance create \
200
+ --name gpu-runner \
201
+ --flavor <gpu-flavor-id> \
202
+ --image <image-id> \
203
+ --network <network-uuid> \
204
+ --boot-volume-size 30 \
205
+ --user-data ./cloud-init.yaml \
206
+ --wait
207
+
198
208
  # --quiet --wait: 첫 IP 한 줄만 stdout (CI 파이프용)
199
209
  IP=$(nhncloud instance create --name ci-runner \
200
210
  --flavor <flavor-id> --image <image-id> --network <network-uuid> \
package/dist/index.js CHANGED
@@ -1310,6 +1310,7 @@ var InstanceClient = class {
1310
1310
  /**
1311
1311
  * 인스턴스를 생성한다 (POST /servers).
1312
1312
  * NHN 확장 필드(ephemeralDiskSize / protect)는 정의됐을 때만 payload 에 포함한다.
1313
+ * userDataBase64 도 정의됐을 때만 `user_data` 로 포함한다 (인코딩은 command 단에서 완료).
1313
1314
  */
1314
1315
  async create(params) {
1315
1316
  const url = `${this.computeEndpoint}/servers`;
@@ -1344,6 +1345,9 @@ var InstanceClient = class {
1344
1345
  if (params.protect !== void 0) {
1345
1346
  serverBody["NHN-EXT-ATTR:protect"] = params.protect;
1346
1347
  }
1348
+ if (params.userDataBase64 !== void 0) {
1349
+ serverBody["user_data"] = params.userDataBase64;
1350
+ }
1347
1351
  let raw;
1348
1352
  try {
1349
1353
  raw = await import_ky6.default.post(url, {
@@ -1480,6 +1484,7 @@ var getCommand = new import_commander8.Command("get").description("\uB2E8\uC77C
1480
1484
 
1481
1485
  // src/commands/instance/create.ts
1482
1486
  var import_commander9 = require("commander");
1487
+ var import_node_fs = require("fs");
1483
1488
  var import_chalk2 = __toESM(require("chalk"));
1484
1489
  function getFirstIp(server) {
1485
1490
  for (const list of Object.values(server.addresses)) {
@@ -1495,12 +1500,45 @@ function getIps3(server) {
1495
1500
  function getImageId2(server) {
1496
1501
  return typeof server.image === "object" ? server.image.id : "";
1497
1502
  }
1498
- var createCommand = new import_commander9.Command("create").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD55C\uB2E4").requiredOption("--name <name>", "\uC778\uC2A4\uD134\uC2A4 \uC774\uB984").requiredOption("--flavor <id>", "flavor ID").requiredOption("--image <id>", "\uC774\uBBF8\uC9C0 ID").requiredOption("--network <uuid>", "\uB124\uD2B8\uC6CC\uD06C UUID (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--boot-volume-size <gb>", "boot-from-volume root \uBCFC\uB968 \uD06C\uAE30 (GB). GPU(g2) \uB4F1 boot-from-volume \uD544\uC218 flavor \uC5D0 \uC9C0\uC815").option("--key-name <name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--security-group <name>", "\uBCF4\uC548 \uADF8\uB8F9 \uC774\uB984 (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--ephemeral-disk-size <gb>", "\uC784\uC2DC \uB514\uC2A4\uD06C \uD06C\uAE30 (GB, NHN \uD655\uC7A5)").option("--protect", "\uC0AD\uC81C \uBC29\uC9C0 \uC124\uC815 (NHN \uD655\uC7A5)").option("--wait", "ACTIVE \uC0C1\uD0DC\uAC00 \uB420 \uB54C\uAE4C\uC9C0 \uB300\uAE30").option("--timeout <sec>", "wait \uD0C0\uC784\uC544\uC6C3 (\uCD08, \uAE30\uBCF8 300)", "300").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
1503
+ var createCommand = new import_commander9.Command("create").description("\uC778\uC2A4\uD134\uC2A4\uB97C \uC0DD\uC131\uD55C\uB2E4").requiredOption("--name <name>", "\uC778\uC2A4\uD134\uC2A4 \uC774\uB984").requiredOption("--flavor <id>", "flavor ID").requiredOption("--image <id>", "\uC774\uBBF8\uC9C0 ID").requiredOption("--network <uuid>", "\uB124\uD2B8\uC6CC\uD06C UUID (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--boot-volume-size <gb>", "boot-from-volume root \uBCFC\uB968 \uD06C\uAE30 (GB). GPU(g2) \uB4F1 boot-from-volume \uD544\uC218 flavor \uC5D0 \uC9C0\uC815").option("--key-name <name>", "\uD0A4\uD398\uC5B4 \uC774\uB984").option("--security-group <name>", "\uBCF4\uC548 \uADF8\uB8F9 \uC774\uB984 (\uC5EC\uB7EC \uAC1C: \uBC18\uBCF5 \uC9C0\uC815)", (v, prev) => [...prev, v], []).option("--ephemeral-disk-size <gb>", "\uC784\uC2DC \uB514\uC2A4\uD06C \uD06C\uAE30 (GB, NHN \uD655\uC7A5)").option("--protect", "\uC0AD\uC81C \uBC29\uC9C0 \uC124\uC815 (NHN \uD655\uC7A5)").option("--user-data <path>", "cloud-init user-data \uD30C\uC77C \uACBD\uB85C (base64 \uC778\uCF54\uB529\uD574 \uC8FC\uC785, \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4)").option("--wait", "ACTIVE \uC0C1\uD0DC\uAC00 \uB420 \uB54C\uAE4C\uC9C0 \uB300\uAE30").option("--timeout <sec>", "wait \uD0C0\uC784\uC544\uC6C3 (\uCD08, \uAE30\uBCF8 300)", "300").option("--region <region>", "region override (\uAE30\uBCF8: iaas \uC790\uACA9\uC99D\uBA85\uC758 region)").option("--profile <name>", "\uC0AC\uC6A9\uD560 profile \uC774\uB984").action(async (_opts, cmd) => {
1499
1504
  const opts = cmd.optsWithGlobals();
1500
1505
  const networks = opts.network ?? [];
1501
1506
  if (networks.length === 0) {
1502
1507
  throw new NhnCloudCliError("--network \uB294 \uCD5C\uC18C 1\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.", EXIT_PARAM_ERROR);
1503
1508
  }
1509
+ let userDataBase64;
1510
+ if (opts.userData !== void 0) {
1511
+ let stat;
1512
+ try {
1513
+ stat = (0, import_node_fs.statSync)(opts.userData);
1514
+ } catch (e) {
1515
+ const reason = e.code ?? (e instanceof Error ? e.message : String(e));
1516
+ throw new NhnCloudCliError(
1517
+ `--user-data \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${opts.userData} (${reason})`,
1518
+ EXIT_PARAM_ERROR
1519
+ );
1520
+ }
1521
+ if (!stat.isFile()) {
1522
+ throw new NhnCloudCliError(
1523
+ `--user-data \uAC00 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${opts.userData}`,
1524
+ EXIT_PARAM_ERROR
1525
+ );
1526
+ }
1527
+ if (stat.size > 49149) {
1528
+ throw new NhnCloudCliError(
1529
+ `--user-data \uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${stat.size} \uBC14\uC774\uD2B8). base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8 \uD55C\uB3C4\uB77C raw 49149 \uBC14\uC774\uD2B8\uAE4C\uC9C0\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.`,
1530
+ EXIT_PARAM_ERROR
1531
+ );
1532
+ }
1533
+ const raw = (0, import_node_fs.readFileSync)(opts.userData);
1534
+ userDataBase64 = raw.toString("base64");
1535
+ if (userDataBase64.length > 65535) {
1536
+ throw new NhnCloudCliError(
1537
+ `--user-data \uAC00 base64 \uC778\uCF54\uB529 \uD6C4 65535 \uBC14\uC774\uD2B8\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4 (${userDataBase64.length} \uBC14\uC774\uD2B8). cloud-init \uB0B4\uC6A9\uC744 \uC904\uC774\uC138\uC694.`,
1538
+ EXIT_PARAM_ERROR
1539
+ );
1540
+ }
1541
+ }
1504
1542
  const timeoutMs = parseInt(opts.timeout ?? "300", 10) * 1e3;
1505
1543
  const { client } = await resolveInstanceClient(opts);
1506
1544
  startSpinner("\uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131 \uC911...");
@@ -1515,7 +1553,8 @@ var createCommand = new import_commander9.Command("create").description("\uC778\
1515
1553
  keyName: opts.keyName,
1516
1554
  securityGroups: opts.securityGroup && opts.securityGroup.length > 0 ? opts.securityGroup : void 0,
1517
1555
  ephemeralDiskSize: opts.ephemeralDiskSize !== void 0 ? parseInt(opts.ephemeralDiskSize, 10) : void 0,
1518
- protect: opts.protect
1556
+ protect: opts.protect,
1557
+ userDataBase64
1519
1558
  });
1520
1559
  } catch (err) {
1521
1560
  stopSpinner(false);
@@ -1595,7 +1634,7 @@ var deleteCommand = new import_commander10.Command("delete").description("\uC778
1595
1634
 
1596
1635
  // src/index.ts
1597
1636
  var program = new import_commander11.Command();
1598
- program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.2.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
1637
+ program.name("nhncloud").description("NHN Cloud CLI \u2014 AI agent & terminal friendly").version("0.3.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "\uCD5C\uC18C \uCD9C\uB825 (\uC790\uB3D9\uD654\uC6A9)").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
1599
1638
  program.hook("preAction", () => {
1600
1639
  const opts = program.opts();
1601
1640
  if (!opts.color || process.env["NO_COLOR"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifos/nhncloud-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for NHN Cloud services — AI agent & terminal friendly",
5
5
  "keywords": [
6
6
  "nhncloud",
@@ -326,6 +326,7 @@ NHNCLOUD_IAAS_PASSWORD=<api-password> nhncloud configure \
326
326
  | `--security-group <name>` | 아니오 | 보안 그룹 이름 (반복 지정) |
327
327
  | `--ephemeral-disk-size <gb>` | 아니오 | 임시 디스크 크기(GB, NHN 확장) |
328
328
  | `--protect` | 아니오 | 삭제 방지 설정 (NHN 확장) |
329
+ | `--user-data <path>` | 아니오 | cloud-init user-data 파일 경로. base64 인코딩해 `user_data` 주입 (인코딩 후 65535 바이트 한도, 초과 시 입력 오류). 부팅 시 드라이버·패키지 자동 셋업에 사용 |
329
330
  | `--wait` | 아니오 | ACTIVE 상태 + IP 할당까지 대기 |
330
331
  | `--timeout <sec>` | 아니오 | wait 타임아웃 (초, 기본 300) |
331
332
  | `--region <region>` | 아니오 | region override (kr1/kr2/kr3/jp1) |
@@ -356,6 +357,16 @@ nhncloud instance delete "$INSTANCE_ID" --yes
356
357
 
357
358
  # 4. 목록에서 id 만 추출
358
359
  nhncloud instance list --quiet
360
+
361
+ # 5. cloud-init 으로 부팅 시 셋업 자동화 (일회성 GPU CI 러너)
362
+ nhncloud instance create \
363
+ --name gpu-ci \
364
+ --flavor <gpu-flavor-id> \
365
+ --image <image-id> \
366
+ --network <network-uuid> \
367
+ --boot-volume-size 30 \
368
+ --user-data ./setup-nvidia-docker.yaml \
369
+ --wait --quiet
359
370
  ```
360
371
 
361
372
  ### instance 에러 코드