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 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 {instance_id} --region {config.region}", bold=True)
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
- click.echo(" To terminate: " + click.style(f"aws-bootstrap terminate {first_id}", bold=True))
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 to terminate, or omit to terminate all
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 = list(instance_ids)
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."""
@@ -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.4.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 specific instances
265
- aws-bootstrap terminate i-abc123 i-def456
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=XqCKxyc294krVtggrsqm2cYrHR6DWaqQeuzrRAN5u_c,20501
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=UFRDgNR8cljV-lwMvCy_BAJQBz7gj4a_cQIulf-2A10,19226
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=Lwzpdovq_iJFB6qZ8NuySqzHFkQ_2Q8AAGXdITXi1Vo,32564
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=iQDd3hJ8to-2-QHW26Brtglfl0q0P6sCE6U_itxoNyY,11609
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.4.0.dist-info/licenses/LICENSE,sha256=Hen77Mt8sazSQJ9DgrmZuAvDwo2vc5JAkR_avuFV-CM,1067
23
- aws_bootstrap_g4dn-0.4.0.dist-info/METADATA,sha256=0OQsG5kVwsbfT7dfaZoNrkOlfNRUrKr9NwljtLBKj1I,13483
24
- aws_bootstrap_g4dn-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
25
- aws_bootstrap_g4dn-0.4.0.dist-info/entry_points.txt,sha256=T8FXfOgmLEvFi8DHaFJ3tCzId9J3_d2Y6qT98OXxCjA,57
26
- aws_bootstrap_g4dn-0.4.0.dist-info/top_level.txt,sha256=mix9gZRs8JUv0OMSB_rwdGcRnTKzsKgHrE5fyAn5zJw,14
27
- aws_bootstrap_g4dn-0.4.0.dist-info/RECORD,,
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,,