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, parse_filters, require,
13 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 image_id = req
243 .query_params
244 .get("ImageId")
245 .cloned()
246 .unwrap_or_else(|| "ami-00000000000000000".to_string());
247 let instance_type = req
248 .query_params
249 .get("InstanceType")
250 .cloned()
251 .unwrap_or_else(|| "t3.micro".to_string());
252 let key_name = req.query_params.get("KeyName").cloned();
253 let mut subnet_id = req.query_params.get("SubnetId").cloned();
254 let mut sg_ids = indexed_list(&req.query_params, "SecurityGroupId");
255 let user_data = req.query_params.get("UserData").cloned();
256 let owner = req.account_id.clone();
257 let az = format!(
258 "{}a",
259 if req.region.is_empty() {
260 "us-east-1"
261 } else {
262 &req.region
263 }
264 );
265
266 let instance_tags = crate::service::tags::tag_specifications_for(&req.query_params, "instance");
269
270 let assoc_public = req
277 .query_params
278 .get("NetworkInterface.1.AssociatePublicIpAddress")
279 .or_else(|| req.query_params.get("AssociatePublicIpAddress"))
280 .map(|v| v == "true");
281 let (vpc_id, subnet_auto_public, instance_network, ip_prefix) = {
282 let accounts = svc.state.read();
283 let empty = Ec2State::new(&req.account_id, &req.region);
284 let state = accounts.get(&req.account_id).unwrap_or(&empty);
285 if subnet_id.is_none() {
290 if let Some(s) = state
291 .subnets
292 .values()
293 .filter(|s| s.default_for_az)
294 .find(|s| s.availability_zone == az)
295 .or_else(|| state.subnets.values().find(|s| s.default_for_az))
296 {
297 subnet_id = Some(s.subnet_id.clone());
298 }
299 }
300 let resolved_vpc = subnet_id
303 .as_ref()
304 .and_then(|sid| state.subnets.get(sid))
305 .map(|s| s.vpc_id.clone());
306 if sg_ids.is_empty() {
307 let vpc = resolved_vpc
308 .clone()
309 .unwrap_or_else(|| crate::defaults::default_vpc_id(&req.account_id));
310 if let Some(sg) = state
311 .security_groups
312 .values()
313 .find(|g| g.vpc_id == vpc && g.group_name == "default")
314 {
315 sg_ids = vec![sg.group_id.clone()];
316 }
317 }
318 let instance_network = subnet_id
321 .as_ref()
322 .map(|sid| crate::runtime::InstanceNetwork {
323 subnet_id: sid.clone(),
324 internal: !crate::defaults::subnet_is_public(state, sid),
325 });
326 let (vpc, auto_public) = match subnet_id.as_ref() {
327 Some(sid) => state
328 .subnets
329 .get(sid)
330 .map(|s| {
331 (
332 Some(s.vpc_id.clone()),
333 s.map_public_ip_on_launch || s.default_for_az,
334 )
335 })
336 .unwrap_or((None, false)),
337 None => (Some(crate::defaults::default_vpc_id(&req.account_id)), true),
340 };
341 let ip_prefix = subnet_id
347 .as_ref()
348 .and_then(|sid| state.subnets.get(sid))
349 .map(|s| subnet_ip_prefix(&s.cidr_block))
350 .unwrap_or_else(|| "10.0.0".to_string());
351 (vpc, auto_public, instance_network, ip_prefix)
352 };
353 let assign_public = assoc_public.unwrap_or(subnet_auto_public);
354
355 let ids: Vec<String> = (0..count).map(|_| gen_id("i")).collect();
363 let mut rendered = Vec::new();
364 {
365 let mut accounts = svc.state.write();
366 let state = accounts.get_or_create(&req.account_id);
367 let sg_names = sg_name_map(state);
368 for (idx, id) in ids.iter().enumerate() {
369 let inst = Instance {
370 instance_id: id.clone(),
371 image_id: image_id.clone(),
372 instance_type: instance_type.clone(),
373 state_code: 0,
374 state_name: "pending".to_string(),
375 private_ip: format!("{ip_prefix}.{}", 10 + idx),
376 public_ip: if assign_public {
377 Some(format!("52.0.0.{}", 10 + idx))
378 } else {
379 None
380 },
381 subnet_id: subnet_id.clone(),
382 vpc_id: vpc_id.clone(),
383 key_name: key_name.clone(),
384 security_group_ids: sg_ids.clone(),
385 reservation_id: reservation_id.clone(),
386 ami_launch_index: idx as i64,
387 monitoring: false,
388 az: az.clone(),
389 launch_time: LAUNCH_TIME.to_string(),
390 container_id: None,
391 disable_api_termination: false,
392 disable_api_stop: false,
393 source_dest_check: true,
394 ebs_optimized: false,
395 instance_initiated_shutdown_behavior: req
396 .query_params
397 .get("InstanceInitiatedShutdownBehavior")
398 .cloned()
399 .unwrap_or_else(|| "stop".to_string()),
400 user_data: user_data.clone().filter(|s| !s.is_empty()),
401 metadata_options: crate::state::MetadataOptions::default(),
402 cpu_options: None,
403 bandwidth_weighting: None,
404 maintenance_options: crate::state::MaintenanceOptions::default(),
405 placement_tenancy: None,
406 placement_affinity: None,
407 placement_group_name: req.query_params.get("Placement.GroupName").cloned(),
408 private_dns_hostname_type: None,
409 enable_resource_name_dns_a_record: false,
410 enable_resource_name_dns_aaaa_record: false,
411 };
412 crate::service::tags::apply_tag_specifications(
413 state,
414 &req.query_params,
415 id,
416 "instance",
417 );
418 let tags = state.tags_for(id).to_vec();
419 let architecture = arch_for(state, &inst.image_id);
420 rendered.push(instance_xml(&inst, &tags, &owner, &sg_names, &architecture));
421 state.instances.insert(id.clone(), inst);
422 }
423 }
424
425 {
427 let svc_state = svc.state.clone();
428 let runtime = svc.runtime.clone();
429 let account_id = req.account_id.clone();
430 let ids = ids.clone();
431 let instance_network = instance_network.clone();
432 tokio::spawn(async move {
433 for id in &ids {
434 let running = if let Some(rt) = &runtime {
435 match rt
436 .run_instance(
437 &account_id,
438 id,
439 user_data.as_deref(),
440 &instance_tags,
441 instance_network.as_ref(),
442 )
443 .await
444 {
445 Ok(r) => Some(r),
446 Err(e) => {
447 tracing::warn!(instance_id = %id, error = %e, "EC2 instance container failed to start; serving metadata-only");
448 None
449 }
450 }
451 } else {
452 None
453 };
454 reconcile_started(&svc_state, &account_id, id, running);
455 }
456 if let Some(rt) = &runtime {
460 if rt.network_isolation_enforced() {
461 super::firewall_model::reconcile(&svc_state, rt).await;
462 }
463 }
464 });
465 }
466
467 let body = reservation_xml(&reservation_id, &owner, &rendered);
468 Ok(Ec2Service::respond("RunInstances", &req.request_id, &body))
469}
470
471fn reconcile_started(
476 state: &crate::state::SharedEc2State,
477 account_id: &str,
478 id: &str,
479 running: Option<crate::runtime::RunningInstance>,
480) {
481 let mut accounts = state.write();
482 let Some(s) = accounts.get_mut(account_id) else {
483 return;
484 };
485 let Some(inst) = s.instances.get_mut(id) else {
486 return;
487 };
488 if inst.state_code == 48 || inst.state_code == 80 {
490 return;
491 }
492 inst.state_code = 16;
493 inst.state_name = "running".to_string();
494 if let Some(r) = running {
495 inst.private_ip = r.private_ip;
496 inst.container_id = Some(r.container_id);
497 }
498}
499
500#[derive(Debug, Clone, Default)]
502pub struct CfnInstanceSpec {
503 pub image_id: Option<String>,
504 pub instance_type: Option<String>,
505 pub subnet_id: Option<String>,
506 pub availability_zone: Option<String>,
507 pub security_group_ids: Vec<String>,
508 pub key_name: Option<String>,
509 pub user_data: Option<String>,
510 pub private_ip: Option<String>,
511}
512
513#[derive(Debug, Clone)]
515pub struct CfnInstanceAttrs {
516 pub instance_id: String,
517 pub private_ip: String,
518 pub public_ip: Option<String>,
519 pub availability_zone: String,
520}
521
522pub(crate) fn cfn_create_instance(
529 svc: &Ec2Service,
530 account_id: &str,
531 region: &str,
532 spec: &CfnInstanceSpec,
533) -> CfnInstanceAttrs {
534 let image_id = spec
535 .image_id
536 .clone()
537 .unwrap_or_else(|| "ami-00000000000000000".to_string());
538 let instance_type = spec
539 .instance_type
540 .clone()
541 .unwrap_or_else(|| "t3.micro".to_string());
542 let region = if region.is_empty() {
543 "us-east-1"
544 } else {
545 region
546 };
547 let mut subnet_id = spec.subnet_id.clone();
548 let mut sg_ids = spec.security_group_ids.clone();
549
550 let (vpc_id, subnet_auto_public, ip_prefix, az) = {
551 let accounts = svc.state.read();
552 let empty = Ec2State::new(account_id, region);
553 let state = accounts.get(account_id).unwrap_or(&empty);
554 if subnet_id.is_none() {
557 let want_az = spec.availability_zone.clone();
558 if let Some(s) = state
559 .subnets
560 .values()
561 .filter(|s| s.default_for_az)
562 .find(|s| want_az.as_deref().is_none_or(|a| s.availability_zone == a))
563 .or_else(|| state.subnets.values().find(|s| s.default_for_az))
564 {
565 subnet_id = Some(s.subnet_id.clone());
566 }
567 }
568 let resolved_vpc = subnet_id
569 .as_ref()
570 .and_then(|sid| state.subnets.get(sid))
571 .map(|s| s.vpc_id.clone());
572 if sg_ids.is_empty() {
573 let vpc = resolved_vpc
574 .clone()
575 .unwrap_or_else(|| crate::defaults::default_vpc_id(account_id));
576 if let Some(sg) = state
577 .security_groups
578 .values()
579 .find(|g| g.vpc_id == vpc && g.group_name == "default")
580 {
581 sg_ids = vec![sg.group_id.clone()];
582 }
583 }
584 let (vpc, auto_public, az) = match subnet_id.as_ref() {
585 Some(sid) => state
586 .subnets
587 .get(sid)
588 .map(|s| {
589 (
590 Some(s.vpc_id.clone()),
591 s.map_public_ip_on_launch || s.default_for_az,
592 s.availability_zone.clone(),
593 )
594 })
595 .unwrap_or((None, false, format!("{region}a"))),
596 None => (
597 Some(crate::defaults::default_vpc_id(account_id)),
598 true,
599 format!("{region}a"),
600 ),
601 };
602 let az = spec.availability_zone.clone().unwrap_or(az);
603 let ip_prefix = subnet_id
604 .as_ref()
605 .and_then(|sid| state.subnets.get(sid))
606 .map(|s| subnet_ip_prefix(&s.cidr_block))
607 .unwrap_or_else(|| "10.0.0".to_string());
608 (vpc, auto_public, ip_prefix, az)
609 };
610
611 let assign_public = subnet_auto_public;
612 let id = gen_id("i");
613 let private_ip = spec
614 .private_ip
615 .clone()
616 .unwrap_or_else(|| format!("{ip_prefix}.10"));
617 let public_ip = if assign_public {
618 Some("52.0.0.10".to_string())
619 } else {
620 None
621 };
622
623 {
624 let mut accounts = svc.state.write();
625 let state = accounts.get_or_create(account_id);
626 let inst = Instance {
627 instance_id: id.clone(),
628 image_id,
629 instance_type,
630 state_code: 0,
631 state_name: "pending".to_string(),
632 private_ip: private_ip.clone(),
633 public_ip: public_ip.clone(),
634 subnet_id: subnet_id.clone(),
635 vpc_id: vpc_id.clone(),
636 key_name: spec.key_name.clone(),
637 security_group_ids: sg_ids,
638 reservation_id: gen_id("r"),
639 ami_launch_index: 0,
640 monitoring: false,
641 az: az.clone(),
642 launch_time: LAUNCH_TIME.to_string(),
643 container_id: None,
644 disable_api_termination: false,
645 disable_api_stop: false,
646 source_dest_check: true,
647 ebs_optimized: false,
648 instance_initiated_shutdown_behavior: "stop".to_string(),
649 user_data: spec.user_data.clone().filter(|s| !s.is_empty()),
650 metadata_options: crate::state::MetadataOptions::default(),
651 cpu_options: None,
652 bandwidth_weighting: None,
653 maintenance_options: crate::state::MaintenanceOptions::default(),
654 placement_tenancy: None,
655 placement_affinity: None,
656 placement_group_name: None,
657 private_dns_hostname_type: None,
658 enable_resource_name_dns_a_record: false,
659 enable_resource_name_dns_aaaa_record: false,
660 };
661 state.instances.insert(id.clone(), inst);
662 }
663
664 CfnInstanceAttrs {
665 instance_id: id,
666 private_ip,
667 public_ip,
668 availability_zone: az,
669 }
670}
671
672pub(crate) async fn cfn_boot_instance(svc: &Ec2Service, account_id: &str, id: &str) {
678 let (user_data, instance_network) = {
679 let accounts = svc.state.read();
680 let Some(state) = accounts.get(account_id) else {
681 return;
682 };
683 let Some(inst) = state.instances.get(id) else {
684 return;
685 };
686 let network = inst
687 .subnet_id
688 .as_ref()
689 .map(|sid| crate::runtime::InstanceNetwork {
690 subnet_id: sid.clone(),
691 internal: !crate::defaults::subnet_is_public(state, sid),
692 });
693 (inst.user_data.clone(), network)
694 };
695
696 let empty_tags = std::collections::BTreeMap::new();
697 let running = if let Some(rt) = &svc.runtime {
698 match rt
699 .run_instance(
700 account_id,
701 id,
702 user_data.as_deref(),
703 &empty_tags,
704 instance_network.as_ref(),
705 )
706 .await
707 {
708 Ok(r) => Some(r),
709 Err(e) => {
710 tracing::warn!(instance_id = %id, error = %e, "CFN EC2 instance container failed to start; serving metadata-only");
711 None
712 }
713 }
714 } else {
715 None
716 };
717 reconcile_started(&svc.state, account_id, id, running);
718
719 if let Some(rt) = &svc.runtime {
720 if rt.network_isolation_enforced() {
721 super::firewall_model::reconcile(&svc.state, rt).await;
722 }
723 }
724}
725
726pub(crate) async fn cfn_terminate_instance(
731 svc: &Ec2Service,
732 account_id: &str,
733 region: &str,
734 instance_id: &str,
735) {
736 let mut query_params = std::collections::HashMap::new();
737 query_params.insert("InstanceId.1".to_string(), instance_id.to_string());
738 let req = AwsRequest {
739 service: "ec2".to_string(),
740 action: "TerminateInstances".to_string(),
741 region: region.to_string(),
742 account_id: account_id.to_string(),
743 request_id: gen_id("req"),
744 headers: http::HeaderMap::new(),
745 query_params,
746 body: bytes::Bytes::new(),
747 body_stream: parking_lot::Mutex::new(None),
748 path_segments: Vec::new(),
749 raw_path: "/".to_string(),
750 raw_query: String::new(),
751 method: http::Method::POST,
752 is_query_protocol: true,
753 access_key_id: None,
754 principal: None,
755 };
756 let _ = terminate_instances(svc, &req).await;
757}
758
759fn validate_enum_instance_type(req: &AwsRequest) -> Result<(), AwsServiceError> {
760 if let Some(v) = req
763 .query_params
764 .get("InstanceType")
765 .filter(|v| !v.is_empty())
766 {
767 if !v.contains('.') {
768 return Err(invalid_parameter_value(format!(
769 "Invalid instance type '{v}'"
770 )));
771 }
772 }
773 Ok(())
774}
775
776fn transition_allowed(current: i64, new_code: i64) -> bool {
781 if current == 48 {
782 return new_code == 48;
784 }
785 true
786}
787
788async fn change_state(
789 svc: &Ec2Service,
790 req: &AwsRequest,
791 action: &str,
792 new_code: i64,
793 new_name: &str,
794) -> Result<AwsResponse, AwsServiceError> {
795 let ids = indexed_list(&req.query_params, "InstanceId");
796
797 {
801 let accounts = svc.state.read();
802 let empty = Ec2State::new(&req.account_id, &req.region);
803 let state = accounts.get(&req.account_id).unwrap_or(&empty);
804 for id in &ids {
805 let inst = state
806 .instances
807 .get(id)
808 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
809 if !transition_allowed(inst.state_code, new_code) {
810 return Err(crate::service_helpers::incorrect_instance_state(
811 id,
812 &inst.state_name,
813 ));
814 }
815 if new_code == 48 && inst.disable_api_termination {
817 return Err(AwsServiceError::aws_error(
818 http::StatusCode::BAD_REQUEST,
819 "OperationNotPermitted",
820 format!(
821 "The instance '{id}' may not be terminated. Modify its 'disableApiTermination' instance attribute and try again."
822 ),
823 ));
824 }
825 if new_code == 80 && inst.disable_api_stop {
826 return Err(AwsServiceError::aws_error(
827 http::StatusCode::BAD_REQUEST,
828 "OperationNotPermitted",
829 format!(
830 "The instance '{id}' may not be stopped. Modify its 'disableApiStop' instance attribute and try again."
831 ),
832 ));
833 }
834 }
835 }
836
837 let applied_code = if new_code == 16 { 0 } else { new_code };
843 let applied_name = if new_code == 16 { "pending" } else { new_name };
844
845 let mut changes = Vec::new();
846 let mut affected: Vec<String> = Vec::new();
847 {
848 let mut accounts = svc.state.write();
849 let state = accounts.get_or_create(&req.account_id);
850 for id in &ids {
851 let (prev_code, prev_name) = state
852 .instances
853 .get(id)
854 .map(|i| (i.state_code, i.state_name.clone()))
855 .unwrap_or((16, "running".to_string()));
856 if let Some(inst) = state.instances.get_mut(id) {
857 inst.state_code = applied_code;
858 inst.state_name = applied_name.to_string();
859 if new_code == 80 || new_code == 48 {
860 inst.public_ip = None;
861 }
862 affected.push(id.clone());
863 if new_code == 48 {
865 inst.container_id = None;
866 }
867 }
868 changes.push(format!(
869 "{}{}{}",
870 ec2_elem("instanceId", id),
871 state_xml("currentState", applied_code, applied_name),
872 state_xml("previousState", prev_code, &prev_name),
873 ));
874 }
875 }
876
877 {
881 let svc_state = svc.state.clone();
882 let runtime = svc.runtime.clone();
883 let account_id = req.account_id.clone();
884 tokio::spawn(async move {
885 for id in &affected {
886 match new_code {
887 16 => {
888 let running = match &runtime {
889 Some(rt) => rt.start_instance(id).await,
890 None => None,
891 };
892 reconcile_started(&svc_state, &account_id, id, running);
893 }
894 80 => {
895 if let Some(rt) = &runtime {
896 rt.stop_instance(id).await;
897 }
898 }
899 48 => {
900 if let Some(rt) = &runtime {
901 rt.terminate_instance(id).await;
902 }
903 }
904 _ => {}
905 }
906 }
907 if let Some(rt) = &runtime {
910 if rt.network_isolation_enforced() {
911 super::firewall_model::reconcile(&svc_state, rt).await;
912 }
913 }
914 });
915 }
916
917 Ok(Ec2Service::respond(
918 action,
919 &req.request_id,
920 &ec2_list("instancesSet", &changes),
921 ))
922}
923
924pub(crate) async fn start_instances(
925 svc: &Ec2Service,
926 req: &AwsRequest,
927) -> Result<AwsResponse, AwsServiceError> {
928 change_state(svc, req, "StartInstances", 16, "running").await
929}
930pub(crate) async fn stop_instances(
931 svc: &Ec2Service,
932 req: &AwsRequest,
933) -> Result<AwsResponse, AwsServiceError> {
934 change_state(svc, req, "StopInstances", 80, "stopped").await
935}
936pub(crate) async fn terminate_instances(
937 svc: &Ec2Service,
938 req: &AwsRequest,
939) -> Result<AwsResponse, AwsServiceError> {
940 change_state(svc, req, "TerminateInstances", 48, "terminated").await
941}
942
943pub(crate) async fn reboot_instances(
944 svc: &Ec2Service,
945 req: &AwsRequest,
946) -> Result<AwsResponse, AwsServiceError> {
947 let ids = indexed_list(&req.query_params, "InstanceId");
948 let backed: Vec<String> = {
951 let accounts = svc.state.read();
952 let empty = Ec2State::new(&req.account_id, &req.region);
953 let state = accounts.get(&req.account_id).unwrap_or(&empty);
954 let mut backed = Vec::new();
955 for id in &ids {
956 let inst = state
957 .instances
958 .get(id)
959 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
960 if inst.state_code == 48 {
961 return Err(crate::service_helpers::incorrect_instance_state(
962 id,
963 &inst.state_name,
964 ));
965 }
966 if inst.container_id.is_some() {
967 backed.push(id.clone());
968 }
969 }
970 backed
971 };
972 {
975 let svc_state = svc.state.clone();
976 let runtime = svc.runtime.clone();
977 let account_id = req.account_id.clone();
978 tokio::spawn(async move {
979 let Some(rt) = runtime else {
980 return;
981 };
982 for id in &backed {
983 if let Some(running) = rt.reboot_instance(id).await {
986 let mut accounts = svc_state.write();
987 if let Some(state) = accounts.get_mut(&account_id) {
988 if let Some(inst) = state.instances.get_mut(id) {
989 if inst.state_code != 48 {
992 inst.private_ip = running.private_ip;
993 inst.container_id = Some(running.container_id);
994 }
995 }
996 }
997 }
998 }
999 if rt.network_isolation_enforced() {
1004 super::firewall_model::reconcile(&svc_state, &rt).await;
1005 }
1006 });
1007 }
1008 Ok(Ec2Service::respond(
1009 "RebootInstances",
1010 &req.request_id,
1011 &ec2_return(true),
1012 ))
1013}
1014
1015pub(crate) fn monitor_instances(
1016 svc: &Ec2Service,
1017 req: &AwsRequest,
1018) -> Result<AwsResponse, AwsServiceError> {
1019 monitor(svc, req, "MonitorInstances", true)
1020}
1021pub(crate) fn unmonitor_instances(
1022 svc: &Ec2Service,
1023 req: &AwsRequest,
1024) -> Result<AwsResponse, AwsServiceError> {
1025 monitor(svc, req, "UnmonitorInstances", false)
1026}
1027
1028fn monitor(
1029 svc: &Ec2Service,
1030 req: &AwsRequest,
1031 action: &str,
1032 enable: bool,
1033) -> Result<AwsResponse, AwsServiceError> {
1034 let ids = indexed_list(&req.query_params, "InstanceId");
1035 {
1036 let mut accounts = svc.state.write();
1037 let state = accounts.get_or_create(&req.account_id);
1038 for id in &ids {
1039 if let Some(i) = state.instances.get_mut(id) {
1040 i.monitoring = enable;
1041 }
1042 }
1043 }
1044 let items: Vec<String> = ids
1045 .iter()
1046 .map(|id| {
1047 format!(
1048 "{}<monitoring><state>{}</state></monitoring>",
1049 ec2_elem("instanceId", id),
1050 if enable { "pending" } else { "disabling" }
1051 )
1052 })
1053 .collect();
1054 Ok(Ec2Service::respond(
1055 action,
1056 &req.request_id,
1057 &ec2_list("instancesSet", &items),
1058 ))
1059}
1060
1061pub(crate) fn describe_instances(
1062 svc: &Ec2Service,
1063 req: &AwsRequest,
1064) -> Result<AwsResponse, AwsServiceError> {
1065 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1066 let max_results = parse_max_results(&req.query_params);
1067 let next_token = req.query_params.get("NextToken").map(String::as_str);
1068 let filters = parse_filters(&req.query_params);
1069 let wanted = indexed_list(&req.query_params, "InstanceId");
1070 let owner = req.account_id.clone();
1071 let accounts = svc.state.read();
1072 let empty = Ec2State::new(&req.account_id, &req.region);
1073 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1074
1075 let mut matching: Vec<&Instance> = state
1079 .instances
1080 .values()
1081 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1082 .filter(|i| {
1083 inst_match(
1084 i,
1085 state.tags_for(&i.instance_id),
1086 &filters,
1087 &arch_for(state, &i.image_id),
1088 )
1089 })
1090 .collect();
1091 matching.sort_by(|a, b| {
1092 a.reservation_id
1093 .cmp(&b.reservation_id)
1094 .then(a.instance_id.cmp(&b.instance_id))
1095 });
1096 let (page, token) = crate::service_helpers::paginate(&matching, next_token, max_results);
1097
1098 let sg_names = sg_name_map(state);
1100 let mut by_res: HashMap<String, Vec<String>> = HashMap::new();
1101 let mut order: Vec<String> = Vec::new();
1102 for i in page {
1103 if !by_res.contains_key(&i.reservation_id) {
1104 order.push(i.reservation_id.clone());
1105 }
1106 by_res
1107 .entry(i.reservation_id.clone())
1108 .or_default()
1109 .push(instance_xml(
1110 i,
1111 state.tags_for(&i.instance_id),
1112 &owner,
1113 &sg_names,
1114 &arch_for(state, &i.image_id),
1115 ));
1116 }
1117 let reservations: Vec<String> = order
1118 .iter()
1119 .map(|rid| {
1120 let insts = by_res.remove(rid).unwrap_or_default();
1121 reservation_xml(rid, &owner, &insts)
1122 })
1123 .collect();
1124 let body = format!(
1125 "{}{}",
1126 ec2_list("reservationSet", &reservations),
1127 token.map(|t| ec2_elem("nextToken", &t)).unwrap_or_default(),
1128 );
1129 Ok(Ec2Service::respond(
1130 "DescribeInstances",
1131 &req.request_id,
1132 &body,
1133 ))
1134}
1135
1136fn parse_max_results(params: &HashMap<String, String>) -> Option<usize> {
1139 params
1140 .get("MaxResults")
1141 .filter(|v| !v.is_empty())
1142 .and_then(|v| v.parse::<usize>().ok())
1143}
1144
1145fn inst_match(i: &Instance, tags: &[Tag], filters: &[Filter], architecture: &str) -> bool {
1146 use crate::service_helpers::filter_value_matches;
1147 filters.iter().all(|f| {
1148 let candidates: Vec<String> = match f.name.as_str() {
1149 "instance-id" => vec![i.instance_id.clone()],
1150 "instance-type" => vec![i.instance_type.clone()],
1151 "image-id" => vec![i.image_id.clone()],
1152 "instance-state-name" => vec![i.state_name.clone()],
1153 "instance-state-code" => vec![i.state_code.to_string()],
1154 "vpc-id" => i.vpc_id.clone().into_iter().collect(),
1155 "subnet-id" => i.subnet_id.clone().into_iter().collect(),
1156 "availability-zone" => vec![i.az.clone()],
1157 "private-ip-address" => vec![i.private_ip.clone()],
1158 "ip-address" => i.public_ip.clone().into_iter().collect(),
1159 "key-name" => i.key_name.clone().into_iter().collect(),
1160 "architecture" => vec![architecture.to_string()],
1161 "tag-key" => tags.iter().map(|t| t.key.clone()).collect(),
1162 name => {
1163 if let Some(key) = name.strip_prefix("tag:") {
1164 tags.iter()
1165 .filter(|t| t.key == key)
1166 .map(|t| t.value.clone())
1167 .collect()
1168 } else {
1169 return false;
1173 }
1174 }
1175 };
1176 f.values
1177 .iter()
1178 .any(|v| candidates.iter().any(|c| filter_value_matches(v, c)))
1179 })
1180}
1181
1182pub(crate) fn describe_instance_status(
1183 svc: &Ec2Service,
1184 req: &AwsRequest,
1185) -> Result<AwsResponse, AwsServiceError> {
1186 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1187 let max_results = parse_max_results(&req.query_params);
1188 let next_token = req.query_params.get("NextToken").map(String::as_str);
1189 let filters = parse_filters(&req.query_params);
1190 let wanted = indexed_list(&req.query_params, "InstanceId");
1191 let include_all = req
1192 .query_params
1193 .get("IncludeAllInstances")
1194 .map(|v| v == "true")
1195 .unwrap_or(false);
1196 let accounts = svc.state.read();
1197 let empty = Ec2State::new(&req.account_id, &req.region);
1198 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1199 let mut matching: Vec<&Instance> = state
1200 .instances
1201 .values()
1202 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1203 .filter(|i| include_all || i.state_name == "running")
1204 .filter(|i| {
1205 inst_match(
1206 i,
1207 state.tags_for(&i.instance_id),
1208 &filters,
1209 &arch_for(state, &i.image_id),
1210 )
1211 })
1212 .collect();
1213 matching.sort_by(|a, b| a.instance_id.cmp(&b.instance_id));
1214 let (page, token) = crate::service_helpers::paginate(&matching, next_token, max_results);
1215 let items: Vec<String> = page
1216 .iter()
1217 .map(|i| {
1218 format!(
1219 "{}{}{}{}{}{}",
1220 ec2_elem("instanceId", &i.instance_id),
1221 ec2_elem("availabilityZone", &i.az),
1222 state_xml("instanceState", i.state_code, &i.state_name),
1223 "<instanceStatus><status>ok</status></instanceStatus>",
1224 "<systemStatus><status>ok</status></systemStatus>",
1225 ec2_list("eventsSet", &[]),
1226 )
1227 })
1228 .collect();
1229 let body = format!(
1230 "{}{}",
1231 ec2_list("instanceStatusSet", &items),
1232 token.map(|t| ec2_elem("nextToken", &t)).unwrap_or_default(),
1233 );
1234 Ok(Ec2Service::respond(
1235 "DescribeInstanceStatus",
1236 &req.request_id,
1237 &body,
1238 ))
1239}
1240
1241fn instance_type_items(req: &AwsRequest) -> Vec<String> {
1242 let wanted = indexed_list(&req.query_params, "InstanceType");
1243 INSTANCE_TYPES
1244 .iter()
1245 .filter(|t| wanted.is_empty() || wanted.iter().any(|w| w == *t))
1246 .map(|t| {
1247 format!(
1248 "{}<currentGeneration>true</currentGeneration><bareMetal>false</bareMetal>\
1249 <hypervisor>nitro</hypervisor><instanceStorageSupported>false</instanceStorageSupported>\
1250 <processorInfo><supportedArchitectures><item>x86_64</item></supportedArchitectures></processorInfo>\
1251 <vCpuInfo><defaultVCpus>2</defaultVCpus></vCpuInfo>\
1252 <memoryInfo><sizeInMiB>1024</sizeInMiB></memoryInfo>\
1253 <supportedVirtualizationTypes><item>hvm</item></supportedVirtualizationTypes>",
1254 ec2_elem("instanceType", t),
1255 )
1256 })
1257 .collect()
1258}
1259
1260pub(crate) fn describe_instance_types(
1261 _svc: &Ec2Service,
1262 req: &AwsRequest,
1263) -> Result<AwsResponse, AwsServiceError> {
1264 crate::service_helpers::validate_max_results(&req.query_params, 5, 100)?;
1265 Ok(Ec2Service::respond(
1266 "DescribeInstanceTypes",
1267 &req.request_id,
1268 &ec2_list("instanceTypeSet", &instance_type_items(req)),
1269 ))
1270}
1271
1272pub(crate) fn get_instance_types_from_requirements(
1273 _svc: &Ec2Service,
1274 req: &AwsRequest,
1275) -> Result<AwsResponse, AwsServiceError> {
1276 require_struct(&req.query_params, "InstanceRequirements")?;
1277 let items: Vec<String> = INSTANCE_TYPES
1278 .iter()
1279 .map(|t| format!("<instanceType>{t}</instanceType>"))
1280 .collect();
1281 Ok(Ec2Service::respond(
1282 "GetInstanceTypesFromInstanceRequirements",
1283 &req.request_id,
1284 &ec2_list("instanceTypeSet", &items),
1285 ))
1286}
1287
1288pub(crate) fn describe_instance_attribute(
1291 svc: &Ec2Service,
1292 req: &AwsRequest,
1293) -> Result<AwsResponse, AwsServiceError> {
1294 let id = require(&req.query_params, "InstanceId")?;
1295 let attribute = require(&req.query_params, "Attribute")?;
1296 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1297 let accounts = svc.state.read();
1298 let acct_state = accounts.get(&req.account_id);
1299 let inst = acct_state
1300 .and_then(|s| s.instances.get(&id))
1301 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1302 let attr_xml = match attribute.as_str() {
1303 "instanceType" => format!(
1304 "<instanceType><value>{}</value></instanceType>",
1305 inst.instance_type
1306 ),
1307 "disableApiTermination" => format!(
1308 "<disableApiTermination><value>{}</value></disableApiTermination>",
1309 inst.disable_api_termination
1310 ),
1311 "disableApiStop" => format!(
1312 "<disableApiStop><value>{}</value></disableApiStop>",
1313 inst.disable_api_stop
1314 ),
1315 "ebsOptimized" => format!(
1316 "<ebsOptimized><value>{}</value></ebsOptimized>",
1317 inst.ebs_optimized
1318 ),
1319 "sourceDestCheck" => format!(
1320 "<sourceDestCheck><value>{}</value></sourceDestCheck>",
1321 inst.source_dest_check
1322 ),
1323 "instanceInitiatedShutdownBehavior" => format!(
1324 "<instanceInitiatedShutdownBehavior><value>{}</value></instanceInitiatedShutdownBehavior>",
1325 inst.instance_initiated_shutdown_behavior
1326 ),
1327 "userData" => match &inst.user_data {
1328 Some(d) => format!("<userData><value>{d}</value></userData>"),
1329 None => "<userData/>".to_string(),
1330 },
1331 "groupSet" => {
1332 let sg_names = acct_state.map(sg_name_map).unwrap_or_default();
1333 let groups: Vec<String> = inst
1334 .security_group_ids
1335 .iter()
1336 .map(|g| {
1337 let name = sg_names.get(g).map(String::as_str).unwrap_or(g.as_str());
1338 format!("{}{}", ec2_elem("groupId", g), ec2_elem("groupName", name))
1339 })
1340 .collect();
1341 ec2_list("groupSet", &groups)
1342 }
1343 _ => String::new(),
1344 };
1345 let body = format!("{}{}", ec2_elem("instanceId", &id), attr_xml);
1346 Ok(Ec2Service::respond(
1347 "DescribeInstanceAttribute",
1348 &req.request_id,
1349 &body,
1350 ))
1351}
1352
1353const ATTRIBUTE_VALUES: &[&str] = &[
1354 "instanceType",
1355 "kernel",
1356 "ramdisk",
1357 "userData",
1358 "disableApiTermination",
1359 "instanceInitiatedShutdownBehavior",
1360 "rootDeviceName",
1361 "blockDeviceMapping",
1362 "productCodes",
1363 "sourceDestCheck",
1364 "groupSet",
1365 "ebsOptimized",
1366 "sriovNetSupport",
1367 "enaSupport",
1368 "enclaveOptions",
1369 "disableApiStop",
1370];
1371
1372fn attr_bool(params: &HashMap<String, String>, key: &str) -> Option<bool> {
1376 params
1377 .get(&format!("{key}.Value"))
1378 .or_else(|| params.get(key))
1379 .map(|v| v == "true")
1380}
1381
1382fn attr_str<'a>(params: &'a HashMap<String, String>, key: &str) -> Option<&'a String> {
1383 params
1384 .get(&format!("{key}.Value"))
1385 .or_else(|| params.get(key))
1386}
1387
1388pub(crate) fn modify_instance_attribute(
1389 svc: &Ec2Service,
1390 req: &AwsRequest,
1391) -> Result<AwsResponse, AwsServiceError> {
1392 let id = require(&req.query_params, "InstanceId")?;
1393 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1397 let p = &req.query_params;
1398 let mut accounts = svc.state.write();
1399 let state = accounts.get_or_create(&req.account_id);
1400 let inst = state
1401 .instances
1402 .get_mut(&id)
1403 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1404
1405 if let Some(attr) = p.get("Attribute").filter(|v| !v.is_empty()) {
1407 let value = p.get("Value").cloned();
1408 match attr.as_str() {
1409 "instanceType" => {
1410 if let Some(v) = value {
1411 inst.instance_type = v;
1412 }
1413 }
1414 "userData" => inst.user_data = value.filter(|s| !s.is_empty()),
1415 "disableApiTermination" => {
1416 inst.disable_api_termination = value.as_deref() == Some("true")
1417 }
1418 "disableApiStop" => inst.disable_api_stop = value.as_deref() == Some("true"),
1419 "sourceDestCheck" => inst.source_dest_check = value.as_deref() == Some("true"),
1420 "ebsOptimized" => inst.ebs_optimized = value.as_deref() == Some("true"),
1421 "instanceInitiatedShutdownBehavior" => {
1422 if let Some(v) = value {
1423 inst.instance_initiated_shutdown_behavior = v;
1424 }
1425 }
1426 _ => {}
1427 }
1428 }
1429 if let Some(v) = attr_bool(p, "DisableApiTermination") {
1431 inst.disable_api_termination = v;
1432 }
1433 if let Some(v) = attr_bool(p, "DisableApiStop") {
1434 inst.disable_api_stop = v;
1435 }
1436 if let Some(v) = attr_bool(p, "SourceDestCheck") {
1437 inst.source_dest_check = v;
1438 }
1439 if let Some(v) = attr_bool(p, "EbsOptimized") {
1440 inst.ebs_optimized = v;
1441 }
1442 if let Some(v) = attr_str(p, "InstanceType") {
1443 inst.instance_type = v.clone();
1444 }
1445 if let Some(v) = attr_str(p, "InstanceInitiatedShutdownBehavior") {
1446 inst.instance_initiated_shutdown_behavior = v.clone();
1447 }
1448 if let Some(v) = attr_str(p, "UserData") {
1449 inst.user_data = Some(v.clone()).filter(|s| !s.is_empty());
1450 }
1451 let new_groups = indexed_list(p, "GroupId");
1452 if !new_groups.is_empty() {
1453 inst.security_group_ids = new_groups;
1454 }
1455
1456 Ok(Ec2Service::respond(
1457 "ModifyInstanceAttribute",
1458 &req.request_id,
1459 &ec2_return(true),
1460 ))
1461}
1462
1463pub(crate) fn reset_instance_attribute(
1464 svc: &Ec2Service,
1465 req: &AwsRequest,
1466) -> Result<AwsResponse, AwsServiceError> {
1467 let id = require(&req.query_params, "InstanceId")?;
1468 let attribute = require(&req.query_params, "Attribute")?;
1469 validate_enum(&req.query_params, "Attribute", ATTRIBUTE_VALUES)?;
1470 let mut accounts = svc.state.write();
1471 let state = accounts.get_or_create(&req.account_id);
1472 let inst = state
1473 .instances
1474 .get_mut(&id)
1475 .ok_or_else(|| crate::service_helpers::instance_not_found(&id))?;
1476 match attribute.as_str() {
1479 "sourceDestCheck" => inst.source_dest_check = true,
1480 "disableApiTermination" => inst.disable_api_termination = false,
1481 "disableApiStop" => inst.disable_api_stop = false,
1482 "ebsOptimized" => inst.ebs_optimized = false,
1483 "userData" => inst.user_data = None,
1484 "instanceInitiatedShutdownBehavior" => {
1485 inst.instance_initiated_shutdown_behavior = "stop".to_string()
1486 }
1487 _ => {}
1488 }
1489 Ok(Ec2Service::respond(
1490 "ResetInstanceAttribute",
1491 &req.request_id,
1492 &ec2_return(true),
1493 ))
1494}
1495
1496fn with_instance_mut<R>(
1501 svc: &Ec2Service,
1502 account_id: &str,
1503 id: &str,
1504 f: impl FnOnce(&mut Instance) -> R,
1505) -> Result<R, AwsServiceError> {
1506 let mut accounts = svc.state.write();
1507 let state = accounts.get_or_create(account_id);
1508 let inst = state
1509 .instances
1510 .get_mut(id)
1511 .ok_or_else(|| crate::service_helpers::instance_not_found(id))?;
1512 Ok(f(inst))
1513}
1514
1515pub(crate) fn modify_instance_placement(
1516 svc: &Ec2Service,
1517 req: &AwsRequest,
1518) -> Result<AwsResponse, AwsServiceError> {
1519 let id = require(&req.query_params, "InstanceId")?;
1520 validate_enum(
1521 &req.query_params,
1522 "Tenancy",
1523 &["default", "dedicated", "host"],
1524 )?;
1525 validate_enum(&req.query_params, "Affinity", &["default", "host"])?;
1526 let p = req.query_params.clone();
1527 with_instance_mut(svc, &req.account_id, &id, |inst| {
1528 if let Some(t) = p.get("Tenancy").filter(|v| !v.is_empty()) {
1529 inst.placement_tenancy = Some(t.clone());
1530 }
1531 if let Some(a) = p.get("Affinity").filter(|v| !v.is_empty()) {
1532 inst.placement_affinity = Some(a.clone());
1533 }
1534 if let Some(g) = p.get("GroupName") {
1535 inst.placement_group_name = Some(g.clone()).filter(|s| !s.is_empty());
1536 }
1537 })?;
1538 Ok(Ec2Service::respond(
1539 "ModifyInstancePlacement",
1540 &req.request_id,
1541 &ec2_return(true),
1542 ))
1543}
1544
1545pub(crate) fn modify_instance_metadata_options(
1546 svc: &Ec2Service,
1547 req: &AwsRequest,
1548) -> Result<AwsResponse, AwsServiceError> {
1549 let id = require(&req.query_params, "InstanceId")?;
1550 validate_enum(&req.query_params, "HttpTokens", &["optional", "required"])?;
1551 validate_enum(&req.query_params, "HttpEndpoint", &["disabled", "enabled"])?;
1552 validate_enum(
1553 &req.query_params,
1554 "HttpProtocolIpv6",
1555 &["disabled", "enabled"],
1556 )?;
1557 validate_enum(
1558 &req.query_params,
1559 "InstanceMetadataTags",
1560 &["disabled", "enabled"],
1561 )?;
1562 let p = req.query_params.clone();
1563 let opts = with_instance_mut(svc, &req.account_id, &id, |inst| {
1564 let m = &mut inst.metadata_options;
1565 if let Some(v) = p.get("HttpTokens").filter(|v| !v.is_empty()) {
1566 m.http_tokens = v.clone();
1567 }
1568 if let Some(v) = p.get("HttpEndpoint").filter(|v| !v.is_empty()) {
1569 m.http_endpoint = v.clone();
1570 }
1571 if let Some(v) = p.get("HttpProtocolIpv6").filter(|v| !v.is_empty()) {
1572 m.http_protocol_ipv6 = v.clone();
1573 }
1574 if let Some(v) = p.get("InstanceMetadataTags").filter(|v| !v.is_empty()) {
1575 m.instance_metadata_tags = v.clone();
1576 }
1577 if let Some(n) = p
1578 .get("HttpPutResponseHopLimit")
1579 .and_then(|v| v.parse::<i64>().ok())
1580 {
1581 m.http_put_response_hop_limit = n;
1582 }
1583 m.clone()
1584 })?;
1585 let body = format!(
1586 "{}<instanceMetadataOptions><state>applied</state><httpTokens>{}</httpTokens>\
1587 <httpPutResponseHopLimit>{}</httpPutResponseHopLimit><httpEndpoint>{}</httpEndpoint>\
1588 <httpProtocolIpv6>{}</httpProtocolIpv6><instanceMetadataTags>{}</instanceMetadataTags></instanceMetadataOptions>",
1589 ec2_elem("instanceId", &id),
1590 opts.http_tokens,
1591 opts.http_put_response_hop_limit,
1592 opts.http_endpoint,
1593 opts.http_protocol_ipv6,
1594 opts.instance_metadata_tags,
1595 );
1596 Ok(Ec2Service::respond(
1597 "ModifyInstanceMetadataOptions",
1598 &req.request_id,
1599 &body,
1600 ))
1601}
1602
1603pub(crate) fn modify_instance_maintenance_options(
1604 svc: &Ec2Service,
1605 req: &AwsRequest,
1606) -> Result<AwsResponse, AwsServiceError> {
1607 let id = require(&req.query_params, "InstanceId")?;
1608 validate_enum(&req.query_params, "AutoRecovery", &["disabled", "default"])?;
1609 validate_enum(
1610 &req.query_params,
1611 "RebootMigration",
1612 &["disabled", "default"],
1613 )?;
1614 let p = req.query_params.clone();
1615 let opts = with_instance_mut(svc, &req.account_id, &id, |inst| {
1616 let m = &mut inst.maintenance_options;
1617 if let Some(v) = p.get("AutoRecovery").filter(|v| !v.is_empty()) {
1618 m.auto_recovery = v.clone();
1619 }
1620 if let Some(v) = p.get("RebootMigration").filter(|v| !v.is_empty()) {
1621 m.reboot_migration = v.clone();
1622 }
1623 m.clone()
1624 })?;
1625 let body = format!(
1626 "{}<maintenanceOptions><autoRecovery>{}</autoRecovery><rebootMigration>{}</rebootMigration></maintenanceOptions>",
1627 ec2_elem("instanceId", &id),
1628 opts.auto_recovery,
1629 opts.reboot_migration,
1630 );
1631 Ok(Ec2Service::respond(
1632 "ModifyInstanceMaintenanceOptions",
1633 &req.request_id,
1634 &body,
1635 ))
1636}
1637
1638pub(crate) fn modify_instance_cpu_options(
1639 svc: &Ec2Service,
1640 req: &AwsRequest,
1641) -> Result<AwsResponse, AwsServiceError> {
1642 let id = require(&req.query_params, "InstanceId")?;
1643 validate_enum(
1644 &req.query_params,
1645 "NestedVirtualization",
1646 &["disabled", "enabled"],
1647 )?;
1648 let core_count = req
1649 .query_params
1650 .get("CoreCount")
1651 .and_then(|v| v.parse::<i64>().ok())
1652 .unwrap_or(2);
1653 let threads_per_core = req
1654 .query_params
1655 .get("ThreadsPerCore")
1656 .and_then(|v| v.parse::<i64>().ok())
1657 .unwrap_or(1);
1658 with_instance_mut(svc, &req.account_id, &id, |inst| {
1659 inst.cpu_options = Some(crate::state::CpuOptions {
1660 core_count,
1661 threads_per_core,
1662 });
1663 })?;
1664 let body = format!(
1665 "{}<coreCount>{core_count}</coreCount><threadsPerCore>{threads_per_core}</threadsPerCore>",
1666 ec2_elem("instanceId", &id)
1667 );
1668 Ok(Ec2Service::respond(
1669 "ModifyInstanceCpuOptions",
1670 &req.request_id,
1671 &body,
1672 ))
1673}
1674
1675pub(crate) fn modify_instance_network_performance_options(
1676 svc: &Ec2Service,
1677 req: &AwsRequest,
1678) -> Result<AwsResponse, AwsServiceError> {
1679 let id = require(&req.query_params, "InstanceId")?;
1680 let weighting = require(&req.query_params, "BandwidthWeighting")?;
1681 validate_enum(
1682 &req.query_params,
1683 "BandwidthWeighting",
1684 &["default", "vpc-1", "ebs-1"],
1685 )?;
1686 with_instance_mut(svc, &req.account_id, &id, |inst| {
1687 inst.bandwidth_weighting = Some(weighting.clone());
1688 })?;
1689 let body = format!(
1690 "{}<bandwidthWeighting>{weighting}</bandwidthWeighting>",
1691 ec2_elem("instanceId", &id)
1692 );
1693 Ok(Ec2Service::respond(
1694 "ModifyInstanceNetworkPerformanceOptions",
1695 &req.request_id,
1696 &body,
1697 ))
1698}
1699
1700pub(crate) fn modify_instance_event_start_time(
1701 _svc: &Ec2Service,
1702 req: &AwsRequest,
1703) -> Result<AwsResponse, AwsServiceError> {
1704 require(&req.query_params, "InstanceId")?;
1705 let event_id = require(&req.query_params, "InstanceEventId")?;
1706 require(&req.query_params, "NotBefore")?;
1707 let body = format!(
1708 "<event>{}<code>system-reboot</code><description>scheduled</description></event>",
1709 ec2_elem("instanceEventId", &event_id)
1710 );
1711 Ok(Ec2Service::respond(
1712 "ModifyInstanceEventStartTime",
1713 &req.request_id,
1714 &body,
1715 ))
1716}
1717
1718pub(crate) fn describe_instance_credit_specifications(
1719 svc: &Ec2Service,
1720 req: &AwsRequest,
1721) -> Result<AwsResponse, AwsServiceError> {
1722 crate::service_helpers::validate_max_results(&req.query_params, 5, 1000)?;
1723 let wanted = indexed_list(&req.query_params, "InstanceId");
1724 let accounts = svc.state.read();
1725 let empty = Ec2State::new(&req.account_id, &req.region);
1726 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1727 let items: Vec<String> = state
1728 .instances
1729 .values()
1730 .filter(|i| wanted.is_empty() || wanted.contains(&i.instance_id))
1731 .map(|i| {
1732 let credits = state
1736 .instance_credit_specs
1737 .get(&i.instance_id)
1738 .cloned()
1739 .unwrap_or_else(|| "standard".to_string());
1740 format!(
1741 "{}{}",
1742 ec2_elem("instanceId", &i.instance_id),
1743 ec2_elem("cpuCredits", &credits)
1744 )
1745 })
1746 .collect();
1747 Ok(Ec2Service::respond(
1748 "DescribeInstanceCreditSpecifications",
1749 &req.request_id,
1750 &ec2_list("instanceCreditSpecificationSet", &items),
1751 ))
1752}
1753
1754pub(crate) fn modify_instance_credit_specification(
1755 svc: &Ec2Service,
1756 req: &AwsRequest,
1757) -> Result<AwsResponse, AwsServiceError> {
1758 let p = &req.query_params;
1759 let mut successful = Vec::new();
1760 let mut unsuccessful = Vec::new();
1761 {
1762 let mut accounts = svc.state.write();
1763 let state = accounts.get_or_create(&req.account_id);
1764 let mut n = 1usize;
1765 loop {
1766 let id_key = format!("InstanceCreditSpecification.{n}.InstanceId");
1767 let Some(instance_id) = p.get(&id_key).cloned() else {
1768 break;
1769 };
1770 let credits = p
1771 .get(&format!("InstanceCreditSpecification.{n}.CpuCredits"))
1772 .cloned()
1773 .unwrap_or_else(|| "standard".to_string());
1774 if state.instances.contains_key(&instance_id) {
1775 state
1776 .instance_credit_specs
1777 .insert(instance_id.clone(), credits);
1778 successful.push(ec2_elem("instanceId", &instance_id));
1779 } else {
1780 unsuccessful.push(format!(
1781 "{}<error><code>InvalidInstanceID.NotFound</code><message>The instance ID '{instance_id}' does not exist</message></error>",
1782 ec2_elem("instanceId", &instance_id)
1783 ));
1784 }
1785 n += 1;
1786 }
1787 }
1788 let body = format!(
1789 "{}{}",
1790 ec2_list("successfulInstanceCreditSpecificationSet", &successful),
1791 ec2_list("unsuccessfulInstanceCreditSpecificationSet", &unsuccessful),
1792 );
1793 Ok(Ec2Service::respond(
1794 "ModifyInstanceCreditSpecification",
1795 &req.request_id,
1796 &body,
1797 ))
1798}
1799
1800pub(crate) fn get_instance_metadata_defaults(
1801 svc: &Ec2Service,
1802 req: &AwsRequest,
1803) -> Result<AwsResponse, AwsServiceError> {
1804 let accounts = svc.state.read();
1805 let d = accounts
1806 .get(&req.account_id)
1807 .and_then(|s| s.instance_metadata_defaults.clone())
1808 .unwrap_or_default();
1809 let mut inner = String::new();
1810 if let Some(v) = &d.http_tokens {
1811 inner.push_str(&ec2_elem("httpTokens", v));
1812 }
1813 if let Some(v) = &d.http_endpoint {
1814 inner.push_str(&ec2_elem("httpEndpoint", v));
1815 }
1816 if let Some(v) = d.http_put_response_hop_limit {
1817 inner.push_str(&ec2_elem("httpPutResponseHopLimit", &v.to_string()));
1818 }
1819 if let Some(v) = &d.instance_metadata_tags {
1820 inner.push_str(&ec2_elem("instanceMetadataTags", v));
1821 }
1822 if let Some(v) = &d.http_tokens_enforced {
1823 inner.push_str(&ec2_elem("httpTokensEnforced", v));
1824 }
1825 Ok(Ec2Service::respond(
1826 "GetInstanceMetadataDefaults",
1827 &req.request_id,
1828 &format!("<accountLevel>{inner}</accountLevel>"),
1829 ))
1830}
1831
1832pub(crate) fn modify_instance_metadata_defaults(
1833 svc: &Ec2Service,
1834 req: &AwsRequest,
1835) -> Result<AwsResponse, AwsServiceError> {
1836 let p = &req.query_params;
1837 validate_enum(p, "HttpTokens", &["optional", "required", "no-preference"])?;
1838 validate_enum(p, "HttpEndpoint", &["disabled", "enabled", "no-preference"])?;
1839 validate_enum(
1840 p,
1841 "InstanceMetadataTags",
1842 &["disabled", "enabled", "no-preference"],
1843 )?;
1844 validate_enum(
1845 p,
1846 "HttpTokensEnforced",
1847 &["disabled", "enabled", "no-preference"],
1848 )?;
1849 let apply = |cur: &mut Option<String>, key: &str| {
1851 if let Some(v) = p.get(key) {
1852 *cur = if v == "no-preference" {
1853 None
1854 } else {
1855 Some(v.clone())
1856 };
1857 }
1858 };
1859 {
1860 let mut accounts = svc.state.write();
1861 let state = accounts.get_or_create(&req.account_id);
1862 let d = state
1863 .instance_metadata_defaults
1864 .get_or_insert_with(Default::default);
1865 apply(&mut d.http_tokens, "HttpTokens");
1866 apply(&mut d.http_endpoint, "HttpEndpoint");
1867 apply(&mut d.instance_metadata_tags, "InstanceMetadataTags");
1868 apply(&mut d.http_tokens_enforced, "HttpTokensEnforced");
1869 if let Some(v) = p
1870 .get("HttpPutResponseHopLimit")
1871 .and_then(|v| v.parse::<i64>().ok())
1872 {
1873 d.http_put_response_hop_limit = if v < 0 { None } else { Some(v) };
1875 }
1876 }
1877 Ok(Ec2Service::respond(
1878 "ModifyInstanceMetadataDefaults",
1879 &req.request_id,
1880 &ec2_return(true),
1881 ))
1882}
1883
1884pub(crate) fn register_event_notification_attributes(
1885 svc: &Ec2Service,
1886 req: &AwsRequest,
1887) -> Result<AwsResponse, AwsServiceError> {
1888 let keys = indexed_sub_keys(&req.query_params);
1889 let include_all = req
1890 .query_params
1891 .get("InstanceTagAttribute.IncludeAllTagsOfInstance")
1892 .map(|v| v == "true");
1893 let xml = {
1894 let mut accounts = svc.state.write();
1895 let state = accounts.get_or_create(&req.account_id);
1896 for k in keys {
1897 if !state.event_notification_tag_keys.contains(&k) {
1898 state.event_notification_tag_keys.push(k);
1899 }
1900 }
1901 if let Some(v) = include_all {
1902 state.event_notification_include_all_tags = v;
1903 }
1904 event_tag_attribute(state)
1905 };
1906 Ok(Ec2Service::respond(
1907 "RegisterInstanceEventNotificationAttributes",
1908 &req.request_id,
1909 &xml,
1910 ))
1911}
1912
1913pub(crate) fn deregister_event_notification_attributes(
1914 svc: &Ec2Service,
1915 req: &AwsRequest,
1916) -> Result<AwsResponse, AwsServiceError> {
1917 let keys = indexed_sub_keys(&req.query_params);
1918 let include_all = req
1919 .query_params
1920 .get("InstanceTagAttribute.IncludeAllTagsOfInstance")
1921 .map(|v| v == "true");
1922 let xml = {
1923 let mut accounts = svc.state.write();
1924 let state = accounts.get_or_create(&req.account_id);
1925 state
1926 .event_notification_tag_keys
1927 .retain(|k| !keys.contains(k));
1928 if include_all == Some(true) {
1930 state.event_notification_include_all_tags = false;
1931 }
1932 event_tag_attribute(state)
1933 };
1934 Ok(Ec2Service::respond(
1935 "DeregisterInstanceEventNotificationAttributes",
1936 &req.request_id,
1937 &xml,
1938 ))
1939}
1940
1941pub(crate) fn describe_event_notification_attributes(
1942 svc: &Ec2Service,
1943 req: &AwsRequest,
1944) -> Result<AwsResponse, AwsServiceError> {
1945 let accounts = svc.state.read();
1946 let empty = Ec2State::new(&req.account_id, &req.region);
1947 let state = accounts.get(&req.account_id).unwrap_or(&empty);
1948 Ok(Ec2Service::respond(
1949 "DescribeInstanceEventNotificationAttributes",
1950 &req.request_id,
1951 &event_tag_attribute(state),
1952 ))
1953}
1954
1955fn indexed_sub_keys(params: &HashMap<String, String>) -> Vec<String> {
1957 let mut out = Vec::new();
1958 let mut n = 1usize;
1959 while let Some(v) = params.get(&format!("InstanceTagAttribute.InstanceTagKey.{n}")) {
1960 out.push(v.clone());
1961 n += 1;
1962 }
1963 out
1964}
1965
1966fn event_tag_attribute(state: &Ec2State) -> String {
1967 let keys: Vec<String> = state
1970 .event_notification_tag_keys
1971 .iter()
1972 .map(|k| fakecloud_aws::xml::xml_escape(k))
1973 .collect();
1974 format!(
1975 "<instanceTagAttribute><includeAllTagsOfInstance>{}</includeAllTagsOfInstance>{}</instanceTagAttribute>",
1976 state.event_notification_include_all_tags,
1977 ec2_list("instanceTagKeySet", &keys)
1978 )
1979}
1980
1981pub(crate) fn report_instance_status(
1982 _svc: &Ec2Service,
1983 req: &AwsRequest,
1984) -> Result<AwsResponse, AwsServiceError> {
1985 require(&req.query_params, "Status")?;
1986 validate_enum(&req.query_params, "Status", &["ok", "impaired"])?;
1987 Ok(Ec2Service::respond(
1988 "ReportInstanceStatus",
1989 &req.request_id,
1990 &ec2_return(true),
1991 ))
1992}
1993
1994pub(crate) fn describe_instance_topology(
1995 _svc: &Ec2Service,
1996 req: &AwsRequest,
1997) -> Result<AwsResponse, AwsServiceError> {
1998 crate::service_helpers::validate_max_results(&req.query_params, 1, 100)?;
1999 Ok(Ec2Service::respond(
2000 "DescribeInstanceTopology",
2001 &req.request_id,
2002 &ec2_list("instanceSet", &[]),
2003 ))
2004}
2005
2006#[cfg(test)]
2007mod tests {
2008 use super::subnet_ip_prefix;
2009
2010 #[test]
2011 fn subnet_ip_prefix_uses_subnet_network() {
2012 assert_eq!(subnet_ip_prefix("172.31.16.0/20"), "172.31.16");
2014 assert_eq!(subnet_ip_prefix("10.0.5.0/24"), "10.0.5");
2015 assert_eq!(subnet_ip_prefix("192.168.1.0"), "192.168.1");
2017 }
2018
2019 #[test]
2020 fn subnet_ip_prefix_falls_back_on_garbage() {
2021 assert_eq!(subnet_ip_prefix(""), "10.0.0");
2022 assert_eq!(subnet_ip_prefix("not-a-cidr"), "10.0.0");
2023 assert_eq!(subnet_ip_prefix("fd00::/8"), "10.0.0");
2025 }
2026}
2027
2028#[cfg(test)]
2029mod modify_tests {
2030 use super::*;
2031
2032 fn req(action: &str, query: &[(&str, &str)]) -> AwsRequest {
2033 AwsRequest {
2034 service: "ec2".into(),
2035 action: action.into(),
2036 region: "us-east-1".into(),
2037 account_id: "000000000000".into(),
2038 request_id: "rid".into(),
2039 headers: http::HeaderMap::new(),
2040 query_params: query
2041 .iter()
2042 .map(|(k, v)| (k.to_string(), v.to_string()))
2043 .collect(),
2044 body: bytes::Bytes::new(),
2045 body_stream: parking_lot::Mutex::new(None),
2046 path_segments: Vec::new(),
2047 raw_path: "/".into(),
2048 raw_query: String::new(),
2049 method: http::Method::POST,
2050 is_query_protocol: true,
2051 access_key_id: None,
2052 principal: None,
2053 }
2054 }
2055
2056 fn body(resp: AwsResponse) -> String {
2057 String::from_utf8_lossy(resp.body.expect_bytes()).to_string()
2058 }
2059
2060 fn seed_instance(svc: &Ec2Service, id: &str) {
2061 let mut accounts = svc.state.write();
2062 let state = accounts.get_or_create("000000000000");
2063 let inst = Instance {
2064 instance_id: id.into(),
2065 image_id: "ami-1".into(),
2066 instance_type: "t3.micro".into(),
2067 state_code: 16,
2068 state_name: "running".into(),
2069 private_ip: "10.0.0.5".into(),
2070 public_ip: None,
2071 subnet_id: Some("subnet-1".into()),
2072 vpc_id: Some("vpc-1".into()),
2073 key_name: None,
2074 security_group_ids: vec![],
2075 reservation_id: "r-1".into(),
2076 ami_launch_index: 0,
2077 monitoring: false,
2078 az: "us-east-1a".into(),
2079 launch_time: "2024-01-01T00:00:00.000Z".into(),
2080 container_id: None,
2081 disable_api_termination: false,
2082 disable_api_stop: false,
2083 source_dest_check: true,
2084 ebs_optimized: false,
2085 instance_initiated_shutdown_behavior: "stop".into(),
2086 user_data: None,
2087 metadata_options: Default::default(),
2088 cpu_options: None,
2089 bandwidth_weighting: None,
2090 maintenance_options: Default::default(),
2091 placement_tenancy: None,
2092 placement_affinity: None,
2093 placement_group_name: None,
2094 private_dns_hostname_type: None,
2095 enable_resource_name_dns_a_record: false,
2096 enable_resource_name_dns_aaaa_record: false,
2097 };
2098 state.instances.insert(id.to_string(), inst);
2099 }
2100
2101 #[test]
2102 fn modify_instance_credit_specification_round_trips() {
2103 let svc = Ec2Service::new();
2104 seed_instance(&svc, "i-1");
2105 modify_instance_credit_specification(
2106 &svc,
2107 &req(
2108 "ModifyInstanceCreditSpecification",
2109 &[
2110 ("InstanceCreditSpecification.1.InstanceId", "i-1"),
2111 ("InstanceCreditSpecification.1.CpuCredits", "unlimited"),
2112 ],
2113 ),
2114 )
2115 .unwrap();
2116 let out = body(
2117 describe_instance_credit_specifications(
2118 &svc,
2119 &req(
2120 "DescribeInstanceCreditSpecifications",
2121 &[("InstanceId.1", "i-1")],
2122 ),
2123 )
2124 .unwrap(),
2125 );
2126 assert!(
2127 out.contains("<cpuCredits>unlimited</cpuCredits>"),
2128 "got: {out}"
2129 );
2130 }
2131
2132 #[test]
2133 fn modify_instance_credit_specification_unknown_is_unsuccessful() {
2134 let svc = Ec2Service::new();
2135 let out = body(
2136 modify_instance_credit_specification(
2137 &svc,
2138 &req(
2139 "ModifyInstanceCreditSpecification",
2140 &[("InstanceCreditSpecification.1.InstanceId", "i-missing")],
2141 ),
2142 )
2143 .unwrap(),
2144 );
2145 assert!(out.contains("InvalidInstanceID.NotFound"), "got: {out}");
2146 }
2147
2148 #[test]
2149 fn instance_metadata_defaults_round_trip_and_reset() {
2150 let svc = Ec2Service::new();
2151 modify_instance_metadata_defaults(
2152 &svc,
2153 &req(
2154 "ModifyInstanceMetadataDefaults",
2155 &[
2156 ("HttpTokens", "required"),
2157 ("HttpEndpoint", "enabled"),
2158 ("HttpTokensEnforced", "enabled"),
2159 ],
2160 ),
2161 )
2162 .unwrap();
2163 let out = body(
2164 get_instance_metadata_defaults(&svc, &req("GetInstanceMetadataDefaults", &[])).unwrap(),
2165 );
2166 assert!(
2167 out.contains("<httpTokens>required</httpTokens>"),
2168 "got: {out}"
2169 );
2170 assert!(out.contains("<httpEndpoint>enabled</httpEndpoint>"));
2171 assert!(out.contains("<httpTokensEnforced>enabled</httpTokensEnforced>"));
2173
2174 modify_instance_metadata_defaults(
2176 &svc,
2177 &req(
2178 "ModifyInstanceMetadataDefaults",
2179 &[("HttpTokens", "no-preference")],
2180 ),
2181 )
2182 .unwrap();
2183 let out = body(
2184 get_instance_metadata_defaults(&svc, &req("GetInstanceMetadataDefaults", &[])).unwrap(),
2185 );
2186 assert!(
2187 !out.contains("<httpTokens>"),
2188 "no-preference should clear it: {out}"
2189 );
2190 }
2191
2192 #[test]
2193 fn event_notification_attributes_persist_keys() {
2194 let svc = Ec2Service::new();
2195 register_event_notification_attributes(
2196 &svc,
2197 &req(
2198 "RegisterInstanceEventNotificationAttributes",
2199 &[
2200 ("InstanceTagAttribute.InstanceTagKey.1", "Name"),
2201 ("InstanceTagAttribute.InstanceTagKey.2", "env"),
2202 ],
2203 ),
2204 )
2205 .unwrap();
2206 let out = body(
2207 describe_event_notification_attributes(
2208 &svc,
2209 &req("DescribeInstanceEventNotificationAttributes", &[]),
2210 )
2211 .unwrap(),
2212 );
2213 assert!(out.contains("<item>Name</item>"), "got: {out}");
2214 assert!(out.contains("<item>env</item>"));
2215 assert!(!out.contains("<item><item>"), "got: {out}");
2218
2219 deregister_event_notification_attributes(
2220 &svc,
2221 &req(
2222 "DeregisterInstanceEventNotificationAttributes",
2223 &[("InstanceTagAttribute.InstanceTagKey.1", "Name")],
2224 ),
2225 )
2226 .unwrap();
2227 let out = body(
2228 describe_event_notification_attributes(
2229 &svc,
2230 &req("DescribeInstanceEventNotificationAttributes", &[]),
2231 )
2232 .unwrap(),
2233 );
2234 assert!(!out.contains("<item>Name</item>"), "got: {out}");
2235 assert!(out.contains("<item>env</item>"));
2236 }
2237}