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.
@@ -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 = {"PublicIpAddress": "1.2.3.4"}
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 = {"PublicIpAddress": "1.2.3.4"}
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