aws-bootstrap-g4dn 0.4.0__py3-none-any.whl → 0.6.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 +190 -14
- aws_bootstrap/config.py +2 -0
- aws_bootstrap/ec2.py +128 -0
- aws_bootstrap/resources/remote_setup.sh +2 -2
- aws_bootstrap/ssh.py +149 -0
- aws_bootstrap/tests/test_cli.py +424 -4
- aws_bootstrap/tests/test_config.py +18 -0
- aws_bootstrap/tests/test_ebs.py +245 -0
- aws_bootstrap/tests/test_ssh_config.py +152 -0
- aws_bootstrap/tests/test_ssh_ebs.py +76 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.6.0.dist-info}/METADATA +62 -10
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.6.0.dist-info}/RECORD +16 -14
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.6.0.dist-info}/WHEEL +0 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.6.0.dist-info}/entry_points.txt +0 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {aws_bootstrap_g4dn-0.4.0.dist-info → aws_bootstrap_g4dn-0.6.0.dist-info}/top_level.txt +0 -0
aws_bootstrap/tests/test_cli.py
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
from datetime import UTC, datetime
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from unittest.mock import patch
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
7
|
|
|
8
8
|
import botocore.exceptions
|
|
9
9
|
from click.testing import CliRunner
|
|
10
10
|
|
|
11
11
|
from aws_bootstrap.cli import main
|
|
12
12
|
from aws_bootstrap.gpu import GpuInfo
|
|
13
|
-
from aws_bootstrap.ssh import SSHHostDetails
|
|
13
|
+
from aws_bootstrap.ssh import CleanupResult, SSHHostDetails
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def test_help():
|
|
@@ -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):
|
|
@@ -295,7 +347,10 @@ def test_launch_output_shows_ssh_alias(
|
|
|
295
347
|
):
|
|
296
348
|
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
297
349
|
mock_launch.return_value = {"InstanceId": "i-test123"}
|
|
298
|
-
mock_wait.return_value = {
|
|
350
|
+
mock_wait.return_value = {
|
|
351
|
+
"PublicIpAddress": "1.2.3.4",
|
|
352
|
+
"Placement": {"AvailabilityZone": "us-west-2a"},
|
|
353
|
+
}
|
|
299
354
|
|
|
300
355
|
key_path = tmp_path / "id_ed25519.pub"
|
|
301
356
|
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
@@ -747,7 +802,10 @@ def test_launch_python_version_passed_to_setup(
|
|
|
747
802
|
):
|
|
748
803
|
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
749
804
|
mock_launch.return_value = {"InstanceId": "i-test123"}
|
|
750
|
-
mock_wait.return_value = {
|
|
805
|
+
mock_wait.return_value = {
|
|
806
|
+
"PublicIpAddress": "1.2.3.4",
|
|
807
|
+
"Placement": {"AvailabilityZone": "us-west-2a"},
|
|
808
|
+
}
|
|
751
809
|
|
|
752
810
|
key_path = tmp_path / "id_ed25519.pub"
|
|
753
811
|
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
@@ -834,3 +892,365 @@ def test_launch_dry_run_omits_ssh_port_when_default(mock_sg, mock_import, mock_a
|
|
|
834
892
|
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--dry-run"])
|
|
835
893
|
assert result.exit_code == 0
|
|
836
894
|
assert "SSH port" not in result.output
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# ---------------------------------------------------------------------------
|
|
898
|
+
# EBS data volume tests
|
|
899
|
+
# ---------------------------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def test_launch_help_shows_ebs_options():
|
|
903
|
+
runner = CliRunner()
|
|
904
|
+
result = runner.invoke(main, ["launch", "--help"])
|
|
905
|
+
assert result.exit_code == 0
|
|
906
|
+
assert "--ebs-storage" in result.output
|
|
907
|
+
assert "--ebs-volume-id" in result.output
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def test_launch_ebs_mutual_exclusivity(tmp_path):
|
|
911
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
912
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
913
|
+
|
|
914
|
+
runner = CliRunner()
|
|
915
|
+
result = runner.invoke(
|
|
916
|
+
main, ["launch", "--key-path", str(key_path), "--ebs-storage", "96", "--ebs-volume-id", "vol-abc"]
|
|
917
|
+
)
|
|
918
|
+
assert result.exit_code != 0
|
|
919
|
+
assert "mutually exclusive" in result.output
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
923
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
924
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
925
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
926
|
+
def test_launch_dry_run_with_ebs_storage(mock_sg, mock_import, mock_ami, mock_session, tmp_path):
|
|
927
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
928
|
+
|
|
929
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
930
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
931
|
+
|
|
932
|
+
runner = CliRunner()
|
|
933
|
+
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--dry-run", "--ebs-storage", "96"])
|
|
934
|
+
assert result.exit_code == 0
|
|
935
|
+
assert "96 GB gp3" in result.output
|
|
936
|
+
assert "/data" in result.output
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
940
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
941
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
942
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
943
|
+
def test_launch_dry_run_with_ebs_volume_id(mock_sg, mock_import, mock_ami, mock_session, tmp_path):
|
|
944
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
945
|
+
|
|
946
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
947
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
948
|
+
|
|
949
|
+
runner = CliRunner()
|
|
950
|
+
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--dry-run", "--ebs-volume-id", "vol-abc"])
|
|
951
|
+
assert result.exit_code == 0
|
|
952
|
+
assert "vol-abc" in result.output
|
|
953
|
+
assert "/data" in result.output
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
@patch("aws_bootstrap.cli.mount_ebs_volume", return_value=True)
|
|
957
|
+
@patch("aws_bootstrap.cli.attach_ebs_volume")
|
|
958
|
+
@patch("aws_bootstrap.cli.create_ebs_volume", return_value="vol-new123")
|
|
959
|
+
@patch("aws_bootstrap.cli.add_ssh_host", return_value="aws-gpu1")
|
|
960
|
+
@patch("aws_bootstrap.cli.run_remote_setup", return_value=True)
|
|
961
|
+
@patch("aws_bootstrap.cli.wait_for_ssh", return_value=True)
|
|
962
|
+
@patch("aws_bootstrap.cli.wait_instance_ready")
|
|
963
|
+
@patch("aws_bootstrap.cli.launch_instance")
|
|
964
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
965
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
966
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
967
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
968
|
+
def test_launch_with_ebs_storage_full_flow(
|
|
969
|
+
mock_session,
|
|
970
|
+
mock_ami,
|
|
971
|
+
mock_import,
|
|
972
|
+
mock_sg,
|
|
973
|
+
mock_launch,
|
|
974
|
+
mock_wait,
|
|
975
|
+
mock_ssh,
|
|
976
|
+
mock_setup,
|
|
977
|
+
mock_add_ssh,
|
|
978
|
+
mock_create_ebs,
|
|
979
|
+
mock_attach_ebs,
|
|
980
|
+
mock_mount_ebs,
|
|
981
|
+
tmp_path,
|
|
982
|
+
):
|
|
983
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
984
|
+
mock_launch.return_value = {"InstanceId": "i-test123"}
|
|
985
|
+
mock_wait.return_value = {
|
|
986
|
+
"PublicIpAddress": "1.2.3.4",
|
|
987
|
+
"Placement": {"AvailabilityZone": "us-west-2a"},
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
991
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
992
|
+
|
|
993
|
+
runner = CliRunner()
|
|
994
|
+
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--ebs-storage", "96", "--no-setup"])
|
|
995
|
+
assert result.exit_code == 0
|
|
996
|
+
assert "vol-new123" in result.output
|
|
997
|
+
mock_create_ebs.assert_called_once()
|
|
998
|
+
mock_attach_ebs.assert_called_once()
|
|
999
|
+
mock_mount_ebs.assert_called_once()
|
|
1000
|
+
# Verify format_volume=True for new volumes
|
|
1001
|
+
assert mock_mount_ebs.call_args[1]["format_volume"] is True
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
@patch("aws_bootstrap.cli.mount_ebs_volume", return_value=True)
|
|
1005
|
+
@patch("aws_bootstrap.cli.attach_ebs_volume")
|
|
1006
|
+
@patch("aws_bootstrap.cli.validate_ebs_volume")
|
|
1007
|
+
@patch("aws_bootstrap.cli.add_ssh_host", return_value="aws-gpu1")
|
|
1008
|
+
@patch("aws_bootstrap.cli.run_remote_setup", return_value=True)
|
|
1009
|
+
@patch("aws_bootstrap.cli.wait_for_ssh", return_value=True)
|
|
1010
|
+
@patch("aws_bootstrap.cli.wait_instance_ready")
|
|
1011
|
+
@patch("aws_bootstrap.cli.launch_instance")
|
|
1012
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
1013
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
1014
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
1015
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1016
|
+
def test_launch_with_ebs_volume_id_full_flow(
|
|
1017
|
+
mock_session,
|
|
1018
|
+
mock_ami,
|
|
1019
|
+
mock_import,
|
|
1020
|
+
mock_sg,
|
|
1021
|
+
mock_launch,
|
|
1022
|
+
mock_wait,
|
|
1023
|
+
mock_ssh,
|
|
1024
|
+
mock_setup,
|
|
1025
|
+
mock_add_ssh,
|
|
1026
|
+
mock_validate,
|
|
1027
|
+
mock_attach_ebs,
|
|
1028
|
+
mock_mount_ebs,
|
|
1029
|
+
tmp_path,
|
|
1030
|
+
):
|
|
1031
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
1032
|
+
mock_launch.return_value = {"InstanceId": "i-test123"}
|
|
1033
|
+
mock_wait.return_value = {
|
|
1034
|
+
"PublicIpAddress": "1.2.3.4",
|
|
1035
|
+
"Placement": {"AvailabilityZone": "us-west-2a"},
|
|
1036
|
+
}
|
|
1037
|
+
mock_validate.return_value = {"VolumeId": "vol-existing", "Size": 200}
|
|
1038
|
+
|
|
1039
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
1040
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
1041
|
+
|
|
1042
|
+
runner = CliRunner()
|
|
1043
|
+
result = runner.invoke(
|
|
1044
|
+
main, ["launch", "--key-path", str(key_path), "--ebs-volume-id", "vol-existing", "--no-setup"]
|
|
1045
|
+
)
|
|
1046
|
+
assert result.exit_code == 0
|
|
1047
|
+
mock_validate.assert_called_once()
|
|
1048
|
+
mock_attach_ebs.assert_called_once()
|
|
1049
|
+
mock_mount_ebs.assert_called_once()
|
|
1050
|
+
# Verify format_volume=False for existing volumes
|
|
1051
|
+
assert mock_mount_ebs.call_args[1]["format_volume"] is False
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
@patch("aws_bootstrap.cli.mount_ebs_volume", return_value=False)
|
|
1055
|
+
@patch("aws_bootstrap.cli.attach_ebs_volume")
|
|
1056
|
+
@patch("aws_bootstrap.cli.create_ebs_volume", return_value="vol-new123")
|
|
1057
|
+
@patch("aws_bootstrap.cli.add_ssh_host", return_value="aws-gpu1")
|
|
1058
|
+
@patch("aws_bootstrap.cli.wait_for_ssh", return_value=True)
|
|
1059
|
+
@patch("aws_bootstrap.cli.wait_instance_ready")
|
|
1060
|
+
@patch("aws_bootstrap.cli.launch_instance")
|
|
1061
|
+
@patch("aws_bootstrap.cli.ensure_security_group", return_value="sg-123")
|
|
1062
|
+
@patch("aws_bootstrap.cli.import_key_pair", return_value="aws-bootstrap-key")
|
|
1063
|
+
@patch("aws_bootstrap.cli.get_latest_ami")
|
|
1064
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1065
|
+
def test_launch_ebs_mount_failure_warns(
|
|
1066
|
+
mock_session,
|
|
1067
|
+
mock_ami,
|
|
1068
|
+
mock_import,
|
|
1069
|
+
mock_sg,
|
|
1070
|
+
mock_launch,
|
|
1071
|
+
mock_wait,
|
|
1072
|
+
mock_ssh,
|
|
1073
|
+
mock_add_ssh,
|
|
1074
|
+
mock_create_ebs,
|
|
1075
|
+
mock_attach_ebs,
|
|
1076
|
+
mock_mount_ebs,
|
|
1077
|
+
tmp_path,
|
|
1078
|
+
):
|
|
1079
|
+
mock_ami.return_value = {"ImageId": "ami-123", "Name": "TestAMI"}
|
|
1080
|
+
mock_launch.return_value = {"InstanceId": "i-test123"}
|
|
1081
|
+
mock_wait.return_value = {
|
|
1082
|
+
"PublicIpAddress": "1.2.3.4",
|
|
1083
|
+
"Placement": {"AvailabilityZone": "us-west-2a"},
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
key_path = tmp_path / "id_ed25519.pub"
|
|
1087
|
+
key_path.write_text("ssh-ed25519 AAAA test@host")
|
|
1088
|
+
|
|
1089
|
+
runner = CliRunner()
|
|
1090
|
+
result = runner.invoke(main, ["launch", "--key-path", str(key_path), "--ebs-storage", "96", "--no-setup"])
|
|
1091
|
+
# Should succeed despite mount failure (just a warning)
|
|
1092
|
+
assert result.exit_code == 0
|
|
1093
|
+
assert "WARNING" in result.output or "Failed to mount" in result.output
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def test_terminate_help_shows_keep_ebs():
|
|
1097
|
+
runner = CliRunner()
|
|
1098
|
+
result = runner.invoke(main, ["terminate", "--help"])
|
|
1099
|
+
assert result.exit_code == 0
|
|
1100
|
+
assert "--keep-ebs" in result.output
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
@patch("aws_bootstrap.cli.delete_ebs_volume")
|
|
1104
|
+
@patch("aws_bootstrap.cli.find_ebs_volumes_for_instance")
|
|
1105
|
+
@patch("aws_bootstrap.cli.remove_ssh_host", return_value=None)
|
|
1106
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1107
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
1108
|
+
@patch("aws_bootstrap.cli.terminate_tagged_instances")
|
|
1109
|
+
def test_terminate_deletes_ebs_by_default(
|
|
1110
|
+
mock_terminate, mock_find, mock_session, mock_remove_ssh, mock_find_ebs, mock_delete_ebs
|
|
1111
|
+
):
|
|
1112
|
+
mock_find.return_value = [
|
|
1113
|
+
{
|
|
1114
|
+
"InstanceId": "i-abc123",
|
|
1115
|
+
"Name": "test",
|
|
1116
|
+
"State": "running",
|
|
1117
|
+
"InstanceType": "g4dn.xlarge",
|
|
1118
|
+
"PublicIp": "1.2.3.4",
|
|
1119
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
1120
|
+
}
|
|
1121
|
+
]
|
|
1122
|
+
mock_terminate.return_value = [
|
|
1123
|
+
{
|
|
1124
|
+
"InstanceId": "i-abc123",
|
|
1125
|
+
"PreviousState": {"Name": "running"},
|
|
1126
|
+
"CurrentState": {"Name": "shutting-down"},
|
|
1127
|
+
}
|
|
1128
|
+
]
|
|
1129
|
+
mock_find_ebs.return_value = [{"VolumeId": "vol-data1", "Size": 96, "Device": "/dev/sdf", "State": "in-use"}]
|
|
1130
|
+
|
|
1131
|
+
# Mock the ec2 client's get_waiter for volume_available
|
|
1132
|
+
mock_ec2 = mock_session.return_value.client.return_value
|
|
1133
|
+
mock_waiter = MagicMock()
|
|
1134
|
+
mock_ec2.get_waiter.return_value = mock_waiter
|
|
1135
|
+
|
|
1136
|
+
runner = CliRunner()
|
|
1137
|
+
result = runner.invoke(main, ["terminate", "--yes"])
|
|
1138
|
+
assert result.exit_code == 0
|
|
1139
|
+
mock_delete_ebs.assert_called_once_with(mock_ec2, "vol-data1")
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
@patch("aws_bootstrap.cli.delete_ebs_volume")
|
|
1143
|
+
@patch("aws_bootstrap.cli.find_ebs_volumes_for_instance")
|
|
1144
|
+
@patch("aws_bootstrap.cli.remove_ssh_host", return_value=None)
|
|
1145
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1146
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
1147
|
+
@patch("aws_bootstrap.cli.terminate_tagged_instances")
|
|
1148
|
+
def test_terminate_keep_ebs_preserves(
|
|
1149
|
+
mock_terminate, mock_find, mock_session, mock_remove_ssh, mock_find_ebs, mock_delete_ebs
|
|
1150
|
+
):
|
|
1151
|
+
mock_find.return_value = [
|
|
1152
|
+
{
|
|
1153
|
+
"InstanceId": "i-abc123",
|
|
1154
|
+
"Name": "test",
|
|
1155
|
+
"State": "running",
|
|
1156
|
+
"InstanceType": "g4dn.xlarge",
|
|
1157
|
+
"PublicIp": "1.2.3.4",
|
|
1158
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
1159
|
+
}
|
|
1160
|
+
]
|
|
1161
|
+
mock_terminate.return_value = [
|
|
1162
|
+
{
|
|
1163
|
+
"InstanceId": "i-abc123",
|
|
1164
|
+
"PreviousState": {"Name": "running"},
|
|
1165
|
+
"CurrentState": {"Name": "shutting-down"},
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
mock_find_ebs.return_value = [{"VolumeId": "vol-data1", "Size": 96, "Device": "/dev/sdf", "State": "in-use"}]
|
|
1169
|
+
|
|
1170
|
+
runner = CliRunner()
|
|
1171
|
+
result = runner.invoke(main, ["terminate", "--yes", "--keep-ebs"])
|
|
1172
|
+
assert result.exit_code == 0
|
|
1173
|
+
assert "Preserving EBS volume: vol-data1" in result.output
|
|
1174
|
+
assert "aws-bootstrap launch --ebs-volume-id vol-data1" in result.output
|
|
1175
|
+
mock_delete_ebs.assert_not_called()
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
@patch("aws_bootstrap.cli.find_ebs_volumes_for_instance")
|
|
1179
|
+
@patch("aws_bootstrap.cli.get_ssh_host_details", return_value=None)
|
|
1180
|
+
@patch("aws_bootstrap.cli.list_ssh_hosts", return_value={})
|
|
1181
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1182
|
+
@patch("aws_bootstrap.cli.get_spot_price")
|
|
1183
|
+
@patch("aws_bootstrap.cli.find_tagged_instances")
|
|
1184
|
+
def test_status_shows_ebs_volumes(mock_find, mock_spot_price, mock_session, mock_ssh_hosts, mock_details, mock_ebs):
|
|
1185
|
+
mock_find.return_value = [
|
|
1186
|
+
{
|
|
1187
|
+
"InstanceId": "i-abc123",
|
|
1188
|
+
"Name": "aws-bootstrap-g4dn.xlarge",
|
|
1189
|
+
"State": "running",
|
|
1190
|
+
"InstanceType": "g4dn.xlarge",
|
|
1191
|
+
"PublicIp": "1.2.3.4",
|
|
1192
|
+
"LaunchTime": datetime(2025, 1, 1, tzinfo=UTC),
|
|
1193
|
+
"Lifecycle": "spot",
|
|
1194
|
+
"AvailabilityZone": "us-west-2a",
|
|
1195
|
+
}
|
|
1196
|
+
]
|
|
1197
|
+
mock_spot_price.return_value = 0.15
|
|
1198
|
+
mock_ebs.return_value = [{"VolumeId": "vol-data1", "Size": 96, "Device": "/dev/sdf", "State": "in-use"}]
|
|
1199
|
+
|
|
1200
|
+
runner = CliRunner()
|
|
1201
|
+
result = runner.invoke(main, ["status"])
|
|
1202
|
+
assert result.exit_code == 0
|
|
1203
|
+
assert "vol-data1" in result.output
|
|
1204
|
+
assert "96 GB" in result.output
|
|
1205
|
+
assert "/data" in result.output
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
# ---------------------------------------------------------------------------
|
|
1209
|
+
# cleanup subcommand
|
|
1210
|
+
# ---------------------------------------------------------------------------
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def test_cleanup_help():
|
|
1214
|
+
runner = CliRunner()
|
|
1215
|
+
result = runner.invoke(main, ["cleanup", "--help"])
|
|
1216
|
+
assert result.exit_code == 0
|
|
1217
|
+
assert "--dry-run" in result.output
|
|
1218
|
+
assert "--yes" in result.output
|
|
1219
|
+
assert "--region" in result.output
|
|
1220
|
+
assert "--profile" in result.output
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
@patch("aws_bootstrap.cli.find_stale_ssh_hosts", return_value=[])
|
|
1224
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1225
|
+
@patch("aws_bootstrap.cli.find_tagged_instances", return_value=[])
|
|
1226
|
+
def test_cleanup_no_stale(mock_find, mock_session, mock_stale):
|
|
1227
|
+
runner = CliRunner()
|
|
1228
|
+
result = runner.invoke(main, ["cleanup"])
|
|
1229
|
+
assert result.exit_code == 0
|
|
1230
|
+
assert "No stale" in result.output
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
@patch("aws_bootstrap.cli.find_stale_ssh_hosts", return_value=[("i-dead1234", "aws-gpu1")])
|
|
1234
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1235
|
+
@patch("aws_bootstrap.cli.find_tagged_instances", return_value=[])
|
|
1236
|
+
def test_cleanup_dry_run(mock_find, mock_session, mock_stale):
|
|
1237
|
+
runner = CliRunner()
|
|
1238
|
+
result = runner.invoke(main, ["cleanup", "--dry-run"])
|
|
1239
|
+
assert result.exit_code == 0
|
|
1240
|
+
assert "Would remove" in result.output
|
|
1241
|
+
assert "aws-gpu1" in result.output
|
|
1242
|
+
assert "i-dead1234" in result.output
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
@patch("aws_bootstrap.cli.cleanup_stale_ssh_hosts")
|
|
1246
|
+
@patch("aws_bootstrap.cli.find_stale_ssh_hosts", return_value=[("i-dead1234", "aws-gpu1")])
|
|
1247
|
+
@patch("aws_bootstrap.cli.boto3.Session")
|
|
1248
|
+
@patch("aws_bootstrap.cli.find_tagged_instances", return_value=[])
|
|
1249
|
+
def test_cleanup_with_yes(mock_find, mock_session, mock_stale, mock_cleanup):
|
|
1250
|
+
mock_cleanup.return_value = [CleanupResult(instance_id="i-dead1234", alias="aws-gpu1", removed=True)]
|
|
1251
|
+
runner = CliRunner()
|
|
1252
|
+
result = runner.invoke(main, ["cleanup", "--yes"])
|
|
1253
|
+
assert result.exit_code == 0
|
|
1254
|
+
assert "Removed aws-gpu1" in result.output
|
|
1255
|
+
assert "Cleaned up 1" in result.output
|
|
1256
|
+
mock_cleanup.assert_called_once()
|
|
@@ -20,6 +20,12 @@ def test_defaults():
|
|
|
20
20
|
assert config.dry_run is False
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def test_ebs_fields_default_none():
|
|
24
|
+
config = LaunchConfig()
|
|
25
|
+
assert config.ebs_storage is None
|
|
26
|
+
assert config.ebs_volume_id is None
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
def test_overrides():
|
|
24
30
|
config = LaunchConfig(
|
|
25
31
|
instance_type="g5.xlarge",
|
|
@@ -33,3 +39,15 @@ def test_overrides():
|
|
|
33
39
|
assert config.spot is False
|
|
34
40
|
assert config.volume_size == 200
|
|
35
41
|
assert config.key_path == Path("/tmp/test.pub")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_ebs_storage_override():
|
|
45
|
+
config = LaunchConfig(ebs_storage=96)
|
|
46
|
+
assert config.ebs_storage == 96
|
|
47
|
+
assert config.ebs_volume_id is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_ebs_volume_id_override():
|
|
51
|
+
config = LaunchConfig(ebs_volume_id="vol-abc123")
|
|
52
|
+
assert config.ebs_volume_id == "vol-abc123"
|
|
53
|
+
assert config.ebs_storage is None
|