use fakecloud_aws::ec2query::{ec2_elem, ec2_list};
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use crate::service::Ec2Service;
use crate::service_helpers::{gen_id, indexed_list, require, validate_enum, validate_max_results};
use crate::state::{
DedicatedHost, Ec2State, ReservedInstances, ReservedInstancesListing,
ReservedInstancesModification, Tag,
};
const FIXED_TIME: &str = "2024-01-01T00:00:00.000Z";
fn ri_xml(r: &ReservedInstances, tags: &[Tag]) -> String {
format!(
"{}{}{}<instanceCount>{}</instanceCount>{}{}<duration>{}</duration>\
<fixedPrice>{}</fixedPrice><usagePrice>{}</usagePrice>{}{}{}{}{}{}{}{}{}",
ec2_elem("reservedInstancesId", &r.id),
ec2_elem("instanceType", &r.instance_type),
ec2_elem("availabilityZone", &r.availability_zone),
r.instance_count,
ec2_elem("productDescription", &r.product_description),
ec2_elem("state", &r.state),
r.duration,
r.fixed_price,
r.usage_price,
ec2_elem("start", FIXED_TIME),
ec2_elem("end", "2025-01-01T00:00:00.000Z"),
ec2_elem("offeringClass", "standard"),
ec2_elem("offeringType", "No Upfront"),
ec2_elem("instanceTenancy", "default"),
ec2_elem("currencyCode", "USD"),
ec2_elem("scope", "Availability Zone"),
ec2_list("recurringCharges", &[]),
super::tags::tag_set_xml(tags),
)
}
pub(crate) fn describe_reserved_instances(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(
&req.query_params,
"OfferingClass",
&["standard", "convertible"],
)?;
validate_enum(
&req.query_params,
"OfferingType",
&[
"Heavy Utilization",
"Medium Utilization",
"Light Utilization",
"No Upfront",
"Partial Upfront",
"All Upfront",
],
)?;
let wanted = indexed_list(&req.query_params, "ReservedInstancesId");
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.reserved_instances
.values()
.filter(|r| wanted.is_empty() || wanted.contains(&r.id))
.map(|r| ri_xml(r, state.tags_for(&r.id)))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeReservedInstances",
&req.request_id,
&ec2_list("reservedInstancesSet", &items),
))
}
pub(crate) fn describe_reserved_instances_offerings(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(
&req.query_params,
"OfferingClass",
&["standard", "convertible"],
)?;
validate_enum(
&req.query_params,
"ProductDescription",
&[
"Linux/UNIX",
"Linux/UNIX (Amazon VPC)",
"Windows",
"Windows (Amazon VPC)",
],
)?;
validate_enum(
&req.query_params,
"InstanceTenancy",
&["default", "dedicated", "host"],
)?;
validate_enum(
&req.query_params,
"OfferingType",
&[
"Heavy Utilization",
"Medium Utilization",
"Light Utilization",
"No Upfront",
"Partial Upfront",
"All Upfront",
],
)?;
if let Some(v) = req
.query_params
.get("InstanceType")
.filter(|v| !v.is_empty())
{
if !v.contains('.') {
return Err(crate::service_helpers::invalid_parameter_value(format!(
"Invalid instance type '{v}'"
)));
}
}
let az = format!(
"{}a",
if req.region.is_empty() {
"us-east-1"
} else {
&req.region
}
);
let item = format!(
"{}{}{}<duration>31536000</duration><fixedPrice>0.0</fixedPrice><usagePrice>0.02</usagePrice>{}{}{}{}{}{}",
ec2_elem("reservedInstancesOfferingId", &gen_id("offering")),
ec2_elem("instanceType", "t3.micro"),
ec2_elem("availabilityZone", &az),
ec2_elem("productDescription", "Linux/UNIX"),
ec2_elem("offeringClass", "standard"),
ec2_elem("offeringType", "No Upfront"),
ec2_elem("instanceTenancy", "default"),
ec2_elem("currencyCode", "USD"),
ec2_list("recurringCharges", &[]),
);
Ok(Ec2Service::respond(
"DescribeReservedInstancesOfferings",
&req.request_id,
&ec2_list("reservedInstancesOfferingsSet", &[item]),
))
}
pub(crate) fn purchase_reserved_instances_offering(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "InstanceCount")?;
require(&req.query_params, "ReservedInstancesOfferingId")?;
let id = gen_id("ri");
let count: i64 = req
.query_params
.get("InstanceCount")
.and_then(|v| v.parse().ok())
.unwrap_or(1);
{
let mut accounts = svc.state.write();
accounts
.get_or_create(&req.account_id)
.reserved_instances
.insert(
id.clone(),
ReservedInstances {
id: id.clone(),
instance_type: "t3.micro".to_string(),
availability_zone: format!(
"{}a",
if req.region.is_empty() {
"us-east-1"
} else {
&req.region
}
),
instance_count: count,
product_description: "Linux/UNIX".to_string(),
state: "active".to_string(),
duration: 31536000,
fixed_price: "0.0".to_string(),
usage_price: "0.02".to_string(),
},
);
}
Ok(Ec2Service::respond(
"PurchaseReservedInstancesOffering",
&req.request_id,
&ec2_elem("reservedInstancesId", &id),
))
}
fn listing_xml(l: &ReservedInstancesListing) -> String {
format!(
"{}{}<status>{}</status><statusMessage>{}</statusMessage>{}{}{}{}{}",
ec2_elem("reservedInstancesListingId", &l.listing_id),
ec2_elem("reservedInstancesId", &l.reserved_instances_id),
l.status,
l.status_message,
ec2_elem("createDate", FIXED_TIME),
ec2_elem("updateDate", FIXED_TIME),
ec2_elem("clientToken", &l.client_token),
ec2_list("instanceCounts", &[]),
ec2_list("priceSchedules", &[]),
)
}
pub(crate) fn create_reserved_instances_listing(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let ri = require(&req.query_params, "ReservedInstancesId")?;
let count: i64 = require(&req.query_params, "InstanceCount")?
.parse()
.unwrap_or(1);
let client_token = require(&req.query_params, "ClientToken")?;
let listing = ReservedInstancesListing {
listing_id: gen_id("ril"),
reserved_instances_id: ri,
instance_count: count,
client_token,
status: "active".to_string(),
status_message: "ACTIVE".to_string(),
};
let xml = listing_xml(&listing);
{
let mut accounts = svc.state.write();
accounts
.get_or_create(&req.account_id)
.reserved_instances_listings
.insert(listing.listing_id.clone(), listing);
}
Ok(Ec2Service::respond(
"CreateReservedInstancesListing",
&req.request_id,
&ec2_list("reservedInstancesListingsSet", &[xml]),
))
}
pub(crate) fn cancel_reserved_instances_listing(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "ReservedInstancesListingId")?;
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
let listing = match state.reserved_instances_listings.get_mut(&id) {
Some(listing) => {
listing.status = "cancelled".to_string();
listing.status_message = "CANCELLED".to_string();
listing.clone()
}
None => ReservedInstancesListing {
listing_id: id.clone(),
reserved_instances_id: "ri-cancelled".to_string(),
instance_count: 0,
client_token: String::new(),
status: "cancelled".to_string(),
status_message: "CANCELLED".to_string(),
},
};
let xml = listing_xml(&listing);
Ok(Ec2Service::respond(
"CancelReservedInstancesListing",
&req.request_id,
&ec2_list("reservedInstancesListingsSet", &[xml]),
))
}
pub(crate) fn describe_reserved_instances_listings(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let want_listing = req
.query_params
.get("ReservedInstancesListingId")
.filter(|v| !v.is_empty());
let want_ri = req
.query_params
.get("ReservedInstancesId")
.filter(|v| !v.is_empty());
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.reserved_instances_listings
.values()
.filter(|l| want_listing.map(|w| w == &l.listing_id).unwrap_or(true))
.filter(|l| {
want_ri
.map(|w| w == &l.reserved_instances_id)
.unwrap_or(true)
})
.map(listing_xml)
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeReservedInstancesListings",
&req.request_id,
&ec2_list("reservedInstancesListingsSet", &items),
))
}
fn modification_xml(m: &ReservedInstancesModification) -> String {
let ids: Vec<String> = m
.reserved_instances_ids
.iter()
.map(|id| format!("<item>{}</item>", ec2_elem("reservedInstancesId", id)))
.collect();
format!(
"{}{}<status>{}</status>{}{}{}{}",
ec2_elem("reservedInstancesModificationId", &m.modification_id),
ec2_elem("clientToken", &m.client_token),
m.status,
ec2_elem("createDate", FIXED_TIME),
ec2_elem("updateDate", FIXED_TIME),
ec2_elem("effectiveDate", FIXED_TIME),
format_args!(
"<reservedInstancesSet>{}</reservedInstancesSet>",
ids.join("")
),
)
}
pub(crate) fn describe_reserved_instances_modifications(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let want = indexed_list(&req.query_params, "ReservedInstancesModificationId");
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.reserved_instances_modifications
.values()
.filter(|m| want.is_empty() || want.contains(&m.modification_id))
.map(modification_xml)
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeReservedInstancesModifications",
&req.request_id,
&ec2_list("reservedInstancesModificationsSet", &items),
))
}
pub(crate) fn modify_reserved_instances(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let reserved_instances_ids = indexed_list(&req.query_params, "ReservedInstancesId");
let client_token = req
.query_params
.get("ClientToken")
.cloned()
.unwrap_or_default();
let modification = ReservedInstancesModification {
modification_id: gen_id("rimod"),
reserved_instances_ids,
status: "processing".to_string(),
client_token,
};
let mod_id = modification.modification_id.clone();
{
let mut accounts = svc.state.write();
accounts
.get_or_create(&req.account_id)
.reserved_instances_modifications
.insert(mod_id.clone(), modification);
}
Ok(Ec2Service::respond(
"ModifyReservedInstances",
&req.request_id,
&ec2_elem("reservedInstancesModificationId", &mod_id),
))
}
pub(crate) fn get_reserved_instances_exchange_quote(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body = format!(
"<isValidExchange>true</isValidExchange>{}<paymentDue>0.0</paymentDue>{}{}",
ec2_elem("currencyCode", "USD"),
ec2_list("reservedInstanceValueSet", &[]),
ec2_list("targetConfigurationValueSet", &[]),
);
Ok(Ec2Service::respond(
"GetReservedInstancesExchangeQuote",
&req.request_id,
&body,
))
}
pub(crate) fn accept_reserved_instances_exchange_quote(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
Ok(Ec2Service::respond(
"AcceptReservedInstancesExchangeQuote",
&req.request_id,
&ec2_elem("exchangeId", &gen_id("riex")),
))
}
pub(crate) fn delete_queued_reserved_instances(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let ids = indexed_list(&req.query_params, "ReservedInstancesId");
let success: Vec<String> = ids
.iter()
.map(|id| ec2_elem("reservedInstancesId", id))
.collect();
let body = format!(
"{}{}",
ec2_list("successfulQueuedPurchaseDeletionSet", &success),
ec2_list("failedQueuedPurchaseDeletionSet", &[])
);
Ok(Ec2Service::respond(
"DeleteQueuedReservedInstances",
&req.request_id,
&body,
))
}
fn host_xml(h: &DedicatedHost, tags: &[Tag], owner: &str) -> String {
format!(
"{}{}{}{}{}<allowsMultipleInstanceTypes>off</allowsMultipleInstanceTypes>{}{}{}{}{}{}",
ec2_elem("hostId", &h.id),
ec2_elem("autoPlacement", &h.auto_placement),
ec2_elem("availabilityZone", &h.availability_zone),
ec2_elem("state", &h.state),
ec2_elem("allocationTime", FIXED_TIME),
ec2_elem("hostRecovery", &h.host_recovery),
ec2_elem("hostMaintenance", &h.host_maintenance),
ec2_elem("ownerId", owner),
format_args!(
"<hostProperties>{}<cores>8</cores><sockets>1</sockets><totalVCpus>32</totalVCpus></hostProperties>",
ec2_elem("instanceType", &h.instance_type)
),
ec2_list("instances", &[]),
super::tags::tag_set_xml(tags),
)
}
pub(crate) fn allocate_hosts(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(&req.query_params, "AutoPlacement", &["on", "off"])?;
validate_enum(&req.query_params, "HostRecovery", &["on", "off"])?;
validate_enum(&req.query_params, "HostMaintenance", &["on", "off"])?;
let quantity: usize = req
.query_params
.get("Quantity")
.and_then(|v| v.parse().ok())
.unwrap_or(1);
let az = req
.query_params
.get("AvailabilityZone")
.cloned()
.unwrap_or_else(|| {
format!(
"{}a",
if req.region.is_empty() {
"us-east-1"
} else {
&req.region
}
)
});
let instance_type = req
.query_params
.get("InstanceType")
.cloned()
.unwrap_or_else(|| "m5.large".to_string());
let mut ids = Vec::new();
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
for _ in 0..quantity.max(1) {
let id = gen_id("h");
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&id,
"dedicated-host",
);
state.dedicated_hosts.insert(
id.clone(),
DedicatedHost {
id: id.clone(),
auto_placement: req
.query_params
.get("AutoPlacement")
.cloned()
.unwrap_or_else(|| "on".to_string()),
availability_zone: az.clone(),
instance_type: instance_type.clone(),
state: "available".to_string(),
host_recovery: req
.query_params
.get("HostRecovery")
.cloned()
.unwrap_or_else(|| "off".to_string()),
host_maintenance: req
.query_params
.get("HostMaintenance")
.cloned()
.unwrap_or_else(|| "off".to_string()),
},
);
ids.push(id);
}
}
Ok(Ec2Service::respond(
"AllocateHosts",
&req.request_id,
&ec2_list("hostIdSet", &ids),
))
}
pub(crate) fn describe_hosts(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let wanted = indexed_list(&req.query_params, "HostId");
let owner = req.account_id.clone();
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.dedicated_hosts
.values()
.filter(|h| h.state != "released")
.filter(|h| wanted.is_empty() || wanted.contains(&h.id))
.map(|h| host_xml(h, state.tags_for(&h.id), &owner))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeHosts",
&req.request_id,
&ec2_list("hostSet", &items),
))
}
pub(crate) fn modify_hosts(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(&req.query_params, "AutoPlacement", &["on", "off"])?;
validate_enum(&req.query_params, "HostRecovery", &["on", "off"])?;
validate_enum(&req.query_params, "HostMaintenance", &["on", "off"])?;
let ids = indexed_list(&req.query_params, "HostId");
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
for id in &ids {
if let Some(h) = state.dedicated_hosts.get_mut(id) {
if let Some(v) = req.query_params.get("AutoPlacement") {
h.auto_placement = v.clone();
}
if let Some(v) = req.query_params.get("HostRecovery") {
h.host_recovery = v.clone();
}
if let Some(v) = req.query_params.get("HostMaintenance") {
h.host_maintenance = v.clone();
}
}
}
}
let body = format!(
"{}{}",
ec2_list("successful", &ids),
ec2_list("unsuccessful", &[])
);
Ok(Ec2Service::respond("ModifyHosts", &req.request_id, &body))
}
pub(crate) fn release_hosts(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let ids = indexed_list(&req.query_params, "HostId");
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
for id in &ids {
state.dedicated_hosts.remove(id);
state.tags.remove(id);
}
}
let body = format!(
"{}{}",
ec2_list("successful", &ids),
ec2_list("unsuccessful", &[])
);
Ok(Ec2Service::respond("ReleaseHosts", &req.request_id, &body))
}
pub(crate) fn describe_mac_hosts(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 500)?;
Ok(Ec2Service::respond(
"DescribeMacHosts",
&req.request_id,
&ec2_list("macHostSet", &[]),
))
}