aws-bootstrap-g4dn 0.1.0__py3-none-any.whl → 0.2.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 +34 -1
- aws_bootstrap/tests/test_cli.py +110 -0
- {aws_bootstrap_g4dn-0.1.0.dist-info → aws_bootstrap_g4dn-0.2.0.dist-info}/METADATA +12 -2
- {aws_bootstrap_g4dn-0.1.0.dist-info → aws_bootstrap_g4dn-0.2.0.dist-info}/RECORD +8 -8
- {aws_bootstrap_g4dn-0.1.0.dist-info → aws_bootstrap_g4dn-0.2.0.dist-info}/WHEEL +0 -0
- {aws_bootstrap_g4dn-0.1.0.dist-info → aws_bootstrap_g4dn-0.2.0.dist-info}/entry_points.txt +0 -0
- {aws_bootstrap_g4dn-0.1.0.dist-info → aws_bootstrap_g4dn-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {aws_bootstrap_g4dn-0.1.0.dist-info → aws_bootstrap_g4dn-0.2.0.dist-info}/top_level.txt +0 -0
aws_bootstrap/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import boto3
|
|
8
|
+
import botocore.exceptions
|
|
8
9
|
import click
|
|
9
10
|
|
|
10
11
|
from .config import LaunchConfig
|
|
@@ -56,7 +57,39 @@ def warn(msg: str) -> None:
|
|
|
56
57
|
click.secho(f" WARNING: {msg}", fg="yellow", err=True)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
class _AWSGroup(click.Group):
|
|
61
|
+
"""Click group that catches common AWS credential/auth errors."""
|
|
62
|
+
|
|
63
|
+
def invoke(self, ctx):
|
|
64
|
+
try:
|
|
65
|
+
return super().invoke(ctx)
|
|
66
|
+
except botocore.exceptions.NoCredentialsError:
|
|
67
|
+
raise CLIError(
|
|
68
|
+
"Unable to locate AWS credentials.\n\n"
|
|
69
|
+
" Make sure you have configured AWS credentials using one of:\n"
|
|
70
|
+
" - Set the AWS_PROFILE environment variable: export AWS_PROFILE=<profile-name>\n"
|
|
71
|
+
" - Pass --profile to the command: aws-bootstrap <command> --profile <profile-name>\n"
|
|
72
|
+
" - Configure a default profile: aws configure\n\n"
|
|
73
|
+
" See: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
|
|
74
|
+
) from None
|
|
75
|
+
except botocore.exceptions.ProfileNotFound as e:
|
|
76
|
+
raise CLIError(f"{e}\n\n List available profiles with: aws configure list-profiles") from None
|
|
77
|
+
except botocore.exceptions.PartialCredentialsError as e:
|
|
78
|
+
raise CLIError(
|
|
79
|
+
f"Incomplete AWS credentials: {e}\n\n Check your AWS configuration with: aws configure list"
|
|
80
|
+
) from None
|
|
81
|
+
except botocore.exceptions.ClientError as e:
|
|
82
|
+
code = e.response["Error"]["Code"]
|
|
83
|
+
if code in ("AuthFailure", "UnauthorizedOperation", "ExpiredTokenException", "ExpiredToken"):
|
|
84
|
+
raise CLIError(
|
|
85
|
+
f"AWS authorization failed: {e.response['Error']['Message']}\n\n"
|
|
86
|
+
" Your credentials may be expired or lack the required permissions.\n"
|
|
87
|
+
" Check your AWS configuration with: aws configure list"
|
|
88
|
+
) from None
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@click.group(cls=_AWSGroup)
|
|
60
93
|
@click.version_option(package_name="aws-bootstrap-g4dn")
|
|
61
94
|
def main():
|
|
62
95
|
"""Bootstrap AWS EC2 GPU instances for hybrid local-remote development."""
|
aws_bootstrap/tests/test_cli.py
CHANGED
|
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from unittest.mock import patch
|
|
7
7
|
|
|
8
|
+
import botocore.exceptions
|
|
8
9
|
from click.testing import CliRunner
|
|
9
10
|
|
|
10
11
|
from aws_bootstrap.cli import main
|
|
@@ -526,3 +527,112 @@ def test_status_without_gpu_flag_no_ssh(mock_find, mock_spot, mock_session, mock
|
|
|
526
527
|
assert result.exit_code == 0
|
|
527
528
|
mock_gpu.assert_not_called()
|
|
528
529
|
mock_details.assert_not_called()
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# ---------------------------------------------------------------------------
|
|
533
|
+
# AWS credential / auth error handling tests
|
|
534
|
+
# ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
538
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
539
|
+
def test_no_credentials_shows_friendly_error(mock_session, mock_find):
|
|
540
|
+
"""NoCredentialsError should show a helpful message, not a raw traceback."""
|
|
541
|
+
mock_find.side_effect = botocore.exceptions.NoCredentialsError()
|
|
542
|
+
runner = CliRunner()
|
|
543
|
+
result = runner.invoke(main, ["status"])
|
|
544
|
+
assert result.exit_code != 0
|
|
545
|
+
assert "Unable to locate AWS credentials" in result.output
|
|
546
|
+
assert "AWS_PROFILE" in result.output
|
|
547
|
+
assert "--profile" in result.output
|
|
548
|
+
assert "aws configure" in result.output
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
552
|
+
def test_profile_not_found_shows_friendly_error(mock_session):
|
|
553
|
+
"""ProfileNotFound should show the missing profile name and list command."""
|
|
554
|
+
mock_session.side_effect = botocore.exceptions.ProfileNotFound(profile="nonexistent")
|
|
555
|
+
runner = CliRunner()
|
|
556
|
+
result = runner.invoke(main, ["status", "--profile", "nonexistent"])
|
|
557
|
+
assert result.exit_code != 0
|
|
558
|
+
assert "nonexistent" in result.output
|
|
559
|
+
assert "aws configure list-profiles" in result.output
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
563
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
564
|
+
def test_partial_credentials_shows_friendly_error(mock_session, mock_find):
|
|
565
|
+
"""PartialCredentialsError should mention incomplete credentials."""
|
|
566
|
+
mock_find.side_effect = botocore.exceptions.PartialCredentialsError(
|
|
567
|
+
provider="env", cred_var="AWS_SECRET_ACCESS_KEY"
|
|
568
|
+
)
|
|
569
|
+
runner = CliRunner()
|
|
570
|
+
result = runner.invoke(main, ["status"])
|
|
571
|
+
assert result.exit_code != 0
|
|
572
|
+
assert "Incomplete AWS credentials" in result.output
|
|
573
|
+
assert "aws configure list" in result.output
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
577
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
578
|
+
def test_expired_token_shows_friendly_error(mock_session, mock_find):
|
|
579
|
+
"""ExpiredTokenException should show authorization failure with context."""
|
|
580
|
+
mock_find.side_effect = botocore.exceptions.ClientError(
|
|
581
|
+
{"Error": {"Code": "ExpiredTokenException", "Message": "The security token is expired"}},
|
|
582
|
+
"DescribeInstances",
|
|
583
|
+
)
|
|
584
|
+
runner = CliRunner()
|
|
585
|
+
result = runner.invoke(main, ["status"])
|
|
586
|
+
assert result.exit_code != 0
|
|
587
|
+
assert "AWS authorization failed" in result.output
|
|
588
|
+
assert "expired" in result.output.lower()
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
592
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
593
|
+
def test_auth_failure_shows_friendly_error(mock_session, mock_find):
|
|
594
|
+
"""AuthFailure ClientError should show authorization failure message."""
|
|
595
|
+
mock_find.side_effect = botocore.exceptions.ClientError(
|
|
596
|
+
{"Error": {"Code": "AuthFailure", "Message": "credentials are invalid"}},
|
|
597
|
+
"DescribeInstances",
|
|
598
|
+
)
|
|
599
|
+
runner = CliRunner()
|
|
600
|
+
result = runner.invoke(main, ["status"])
|
|
601
|
+
assert result.exit_code != 0
|
|
602
|
+
assert "AWS authorization failed" in result.output
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
606
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
607
|
+
def test_unhandled_client_error_propagates(mock_session, mock_find):
|
|
608
|
+
"""Non-auth ClientErrors should propagate without being caught."""
|
|
609
|
+
mock_find.side_effect = botocore.exceptions.ClientError(
|
|
610
|
+
{"Error": {"Code": "UnknownError", "Message": "something else"}},
|
|
611
|
+
"DescribeInstances",
|
|
612
|
+
)
|
|
613
|
+
runner = CliRunner()
|
|
614
|
+
result = runner.invoke(main, ["status"])
|
|
615
|
+
assert result.exit_code != 0
|
|
616
|
+
assert isinstance(result.exception, botocore.exceptions.ClientError)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
620
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
621
|
+
def test_no_credentials_caught_on_terminate(mock_session, mock_find):
|
|
622
|
+
"""Credential errors are caught for all subcommands, not just status."""
|
|
623
|
+
mock_find.side_effect = botocore.exceptions.NoCredentialsError()
|
|
624
|
+
runner = CliRunner()
|
|
625
|
+
result = runner.invoke(main, ["terminate"])
|
|
626
|
+
assert result.exit_code != 0
|
|
627
|
+
assert "Unable to locate AWS credentials" in result.output
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@patch("aws_bootstrap.cli.list_instance_types")
|
|
631
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
632
|
+
def test_no_credentials_caught_on_list(mock_session, mock_list):
|
|
633
|
+
"""Credential errors are caught for nested subcommands (list instance-types)."""
|
|
634
|
+
mock_list.side_effect = botocore.exceptions.NoCredentialsError()
|
|
635
|
+
runner = CliRunner()
|
|
636
|
+
result = runner.invoke(main, ["list", "instance-types"])
|
|
637
|
+
assert result.exit_code != 0
|
|
638
|
+
assert "Unable to locate AWS credentials" in result.output
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-bootstrap-g4dn
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -66,6 +66,16 @@ ssh aws-gpu1 # You're in, venv activated, PyTorch works
|
|
|
66
66
|
pip install aws-bootstrap-g4dn
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### With uvx (no install needed)
|
|
70
|
+
|
|
71
|
+
[uvx](https://docs.astral.sh/uv/guides/tools/) runs the CLI directly in a temporary environment — no global install required:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uvx --from aws-bootstrap-g4dn aws-bootstrap launch
|
|
75
|
+
uvx --from aws-bootstrap-g4dn aws-bootstrap status
|
|
76
|
+
uvx --from aws-bootstrap-g4dn aws-bootstrap terminate
|
|
77
|
+
```
|
|
78
|
+
|
|
69
79
|
### From source (development)
|
|
70
80
|
|
|
71
81
|
```bash
|
|
@@ -75,7 +85,7 @@ uv venv
|
|
|
75
85
|
uv sync
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
All methods install the `aws-bootstrap` CLI.
|
|
79
89
|
|
|
80
90
|
## SSH Key Setup
|
|
81
91
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
aws_bootstrap/__init__.py,sha256=kl_jvrunGyIyizdRqAP6ROb5P1BBrXX5PTq5gq1ipU0,82
|
|
2
|
-
aws_bootstrap/cli.py,sha256=
|
|
2
|
+
aws_bootstrap/cli.py,sha256=3PWGU4djqCvABNpLvYTk473Nmmmrad3JQ3iQtg5YmnE,17917
|
|
3
3
|
aws_bootstrap/config.py,sha256=bOADtpujEacED0pu9m7D781UFlMhZrmtHQ7eqI6ySjk,834
|
|
4
4
|
aws_bootstrap/ec2.py,sha256=-yEyGMCycY4ccsmbgqHnLK2FRFWX2kr7nLfYWXSKeaY,12242
|
|
5
5
|
aws_bootstrap/ssh.py,sha256=-8F0PAkl7CCY1b9n46ZhWJ6faIMlSvA26BleeIp-rMA,17533
|
|
@@ -9,14 +9,14 @@ aws_bootstrap/resources/gpu_smoke_test.ipynb,sha256=XvAOEIPa5H9ri5mRZqOdknmwOwKN
|
|
|
9
9
|
aws_bootstrap/resources/remote_setup.sh,sha256=FzpXEw-LvlXROi-PmO72yEyDWWi-3Tul6D7-vFDubXQ,5460
|
|
10
10
|
aws_bootstrap/resources/requirements.txt,sha256=gpYl1MFCfWXiAhbIUgAjuTHONz3MKci25msIyOkMmUk,75
|
|
11
11
|
aws_bootstrap/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
aws_bootstrap/tests/test_cli.py,sha256=
|
|
12
|
+
aws_bootstrap/tests/test_cli.py,sha256=xGCC07aPZMc5pExo__qz7X1Tm2v9Z1Xn4K99JocESas,23627
|
|
13
13
|
aws_bootstrap/tests/test_config.py,sha256=arvET6KNl4Vqsz0zFrSdhciXGU688bfsvCr3dSpziN0,1050
|
|
14
14
|
aws_bootstrap/tests/test_ec2.py,sha256=Jmqsjv973hxXbZWfGgECtm6aa2156Lzji227sYMBuMg,10547
|
|
15
15
|
aws_bootstrap/tests/test_ssh_config.py,sha256=Rt3e7B22d8kK0PzFgXB4gwpF4HvIseiqzcpouCwMo5M,10333
|
|
16
16
|
aws_bootstrap/tests/test_ssh_gpu.py,sha256=W6GQzILCy_qPrvWQlCC8Ris-vuTzTGiyNXEyMzwD1kM,5154
|
|
17
|
-
aws_bootstrap_g4dn-0.
|
|
18
|
-
aws_bootstrap_g4dn-0.
|
|
19
|
-
aws_bootstrap_g4dn-0.
|
|
20
|
-
aws_bootstrap_g4dn-0.
|
|
21
|
-
aws_bootstrap_g4dn-0.
|
|
22
|
-
aws_bootstrap_g4dn-0.
|
|
17
|
+
aws_bootstrap_g4dn-0.2.0.dist-info/licenses/LICENSE,sha256=Hen77Mt8sazSQJ9DgrmZuAvDwo2vc5JAkR_avuFV-CM,1067
|
|
18
|
+
aws_bootstrap_g4dn-0.2.0.dist-info/METADATA,sha256=XqtBIr0EdnRvsy83usvZ5n-B9WNpfLCGoVaVR9_1eaI,11927
|
|
19
|
+
aws_bootstrap_g4dn-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
aws_bootstrap_g4dn-0.2.0.dist-info/entry_points.txt,sha256=T8FXfOgmLEvFi8DHaFJ3tCzId9J3_d2Y6qT98OXxCjA,57
|
|
21
|
+
aws_bootstrap_g4dn-0.2.0.dist-info/top_level.txt,sha256=mix9gZRs8JUv0OMSB_rwdGcRnTKzsKgHrE5fyAn5zJw,14
|
|
22
|
+
aws_bootstrap_g4dn-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|