ibee-cli 0.4.1__tar.gz → 0.4.2__tar.gz

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.
Files changed (45) hide show
  1. {ibee_cli-0.4.1/src/ibee_cli.egg-info → ibee_cli-0.4.2}/PKG-INFO +17 -28
  2. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/README.md +15 -26
  3. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/pyproject.toml +2 -2
  4. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/__init__.py +1 -1
  5. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/block_storage.py +1 -6
  6. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/buckets.py +4 -4
  7. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/cdn.py +4 -4
  8. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/gpus.py +2 -2
  9. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/load_balancers.py +3 -3
  10. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/networking.py +2 -2
  11. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/reserved_ips.py +12 -20
  12. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/secrets.py +4 -6
  13. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/vm_lifecycle.py +4 -4
  14. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/vms.py +3 -3
  15. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/helpers.py +6 -50
  16. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/main.py +2 -2
  17. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/render.py +1 -1
  18. {ibee_cli-0.4.1 → ibee_cli-0.4.2/src/ibee_cli.egg-info}/PKG-INFO +17 -28
  19. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/requires.txt +1 -1
  20. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_billing_preflight.py +35 -4
  21. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_block_storage_cdn_commands.py +4 -5
  22. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_cli.py +1 -1
  23. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_foundations.py +41 -13
  24. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_networking_parity_commands.py +11 -10
  25. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_object_storage_commands.py +1 -1
  26. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_review_fixes.py +4 -3
  27. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_secret_store_commands.py +6 -6
  28. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_vm_parity_commands.py +3 -3
  29. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/LICENSE +0 -0
  30. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/setup.cfg +0 -0
  31. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/__init__.py +0 -0
  32. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/billing.py +0 -0
  33. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/compute.py +0 -0
  34. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/console.py +0 -0
  35. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/firewalls.py +0 -0
  36. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/ops.py +0 -0
  37. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/context.py +0 -0
  38. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/SOURCES.txt +0 -0
  39. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/dependency_links.txt +0 -0
  40. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/entry_points.txt +0 -0
  41. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/top_level.txt +0 -0
  42. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_cloud_vm_commands.py +0 -0
  43. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_networking_commands.py +0 -0
  44. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_parity_alignment.py +0 -0
  45. {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_vm_lifecycle_commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ibee-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Official command-line interface for the IBEE Solutions cloud platform
5
5
  Author-email: IBEE Solutions <support@ibee.ai>
6
6
  License-Expression: MIT
@@ -13,7 +13,7 @@ Classifier: Environment :: Console
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: ibee<0.5.0,>=0.4.1
16
+ Requires-Dist: ibee<0.5.0,>=0.4.2
17
17
  Requires-Dist: httpx>=0.27
18
18
  Requires-Dist: typer>=0.12
19
19
  Requires-Dist: click>=8.1
@@ -305,22 +305,15 @@ an interactive hidden prompt, or pipe one line to `--password-stdin`.
305
305
  Bucket and VM placement are automatic when `--site-id` is omitted. Use
306
306
  `ibee compute sites` and pass `--site-id` only when placement must be pinned.
307
307
 
308
- Billable creates send one product request. The public gateway performs the
309
- authoritative, fail-closed billing decision before routing; a valid denial is
310
- returned without calling the product service. `ibee billing eligibility`
311
- remains available as an optional point-in-time preview, and the global
312
- `--check-billing` flag (or `IBEE_CHECK_BILLING=1`) runs the portal's preflight
313
- before a billable create: the create is sent only when billing answers
314
- `allowed: true`, otherwise the portal's explanation is printed (with top-up
315
- guidance when adding credits in the portal can resolve it) and the command
316
- exits 1. The preflight never reserves funds.
317
-
318
- For Cloud/GPU VM creates, `--check-billing` / `--preflight-billing` checks Billing's
319
- account status only. No SKU-only monthly probe or client-calculated price is
320
- sent. The selected catalog term goes to the create API, where the upstream
321
- catalog quote and Billing decide affordability before provisioning. A passing
322
- preflight is not purchase approval. CLI 0.4.1 requires Python SDK 0.4.1 or later;
323
- the CLI delegates to it and does not implement its own pricing policy.
308
+ All product writes reach the upstream API for fresh Billing and lifecycle decisions.
309
+ The CLI never queries billing eligibility or calculates an admission estimate automatically.
310
+ Legacy `--check-billing`, `--preflight-billing`, NAT `--preflight`,
311
+ `--billing-check/--no-billing-check`, and `IBEE_CHECK_BILLING` are deprecated no-ops.
312
+ Structural, workspace, state, scope and destructive-action checks remain in place.
313
+ `ibee billing eligibility` is an explicit diagnostic query: `allowed: false` is data (exit 0);
314
+ the explicit `--require` option retains exit 1 on denial.
315
+ It supports `REVOKE_CREDENTIAL` and `SECURITY_RECOVERY`.
316
+ Upstream errors retain their classification; suspension messages do not infer who suspended an organization.
324
317
 
325
318
  Load balancers take backends, routing, policy, health checks and L7 rules either
326
319
  as flags (`--backend TYPE:TARGET:PORT[:WEIGHT][:tls]`, `--rule
@@ -413,7 +406,7 @@ on) controls the read-only checks that need the current state.
413
406
  - **NAT gateways** exist only in `nat_gateway` VPCs (one per VPC; `nat create`
414
407
  prints the existing one instead of sending a create). A Reserved IP must be in
415
408
  the VPC's site, unattached and `reserved`. `--preflight` (or `--check-billing`)
416
- checks NAT-GATEWAY eligibility first. `nat delete` shows how many NAT VMs lose
409
+ is a deprecated no-op; upstream decides admission. `nat delete` shows how many NAT VMs lose
417
410
  outbound internet and how many forwarding rules are deleted before it asks;
418
411
  `--ip-action reserve` of a platform-assigned IP needs `--billing-catalog` (the
419
412
  RESERVED-IP SKU); deletion is refused while a virtual IP holds a Reserved IP.
@@ -429,8 +422,8 @@ on) controls the read-only checks that need the current state.
429
422
  253 characters (`--reverse-dns ""` clears it); `release` is refused while
430
423
  attached; `attach` is for unattached IPs (`move` for an IP on another VM,
431
424
  `--detach-from-service` for one on a NAT gateway or virtual IP); an attach to a VM
432
- outside a VPC points at `reserved-ips convert`. `convert` runs the RESERVED-IP
433
- billing check first (`--no-billing-check` skips it; it needs `billing.read`).
425
+ outside a VPC points at `reserved-ips convert`. The API decides admission for
426
+ `convert`.
434
427
  - **Firewalls**: group names are unique in any case (1-120 characters) and
435
428
  customers cannot create default groups. Rules: tcp and udp need `--port` (22 or
436
429
  8000-8080), icmp and any take none; sources are IPv4 addresses or CIDRs (a bare
@@ -495,8 +488,7 @@ for confirmation, the read-only checks run first (`--no-check-state` skips them)
495
488
  timeout). `purge` needs `--mode`; `--mode all` asks first; only the selector for the
496
489
  mode is accepted (`--path` 1-30, https URLs only; `--hostname`, `--tag`, `--prefix`
497
490
  1-100). A purge the CDN reports as failed exits 1.
498
- - `--check-billing` (or `--preflight-billing`) checks OBJECTST-STD before a bucket or
499
- S3 credential create and CUSTOMDO-STD before a custom domain.
491
+ - `--check-billing` and `--preflight-billing` are deprecated no-ops for storage and CDN.
500
492
  - `block-storage create --vm-type`/`--delete-on-termination`, the Block Storage delete
501
493
  idempotency key, `cdn cache-policies` and `cdn metrics` are not yet part of the
502
494
  published API contract; behaviour may change. Block Storage plans, bucket emptying,
@@ -527,10 +519,7 @@ inputs first.
527
519
  values are refused. In `patch-value`, a JSON `null` deletes that key. `--cas` is an
528
520
  integer >= 0; a check-and-set mismatch is reported as a conflict and never retried.
529
521
  `list` sends a trimmed `--query` (at most 128 characters) only when it is not blank.
530
- - **Billing**: `stores create` and `secrets create` check SECRETMA-STD eligibility
531
- first, as the portal does (`--no-billing-check` turns it off; the global
532
- `--check-billing` forces it on). When the token lacks `billing.read` the check is
533
- skipped with a warning, because the API still enforces billing on the create.
522
+ - **Billing**: store and secret writes use upstream admission. Both legacy billing-check flags are no-ops.
534
523
  - **Batch create**: every item follows the `secrets create` rules and the error names
535
524
  its index. Duplicate names are reported (the API skips them). A file with more than
536
525
  500 secrets or over 64 KiB is sent as consecutive requests and the results are
@@ -571,7 +560,7 @@ inputs first.
571
560
  | `-o`, `--output` | `IBEE_OUTPUT` | `table`, `json`, `yaml` or `id` |
572
561
  | `--json` | | Same as `-o json` |
573
562
  | `--yes`, `-y` | `IBEE_ASSUME_YES=1` | Answer yes to every confirmation |
574
- | `--check-billing` | `IBEE_CHECK_BILLING=1` | Billing preflight before billable creates |
563
+ | `--check-billing` | `IBEE_CHECK_BILLING=1` | Deprecated no-op; upstream decides admission |
575
564
 
576
565
  Without `-o`, each command keeps its usual format (tables for most lists, JSON
577
566
  for single resources and operation results). `-o yaml` prints YAML and `-o id`
@@ -280,22 +280,15 @@ an interactive hidden prompt, or pipe one line to `--password-stdin`.
280
280
  Bucket and VM placement are automatic when `--site-id` is omitted. Use
281
281
  `ibee compute sites` and pass `--site-id` only when placement must be pinned.
282
282
 
283
- Billable creates send one product request. The public gateway performs the
284
- authoritative, fail-closed billing decision before routing; a valid denial is
285
- returned without calling the product service. `ibee billing eligibility`
286
- remains available as an optional point-in-time preview, and the global
287
- `--check-billing` flag (or `IBEE_CHECK_BILLING=1`) runs the portal's preflight
288
- before a billable create: the create is sent only when billing answers
289
- `allowed: true`, otherwise the portal's explanation is printed (with top-up
290
- guidance when adding credits in the portal can resolve it) and the command
291
- exits 1. The preflight never reserves funds.
292
-
293
- For Cloud/GPU VM creates, `--check-billing` / `--preflight-billing` checks Billing's
294
- account status only. No SKU-only monthly probe or client-calculated price is
295
- sent. The selected catalog term goes to the create API, where the upstream
296
- catalog quote and Billing decide affordability before provisioning. A passing
297
- preflight is not purchase approval. CLI 0.4.1 requires Python SDK 0.4.1 or later;
298
- the CLI delegates to it and does not implement its own pricing policy.
283
+ All product writes reach the upstream API for fresh Billing and lifecycle decisions.
284
+ The CLI never queries billing eligibility or calculates an admission estimate automatically.
285
+ Legacy `--check-billing`, `--preflight-billing`, NAT `--preflight`,
286
+ `--billing-check/--no-billing-check`, and `IBEE_CHECK_BILLING` are deprecated no-ops.
287
+ Structural, workspace, state, scope and destructive-action checks remain in place.
288
+ `ibee billing eligibility` is an explicit diagnostic query: `allowed: false` is data (exit 0);
289
+ the explicit `--require` option retains exit 1 on denial.
290
+ It supports `REVOKE_CREDENTIAL` and `SECURITY_RECOVERY`.
291
+ Upstream errors retain their classification; suspension messages do not infer who suspended an organization.
299
292
 
300
293
  Load balancers take backends, routing, policy, health checks and L7 rules either
301
294
  as flags (`--backend TYPE:TARGET:PORT[:WEIGHT][:tls]`, `--rule
@@ -388,7 +381,7 @@ on) controls the read-only checks that need the current state.
388
381
  - **NAT gateways** exist only in `nat_gateway` VPCs (one per VPC; `nat create`
389
382
  prints the existing one instead of sending a create). A Reserved IP must be in
