nodepick-cli 0.1.3__tar.gz → 0.1.4__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.
- nodepick_cli-0.1.4/CHANGELOG.md +27 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/PKG-INFO +11 -2
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/README.md +10 -1
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/pyproject.toml +1 -1
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/__init__.py +1 -1
- nodepick_cli-0.1.4/src/nodepick_cli/commands/compute.py +117 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/commands/network.py +6 -7
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/commands/node.py +31 -101
- nodepick_cli-0.1.4/src/nodepick_cli/commands/regions.py +83 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/main.py +11 -4
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/test_cli.py +248 -23
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/uv.lock +2 -2
- nodepick_cli-0.1.3/CHANGELOG.md +0 -17
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/.gitignore +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/install.sh +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/auth/__init__.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/auth/login.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/commands/__init__.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/commands/ai.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/commands/ssh.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/core/__init__.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/core/config.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/core/exceptions.py +0 -0
- {nodepick_cli-0.1.3 → nodepick_cli-0.1.4}/src/nodepick_cli/core/formatters.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# CHANGELOG.md
|
|
2
|
+
|
|
3
|
+
## [0.1.4] - 2026-10-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added short option `-s` alias to `--storage` on `np node create`.
|
|
7
|
+
- Added `--region` (`-r`) option to `np node create` to specify the deployment region.
|
|
8
|
+
- Added `--host` option to `np node create` to deploy directly onto a specified compute Host ID.
|
|
9
|
+
- Added region validation against supported active regions for `np node create` and `np network create`.
|
|
10
|
+
- Added `np regions` command to list available compute regions.
|
|
11
|
+
- Added `np compute` top-level command for compute discovery with real-time pricing and consolidated RAM/Disk/CPU specs.
|
|
12
|
+
- Added `np --version` (`-v`) flag.
|
|
13
|
+
- Added `np ai mcp configure <agent> [nodes...]` command to configure MCP servers on AI agents (Antigravity, OpenCode).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Changed authentication command from `np auth save` to `np auth configure`.
|
|
17
|
+
- Replaced `np find` and `np node find` with top-level `np compute`.
|
|
18
|
+
- Removed NAT configuration options from `np network` commands (outbound NAT enabled upstream by default).
|
|
19
|
+
- Updated `--region` help message on `np node create` and `np network create` to "Target region or datacenter (e.g. us-west-1, fmt1)".
|
|
20
|
+
- Cleaned option type formatting in `np node create` help output (removed redundant `[x>=...]` range tags).
|
|
21
|
+
|
|
22
|
+
## [0.1.0] - 2026-07-31
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Created `np` CLI tool in `cli/` using Typer and Rich.
|
|
26
|
+
- Added commands: `list`, `create`, `get`, `delete`, `shutdown`, `reboot`.
|
|
27
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nodepick-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Official Python CLI for nodepick.ai
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: httpx>=0.24.0
|
|
@@ -65,8 +65,17 @@ Check the official [`Documentation`](https://docs.nodepick.ai/cli-reference/over
|
|
|
65
65
|
### Basic Usage
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
#
|
|
68
|
+
# Check version
|
|
69
69
|
np --version
|
|
70
|
+
|
|
71
|
+
# List available regions
|
|
72
|
+
np regions
|
|
73
|
+
|
|
74
|
+
# Deploy a compute node in a specific region
|
|
75
|
+
np node create --name dev-node --region us-west-1
|
|
76
|
+
|
|
77
|
+
# List compute nodes
|
|
78
|
+
np node list
|
|
70
79
|
```
|
|
71
80
|
|
|
72
81
|
### Authentication
|
|
@@ -53,8 +53,17 @@ Check the official [`Documentation`](https://docs.nodepick.ai/cli-reference/over
|
|
|
53
53
|
### Basic Usage
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
#
|
|
56
|
+
# Check version
|
|
57
57
|
np --version
|
|
58
|
+
|
|
59
|
+
# List available regions
|
|
60
|
+
np regions
|
|
61
|
+
|
|
62
|
+
# Deploy a compute node in a specific region
|
|
63
|
+
np node create --name dev-node --region us-west-1
|
|
64
|
+
|
|
65
|
+
# List compute nodes
|
|
66
|
+
np node list
|
|
58
67
|
```
|
|
59
68
|
|
|
60
69
|
### Authentication
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table, Column
|
|
5
|
+
from .node import get_client
|
|
6
|
+
from ..core.exceptions import handle_error
|
|
7
|
+
from ..core.formatters import OutputFormat, set_output_format, print_output
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _render_compute_hosts_table(hosts):
|
|
13
|
+
if not hosts:
|
|
14
|
+
console.print("[yellow]No compute hosts found matching criteria.[/yellow]")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
table = Table(
|
|
18
|
+
Column("Host ID", no_wrap=True),
|
|
19
|
+
"Status",
|
|
20
|
+
"Region",
|
|
21
|
+
"Datacenter",
|
|
22
|
+
"RAM/Disk/CPU",
|
|
23
|
+
"Price/hr ($)",
|
|
24
|
+
"Price/mo ($)",
|
|
25
|
+
)
|
|
26
|
+
for host in hosts:
|
|
27
|
+
# Show requested specs (RAM, CPU, and Disk) for which pricing was evaluated,
|
|
28
|
+
# rather than the compute host's total reported capacity.
|
|
29
|
+
req_cpu = host.get("requested_cpu")
|
|
30
|
+
if req_cpu is None:
|
|
31
|
+
cpu_info = host.get("cpu", {})
|
|
32
|
+
req_cpu = cpu_info.get("cores") if isinstance(cpu_info, dict) else cpu_info
|
|
33
|
+
|
|
34
|
+
req_mem = host.get("requested_memory_gb")
|
|
35
|
+
if req_mem is None:
|
|
36
|
+
req_mem = host.get("memory_gb")
|
|
37
|
+
elif isinstance(req_mem, (int, float)) and req_mem == int(req_mem):
|
|
38
|
+
req_mem = int(req_mem)
|
|
39
|
+
|
|
40
|
+
req_disk = host.get("requested_storage_gb")
|
|
41
|
+
if req_disk is None:
|
|
42
|
+
req_disk = host.get("storage_gb")
|
|
43
|
+
|
|
44
|
+
def _fmt(val):
|
|
45
|
+
if val is None:
|
|
46
|
+
return "?"
|
|
47
|
+
if isinstance(val, (int, float)):
|
|
48
|
+
return str(int(val)) if val == int(val) else str(val)
|
|
49
|
+
return str(val)
|
|
50
|
+
|
|
51
|
+
specs_str = f"{_fmt(req_mem)}/{_fmt(req_disk)}/{_fmt(req_cpu)}"
|
|
52
|
+
|
|
53
|
+
pricing = host.get("pricing") if isinstance(host.get("pricing"), dict) else {}
|
|
54
|
+
hourly = pricing.get("hourly") or pricing.get("hour") or host.get("estimated_hourly_cost_usd")
|
|
55
|
+
monthly = pricing.get("monthly") or host.get("estimated_monthly_cost_usd")
|
|
56
|
+
|
|
57
|
+
if hourly is not None:
|
|
58
|
+
try:
|
|
59
|
+
hourly_str = f"${float(hourly):.4f}"
|
|
60
|
+
except (ValueError, TypeError):
|
|
61
|
+
hourly_str = f"${hourly}"
|
|
62
|
+
else:
|
|
63
|
+
hourly_str = "N/A"
|
|
64
|
+
|
|
65
|
+
if monthly is not None:
|
|
66
|
+
try:
|
|
67
|
+
monthly_str = f"${float(monthly):.2f}"
|
|
68
|
+
except (ValueError, TypeError):
|
|
69
|
+
monthly_str = f"${monthly}"
|
|
70
|
+
else:
|
|
71
|
+
monthly_str = "N/A"
|
|
72
|
+
|
|
73
|
+
status_val = str(host.get("status") or "active")
|
|
74
|
+
|
|
75
|
+
table.add_row(
|
|
76
|
+
str(host.get("id") or "N/A"),
|
|
77
|
+
status_val,
|
|
78
|
+
str(host.get("region") or "N/A"),
|
|
79
|
+
str(host.get("datacenter") or "N/A"),
|
|
80
|
+
specs_str,
|
|
81
|
+
hourly_str,
|
|
82
|
+
monthly_str,
|
|
83
|
+
)
|
|
84
|
+
console.print(table)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compute_command(
|
|
88
|
+
cpu: Optional[int] = typer.Option(None, "--cpu", "-c", help="Minimum CPU cores"),
|
|
89
|
+
memory_gb: Optional[float] = typer.Option(None, "--memory", "-m", help="Minimum RAM in GB"),
|
|
90
|
+
storage_gb: Optional[int] = typer.Option(None, "--storage", "-s", help="Minimum storage in GB"),
|
|
91
|
+
max_price: Optional[float] = typer.Option(None, "--max-price", "-p", help="Maximum monthly price in USD"),
|
|
92
|
+
region: Optional[str] = typer.Option(None, "--region", "-r", help="Filter by target region"),
|
|
93
|
+
datacenter: Optional[str] = typer.Option(None, "--datacenter", help="Filter by datacenter facility code"),
|
|
94
|
+
status: Optional[str] = typer.Option(None, "--status", help="Filter by host status (active, reservable, all)"),
|
|
95
|
+
format: OutputFormat = typer.Option(
|
|
96
|
+
OutputFormat.TABLE,
|
|
97
|
+
"--format", "-f",
|
|
98
|
+
help="Output format (table or json).",
|
|
99
|
+
case_sensitive=False,
|
|
100
|
+
),
|
|
101
|
+
):
|
|
102
|
+
"""Find and discover available compute hosts matching specific hardware and pricing criteria."""
|
|
103
|
+
set_output_format(format)
|
|
104
|
+
client = get_client()
|
|
105
|
+
try:
|
|
106
|
+
hosts = client.find_compute(
|
|
107
|
+
min_cpu=cpu,
|
|
108
|
+
min_memory_gb=memory_gb,
|
|
109
|
+
min_storage_gb=storage_gb,
|
|
110
|
+
max_price=max_price,
|
|
111
|
+
region=region,
|
|
112
|
+
datacenter=datacenter,
|
|
113
|
+
status=status,
|
|
114
|
+
)
|
|
115
|
+
print_output(hosts, table_render_func=_render_compute_hosts_table)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
handle_error(e, "Error finding compute hosts")
|
|
@@ -2,7 +2,7 @@ import typer
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
from rich.table import Table
|
|
5
|
-
import
|
|
5
|
+
from ..commands.regions import validate_region
|
|
6
6
|
from ..commands.node import get_client
|
|
7
7
|
from ..core.exceptions import handle_error
|
|
8
8
|
from ..core.formatters import OutputFormat, set_output_format, print_output
|
|
@@ -16,14 +16,13 @@ def _render_networks_table(networks):
|
|
|
16
16
|
console.print("[yellow]No VPC networks found.[/yellow]")
|
|
17
17
|
return
|
|
18
18
|
|
|
19
|
-
table = Table("ID", "Name", "Subnet", "Region"
|
|
19
|
+
table = Table("ID", "Name", "Subnet", "Region")
|
|
20
20
|
for net in networks:
|
|
21
21
|
table.add_row(
|
|
22
22
|
str(net.get("id") or "N/A"),
|
|
23
23
|
str(net.get("name") or "N/A"),
|
|
24
24
|
str(net.get("subnet") or "N/A"),
|
|
25
25
|
str(net.get("region") or "-"),
|
|
26
|
-
"Enabled" if net.get("enableOutboundNat", True) else "Disabled",
|
|
27
26
|
)
|
|
28
27
|
console.print(table)
|
|
29
28
|
|
|
@@ -72,20 +71,20 @@ def network_list(
|
|
|
72
71
|
|
|
73
72
|
@app.command("create")
|
|
74
73
|
def network_create(
|
|
75
|
-
name: str = typer.Argument(..., help="
|
|
76
|
-
region: str = typer.Option(..., "--region", "-r", help="Target
|
|
74
|
+
name: str = typer.Argument(..., help="Name for the VPC network (alphanumeric, dashes, and underscores)"),
|
|
75
|
+
region: str = typer.Option(..., "--region", "-r", help="Target region or datacenter (e.g. us-west-1, fmt1)"),
|
|
77
76
|
description: Optional[str] = typer.Option(None, "--description", "-d", help="Optional description"),
|
|
78
|
-
enable_nat: bool = typer.Option(True, "--nat/--no-nat", help="Enable outbound NAT gateway"),
|
|
79
77
|
):
|
|
80
78
|
"""Create a new VPC network."""
|
|
81
79
|
client = get_client()
|
|
80
|
+
if region:
|
|
81
|
+
validate_region(client, region)
|
|
82
82
|
try:
|
|
83
83
|
console.print(f"[cyan]Creating VPC network '{name}'...[/cyan]")
|
|
84
84
|
network = client.vpc_create(
|
|
85
85
|
name=name,
|
|
86
86
|
region=region,
|
|
87
87
|
description=description,
|
|
88
|
-
enable_outbound_nat=enable_nat,
|
|
89
88
|
)
|
|
90
89
|
vpc_id = network.get("id")
|
|
91
90
|
console.print(f"[bold green]VPC network created successfully![/bold green] ID: {vpc_id}")
|
|
@@ -83,104 +83,7 @@ def _render_nodes_table(nodes, client=None):
|
|
|
83
83
|
console.print(table)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def _render_compute_hosts_table(hosts):
|
|
87
|
-
if not hosts:
|
|
88
|
-
console.print("[yellow]No compute hosts found matching criteria.[/yellow]")
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
table = Table(
|
|
92
|
-
"Host ID",
|
|
93
|
-
"Region",
|
|
94
|
-
"Datacenter",
|
|
95
|
-
"RAM (GB)",
|
|
96
|
-
"Disk (GB)",
|
|
97
|
-
"CPU (Cores)",
|
|
98
|
-
"Price/hr ($)",
|
|
99
|
-
"Price/mo ($)",
|
|
100
|
-
)
|
|
101
|
-
for host in hosts:
|
|
102
|
-
# Show requested specs (RAM, CPU, and Disk) for which pricing was evaluated,
|
|
103
|
-
# rather than the compute host's total reported capacity.
|
|
104
|
-
req_cpu = host.get("requested_cpu")
|
|
105
|
-
if req_cpu is None:
|
|
106
|
-
cpu_info = host.get("cpu", {})
|
|
107
|
-
req_cpu = cpu_info.get("cores") if isinstance(cpu_info, dict) else cpu_info
|
|
108
|
-
|
|
109
|
-
req_mem = host.get("requested_memory_gb")
|
|
110
|
-
if req_mem is None:
|
|
111
|
-
req_mem = host.get("memory_gb")
|
|
112
|
-
elif isinstance(req_mem, (int, float)) and req_mem == int(req_mem):
|
|
113
|
-
req_mem = int(req_mem)
|
|
114
|
-
|
|
115
|
-
req_disk = host.get("requested_storage_gb")
|
|
116
|
-
if req_disk is None:
|
|
117
|
-
req_disk = host.get("storage_gb")
|
|
118
|
-
|
|
119
|
-
pricing = host.get("pricing") if isinstance(host.get("pricing"), dict) else {}
|
|
120
|
-
hourly = pricing.get("hourly") or pricing.get("hour") or host.get("estimated_hourly_cost_usd")
|
|
121
|
-
monthly = pricing.get("monthly") or host.get("estimated_monthly_cost_usd")
|
|
122
|
-
|
|
123
|
-
if hourly is not None:
|
|
124
|
-
try:
|
|
125
|
-
hourly_str = f"${float(hourly):.4f}"
|
|
126
|
-
except (ValueError, TypeError):
|
|
127
|
-
hourly_str = f"${hourly}"
|
|
128
|
-
else:
|
|
129
|
-
hourly_str = "N/A"
|
|
130
|
-
|
|
131
|
-
if monthly is not None:
|
|
132
|
-
try:
|
|
133
|
-
monthly_str = f"${float(monthly):.2f}"
|
|
134
|
-
except (ValueError, TypeError):
|
|
135
|
-
monthly_str = f"${monthly}"
|
|
136
|
-
else:
|
|
137
|
-
monthly_str = "N/A"
|
|
138
86
|
|
|
139
|
-
table.add_row(
|
|
140
|
-
str(host.get("id") or "N/A"),
|
|
141
|
-
str(host.get("region") or "N/A"),
|
|
142
|
-
str(host.get("datacenter") or "N/A"),
|
|
143
|
-
str(req_mem if req_mem is not None else "N/A"),
|
|
144
|
-
str(req_disk if req_disk is not None else "N/A"),
|
|
145
|
-
str(req_cpu if req_cpu is not None else "N/A"),
|
|
146
|
-
hourly_str,
|
|
147
|
-
monthly_str,
|
|
148
|
-
)
|
|
149
|
-
console.print(table)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@app.command("find")
|
|
153
|
-
def node_find(
|
|
154
|
-
cpu: Optional[int] = typer.Option(None, "--cpu", "-c", help="Minimum CPU cores"),
|
|
155
|
-
memory_gb: Optional[float] = typer.Option(None, "--memory", "-m", help="Minimum RAM in GB"),
|
|
156
|
-
storage_gb: Optional[int] = typer.Option(None, "--storage", "-s", help="Minimum storage in GB"),
|
|
157
|
-
max_price: Optional[float] = typer.Option(None, "--max-price", "-p", help="Maximum monthly price in USD"),
|
|
158
|
-
region: Optional[str] = typer.Option(None, "--region", "-r", help="Filter by target region"),
|
|
159
|
-
datacenter: Optional[str] = typer.Option(None, "--datacenter", help="Filter by datacenter facility code"),
|
|
160
|
-
status: str = typer.Option("active", "--status", help="Filter by host status (active, reserved, all)"),
|
|
161
|
-
format: OutputFormat = typer.Option(
|
|
162
|
-
OutputFormat.TABLE,
|
|
163
|
-
"--format", "-f",
|
|
164
|
-
help="Output format (table or json).",
|
|
165
|
-
case_sensitive=False,
|
|
166
|
-
),
|
|
167
|
-
):
|
|
168
|
-
"""Find and discover available compute hosts matching specific hardware and pricing criteria."""
|
|
169
|
-
set_output_format(format)
|
|
170
|
-
client = get_client()
|
|
171
|
-
try:
|
|
172
|
-
hosts = client.find_compute(
|
|
173
|
-
min_cpu=cpu,
|
|
174
|
-
min_memory_gb=memory_gb,
|
|
175
|
-
min_storage_gb=storage_gb,
|
|
176
|
-
max_price=max_price,
|
|
177
|
-
region=region,
|
|
178
|
-
datacenter=datacenter,
|
|
179
|
-
status=status,
|
|
180
|
-
)
|
|
181
|
-
print_output(hosts, table_render_func=_render_compute_hosts_table)
|
|
182
|
-
except Exception as e:
|
|
183
|
-
handle_error(e, "Error finding compute hosts")
|
|
184
87
|
|
|
185
88
|
|
|
186
89
|
@app.command("boot")
|
|
@@ -197,16 +100,41 @@ def node_boot(
|
|
|
197
100
|
handle_error(e, "Error booting node")
|
|
198
101
|
|
|
199
102
|
|
|
103
|
+
from .regions import validate_region, _render_regions_table
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_cpu(val: int) -> int:
|
|
107
|
+
if val < 1:
|
|
108
|
+
raise typer.BadParameter("Number of vCPUs must be at least 1.")
|
|
109
|
+
return val
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate_memory(val: int) -> int:
|
|
113
|
+
if val < 1:
|
|
114
|
+
raise typer.BadParameter("Memory size must be at least 1 GB.")
|
|
115
|
+
return val
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _validate_storage(val: int) -> int:
|
|
119
|
+
if val < 10:
|
|
120
|
+
raise typer.BadParameter("Disk storage must be at least 10 GB.")
|
|
121
|
+
return val
|
|
122
|
+
|
|
123
|
+
|
|
200
124
|
@app.command("create")
|
|
201
125
|
def node_create(
|
|
202
126
|
display_name: Optional[str] = typer.Option(None, "--name", "-n", help="Display name for the node"),
|
|
203
|
-
cpu: int = typer.Option(1, "--cpu", "-c",
|
|
204
|
-
memory: int = typer.Option(1, "--memory", "-m",
|
|
205
|
-
storage_gb:
|
|
127
|
+
cpu: int = typer.Option(1, "--cpu", "-c", callback=_validate_cpu, help="Number of vCPUs (min: 1)"),
|
|
128
|
+
memory: int = typer.Option(1, "--memory", "-m", callback=_validate_memory, help="Memory size in GB (min: 1)"),
|
|
129
|
+
storage_gb: int = typer.Option(10, "--storage", "-s", callback=_validate_storage, help="Disk storage in GB (min: 10)"),
|
|
206
130
|
vpc: Optional[str] = typer.Option(None, "--vpc", help="VPC network ID or name to deploy node into"),
|
|
131
|
+
region: Optional[str] = typer.Option(None, "--region", "-r", help="Target region or datacenter (e.g. us-west-1, fmt1)"),
|
|
132
|
+
host_id: Optional[str] = typer.Option(None, "--host", "--host-id", "--system-id", help="Target Host ID to deploy onto"),
|
|
207
133
|
):
|
|
208
134
|
"""Deploy a new compute node."""
|
|
209
135
|
client = get_client()
|
|
136
|
+
if region:
|
|
137
|
+
validate_region(client, region)
|
|
210
138
|
try:
|
|
211
139
|
console.print("[cyan]Creating node...[/cyan]")
|
|
212
140
|
node = client.node_create(
|
|
@@ -215,8 +143,10 @@ def node_create(
|
|
|
215
143
|
display_name=display_name,
|
|
216
144
|
storage_gb=storage_gb,
|
|
217
145
|
vpc=vpc,
|
|
146
|
+
region=region,
|
|
147
|
+
host_id=host_id,
|
|
218
148
|
)
|
|
219
|
-
node_id = node.get("vm_uuid")
|
|
149
|
+
node_id = node.get("vm_uuid") or node.get("id")
|
|
220
150
|
console.print(f"[bold green]Node created successfully![/bold green] ID: {node_id}")
|
|
221
151
|
except Exception as e:
|
|
222
152
|
handle_error(e, "Error creating node")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import nodepick
|
|
6
|
+
from nodepick_cli.core.config import get_api_key, get_base_url
|
|
7
|
+
from nodepick_cli.core.formatters import OutputFormat, set_output_format, print_output
|
|
8
|
+
from nodepick_cli.core.exceptions import handle_error
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_client() -> nodepick.NodePickClient:
|
|
14
|
+
key = get_api_key()
|
|
15
|
+
url = get_base_url()
|
|
16
|
+
if not key:
|
|
17
|
+
console.print(
|
|
18
|
+
"[yellow]No API key found.[/yellow] "
|
|
19
|
+
"Run [bold]np auth configure[/bold] to store your API key."
|
|
20
|
+
)
|
|
21
|
+
raise typer.Exit(1)
|
|
22
|
+
return nodepick.NodePickClient(api_key=key, base_url=url)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_region(client, region: Optional[str]) -> None:
|
|
26
|
+
"""Validate that region is among available regions if regions are returned by the API."""
|
|
27
|
+
if not region:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
raw_regions = client.get_available_regions()
|
|
31
|
+
if isinstance(raw_regions, list) and len(raw_regions) > 0:
|
|
32
|
+
supported = set()
|
|
33
|
+
for r in raw_regions:
|
|
34
|
+
if isinstance(r, dict):
|
|
35
|
+
if r.get("id"):
|
|
36
|
+
supported.add(str(r["id"]).lower())
|
|
37
|
+
for dc in r.get("datacenters") or []:
|
|
38
|
+
supported.add(str(dc).lower())
|
|
39
|
+
if supported and region.lower() not in supported:
|
|
40
|
+
sorted_ids = sorted(list({str(r.get("id")) for r in raw_regions if isinstance(r, dict) and r.get("id")}))
|
|
41
|
+
raise typer.BadParameter(
|
|
42
|
+
f"Unsupported region '{region}'. Supported regions are: {', '.join(sorted_ids)}"
|
|
43
|
+
)
|
|
44
|
+
except typer.BadParameter:
|
|
45
|
+
raise
|
|
46
|
+
except Exception:
|
|
47
|
+
# Ignore network/mock failures during pre-validation; backend API will enforce
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _render_regions_table(regions):
|
|
52
|
+
if not regions:
|
|
53
|
+
console.print("[yellow]No regions found.[/yellow]")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
table = Table("Region ID", "Name", "Datacenters", "Location")
|
|
57
|
+
for r in regions:
|
|
58
|
+
table.add_row(
|
|
59
|
+
str(r.get("id") or "N/A"),
|
|
60
|
+
str(r.get("name") or r.get("label") or "N/A"),
|
|
61
|
+
", ".join(r.get("datacenters") or []) or "-",
|
|
62
|
+
str(r.get("location") or r.get("continent") or "-"),
|
|
63
|
+
)
|
|
64
|
+
console.print(table)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def regions_command(
|
|
68
|
+
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter regions by status ('active' or 'reservable')"),
|
|
69
|
+
format: OutputFormat = typer.Option(
|
|
70
|
+
OutputFormat.TABLE,
|
|
71
|
+
"--format", "-f",
|
|
72
|
+
help="Output format (table or json).",
|
|
73
|
+
case_sensitive=False,
|
|
74
|
+
),
|
|
75
|
+
):
|
|
76
|
+
"""List available compute regions."""
|
|
77
|
+
set_output_format(format)
|
|
78
|
+
client = get_client()
|
|
79
|
+
try:
|
|
80
|
+
regions = client.get_available_regions(status=status)
|
|
81
|
+
print_output(regions, table_render_func=_render_regions_table)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
handle_error(e, "Error listing regions")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
from rich.console import Console
|
|
3
|
-
from .commands.node import app as node_app
|
|
3
|
+
from .commands.node import app as node_app
|
|
4
|
+
from .commands.compute import compute_command
|
|
5
|
+
from .commands.regions import regions_command
|
|
4
6
|
from .commands.network import app as network_app
|
|
5
7
|
from .commands.ssh import app as ssh_app
|
|
6
8
|
from .commands.ai import app as ai_app
|
|
@@ -58,11 +60,16 @@ app.add_typer(network_app, name="network")
|
|
|
58
60
|
app.add_typer(ssh_app, name="ssh")
|
|
59
61
|
app.add_typer(ai_app, name="ai")
|
|
60
62
|
|
|
61
|
-
#
|
|
63
|
+
# Commands: np compute, np regions
|
|
62
64
|
app.command(
|
|
63
|
-
"
|
|
65
|
+
"compute",
|
|
64
66
|
help="Find and discover available compute hosts matching specific hardware and pricing criteria.",
|
|
65
|
-
)(
|
|
67
|
+
)(compute_command)
|
|
68
|
+
|
|
69
|
+
app.command(
|
|
70
|
+
"regions",
|
|
71
|
+
help="List available compute regions.",
|
|
72
|
+
)(regions_command)
|
|
66
73
|
|
|
67
74
|
if __name__ == "__main__":
|
|
68
75
|
app()
|
|
@@ -32,6 +32,17 @@ class TestCliCommands(unittest.TestCase):
|
|
|
32
32
|
self.assertEqual(result.exit_code, 0)
|
|
33
33
|
self.assertIn("Manage Compute Nodes", result.output)
|
|
34
34
|
|
|
35
|
+
res_create = runner.invoke(app, ["node", "create", "--help"], env={"COLUMNS": "160"})
|
|
36
|
+
self.assertEqual(res_create.exit_code, 0)
|
|
37
|
+
self.assertIn("-s", res_create.output)
|
|
38
|
+
self.assertIn("--storage", res_create.output)
|
|
39
|
+
self.assertIn("Target region or datacenter (e.g. us-west-1, fmt1)", res_create.output)
|
|
40
|
+
self.assertIn("Number of vCPUs (min: 1) [default: 1]", res_create.output)
|
|
41
|
+
self.assertIn("Memory size in GB (min: 1) [default: 1]", res_create.output)
|
|
42
|
+
self.assertIn("Disk storage in GB (min: 10) [default: 10]", res_create.output)
|
|
43
|
+
self.assertNotIn("[x>=1]", res_create.output)
|
|
44
|
+
self.assertNotIn("[x>=10]", res_create.output)
|
|
45
|
+
|
|
35
46
|
def test_node_get_help(self):
|
|
36
47
|
result = runner.invoke(app, ["node", "get", "--help"])
|
|
37
48
|
self.assertEqual(result.exit_code, 0)
|
|
@@ -338,8 +349,9 @@ class TestCliCommands(unittest.TestCase):
|
|
|
338
349
|
self.assertIn("vpc-1", result.output)
|
|
339
350
|
self.assertIn("prod-net", result.output)
|
|
340
351
|
self.assertIn("10.0.1.0/24", result.output)
|
|
341
|
-
# Verify AZ
|
|
352
|
+
# Verify AZ and NAT columns are not present
|
|
342
353
|
self.assertNotIn(" AZ ", result.output)
|
|
354
|
+
self.assertNotIn(" NAT ", result.output)
|
|
343
355
|
|
|
344
356
|
def test_network_create(self):
|
|
345
357
|
from unittest.mock import patch, MagicMock
|
|
@@ -354,7 +366,15 @@ class TestCliCommands(unittest.TestCase):
|
|
|
354
366
|
name="devnet",
|
|
355
367
|
region="fmt1",
|
|
356
368
|
description=None,
|
|
357
|
-
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Verify VPC name with dashes (e.g. dev-vpc)
|
|
372
|
+
result_dash = runner.invoke(app, ["network", "create", "dev-vpc", "--region", "fmt1"])
|
|
373
|
+
self.assertEqual(result_dash.exit_code, 0)
|
|
374
|
+
mock_client.vpc_create.assert_called_with(
|
|
375
|
+
name="dev-vpc",
|
|
376
|
+
region="fmt1",
|
|
377
|
+
description=None,
|
|
358
378
|
)
|
|
359
379
|
|
|
360
380
|
# Verify --az option is no longer supported
|
|
@@ -362,6 +382,11 @@ class TestCliCommands(unittest.TestCase):
|
|
|
362
382
|
self.assertNotEqual(result_az.exit_code, 0)
|
|
363
383
|
self.assertIn("No such option: --az", result_az.output)
|
|
364
384
|
|
|
385
|
+
# Verify --nat option is no longer supported
|
|
386
|
+
result_nat = runner.invoke(app, ["network", "create", "devnet", "--region", "fmt1", "--nat"])
|
|
387
|
+
self.assertNotEqual(result_nat.exit_code, 0)
|
|
388
|
+
self.assertIn("No such option: --nat", result_nat.output)
|
|
389
|
+
|
|
365
390
|
def test_network_get(self):
|
|
366
391
|
from unittest.mock import patch, MagicMock
|
|
367
392
|
mock_client = MagicMock()
|
|
@@ -387,12 +412,13 @@ class TestCliCommands(unittest.TestCase):
|
|
|
387
412
|
self.assertEqual(result.exit_code, 0)
|
|
388
413
|
self.assertIn("VPC network vpc-123 deleted", result.output)
|
|
389
414
|
|
|
390
|
-
def
|
|
415
|
+
def test_compute_command(self):
|
|
391
416
|
from unittest.mock import patch, MagicMock
|
|
392
417
|
mock_client = MagicMock()
|
|
393
418
|
mock_client.find_compute.return_value = [
|
|
394
419
|
{
|
|
395
420
|
"id": "host-1",
|
|
421
|
+
"status": "active",
|
|
396
422
|
"region": "fmt1",
|
|
397
423
|
"datacenter": "facility-1",
|
|
398
424
|
"memory_gb": 32,
|
|
@@ -406,38 +432,66 @@ class TestCliCommands(unittest.TestCase):
|
|
|
406
432
|
"monthly": "10.95",
|
|
407
433
|
"currency": "USD"
|
|
408
434
|
}
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"id": "host-2",
|
|
438
|
+
"status": "reservable",
|
|
439
|
+
"region": "fmt1",
|
|
440
|
+
"datacenter": "facility-2",
|
|
441
|
+
"memory_gb": 64,
|
|
442
|
+
"storage_gb": 1000,
|
|
443
|
+
"cpu": {"cores": 32},
|
|
444
|
+
"requested_cpu": 2,
|
|
445
|
+
"requested_memory_gb": 4,
|
|
446
|
+
"requested_storage_gb": 50,
|
|
447
|
+
"pricing": {
|
|
448
|
+
"hourly": "0.0200",
|
|
449
|
+
"monthly": "14.60",
|
|
450
|
+
"currency": "USD"
|
|
451
|
+
}
|
|
409
452
|
}
|
|
410
453
|
]
|
|
411
|
-
with patch("nodepick_cli.commands.
|
|
412
|
-
# Test 'np
|
|
413
|
-
result = runner.invoke(app, ["
|
|
454
|
+
with patch("nodepick_cli.commands.compute.get_client", return_value=mock_client):
|
|
455
|
+
# Test 'np compute'
|
|
456
|
+
result = runner.invoke(app, ["compute", "-c", "2", "-m", "4", "-s", "50", "-p", "50"], env={"COLUMNS": "160"})
|
|
414
457
|
self.assertEqual(result.exit_code, 0)
|
|
415
458
|
# Verify required columns are present in output:
|
|
416
|
-
# host id, region, datacenter,
|
|
459
|
+
# host id (full), status, region, datacenter, consolidated specs (RAM/Disk/CPU), price-per-hour, price-per-month
|
|
417
460
|
self.assertIn("Host ID", result.output)
|
|
461
|
+
self.assertIn("Status", result.output)
|
|
418
462
|
self.assertIn("Region", result.output)
|
|
419
463
|
self.assertIn("Datacenter", result.output)
|
|
420
|
-
self.assertIn("RAM
|
|
421
|
-
self.assertIn("Disk (GB)", result.output)
|
|
422
|
-
self.assertIn("CPU (Cores)", result.output)
|
|
464
|
+
self.assertIn("RAM/Disk/CPU", result.output)
|
|
423
465
|
self.assertIn("Price/hr ($)", result.output)
|
|
424
466
|
self.assertIn("Price/mo ($)", result.output)
|
|
425
467
|
self.assertIn("host-1", result.output)
|
|
468
|
+
self.assertIn("host-2", result.output)
|
|
469
|
+
self.assertIn("active", result.output)
|
|
470
|
+
self.assertIn("reservable", result.output)
|
|
426
471
|
self.assertIn("fmt1", result.output)
|
|
427
472
|
self.assertIn("facility-1", result.output)
|
|
428
473
|
self.assertIn("$0.0150", result.output)
|
|
429
474
|
self.assertIn("$10.95", result.output)
|
|
430
475
|
|
|
431
|
-
# Verify requested
|
|
432
|
-
self.assertIn("4", result.output)
|
|
433
|
-
self.assertIn("50", result.output)
|
|
434
|
-
self.assertIn("2", result.output)
|
|
476
|
+
# Verify consolidated requested specs "4/50/2" (4 GB RAM, 50 GB Disk, 2 CPU cores)
|
|
477
|
+
self.assertIn("4/50/2", result.output)
|
|
435
478
|
|
|
436
|
-
# Test
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
self.
|
|
440
|
-
|
|
479
|
+
# Test filter with --status reservable
|
|
480
|
+
mock_client.find_compute.reset_mock()
|
|
481
|
+
res_reservable = runner.invoke(app, ["compute", "--status", "reservable"], env={"COLUMNS": "160"})
|
|
482
|
+
self.assertEqual(res_reservable.exit_code, 0)
|
|
483
|
+
mock_client.find_compute.assert_called_once()
|
|
484
|
+
self.assertEqual(mock_client.find_compute.call_args[1]["status"], "reservable")
|
|
485
|
+
|
|
486
|
+
# Verify 'np find' is no longer supported (renamed to 'np compute')
|
|
487
|
+
res_find = runner.invoke(app, ["find"])
|
|
488
|
+
self.assertNotEqual(res_find.exit_code, 0)
|
|
489
|
+
self.assertIn("No such command 'find'", res_find.output)
|
|
490
|
+
|
|
491
|
+
# Verify 'np node find' is no longer supported
|
|
492
|
+
res_node_find = runner.invoke(app, ["node", "find"])
|
|
493
|
+
self.assertNotEqual(res_node_find.exit_code, 0)
|
|
494
|
+
self.assertIn("No such command 'find'", res_node_find.output)
|
|
441
495
|
|
|
442
496
|
def test_node_create_with_vpc_and_no_network_flag(self):
|
|
443
497
|
from unittest.mock import patch, MagicMock
|
|
@@ -452,8 +506,10 @@ class TestCliCommands(unittest.TestCase):
|
|
|
452
506
|
memory=1,
|
|
453
507
|
cpu=1,
|
|
454
508
|
display_name="my-node",
|
|
455
|
-
storage_gb=
|
|
509
|
+
storage_gb=10,
|
|
456
510
|
vpc="devnet",
|
|
511
|
+
region=None,
|
|
512
|
+
host_id=None,
|
|
457
513
|
)
|
|
458
514
|
|
|
459
515
|
# 2. Create with custom --memory 2
|
|
@@ -464,25 +520,194 @@ class TestCliCommands(unittest.TestCase):
|
|
|
464
520
|
memory=2,
|
|
465
521
|
cpu=1,
|
|
466
522
|
display_name="my-node-2",
|
|
467
|
-
storage_gb=
|
|
523
|
+
storage_gb=10,
|
|
468
524
|
vpc=None,
|
|
525
|
+
region=None,
|
|
526
|
+
host_id=None,
|
|
469
527
|
)
|
|
470
528
|
|
|
471
|
-
# 3.
|
|
529
|
+
# 3. Create with --region fmt1 and -r us-west-1
|
|
530
|
+
mock_client.node_create.reset_mock()
|
|
531
|
+
result_reg = runner.invoke(app, ["node", "create", "--name", "my-node-reg", "--region", "fmt1"])
|
|
532
|
+
self.assertEqual(result_reg.exit_code, 0)
|
|
533
|
+
mock_client.node_create.assert_called_once_with(
|
|
534
|
+
memory=1,
|
|
535
|
+
cpu=1,
|
|
536
|
+
display_name="my-node-reg",
|
|
537
|
+
storage_gb=10,
|
|
538
|
+
vpc=None,
|
|
539
|
+
region="fmt1",
|
|
540
|
+
host_id=None,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
mock_client.node_create.reset_mock()
|
|
544
|
+
result_reg_short = runner.invoke(app, ["node", "create", "--name", "my-node-reg2", "-r", "us-west-1"])
|
|
545
|
+
self.assertEqual(result_reg_short.exit_code, 0)
|
|
546
|
+
mock_client.node_create.assert_called_once_with(
|
|
547
|
+
memory=1,
|
|
548
|
+
cpu=1,
|
|
549
|
+
display_name="my-node-reg2",
|
|
550
|
+
storage_gb=10,
|
|
551
|
+
vpc=None,
|
|
552
|
+
region="us-west-1",
|
|
553
|
+
host_id=None,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# 4. Create with --storage and short -s
|
|
557
|
+
mock_client.node_create.reset_mock()
|
|
558
|
+
result_storage = runner.invoke(app, ["node", "create", "--name", "my-node-stor", "--storage", "25"])
|
|
559
|
+
self.assertEqual(result_storage.exit_code, 0)
|
|
560
|
+
mock_client.node_create.assert_called_once_with(
|
|
561
|
+
memory=1,
|
|
562
|
+
cpu=1,
|
|
563
|
+
display_name="my-node-stor",
|
|
564
|
+
storage_gb=25,
|
|
565
|
+
vpc=None,
|
|
566
|
+
region=None,
|
|
567
|
+
host_id=None,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
mock_client.node_create.reset_mock()
|
|
571
|
+
result_storage_short = runner.invoke(app, ["node", "create", "--name", "my-node-stor2", "-s", "30"])
|
|
572
|
+
self.assertEqual(result_storage_short.exit_code, 0)
|
|
573
|
+
mock_client.node_create.assert_called_once_with(
|
|
574
|
+
memory=1,
|
|
575
|
+
cpu=1,
|
|
576
|
+
display_name="my-node-stor2",
|
|
577
|
+
storage_gb=30,
|
|
578
|
+
vpc=None,
|
|
579
|
+
region=None,
|
|
580
|
+
host_id=None,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
mock_client.node_create.reset_mock()
|
|
584
|
+
result_combo = runner.invoke(app, ["node", "create", "-n", "combo-node", "-c", "2", "-m", "4", "-s", "50", "-r", "us-west-1"])
|
|
585
|
+
self.assertEqual(result_combo.exit_code, 0)
|
|
586
|
+
mock_client.node_create.assert_called_once_with(
|
|
587
|
+
memory=4,
|
|
588
|
+
cpu=2,
|
|
589
|
+
display_name="combo-node",
|
|
590
|
+
storage_gb=50,
|
|
591
|
+
vpc=None,
|
|
592
|
+
region="us-west-1",
|
|
593
|
+
host_id=None,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# 5. Verify --network option no longer exists
|
|
472
597
|
result_invalid = runner.invoke(app, ["node", "create", "--network", "public"])
|
|
473
598
|
self.assertNotEqual(result_invalid.exit_code, 0)
|
|
474
599
|
self.assertIn("No such option: --network", result_invalid.output)
|
|
475
600
|
|
|
476
|
-
#
|
|
601
|
+
# 6. Verify min constraints in CLI (cpu >= 1, memory >= 1 GB, storage >= 10 GB)
|
|
477
602
|
res_bad_cpu = runner.invoke(app, ["node", "create", "--cpu", "0"])
|
|
478
603
|
self.assertNotEqual(res_bad_cpu.exit_code, 0)
|
|
479
604
|
|
|
605
|
+
res_bad_storage_short = runner.invoke(app, ["node", "create", "-s", "5"])
|
|
606
|
+
self.assertNotEqual(res_bad_storage_short.exit_code, 0)
|
|
607
|
+
|
|
608
|
+
def test_node_create_with_specific_host_id(self):
|
|
609
|
+
from unittest.mock import patch, MagicMock
|
|
610
|
+
mock_client = MagicMock()
|
|
611
|
+
mock_client.node_create.return_value = {"vm_uuid": "node-pinned-123"}
|
|
612
|
+
with patch("nodepick_cli.commands.node.get_client", return_value=mock_client):
|
|
613
|
+
# 1. Using --host
|
|
614
|
+
result = runner.invoke(app, ["node", "create", "--name", "pinned-node", "--host", "39e77410-ddf4-44aa-bbcc-1234567890ab"])
|
|
615
|
+
self.assertEqual(result.exit_code, 0)
|
|
616
|
+
self.assertIn("Node created successfully", result.output)
|
|
617
|
+
mock_client.node_create.assert_called_once_with(
|
|
618
|
+
memory=1,
|
|
619
|
+
cpu=1,
|
|
620
|
+
display_name="pinned-node",
|
|
621
|
+
storage_gb=10,
|
|
622
|
+
vpc=None,
|
|
623
|
+
region=None,
|
|
624
|
+
host_id="39e77410-ddf4-44aa-bbcc-1234567890ab",
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# 2. Using --host-id
|
|
628
|
+
mock_client.node_create.reset_mock()
|
|
629
|
+
result_host_id = runner.invoke(app, ["node", "create", "--name", "pinned-2", "--host-id", "custom-host-id"])
|
|
630
|
+
self.assertEqual(result_host_id.exit_code, 0)
|
|
631
|
+
mock_client.node_create.assert_called_once_with(
|
|
632
|
+
memory=1,
|
|
633
|
+
cpu=1,
|
|
634
|
+
display_name="pinned-2",
|
|
635
|
+
storage_gb=10,
|
|
636
|
+
vpc=None,
|
|
637
|
+
region=None,
|
|
638
|
+
host_id="custom-host-id",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# 3. Using --system-id
|
|
642
|
+
mock_client.node_create.reset_mock()
|
|
643
|
+
result_sys_id = runner.invoke(app, ["node", "create", "--name", "pinned-3", "--system-id", "sys-host-id"])
|
|
644
|
+
self.assertEqual(result_sys_id.exit_code, 0)
|
|
645
|
+
mock_client.node_create.assert_called_once_with(
|
|
646
|
+
memory=1,
|
|
647
|
+
cpu=1,
|
|
648
|
+
display_name="pinned-3",
|
|
649
|
+
storage_gb=10,
|
|
650
|
+
vpc=None,
|
|
651
|
+
region=None,
|
|
652
|
+
host_id="sys-host-id",
|
|
653
|
+
)
|
|
654
|
+
|
|
480
655
|
res_bad_mem = runner.invoke(app, ["node", "create", "--memory", "0"])
|
|
481
656
|
self.assertNotEqual(res_bad_mem.exit_code, 0)
|
|
482
657
|
|
|
483
658
|
res_bad_storage = runner.invoke(app, ["node", "create", "--storage", "5"])
|
|
484
659
|
self.assertNotEqual(res_bad_storage.exit_code, 0)
|
|
485
660
|
|
|
661
|
+
def test_node_create_unsupported_region(self):
|
|
662
|
+
from unittest.mock import patch, MagicMock
|
|
663
|
+
mock_client = MagicMock()
|
|
664
|
+
mock_client.get_available_regions.return_value = [
|
|
665
|
+
{"id": "us-west-1", "name": "US West", "datacenters": ["fmt1"], "status": "active"},
|
|
666
|
+
{"id": "us-east-1", "name": "US East", "datacenters": ["iad1"], "status": "active"},
|
|
667
|
+
]
|
|
668
|
+
with patch("nodepick_cli.commands.node.get_client", return_value=mock_client):
|
|
669
|
+
result = runner.invoke(app, ["node", "create", "--region", "unsupported-region"])
|
|
670
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
671
|
+
self.assertIn("Unsupported region 'unsupported-region'", result.output)
|
|
672
|
+
self.assertIn("Supported regions", result.output)
|
|
673
|
+
self.assertIn("us-east-1", result.output)
|
|
674
|
+
|
|
675
|
+
def test_network_create_unsupported_region(self):
|
|
676
|
+
from unittest.mock import patch, MagicMock
|
|
677
|
+
mock_client = MagicMock()
|
|
678
|
+
mock_client.get_available_regions.return_value = [
|
|
679
|
+
{"id": "us-west-1", "name": "US West", "datacenters": ["fmt1"], "status": "active"},
|
|
680
|
+
]
|
|
681
|
+
with patch("nodepick_cli.commands.network.get_client", return_value=mock_client):
|
|
682
|
+
result = runner.invoke(app, ["network", "create", "test-vpc", "--region", "unsupported-region"])
|
|
683
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
684
|
+
self.assertIn("Unsupported region 'unsupported-region'", result.output)
|
|
685
|
+
|
|
686
|
+
def test_top_level_regions_command(self):
|
|
687
|
+
from unittest.mock import patch, MagicMock
|
|
688
|
+
mock_client = MagicMock()
|
|
689
|
+
mock_client.get_available_regions.return_value = [
|
|
690
|
+
{
|
|
691
|
+
"id": "us-west-1",
|
|
692
|
+
"name": "US West (Silicon Valley)",
|
|
693
|
+
"datacenters": ["fmt1"],
|
|
694
|
+
"location": "Silicon Valley",
|
|
695
|
+
}
|
|
696
|
+
]
|
|
697
|
+
with patch("nodepick_cli.commands.regions.get_client", return_value=mock_client):
|
|
698
|
+
# Test top-level np regions
|
|
699
|
+
res = runner.invoke(app, ["regions"])
|
|
700
|
+
self.assertEqual(res.exit_code, 0)
|
|
701
|
+
self.assertIn("us-west-1", res.output)
|
|
702
|
+
self.assertIn("fmt1", res.output)
|
|
703
|
+
self.assertIn("Silicon Valley", res.output)
|
|
704
|
+
self.assertNotIn("Status", res.output)
|
|
705
|
+
|
|
706
|
+
# Verify np node regions is no longer a command
|
|
707
|
+
res_node_regions = runner.invoke(app, ["node", "regions"])
|
|
708
|
+
self.assertNotEqual(res_node_regions.exit_code, 0)
|
|
709
|
+
self.assertIn("No such command", res_node_regions.output)
|
|
710
|
+
|
|
486
711
|
def test_node_attach_and_detach_ip_ipv4(self):
|
|
487
712
|
from unittest.mock import patch, MagicMock
|
|
488
713
|
mock_client = MagicMock()
|
|
@@ -514,7 +514,7 @@ wheels = [
|
|
|
514
514
|
|
|
515
515
|
[[package]]
|
|
516
516
|
name = "nodepick"
|
|
517
|
-
version = "0.1.
|
|
517
|
+
version = "0.1.4"
|
|
518
518
|
source = { editable = "../sdk/python" }
|
|
519
519
|
dependencies = [
|
|
520
520
|
{ name = "httpx" },
|
|
@@ -529,7 +529,7 @@ requires-dist = [
|
|
|
529
529
|
|
|
530
530
|
[[package]]
|
|
531
531
|
name = "nodepick-cli"
|
|
532
|
-
version = "0.1.
|
|
532
|
+
version = "0.1.4"
|
|
533
533
|
source = { editable = "." }
|
|
534
534
|
dependencies = [
|
|
535
535
|
{ name = "httpx" },
|
nodepick_cli-0.1.3/CHANGELOG.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# CHANGELOG.md
|
|
2
|
-
|
|
3
|
-
## [Unreleased]
|
|
4
|
-
|
|
5
|
-
### Changed
|
|
6
|
-
- Changed authentication command from `np auth save` to `np auth configure`.
|
|
7
|
-
|
|
8
|
-
### Added
|
|
9
|
-
- Added `np --version` (`-v`) flag.
|
|
10
|
-
- Added `np ai mcp configure <agent> [nodes...]` command to configure MCP servers on AI agents (such as Antigravity) over HTTP Stream with self-signed certificate verification bypassed.
|
|
11
|
-
|
|
12
|
-
## [0.1.0] - 2026-07-31
|
|
13
|
-
|
|
14
|
-
### Added
|
|
15
|
-
- Created `np` CLI tool in `cli/` using Typer and Rich.
|
|
16
|
-
- Added commands: `list`, `create`, `get`, `delete`, `shutdown`, `reboot`.
|
|
17
|
-
|
|
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
|