aws-bootstrap-g4dn 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- aws_bootstrap/cli.py +18 -6
- aws_bootstrap/ssh.py +28 -0
- aws_bootstrap/tests/test_cli.py +52 -0
- aws_bootstrap/tests/test_ssh_config.py +76 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.5.0.dist-info}/METADATA +10 -4
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.5.0.dist-info}/RECORD +10 -10
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.5.0.dist-info}/WHEEL +0 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.5.0.dist-info}/entry_points.txt +0 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.5.0.dist-info}/top_level.txt +0 -0
aws_bootstrap/cli.py
CHANGED
|
@@ -29,6 +29,7 @@ from .ssh import (
|
|
|
29
29
|
private_key_path,
|
|
30
30
|
query_gpu_info,
|
|
31
31
|
remove_ssh_host,
|
|
32
|
+
resolve_instance_id,
|
|
32
33
|
run_remote_setup,
|
|
33
34
|
wait_for_ssh,
|
|
34
35
|
)
|
|
@@ -288,7 +289,7 @@ def launch(
|
|
|
288
289
|
|
|
289
290
|
click.echo()
|
|
290
291
|
click.secho(" Terminate:", fg="cyan")
|
|
291
|
-
click.secho(f" aws-bootstrap terminate {
|
|
292
|
+
click.secho(f" aws-bootstrap terminate {alias} --region {config.region}", bold=True)
|
|
292
293
|
click.echo()
|
|
293
294
|
|
|
294
295
|
|
|
@@ -419,7 +420,8 @@ def status(region, profile, gpu, instructions):
|
|
|
419
420
|
|
|
420
421
|
click.echo()
|
|
421
422
|
first_id = instances[0]["InstanceId"]
|
|
422
|
-
|
|
423
|
+
first_ref = ssh_hosts.get(first_id, first_id)
|
|
424
|
+
click.echo(" To terminate: " + click.style(f"aws-bootstrap terminate {first_ref}", bold=True))
|
|
423
425
|
click.echo()
|
|
424
426
|
|
|
425
427
|
|
|
@@ -427,18 +429,28 @@ def status(region, profile, gpu, instructions):
|
|
|
427
429
|
@click.option("--region", default="us-west-2", show_default=True, help="AWS region.")
|
|
428
430
|
@click.option("--profile", default=None, help="AWS profile override.")
|
|
429
431
|
@click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompt.")
|
|
430
|
-
@click.argument("instance_ids", nargs=-1)
|
|
432
|
+
@click.argument("instance_ids", nargs=-1, metavar="[INSTANCE_ID_OR_ALIAS]...")
|
|
431
433
|
def terminate(region, profile, yes, instance_ids):
|
|
432
434
|
"""Terminate instances created by aws-bootstrap.
|
|
433
435
|
|
|
434
|
-
Pass specific instance IDs
|
|
435
|
-
aws-bootstrap instances in the region.
|
|
436
|
+
Pass specific instance IDs or SSH aliases (e.g. aws-gpu1) to terminate,
|
|
437
|
+
or omit to terminate all aws-bootstrap instances in the region.
|
|
436
438
|
"""
|
|
437
439
|
session = boto3.Session(profile_name=profile, region_name=region)
|
|
438
440
|
ec2 = session.client("ec2")
|
|
439
441
|
|
|
440
442
|
if instance_ids:
|
|
441
|
-
targets =
|
|
443
|
+
targets = []
|
|
444
|
+
for value in instance_ids:
|
|
445
|
+
resolved = resolve_instance_id(value)
|
|
446
|
+
if resolved is None:
|
|
447
|
+
raise CLIError(
|
|
448
|
+
f"Could not resolve '{value}' to an instance ID.\n\n"
|
|
449
|
+
" It is not a valid instance ID or a known SSH alias."
|
|
450
|
+
)
|
|
451
|
+
if resolved != value:
|
|
452
|
+
info(f"Resolved alias '{value}' -> {resolved}")
|
|
453
|
+
targets.append(resolved)
|
|
442
454
|
else:
|
|
443
455
|
instances = find_tagged_instances(ec2, "aws-bootstrap-g4dn")
|
|
444
456
|
if not instances:
|
aws_bootstrap/ssh.py
CHANGED
|
@@ -374,6 +374,34 @@ def list_ssh_hosts(config_path: Path | None = None) -> dict[str, str]:
|
|
|
374
374
|
return result
|
|
375
375
|
|
|
376
376
|
|
|
377
|
+
_INSTANCE_ID_RE = re.compile(r"^i-[0-9a-f]{8,17}$")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _is_instance_id(value: str) -> bool:
|
|
381
|
+
"""Return ``True`` if *value* looks like an EC2 instance ID (``i-`` + hex)."""
|
|
382
|
+
return _INSTANCE_ID_RE.match(value) is not None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def resolve_instance_id(value: str, config_path: Path | None = None) -> str | None:
|
|
386
|
+
"""Resolve *value* to an EC2 instance ID.
|
|
387
|
+
|
|
388
|
+
If *value* already looks like an instance ID (``i-`` prefix followed by hex
|
|
389
|
+
digits) it is returned as-is. Otherwise it is treated as an SSH host alias
|
|
390
|
+
and looked up in the managed SSH config blocks.
|
|
391
|
+
|
|
392
|
+
Returns the instance ID on success, or ``None`` if the alias was not found.
|
|
393
|
+
"""
|
|
394
|
+
if _is_instance_id(value):
|
|
395
|
+
return value
|
|
396
|
+
|
|
397
|
+
hosts = list_ssh_hosts(config_path)
|
|
398
|
+
# Reverse lookup: alias -> instance_id
|
|
399
|
+
for iid, alias in hosts.items():
|
|
400
|
+
if alias == value:
|
|
401
|
+
return iid
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
377
405
|
@dataclass
|
|
378
406
|
class SSHHostDetails:
|
|
379
407
|
"""Connection details parsed from an SSH config stanza."""
|
aws_bootstrap/tests/test_cli.py
CHANGED
|
@@ -170,6 +170,58 @@ def test_terminate_with_confirm(mock_terminate, mock_find, mock_session, mock_re
|
|
|
170
170
|
assert mock_terminate.call_args[0][1] == ["i-abc123"]
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
@patch("aws_bootstrap.cli.remove_ssh_host", return_value=None)
|
|
174
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
175
|
+
@patch("aws_bootstrap.cli.terminate_tagged_instances")
|
|
176
|
+
@patch("aws_bootstrap.cli.resolve_instance_id", return_value="i-abc123")
|
|
177
|
+
def test_terminate_by_alias(mock_resolve, mock_terminate, mock_session, mock_remove_ssh):
|
|
178
|
+
mock_terminate.return_value = [
|
|
179
|
+
{
|
|
180
|
+
"InstanceId": "i-abc123",
|
|
181
|
+
"PreviousState": {"Name": "running"},
|
|
182
|
+
"CurrentState": {"Name": "shutting-down"},
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
runner = CliRunner()
|
|
186
|
+
result = runner.invoke(main, ["terminate", "--yes", "aws-gpu1"])
|
|
187
|
+
assert result.exit_code == 0
|
|
188
|
+
assert "Resolved alias 'aws-gpu1' -> i-abc123" in result.output
|
|
189
|
+
assert "Terminated 1" in result.output
|
|
190
|
+
mock_resolve.assert_called_once_with("aws-gpu1")
|
|
191
|
+
mock_terminate.assert_called_once()
|
|
192
|
+
assert mock_terminate.call_args[0][1] == ["i-abc123"]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
196
|
+
@patch("aws_bootstrap.cli.resolve_instance_id", return_value=None)
|
|
197
|
+
def test_terminate_unknown_alias_errors(mock_resolve, mock_session):
|
|
198
|
+
runner = CliRunner()
|
|
199
|
+
result = runner.invoke(main, ["terminate", "--yes", "aws-gpu99"])
|
|
200
|
+
assert result.exit_code != 0
|
|
201
|
+
assert "Could not resolve 'aws-gpu99'" in result.output
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@patch("aws_bootstrap.cli.remove_ssh_host", return_value=None)
|
|
205
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
206
|
+
@patch("aws_bootstrap.cli.terminate_tagged_instances")
|
|
207
|
+
@patch("aws_bootstrap.cli.resolve_instance_id", return_value="i-abc123")
|
|
208
|
+
def test_terminate_by_instance_id_passthrough(mock_resolve, mock_terminate, mock_session, mock_remove_ssh):
|
|
209
|
+
"""Instance IDs are passed through without resolution message."""
|
|
210
|
+
mock_resolve.return_value = "i-abc123"
|
|
211
|
+
mock_terminate.return_value = [
|
|
212
|
+
{
|
|
213
|
+
"InstanceId": "i-abc123",
|
|
214
|
+
"PreviousState": {"Name": "running"},
|
|
215
|
+
"CurrentState": {"Name": "shutting-down"},
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
runner = CliRunner()
|
|
219
|
+
result = runner.invoke(main, ["terminate", "--yes", "i-abc123"])
|
|
220
|
+
assert result.exit_code == 0
|
|
221
|
+
assert "Resolved alias" not in result.output
|
|
222
|
+
assert "Terminated 1" in result.output
|
|
223
|
+
|
|
224
|
+
|
|
173
225
|
@patch("aws_bootstrap.cli.boto3.Session")
|
|
174
226
|
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
175
227
|
def test_terminate_cancelled(mock_find, mock_session):
|
|
@@ -6,6 +6,7 @@ import stat
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from aws_bootstrap.ssh import (
|
|
9
|
+
_is_instance_id,
|
|
9
10
|
_next_alias,
|
|
10
11
|
_read_ssh_config,
|
|
11
12
|
add_ssh_host,
|
|
@@ -13,6 +14,7 @@ from aws_bootstrap.ssh import (
|
|
|
13
14
|
get_ssh_host_details,
|
|
14
15
|
list_ssh_hosts,
|
|
15
16
|
remove_ssh_host,
|
|
17
|
+
resolve_instance_id,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -331,3 +333,77 @@ def test_get_ssh_host_details_default_port(tmp_path):
|
|
|
331
333
|
details = get_ssh_host_details("i-abc123", config_path=cfg)
|
|
332
334
|
assert details is not None
|
|
333
335
|
assert details.port == 22
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Instance ID detection
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_is_instance_id_valid_short():
|
|
344
|
+
assert _is_instance_id("i-abcdef01") is True
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_is_instance_id_valid_long():
|
|
348
|
+
assert _is_instance_id("i-0123456789abcdef0") is True
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_is_instance_id_rejects_alias():
|
|
352
|
+
assert _is_instance_id("aws-gpu1") is False
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_is_instance_id_rejects_empty():
|
|
356
|
+
assert _is_instance_id("") is False
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_is_instance_id_rejects_prefix_only():
|
|
360
|
+
assert _is_instance_id("i-") is False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_is_instance_id_rejects_uppercase():
|
|
364
|
+
assert _is_instance_id("i-ABCDEF01") is False
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_is_instance_id_rejects_too_short():
|
|
368
|
+
assert _is_instance_id("i-abc") is False
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# resolve_instance_id
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_resolve_passthrough_instance_id(tmp_path):
|
|
377
|
+
"""Instance IDs are returned as-is without consulting SSH config."""
|
|
378
|
+
cfg = _config_path(tmp_path)
|
|
379
|
+
cfg.parent.mkdir(parents=True, exist_ok=True)
|
|
380
|
+
cfg.write_text("")
|
|
381
|
+
result = resolve_instance_id("i-0123456789abcdef0", config_path=cfg)
|
|
382
|
+
assert result == "i-0123456789abcdef0"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_resolve_alias_to_instance_id(tmp_path):
|
|
386
|
+
cfg = _config_path(tmp_path)
|
|
387
|
+
add_ssh_host("i-abc12345", "1.2.3.4", "ubuntu", KEY_PATH, config_path=cfg)
|
|
388
|
+
result = resolve_instance_id("aws-gpu1", config_path=cfg)
|
|
389
|
+
assert result == "i-abc12345"
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_resolve_alias_multiple_hosts(tmp_path):
|
|
393
|
+
cfg = _config_path(tmp_path)
|
|
394
|
+
add_ssh_host("i-111aaa11", "1.1.1.1", "ubuntu", KEY_PATH, config_path=cfg)
|
|
395
|
+
add_ssh_host("i-222bbb22", "2.2.2.2", "ubuntu", KEY_PATH, config_path=cfg)
|
|
396
|
+
assert resolve_instance_id("aws-gpu1", config_path=cfg) == "i-111aaa11"
|
|
397
|
+
assert resolve_instance_id("aws-gpu2", config_path=cfg) == "i-222bbb22"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_resolve_unknown_alias_returns_none(tmp_path):
|
|
401
|
+
cfg = _config_path(tmp_path)
|
|
402
|
+
cfg.parent.mkdir(parents=True, exist_ok=True)
|
|
403
|
+
cfg.write_text("")
|
|
404
|
+
assert resolve_instance_id("aws-gpu99", config_path=cfg) is None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_resolve_nonexistent_config_returns_none(tmp_path):
|
|
408
|
+
cfg = tmp_path / "no_such_file"
|
|
409
|
+
assert resolve_instance_id("aws-gpu1", config_path=cfg) is None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-bootstrap-g4dn
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Bootstrap AWS EC2 GPU instances for hybrid local-remote development
|
|
5
5
|
Author: Adam Ever-Hadani
|
|
6
6
|
License-Expression: MIT
|
|
@@ -261,8 +261,14 @@ aws-bootstrap status --region us-east-1
|
|
|
261
261
|
# Terminate all aws-bootstrap instances (with confirmation prompt)
|
|
262
262
|
aws-bootstrap terminate
|
|
263
263
|
|
|
264
|
-
# Terminate
|
|
265
|
-
aws-bootstrap terminate
|
|
264
|
+
# Terminate by SSH alias (resolved via ~/.ssh/config)
|
|
265
|
+
aws-bootstrap terminate aws-gpu1
|
|
266
|
+
|
|
267
|
+
# Terminate by instance ID
|
|
268
|
+
aws-bootstrap terminate i-abc123
|
|
269
|
+
|
|
270
|
+
# Mix aliases and instance IDs
|
|
271
|
+
aws-bootstrap terminate aws-gpu1 i-def456
|
|
266
272
|
|
|
267
273
|
# Skip confirmation prompt
|
|
268
274
|
aws-bootstrap terminate --yes
|
|
@@ -274,7 +280,7 @@ aws-bootstrap terminate --yes
|
|
|
274
280
|
CUDA: 12.8 (driver supports up to 13.0)
|
|
275
281
|
```
|
|
276
282
|
|
|
277
|
-
SSH aliases are managed automatically — they're created on `launch`, shown in `status`, and cleaned up on `terminate`. Aliases use sequential numbering (`aws-gpu1`, `aws-gpu2`, etc.) and never reuse numbers from previous instances.
|
|
283
|
+
SSH aliases are managed automatically — they're created on `launch`, shown in `status`, and cleaned up on `terminate`. Aliases use sequential numbering (`aws-gpu1`, `aws-gpu2`, etc.) and never reuse numbers from previous instances. You can use aliases anywhere you'd use an instance ID, e.g. `aws-bootstrap terminate aws-gpu1`.
|
|
278
284
|
|
|
279
285
|
## EC2 vCPU Quotas
|
|
280
286
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
aws_bootstrap/__init__.py,sha256=kl_jvrunGyIyizdRqAP6ROb5P1BBrXX5PTq5gq1ipU0,82
|
|
2
|
-
aws_bootstrap/cli.py,sha256=
|
|
2
|
+
aws_bootstrap/cli.py,sha256=N2hT0XEC-4k5Cs3iGfA_xt_onc__NMNmh8fCaV4frgc,21076
|
|
3
3
|
aws_bootstrap/config.py,sha256=TeCOYDlijT-KD5SFIzc-VvBhOqcq9YCgen9NK63rka8,895
|
|
4
4
|
aws_bootstrap/ec2.py,sha256=LHpzW91ayK45gsWV_B4LanSZIhWggqTsL31qHUceiaA,12274
|
|
5
5
|
aws_bootstrap/gpu.py,sha256=WTnHR0s3mQHDlnzqRgqAC6omWz7nT5YtGpcs0Bf88jk,692
|
|
6
|
-
aws_bootstrap/ssh.py,sha256=
|
|
6
|
+
aws_bootstrap/ssh.py,sha256=0acHNX7IG6PUvp6T72l9kHTwUs5sVXFAyJXvUfA3qnE,20131
|
|
7
7
|
aws_bootstrap/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
aws_bootstrap/resources/gpu_benchmark.py,sha256=1eFt_3MXvoLhs9HahrRPhbxvtdjFaXG2Ty3GEg7Gud0,29366
|
|
9
9
|
aws_bootstrap/resources/gpu_smoke_test.ipynb,sha256=XvAOEIPa5H9ri5mRZqOdknmwOwKNvCME6DzBGuhRYfg,10698
|
|
@@ -13,15 +13,15 @@ aws_bootstrap/resources/requirements.txt,sha256=gpYl1MFCfWXiAhbIUgAjuTHONz3MKci2
|
|
|
13
13
|
aws_bootstrap/resources/saxpy.cu,sha256=1BSESEwGGCx3KWx9ZJ8jiPHQ42KzQN6i2aP0I28bPsA,1178
|
|
14
14
|
aws_bootstrap/resources/tasks.json,sha256=6U8pB1N8YIWgUCfFet4ne3nYnI92tWv5D5kPiQG3Zlg,1576
|
|
15
15
|
aws_bootstrap/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
aws_bootstrap/tests/test_cli.py,sha256=
|
|
16
|
+
aws_bootstrap/tests/test_cli.py,sha256=m_4tIX0xYZ8BxDkHPGAWiPAKo4vETaTSKSJbyn3K1Cg,34731
|
|
17
17
|
aws_bootstrap/tests/test_config.py,sha256=arvET6KNl4Vqsz0zFrSdhciXGU688bfsvCr3dSpziN0,1050
|
|
18
18
|
aws_bootstrap/tests/test_ec2.py,sha256=Jmqsjv973hxXbZWfGgECtm6aa2156Lzji227sYMBuMg,10547
|
|
19
19
|
aws_bootstrap/tests/test_gpu.py,sha256=rbMuda_sIVbaCzkWXoLv9YIfnWztgRoP7NuVL8XHrUY,3871
|
|
20
|
-
aws_bootstrap/tests/test_ssh_config.py,sha256=
|
|
20
|
+
aws_bootstrap/tests/test_ssh_config.py,sha256=YYtv82zBBLGioTo58iC31_5jUli1s0eoGV9VRCobOgY,14059
|
|
21
21
|
aws_bootstrap/tests/test_ssh_gpu.py,sha256=dRp86Og-8GqiATSff3rxhu83mBZdGgqI4UOnoC00Ln0,1454
|
|
22
|
-
aws_bootstrap_g4dn-0.
|
|
23
|
-
aws_bootstrap_g4dn-0.
|
|
24
|
-
aws_bootstrap_g4dn-0.
|
|
25
|
-
aws_bootstrap_g4dn-0.
|
|
26
|
-
aws_bootstrap_g4dn-0.
|
|
27
|
-
aws_bootstrap_g4dn-0.
|
|
22
|
+
aws_bootstrap_g4dn-0.5.0.dist-info/licenses/LICENSE,sha256=Hen77Mt8sazSQJ9DgrmZuAvDwo2vc5JAkR_avuFV-CM,1067
|
|
23
|
+
aws_bootstrap_g4dn-0.5.0.dist-info/METADATA,sha256=t8m53ZodJlZyMffeSu3Wk5bMt-Dm_Jl3q_HTbRLQbYE,13728
|
|
24
|
+
aws_bootstrap_g4dn-0.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
25
|
+
aws_bootstrap_g4dn-0.5.0.dist-info/entry_points.txt,sha256=T8FXfOgmLEvFi8DHaFJ3tCzId9J3_d2Y6qT98OXxCjA,57
|
|
26
|
+
aws_bootstrap_g4dn-0.5.0.dist-info/top_level.txt,sha256=mix9gZRs8JUv0OMSB_rwdGcRnTKzsKgHrE5fyAn5zJw,14
|
|
27
|
+
aws_bootstrap_g4dn-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|