390
383
  the VPC's site, unattached and `reserved`. `--preflight` (or `--check-billing`)
391
- checks NAT-GATEWAY eligibility first. `nat delete` shows how many NAT VMs lose
384
+ is a deprecated no-op; upstream decides admission. `nat delete` shows how many NAT VMs lose
392
385
  outbound internet and how many forwarding rules are deleted before it asks;
393
386
  `--ip-action reserve` of a platform-assigned IP needs `--billing-catalog` (the
394
387
  RESERVED-IP SKU); deletion is refused while a virtual IP holds a Reserved IP.
@@ -404,8 +397,8 @@ on) controls the read-only checks that need the current state.
404
397
  253 characters (`--reverse-dns ""` clears it); `release` is refused while
405
398
  attached; `attach` is for unattached IPs (`move` for an IP on another VM,
406
399
  `--detach-from-service` for one on a NAT gateway or virtual IP); an attach to a VM
407
- outside a VPC points at `reserved-ips convert`. `convert` runs the RESERVED-IP
408
- billing check first (`--no-billing-check` skips it; it needs `billing.read`).
400
+ outside a VPC points at `reserved-ips convert`. The API decides admission for
401
+ `convert`.
409
402
  - **Firewalls**: group names are unique in any case (1-120 characters) and
410
403
  customers cannot create default groups. Rules: tcp and udp need `--port` (22 or
411
404
  8000-8080), icmp and any take none; sources are IPv4 addresses or CIDRs (a bare
@@ -470,8 +463,7 @@ for confirmation, the read-only checks run first (`--no-check-state` skips them)
470
463
  timeout). `purge` needs `--mode`; `--mode all` asks first; only the selector for the
471
464
  mode is accepted (`--path` 1-30, https URLs only; `--hostname`, `--tag`, `--prefix`
472
465
  1-100). A purge the CDN reports as failed exits 1.
473
- - `--check-billing` (or `--preflight-billing`) checks OBJECTST-STD before a bucket or
474
- S3 credential create and CUSTOMDO-STD before a custom domain.
466
+ - `--check-billing` and `--preflight-billing` are deprecated no-ops for storage and CDN.
475
467
  - `block-storage create --vm-type`/`--delete-on-termination`, the Block Storage delete
476
468
  idempotency key, `cdn cache-policies` and `cdn metrics` are not yet part of the
477
469
  published API contract; behaviour may change. Block Storage plans, bucket emptying,
@@ -502,10 +494,7 @@ inputs first.
502
494
  values are refused. In `patch-value`, a JSON `null` deletes that key. `--cas` is an
503
495
  integer >= 0; a check-and-set mismatch is reported as a conflict and never retried.
504
496
  `list` sends a trimmed `--query` (at most 128 characters) only when it is not blank.
505
- - **Billing**: `stores create` and `secrets create` check SECRETMA-STD eligibility
506
- first, as the portal does (`--no-billing-check` turns it off; the global
507
- `--check-billing` forces it on). When the token lacks `billing.read` the check is
508
- skipped with a warning, because the API still enforces billing on the create.
497
+ - **Billing**: store and secret writes use upstream admission. Both legacy billing-check flags are no-ops.
509
498
  - **Batch create**: every item follows the `secrets create` rules and the error names
510
499
  its index. Duplicate names are reported (the API skips them). A file with more than
511
500
  500 secrets or over 64 KiB is sent as consecutive requests and the results are
@@ -546,7 +535,7 @@ inputs first.
546
535
  | `-o`, `--output` | `IBEE_OUTPUT` | `table`, `json`, `yaml` or `id` |
547
536
  | `--json` | | Same as `-o json` |
548
537
  | `--yes`, `-y` | `IBEE_ASSUME_YES=1` | Answer yes to every confirmation |
549
- | `--check-billing` | `IBEE_CHECK_BILLING=1` | Billing preflight before billable creates |
538
+ | `--check-billing` | `IBEE_CHECK_BILLING=1` | Deprecated no-op; upstream decides admission |
550
539
 
551
540
  Without `-o`, each command keeps its usual format (tables for most lists, JSON
552
541
  for single resources and operation results). `-o yaml` prints YAML and `-o id`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ibee-cli"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Official command-line interface for the IBEE Solutions cloud platform"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Environment :: Console",
19
19
  ]
