1use std::collections::HashMap;
6
7use fakecloud_aws::ec2query::{ec2_elem, ec2_list, ec2_return};
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
9
10use crate::service::Ec2Service;
11use crate::service_helpers::{
12 gen_id, indexed_list, instance_limit_exceeded, invalid_parameter_value, missing_parameter,
13 parse_filters, require, require_struct, validate_enum, Filter,
14};
15use crate::state::{Ec2State, Instance, Tag};
16
17const LAUNCH_TIME: &str = "2024-01-01T00:00:00.000Z";
18
19const INSTANCE_TYPES: &[&str] = &[
20 "t3.micro",
21 "t3.small",
22 "t3.medium",
23 "t3.large",
24 "m5.large",
25 "m5.xlarge",
26 "c5.large",
27 "r5.large",
28 "t2.micro",
29];
30
31fn state_xml(tag: &str, code: i64, name: &str) -> String {
32 format!("<{tag}><code>{code}</code><name>{name}</name></{tag}>")
33}
34
35fn subnet_ip_prefix(cidr: &str) -> String {
39 let addr = cidr.split('/').next().unwrap_or(cidr);
40 let octets: Vec<&str> = addr.split('.').collect();
41 if octets.len() == 4 && octets.iter().all(|o| o.parse::<u8>().is_ok()) {
42 format!("{}.{}.{}", octets[0], octets[1], octets[2])
43 } else {
44 "10.0.0".to_string()
45 }
46}
47
48fn sg_name_map(state: &Ec2State) -> HashMap<String, String> {
52 state
53 .security_groups
54 .values()
55 .map(|g| (g.group_id.clone(), g.group_name.clone()))
56 .collect()
57}
58
59fn arch_for(state: &Ec2State, image_id: &str) -> String {
63 state
64 .images
65 .get(image_id)
66 .map(|img| img.architecture.clone())
67 .unwrap_or_else(|| "x86_64".to_string())
68}
69
70fn instance_xml(
71 i: &Instance,
72 tags: &[Tag],
73 owner: &str,
74 sg_names: &HashMap<String, String>,
75 architecture: &str,
76) -> String {
77 let groups: Vec<String> = i
78 .security_group_ids
79 .iter()
80 .map(|g| {
81 let name = sg_names.get(g).map(String::as_str).unwrap_or(g.as_str());
82 format!("{}{}", ec2_elem("groupId", g), ec2_elem("groupName", name))
83 })
84 .collect();
85 let public = i
86 .public_ip
87 .as_ref()
88 .map(|ip| {
89 format!(
90 "{}{}",
91 ec2_elem("ipAddress", ip),
92 ec2_elem(
93 "dnsName",
94 &format!("ec2-{}.compute.amazonaws.com", ip.replace('.', "-"))
95 )
96 )
97 })
98 .unwrap_or_default();
99 let m = &i.metadata_options;
100 let metadata_options = format!(
101 "<metadataOptions><state>applied</state><httpTokens>{}</httpTokens>\
102 <httpPutResponseHopLimit>{}</httpPutResponseHopLimit><httpEndpoint>{}</httpEndpoint>\
103 <httpProtocolIpv6>{}</httpProtocolIpv6><instanceMetadataTags>{}</instanceMetadataTags></metadataOptions>",
104 m.http_tokens,
105 m.http_put_response_hop_limit,
106 m.http_endpoint,
107 m.http_protocol_ipv6,
108 m.instance_metadata_tags,
109 );
110 let cpu_options = i
111 .cpu_options
112 .as_ref()
113 .map(|c| {
114 format!(
115 "<cpuOptions><coreCount>{}</coreCount><threadsPerCore>{}</threadsPerCore></cpuOptions>",
116 c.core_count, c.threads_per_core
117 )
118 })
119 .unwrap_or_default();
120 let private_dns_name_options = format!(
121 "<privateDnsNameOptions><hostnameType>{}</hostnameType>\
122 <enableResourceNameDnsARecord>{}</enableResourceNameDnsARecord>\
123 <enableResourceNameDnsAAAARecord>{}</enableResourceNameDnsAAAARecord></privateDnsNameOptions>",
124 i.private_dns_hostname_type.as_deref().unwrap_or("ip-name"),
125 i.enable_resource_name_dns_a_record,
126 i.enable_resource_name_dns_aaaa_record,
127 );
128 let tenancy = i.placement_tenancy.as_deref().unwrap_or("default");
129 let placement_group = i
130 .placement_group_name
131 .as_ref()
132 .map(|g| ec2_elem("groupName", g))
133 .unwrap_or_default();
134 format!(
135 "{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
136 ec2_elem("instanceId", &i.instance_id),
137 ec2_elem("imageId", &i.image_id),
138 state_xml("instanceState", i.state_code, &i.state_name),
139 ec2_elem(
140 "privateDnsName",
141 &format!("ip-{}.ec2.internal", i.private_ip.replace('.', "-"))
142 ),
143 ec2_elem("privateIpAddress", &i.private_ip),
144 public,
145 ec2_elem("instanceType", &i.instance_type),
146 ec2_elem("launchTime", &i.launch_time),
147 ec2_elem("amiLaunchIndex", &i.ami_launch_index.to_string()),
148 ec2_elem("architecture", architecture),
149 ec2_elem("rootDeviceType", "ebs"),
150 ec2_elem("rootDeviceName", "/dev/xvda"),
151 ec2_elem("virtualizationType", "hvm"),
152 ec2_elem("hypervisor", "xen"),
153 format_args!(
154 "<ebsOptimized>{}</ebsOptimized><sourceDestCheck>{}</sourceDestCheck>",
155 i.ebs_optimized, i.source_dest_check
156 ),
157 format_args!(
158 "<placement><availabilityZone>{}</availabilityZone>{}<tenancy>{}</tenancy></placement>",
159 i.az, placement_group, tenancy
160 ),
161 format_args!(
162 "<monitoring><state>{}</state></monitoring>",
163 if i.monitoring { "enabled" } else { "disabled" }
164 ),
165 format_args!(
166 "{}{}",
167 i.subnet_id
168 .as_ref()
169 .map(|s| ec2_elem("subnetId", s))
170 .unwrap_or_default(),
171 i.vpc_id
172 .as_ref()
173 .map(|s| ec2_elem("vpcId", s))
174 .unwrap_or_default(),
175 ),
176 i.key_name
177 .as_ref()
178 .map(|k| ec2_elem("keyName", k))
179 .unwrap_or_default(),
180 format_args!(
181 "{}{}",
182 ec2_list("groupSet", &groups),
183 ec2_elem("ownerId", owner)
184 ),
185 metadata_options,
186 cpu_options,
187 super::tags::tag_set_xml(tags),
188 private_dns_name_options,
189 )
190}
191
192fn reservation_xml(reservation_id: &str, owner: &str, instances: &[String]) -> String {
193 format!(
194 "{}{}{}{}",
195 ec2_elem("reservationId", reservation_id),
196 ec2_elem("ownerId", owner),
197 ec2_list("groupSet", &[]),
198 ec2_list("instancesSet", instances),
199 )
200}
201
202pub(crate) async fn run_instances(
203 svc: &Ec2Service,
204 req: &AwsRequest,
205) -> Result<AwsResponse, AwsServiceError> {
206 let min: usize = require(&req.query_params, "MinCount")?
207 .parse()
208 .map_err(|_| invalid_parameter_value("MinCount must be an integer"))?;
209 let max: usize = require(&req.query_params, "MaxCount")?
210 .parse()
211 .map_err(|_| invalid_parameter_value("MaxCount must be an integer"))?;
212 if min == 0 {
213 return Err(invalid_parameter_value("MinCount must be at least 1"));
214 }
215 if min > max {
216 return Err(invalid_parameter_value(format!(
217 "Invalid value '{max}' for parameter maxCount is invalid. The maxCount must be equal to or greater than the minCount."
218 )));
219 }
220 const MAX_INSTANCES_PER_REQUEST: usize = 64;
225 if min > MAX_INSTANCES_PER_REQUEST {
226 return Err(instance_limit_exceeded(format!(
227 "You have requested more instances ({min}) than your current instance limit of {MAX_INSTANCES_PER_REQUEST} allows for this launch."
228 )));
229 }
230 validate_enum_instance_type(req)?;
231 validate_enum(
232 &req.query_params,
233 "InstanceInitiatedShutdownBehavior",
234 &["stop", "terminate"],
235 )?;
236 let count = max.min(MAX_INSTANCES_PER_REQUEST).max(min);
241 let reservation_id = gen_id("r");
242 let uses_launch_template = req.query_params.keys().any(|k| {
246 k.starts_with("LaunchTemplate.LaunchTemplateId")
247 || k.starts_with("LaunchTemplate.LaunchTemplateName")
248 });
249 let image_id = match req.query_params.get("ImageId").filter(|v| !v.is_empty()) {
250 Some(v) => v.clone(),
251 None if uses_launch_template => "ami-00000000000000000".to_string(),
252 None => return Err(missing_parameter("ImageId")),
253 };
254 let instance_type = req
255 .query_params
256 .get("InstanceType")
257 .cloned()
258 .unwrap_or_else(|| "t3.micro".to_string());
259 let key_name = req.query_params.get("KeyName").cloned();
260 let mut subnet_id = req.query_params.get("SubnetId").cloned();
261 let mut sg_ids = indexed_list(&req.query_params, "SecurityGroupId");
262 let user_data = req.query_params.get("UserData").cloned();
263 let owner = req.account_id.clone();
264 let az = format!(
265 "{}a",
266 if req.region.is_empty() {
267 "us-east-1"
268 } else {
269 &req.region
270 }
271 );
272
273 let instance_tags = crate::service::tags::tag_specifications_for(&req.query_params, "instance");
276
277 let assoc_public = req
284 .query_params
285 .get("NetworkInterface.1.AssociatePublicIpAddress")
286 .or_else(|| req.query_params.get("AssociatePublicIpAddress"))
287 .map(|v| v == "true");
288 let (vpc_id, subnet_auto_public, instance_network, ip_prefix) = {
289 let accounts = svc.state.read();
290 let empty = Ec2State::new(&req.account_id, &req.region);
291 let state = accounts.get(&req.account_id).unwrap_or(&empty);
292 if subnet_id.is_none() {
297 if let Some(s) = state
298 .subnets
299 .values()
300 .filter(|s| s.default_for_az)
301 .find(|s| s.availability_zone == az)
302 .or_else(|| state.subnets.values().find(|s| s.default_for_az))
303 {
304 subnet_id = Some(s.subnet_id.clone());
305 }
306 }
307 let resolved_vpc = subnet_id
310 .as_ref()
311 .and_then(|sid| state.subnets.get(sid))
312 .map(|s| s.vpc_id.clone());
313 let sg_names = indexed_list(&req.query_params, "SecurityGroup");
316 if !sg_names.is_empty() {
317 let target_vpc = resolved_vpc
318 .clone()
319 .unwrap_or_else(|| crate::defaults::default_vpc_id(&req.account_id));
320 for name in &sg_names {
321 if let Some(sg) = state
322 .security_groups
323 .values()
324 .find(|g| g.vpc_id == target_vpc && &g.group_name == name)
325 {
326 if !sg_ids.contains(&sg.group_id) {
327 sg_ids.push(sg.group_id.clone());
328 }
329 }
330 }
331 }
332 if sg_ids.is_empty() {
333 let vpc = resolved_vpc
334 .clone()
335 .unwrap_or_else(|| crate::defaults::default_vpc_id(&req.account_id));
336 if let Some(sg) = state
337 .security_groups
338 .values()
339 .find(|g| g.vpc_id == vpc && g.group_name == "default")
340 {
341 sg_ids = vec![sg.group_id.clone()];
342 }
343 }
344 let instance_network = subnet_id
347 .as_ref()
348 .map(|sid| crate::runtime::InstanceNetwork {
349 subnet_id: sid.clone(),
350 internal: !crate::defaults::subnet_is_public(state, sid),
351 });
352 let (vpc, auto_public) = match subnet_id.as_ref() {
353 Some(sid) => state
354 .subnets
355 .get(sid)
356 .map(|s| {
357 (
358 Some(s.vpc_id.clone()),
359 s.map_public_ip_on_launch || s.default_for_az,
360 )
361 })
362 .unwrap_or((None, false)),
363 None => (Some(crate::defaults::default_vpc_id(&req.account_id)), true),
366 };
367 let ip_prefix = subnet_id
373 .as_ref()
374 .and_then(|sid| state.subnets.get(sid))
375 .map(|s| subnet_ip_prefix(&s.cidr_block))
376 .unwrap_or_else(|| "10.0.0".to_string());
377 (vpc, auto_public, instance_network, ip_prefix)
378 };
379 let assign_public = assoc_public.unwrap_or(subnet_auto_public);
380
381 let ids: Vec<String> = (0..count).map(|_| gen_id("i")).collect();
389 let mut rendered = Vec::new();
390 {
391 let mut accounts = svc.state.write();
392 let state = accounts.get_or_create(&req.account_id);
393 let sg_names = sg_name_map(state);
394 for (idx, id) in ids.iter().enumerate() {
395 let inst = Instance {
396 instance_id: id.clone(),
397 image_id: image_id.clone(),
398 instance_type: instance_type.clone(),
399 state_code: 0,
400 state_name: "pending".to_string(),
401 private_ip: format!("{ip_prefix}.{}", 10 + idx),
402 public_ip: if assign_public {
403 Some(format!("52.0.0.{}", 10 + idx))
404 } else {
405 None
406 },
407 subnet_id: subnet_id.clone(),
408 vpc_id: vpc_id.clone(),
409 key_name: key_name.clone(),
410 security_group_ids: sg_ids.clone(),
411 reservation_id: reservation_id.clone(),
412 ami_launch_index: idx as i64,
413 monitoring: false,
414 az: az.clone(),
415 launch_time: LAUNCH_TIME.to_string(),
416 container_id: None,
417 disable_api_termination: false,
418 disable_api_stop: false,
419 source_dest_check: true,
420 ebs_optimized: false,
421 instance_initiated_shutdown_behavior: req
422 .query_params
423 .get("InstanceInitiatedShutdownBehavior")
424 .cloned()
425 .unwrap_or_else(|| "stop".to_string()),
426 user_data: user_data.clone().filter(|s| !s.is_empty()),
427 metadata_options: crate::state::MetadataOptions::default(),
428 cpu_options: None,
429 bandwidth_weighting: None,
430 maintenance_options: crate::state::MaintenanceOptions::default(),
431 placement_tenancy: None,
432 placement_affinity: None,
433 placement_group_name: req.query_params.get("Placement.GroupName").cloned(),
434 private_dns_hostname_type: None,
435 enable_resource_name_dns_a_record: false,
436 enable_resource_name_dns_aaaa_record: false,
437 };
438 crate::service::tags::apply_tag_specifications(
439 state,
440 &req.query_params,
441 id,
442 "instance",
443 );
444 let tags = state.tags_for(id).to_vec();
445 let architecture = arch_for(state, &inst.image_id);
446 rendered.push(instance_xml(&inst, &tags, &owner, &sg_names, &architecture));
447 state.instances.insert(id.clone(), inst);
448 }
449 }
450
451 {
453 let svc_state = svc.state.clone();
454 let runtime = svc.runtime.clone();
455 let account_id = req.account_id.clone();
456 let ids = ids.clone();
457 let instance_network = instance_network.clone();
458 let snapshot_hook = svc.snapshot_hook();
462 tokio::spawn(async move {
463 for id in &ids {
464 let running = if let Some(rt) = &runtime {
465 match rt
466 .run_instance(
467 &account_id,
468 id,
469 user_data.as_deref(),
470 &instance_tags,
471 instance_network.as_ref(),
472 )
473 .await
474 {
475 Ok(r) => Some(r),
476 Err(e) => {
477 tracing::warn!(instance_id = %id, error = %e, "EC2 instance container failed to start; serving metadata-only");
478 None
479 }
480 }
481 } else {
482 None
483 };
484 reconcile_started(&svc_state, &account_id, id, running);
485 }
486 if let Some(hook) = &snapshot_hook {
489 hook().await;
490 }
491 if let Some(rt) = &runtime {
495 if rt.network_isolation_enforced() {
496 super::firewall_model::reconcile(&svc_state, rt).await;
497 }
498 }
499 });
500 }
501
502 let body = reservation_xml(&reservation_id, &owner, &rendered);
503 Ok(Ec2Service::respond("RunInstances", &req.request_id, &body))
504}
505
506fn reconcile_started(
511 state: &crate::state::SharedEc2State,
512 account_id: &str,
513 id: &str,
514 running: Option<crate::runtime::RunningInstance>,
515) {
516 let mut accounts = state.write();
517 let Some(s) = accounts.get_mut(account_id) else {
518 return;
519 };
520 let Some(inst) = s.instances.get_mut(id) else {
521 return;
522 };
523 if inst.state_code == 48 || inst.state_code == 80 {
525 return;
526 }
527 inst.state_code = 16;
528 inst.state_name = "running".to_string();
529 if let Some(r) = running {
530 inst.private_ip = r.private_ip;
531 inst.container_id = Some(r.container_id);
532 }
533}
534
535#[derive(Debug, Clone, Default)]
537pub struct CfnInstanceSpec {
538 pub image_id: Option<String>,
539 pub instance_type: Option<String>,
540 pub subnet_id: Option<String>,
541 pub availability_zone: Option<String>,
542 pub security_group_ids: Vec<String>,
543 pub key_name: Option<String>,
544 pub user_data: Option<String>,
545 pub private_ip: Option<String>,
546}
547
548#[derive(Debug, Clone)]
550pub struct CfnInstanceAttrs {
551 pub instance_id: String,
552 pub private_ip: String,
553 pub public_ip: Option<String>,
554 pub availability_zone: String,
555}
556
557pub(crate) fn cfn_create_instance(
564 svc: &Ec2Service,
565 account_id: &str,
566 region: &str,
567 spec: &CfnInstanceSpec,
568) -> CfnInstanceAttrs {
569 let image_id = spec
570 .image_id
571 .clone()
572 .unwrap_or_else(|| "ami-00000000000000000".to_string());
573 let instance_type = spec
574 .instance_type
575 .clone()
576 .unwrap_or_else(|| "t3.micro".to_string());
577 let region = if region.is_empty() {
578 "us-east-1"
579 } else {
580 region
581 };
582 let mut subnet_id = spec.subnet_id.clone();
583 let mut sg_ids = spec.security_group_ids.clone();
584
585 let (vpc_id, subnet_auto_public, ip_prefix, az) = {
586 let accounts = svc.state.read();
587 let empty = Ec2State::new(account_id, region);
588 let state = accounts.get(account_id).unwrap_or(&empty);
589 if subnet_id.is_none() {
592 let want_az = spec.availability_zone.clone();
593 if let Some(s) = state
594 .subnets
595 .values()
596 .filter(|s| s.default_for_az)
597 .find(|s| want_az.as_deref().is_none_or(|a| s.availability_zone == a))
598 .or_else(|| state.subnets.values().find(|s| s.default_for_az))
599 {
600 subnet_id = Some(s.subnet_id.clone());
601 }
602 }
603 let resolved_vpc = subnet_id
604 .as_ref()
605 .and_then(|sid| state.subnets.get(sid))
606 .map(|s| s.vpc_id.clone());
607 if sg_ids.is_empty() {
608 let vpc = resolved_vpc
609 .clone()
610 .unwrap_or_else(|| crate::defaults::default_vpc_id(account_id));
611 if let Some(sg) = state
612 .security_groups
613 .values()
614 .find(|g| g.vpc_id == vpc && g.group_name == "default")
615 {
616 sg_ids = vec![sg.group_id.clone()];
617 }
618 }
619 let (vpc, auto_public, az) = match subnet_id.as_ref() {
620 Some(sid) => state
621 .subnets
622 .get(sid)
623 .map(|s| {
624 (
625 Some(s.vpc_id.clone()),
626 s.map_public_ip_on_launch || s.default_for_az,
627 s.availability_zone.clone(),
628 )
629 })
630 .unwrap_or((None, false, format!("{region}a"))),
631 None => (
632 Some(crate::defaults::default_vpc_id(account_id)),
633 true,
634 format!("{region}a"),
635 ),
636 };
637 let az = spec.availability_zone.clone().unwrap_or(az);
638 let ip_prefix = subnet_id
639 .as_ref()
640 .and_then(|sid| state.subnets.get(sid))
641 .map(|s| subnet_ip_prefix(&s.cidr_block))
642 .unwrap_or_else(|| "10.0.0".to_string());
643 (vpc, auto_public, ip_prefix, az)
644 };
645
646 let assign_public = subnet_auto_public;
647 let id = gen_id("i");
648 let private_ip = spec
649 .private_ip
650 .clone()
651 .unwrap_or_else(|| format!("{ip_prefix}.10"));
652 let public_ip = if assign_public {
653 Some("52.0.0.10".to_string())
654 } else {
655 None
656 };
657
658 {
659 let mut accounts = svc.state.write();
660 let state = accounts.get_or_create(account_id);
661 let inst = Instance {
662 instance_id: id.clone(),
663 image_id,
664 instance_type,
665 state_code: 0,
666 state_name: "pending".to_string(),
667 private_ip: private_ip.clone(),
668 public_ip: public_ip.clone(),
669 subnet_id: subnet_id.clone(),
670 vpc_id: vpc_id.clone(),
671 key_name: spec.key_name.clone(),
672 security_group_ids: sg_ids,
673 reservation_id: gen_id("r"),
674 ami_launch_index: 0,
675 monitoring: false,
676 az: az.clone(),
677 launch_time: LAUNCH_TIME.to_string(),
678 container_id: None,
679 disable_api_termination: false,
680 disable_api_stop: false,
681 source_dest_check: true,
682 ebs_optimized: false,
683 instance_initiated_shutdown_behavior: "stop".to_string(),
684 user_data: spec.user_data.clone().filter(|s| !s.is_empty()),
685 metadata_options: crate::state::MetadataOptions::default(),
686 cpu_options: None,
687 bandwidth_weighting: None,
688 maintenance_options: crate::state::MaintenanceOptions::default(),
689 placement_tenancy: None,
690 placement_affinity: None,
691 placement_group_name: None,
692 private_dns_hostname_type: None,
693 enable_resource_name_dns_a_record: false,
694 enable_resource_name_dns_aaaa_record: false,
695 };
696 state.instances.insert(id.clone(), inst);
697 }
698
699 CfnInstanceAttrs {
700 instance_id: id,
701 private_ip,
702 public_ip,
703 availability_zone: az,
704 }
705}
706
707pub(crate) async fn cfn_boot_instance(svc: &Ec2Service, account_id: &str, id: &str) {
713 let (user_data, instance_network) = {
714 let accounts = svc.state.read();
715 let Some(state) = accounts.get(account_id) else {
716 return;
717 };
718 let Some(inst) = state.instances.get(id) else {
719 return;
720 };
721 let network = inst
722 .subnet_id
723 .as_ref()
724 .map(|sid| crate::runtime::InstanceNetwork {
725 subnet_id: sid.clone(),
726 internal: !crate::defaults::subnet_is_public(state, sid),
727 });
728 (inst.user_data.clone(), network)
729 };
730
731 let empty_tags = std::collections::BTreeMap::new();
732 let running = if let Some(rt) = &svc.runtime {
733 match rt
734 .run_instance(
735 account_id,
736 id,
737 user_data.as_deref(),
738 &empty_tags,
739 instance_network.as_ref(),
740 )
741 .await
742 {
743 Ok(r) => Some(r),
744 Err(e) => {
745 tracing::warn!(instance_id = %id, error = %e, "CFN EC2 instance container failed to start; serving metadata-only");
746 None
747 }
748 }
749 } else {
750 None
751 };
752 reconcile_started(&svc.state, account_id, id, running);
753
754 if let Some(rt) = &svc.runtime {
755 if rt.network_isolation_enforced() {
756 super::firewall_model::reconcile(&svc.state, rt).await;
757 }
758 }
759}
760
761pub(crate) async fn cfn_terminate_instance(
766 svc: &Ec2Service,
767 account_id: &str,
768 region: &str,
769 instance_id: &str,
770) {
771 let mut query_params = std::collections::HashMap::new();
772 query_params.insert("InstanceId.1".to_string(), instance_id.to_string());
773 let req = AwsRequest {
774 service: "ec2".to_string(),
775 action: "TerminateInstances".to_string(),
776 region: region.to_string(),
777 account_id: account_id.to_string(),
778 request_id: gen_id("req"),
779 headers: http::HeaderMap::new(),
780 query_params,
781 body: bytes::Bytes::new(),
782 body_stream: parking_lot::Mutex::new(None),
783 path_segments: Vec::new(),
784 raw_path: "/".to_string(),
785 raw_query: String::new(),
786 method: http::Method::POST,
787 is_query_protocol: true,
788 access_key_id: None,
789 principal: None,
790 };
791 let _ = terminate_instances(svc, &req).await;
792}
793
794fn validate_enum_instance_type(req: &AwsRequest) -> Result<(), AwsServiceError> {
795 if let Some(v) = req
798 .query_params
799 .get("InstanceType")
800 .filter(|v| !v.is_empty())
801 {
802 if !v.contains('.') {
803 return Err(invalid_parameter_value(format!(
804 "Invalid instance type '{v}'"
805 )));
806 }
807 }
808 Ok(())
809}
810
811fn transition_allowed(current: i64, new_code: i64) -> bool {
816 if current == 48 {
817 return new_code == 48;
819 }
820 true
821}
822
823fn check_transition(inst: &Instance, id: &str, new_code: i64) -> Result<(), AwsServiceError> {
830 if !transition_allowed(inst.state_code, new_code) {
831 return Err(crate::service_helpers::incorrect_instance_state(
832 id,
833 &inst.state_name,
834 ));
835 }
836 if new_code == 48 && inst.disable_api_termination {
838 return Err(AwsServiceError::aws_error(
839 http::StatusCode::BAD_REQUEST,
840 "OperationNotPermitted",
841 format!(
842 "The instance '{id}' may not be terminated. Modify its 'disableApiTermination' instance attribute and try again."
843 ),
844 ));
845 }
846 if new_code == 80 && inst.disable_api_stop {
847 return Err(AwsServiceError::aws_error(
848 http::StatusCode::BAD_REQUEST,
849 "OperationNotPermitted",
850 format!(
851 "The instance '{id}' may not be stopped. Modify its 'disableApiStop' instance attribute and try again."
852 ),
853 ));
854 }
855 Ok(())
856}
857
858async fn change_state(
859 svc: &Ec2Service,
860 req: &AwsRequest,
861 action: &str,
862 new_code: i64,
863 new_name: &str,
864) -> Result<AwsResponse, AwsServiceError> {
865 let ids = indexed_list(&req.query_params, "InstanceId");
866
867 {
871 let accounts = svc.state.read();
872 let empty = Ec2State::new(&req.account_id, &req.region);
873 let state = accounts.get(&req.account_id).unwrap_or(&empty);
874 for id in &ids {
875 let inst = state
876 .instances
877 .get(id)
878 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
879 check_transition(inst, id, new_code)?;
880 }
881 }
882
883 let applied_code = if new_code == 16 { 0 } else { new_code };
889 let applied_name = if new_code == 16 { "pending" } else { new_name };
890
891 let mut changes = Vec::new();
892 let mut affected: Vec<String> = Vec::new();
893 {
894 let mut accounts = svc.state.write();
895 let state = accounts.get_or_create(&req.account_id);
896 for id in &ids {
906 let inst = state
907 .instances
908 .get(id)
909 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
910 check_transition(inst, id, new_code)?;
911 }
912 for id in &ids {
913 let (prev_code, prev_name) = state
914 .instances
915 .get(id)
916 .map(|i| (i.state_code, i.state_name.clone()))
917 .unwrap_or((16, "running".to_string()));
918 if let Some(inst) = state.instances.get_mut(id) {
919 inst.state_code = applied_code;
920 inst.state_name = applied_name.to_string();
921 if new_code == 80 || new_code == 48 {
922 inst.public_ip = None;
923 }
924 affected.push(id.clone());
925 if new_code == 48 {
927 inst.container_id = None;
928 }
929 }
930 changes.push(format!(
931 "{}{}{}",
932 ec2_elem("instanceId", id),
933 state_xml("currentState", applied_code, applied_name),
934 state_xml("previousState", prev_code, &prev_name),
935 ));
936 }
937 }
938
939 {
943 let svc_state = svc.state.clone();
944 let runtime = svc.runtime.clone();
945 let account_id = req.account_id.clone();
946 tokio::spawn(async move {
947 for id in &affected {
948 match new_code {
949 16 => {
950 let running = match &runtime {
951 Some(rt) => rt.start_instance(id).await,
952 None => None,
953 };
954 reconcile_started(&svc_state, &account_id, id, running);
955 }
956 80 => {
957 if let Some(rt) = &runtime {
958 rt.stop_instance(id).await;
959 }
960 }
961 48 => {
962 if let Some(rt) = &runtime {
963 rt.terminate_instance(id).await;
964 }
965 }
966 _ => {}
967 }
968 }
969 if let Some(rt) = &runtime {
972 if rt.network_isolation_enforced() {
973 super::firewall_model::reconcile(&svc_state, rt).await;
974 }
975 }
976 });
977 }
978
979 Ok(Ec2Service::respond(
980 action,
981 &req.request_id,
982 &ec2_list("instancesSet", &changes),
983 ))
984}
985
986pub(crate) async fn start_instances(
987 svc: &Ec2Service,
988 req: &AwsRequest,
989) -> Result<AwsResponse, AwsServiceError> {
990 change_state(svc, req, "StartInstances", 16, "running").await
991}
992pub(crate) async fn stop_instances(
993 svc: &Ec2Service,
994 req: &AwsRequest,
995) -> Result<AwsResponse, AwsServiceError> {
996 change_state(svc, req, "StopInstances", 80, "stopped").await
997}
998pub(crate) async fn terminate_instances(
999 svc: &Ec2Service,
1000 req: &AwsRequest,
1001) -> Result<AwsResponse, AwsServiceError> {
1002 change_state(svc, req, "TerminateInstances", 48, "terminated").await
1003}
1004
1005pub(crate) async fn reboot_instances(
1006 svc: &Ec2Service,
1007 req: &AwsRequest,
1008) -> Result<AwsResponse, AwsServiceError> {
1009 let ids = indexed_list(&req.query_params, "InstanceId");
1010 let backed: Vec<String> = {
1013 let accounts = svc.state.read();
1014 let empty = Ec2State::new(&req.account_id, &req.region);
1015 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1016 let mut backed = Vec::new();
1017 for id in &ids {
1018 let inst = state
1019 .instances
1020 .get(id)
1021 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
1022 if inst.state_code == 48 {
1023 return Err(crate::service_helpers::incorrect_instance_state(
1024 id,
1025 &inst.state_name,
1026 ));
1027 }
1028 if inst.container_id.is_some() {
1029 backed.push(id.clone());
1030 }
1031 }
1032 backed
1033 };
1034 {
1037 let svc_state = svc.state.clone();
1038 let runtime = svc.runtime.clone();
1039 let account_id = req.account_id.clone();
1040 tokio::spawn(async move {
1041 let Some(rt) = runtime else {
1042 return;
1043 };
1044 for id in &backed {
1045 if let Some(running) = rt.reboot_instance(id).await {
1048 let mut accounts = svc_state.write();
1049 if let Some(state) = accounts.get_mut(&account_id) {
1050 if let Some(inst) = state.instances.get_mut(id) {
1051 if inst.state_code != 48 {
1054 inst.private_ip = running.private_ip;
1055 inst.container_id = Some(running.container_id);
1056 }
1057 }
1058 }
1059 }
1060 }
1061 if rt.network_isolation_enforced() {
1066 super::firewall_model::reconcile(&svc_state, &rt).await;
1067 }
1068 });
1069 }
1070 Ok(Ec2Service::respond(
1071 "RebootInstances",
1072 &req.request_id,
1073 &ec2_return(true),
1074 ))
1075}
1076
1077pub(crate) fn monitor_instances(
1078 svc: &Ec2Service,
1079 req: &AwsRequest,
1080) -> Result<AwsResponse, AwsServiceError> {
1081 monitor(svc, req, "MonitorInstances", true)
1082}
1083pub(crate) fn unmonitor_instances(
1084 svc: &Ec2Service,
1085 req: &AwsRequest,
1086) -> Result<AwsResponse, AwsServiceError> {
1087 monitor(svc, req, "UnmonitorInstances", false)
1088}
1089
1090fn monitor(
1091 svc: &Ec2Service,
1092 req: &AwsRequest,
1093 action: &str,
1094 enable: bool,
1095) -> Result<AwsResponse, AwsServiceError> {
1096 let ids = indexed_list(&req.query_params, "InstanceId");
1097 {
1098 let mut accounts = svc.state.write();
1099 let state = accounts.get_or_create(&req.account_id);
1100 for id in &ids {
1102 if !state.instances.contains_key(id) {
1103 return Err(crate::service_helpers::instance_not_found(id));
1104 }
1105 }
1106 for id in &ids {
1107 if let Some(i) = state.instances.get_mut(id) {
1108 i.monitoring = enable;
1109 }
1110 }
1111 }
1112 let items: Vec<String> = ids
1113 .iter()
1114 .map(|id| {
1115 format!(
1116 "{}<monitoring><state>{}</state></monitoring>",
1117 ec2_elem("instanceId", id),
1118 if enable { "pending" } else { "disabling" }
1119 )
1120 })
1121 .collect();
1122 Ok(Ec2Service::respond(
1123 action,
1124 &req.request_id,
1125 &ec2_list("instancesSet", &items),
1126 ))
1127}
1128
1129pub(crate) fn describe_instances(
1130 svc: &Ec2Service,
1131 req: &AwsRequest,
1132) -> Result<AwsResponse, AwsServiceError> {
1133 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1134 let max_results = parse_max_results(&req.query_params);
1135 let next_token = req.query_params.get("NextToken").map(String::as_str);
1136 let filters = parse_filters(&req.query_params);
1137 let wanted = indexed_list(&req.query_params, "InstanceId");
1138 let owner = req.account_id.clone();
1139 let accounts = svc.state.read();
1140 let empty = Ec2State::new(&req.account_id, &req.region);
1141 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1142
1143 for id in &wanted {
1146 if !state.instances.contains_key(id) {
1147 return Err(crate::service_helpers::instance_not_found(id));
1148 }
1149 }
1150
1151 let mut matching: Vec<&Instance> = state
1155 .instances
1156 .values()
1157 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1158 .filter(|i| {
1159 inst_match(
1160 i,
1161 state.tags_for(&i.instance_id),
1162 &filters,
1163 &arch_for(state, &i.image_id),
1164 )
1165 })
1166 .collect();
1167 matching.sort_by(|a, b| {
1168 a.reservation_id
1169 .cmp(&b.reservation_id)
1170 .then(a.instance_id.cmp(&b.instance_id))
1171 });
1172 let (page, token) = crate::service_helpers::paginate(&matching, next_token, max_results);
1173
1174 let sg_names = sg_name_map(state);
1176 let mut by_res: HashMap<String, Vec<String>> = HashMap::new();
1177 let mut order: Vec<String> = Vec::new();
1178 for i in page {
1179 if !by_res.contains_key(&i.reservation_id) {
1180 order.push(i.reservation_id.clone());
1181 }
1182 by_res
1183 .entry(i.reservation_id.clone())
1184 .or_default()
1185 .push(instance_xml(
1186 i,
1187 state.tags_for(&i.instance_id),
1188 &owner,
1189 &sg_names,
1190 &arch_for(state, &i.image_id),
1191 ));
1192 }
1193 let reservations: Vec<String> = order
1194 .iter()
1195 .map(|rid| {
1196 let insts = by_res.remove(rid).unwrap_or_default();
1197 reservation_xml(rid, &owner, &insts)
1198 })
1199 .collect();
1200 let body = format!(
1201 "{}{}",
1202 ec2_list("reservationSet", &reservations),
1203 token.map(|t| ec2_elem("nextToken", &t)).unwrap_or_default(),
1204 );
1205 Ok(Ec2Service::respond(
1206 "DescribeInstances",
1207 &req.request_id,
1208 &body,
1209 ))
1210}
1211
1212fn parse_max_results(params: &HashMap<String, String>) -> Option<usize> {
1215 params
1216 .get("MaxResults")
1217 .filter(|v| !v.is_empty())
1218 .and_then(|v| v.parse::<usize>().ok())
1219}
1220
1221fn inst_match(i: &Instance, tags: &[Tag], filters: &[Filter], architecture: &str) -> bool {
1222 use crate::service_helpers::filter_value_matches;
1223 filters.iter().all(|f| {
1224 let candidates: Vec<String> = match f.name.as_str() {
1225 "instance-id" => vec![i.instance_id.clone()],
1226 "instance-type" => vec![i.instance_type.clone()],
1227 "image-id" => vec![i.image_id.clone()],
1228 "instance-state-name" => vec![i.state_name.clone()],
1229 "instance-state-code" => vec![i.state_code.to_string()],
1230 "vpc-id" => i.vpc_id.clone().into_iter().collect(),
1231 "subnet-id" => i.subnet_id.clone().into_iter().collect(),
1232 "availability-zone" => vec![i.az.clone()],
1233 "private-ip-address" => vec![i.private_ip.clone()],
1234 "ip-address" => i.public_ip.clone().into_iter().collect(),
1235 "key-name" => i.key_name.clone().into_iter().collect(),
1236 "architecture" => vec![architecture.to_string()],
1237 "tag-key" => tags.iter().map(|t| t.key.clone()).collect(),
1238 name => {
1239 if let Some(key) = name.strip_prefix("tag:") {
1240 tags.iter()
1241 .filter(|t| t.key == key)
1242 .map(|t| t.value.clone())
1243 .collect()
1244 } else {
1245 return false;
1249 }
1250 }
1251 };
1252 f.values
1253 .iter()
1254 .any(|v| candidates.iter().any(|c| filter_value_matches(v, c)))
1255 })
1256}
1257
1258pub(crate) fn describe_instance_status(
1259 svc: &Ec2Service,
1260 req: &AwsRequest,
1261) -> Result<AwsResponse, AwsServiceError> {
1262 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1263 let max_results = parse_max_results(&req.query_params);
1264 let next_token = req.query_params.get("NextToken").map(String::as_str);
1265 let filters = parse_filters(&req.query_params);
1266 let wanted = indexed_list(&req.query_params, "InstanceId");
1267 let include_all = req
1268 .query_params
1269 .get("IncludeAllInstances")
1270 .map(|v| v == "true")
1271 .unwrap_or(false);
1272 let accounts = svc.state.read();
1273 let empty = Ec2State::new(&req.account_id, &req.region);
1274 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1275 let mut matching: Vec<&Instance> = state
1276 .instances
1277 .values()
1278 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1279 .filter(|i| include_all || i.state_name == "running")
1280 .filter(|i| {
1281 inst_match(
1282 i,
1283 state.tags_for(&i.instance_id),
1284 &filters,
1285 &arch_for(state, &i.image_id),
1286 )
1287 })
1288 .collect();
1289 matching.sort_by(|a, b| a.instance_id.cmp(&b.instance_id));
1290 let (page, token) = crate::service_helpers::paginate(&matching, next_token, max_results);
1291 let items: Vec<String> = page
1292 .iter()
1293 .map(|i| {
1294 format!(
1295 "{}{}{}{}{}{}",
1296 ec2_elem("instanceId", &i.instance_id),
1297 ec2_elem("availabilityZone", &i.az),
1298 state_xml("instanceState", i.state_code, &i.state_name),
1299 "<instanceStatus><status>ok</status></instanceStatus>",
1300 "<systemStatus><status>ok</status></systemStatus>",
1301 ec2_list("eventsSet", &[]),
1302 )
1303 })
1304 .collect();
1305 let body = format!(
1306 "{}{}",
1307 ec2_list("instanceStatusSet", &items),
1308 token.map(|t| ec2_elem("nextToken", &t)).unwrap_or_default(),
1309 );
1310 Ok(Ec2Service::respond(
1311 "DescribeInstanceStatus",
1312 &req.request_id,
1313 &body,
1314 ))
1315}
1316
1317fn instance_type_items(req: &AwsRequest) -> Vec<String> {
1318 let wanted = indexed_list(&req.query_params, "InstanceType");
1319 INSTANCE_TYPES
1320 .iter()
1321 .filter(|t| wanted.is_empty() || wanted.iter().any(|w| w == *t))
1322 .map(|t| {
1323 format!(
1324 "{}<currentGeneration>true</currentGeneration><bareMetal>false</bareMetal>\
1325 <hypervisor>nitro</hypervisor><instanceStorageSupported>false</instanceStorageSupported>\
1326 <processorInfo><supportedArchitectures><item>x86_64</item></supportedArchitectures></processorInfo>\
1327 <vCpuInfo><defaultVCpus>2</defaultVCpus></vCpuInfo>\
1328 <memoryInfo><sizeInMiB>1024</sizeInMiB></memoryInfo>\
1329 <supportedVirtualizationTypes><item>hvm</item></supportedVirtualizationTypes>",
1330 ec2_elem("instanceType", t),
1331 )
1332 })
1333 .collect()
1334}
1335
1336pub(crate) fn describe_instance_types(
1337 _svc: &Ec2Service,
1338 req: &AwsRequest,
1339) -> Result<AwsResponse, AwsServiceError> {
1340 crate::service_helpers::validate_max_results(&req.query_params, 5, 100)?;
1341 Ok(Ec2Service::respond(
1342 "DescribeInstanceTypes",
1343 &req.request_id,
1344 &ec2_list("instanceTypeSet", &instance_type_items(req)),
1345 ))
1346}
1347
1348pub(crate) fn get_instance_types_from_requirements(
1349 _svc: &Ec2Service,
1350 req: &AwsRequest,
1351) -> Result<AwsResponse, AwsServiceError> {
1352 require_struct(&req.query_params, "InstanceRequirements")?;
1353 let items: Vec<String> = INSTANCE_TYPES
1354 .iter()
1355 .map(|t| format!("<instanceType>{t}</instanceType>"))
1356 .collect();
1357 Ok(Ec2Service::respond(
1358 "GetInstanceTypesFromInstanceRequirements",
1359 &req.request_id,
1360 &ec2_list("instanceTypeSet", &items),
1361 ))
1362}
1363
1364pub(crate) fn describe_instance_attribute(
1367 svc: &Ec2Service,
1368 req: &AwsRequest,
1369) -> Result<AwsResponse, AwsServiceError> {
1370 let id = require(&req.query_params, "InstanceId")?;
1371 let attribute = require(&req.query_params, "Attribute")?;
1372 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1373 let accounts = svc.state.read();
1374 let acct_state = accounts.get(&req.account_id);
1375 let inst = acct_state
1376 .and_then(|s| s.instances.get(&id))
1377 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1378 let attr_xml = match attribute.as_str() {
1379 "instanceType" => format!(
1380 "<instanceType><value>{}</value></instanceType>",
1381 inst.instance_type
1382 ),
1383 "disableApiTermination" => format!(
1384 "<disableApiTermination><value>{}</value></disableApiTermination>",
1385 inst.disable_api_termination
1386 ),
1387 "disableApiStop" => format!(
1388 "<disableApiStop><value>{}</value></disableApiStop>",
1389 inst.disable_api_stop
1390 ),
1391 "ebsOptimized" => format!(
1392 "<ebsOptimized><value>{}</value></ebsOptimized>",
1393 inst.ebs_optimized
1394 ),
1395 "sourceDestCheck" => format!(
1396 "<sourceDestCheck><value>{}</value></sourceDestCheck>",
1397 inst.source_dest_check
1398 ),
1399 "instanceInitiatedShutdownBehavior" => format!(
1400 "<instanceInitiatedShutdownBehavior><value>{}</value></instanceInitiatedShutdownBehavior>",
1401 inst.instance_initiated_shutdown_behavior
1402 ),
1403 "userData" => match &inst.user_data {
1404 Some(d) => format!("<userData><value>{d}</value></userData>"),
1405 None => "<userData/>".to_string(),
1406 },
1407 "groupSet" => {
1408 let sg_names = acct_state.map(sg_name_map).unwrap_or_default();
1409 let groups: Vec<String> = inst
1410 .security_group_ids
1411 .iter()
1412 .map(|g| {
1413 let name = sg_names.get(g).map(String::as_str).unwrap_or(g.as_str());
1414 format!("{}{}", ec2_elem("groupId", g), ec2_elem("groupName", name))
1415 })
1416 .collect();
1417 ec2_list("groupSet", &groups)
1418 }
1419 _ => String::new(),
1420 };
1421 let body = format!("{}{}", ec2_elem("instanceId", &id), attr_xml);
1422 Ok(Ec2Service::respond(
1423 "DescribeInstanceAttribute",
1424 &req.request_id,
1425 &body,
1426 ))
1427}
1428
1429const ATTRIBUTE_VALUES: &[&str] = &[
1430 "instanceType",
1431 "kernel",
1432 "ramdisk",
1433 "userData",
1434 "disableApiTermination",
1435 "instanceInitiatedShutdownBehavior",
1436 "rootDeviceName",
1437 "blockDeviceMapping",
1438 "productCodes",
1439 "sourceDestCheck",
1440 "groupSet",
1441 "ebsOptimized",
1442 "sriovNetSupport",
1443 "enaSupport",
1444 "enclaveOptions",
1445 "disableApiStop",
1446];
1447
1448fn attr_bool(params: &HashMap<String, String>, key: &str) -> Option<bool> {
1452 params
1453 .get(&format!("{key}.Value"))
1454 .or_else(|| params.get(key))
1455 .map(|v| v == "true")
1456}
1457
1458fn attr_str<'a>(params: &'a HashMap<String, String>, key: &str) -> Option<&'a String> {
1459 params
1460 .get(&format!("{key}.Value"))
1461 .or_else(|| params.get(key))
1462}
1463
1464pub(crate) fn modify_instance_attribute(
1465 svc: &Ec2Service,
1466 req: &AwsRequest,
1467) -> Result<AwsResponse, AwsServiceError> {
1468 let id = require(&req.query_params, "InstanceId")?;
1469 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1473 let p = &req.query_params;
1474 let mut accounts = svc.state.write();
1475 let state = accounts.get_or_create(&req.account_id);
1476 let inst = state
1477 .instances
1478 .get_mut(&id)
1479 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1480
1481 if let Some(attr) = p.get("Attribute").filter(|v| !v.is_empty()) {
1483 let value = p.get("Value").cloned();
1484 match attr.as_str() {
1485 "instanceType" => {
1486 if let Some(v) = value {
1487 inst.instance_type = v;
1488 }
1489 }
1490 "userData" => inst.user_data = value.filter(|s| !s.is_empty()),
1491 "disableApiTermination" => {
1492 inst.disable_api_termination = value.as_deref() == Some("true")
1493 }
1494 "disableApiStop" => inst.disable_api_stop = value.as_deref() == Some("true"),
1495 "sourceDestCheck" => inst.source_dest_check = value.as_deref() == Some("true"),
1496 "ebsOptimized" => inst.ebs_optimized = value.as_deref() == Some("true"),
1497 "instanceInitiatedShutdownBehavior" => {
1498 if let Some(v) = value {
1499 inst.instance_initiated_shutdown_behavior = v;
1500 }
1501 }
1502 _ => {}
1503 }
1504 }
1505 if let Some(v) = attr_bool(p, "DisableApiTermination") {
1507 inst.disable_api_termination = v;
1508 }
1509 if let Some(v) = attr_bool(p, "DisableApiStop") {
1510 inst.disable_api_stop = v;
1511 }
1512 if let Some(v) = attr_bool(p, "SourceDestCheck") {
1513 inst.source_dest_check = v;
1514 }
1515 if let Some(v) = attr_bool(p, "EbsOptimized") {
1516 inst.ebs_optimized = v;
1517 }
1518 if let Some(v) = attr_str(p, "InstanceType") {
1519 inst.instance_type = v.clone();
1520 }
1521 if let Some(v) = attr_str(p, "InstanceInitiatedShutdownBehavior") {
1522 inst.instance_initiated_shutdown_behavior = v.clone();
1523 }
1524 if let Some(v) = attr_str(p, "UserData") {
1525 inst.user_data = Some(v.clone()).filter(|s| !s.is_empty());
1526 }
1527 let new_groups = indexed_list(p, "GroupId");
1528 if !new_groups.is_empty() {
1529 inst.security_group_ids = new_groups;
1530 }
1531
1532 Ok(Ec2Service::respond(
1533 "ModifyInstanceAttribute",
1534 &req.request_id,
1535 &ec2_return(true),
1536 ))
1537}
1538
1539pub(crate) fn reset_instance_attribute(
1540 svc: &Ec2Service,
1541 req: &AwsRequest,
1542) -> Result<AwsResponse, AwsServiceError> {
1543 let id = require(&req.query_params, "InstanceId")?;
1544 let attribute = require(&req.query_params, "Attribute")?;
1545 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1546 let mut accounts = svc.state.write();
1547 let state = accounts.get_or_create(&req.account_id);
1548 let inst = state
1549 .instances
1550 .get_mut(&id)
1551 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1552 match attribute.as_str() {
1555 "sourceDestCheck" => inst.source_dest_check = true,
1556 "disableApiTermination" => inst.disable_api_termination = false,
1557 "disableApiStop" => inst.disable_api_stop = false,
1558 "ebsOptimized" => inst.ebs_optimized = false,
1559 "userData" => inst.user_data = None,
1560 "instanceInitiatedShutdownBehavior" => {
1561 inst.instance_initiated_shutdown_behavior = "stop".to_string()
1562 }
1563 _ => {}
1564 }
1565 Ok(Ec2Service::respond(
1566 "ResetInstanceAttribute",
1567 &req.request_id,
1568 &ec2_return(true),
1569 ))
1570}
1571
1572fn with_instance_mut<R>(
1577 svc: &Ec2Service,
1578 account_id: &str,
1579 id: &str,
1580 f: impl FnOnce(&mut Instance) -> R,
1581) -> Result<R, AwsServiceError> {
1582 let mut accounts = svc.state.write();
1583 let state = accounts.get_or_create(account_id);
1584 let inst = state
1585 .instances
1586 .get_mut(id)
1587 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
1588 Ok(f(inst))
1589}
1590
1591pub(crate) fn modify_instance_placement(
1592 svc: &Ec2Service,
1593 req: &AwsRequest,
1594) -> Result<AwsResponse, AwsServiceError> {
1595 let id = require(&req.query_params, "InstanceId")?;
1596 validate_enum(
1597 &req.query_params,
1598 "Tenancy",
1599 &["default", "dedicated", "host"],
1600 )?;
1601 validate_enum(&req.query_params, "Affinity", &["default", "host"])?;
1602 let p = req.query_params.clone();
1603 with_instance_mut(svc, &req.account_id, &id, |inst| {
1604 if let Some(t) = p.get("Tenancy").filter(|v| !v.is_empty()) {
1605 inst.placement_tenancy = Some(t.clone());
1606 }
1607 if let Some(a) = p.get("Affinity").filter(|v| !v.is_empty()) {
1608 inst.placement_affinity = Some(a.clone());
1609 }
1610 if let Some(g) = p.get("GroupName") {
1611 inst.placement_group_name = Some(g.clone()).filter(|s| !s.is_empty());
1612 }
1613 })?;
1614 Ok(Ec2Service::respond(
1615 "ModifyInstancePlacement",
1616 &req.request_id,
1617 &ec2_return(true),
1618 ))
1619}
1620
1621pub(crate) fn modify_instance_metadata_options(
1622 svc: &Ec2Service,
1623 req: &AwsRequest,
1624) -> Result<AwsResponse, AwsServiceError> {
1625 let id = require(&req.query_params, "InstanceId")?;
1626 validate_enum(&req.query_params, "HttpTokens", &["optional", "required"])?;
1627 validate_enum(&req.query_params, "HttpEndpoint", &["disabled", "enabled"])?;
1628 validate_enum(
1629 &req.query_params,
1630 "HttpProtocolIpv6",
1631 &["disabled", "enabled"],
1632 )?;
1633 validate_enum(
1634 &req.query_params,
1635 "InstanceMetadataTags",
1636 &["disabled", "enabled"],
1637 )?;
1638 let p = req.query_params.clone();
1639 let opts = with_instance_mut(svc, &req.account_id, &id, |inst| {
1640 let m = &mut inst.metadata_options;
1641 if let Some(v) = p.get("HttpTokens").filter(|v| !v.is_empty()) {
1642 m.http_tokens = v.clone();
1643 }
1644 if let Some(v) = p.get("HttpEndpoint").filter(|v| !v.is_empty()) {
1645 m.http_endpoint = v.clone();
1646 }
1647 if let Some(v) = p.get("HttpProtocolIpv6").filter(|v| !v.is_empty()) {
1648 m.http_protocol_ipv6 = v.clone();
1649 }
1650 if let Some(v) = p.get("InstanceMetadataTags").filter(|v| !v.is_empty()) {
1651 m.instance_metadata_tags = v.clone();
1652 }
1653 if let Some(n) = p
1654 .get("HttpPutResponseHopLimit")
1655 .and_then(|v| v.parse::<i64>().ok())
1656 {
1657 m.http_put_response_hop_limit = n;
1658 }
1659 m.clone()
1660 })?;
1661 let body = format!(
1662 "{}<instanceMetadataOptions><state>applied</state><httpTokens>{}</httpTokens>\
1663 <httpPutResponseHopLimit>{}</httpPutResponseHopLimit><httpEndpoint>{}</httpEndpoint>\
1664 <httpProtocolIpv6>{}</httpProtocolIpv6><instanceMetadataTags>{}</instanceMetadataTags></instanceMetadataOptions>",
1665 ec2_elem("instanceId", &id),
1666 opts.http_tokens,
1667 opts.http_put_response_hop_limit,
1668 opts.http_endpoint,
1669 opts.http_protocol_ipv6,
1670 opts.instance_metadata_tags,
1671 );
1672 Ok(Ec2Service::respond(
1673 "ModifyInstanceMetadataOptions",
1674 &req.request_id,
1675 &body,
1676 ))
1677}
1678
1679pub(crate) fn modify_instance_maintenance_options(
1680 svc: &Ec2Service,
1681 req: &AwsRequest,
1682) -> Result<AwsResponse, AwsServiceError> {
1683 let id = require(&req.query_params, "InstanceId")?;
1684 validate_enum(&req.query_params, "AutoRecovery", &["disabled", "default"])?;
1685 validate_enum(
1686 &req.query_params,
1687 "RebootMigration",
1688 &["disabled", "default"],
1689 )?;
1690 let p = req.query_params.clone();
1691 let opts = with_instance_mut(svc, &req.account_id, &id, |inst| {
1692 let m = &mut inst.maintenance_options;
1693 if let Some(v) = p.get("AutoRecovery").filter(|v| !v.is_empty()) {
1694 m.auto_recovery = v.clone();
1695 }
1696 if let Some(v) = p.get("RebootMigration").filter(|v| !v.is_empty()) {
1697 m.reboot_migration = v.clone();
1698 }
1699 m.clone()
1700 })?;
1701 let body = format!(
1702 "{}<maintenanceOptions><autoRecovery>{}</autoRecovery><rebootMigration>{}</rebootMigration></maintenanceOptions>",
1703 ec2_elem("instanceId", &id),
1704 opts.auto_recovery,
1705 opts.reboot_migration,
1706 );
1707 Ok(Ec2Service::respond(
1708 "ModifyInstanceMaintenanceOptions",
1709 &req.request_id,
1710 &body,
1711 ))
1712}
1713
1714pub(crate) fn modify_instance_cpu_options(
1715 svc: &Ec2Service,
1716 req: &AwsRequest,
1717) -> Result<AwsResponse, AwsServiceError> {
1718 let id = require(&req.query_params, "InstanceId")?;
1719 validate_enum(
1720 &req.query_params,
1721 "NestedVirtualization",
1722 &["disabled", "enabled"],
1723 )?;
1724 let core_count = req
1725 .query_params
1726 .get("CoreCount")
1727 .and_then(|v| v.parse::<i64>().ok())
1728 .unwrap_or(2);
1729 let threads_per_core = req
1730 .query_params
1731 .get("ThreadsPerCore")
1732 .and_then(|v| v.parse::<i64>().ok())
1733 .unwrap_or(1);
1734 with_instance_mut(svc, &req.account_id, &id, |inst| {
1735 inst.cpu_options = Some(crate::state::CpuOptions {
1736 core_count,
1737 threads_per_core,
1738 });
1739 })?;
1740 let body = format!(
1741 "{}<coreCount>{core_count}</coreCount><threadsPerCore>{threads_per_core}</threadsPerCore>",
1742 ec2_elem("instanceId", &id)
1743 );
1744 Ok(Ec2Service::respond(
1745 "ModifyInstanceCpuOptions",
1746 &req.request_id,
1747 &body,
1748 ))
1749}
1750
1751pub(crate) fn modify_instance_network_performance_options(
1752 svc: &Ec2Service,
1753 req: &AwsRequest,
1754) -> Result<AwsResponse, AwsServiceError> {
1755 let id = require(&req.query_params, "InstanceId")?;
1756 let weighting = require(&req.query_params, "BandwidthWeighting")?;
1757 validate_enum(
1758 &req.query_params,
1759 "BandwidthWeighting",
1760 &["default", "vpc-1", "ebs-1"],
1761 )?;
1762 with_instance_mut(svc, &req.account_id, &id, |inst| {
1763 inst.bandwidth_weighting = Some(weighting.clone());
1764 })?;
1765 let body = format!(
1766 "{}<bandwidthWeighting>{weighting}</bandwidthWeighting>",
1767 ec2_elem("instanceId", &id)
1768 );
1769 Ok(Ec2Service::respond(
1770 "ModifyInstanceNetworkPerformanceOptions",
1771 &req.request_id,
1772 &body,
1773 ))
1774}
1775
1776pub(crate) fn modify_instance_event_start_time(
1777 _svc: &Ec2Service,
1778 req: &AwsRequest,
1779) -> Result<AwsResponse, AwsServiceError> {
1780 require(&req.query_params, "InstanceId")?;
1781 let event_id = require(&req.query_params, "InstanceEventId")?;
1782 require(&req.query_params, "NotBefore")?;
1783 let body = format!(
1784 "<event>{}<code>system-reboot</code><description>scheduled</description></event>",
1785 ec2_elem("instanceEventId", &event_id)
1786 );
1787 Ok(Ec2Service::respond(
1788 "ModifyInstanceEventStartTime",
1789 &req.request_id,
1790 &body,
1791 ))
1792}
1793
1794pub(crate) fn describe_instance_credit_specifications(
1795 svc: &Ec2Service,
1796 req: &AwsRequest,
1797) -> Result<AwsResponse, AwsServiceError> {
1798 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1799 let wanted = indexed_list(&req.query_params, "InstanceId");
1800 let accounts = svc.state.read();
1801 let empty = Ec2State::new(&req.account_id, &req.region);
1802 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1803 let items: Vec<String> = state
1804 .instances
1805 .values()
1806 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1807 .map(|i| {
1808 let credits = state
1812 .instance_credit_specs
1813 .get(&i.instance_id)
1814 .cloned()
1815 .unwrap_or_else(|| "standard".to_string());
1816 format!(
1817 "{}{}",
1818 ec2_elem("instanceId", &i.instance_id),
1819 ec2_elem("cpuCredits", &credits)
1820 )
1821 })
1822 .collect();
1823 Ok(Ec2Service::respond(
1824 "DescribeInstanceCreditSpecifications",
1825 &req.request_id,
1826 &ec2_list("instanceCreditSpecificationSet", &items),
1827 ))
1828}
1829
1830pub(crate) fn modify_instance_credit_specification(
1831 svc: &Ec2Service,
1832 req: &AwsRequest,
1833) -> Result<AwsResponse, AwsServiceError> {
1834 let p = &req.query_params;
1835 let mut successful = Vec::new();
1836 let mut unsuccessful = Vec::new();
1837 {
1838 let mut accounts = svc.state.write();
1839 let state = accounts.get_or_create(&req.account_id);
1840 let mut n = 1usize;
1841 loop {
1842 let id_key = format!("InstanceCreditSpecification.{n}.InstanceId");
1843 let Some(instance_id) = p.get(&id_key).cloned() else {
1844 break;
1845 };
1846 let credits = p
1847 .get(&format!("InstanceCreditSpecification.{n}.CpuCredits"))
1848 .cloned()
1849 .unwrap_or_else(|| "standard".to_string());
1850 if state.instances.contains_key(&instance_id) {
1851 state
1852 .instance_credit_specs
1853 .insert(instance_id.clone(), credits);
1854 successful.push(ec2_elem("instanceId", &instance_id));
1855 } else {
1856 unsuccessful.push(format!(
1857 "{}<error><code>InvalidInstanceID.NotFound</code><message>The instance ID '{instance_id}' does not exist</message></error>",
1858 ec2_elem("instanceId", &instance_id)
1859 ));
1860 }
1861 n += 1;
1862 }
1863 }
1864 let body = format!(
1865 "{}{}",
1866 ec2_list("successfulInstanceCreditSpecificationSet", &successful),
1867 ec2_list("unsuccessfulInstanceCreditSpecificationSet", &unsuccessful),
1868 );
1869 Ok(Ec2Service::respond(
1870 "ModifyInstanceCreditSpecification",
1871 &req.request_id,
1872 &body,
1873 ))
1874}
1875
1876pub(crate) fn get_instance_metadata_defaults(
1877 svc: &Ec2Service,
1878 req: &AwsRequest,
1879) -> Result<AwsResponse, AwsServiceError> {
1880 let accounts = svc.state.read();
1881 let d = accounts
1882 .get(&req.account_id)
1883 .and_then(|s| s.instance_metadata_defaults.clone())
1884 .unwrap_or_default();
1885 let mut inner = String::new();
1886 if let Some(v) = &d.http_tokens {
1887 inner.push_str(&ec2_elem("httpTokens", v));
1888 }
1889 if let Some(v) = &d.http_endpoint {
1890 inner.push_str(&ec2_elem("httpEndpoint", v));
1891 }
1892 if let Some(v) = d.http_put_response_hop_limit {
1893 inner.push_str(&ec2_elem("httpPutResponseHopLimit", &v.to_string()));
1894 }
1895 if let Some(v) = &d.instance_metadata_tags {
1896 inner.push_str(&ec2_elem("instanceMetadataTags", v));
1897 }
1898 if let Some(v) = &d.http_tokens_enforced {
1899 inner.push_str(&ec2_elem("httpTokensEnforced", v));
1900 }
1901 Ok(Ec2Service::respond(
1902 "GetInstanceMetadataDefaults",
1903 &req.request_id,
1904 &format!("<accountLevel>{inner}</accountLevel>"),
1905 ))
1906}
1907
1908pub(crate) fn modify_instance_metadata_defaults(
1909 svc: &Ec2Service,
1910 req: &AwsRequest,
1911) -> Result<AwsResponse, AwsServiceError> {
1912 let p = &req.query_params;
1913 validate_enum(p, "HttpTokens", &["optional", "required", "no-preference"])?;
1914 validate_enum(p, "HttpEndpoint", &["disabled", "enabled", "no-preference"])?;
1915 validate_enum(
1916 p,
1917 "InstanceMetadataTags",
1918 &["disabled", "enabled", "no-preference"],
1919 )?;
1920 validate_enum(
1921 p,
1922 "HttpTokensEnforced",
1923 &["disabled", "enabled", "no-preference"],
1924 )?;
1925 let apply = |cur: &mut Option<String>, key: &str| {
1927 if let Some(v) = p.get(key) {
1928 *cur = if v == "no-preference" {
1929 None
1930 } else {
1931 Some(v.clone())
1932 };
1933 }
1934 };
1935 {
1936 let mut accounts = svc.state.write();
1937 let state = accounts.get_or_create(&req.account_id);
1938 let d = state
1939 .instance_metadata_defaults
1940 .get_or_insert_with(Default::default);
1941 apply(&mut d.http_tokens, "HttpTokens");
1942 apply(&mut d.http_endpoint, "HttpEndpoint");
1943 apply(&mut d.instance_metadata_tags, "InstanceMetadataTags");
1944 apply(&mut d.http_tokens_enforced, "HttpTokensEnforced");
1945 if let Some(v) = p
1946 .get("HttpPutResponseHopLimit")
1947 .and_then(|v| v.parse::<i64>().ok())
1948 {
1949 d.http_put_response_hop_limit = if v < 0 { None } else { Some(v) };
1951 }
1952 }
1953 Ok(Ec2Service::respond(
1954 "ModifyInstanceMetadataDefaults",
1955 &req.request_id,
1956 &ec2_return(true),
1957 ))
1958}
1959
1960pub(crate) fn register_event_notification_attributes(
1961 svc: &Ec2Service,
1962 req: &AwsRequest,
1963) -> Result<AwsResponse, AwsServiceError> {
1964 let keys = indexed_sub_keys(&req.query_params);
1965 let include_all = req
1966 .query_params
1967 .get("InstanceTagAttribute.IncludeAllTagsOfInstance")
1968 .map(|v| v == "true");
1969 let xml = {
1970 let mut accounts = svc.state.write();
1971 let state = accounts.get_or_create(&req.account_id);
1972 for k in keys {
1973 if !state.event_notification_tag_keys.contains(&k) {
1974 state.event_notification_tag_keys.push(k);
1975 }
1976 }
1977 if let Some(v) = include_all {
1978 state.event_notification_include_all_tags = v;
1979 }
1980 event_tag_attribute(state)
1981 };
1982 Ok(Ec2Service::respond(
1983 "RegisterInstanceEventNotificationAttributes",
1984 &req.request_id,
1985 &xml,
1986 ))
1987}
1988
1989pub(crate) fn deregister_event_notification_attributes(
1990 svc: &Ec2Service,
1991 req: &AwsRequest,
1992) -> Result<AwsResponse, AwsServiceError> {
1993 let keys = indexed_sub_keys(&req.query_params);
1994 let include_all = req
1995 .query_params
1996 .get("InstanceTagAttribute.IncludeAllTagsOfInstance")
1997 .map(|v| v == "true");
1998 let xml = {
1999 let mut accounts = svc.state.write();
2000 let state = accounts.get_or_create(&req.account_id);
2001 state
2002 .event_notification_tag_keys
2003 .retain(|k| !keys.contains(k));
2004 if include_all == Some(true) {
2006 state.event_notification_include_all_tags = false;
2007 }
2008 event_tag_attribute(state)
2009 };
2010 Ok(Ec2Service::respond(
2011 "DeregisterInstanceEventNotificationAttributes",
2012 &req.request_id,
2013 &xml,
2014 ))
2015}
2016
2017pub(crate) fn describe_event_notification_attributes(
2018 svc: &Ec2Service,
2019 req: &AwsRequest,
2020) -> Result<AwsResponse, AwsServiceError> {
2021 let accounts = svc.state.read();
2022 let empty = Ec2State::new(&req.account_id, &req.region);
2023 let state = accounts.get(&req.account_id).unwrap_or(&empty);
2024 Ok(Ec2Service::respond(
2025 "DescribeInstanceEventNotificationAttributes",
2026 &req.request_id,
2027 &event_tag_attribute(state),
2028 ))
2029}
2030
2031fn indexed_sub_keys(params: &HashMap<String, String>) -> Vec<String> {
2033 let mut out = Vec::new();
2034 let mut n = 1usize;
2035 while let Some(v) = params.get(&format!("InstanceTagAttribute.InstanceTagKey.{n}")) {
2036 out.push(v.clone());
2037 n += 1;
2038 }
2039 out
2040}
2041
2042fn event_tag_attribute(state: &Ec2State) -> String {
2043 let keys: Vec<String> = state
2046 .event_notification_tag_keys
2047 .iter()
2048 .map(|k| fakecloud_aws::xml::xml_escape(k))
2049 .collect();
2050 format!(
2051 "<instanceTagAttribute><includeAllTagsOfInstance>{}</includeAllTagsOfInstance>{}</instanceTagAttribute>",
2052 state.event_notification_include_all_tags,
2053 ec2_list("instanceTagKeySet", &keys)
2054 )
2055}
2056
2057pub(crate) fn report_instance_status(
2058 _svc: &Ec2Service,
2059 req: &AwsRequest,
2060) -> Result<AwsResponse, AwsServiceError> {
2061 require(&req.query_params, "Status")?;
2062 validate_enum(&req.query_params, "Status", &["ok", "impaired"])?;
2063 Ok(Ec2Service::respond(
2064 "ReportInstanceStatus",
2065 &req.request_id,
2066 &ec2_return(true),
2067 ))
2068}
2069
2070pub(crate) fn describe_instance_topology(
2071 _svc: &Ec2Service,
2072 req: &AwsRequest,
2073) -> Result<AwsResponse, AwsServiceError> {
2074 crate::service_helpers::validate_max_results(&req.query_params, 1, 100)?;
2075 Ok(Ec2Service::respond(
2076 "DescribeInstanceTopology",
2077 &req.request_id,
2078 &ec2_list("instanceSet", &[]),
2079 ))
2080}
2081
2082#[cfg(test)]
2083mod tests {
2084 use super::subnet_ip_prefix;
2085
2086 #[test]
2087 fn subnet_ip_prefix_uses_subnet_network() {
2088 assert_eq!(subnet_ip_prefix("172.31.16.0/20"), "172.31.16");
2090 assert_eq!(subnet_ip_prefix("10.0.5.0/24"), "10.0.5");
2091 assert_eq!(subnet_ip_prefix("192.168.1.0"), "192.168.1");
2093 }
2094
2095 #[test]
2096 fn subnet_ip_prefix_falls_back_on_garbage() {
2097 assert_eq!(subnet_ip_prefix(""), "10.0.0");
2098 assert_eq!(subnet_ip_prefix("not-a-cidr"), "10.0.0");
2099 assert_eq!(subnet_ip_prefix("fd00::/8"), "10.0.0");
2101 }
2102}
2103
2104#[cfg(test)]
2105mod modify_tests {
2106 use super::*;
2107
2108 fn req(action: &str, query: &[(&str, &str)]) -> AwsRequest {
2109 AwsRequest {
2110 service: "ec2".into(),
2111 action: action.into(),
2112 region: "us-east-1".into(),
2113 account_id: "000000000000".into(),
2114 request_id: "rid".into(),
2115 headers: http::HeaderMap::new(),
2116 query_params: query
2117 .iter()
2118 .map(|(k, v)| (k.to_string(), v.to_string()))
2119 .collect(),
2120 body: bytes::Bytes::new(),
2121 body_stream: parking_lot::Mutex::new(None),
2122 path_segments: Vec::new(),
2123 raw_path: "/".into(),
2124 raw_query: String::new(),
2125 method: http::Method::POST,
2126 is_query_protocol: true,
2127 access_key_id: None,
2128 principal: None,
2129 }
2130 }
2131
2132 fn body(resp: AwsResponse) -> String {
2133 String::from_utf8_lossy(resp.body.expect_bytes()).to_string()
2134 }
2135
2136 fn seed_instance(svc: &Ec2Service, id: &str) {
2137 let mut accounts = svc.state.write();
2138 let state = accounts.get_or_create("000000000000");
2139 let inst = Instance {
2140 instance_id: id.into(),
2141 image_id: "ami-1".into(),
2142 instance_type: "t3.micro".into(),
2143 state_code: 16,
2144 state_name: "running".into(),
2145 private_ip: "10.0.0.5".into(),
2146 public_ip: None,
2147 subnet_id: Some("subnet-1".into()),
2148 vpc_id: Some("vpc-1".into()),
2149 key_name: None,
2150 security_group_ids: vec![],
2151 reservation_id: "r-1".into(),
2152 ami_launch_index: 0,
2153 monitoring: false,
2154 az: "us-east-1a".into(),
2155 launch_time: "2024-01-01T00:00:00.000Z".into(),
2156 container_id: None,
2157 disable_api_termination: false,
2158 disable_api_stop: false,
2159 source_dest_check: true,
2160 ebs_optimized: false,
2161 instance_initiated_shutdown_behavior: "stop".into(),
2162 user_data: None,
2163 metadata_options: Default::default(),
2164 cpu_options: None,
2165 bandwidth_weighting: None,
2166 maintenance_options: Default::default(),
2167 placement_tenancy: None,
2168 placement_affinity: None,
2169 placement_group_name: None,
2170 private_dns_hostname_type: None,
2171 enable_resource_name_dns_a_record: false,
2172 enable_resource_name_dns_aaaa_record: false,
2173 };
2174 state.instances.insert(id.to_string(), inst);
2175 }
2176
2177 #[test]
2178 fn modify_instance_credit_specification_round_trips() {
2179 let svc = Ec2Service::new();
2180 seed_instance(&svc, "i-1");
2181 modify_instance_credit_specification(
2182 &svc,
2183 &req(
2184 "ModifyInstanceCreditSpecification",
2185 &[
2186 ("InstanceCreditSpecification.1.InstanceId", "i-1"),
2187 ("InstanceCreditSpecification.1.CpuCredits", "unlimited"),
2188 ],
2189 ),
2190 )
2191 .unwrap();
2192 let out = body(
2193 describe_instance_credit_specifications(
2194 &svc,
2195 &req(
2196 "DescribeInstanceCreditSpecifications",
2197 &[("InstanceId.1", "i-1")],
2198 ),
2199 )
2200 .unwrap(),
2201 );
2202 assert!(
2203 out.contains("<cpuCredits>unlimited</cpuCredits>"),
2204 "got: {out}"
2205 );
2206 }
2207
2208 #[test]
2209 fn modify_instance_credit_specification_unknown_is_unsuccessful() {
2210 let svc = Ec2Service::new();
2211 let out = body(
2212 modify_instance_credit_specification(
2213 &svc,
2214 &req(
2215 "ModifyInstanceCreditSpecification",
2216 &[("InstanceCreditSpecification.1.InstanceId", "i-missing")],
2217 ),
2218 )
2219 .unwrap(),
2220 );
2221 assert!(out.contains("InvalidInstanceID.NotFound"), "got: {out}");
2222 }
2223
2224 #[test]
2225 fn instance_metadata_defaults_round_trip_and_reset() {
2226 let svc = Ec2Service::new();
2227 modify_instance_metadata_defaults(
2228 &svc,
2229 &req(
2230 "ModifyInstanceMetadataDefaults",
2231 &[
2232 ("HttpTokens", "required"),
2233 ("HttpEndpoint", "enabled"),
2234 ("HttpTokensEnforced", "enabled"),
2235 ],
2236 ),
2237 )
2238 .unwrap();
2239 let out = body(
2240 get_instance_metadata_defaults(&svc, &req("GetInstanceMetadataDefaults", &[])).unwrap(),
2241 );
2242 assert!(
2243 out.contains("<httpTokens>required</httpTokens>"),
2244 "got: {out}"
2245 );
2246 assert!(out.contains("<httpEndpoint>enabled</httpEndpoint>"));
2247 assert!(out.contains("<httpTokensEnforced>enabled</httpTokensEnforced>"));
2249
2250 modify_instance_metadata_defaults(
2252 &svc,
2253 &req(
2254 "ModifyInstanceMetadataDefaults",
2255 &[("HttpTokens", "no-preference")],
2256 ),
2257 )
2258 .unwrap();
2259 let out = body(
2260 get_instance_metadata_defaults(&svc, &req("GetInstanceMetadataDefaults", &[])).unwrap(),
2261 );
2262 assert!(
2263 !out.contains("<httpTokens>"),
2264 "no-preference should clear it: {out}"
2265 );
2266 }
2267
2268 #[test]
2269 fn event_notification_attributes_persist_keys() {
2270 let svc = Ec2Service::new();
2271 register_event_notification_attributes(
2272 &svc,
2273 &req(
2274 "RegisterInstanceEventNotificationAttributes",
2275 &[
2276 ("InstanceTagAttribute.InstanceTagKey.1", "Name"),
2277 ("InstanceTagAttribute.InstanceTagKey.2", "env"),
2278 ],
2279 ),
2280 )
2281 .unwrap();
2282 let out = body(
2283 describe_event_notification_attributes(
2284 &svc,
2285 &req("DescribeInstanceEventNotificationAttributes", &[]),
2286 )
2287 .unwrap(),
2288 );
2289 assert!(out.contains("<item>Name</item>"), "got: {out}");
2290 assert!(out.contains("<item>env</item>"));
2291 assert!(!out.contains("<item><item>"), "got: {out}");
2294
2295 deregister_event_notification_attributes(
2296 &svc,
2297 &req(
2298 "DeregisterInstanceEventNotificationAttributes",
2299 &[("InstanceTagAttribute.InstanceTagKey.1", "Name")],
2300 ),
2301 )
2302 .unwrap();
2303 let out = body(
2304 describe_event_notification_attributes(
2305 &svc,
2306 &req("DescribeInstanceEventNotificationAttributes", &[]),
2307 )
2308 .unwrap(),
2309 );
2310 assert!(!out.contains("<item>Name</item>"), "got: {out}");
2311 assert!(out.contains("<item>env</item>"));
2312 }
2313
2314 #[test]
2315 fn describe_instances_explicit_missing_id_errors() {
2316 let svc = Ec2Service::new();
2317 seed_instance(&svc, "i-1");
2318 let err = crate::test_support::err_of(describe_instances(
2319 &svc,
2320 &req("DescribeInstances", &[("InstanceId.1", "i-missing")]),
2321 ));
2322 assert_eq!(err.code(), "InvalidInstanceID.NotFound");
2323 }
2324
2325 #[test]
2326 fn describe_instances_existing_id_ok() {
2327 let svc = Ec2Service::new();
2328 seed_instance(&svc, "i-1");
2329 let out = body(
2330 describe_instances(&svc, &req("DescribeInstances", &[("InstanceId.1", "i-1")]))
2331 .unwrap(),
2332 );
2333 assert!(out.contains("<instanceId>i-1</instanceId>"), "got: {out}");
2334 }
2335
2336 #[tokio::test]
2337 async fn reconcile_pending_metadata_only_flips_to_running() {
2338 let svc = Ec2Service::new();
2339 seed_instance(&svc, "i-1");
2340 {
2342 let mut accounts = svc.state.write();
2343 let inst = accounts
2344 .get_mut("000000000000")
2345 .unwrap()
2346 .instances
2347 .get_mut("i-1")
2348 .unwrap();
2349 inst.state_code = 0;
2350 inst.state_name = "pending".into();
2351 }
2352 svc.reconcile_pending_metadata_only().await;
2353 let accounts = svc.state.read();
2354 let inst = &accounts.get("000000000000").unwrap().instances["i-1"];
2355 assert_eq!(inst.state_code, 16);
2356 assert_eq!(inst.state_name, "running");
2357 }
2358
2359 fn state_of(svc: &Ec2Service, id: &str) -> (i64, Option<String>) {
2360 let accounts = svc.state.read();
2361 let inst = &accounts.get("000000000000").unwrap().instances[id];
2362 (inst.state_code, inst.container_id.clone())
2363 }
2364
2365 #[test]
2366 fn check_transition_enforces_terminal_and_protection() {
2367 let svc = Ec2Service::new();
2370 seed_instance(&svc, "i-1");
2371 let mut accounts = svc.state.write();
2372 let state = accounts.get_or_create("000000000000");
2373 let inst = state.instances.get_mut("i-1").unwrap();
2374
2375 assert!(check_transition(inst, "i-1", 16).is_ok());
2377 assert!(check_transition(inst, "i-1", 80).is_ok());
2378 assert!(check_transition(inst, "i-1", 48).is_ok());
2379
2380 inst.state_code = 48;
2382 inst.state_name = "terminated".into();
2383 assert_eq!(
2384 check_transition(inst, "i-1", 16).unwrap_err().code(),
2385 "IncorrectInstanceState"
2386 );
2387 assert!(check_transition(inst, "i-1", 48).is_ok());
2388
2389 inst.state_code = 16;
2391 inst.state_name = "running".into();
2392 inst.disable_api_termination = true;
2393 assert_eq!(
2394 check_transition(inst, "i-1", 48).unwrap_err().code(),
2395 "OperationNotPermitted"
2396 );
2397 inst.disable_api_termination = false;
2398 inst.disable_api_stop = true;
2399 assert_eq!(
2400 check_transition(inst, "i-1", 80).unwrap_err().code(),
2401 "OperationNotPermitted"
2402 );
2403 }
2404
2405 #[tokio::test]
2406 async fn start_after_terminate_does_not_resurrect() {
2407 let svc = Ec2Service::new();
2412 seed_instance(&svc, "i-1");
2413
2414 terminate_instances(&svc, &req("TerminateInstances", &[("InstanceId.1", "i-1")]))
2415 .await
2416 .unwrap();
2417 let (code, container) = state_of(&svc, "i-1");
2418 assert_eq!(code, 48, "terminate must set code 48");
2419 assert_eq!(container, None, "terminate must drop the container handle");
2420
2421 let err = start_instances(&svc, &req("StartInstances", &[("InstanceId.1", "i-1")]))
2422 .await
2423 .err()
2424 .expect("starting a terminated instance must fail");
2425 assert_eq!(err.code(), "IncorrectInstanceState");
2426
2427 let (code, _) = state_of(&svc, "i-1");
2429 assert_eq!(code, 48, "instance must stay terminated");
2430 }
2431
2432 #[tokio::test]
2433 async fn change_state_is_atomic_on_mixed_ids() {
2434 let svc = Ec2Service::new();
2437 seed_instance(&svc, "i-ok");
2438 seed_instance(&svc, "i-dead");
2439 terminate_instances(
2440 &svc,
2441 &req("TerminateInstances", &[("InstanceId.1", "i-dead")]),
2442 )
2443 .await
2444 .unwrap();
2445
2446 let err = start_instances(
2447 &svc,
2448 &req(
2449 "StartInstances",
2450 &[("InstanceId.1", "i-ok"), ("InstanceId.2", "i-dead")],
2451 ),
2452 )
2453 .await
2454 .err()
2455 .expect("batch with a terminated id must fail");
2456 assert_eq!(err.code(), "IncorrectInstanceState");
2457
2458 let (code, _) = state_of(&svc, "i-ok");
2460 assert_eq!(
2461 code, 16,
2462 "healthy instance must be untouched on atomic fail"
2463 );
2464 }
2465}