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.
- {ibee_cli-0.4.1/src/ibee_cli.egg-info → ibee_cli-0.4.2}/PKG-INFO +17 -28
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/README.md +15 -26
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/pyproject.toml +2 -2
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/__init__.py +1 -1
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/block_storage.py +1 -6
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/buckets.py +4 -4
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/cdn.py +4 -4
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/gpus.py +2 -2
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/load_balancers.py +3 -3
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/networking.py +2 -2
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/reserved_ips.py +12 -20
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/secrets.py +4 -6
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/vm_lifecycle.py +4 -4
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/vms.py +3 -3
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/helpers.py +6 -50
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/main.py +2 -2
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/render.py +1 -1
- {ibee_cli-0.4.1 → ibee_cli-0.4.2/src/ibee_cli.egg-info}/PKG-INFO +17 -28
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/requires.txt +1 -1
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_billing_preflight.py +35 -4
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_block_storage_cdn_commands.py +4 -5
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_cli.py +1 -1
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_foundations.py +41 -13
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_networking_parity_commands.py +11 -10
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_object_storage_commands.py +1 -1
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_review_fixes.py +4 -3
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_secret_store_commands.py +6 -6
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_vm_parity_commands.py +3 -3
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/LICENSE +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/setup.cfg +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/__init__.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/billing.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/compute.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/console.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/firewalls.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/commands/ops.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli/context.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/SOURCES.txt +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/dependency_links.txt +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/entry_points.txt +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/src/ibee_cli.egg-info/top_level.txt +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_cloud_vm_commands.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_networking_commands.py +0 -0
- {ibee_cli-0.4.1 → ibee_cli-0.4.2}/tests/test_parity_alignment.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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`.
|
|
433
|
-
|
|
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`
|
|
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**:
|
|
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` |
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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`.
|
|
408
|
-
|
|
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`
|
|
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**:
|
|
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` |
|
|
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.
|
|
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.
|
|
21
|
+
"ibee>=0.4.2,<0.5.0",
|
|
22
22
|
"httpx>=0.27",
|
|
23
23
|
"typer>=0.12",
|
|
24
24
|
"click>=8.1",
|
|
@@ -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
|
|
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="
|
|
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=
|
|
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="
|
|
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=
|
|
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="
|
|
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=
|
|
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="
|
|
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=
|
|
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="
|
|
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="
|
|
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=
|
|
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="
|
|
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="
|
|
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="
|
|
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=
|
|
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
|
|
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="
|
|
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=
|
|
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="
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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="
|
|
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=
|
|
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="
|
|
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=
|
|
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"] =
|
|
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"] =
|
|
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="
|
|
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=
|
|
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="
|
|
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="
|
|
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`),
|
|
6
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"
|
|
91
|
-
"(
|
|
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):
|
|
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.
|
|
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.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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`.
|
|
433
|
-
|
|
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`
|
|
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**:
|
|
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` |
|
|
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`
|
|
@@ -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
|
|
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",
|
|
161
|
-
("secret",
|
|
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
|
|
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
|
-
|
|
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", "
|
|
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
|
|
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.
|
|
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 "
|
|
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
|
|
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
|
|
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 "
|
|
677
|
-
assert "
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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 "
|
|
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
|
|
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", "
|
|
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 "
|
|
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
|
|
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 "
|
|
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()] == ["
|
|
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", "
|
|
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()] == ["
|
|
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
|
|
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
|
|
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", "
|
|
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 "
|
|
207
|
-
assert
|
|
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",
|
|
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",
|
|
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] == [
|
|
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] == [
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|