20
20
  dependencies = [
21
- "ibee>=0.4.1,<0.5.0",
21
+ "ibee>=0.4.2,<0.5.0",
22
22
  "httpx>=0.27",
23
23
  "typer>=0.12",
24
24
  "click>=8.1",
@@ -1,3 +1,3 @@
1
1
  """IBEE Solutions command-line interface."""
2
2
 
3
- __version__ = "0.4.1"
3
+ __version__ = "0.4.2"
@@ -47,7 +47,6 @@ from ..helpers import (
47
47
  idempotency_key_option,
48
48
  load_json_input,
49
49
  poll_interval_option,
50
- preflight_billing,
51
50
  resolve_idempotency_key,
52
51
  resolve_wait,
53
52
  timeout_option,
@@ -264,7 +263,7 @@ def create_volume(
264
263
 
265
264
  settings, workspace, client = _session(ctx)
266
265
  key = resolve_idempotency_key(idempotency_key, "block-create", name)
267
- # Validate first (exit 2) so a bad request never reaches the billing preflight.
266
+ # Validate first (exit 2) so a bad request never reaches the API.
268
267
  body = build_block_volume_create_body(
269
268
  name=name,
270
269
  size_gb=size_gb,
@@ -277,10 +276,6 @@ def create_volume(
277
276
  vm_type=vm_type,
278
277
  delete_on_termination=delete_on_termination,
279
278
  )
280
- if settings.check_billing:
281
- preflight_billing(
282
- settings, client, workspace, sku_code=body.get("sku_code"), resource_type="block_storage"
283
- )
284
279
  try:
285
280
  with api_hints({400: CREATE_PLAN_HINT}):
286
281
  result = client.block_storage.create_block_volume(
@@ -176,7 +176,7 @@ def create_bucket(
176
176
  preflight: bool = typer.Option(
177
177
  False,
178
178
  "--preflight-billing",
179
- help="Check OBJECTST-STD billing eligibility first (same as the global --check-billing)",
179
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
180
180
  ),
181
181
  ) -> None:
182
182
  """Create a bucket."""
@@ -191,7 +191,7 @@ def create_bucket(
191
191
  object_lock_enabled=bucket_lock,
192
192
  default_retention=retention,
193
193
  tags=tag or None,
194
- preflight_billing=preflight or settings.check_billing,
194
+ preflight_billing=False,
195
195
  )
196
196
  print_json(result, id_field="name")
197
197
 
@@ -353,7 +353,7 @@ def create_credential(
353
353
  preflight: bool = typer.Option(
354
354
  False,
355
355
  "--preflight-billing",
356
- help="Check OBJECTST-STD billing eligibility first (same as the global --check-billing)",
356
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
357
357
  ),
358
358
  ) -> None:
359
359
  """Create an S3 access key; its secret is displayed only once.
@@ -368,7 +368,7 @@ def create_credential(
368
368
  permission_type=permission_type,
369
369
  bucket_scope=bucket_scope,
370
370
  allowed_buckets=allowed_bucket or None,
371
- preflight_billing=preflight or settings.check_billing,
371
+ preflight_billing=False,
372
372
  )
373
373
  # Never print only the ID: the one-time secret would be lost. -o id falls back to JSON.
374
374
  print_structured(result, "yaml" if settings.output == "yaml" else "json", id_field="access_key_id")
@@ -152,7 +152,7 @@ def create_distribution(
152
152
  preflight: bool = typer.Option(
153
153
  False,
154
154
  "--preflight-billing",
155
- help="Check billing eligibility first (same as the global --check-billing)",
155
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
156
156
  ),
157
157
  ) -> None:
158
158
  """Create a CDN distribution for a public bucket.
@@ -171,7 +171,7 @@ def create_distribution(
171
171
  origin_type=origin_type,
172
172
  cache_policy=cache_policy,
173
173
  check_origin_public=check_origin,
174
- preflight_billing=preflight or settings.check_billing,
174
+ preflight_billing=False,
175
175
  ))
176
176
 
177
177
 
@@ -327,7 +327,7 @@ def create_domain(
327
327
  preflight: bool = typer.Option(
328
328
  False,
329
329
  "--preflight-billing",
330
- help="Check CUSTOMDO-STD billing eligibility first (same as the global --check-billing)",
330
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
331
331
  ),
332
332
  ) -> None:
333
333
  """Add a custom domain (billed), then print the CNAME record to create."""
@@ -337,7 +337,7 @@ def create_domain(
337
337
  distribution_id,
338
338
  workspace_id=workspace,
339
339
  domain=domain,
340
- preflight_billing=preflight or settings.check_billing,
340
+ preflight_billing=False,
341
341
  )
342
342
  _print(result)
343
343
  if not settings.structured_output:
@@ -151,7 +151,7 @@ def create_gpu_vm(
151
151
  preflight: bool = typer.Option(
152
152
  False,
153
153
  "--preflight-billing",
154
- help="Check Billing account status first; upstream create decides affordability (same as --check-billing)",
154
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
155
155
  ),
156
156
  wait: bool = typer.Option(False, "--wait", help="Poll until the VM is provisioned"),
157
157
  timeout: Optional[float] = timeout_option(),
@@ -223,7 +223,7 @@ def delete_gpu_vm(
223
223
  reserved_ip_billing_catalog_file: Optional[str] = typer.Option(None, "--reserved-ip-billing-catalog-file"),
224
224
  requested_by: Optional[str] = typer.Option(None, "--requested-by"),
225
225
  preflight: bool = typer.Option(
226
- False, "--preflight-billing", help="With --reserve-public-ip, check billing eligibility for the Reserved IP SKU"
226
+ False, "--preflight-billing", help="Deprecated no-op; upstream decides billing and lifecycle admission."
227
227
  ),
228
228
  check_state: bool = check_state_option(),
229
229
  wait: bool = typer.Option(False, "--wait", help="Poll until the VM is deleted"),
@@ -275,7 +275,7 @@ def _create(
275
275
  ) -> None:
276
276
  settings, workspace, client = _session(ctx)
277
277
  method = getattr(client.load_balancers, f"create_{layer}load_balancer")
278
- lb = method(workspace_id=workspace, check_billing=check_billing or settings.check_billing,
278
+ lb = method(workspace_id=workspace, check_billing=False,
279
279
  **{key: value for key, value in fields.items() if value is not None})
280
280
  print_json(lb, id_field=ID_FIELD)
281
281
 
@@ -333,7 +333,7 @@ def create_l4_load_balancer(
333
333
  None, "--tls", help="TLS JSON object (managed certificates only; default for tls_passthrough)"
334
334
  ),
335
335
  check_billing: bool = typer.Option(
336
- False, "--check-billing", help="Check LOADBALA-STD billing eligibility first (same as the global option)"
336
+ False, "--check-billing", help="Deprecated no-op; upstream decides billing and lifecycle admission."
337
337
  ),
338
338
  ) -> None:
339
339
  """Create an L4 load balancer (TCP or TLS passthrough)."""
@@ -402,7 +402,7 @@ def create_l7_load_balancer(
402
402
  ),
403
403
  rules: Optional[str] = typer.Option(None, "--rules", help="Rules JSON array"),
404
404
  check_billing: bool = typer.Option(
405
- False, "--check-billing", help="Check LOADBALA-STD billing eligibility first (same as the global option)"
405
+ False, "--check-billing", help="Deprecated no-op; upstream decides billing and lifecycle admission."
406
406
  ),
407
407
  ) -> None:
408
408
  """Create an L7 load balancer (HTTP or HTTPS)."""
@@ -529,7 +529,7 @@ def create_nat_gateway(
529
529
  ),
530
530
  preflight: bool = typer.Option(
531
531
  False, "--preflight", "--preflight-billing",
532
- help="Check NAT-GATEWAY billing eligibility first (also set by the global --check-billing)",
532
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
533
533
  ),
534
534
  check_state: bool = check_state_option(),
535
535
  ) -> None:
@@ -561,7 +561,7 @@ def create_nat_gateway(
561
561
  reserved_public_ip_id=reserved_public_ip_id,
562
562
  name=name,
563
563
  billing_catalog=catalog,
564
- preflight_billing=preflight or settings.check_billing,
564
+ preflight_billing=False,
565
565
  check_state=False,
566
566
  )
567
567
  print_json(gateway, id_field="nat_gateway_id")
@@ -2,7 +2,7 @@
2
2
 
3
3
  The Python SDK applies the portal's rules: label and reverse-DNS formats, no release
4
4
  while attached, attach versus move, no detach of a converted address that is still
5
- the VM's active IP, and the RESERVED-IP billing preflight for conversions.
5
+ the VM's active IP, and upstream admission for conversions.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -60,7 +60,7 @@ def reserve_ip(
60
60
  None, "--billing-catalog-file", metavar="PATH", help="Read --billing-catalog from a JSON file"
61
61
  ),
62
62
  check_billing: bool = typer.Option(
63
- False, "--check-billing", help="Check RESERVED-IP billing eligibility first (same as the global option)"
63
+ False, "--check-billing", help="Deprecated no-op; upstream decides billing and lifecycle admission."
64
64
  ),
65
65
  ) -> None:
66
66
  """Reserve a new public IP address (billed while reserved)."""
@@ -72,7 +72,7 @@ def reserve_ip(
72
72
  site_id=site_id,
73
73
  label=label,
74
74
  billing_catalog=catalog,
75
- check_billing=check_billing or settings.check_billing,
75
+ check_billing=False,
76
76
  )
77
77
  print_json(reserved, id_field=ID_FIELD)
78
78
 
@@ -93,7 +93,7 @@ def convert_vm_public_ip(
93
93
  billing_check: bool = typer.Option(
94
94
  True,
95
95
  "--billing-check/--no-billing-check",
96
- help="Check RESERVED-IP billing eligibility first (default: on; needs the billing.read scope)",
96
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
97
97
  ),
98
98
  ) -> None:
99
99
  """Keep a VM's current public IPv4 as a Reserved IP (VMs outside a VPC).
@@ -103,22 +103,14 @@ def convert_vm_public_ip(
103
103
 
104
104
  _settings, workspace, client = _session(ctx)
105
105
  catalog = load_json_input(billing_catalog, billing_catalog_file, "billing-catalog")
106
- try:
107
- reserved = client.reserved_ips.convert_vm_public_ip_to_reserved_ip(
108
- workspace_id=workspace,
109
- vm_id=vm_id,
110
- site_id=site_id,
111
- label=label,
112
- billing_catalog=catalog,
113
- billing_check=billing_check,
114
- )
115
- except ForbiddenError as exc:
116
- text = str(getattr(exc, "message", "") or "")
117
- if billing_check and (getattr(exc, "required_scope", None) == "billing.read" or "billing.read" in text):
118
- exc.cli_hint = ( # type: ignore[attr-defined]
119
- "The RESERVED-IP billing check needs the billing.read scope; grant it or pass --no-billing-check."
120
- )
121
- raise
106
+ reserved = client.reserved_ips.convert_vm_public_ip_to_reserved_ip(
107
+ workspace_id=workspace,
108
+ vm_id=vm_id,
109
+ site_id=site_id,
110
+ label=label,
111
+ billing_catalog=catalog,
112
+ billing_check=False,
113
+ )
122
114
  print_json(reserved, id_field=ID_FIELD)
123
115
 
124
116
 
@@ -278,8 +278,7 @@ def create_store(
278
278
  billing_check: bool = typer.Option(
279
279
  True,
280
280
  "--billing-check/--no-billing-check",
281
- help="Check SECRETMA-STD billing eligibility first, as the portal does (default: on). "
282
- "Skipped with a warning when the token lacks billing.read",
281
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
283
282
  ),
284
283
  if_exists: IfExists = typer.Option(
285
284
  IfExists.ERROR,
@@ -299,7 +298,7 @@ def create_store(
299
298
  workspace_id=workspace,
300
299
  name=body["name"],
301
300
  description=body.get("description"),
302
- preflight_billing=bool(billing_check or settings.check_billing),
301
+ preflight_billing=False,
303
302
  )
304
303
  except ConflictError as exc:
305
304
  if if_exists is not IfExists.REUSE or type(exc) is not ConflictError:
@@ -997,8 +996,7 @@ def create_secret(
997
996
  billing_check: bool = typer.Option(
998
997
  True,
999
998
  "--billing-check/--no-billing-check",
1000
- help="Check SECRETMA-STD billing eligibility first, as the portal does (default: on). "
1001
- "Skipped with a warning when the token lacks billing.read",
999
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
1002
1000
  ),
1003
1001
  ) -> None:
1004
1002
  """Create a secret in an active store."""
@@ -1016,7 +1014,7 @@ def create_secret(
1016
1014
  workspace_id=workspace,
1017
1015
  secret_name=secret_name,
1018
1016
  value=secret_value,
1019
- preflight_billing=bool(billing_check or settings.check_billing),
1017
+ preflight_billing=False,
1020
1018
  )
1021
1019
  if settings.structured_output:
1022
1020
  print_json(result)
@@ -398,7 +398,7 @@ def create_vms(
398
398
  ),
399
399
  )
400
400
  if preflight or getattr(settings, "check_billing", False):
401
- common["preflight_billing"] = True
401
+ common["preflight_billing"] = False
402
402
  results = []
403
403
  for vm_name, key in zip(names, keys):
404
404
  try:
@@ -521,7 +521,7 @@ def delete_vm(
521
521
  kwargs = resolve("reserve" if reserve else "release")
522
522
  if preflight or getattr(settings, "check_billing", False):
523
523
  if kwargs.get("public_ip_action") == "reserve":
524
- kwargs["preflight_billing"] = True
524
+ kwargs["preflight_billing"] = False
525
525
  result = _method(resource, spec, "delete")(
526
526
  vm_id=vm_id,
527
527
  workspace_id=workspace,
@@ -1041,7 +1041,7 @@ def register_vm_lifecycle(app: typer.Typer, spec: VmCommandSpec) -> None:
1041
1041
  billing_catalog: Optional[str] = typer.Option(None, "--billing-catalog", help=SNAPSHOT_SKU_HELP),
1042
1042
  billing_catalog_file: Optional[str] = typer.Option(None, "--billing-catalog-file", help="File with the SKU JSON"),
1043
1043
  preflight: bool = typer.Option(
1044
- False, "--preflight-billing", help="Check billing eligibility for the SKU first (needs billing.read)"
1044
+ False, "--preflight-billing", help="Deprecated no-op; upstream decides billing and lifecycle admission."
1045
1045
  ),
1046
1046
  requested_by: Optional[str] = typer.Option(None, "--requested-by"),
1047
1047
  check_state: bool = check_state_option(),
@@ -1065,7 +1065,7 @@ def register_vm_lifecycle(app: typer.Typer, spec: VmCommandSpec) -> None:
1065
1065
  mode=mode,
1066
1066
  selected_data_volume_ids=selected_data_volume_id,
1067
1067
  requested_by=requested_by,
1068
- preflight_billing=True if preflight or getattr(settings, "check_billing", False) else None,
1068
+ preflight_billing=False,
1069
1069
  ),
1070
1070
  )
1071
1071
  snapshot_id = cell(result, "snapshot_set_id") or cell(result, "id")
@@ -103,7 +103,7 @@ def create_vm(
103
103
  ram_mb: Optional[int] = typer.Option(None, "--ram-mb", help="RAM in MB (default: the plan's; must match it)"),
104
104
  disk_gb: Optional[int] = typer.Option(None, "--disk-gb", help="Root disk in GB (default: the plan's; must match it)"),
105
105
  billing_term: Optional[str] = typer.Option(
106
- None, "--billing-term", help="Billing term: " + ", ".join(BILLING_TERMS) + " (default HOURLY, or the plan's first term)"
106
+ None, "--billing-term", help="Billing term sent to the API: " + ", ".join(BILLING_TERMS) + " (default HOURLY, or the plan's first term); admission is decided upstream"
107
107
  ),
108
108
  windows_license: Optional[str] = typer.Option(
109
109
  None,
@@ -150,7 +150,7 @@ def create_vm(
150
150
  preflight: bool = typer.Option(
151
151
  False,
152
152
  "--preflight-billing",
153
- help="Check Billing account status first; upstream create decides affordability (same as --check-billing)",
153
+ help="Deprecated no-op; upstream decides billing and lifecycle admission.",
154
154
  ),
155
155
  wait: bool = typer.Option(False, "--wait", help="Poll until the VM is provisioned"),
156
156
  timeout: Optional[float] = timeout_option(),
@@ -222,7 +222,7 @@ def delete_vm(
222
222
  reserved_ip_billing_catalog_file: Optional[str] = typer.Option(None, "--reserved-ip-billing-catalog-file"),
223
223
  requested_by: Optional[str] = typer.Option(None, "--requested-by"),
224
224
  preflight: bool = typer.Option(
225
- False, "--preflight-billing", help="With --reserve-public-ip, check billing eligibility for the Reserved IP SKU"
225
+ False, "--preflight-billing", help="Deprecated no-op; upstream decides billing and lifecycle admission."
226
226
  ),
227
227
  check_state: bool = check_state_option(),
228
228
  wait: bool = typer.Option(False, "--wait", help="Poll until the VM is deleted"),
@@ -2,8 +2,8 @@
2
2
 
3
3
  Compute writes (VM create/delete/power, access, resize, and volume actions)
4
4
  return `202 + operation_id`; these helpers build idempotency keys, optionally
5
- poll the operation to a terminal state (`--wait`), run the optional billing
6
- preflight (`--check-billing`), ask for confirmation of destructive actions,
5
+ poll the operation to a terminal state (`--wait`), retain deprecated no-op billing
6
+ flags, ask for confirmation of destructive actions,
7
7
  and parse structured values from the command line.
8
8
  """
9
9
 
@@ -549,36 +549,8 @@ def preflight_billing(
549
549
  estimated_cost_minor: Optional[int | float] = None,
550
550
  resource_type: str = "resource",
551
551
  ) -> Any:
552
- """Portal billing preflight before a billable create, when ``--check-billing`` is set.
553
-
554
- Without ``--check-billing`` (or ``IBEE_CHECK_BILLING``) nothing is called, so a
555
- create stays a single request and the API edge alone decides admission. With it,
556
- the create continues only when billing answers ``allowed`` exactly ``true``;
557
- otherwise ``BillingDeniedError`` is raised (printed with the portal wording, exit 1).
558
- """
559
-
560
- if not getattr(settings, "check_billing", False):
561
- return None
562
- billing = getattr(client, "billing", None)
563
- require = getattr(billing, "require_resource_eligibility", None)
564
- if callable(require):
565
- kwargs: dict[str, Any] = {"workspace_id": workspace_id, "resource_type": resource_type}
566
- sku = normalize_sku_code(sku_code)
567
- if sku:
568
- kwargs["sku_code"] = sku
569
- cost = normalize_estimated_cost_minor(estimated_cost_minor)
570
- if cost is not None:
571
- kwargs["estimated_cost_minor"] = cost
572
- return require(**kwargs)
573
- decision = check_billing_eligibility(
574
- client,
575
- workspace_id,
576
- sku_code=sku_code,
577
- estimated_cost_minor=estimated_cost_minor,
578
- )
579
- if _field(decision, "allowed") is not True:
580
- raise BillingDeniedError(decision=decision, create_type=resource_type)
581
- return decision
552
+ """Deprecated compatibility no-op; the mutation endpoint decides admission."""
553
+ return None
582
554
 
583
555
 
584
556
  def preflight_create(
@@ -589,24 +561,8 @@ def preflight_create(
589
561
  sku_code: Optional[str] = None,
590
562
  estimated_cost_minor: Optional[int | float] = None,
591
563
  ) -> Any:
592
- """``preflight_billing`` for a billable create command, building the client on demand.
593
-
594
- Does nothing (and sends nothing) unless ``--check-billing``/``IBEE_CHECK_BILLING`` is set.
595
- """
596
-
597
- if not getattr(settings, "check_billing", False):
598
- return None
599
- from .context import get_client, require_workspace
600
-
601
- workspace = require_workspace(settings)
602
- return preflight_billing(
603
- settings,
604
- client if client is not None else get_client(settings),
605
- workspace,
606
- sku_code=sku_code,
607
- estimated_cost_minor=estimated_cost_minor,
608
- resource_type=resource_type,
609
- )
564
+ """Deprecated compatibility no-op; no client or workspace is resolved."""
565
+ return None
610
566
 
611
567
 
612
568
  # ---------------------------------------------------------------------------
@@ -87,8 +87,8 @@ def main(
87
87
  False,
88
88
  "--check-billing",
89
89
  help=(
90
- "Ask billing before a billable create and stop when it would be denied "
91
- "(env IBEE_CHECK_BILLING=1)"
90
+ "Deprecated no-op; upstream decides mutation admission "
91
+ "(also applies to IBEE_CHECK_BILLING)"
92
92
  ),
93
93
  ),
94
94
  version: bool = typer.Option(
@@ -495,7 +495,7 @@ def _api_error_lines(exc: ApiError) -> list[str]:
495
495
  return [f"Validation failed (422): {message}"]
496
496
  if status == 423 or isinstance(exc, OrganizationSuspendedError):
497
497
  return [
498
- "Organization suspended (423): billing has suspended this organization, "
498
+ "Organization suspended (423): this organization is suspended, "
499
499
  f"so this change is blocked. {message}"
500
500
  ]
501
501
  if status == 429:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ibee-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Official command-line interface for the IBEE Solutions cloud platform
5
5
  Author-email: IBEE Solutions <support@ibee.ai>
6
6
  License-Expression: MIT
@@ -13,7 +13,7 @@ Classifier: Environment :: Console
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: ibee<0.5.0,>=0.4.1
16
+ Requires-Dist: ibee<0.5.0,>=0.4.2
17
17
  Requires-Dist: httpx>=0.27
18
18
  Requires-Dist: typer>=0.12
19
19
  Requires-Dist: click>=8.1
@@ -305,22 +305,15 @@ an interactive hidden prompt, or pipe one line to `--password-stdin`.
305
305
  Bucket and VM placement are automatic when `--site-id` is omitted. Use
306
306
  `ibee compute sites` and pass `--site-id` only when placement must be pinned.
307
307
 
308
- Billable creates send one product request. The public gateway performs the
309
- authoritative, fail-closed billing decision before routing; a valid denial is
310
- returned without calling the product service. `ibee billing eligibility`
311
- remains available as an optional point-in-time preview, and the global
312
- `--check-billing` flag (or `IBEE_CHECK_BILLING=1`) runs the portal's preflight
313
- before a billable create: the create is sent only when billing answers
314
- `allowed: true`, otherwise the portal's explanation is printed (with top-up
315
- guidance when adding credits in the portal can resolve it) and the command
316
- exits 1. The preflight never reserves funds.
317
-
318
- For Cloud/GPU VM creates, `--check-billing` / `--preflight-billing` checks Billing's
319
- account status only. No SKU-only monthly probe or client-calculated price is
320
- sent. The selected catalog term goes to the create API, where the upstream
321
- catalog quote and Billing decide affordability before provisioning. A passing
322
- preflight is not purchase approval. CLI 0.4.1 requires Python SDK 0.4.1 or later;
323
- the CLI delegates to it and does not implement its own pricing policy.
308
+ All product writes reach the upstream API for fresh Billing and lifecycle decisions.
309
+ The CLI never queries billing eligibility or calculates an admission estimate automatically.
310
+ Legacy `--check-billing`, `--preflight-billing`, NAT `--preflight`,
311
+ `--billing-check/--no-billing-check`, and `IBEE_CHECK_BILLING` are deprecated no-ops.
312
+ Structural, workspace, state, scope and destructive-action checks remain in place.
313
+ `ibee billing eligibility` is an explicit diagnostic query: `allowed: false` is data (exit 0);
314
+ the explicit `--require` option retains exit 1 on denial.
315
+ It supports `REVOKE_CREDENTIAL` and `SECURITY_RECOVERY`.
316
+ Upstream errors retain their classification; suspension messages do not infer who suspended an organization.
324
317
 
325
318
  Load balancers take backends, routing, policy, health checks and L7 rules either
326
319
  as flags (`--backend TYPE:TARGET:PORT[:WEIGHT][:tls]`, `--rule
@@ -413,7 +406,7 @@ on) controls the read-only checks that need the current state.
413
406
  - **NAT gateways** exist only in `nat_gateway` VPCs (one per VPC; `nat create`
414
407
  prints the existing one instead of sending a create). A Reserved IP must be in
415
408
  the VPC's site, unattached and `reserved`. `--preflight` (or `--check-billing`)
416
- checks NAT-GATEWAY eligibility first. `nat delete` shows how many NAT VMs lose
409
+ is a deprecated no-op; upstream decides admission. `nat delete` shows how many NAT VMs lose
417
410
  outbound internet and how many forwarding rules are deleted before it asks;
418
411
  `--ip-action reserve` of a platform-assigned IP needs `--billing-catalog` (the
419
412
  RESERVED-IP SKU); deletion is refused while a virtual IP holds a Reserved IP.
@@ -429,8 +422,8 @@ on) controls the read-only checks that need the current state.
429
422
  253 characters (`--reverse-dns ""` clears it); `release` is refused while
430
423
  attached; `attach` is for unattached IPs (`move` for an IP on another VM,
431
424
  `--detach-from-service` for one on a NAT gateway or virtual IP); an attach to a VM
432
- outside a VPC points at `reserved-ips convert`. `convert` runs the RESERVED-IP
433
- billing check first (`--no-billing-check` skips it; it needs `billing.read`).
425
+ outside a VPC points at `reserved-ips convert`. The API decides admission for
426
+ `convert`.
434
427
  - **Firewalls**: group names are unique in any case (1-120 characters) and
435
428
  customers cannot create default groups. Rules: tcp and udp need `--port` (22 or
436
429
  8000-8080), icmp and any take none; sources are IPv4 addresses or CIDRs (a bare
@@ -495,8 +488,7 @@ for confirmation, the read-only checks run first (`--no-check-state` skips them)
495
488
  timeout). `purge` needs `--mode`; `--mode all` asks first; only the selector for the
496
489
  mode is accepted (`--path` 1-30, https URLs only; `--hostname`, `--tag`, `--prefix`
497
490
  1-100). A purge the CDN reports as failed exits 1.
498
- - `--check-billing` (or `--preflight-billing`) checks OBJECTST-STD before a bucket or
499
- S3 credential create and CUSTOMDO-STD before a custom domain.
491
+ - `--check-billing` and `--preflight-billing` are deprecated no-ops for storage and CDN.
500
492
  - `block-storage create --vm-type`/`--delete-on-termination`, the Block Storage delete
501
493
  idempotency key, `cdn cache-policies` and `cdn metrics` are not yet part of the
502
494
  published API contract; behaviour may change. Block Storage plans, bucket emptying,
@@ -527,10 +519,7 @@ inputs first.
527
519
  values are refused. In `patch-value`, a JSON `null` deletes that key. `--cas` is an
528
520
  integer >= 0; a check-and-set mismatch is reported as a conflict and never retried.
529
521
  `list` sends a trimmed `--query` (at most 128 characters) only when it is not blank.
530
- - **Billing**: `stores create` and `secrets create` check SECRETMA-STD eligibility
531
- first, as the portal does (`--no-billing-check` turns it off; the global
532
- `--check-billing` forces it on). When the token lacks `billing.read` the check is
533
- skipped with a warning, because the API still enforces billing on the create.
522
+ - **Billing**: store and secret writes use upstream admission. Both legacy billing-check flags are no-ops.
534
523
  - **Batch create**: every item follows the `secrets create` rules and the error names
535
524
  its index. Duplicate names are reported (the API skips them). A file with more than
536
525
  500 secrets or over 64 KiB is sent as consecutive requests and the results are
@@ -571,7 +560,7 @@ inputs first.
571
560
  | `-o`, `--output` | `IBEE_OUTPUT` | `table`, `json`, `yaml` or `id` |
572
561
  | `--json` | | Same as `-o json` |
573
562
  | `--yes`, `-y` | `IBEE_ASSUME_YES=1` | Answer yes to every confirmation |
574
- | `--check-billing` | `IBEE_CHECK_BILLING=1` | Billing preflight before billable creates |
563
+ | `--check-billing` | `IBEE_CHECK_BILLING=1` | Deprecated no-op; upstream decides admission |
575
564
 
576
565
  Without `-o`, each command keeps its usual format (tables for most lists, JSON
577
566
  for single resources and operation results). `-o yaml` prints YAML and `-o id`
@@ -1,4 +1,4 @@
1
- ibee<0.5.0,>=0.4.1
1
+ ibee<0.5.0,>=0.4.2
2
2
  httpx>=0.27
3
3
  typer>=0.12
4
4
  click>=8.1
@@ -16,6 +16,37 @@ runner = CliRunner()
16
16
  BASE_ARGS = ["--token", "test-token", "--workspace", "973318"]
17
17
 
18
18
 
19
+ @pytest.mark.parametrize("flag", [[], ["--check-billing"]])
20
+ @pytest.mark.parametrize("status,code", [
21
+ (402, "billing_denied"), (403, "organization_restricted"),
22
+ (423, "organization_suspended"), (403, "key_revoked"),
23
+ (403, "workspace_not_allowed"), (403, "insufficient_scope"),
24
+ ])
25
+ def test_mutation_errors_are_upstream_and_suspension_is_source_neutral(gw, flag, status, code):
26
+ gw.on("POST", "secret-store/stores", (status, {
27
+ "error": code, "message": "Admin lifecycle decision",
28
+ "enforcement_source": "ADMIN", "billing_reason": "insufficient_balance",
29
+ "required_scope": "secret_store.write",
30
+ }))
31
+ result = runner.invoke(app, [*BASE_ARGS, *flag, "secrets", "stores", "create", "app"])
32
+ assert result.exit_code == 1, result.output
33
+ assert [c.path for c in gw.writes()] == ["secret-store/stores"]
34
+ if status == 423:
35
+ assert "Organization suspended (423)" in result.output
36
+ assert "billing has suspended" not in result.output.lower()
37
+
38
+
39
+ @pytest.mark.parametrize("operation", ["REVOKE_CREDENTIAL", "SECURITY_RECOVERY"])
40
+ def test_explicit_lifecycle_eligibility_returns_denial_as_data(gw, monkeypatch, operation):
41
+ monkeypatch.setattr(billing, "get_client", lambda _settings: gw.client())
42
+ gw.on("POST", "billing/resource-eligibility", {
43
+ "organization_id": "org", "allowed": False, "reason": "upstream", "operation": operation,
44
+ })
45
+ result = runner.invoke(app, [*BASE_ARGS, "billing", "eligibility", "--operation", operation])
46
+ assert result.exit_code == 0, result.output
47
+ assert gw.last("POST", "billing/resource-eligibility").json == {"operation": operation}
48
+
49
+
19
50
  class Billing:
20
51
  def __init__(self, calls, response=None):
21
52
  self.calls = calls
@@ -89,7 +120,7 @@ def test_vm_preflight_uses_upstream_status_and_preserves_create_denial(gw, monke
89
120
  "--site-id", "site-1", "--plan-id", "plan", "--template-id", "image",
90
121
  "--billing-term", "HOURLY"])
91
122
  assert result.exit_code == (1 if denied else 0), result.output
92
- assert gw.last("POST", "billing/resource-eligibility").json == {}
123
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
93
124
  creates = [c for c in gw.calls if c.method == "POST" and c.path == f"compute/{family}-vms"]
94
125
  assert len(creates) == 1
95
126
  assert creates[0].json["billing_catalog"]["billing_interval"] == "HOURLY"
@@ -157,8 +188,8 @@ def test_secret_store_and_secret_create_run_the_portal_billing_preflight(monkeyp
157
188
  result = runner.invoke(app, [*BASE_ARGS, *secret_args, *extra])
158
189
  assert result.exit_code == 0, result.output
159
190
  assert [(name, kwargs["preflight_billing"]) for name, kwargs in resource_calls] == [
160
- ("store", True),
161
- ("secret", True),
191
+ ("store", False),
192
+ ("secret", False),
162
193
  ("store", False),
163
194
  ("secret", False),
164
195
  ]
@@ -168,4 +199,4 @@ def test_secret_store_and_secret_create_run_the_portal_billing_preflight(monkeyp
168
199
  app, [*BASE_ARGS, "--check-billing", "secrets", "stores", "create", "production", "--no-billing-check"]
169
200
  )
170
201
  assert result.exit_code == 0, result.output
171
- assert resource_calls[0][1]["preflight_billing"] is True
202
+ assert resource_calls[0][1]["preflight_billing"] is False
@@ -618,8 +618,7 @@ def test_domain_create_check_billing_uses_custom_domain_sku(gw):
618
618
  gw.on("POST", f"{D}/custom-domains", {"domain": "cdn.example.com", "status": "pending_validation"})
619
619
  result = run(["--check-billing", "cdn", "domains", "create", "dist-1", "cdn.example.com"])
620
620
  assert result.exit_code == 0, result.output
621
- body = gw.last("POST", "billing/resource-eligibility").json
622
- assert body["sku_code"] == "CUSTOMDO-STD" and body["estimated_cost_minor"] == 19900
621
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
623
622
 
624
623
 
625
624
  def test_domain_path_is_normalised(gw):
@@ -729,11 +728,11 @@ def test_purge_success_false_exits_1(gw):
729
728
 
730
729
  def test_cdn_create_check_billing_denial_stops_before_create(gw):
731
730
  gw.on("GET", "object-storage/buckets/b1", {"name": "b1", "region": "r", "is_public": True})
732
- gw.on("POST", "billing/resource-eligibility",
733
- {"allowed": False, "reason": "insufficient_balance", "organization_id": "org-1"})
731
+ gw.on("POST", "cdn/distributions", (402, {"error": "billing_denied", "billing_reason": "insufficient_balance"}))
734
732
  result = run(["--check-billing", "cdn", "create", "site-cdn", "--origin-id", "b1"])
735
733
  assert result.exit_code == 1
736
- assert not any(c.path == "cdn/distributions" for c in gw.calls)
734
+ assert any(c.path == "cdn/distributions" for c in gw.calls)
735
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
737
736
 
738
737
 
739
738
  def test_cdn_edge_billing_denial_prints_portal_copy(gw):
@@ -37,7 +37,7 @@ def test_help_lists_command_groups():
37
37
  def test_version():
38
38
  result = runner.invoke(app, ["--version"])
39
39
  assert result.exit_code == 0
40
- assert "ibee-cli 0.4.1" in result.output
40
+ assert "ibee-cli 0.4.2" in result.output
41
41
 
42
42
 
43
43
  def test_subcommand_help():
@@ -14,6 +14,7 @@ from click import unstyle
14
14
  from typing import List, Optional
15
15
 
16
16
  import typer
17
+ from ibee.billing import TOPUP_GUIDANCE
17
18
  from ibee.errors import error_from_response
18
19
  from typer.models import TyperInfo
19
20
  from typer.testing import CliRunner
@@ -434,7 +435,34 @@ def test_edge_billing_denial_prints_portal_copy_and_topup(probe):
434
435
  assert "Your available wallet balance does not cover this CDN distribution" in output
435
436
  assert "reason=insufficient_balance" in output
436
437
  assert "admission_context_id=adm-9" in output
437
- assert "Add credits in the IBEE portal" in output
438
+ assert "Review billing for available actions" in output
439
+ assert "add credits" not in output.lower()
440
+
441
+
442
+ def test_edge_billing_denial_topup_guidance_follows_allowed_operations(probe):
443
+ http = probe
444
+ denial = {
445
+ "error": "billing_denied",
446
+ "billing_reason": "insufficient_balance",
447
+ "billing_sku_code": "CDN-1",
448
+ "admission_context_id": "adm-9",
449
+ }
450
+ http.responses = [FakeResponse({**denial, "allowed_operations": ["billing_topup"]}, 402)]
451
+ result = run([*GATEWAY, *call("POST", "cdn/distributions", CDN_BODY)])
452
+ assert result.exit_code == 1
453
+ output = plain(result)
454
+ assert "Your available wallet balance does not cover this CDN distribution" in output
455
+ assert "You can add credits in the IBEE portal" in output
456
+ assert TOPUP_GUIDANCE in output
457
+
458
+ for extra in ({}, {"allowed_operations": ["billing_review"]}):
459
+ http.responses = [FakeResponse({**denial, **extra}, 402)]
460
+ result = run([*GATEWAY, *call("POST", "cdn/distributions", CDN_BODY)])
461
+ assert result.exit_code == 1
462
+ output = plain(result)
463
+ assert "reason=insufficient_balance" in output
464
+ assert "add credits" not in output.lower()
465
+ assert TOPUP_GUIDANCE not in output
438
466
 
439
467
 
440
468
  def test_service_error_shows_request_id(probe):
@@ -654,7 +682,7 @@ def test_check_billing_asks_the_sdk_to_preflight_the_plan_sku(monkeypatch):
654
682
  # The SDK checks the plan's SKU and cost itself, so no SKU-less CLI preflight is sent.
655
683
  assert calls == []
656
684
  assert [name for name, _ in vms_fake.calls] == ["create"]
657
- assert vms_fake.calls[0][1]["preflight_billing"] is True
685
+ assert vms_fake.calls[0][1]["preflight_billing"] is False
658
686
 
659
687
 
660
688
  def test_check_billing_denial_blocks_create(monkeypatch):
@@ -663,7 +691,7 @@ def test_check_billing_denial_blocks_create(monkeypatch):
663
691
  class DenyingVms(FakeVms):
664
692
  def create_cloud_vm(self, **kwargs):
665
693
  self.calls.append(("create", kwargs))
666
- assert kwargs["preflight_billing"] is True
694
+ assert kwargs["preflight_billing"] is False
667
695
  raise BillingDeniedError(
668
696
  decision={"allowed": False, "reason": "initial_topup_required"}, create_type="vm"
669
697
  )
@@ -673,8 +701,10 @@ def test_check_billing_denial_blocks_create(monkeypatch):
673
701
  result = run(CREATE_ARGS, env={"IBEE_CHECK_BILLING": "1"})
674
702
  assert result.exit_code == 1
675
703
  output = plain(result)
676
- assert "Add at least ₹2,000 to your wallet before creating your first cloud VM." in output
677
- assert "Add credits in the IBEE portal" in output
704
+ assert "Billing requires an initial wallet top-up before creating your first cloud VM." in output
705
+ assert "Review billing for available actions" in output
706
+ assert "₹" not in output
707
+ assert "add credits" not in output.lower()
678
708
 
679
709
 
680
710
  def test_check_billing_falls_back_to_check_with_exact_true_gate():
@@ -683,9 +713,8 @@ def test_check_billing_falls_back_to_check_with_exact_true_gate():
683
713
  billing_fake = FakeBilling({"allowed": "true", "reason": "ok"})
684
714
  settings = SimpleNamespace(check_billing=True)
685
715
  client = SimpleNamespace(billing=billing_fake)
686
- with pytest.raises(BillingDeniedError):
687
- helpers.preflight_billing(settings, client, "973318", resource_type="gpu_vm")
688
- assert [name for name, _ in billing_fake.calls] == ["check"]
716
+ assert helpers.preflight_billing(settings, client, "973318", resource_type="gpu_vm") is None
717
+ assert billing_fake.calls == []
689
718
 
690
719
 
691
720
  def test_preflight_create_uses_require_eligibility():
@@ -694,10 +723,8 @@ def test_preflight_create_uses_require_eligibility():
694
723
  calls = []
695
724
  client = SimpleNamespace(billing=RequireBilling(calls, allowed=False, reason="unknown_sku"))
696
725
  settings = SimpleNamespace(check_billing=True, workspace="973318")
697
- with pytest.raises(BillingDeniedError) as info:
698
- helpers.preflight_create(settings, "cdn", client=client)
699
- assert "could not be verified" in str(info.value)
700
- assert calls[0][1]["resource_type"] == "cdn"
726
+ assert helpers.preflight_create(settings, "cdn", client=client) is None
727
+ assert calls == []
701
728
  assert helpers.preflight_create(SimpleNamespace(check_billing=False), "cdn") is None
702
729
 
703
730
 
@@ -724,7 +751,8 @@ def test_billing_eligibility_require_exit_codes(monkeypatch):
724
751
  assert run(["billing", "eligibility"]).exit_code == 0
725
752
  result = run(["billing", "eligibility", "--require"])
726
753
  assert result.exit_code == 1
727
- assert "Add credits in the IBEE portal" in plain(result)
754
+ assert "Review billing for available actions" in plain(result)
755
+ assert "add credits" not in plain(result).lower()
728
756
 
729
757
  billing_fake.decision = {"allowed": True, "reason": "ok"}
730
758
  assert run(["billing", "eligibility", "--require"]).exit_code == 0
@@ -291,19 +291,19 @@ def test_nat_create_warns_without_catalog_and_runs_preflight(gw):
291
291
  result = ok(run(["vpcs", "nat", "create", "vpc-1", "--preflight"]))
292
292
  assert "not be metered" in plain(result)
293
293
  assert [(c.method, c.path) for c in gw.writes()] == [
294
- ("POST", "billing/resource-eligibility"),
295
294
  ("POST", f"{V}/nat-gateways"),
296
295
  ]
297
- assert gw.writes()[0].json == {"sku_code": "NAT-GATEWAY"}
296
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
298
297
  assert gw.last("POST", f"{V}/nat-gateways").json == {"name": "NAT Gateway"}
299
298
 
300
299
 
301
300
  def test_nat_create_global_check_billing_denial(gw):
302
301
  gw.on("GET", V, vpc())
303
- gw.on("POST", "billing/resource-eligibility", decision("NAT-GATEWAY", False, "insufficient_balance"))
302
+ gw.on("POST", f"{V}/nat-gateways", (402, {"error": "billing_denied", "billing_reason": "insufficient_balance"}))
304
303
  result = run(["--check-billing", "vpcs", "nat", "create", "vpc-1", "--billing-catalog", json.dumps(NAT_CATALOG)])
305
304
  assert result.exit_code == 1, result.output
306
- assert "Add credits" in plain(result)
305
+ assert "Review billing for available actions" in plain(result)
306
+ assert "add credits" not in plain(result).lower()
307
307
  assert not gw.requests("POST")[1:]
308
308
 
309
309
 
@@ -532,7 +532,7 @@ def test_reserve_ip_body_and_billing_check(gw):
532
532
  result = ok(run(["reserved-ips", "reserve", "--site-id", " site-1 ", "--label", " edge ", "--billing-catalog",
533
533
  json.dumps({**RIP_CATALOG, "billing_options": [1]}), "--check-billing"]))
534
534
  assert json.loads(result.output)["public_ip_id"] == "rip-1"
535
- assert gw.writes()[0].json == {"sku_code": "RESERVED-IP"}
535
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
536
536
  assert gw.last("POST", "networking/reserved-ips").json == {
537
537
  "site_id": "site-1",
538
538
  "label": "edge",
@@ -559,14 +559,15 @@ def test_reserve_ip_edge_denial_names_the_reserved_ip(gw):
559
559
  result = run(["reserved-ips", "reserve", "--site-id", "site-1"])
560
560
  assert result.exit_code == 1
561
561
  assert "does not cover this Reserved IP" in plain(result)
562
- assert "Add credits in the IBEE portal" in plain(result)
562
+ assert "Review billing for available actions" in plain(result)
563
+ assert "add credits" not in plain(result).lower()
563
564
 
564
565
 
565
566
  def test_convert_runs_billing_check_by_default(gw):
566
567
  gw.on("POST", "billing/resource-eligibility", decision("RESERVED-IP"))
567
568
  gw.on("POST", "networking/reserved-ips/convert", rip(allocation_method="converted"))
568
569
  ok(run(["reserved-ips", "convert", "--vm-id", "vm-1", "--site-id", "site-1", "--label", "web"]))
569
- assert [c.path for c in gw.writes()] == ["billing/resource-eligibility", "networking/reserved-ips/convert"]
570
+ assert [c.path for c in gw.writes()] == ["networking/reserved-ips/convert"]
570
571
  assert gw.last("POST", "networking/reserved-ips/convert").json == {
571
572
  "vm_id": "vm-1", "site_id": "site-1", "label": "web"
572
573
  }
@@ -576,10 +577,10 @@ def test_convert_runs_billing_check_by_default(gw):
576
577
 
577
578
 
578
579
  def test_convert_billing_denial_stops_before_convert(gw):
579
- gw.on("POST", "billing/resource-eligibility", decision("RESERVED-IP", False, "initial_topup_required"))
580
+ gw.on("POST", "networking/reserved-ips/convert", (402, {"error": "billing_denied", "billing_reason": "initial_topup_required"}))
580
581
  result = run(["reserved-ips", "convert", "--vm-id", "vm-1", "--site-id", "site-1"])
581
582
  assert result.exit_code == 1
582
- assert [c.path for c in gw.writes()] == ["billing/resource-eligibility"]
583
+ assert [c.path for c in gw.writes()] == ["networking/reserved-ips/convert"]
583
584
 
584
585
 
585
586
  def test_release_refuses_while_attached(gw):
@@ -770,7 +771,7 @@ def test_create_l7_rules_sticky_and_billing_check(gw):
770
771
  ok(run(["--check-billing", "load-balancers", "create-l7", "web", "--protocol", "https", "--backend",
771
772
  "service:api:8080", "--sticky-header", "X-User-ID", "--rule", "1:/", "--rule", "2:/api:X-Env=beta",
772
773
  "--health-check-path", "/ready"]))
773
- assert gw.writes()[0].json == {"sku_code": "LOADBALA-STD"}
774
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
774
775
  body = gw.last("POST", f"{LB}/l7").json
775
776
  assert body["routing"] == {"sticky_header": "X-User-ID"}
776
777
  assert body["rules"] == [
@@ -103,7 +103,7 @@ def test_create_bucket_preflight_uses_object_storage_sku(gw):
103
103
  gw.on("POST", B, bucket())
104
104
  result = run(["buckets", "create", "assets", "--region", "r1", "--preflight-billing"])
105
105
  assert result.exit_code == 0, result.output
106
- assert gw.last("POST", "billing/resource-eligibility").json["sku_code"] == "OBJECTST-STD"
106
+ assert not any(c.path == "billing/resource-eligibility" for c in gw.calls)
107
107
 
108
108
 
109
109
  def test_list_buckets_table_and_next_page_hint(gw):
@@ -200,11 +200,12 @@ def test_firewall_summary_page_of_exactly_100_checks_for_more(gw):
200
200
 
201
201
 
202
202
  def test_reserved_ip_convert_without_billing_scope_points_at_the_flag(gw):
203
- gw.on("POST", "billing/resource-eligibility", SCOPE_403("billing.read"))
203
+ gw.on("POST", "networking/reserved-ips/convert", SCOPE_403("networking.write"))
204
204
  result = run(["reserved-ips", "convert", "--vm-id", "vm-1", "--site-id", "site-1"])
205
205
  assert result.exit_code == 1
206
- assert "--no-billing-check" in plain(result)
207
- assert all(c.path != "networking/reserved-ips/convert" for c in gw.calls)
206
+ assert "networking.write" in plain(result)
207
+ assert "--no-billing-check" not in plain(result)
208
+ assert [c.path for c in gw.writes()] == ["networking/reserved-ips/convert"]
208
209
 
209
210
 
210
211
  @pytest.mark.parametrize(
@@ -168,17 +168,16 @@ def test_store_create_runs_billing_preflight_then_creates_trimmed_name(gw):
168
168
  gw.on("POST", ELIGIBILITY, {"allowed": True, "reason": "ok", "sku_code": "SECRETMA-STD", "organization_id": "org"})
169
169
  gw.on("POST", S, store())
170
170
  result = ok(run(["secrets", "stores", "create", " Production ", "-d", " main "]))
171
- assert [(c.method, c.path) for c in gw.calls] == [("POST", ELIGIBILITY), ("POST", S)]
172
- assert gw.last("POST", ELIGIBILITY).json["sku_code"] == "SECRETMA-STD"
171
+ assert [(c.method, c.path) for c in gw.calls] == [("POST", S)]
173
172
  assert gw.last("POST", S).json == {"name": "Production", "description": "main"}
174
173
  assert "created" in plain(result)
175
174
 
176
175
 
177
176
  def test_store_create_billing_denied_sends_no_create(gw):
178
- gw.on("POST", ELIGIBILITY, {"allowed": False, "reason": "insufficient_balance", "sku_code": "SECRETMA-STD", "organization_id": "org"})
177
+ gw.on("POST", S, (402, {"error": "billing_denied", "billing_reason": "insufficient_balance"}))
179
178
  result = run(["secrets", "stores", "create", "Production"])
180
179
  assert result.exit_code == 1, result.output
181
- assert [c.path for c in gw.calls] == [ELIGIBILITY]
180
+ assert [c.path for c in gw.calls] == [S]
182
181
  assert "402" in plain(result) or "Payment" in plain(result)
183
182
 
184
183
 
@@ -186,7 +185,8 @@ def test_store_create_skips_billing_check_without_billing_read(gw):
186
185
  gw.on("POST", ELIGIBILITY, (403, {"error": "insufficient_scope", "required_scope": "billing.read"}))
187
186
  gw.on("POST", S, store())
188
187
  result = ok(run(["secrets", "stores", "create", "Production"]))
189
- assert "Billing preflight skipped" in plain(result)
188
+ assert "Billing preflight skipped" not in plain(result)
189
+ assert not any(c.path == ELIGIBILITY for c in gw.calls)
190
190
  assert gw.last("POST", S).json == {"name": "Production"}
191
191
 
192
192
 
@@ -332,7 +332,7 @@ def test_secret_create_normalizes_name_and_runs_preflight(gw):
332
332
  result = ok(run(["secrets", "create", "-s", "store-1", "-n", " DB-URL ", "--value", '{" url ":"postgres://x"}']))
333
333
  assert gw.last("POST", f"{S}/store-1/secrets").json == {"secret_name": "db-url", "value": {"url": "postgres://x"}}
334
334
  assert "normalized to 'db-url'" in plain(result)
335
- assert [c.path for c in gw.calls] == [ELIGIBILITY, f"{S}/store-1/secrets"]
335
+ assert [c.path for c in gw.calls] == [f"{S}/store-1/secrets"]
336
336
 
337
337
 
338
338
  @pytest.mark.parametrize(
@@ -125,7 +125,7 @@ def test_gpu_create_takes_shape_from_plan(sdk):
125
125
  name, args, kwargs = sdk.calls[0]
126
126
  assert name == "create_gpu_vm"
127
127
  assert "gpu_model" not in kwargs and "gpu_count" not in kwargs
128
- assert kwargs["preflight_billing"] is True
128
+ assert kwargs["preflight_billing"] is False
129
129
  assert kwargs["idempotency_key"].startswith("cli-gpu-create-train-")
130
130
  assert_sdk_transport("gpu_vms", name, args, kwargs)
131
131
 
@@ -283,7 +283,7 @@ def test_delete_reserve_flag_with_preflight(sdk):
283
283
  assert result.exit_code == 0, result.output
284
284
  kwargs = sdk.calls[-1][2]
285
285
  assert (kwargs["public_ip_action"], kwargs["reserved_ip_label"], kwargs["preflight_billing"]) == (
286
- "reserve", "keep", True
286
+ "reserve", "keep", False
287
287
  )
288
288
 
289
289
 
@@ -497,7 +497,7 @@ def test_snapshot_create_from_file_waits_for_success(sdk, tmp_path):
497
497
  (create, c_args, c_kwargs), (wait, w_args, w_kwargs) = sdk.calls
498
498
  assert create == "create_gpu_vm_snapshot"
499
499
  assert c_kwargs["billing_catalog"] == SNAP_SKU
500
- assert c_kwargs["preflight_billing"] is True
500
+ assert c_kwargs["preflight_billing"] is False
501
501
  assert_sdk_transport("gpu_vms", create, c_args, c_kwargs)
502
502
  assert wait == "wait_for_gpu_vm_snapshot"
503
503
  assert w_args == ("snap-1",)
File without changes
File without changes