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
823async fn change_state(
824 svc: &Ec2Service,
825 req: &AwsRequest,
826 action: &str,
827 new_code: i64,
828 new_name: &str,
829) -> Result<AwsResponse, AwsServiceError> {
830 let ids = indexed_list(&req.query_params, "InstanceId");
831
832 {
836 let accounts = svc.state.read();
837 let empty = Ec2State::new(&req.account_id, &req.region);
838 let state = accounts.get(&req.account_id).unwrap_or(&empty);
839 for id in &ids {
840 let inst = state
841 .instances
842 .get(id)
843 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
844 if !transition_allowed(inst.state_code, new_code) {
845 return Err(crate::service_helpers::incorrect_instance_state(
846 id,
847 &inst.state_name,
848 ));
849 }
850 if new_code == 48 && inst.disable_api_termination {
852 return Err(AwsServiceError::aws_error(
853 http::StatusCode::BAD_REQUEST,
854 "OperationNotPermitted",
855 format!(
856 "The instance '{id}' may not be terminated. Modify its 'disableApiTermination' instance attribute and try again."
857 ),
858 ));
859 }
860 if new_code == 80 && inst.disable_api_stop {
861 return Err(AwsServiceError::aws_error(
862 http::StatusCode::BAD_REQUEST,
863 "OperationNotPermitted",
864 format!(
865 "The instance '{id}' may not be stopped. Modify its 'disableApiStop' instance attribute and try again."
866 ),
867 ));
868 }
869 }
870 }
871
872 let applied_code = if new_code == 16 { 0 } else { new_code };
878 let applied_name = if new_code == 16 { "pending" } else { new_name };
879
880 let mut changes = Vec::new();
881 let mut affected: Vec<String> = Vec::new();
882 {
883 let mut accounts = svc.state.write();
884 let state = accounts.get_or_create(&req.account_id);
885 for id in &ids {
886 let (prev_code, prev_name) = state
887 .instances
888 .get(id)
889 .map(|i| (i.state_code, i.state_name.clone()))
890 .unwrap_or((16, "running".to_string()));
891 if let Some(inst) = state.instances.get_mut(id) {
892 inst.state_code = applied_code;
893 inst.state_name = applied_name.to_string();
894 if new_code == 80 || new_code == 48 {
895 inst.public_ip = None;
896 }
897 affected.push(id.clone());
898 if new_code == 48 {
900 inst.container_id = None;
901 }
902 }
903 changes.push(format!(
904 "{}{}{}",
905 ec2_elem("instanceId", id),
906 state_xml("currentState", applied_code, applied_name),
907 state_xml("previousState", prev_code, &prev_name),
908 ));
909 }
910 }
911
912 {
916 let svc_state = svc.state.clone();
917 let runtime = svc.runtime.clone();
918 let account_id = req.account_id.clone();
919 tokio::spawn(async move {
920 for id in &affected {
921 match new_code {
922 16 => {
923 let running = match &runtime {
924 Some(rt) => rt.start_instance(id).await,
925 None => None,
926 };
927 reconcile_started(&svc_state, &account_id, id, running);
928 }
929 80 => {
930 if let Some(rt) = &runtime {
931 rt.stop_instance(id).await;
932 }
933 }
934 48 => {
935 if let Some(rt) = &runtime {
936 rt.terminate_instance(id).await;
937 }
938 }
939 _ => {}
940 }
941 }
942 if let Some(rt) = &runtime {
945 if rt.network_isolation_enforced() {
946 super::firewall_model::reconcile(&svc_state, rt).await;
947 }
948 }
949 });
950 }
951
952 Ok(Ec2Service::respond(
953 action,
954 &req.request_id,
955 &ec2_list("instancesSet", &changes),
956 ))
957}
958
959pub(crate) async fn start_instances(
960 svc: &Ec2Service,
961 req: &AwsRequest,
962) -> Result<AwsResponse, AwsServiceError> {
963 change_state(svc, req, "StartInstances", 16, "running").await
964}
965pub(crate) async fn stop_instances(
966 svc: &Ec2Service,
967 req: &AwsRequest,
968) -> Result<AwsResponse, AwsServiceError> {
969 change_state(svc, req, "StopInstances", 80, "stopped").await
970}
971pub(crate) async fn terminate_instances(
972 svc: &Ec2Service,
973 req: &AwsRequest,
974) -> Result<AwsResponse, AwsServiceError> {
975 change_state(svc, req, "TerminateInstances", 48, "terminated").await
976}
977
978pub(crate) async fn reboot_instances(
979 svc: &Ec2Service,
980 req: &AwsRequest,
981) -> Result<AwsResponse, AwsServiceError> {
982 let ids = indexed_list(&req.query_params, "InstanceId");
983 let backed: Vec<String> = {
986 let accounts = svc.state.read();
987 let empty = Ec2State::new(&req.account_id, &req.region);
988 let state = accounts.get(&req.account_id).unwrap_or(&empty);
989 let mut backed = Vec::new();
990 for id in &ids {
991 let inst = state
992 .instances
993 .get(id)
994 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
995 if inst.state_code == 48 {
996 return Err(crate::service_helpers::incorrect_instance_state(
997 id,
998 &inst.state_name,
999 ));
1000 }
1001 if inst.container_id.is_some() {
1002 backed.push(id.clone());
1003 }
1004 }
1005 backed
1006 };
1007 {
1010 let svc_state = svc.state.clone();
1011 let runtime = svc.runtime.clone();
1012 let account_id = req.account_id.clone();
1013 tokio::spawn(async move {
1014 let Some(rt) = runtime else {
1015 return;
1016 };
1017 for id in &backed {
1018 if let Some(running) = rt.reboot_instance(id).await {
1021 let mut accounts = svc_state.write();
1022 if let Some(state) = accounts.get_mut(&account_id) {
1023 if let Some(inst) = state.instances.get_mut(id) {
1024 if inst.state_code != 48 {
1027 inst.private_ip = running.private_ip;
1028 inst.container_id = Some(running.container_id);
1029 }
1030 }
1031 }
1032 }
1033 }
1034 if rt.network_isolation_enforced() {
1039 super::firewall_model::reconcile(&svc_state, &rt).await;
1040 }
1041 });
1042 }
1043 Ok(Ec2Service::respond(
1044 "RebootInstances",
1045 &req.request_id,
1046 &ec2_return(true),
1047 ))
1048}
1049
1050pub(crate) fn monitor_instances(
1051 svc: &Ec2Service,
1052 req: &AwsRequest,
1053) -> Result<AwsResponse, AwsServiceError> {
1054 monitor(svc, req, "MonitorInstances", true)
1055}
1056pub(crate) fn unmonitor_instances(
1057 svc: &Ec2Service,
1058 req: &AwsRequest,
1059) -> Result<AwsResponse, AwsServiceError> {
1060 monitor(svc, req, "UnmonitorInstances", false)
1061}
1062
1063fn monitor(
1064 svc: &Ec2Service,
1065 req: &AwsRequest,
1066 action: &str,
1067 enable: bool,
1068) -> Result<AwsResponse, AwsServiceError> {
1069 let ids = indexed_list(&req.query_params, "InstanceId");
1070 {
1071 let mut accounts = svc.state.write();
1072 let state = accounts.get_or_create(&req.account_id);
1073 for id in &ids {
1075 if !state.instances.contains_key(id) {
1076 return Err(crate::service_helpers::instance_not_found(id));
1077 }
1078 }
1079 for id in &ids {
1080 if let Some(i) = state.instances.get_mut(id) {
1081 i.monitoring = enable;
1082 }
1083 }
1084 }
1085 let items: Vec<String> = ids
1086 .iter()
1087 .map(|id| {
1088 format!(
1089 "{}<monitoring><state>{}</state></monitoring>",
1090 ec2_elem("instanceId", id),
1091 if enable { "pending" } else { "disabling" }
1092 )
1093 })
1094 .collect();
1095 Ok(Ec2Service::respond(
1096 action,
1097 &req.request_id,
1098 &ec2_list("instancesSet", &items),
1099 ))
1100}
1101
1102pub(crate) fn describe_instances(
1103 svc: &Ec2Service,
1104 req: &AwsRequest,
1105) -> Result<AwsResponse, AwsServiceError> {
1106 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1107 let max_results = parse_max_results(&req.query_params);
1108 let next_token = req.query_params.get("NextToken").map(String::as_str);
1109 let filters = parse_filters(&req.query_params);
1110 let wanted = indexed_list(&req.query_params, "InstanceId");
1111 let owner = req.account_id.clone();
1112 let accounts = svc.state.read();
1113 let empty = Ec2State::new(&req.account_id, &req.region);
1114 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1115
1116 for id in &wanted {
1119 if !state.instances.contains_key(id) {
1120 return Err(crate::service_helpers::instance_not_found(id));
1121 }
1122 }
1123
1124 let mut matching: Vec<&Instance> = state
1128 .instances
1129 .values()
1130 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1131 .filter(|i| {
1132 inst_match(
1133 i,
1134 state.tags_for(&i.instance_id),
1135 &filters,
1136 &arch_for(state, &i.image_id),
1137 )
1138 })
1139 .collect();
1140 matching.sort_by(|a, b| {
1141 a.reservation_id
1142 .cmp(&b.reservation_id)
1143 .then(a.instance_id.cmp(&b.instance_id))
1144 });
1145 let (page, token) = crate::service_helpers::paginate(&matching, next_token, max_results);
1146
1147 let sg_names = sg_name_map(state);
1149 let mut by_res: HashMap<String, Vec<String>> = HashMap::new();
1150 let mut order: Vec<String> = Vec::new();
1151 for i in page {
1152 if !by_res.contains_key(&i.reservation_id) {
1153 order.push(i.reservation_id.clone());
1154 }
1155 by_res
1156 .entry(i.reservation_id.clone())
1157 .or_default()
1158 .push(instance_xml(
1159 i,
1160 state.tags_for(&i.instance_id),
1161 &owner,
1162 &sg_names,
1163 &arch_for(state, &i.image_id),
1164 ));
1165 }
1166 let reservations: Vec<String> = order
1167 .iter()
1168 .map(|rid| {
1169 let insts = by_res.remove(rid).unwrap_or_default();
1170 reservation_xml(rid, &owner, &insts)
1171 })
1172 .collect();
1173 let body = format!(
1174 "{}{}",
1175 ec2_list("reservationSet", &reservations),
1176 token.map(|t| ec2_elem("nextToken", &t)).unwrap_or_default(),
1177 );
1178 Ok(Ec2Service::respond(
1179 "DescribeInstances",
1180 &req.request_id,
1181 &body,
1182 ))
1183}
1184
1185fn parse_max_results(params: &HashMap<String, String>) -> Option<usize> {
1188 params
1189 .get("MaxResults")
1190 .filter(|v| !v.is_empty())
1191 .and_then(|v| v.parse::<usize>().ok())
1192}
1193
1194fn inst_match(i: &Instance, tags: &[Tag], filters: &[Filter], architecture: &str) -> bool {
1195 use crate::service_helpers::filter_value_matches;
1196 filters.iter().all(|f| {
1197 let candidates: Vec<String> = match f.name.as_str() {
1198 "instance-id" => vec![i.instance_id.clone()],
1199 "instance-type" => vec![i.instance_type.clone()],
1200 "image-id" => vec![i.image_id.clone()],
1201 "instance-state-name" => vec![i.state_name.clone()],
1202 "instance-state-code" => vec![i.state_code.to_string()],
1203 "vpc-id" => i.vpc_id.clone().into_iter().collect(),
1204 "subnet-id" => i.subnet_id.clone().into_iter().collect(),
1205 "availability-zone" => vec![i.az.clone()],
1206 "private-ip-address" => vec![i.private_ip.clone()],
1207 "ip-address" => i.public_ip.clone().into_iter().collect(),
1208 "key-name" => i.key_name.clone().into_iter().collect(),
1209 "architecture" => vec![architecture.to_string()],
1210 "tag-key" => tags.iter().map(|t| t.key.clone()).collect(),
1211 name => {
1212 if let Some(key) = name.strip_prefix("tag:") {
1213 tags.iter()
1214 .filter(|t| t.key == key)
1215 .map(|t| t.value.clone())
1216 .collect()
1217 } else {
1218 return false;
1222 }
1223 }
1224 };
1225 f.values
1226 .iter()
1227 .any(|v| candidates.iter().any(|c| filter_value_matches(v, c)))
1228 })
1229}
1230
1231pub(crate) fn describe_instance_status(
1232 svc: &Ec2Service,
1233 req: &AwsRequest,
1234) -> Result<AwsResponse, AwsServiceError> {
1235 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1236 let max_results = parse_max_results(&req.query_params);
1237 let next_token = req.query_params.get("NextToken").map(String::as_str);
1238 let filters = parse_filters(&req.query_params);
1239 let wanted = indexed_list(&req.query_params, "InstanceId");
1240 let include_all = req
1241 .query_params
1242 .get("IncludeAllInstances")
1243 .map(|v| v == "true")
1244 .unwrap_or(false);
1245 let accounts = svc.state.read();
1246 let empty = Ec2State::new(&req.account_id, &req.region);
1247 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1248 let mut matching: Vec<&Instance> = state
1249 .instances
1250 .values()
1251 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1252 .filter(|i| include_all || i.state_name == "running")
1253 .filter(|i| {
1254 inst_match(
1255 i,
1256 state.tags_for(&i.instance_id),
1257 &filters,
1258 &arch_for(state, &i.image_id),
1259 )
1260 })
1261 .collect();
1262 matching.sort_by(|a, b| a.instance_id.cmp(&b.instance_id));
1263 let (page, token) = crate::service_helpers::paginate(&matching, next_token, max_results);
1264 let items: Vec<String> = page
1265 .iter()
1266 .map(|i| {
1267 format!(
1268 "{}{}{}{}{}{}",
1269 ec2_elem("instanceId", &i.instance_id),
1270 ec2_elem("availabilityZone", &i.az),
1271 state_xml("instanceState", i.state_code, &i.state_name),
1272 "<instanceStatus><status>ok</status></instanceStatus>",
1273 "<systemStatus><status>ok</status></systemStatus>",
1274 ec2_list("eventsSet", &[]),
1275 )
1276 })
1277 .collect();
1278 let body = format!(
1279 "{}{}",
1280 ec2_list("instanceStatusSet", &items),
1281 token.map(|t| ec2_elem("nextToken", &t)).unwrap_or_default(),
1282 );
1283 Ok(Ec2Service::respond(
1284 "DescribeInstanceStatus",
1285 &req.request_id,
1286 &body,
1287 ))
1288}
1289
1290fn instance_type_items(req: &AwsRequest) -> Vec<String> {
1291 let wanted = indexed_list(&req.query_params, "InstanceType");
1292 INSTANCE_TYPES
1293 .iter()
1294 .filter(|t| wanted.is_empty() || wanted.iter().any(|w| w == *t))
1295 .map(|t| {
1296 format!(
1297 "{}<currentGeneration>true</currentGeneration><bareMetal>false</bareMetal>\
1298 <hypervisor>nitro</hypervisor><instanceStorageSupported>false</instanceStorageSupported>\
1299 <processorInfo><supportedArchitectures><item>x86_64</item></supportedArchitectures></processorInfo>\
1300 <vCpuInfo><defaultVCpus>2</defaultVCpus></vCpuInfo>\
1301 <memoryInfo><sizeInMiB>1024</sizeInMiB></memoryInfo>\
1302 <supportedVirtualizationTypes><item>hvm</item></supportedVirtualizationTypes>",
1303 ec2_elem("instanceType", t),
1304 )
1305 })
1306 .collect()
1307}
1308
1309pub(crate) fn describe_instance_types(
1310 _svc: &Ec2Service,
1311 req: &AwsRequest,
1312) -> Result<AwsResponse, AwsServiceError> {
1313 crate::service_helpers::validate_max_results(&req.query_params, 5, 100)?;
1314 Ok(Ec2Service::respond(
1315 "DescribeInstanceTypes",
1316 &req.request_id,
1317 &ec2_list("instanceTypeSet", &instance_type_items(req)),
1318 ))
1319}
1320
1321pub(crate) fn get_instance_types_from_requirements(
1322 _svc: &Ec2Service,
1323 req: &AwsRequest,
1324) -> Result<AwsResponse, AwsServiceError> {
1325 require_struct(&req.query_params, "InstanceRequirements")?;
1326 let items: Vec<String> = INSTANCE_TYPES
1327 .iter()
1328 .map(|t| format!("<instanceType>{t}</instanceType>"))
1329 .collect();
1330 Ok(Ec2Service::respond(
1331 "GetInstanceTypesFromInstanceRequirements",
1332 &req.request_id,
1333 &ec2_list("instanceTypeSet", &items),
1334 ))
1335}
1336
1337pub(crate) fn describe_instance_attribute(
1340 svc: &Ec2Service,
1341 req: &AwsRequest,
1342) -> Result<AwsResponse, AwsServiceError> {
1343 let id = require(&req.query_params, "InstanceId")?;
1344 let attribute = require(&req.query_params, "Attribute")?;
1345 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1346 let accounts = svc.state.read();
1347 let acct_state = accounts.get(&req.account_id);
1348 let inst = acct_state
1349 .and_then(|s| s.instances.get(&id))
1350 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1351 let attr_xml = match attribute.as_str() {
1352 "instanceType" => format!(
1353 "<instanceType><value>{}</value></instanceType>",
1354 inst.instance_type
1355 ),
1356 "disableApiTermination" => format!(
1357 "<disableApiTermination><value>{}</value></disableApiTermination>",
1358 inst.disable_api_termination
1359 ),
1360 "disableApiStop" => format!(
1361 "<disableApiStop><value>{}</value></disableApiStop>",
1362 inst.disable_api_stop
1363 ),
1364 "ebsOptimized" => format!(
1365 "<ebsOptimized><value>{}</value></ebsOptimized>",
1366 inst.ebs_optimized
1367 ),
1368 "sourceDestCheck" => format!(
1369 "<sourceDestCheck><value>{}</value></sourceDestCheck>",
1370 inst.source_dest_check
1371 ),
1372 "instanceInitiatedShutdownBehavior" => format!(
1373 "<instanceInitiatedShutdownBehavior><value>{}</value></instanceInitiatedShutdownBehavior>",
1374 inst.instance_initiated_shutdown_behavior
1375 ),
1376 "userData" => match &inst.user_data {
1377 Some(d) => format!("<userData><value>{d}</value></userData>"),
1378 None => "<userData/>".to_string(),
1379 },
1380 "groupSet" => {
1381 let sg_names = acct_state.map(sg_name_map).unwrap_or_default();
1382 let groups: Vec<String> = inst
1383 .security_group_ids
1384 .iter()
1385 .map(|g| {
1386 let name = sg_names.get(g).map(String::as_str).unwrap_or(g.as_str());
1387 format!("{}{}", ec2_elem("groupId", g), ec2_elem("groupName", name))
1388 })
1389 .collect();
1390 ec2_list("groupSet", &groups)
1391 }
1392 _ => String::new(),
1393 };
1394 let body = format!("{}{}", ec2_elem("instanceId", &id), attr_xml);
1395 Ok(Ec2Service::respond(
1396 "DescribeInstanceAttribute",
1397 &req.request_id,
1398 &body,
1399 ))
1400}
1401
1402const ATTRIBUTE_VALUES: &[&str] = &[
1403 "instanceType",
1404 "kernel",
1405 "ramdisk",
1406 "userData",
1407 "disableApiTermination",
1408 "instanceInitiatedShutdownBehavior",
1409 "rootDeviceName",
1410 "blockDeviceMapping",
1411 "productCodes",
1412 "sourceDestCheck",
1413 "groupSet",
1414 "ebsOptimized",
1415 "sriovNetSupport",
1416 "enaSupport",
1417 "enclaveOptions",
1418 "disableApiStop",
1419];
1420
1421fn attr_bool(params: &HashMap<String, String>, key: &str) -> Option<bool> {
1425 params
1426 .get(&format!("{key}.Value"))
1427 .or_else(|| params.get(key))
1428 .map(|v| v == "true")
1429}
1430
1431fn attr_str<'a>(params: &'a HashMap<String, String>, key: &str) -> Option<&'a String> {
1432 params
1433 .get(&format!("{key}.Value"))
1434 .or_else(|| params.get(key))
1435}
1436
1437pub(crate) fn modify_instance_attribute(
1438 svc: &Ec2Service,
1439 req: &AwsRequest,
1440) -> Result<AwsResponse, AwsServiceError> {
1441 let id = require(&req.query_params, "InstanceId")?;
1442 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1446 let p = &req.query_params;
1447 let mut accounts = svc.state.write();
1448 let state = accounts.get_or_create(&req.account_id);
1449 let inst = state
1450 .instances
1451 .get_mut(&id)
1452 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1453
1454 if let Some(attr) = p.get("Attribute").filter(|v| !v.is_empty()) {
1456 let value = p.get("Value").cloned();
1457 match attr.as_str() {
1458 "instanceType" => {
1459 if let Some(v) = value {
1460 inst.instance_type = v;
1461 }
1462 }
1463 "userData" => inst.user_data = value.filter(|s| !s.is_empty()),
1464 "disableApiTermination" => {
1465 inst.disable_api_termination = value.as_deref() == Some("true")
1466 }
1467 "disableApiStop" => inst.disable_api_stop = value.as_deref() == Some("true"),
1468 "sourceDestCheck" => inst.source_dest_check = value.as_deref() == Some("true"),
1469 "ebsOptimized" => inst.ebs_optimized = value.as_deref() == Some("true"),
1470 "instanceInitiatedShutdownBehavior" => {
1471 if let Some(v) = value {
1472 inst.instance_initiated_shutdown_behavior = v;
1473 }
1474 }
1475 _ => {}
1476 }
1477 }
1478 if let Some(v) = attr_bool(p, "DisableApiTermination") {
1480 inst.disable_api_termination = v;
1481 }
1482 if let Some(v) = attr_bool(p, "DisableApiStop") {
1483 inst.disable_api_stop = v;
1484 }
1485 if let Some(v) = attr_bool(p, "SourceDestCheck") {
1486 inst.source_dest_check = v;
1487 }
1488 if let Some(v) = attr_bool(p, "EbsOptimized") {
1489 inst.ebs_optimized = v;
1490 }
1491 if let Some(v) = attr_str(p, "InstanceType") {
1492 inst.instance_type = v.clone();
1493 }
1494 if let Some(v) = attr_str(p, "InstanceInitiatedShutdownBehavior") {
1495 inst.instance_initiated_shutdown_behavior = v.clone();
1496 }
1497 if let Some(v) = attr_str(p, "UserData") {
1498 inst.user_data = Some(v.clone()).filter(|s| !s.is_empty());
1499 }
1500 let new_groups = indexed_list(p, "GroupId");
1501 if !new_groups.is_empty() {
1502 inst.security_group_ids = new_groups;
1503 }
1504
1505 Ok(Ec2Service::respond(
1506 "ModifyInstanceAttribute",
1507 &req.request_id,
1508 &ec2_return(true),
1509 ))
1510}
1511
1512pub(crate) fn reset_instance_attribute(
1513 svc: &Ec2Service,
1514 req: &AwsRequest,
1515) -> Result<AwsResponse, AwsServiceError> {
1516 let id = require(&req.query_params, "InstanceId")?;
1517 let attribute = require(&req.query_params, "Attribute")?;
1518 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1519 let mut accounts = svc.state.write();
1520 let state = accounts.get_or_create(&req.account_id);
1521 let inst = state
1522 .instances
1523 .get_mut(&id)
1524 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1525 match attribute.as_str() {
1528 "sourceDestCheck" => inst.source_dest_check = true,
1529 "disableApiTermination" => inst.disable_api_termination = false,
1530 "disableApiStop" => inst.disable_api_stop = false,
1531 "ebsOptimized" => inst.ebs_optimized = false,
1532 "userData" => inst.user_data = None,
1533 "instanceInitiatedShutdownBehavior" => {
1534 inst.instance_initiated_shutdown_behavior = "stop".to_string()
1535 }
1536 _ => {}
1537 }
1538 Ok(Ec2Service::respond(
1539 "ResetInstanceAttribute",
1540 &req.request_id,
1541 &ec2_return(true),
1542 ))
1543}
1544
1545fn with_instance_mut<R>(
1550 svc: &Ec2Service,
1551 account_id: &str,
1552 id: &str,
1553 f: impl FnOnce(&mut Instance) -> R,
1554) -> Result<R, AwsServiceError> {
1555 let mut accounts = svc.state.write();
1556 let state = accounts.get_or_create(account_id);
1557 let inst = state
1558 .instances
1559 .get_mut(id)
1560 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
1561 Ok(f(inst))
1562}
1563
1564pub(crate) fn modify_instance_placement(
1565 svc: &Ec2Service,
1566 req: &AwsRequest,
1567) -> Result<AwsResponse, AwsServiceError> {
1568 let id = require(&req.query_params, "InstanceId")?;
1569 validate_enum(
1570 &req.query_params,
1571 "Tenancy",
1572 &["default", "dedicated", "host"],
1573 )?;
1574 validate_enum(&req.query_params, "Affinity", &["default", "host"])?;
1575 let p = req.query_params.clone();
1576 with_instance_mut(svc, &req.account_id, &id, |inst| {
1577 if let Some(t) = p.get("Tenancy").filter(|v| !v.is_empty()) {
1578 inst.placement_tenancy = Some(t.clone());
1579 }
1580 if let Some(a) = p.get("Affinity").filter(|v| !v.is_empty()) {
1581 inst.placement_affinity = Some(a.clone());
1582 }
1583 if let Some(g) = p.get("GroupName") {
1584 inst.placement_group_name = Some(g.clone()).filter(|s| !s.is_empty());
1585 }
1586 })?;
1587 Ok(Ec2Service::respond(
1588 "ModifyInstancePlacement",
1589 &req.request_id,
1590 &ec2_return(true),
1591 ))
1592}
1593
1594pub(crate) fn modify_instance_metadata_options(
1595 svc: &Ec2Service,
1596 req: &AwsRequest,
1597) -> Result<AwsResponse, AwsServiceError> {
1598 let id = require(&req.query_params, "InstanceId")?;
1599 validate_enum(&req.query_params, "HttpTokens", &["optional", "required"])?;
1600 validate_enum(&req.query_params, "HttpEndpoint", &["disabled", "enabled"])?;
1601 validate_enum(
1602 &req.query_params,
1603 "HttpProtocolIpv6",
1604 &["disabled", "enabled"],
1605 )?;
1606 validate_enum(
1607 &req.query_params,
1608 "InstanceMetadataTags",
1609 &["disabled", "enabled"],
1610 )?;
1611 let p = req.query_params.clone();
1612 let opts = with_instance_mut(svc, &req.account_id, &id, |inst| {
1613 let m = &mut inst.metadata_options;
1614 if let Some(v) = p.get("HttpTokens").filter(|v| !v.is_empty()) {
1615 m.http_tokens = v.clone();
1616 }
1617 if let Some(v) = p.get("HttpEndpoint").filter(|v| !v.is_empty()) {
1618 m.http_endpoint = v.clone();
1619 }
1620 if let Some(v) = p.get("HttpProtocolIpv6").filter(|v| !v.is_empty()) {
1621 m.http_protocol_ipv6 = v.clone();
1622 }
1623 if let Some(v) = p.get("InstanceMetadataTags").filter(|v| !v.is_empty()) {
1624 m.instance_metadata_tags = v.clone();
1625 }
1626 if let Some(n) = p
1627 .get("HttpPutResponseHopLimit")
1628 .and_then(|v| v.parse::<i64>().ok())
1629 {
1630 m.http_put_response_hop_limit = n;
1631 }
1632 m.clone()
1633 })?;
1634 let body = format!(
1635 "{}<instanceMetadataOptions><state>applied</state><httpTokens>{}</httpTokens>\
1636 <httpPutResponseHopLimit>{}</httpPutResponseHopLimit><httpEndpoint>{}</httpEndpoint>\
1637 <httpProtocolIpv6>{}</httpProtocolIpv6><instanceMetadataTags>{}</instanceMetadataTags></instanceMetadataOptions>",
1638 ec2_elem("instanceId", &id),
1639 opts.http_tokens,
1640 opts.http_put_response_hop_limit,
1641 opts.http_endpoint,
1642 opts.http_protocol_ipv6,
1643 opts.instance_metadata_tags,
1644 );
1645 Ok(Ec2Service::respond(
1646 "ModifyInstanceMetadataOptions",
1647 &req.request_id,
1648 &body,
1649 ))
1650}
1651
1652pub(crate) fn modify_instance_maintenance_options(
1653 svc: &Ec2Service,
1654 req: &AwsRequest,
1655) -> Result<AwsResponse, AwsServiceError> {
1656 let id = require(&req.query_params, "InstanceId")?;
1657 validate_enum(&req.query_params, "AutoRecovery", &["disabled", "default"])?;
1658 validate_enum(
1659 &req.query_params,
1660 "RebootMigration",
1661 &["disabled", "default"],
1662 )?;
1663 let p = req.query_params.clone();
1664 let opts = with_instance_mut(svc, &req.account_id, &id, |inst| {
1665 let m = &mut inst.maintenance_options;
1666 if let Some(v) = p.get("AutoRecovery").filter(|v| !v.is_empty()) {
1667 m.auto_recovery = v.clone();
1668 }
1669 if let Some(v) = p.get("RebootMigration").filter(|v| !v.is_empty()) {
1670 m.reboot_migration = v.clone();
1671 }
1672 m.clone()
1673 })?;
1674 let body = format!(
1675 "{}<maintenanceOptions><autoRecovery>{}</autoRecovery><rebootMigration>{}</rebootMigration></maintenanceOptions>",
1676 ec2_elem("instanceId", &id),
1677 opts.auto_recovery,
1678 opts.reboot_migration,
1679 );
1680 Ok(Ec2Service::respond(
1681 "ModifyInstanceMaintenanceOptions",
1682 &req.request_id,
1683 &body,
1684 ))
1685}
1686
1687pub(crate) fn modify_instance_cpu_options(
1688 svc: &Ec2Service,
1689 req: &AwsRequest,
1690) -> Result<AwsResponse, AwsServiceError> {
1691 let id = require(&req.query_params, "InstanceId")?;
1692 validate_enum(
1693 &req.query_params,
1694 "NestedVirtualization",
1695 &["disabled", "enabled"],
1696 )?;
1697 let core_count = req
1698 .query_params
1699 .get("CoreCount")
1700 .and_then(|v| v.parse::<i64>().ok())
1701 .unwrap_or(2);
1702 let threads_per_core = req
1703 .query_params
1704 .get("ThreadsPerCore")
1705 .and_then(|v| v.parse::<i64>().ok())
1706 .unwrap_or(1);
1707 with_instance_mut(svc, &req.account_id, &id, |inst| {
1708 inst.cpu_options = Some(crate::state::CpuOptions {
1709 core_count,
1710 threads_per_core,
1711 });
1712 })?;
1713 let body = format!(
1714 "{}<coreCount>{core_count}</coreCount><threadsPerCore>{threads_per_core}</threadsPerCore>",
1715 ec2_elem("instanceId", &id)
1716 );
1717 Ok(Ec2Service::respond(
1718 "ModifyInstanceCpuOptions",
1719 &req.request_id,
1720 &body,
1721 ))
1722}
1723
1724pub(crate) fn modify_instance_network_performance_options(
1725 svc: &Ec2Service,
1726 req: &AwsRequest,
1727) -> Result<AwsResponse, AwsServiceError> {
1728 let id = require(&req.query_params, "InstanceId")?;
1729 let weighting = require(&req.query_params, "BandwidthWeighting")?;
1730 validate_enum(
1731 &req.query_params,
1732 "BandwidthWeighting",
1733 &["default", "vpc-1", "ebs-1"],
1734 )?;
1735 with_instance_mut(svc, &req.account_id, &id, |inst| {
1736 inst.bandwidth_weighting = Some(weighting.clone());
1737 })?;
1738 let body = format!(
1739 "{}<bandwidthWeighting>{weighting}</bandwidthWeighting>",
1740 ec2_elem("instanceId", &id)
1741 );
1742 Ok(Ec2Service::respond(
1743 "ModifyInstanceNetworkPerformanceOptions",
1744 &req.request_id,
1745 &body,
1746 ))
1747}
1748
1749pub(crate) fn modify_instance_event_start_time(
1750 _svc: &Ec2Service,
1751 req: &AwsRequest,
1752) -> Result<AwsResponse, AwsServiceError> {
1753 require(&req.query_params, "InstanceId")?;
1754 let event_id = require(&req.query_params, "InstanceEventId")?;
1755 require(&req.query_params, "NotBefore")?;
1756 let body = format!(
1757 "<event>{}<code>system-reboot</code><description>scheduled</description></event>",
1758 ec2_elem("instanceEventId", &event_id)
1759 );
1760 Ok(Ec2Service::respond(
1761 "ModifyInstanceEventStartTime",
1762 &req.request_id,
1763 &body,
1764 ))
1765}
1766
1767pub(crate) fn describe_instance_credit_specifications(
1768 svc: &Ec2Service,
1769 req: &AwsRequest,
1770) -> Result<AwsResponse, AwsServiceError> {
1771 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1772 let wanted = indexed_list(&req.query_params, "InstanceId");
1773 let accounts = svc.state.read();
1774 let empty = Ec2State::new(&req.account_id, &req.region);
1775 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1776 let items: Vec<String> = state
1777 .instances
1778 .values()
1779 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1780 .map(|i| {
1781 let credits = state
1785 .instance_credit_specs
1786 .get(&i.instance_id)
1787 .cloned()
1788 .unwrap_or_else(|| "standard".to_string());
1789 format!(
1790 "{}{}",
1791 ec2_elem("instanceId", &i.instance_id),
1792 ec2_elem("cpuCredits", &credits)
1793 )
1794 })
1795 .collect();
1796 Ok(Ec2Service::respond(
1797 "DescribeInstanceCreditSpecifications",
1798 &req.request_id,
1799 &ec2_list("instanceCreditSpecificationSet", &items),
1800 ))
1801}
1802
1803pub(crate) fn modify_instance_credit_specification(
1804 svc: &Ec2Service,
1805 req: &AwsRequest,
1806) -> Result<AwsResponse, AwsServiceError> {
1807 let p = &req.query_params;
1808 let mut successful = Vec::new();
1809 let mut unsuccessful = Vec::new();
1810 {
1811 let mut accounts = svc.state.write();
1812 let state = accounts.get_or_create(&req.account_id);
1813 let mut n = 1usize;
1814 loop {
1815 let id_key = format!("InstanceCreditSpecification.{n}.InstanceId");
1816 let Some(instance_id) = p.get(&id_key).cloned() else {
1817 break;
1818 };
1819 let credits = p
1820 .get(&format!("InstanceCreditSpecification.{n}.CpuCredits"))
1821 .cloned()
1822 .unwrap_or_else(|| "standard".to_string());
1823 if state.instances.contains_key(&instance_id) {
1824 state
1825 .instance_credit_specs
1826 .insert(instance_id.clone(), credits);
1827 successful.push(ec2_elem("instanceId", &instance_id));
1828 } else {
1829 unsuccessful.push(format!(
1830 "{}<error><code>InvalidInstanceID.NotFound</code><message>The instance ID '{instance_id}' does not exist</message></error>",
1831 ec2_elem("instanceId", &instance_id)
1832 ));
1833 }
1834 n += 1;
1835 }
1836 }
1837 let body = format!(
1838 "{}{}",
1839 ec2_list("successfulInstanceCreditSpecificationSet", &successful),
1840 ec2_list("unsuccessfulInstanceCreditSpecificationSet", &unsuccessful),
1841 );
1842 Ok(Ec2Service::respond(
1843 "ModifyInstanceCreditSpecification",
1844 &req.request_id,
1845 &body,
1846 ))
1847}
1848
1849pub(crate) fn get_instance_metadata_defaults(
1850 svc: &Ec2Service,
1851 req: &AwsRequest,
1852) -> Result<AwsResponse, AwsServiceError> {
1853 let accounts = svc.state.read();
1854 let d = accounts
1855 .get(&req.account_id)
1856 .and_then(|s| s.instance_metadata_defaults.clone())
1857 .unwrap_or_default();
1858 let mut inner = String::new();
1859 if let Some(v) = &d.http_tokens {
1860 inner.push_str(&ec2_elem("httpTokens", v));
1861 }
1862 if let Some(v) = &d.http_endpoint {
1863 inner.push_str(&ec2_elem("httpEndpoint", v));
1864 }
1865 if let Some(v) = d.http_put_response_hop_limit {
1866 inner.push_str(&ec2_elem("httpPutResponseHopLimit", &v.to_string()));
1867 }
1868 if let Some(v) = &d.instance_metadata_tags {
1869 inner.push_str(&ec2_elem("instanceMetadataTags", v));
1870 }
1871 if let Some(v) = &d.http_tokens_enforced {
1872 inner.push_str(&ec2_elem("httpTokensEnforced", v));
1873 }
1874 Ok(Ec2Service::respond(
1875 "GetInstanceMetadataDefaults",
1876 &req.request_id,
1877 &format!("<accountLevel>{inner}</accountLevel>"),
1878 ))
1879}
1880
1881pub(crate) fn modify_instance_metadata_defaults(
1882 svc: &Ec2Service,
1883 req: &AwsRequest,
1884) -> Result<AwsResponse, AwsServiceError> {
1885 let p = &req.query_params;
1886 validate_enum(p, "HttpTokens", &["optional", "required", "no-preference"])?;
1887 validate_enum(p, "HttpEndpoint", &["disabled", "enabled", "no-preference"])?;
1888 validate_enum(
1889 p,
1890 "InstanceMetadataTags",
1891 &["disabled", "enabled", "no-preference"],
1892 )?;
1893 validate_enum(
1894 p,
1895 "HttpTokensEnforced",
1896 &["disabled", "enabled", "no-preference"],
1897 )?;
1898 let apply = |cur: &mut Option<String>, key: &str| {
1900 if let Some(v) = p.get(key) {
1901 *cur = if v == "no-preference" {
1902 None
1903 } else {
1904 Some(v.clone())
1905 };
1906 }
1907 };
1908 {
1909 let mut accounts = svc.state.write();
1910 let state = accounts.get_or_create(&req.account_id);
1911 let d = state
1912 .instance_metadata_defaults
1913 .get_or_insert_with(Default::default);
1914 apply(&mut d.http_tokens, "HttpTokens");
1915 apply(&mut d.http_endpoint, "HttpEndpoint");
1916 apply(&mut d.instance_metadata_tags, "InstanceMetadataTags");
1917 apply(&mut d.http_tokens_enforced, "HttpTokensEnforced");
1918 if let Some(v) = p
1919 .get("HttpPutResponseHopLimit")
1920 .and_then(|v| v.parse::<i64>().ok())
1921 {
1922 d.http_put_response_hop_limit = if v < 0 { None } else { Some(v) };
1924 }
1925 }
1926 Ok(Ec2Service::respond(
1927 "ModifyInstanceMetadataDefaults",
1928 &req.request_id,
1929 &ec2_return(true),
1930 ))
1931}
1932
1933pub(crate) fn register_event_notification_attributes(
1934 svc: &Ec2Service,
1935 req: &AwsRequest,
1936) -> Result<AwsResponse, AwsServiceError> {
1937 let keys = indexed_sub_keys(&req.query_params);
1938 let include_all = req
1939 .query_params
1940 .get("InstanceTagAttribute.IncludeAllTagsOfInstance")
1941 .map(|v| v == "true");
1942 let xml = {
1943 let mut accounts = svc.state.write();
1944 let state = accounts.get_or_create(&req.account_id);
1945 for k in keys {
1946 if !state.event_notification_tag_keys.contains(&k) {
1947 state.event_notification_tag_keys.push(k);
1948 }
1949 }
1950 if let Some(v) = include_all {
1951 state.event_notification_include_all_tags = v;
1952 }
1953 event_tag_attribute(state)
1954 };
1955 Ok(Ec2Service::respond(
1956 "RegisterInstanceEventNotificationAttributes",
1957 &req.request_id,
1958 &xml,
1959 ))
1960}
1961
1962pub(crate) fn deregister_event_notification_attributes(
1963 svc: &Ec2Service,
1964 req: &AwsRequest,
1965) -> Result<AwsResponse, AwsServiceError> {
1966 let keys = indexed_sub_keys(&req.query_params);
1967 let include_all = req
1968 .query_params
1969 .get("InstanceTagAttribute.IncludeAllTagsOfInstance")
1970 .map(|v| v == "true");
1971 let xml = {
1972 let mut accounts = svc.state.write();
1973 let state = accounts.get_or_create(&req.account_id);
1974 state
1975 .event_notification_tag_keys
1976 .retain(|k| !keys.contains(k));
1977 if include_all == Some(true) {
1979 state.event_notification_include_all_tags = false;
1980 }
1981 event_tag_attribute(state)
1982 };
1983 Ok(Ec2Service::respond(
1984 "DeregisterInstanceEventNotificationAttributes",
1985 &req.request_id,
1986 &xml,
1987 ))
1988}
1989
1990pub(crate) fn describe_event_notification_attributes(
1991 svc: &Ec2Service,
1992 req: &AwsRequest,
1993) -> Result<AwsResponse, AwsServiceError> {
1994 let accounts = svc.state.read();
1995 let empty = Ec2State::new(&req.account_id, &req.region);
1996 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1997 Ok(Ec2Service::respond(
1998 "DescribeInstanceEventNotificationAttributes",
1999 &req.request_id,
2000 &event_tag_attribute(state),
2001 ))
2002}
2003
2004fn indexed_sub_keys(params: &HashMap<String, String>) -> Vec<String> {
2006 let mut out = Vec::new();
2007 let mut n = 1usize;
2008 while let Some(v) = params.get(&format!("InstanceTagAttribute.InstanceTagKey.{n}")) {
2009 out.push(v.clone());
2010 n += 1;
2011 }
2012 out
2013}
2014
2015fn event_tag_attribute(state: &Ec2State) -> String {
2016 let keys: Vec<String> = state
2019 .event_notification_tag_keys
2020 .iter()
2021 .map(|k| fakecloud_aws::xml::xml_escape(k))
2022 .collect();
2023 format!(
2024 "<instanceTagAttribute><includeAllTagsOfInstance>{}</includeAllTagsOfInstance>{}</instanceTagAttribute>",
2025 state.event_notification_include_all_tags,
2026 ec2_list("instanceTagKeySet", &keys)
2027 )
2028}
2029
2030pub(crate) fn report_instance_status(
2031 _svc: &Ec2Service,
2032 req: &AwsRequest,
2033) -> Result<AwsResponse, AwsServiceError> {
2034 require(&req.query_params, "Status")?;
2035 validate_enum(&req.query_params, "Status", &["ok", "impaired"])?;
2036 Ok(Ec2Service::respond(
2037 "ReportInstanceStatus",
2038 &req.request_id,
2039 &ec2_return(true),
2040 ))
2041}
2042
2043pub(crate) fn describe_instance_topology(
2044 _svc: &Ec2Service,
2045 req: &AwsRequest,
2046) -> Result<AwsResponse, AwsServiceError> {
2047 crate::service_helpers::validate_max_results(&req.query_params, 1, 100)?;
2048 Ok(Ec2Service::respond(
2049 "DescribeInstanceTopology",
2050 &req.request_id,
2051 &ec2_list("instanceSet", &[]),
2052 ))
2053}
2054
2055#[cfg(test)]
2056mod tests {
2057 use super::subnet_ip_prefix;
2058
2059 #[test]
2060 fn subnet_ip_prefix_uses_subnet_network() {
2061 assert_eq!(subnet_ip_prefix("172.31.16.0/20"), "172.31.16");
2063 assert_eq!(subnet_ip_prefix("10.0.5.0/24"), "10.0.5");
2064 assert_eq!(subnet_ip_prefix("192.168.1.0"), "192.168.1");
2066 }
2067
2068 #[test]
2069 fn subnet_ip_prefix_falls_back_on_garbage() {
2070 assert_eq!(subnet_ip_prefix(""), "10.0.0");
2071 assert_eq!(subnet_ip_prefix("not-a-cidr"), "10.0.0");
2072 assert_eq!(subnet_ip_prefix("fd00::/8"), "10.0.0");
2074 }
2075}
2076
2077#[cfg(test)]
2078mod modify_tests {
2079 use super::*;
2080
2081 fn req(action: &str, query: &[(&str, &str)]) -> AwsRequest {
2082 AwsRequest {
2083 service: "ec2".into(),
2084 action: action.into(),
2085 region: "us-east-1".into(),
2086 account_id: "000000000000".into(),
2087 request_id: "rid".into(),
2088 headers: http::HeaderMap::new(),
2089 query_params: query
2090 .iter()
2091 .map(|(k, v)| (k.to_string(), v.to_string()))
2092 .collect(),
2093 body: bytes::Bytes::new(),
2094 body_stream: parking_lot::Mutex::new(None),
2095 path_segments: Vec::new(),
2096 raw_path: "/".into(),
2097 raw_query: String::new(),
2098 method: http::Method::POST,
2099 is_query_protocol: true,
2100 access_key_id: None,
2101 principal: None,
2102 }
2103 }
2104
2105 fn body(resp: AwsResponse) -> String {
2106 String::from_utf8_lossy(resp.body.expect_bytes()).to_string()
2107 }
2108
2109 fn seed_instance(svc: &Ec2Service, id: &str) {
2110 let mut accounts = svc.state.write();
2111 let state = accounts.get_or_create("000000000000");
2112 let inst = Instance {
2113 instance_id: id.into(),
2114 image_id: "ami-1".into(),
2115 instance_type: "t3.micro".into(),
2116 state_code: 16,
2117 state_name: "running".into(),
2118 private_ip: "10.0.0.5".into(),
2119 public_ip: None,
2120 subnet_id: Some("subnet-1".into()),
2121 vpc_id: Some("vpc-1".into()),
2122 key_name: None,
2123 security_group_ids: vec![],
2124 reservation_id: "r-1".into(),
2125 ami_launch_index: 0,
2126 monitoring: false,
2127 az: "us-east-1a".into(),
2128 launch_time: "2024-01-01T00:00:00.000Z".into(),
2129 container_id: None,
2130 disable_api_termination: false,
2131 disable_api_stop: false,
2132 source_dest_check: true,
2133 ebs_optimized: false,
2134 instance_initiated_shutdown_behavior: "stop".into(),
2135 user_data: None,
2136 metadata_options: Default::default(),
2137 cpu_options: None,
2138 bandwidth_weighting: None,
2139 maintenance_options: Default::default(),
2140 placement_tenancy: None,
2141 placement_affinity: None,
2142 placement_group_name: None,
2143 private_dns_hostname_type: None,
2144 enable_resource_name_dns_a_record: false,
2145 enable_resource_name_dns_aaaa_record: false,
2146 };
2147 state.instances.insert(id.to_string(), inst);
2148 }
2149
2150 #[test]
2151 fn modify_instance_credit_specification_round_trips() {
2152 let svc = Ec2Service::new();
2153 seed_instance(&svc, "i-1");
2154 modify_instance_credit_specification(
2155 &svc,
2156 &req(
2157 "ModifyInstanceCreditSpecification",
2158 &[
2159 ("InstanceCreditSpecification.1.InstanceId", "i-1"),
2160 ("InstanceCreditSpecification.1.CpuCredits", "unlimited"),
2161 ],
2162 ),
2163 )
2164 .unwrap();
2165 let out = body(
2166 describe_instance_credit_specifications(
2167 &svc,
2168 &req(
2169 "DescribeInstanceCreditSpecifications",
2170 &[("InstanceId.1", "i-1")],
2171 ),
2172 )
2173 .unwrap(),
2174 );
2175 assert!(
2176 out.contains("<cpuCredits>unlimited</cpuCredits>"),
2177 "got: {out}"
2178 );
2179 }
2180
2181 #[test]
2182 fn modify_instance_credit_specification_unknown_is_unsuccessful() {
2183 let svc = Ec2Service::new();
2184 let out = body(
2185 modify_instance_credit_specification(
2186 &svc,
2187 &req(
2188 "ModifyInstanceCreditSpecification",
2189 &[("InstanceCreditSpecification.1.InstanceId", "i-missing")],
2190 ),
2191 )
2192 .unwrap(),
2193 );
2194 assert!(out.contains("InvalidInstanceID.NotFound"), "got: {out}");
2195 }
2196
2197 #[test]
2198 fn instance_metadata_defaults_round_trip_and_reset() {
2199 let svc = Ec2Service::new();
2200 modify_instance_metadata_defaults(
2201 &svc,
2202 &req(
2203 "ModifyInstanceMetadataDefaults",
2204 &[
2205 ("HttpTokens", "required"),
2206 ("HttpEndpoint", "enabled"),
2207 ("HttpTokensEnforced", "enabled"),
2208 ],
2209 ),
2210 )
2211 .unwrap();
2212 let out = body(
2213 get_instance_metadata_defaults(&svc, &req("GetInstanceMetadataDefaults", &[])).unwrap(),
2214 );
2215 assert!(
2216 out.contains("<httpTokens>required</httpTokens>"),
2217 "got: {out}"
2218 );
2219 assert!(out.contains("<httpEndpoint>enabled</httpEndpoint>"));
2220 assert!(out.contains("<httpTokensEnforced>enabled</httpTokensEnforced>"));
2222
2223 modify_instance_metadata_defaults(
2225 &svc,
2226 &req(
2227 "ModifyInstanceMetadataDefaults",
2228 &[("HttpTokens", "no-preference")],
2229 ),
2230 )
2231 .unwrap();
2232 let out = body(
2233 get_instance_metadata_defaults(&svc, &req("GetInstanceMetadataDefaults", &[])).unwrap(),
2234 );
2235 assert!(
2236 !out.contains("<httpTokens>"),
2237 "no-preference should clear it: {out}"
2238 );
2239 }
2240
2241 #[test]
2242 fn event_notification_attributes_persist_keys() {
2243 let svc = Ec2Service::new();
2244 register_event_notification_attributes(
2245 &svc,
2246 &req(
2247 "RegisterInstanceEventNotificationAttributes",
2248 &[
2249 ("InstanceTagAttribute.InstanceTagKey.1", "Name"),
2250 ("InstanceTagAttribute.InstanceTagKey.2", "env"),
2251 ],
2252 ),
2253 )
2254 .unwrap();
2255 let out = body(
2256 describe_event_notification_attributes(
2257 &svc,
2258 &req("DescribeInstanceEventNotificationAttributes", &[]),
2259 )
2260 .unwrap(),
2261 );
2262 assert!(out.contains("<item>Name</item>"), "got: {out}");
2263 assert!(out.contains("<item>env</item>"));
2264 assert!(!out.contains("<item><item>"), "got: {out}");
2267
2268 deregister_event_notification_attributes(
2269 &svc,
2270 &req(
2271 "DeregisterInstanceEventNotificationAttributes",
2272 &[("InstanceTagAttribute.InstanceTagKey.1", "Name")],
2273 ),
2274 )
2275 .unwrap();
2276 let out = body(
2277 describe_event_notification_attributes(
2278 &svc,
2279 &req("DescribeInstanceEventNotificationAttributes", &[]),
2280 )
2281 .unwrap(),
2282 );
2283 assert!(!out.contains("<item>Name</item>"), "got: {out}");
2284 assert!(out.contains("<item>env</item>"));
2285 }
2286
2287 #[test]
2288 fn describe_instances_explicit_missing_id_errors() {
2289 let svc = Ec2Service::new();
2290 seed_instance(&svc, "i-1");
2291 let err = crate::test_support::err_of(describe_instances(
2292 &svc,
2293 &req("DescribeInstances", &[("InstanceId.1", "i-missing")]),
2294 ));
2295 assert_eq!(err.code(), "InvalidInstanceID.NotFound");
2296 }
2297
2298 #[test]
2299 fn describe_instances_existing_id_ok() {
2300 let svc = Ec2Service::new();
2301 seed_instance(&svc, "i-1");
2302 let out = body(
2303 describe_instances(&svc, &req("DescribeInstances", &[("InstanceId.1", "i-1")]))
2304 .unwrap(),
2305 );
2306 assert!(out.contains("<instanceId>i-1</instanceId>"), "got: {out}");
2307 }
2308
2309 #[tokio::test]
2310 async fn reconcile_pending_metadata_only_flips_to_running() {
2311 let svc = Ec2Service::new();
2312 seed_instance(&svc, "i-1");
2313 {
2315 let mut accounts = svc.state.write();
2316 let inst = accounts
2317 .get_mut("000000000000")
2318 .unwrap()
2319 .instances
2320 .get_mut("i-1")
2321 .unwrap();
2322 inst.state_code = 0;
2323 inst.state_name = "pending".into();
2324 }
2325 svc.reconcile_pending_metadata_only().await;
2326 let accounts = svc.state.read();
2327 let inst = &accounts.get("000000000000").unwrap().instances["i-1"];
2328 assert_eq!(inst.state_code, 16);
2329 assert_eq!(inst.state_name, "running");
2330 }
2331}