use fakecloud_aws::ec2query::{ec2_elem, ec2_list, ec2_return};
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use crate::service::Ec2Service;
use crate::service_helpers::{
gen_id, indexed_list, parse_filters, require, validate_enum, validate_max_results, Filter,
};
use crate::state::{Ec2State, ElasticIp, KeyPair, PlacementGroup, Tag};
fn address_xml(a: &ElasticIp, tags: &[Tag]) -> String {
let mut out = format!(
"{}{}{}",
ec2_elem("publicIp", &a.public_ip),
ec2_elem("allocationId", &a.allocation_id),
ec2_elem("domain", &a.domain),
);
if let Some(v) = &a.association_id {
out.push_str(&ec2_elem("associationId", v));
}
if let Some(v) = &a.instance_id {
out.push_str(&ec2_elem("instanceId", v));
}
if let Some(v) = &a.network_interface_id {
out.push_str(&ec2_elem("networkInterfaceId", v));
}
if let Some(v) = &a.private_ip_address {
out.push_str(&ec2_elem("privateIpAddress", v));
}
out.push_str(&super::tags::tag_set_xml(tags));
out
}
pub(crate) fn allocate_address(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(&req.query_params, "Domain", &["vpc", "standard"])?;
let alloc_id = gen_id("eipalloc");
let public_ip = format!(
"52.0.{}.{}",
alloc_id.len() % 250,
alloc_id.as_bytes()[9] as u16 % 250
);
let domain = req
.query_params
.get("Domain")
.cloned()
.unwrap_or_else(|| "vpc".to_string());
let eip = ElasticIp {
allocation_id: alloc_id.clone(),
public_ip: public_ip.clone(),
domain: domain.clone(),
association_id: None,
instance_id: None,
network_interface_id: None,
private_ip_address: None,
};
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&alloc_id,
"elastic-ip",
);
state.elastic_ips.insert(alloc_id.clone(), eip);
}
let body = format!(
"{}{}{}{}{}",
ec2_elem("allocationId", &alloc_id),
ec2_elem("publicIp", &public_ip),
ec2_elem("domain", &domain),
ec2_elem(
"networkBorderGroup",
if req.region.is_empty() {
"us-east-1"
} else {
&req.region
}
),
ec2_elem("publicIpv4Pool", "amazon"),
);
Ok(Ec2Service::respond(
"AllocateAddress",
&req.request_id,
&body,
))
}
pub(crate) fn release_address(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(id) = req.query_params.get("AllocationId") {
state.elastic_ips.remove(id);
state.tags.remove(id);
} else if let Some(ip) = req.query_params.get("PublicIp") {
let ids: Vec<String> = state
.elastic_ips
.values()
.filter(|e| &e.public_ip == ip)
.map(|e| e.allocation_id.clone())
.collect();
for id in ids {
state.elastic_ips.remove(&id);
}
}
Ok(Ec2Service::respond(
"ReleaseAddress",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn describe_addresses(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let filters = parse_filters(&req.query_params);
let wanted = indexed_list(&req.query_params, "AllocationId");
let wanted_ips = indexed_list(&req.query_params, "PublicIp");
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
.elastic_ips
.values()
.filter(|e| wanted.is_empty() || wanted.contains(&e.allocation_id))
.filter(|e| wanted_ips.is_empty() || wanted_ips.contains(&e.public_ip))
.filter(|e| addr_match(e, state.tags_for(&e.allocation_id), &filters))
.map(|e| address_xml(e, state.tags_for(&e.allocation_id)))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeAddresses",
&req.request_id,
&ec2_list("addressesSet", &items),
))
}
fn addr_match(e: &ElasticIp, tags: &[Tag], filters: &[Filter]) -> bool {
filters.iter().all(|f| {
let candidates: Vec<String> = match f.name.as_str() {
"allocation-id" => vec![e.allocation_id.clone()],
"public-ip" => vec![e.public_ip.clone()],
"domain" => vec![e.domain.clone()],
"tag-key" => tags.iter().map(|t| t.key.clone()).collect(),
name => {
if let Some(key) = name.strip_prefix("tag:") {
tags.iter()
.filter(|t| t.key == key)
.map(|t| t.value.clone())
.collect()
} else {
return true;
}
}
};
f.values.iter().any(|v| candidates.iter().any(|c| c == v))
})
}
pub(crate) fn associate_address(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let assoc_id = gen_id("eipassoc");
if let Some(alloc) = req.query_params.get("AllocationId") {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(e) = state.elastic_ips.get_mut(alloc) {
e.association_id = Some(assoc_id.clone());
e.instance_id = req.query_params.get("InstanceId").cloned();
e.network_interface_id = req.query_params.get("NetworkInterfaceId").cloned();
}
}
Ok(Ec2Service::respond(
"AssociateAddress",
&req.request_id,
&ec2_elem("associationId", &assoc_id),
))
}
pub(crate) fn disassociate_address(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
if let Some(assoc) = req.query_params.get("AssociationId") {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
for e in state.elastic_ips.values_mut() {
if e.association_id.as_deref() == Some(assoc.as_str()) {
e.association_id = None;
e.instance_id = None;
e.network_interface_id = None;
}
}
}
Ok(Ec2Service::respond(
"DisassociateAddress",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn describe_addresses_attribute(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 1, 1000)?;
validate_enum(&req.query_params, "Attribute", &["domain-name"])?;
Ok(Ec2Service::respond(
"DescribeAddressesAttribute",
&req.request_id,
&ec2_list("addressSet", &[]),
))
}
fn address_attribute_body(req: &AwsRequest) -> String {
let alloc = req
.query_params
.get("AllocationId")
.cloned()
.unwrap_or_default();
format!(
"<address>{}{}</address>",
ec2_elem("publicIp", "52.0.0.1"),
ec2_elem("allocationId", &alloc),
)
}
pub(crate) fn modify_address_attribute(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "AllocationId")?;
Ok(Ec2Service::respond(
"ModifyAddressAttribute",
&req.request_id,
&address_attribute_body(req),
))
}
pub(crate) fn reset_address_attribute(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "AllocationId")?;
require(&req.query_params, "Attribute")?;
validate_enum(&req.query_params, "Attribute", &["domain-name"])?;
Ok(Ec2Service::respond(
"ResetAddressAttribute",
&req.request_id,
&address_attribute_body(req),
))
}
pub(crate) fn move_address_to_vpc(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "PublicIp")?;
let body = format!(
"{}{}",
ec2_elem("allocationId", &gen_id("eipalloc")),
ec2_elem("status", "MoveInProgress"),
);
Ok(Ec2Service::respond(
"MoveAddressToVpc",
&req.request_id,
&body,
))
}
pub(crate) fn restore_address_to_classic(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let ip = require(&req.query_params, "PublicIp")?;
let body = format!(
"{}{}",
ec2_elem("publicIp", &ip),
ec2_elem("status", "InClassic")
);
Ok(Ec2Service::respond(
"RestoreAddressToClassic",
&req.request_id,
&body,
))
}
fn address_transfer_xml(req: &AwsRequest, status: &str) -> String {
format!(
"<addressTransfer>{}{}{}{}</addressTransfer>",
ec2_elem(
"publicIp",
req.query_params
.get("Address")
.map(|s| s.as_str())
.unwrap_or("52.0.0.1")
),
ec2_elem(
"allocationId",
req.query_params
.get("AllocationId")
.map(|s| s.as_str())
.unwrap_or("eipalloc-0")
),
ec2_elem(
"transferAccountId",
req.query_params
.get("TransferAccountId")
.map(|s| s.as_str())
.unwrap_or("123456789012")
),
ec2_elem("addressTransferStatus", status),
)
}
pub(crate) fn accept_address_transfer(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "Address")?;
Ok(Ec2Service::respond(
"AcceptAddressTransfer",
&req.request_id,
&address_transfer_xml(req, "accepted"),
))
}
pub(crate) fn enable_address_transfer(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "AllocationId")?;
require(&req.query_params, "TransferAccountId")?;
Ok(Ec2Service::respond(
"EnableAddressTransfer",
&req.request_id,
&address_transfer_xml(req, "pending"),
))
}
pub(crate) fn disable_address_transfer(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
require(&req.query_params, "AllocationId")?;
Ok(Ec2Service::respond(
"DisableAddressTransfer",
&req.request_id,
&address_transfer_xml(req, "disabled"),
))
}
pub(crate) fn describe_address_transfers(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 1000)?;
Ok(Ec2Service::respond(
"DescribeAddressTransfers",
&req.request_id,
&ec2_list("addressTransferSet", &[]),
))
}
pub(crate) fn describe_moving_addresses(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 1000)?;
Ok(Ec2Service::respond(
"DescribeMovingAddresses",
&req.request_id,
&ec2_list("movingAddressStatusSet", &[]),
))
}
const FAKE_KEY_MATERIAL: &str =
"-----BEGIN RSA PRIVATE KEY-----\nMIIfakefakefakefakefake\n-----END RSA PRIVATE KEY-----";
pub(crate) fn create_key_pair(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let key_name = require(&req.query_params, "KeyName")?;
validate_enum(&req.query_params, "KeyType", &["rsa", "ed25519"])?;
validate_enum(&req.query_params, "KeyFormat", &["pem", "ppk"])?;
let key_pair_id = gen_id("key");
let fingerprint = "1a:2b:3c:4d:5e:6f:00:11:22:33:44:55:66:77:88:99".to_string();
let kp = KeyPair {
key_pair_id: key_pair_id.clone(),
key_name: key_name.clone(),
key_type: req
.query_params
.get("KeyType")
.cloned()
.unwrap_or_else(|| "rsa".to_string()),
key_fingerprint: fingerprint.clone(),
};
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&key_pair_id,
"key-pair",
);
state.key_pairs.insert(key_name.clone(), kp);
}
let body = format!(
"{}{}{}{}",
ec2_elem("keyName", &key_name),
ec2_elem("keyPairId", &key_pair_id),
ec2_elem("keyFingerprint", &fingerprint),
ec2_elem("keyMaterial", FAKE_KEY_MATERIAL),
);
Ok(Ec2Service::respond("CreateKeyPair", &req.request_id, &body))
}
pub(crate) fn import_key_pair(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let key_name = require(&req.query_params, "KeyName")?;
require(&req.query_params, "PublicKeyMaterial")?;
let key_pair_id = gen_id("key");
let fingerprint = "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99".to_string();
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
state.key_pairs.insert(
key_name.clone(),
KeyPair {
key_pair_id: key_pair_id.clone(),
key_name: key_name.clone(),
key_type: "rsa".to_string(),
key_fingerprint: fingerprint.clone(),
},
);
}
let body = format!(
"{}{}{}",
ec2_elem("keyName", &key_name),
ec2_elem("keyPairId", &key_pair_id),
ec2_elem("keyFingerprint", &fingerprint),
);
Ok(Ec2Service::respond("ImportKeyPair", &req.request_id, &body))
}
pub(crate) fn delete_key_pair(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(name) = req.query_params.get("KeyName") {
state.key_pairs.remove(name);
} else if let Some(id) = req.query_params.get("KeyPairId") {
let names: Vec<String> = state
.key_pairs
.values()
.filter(|k| &k.key_pair_id == id)
.map(|k| k.key_name.clone())
.collect();
for n in names {
state.key_pairs.remove(&n);
}
}
Ok(Ec2Service::respond(
"DeleteKeyPair",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn describe_key_pairs(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let wanted = indexed_list(&req.query_params, "KeyName");
let wanted_ids = indexed_list(&req.query_params, "KeyPairId");
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
.key_pairs
.values()
.filter(|k| wanted.is_empty() || wanted.contains(&k.key_name))
.filter(|k| wanted_ids.is_empty() || wanted_ids.contains(&k.key_pair_id))
.map(|k| {
format!(
"{}{}{}{}",
ec2_elem("keyName", &k.key_name),
ec2_elem("keyPairId", &k.key_pair_id),
ec2_elem("keyType", &k.key_type),
ec2_elem("keyFingerprint", &k.key_fingerprint),
)
})
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeKeyPairs",
&req.request_id,
&ec2_list("keySet", &items),
))
}
fn pg_xml(p: &PlacementGroup, tags: &[Tag], owner: &str, region: &str) -> String {
let mut out = format!(
"{}{}{}{}{}",
ec2_elem("groupName", &p.group_name),
ec2_elem("groupId", &p.group_id),
ec2_elem("state", &p.state),
ec2_elem("strategy", &p.strategy),
ec2_elem(
"groupArn",
&format!(
"arn:aws:ec2:{region}:{owner}:placement-group/{}",
p.group_name
)
),
);
if let Some(n) = p.partition_count {
out.push_str(&format!("<partitionCount>{n}</partitionCount>"));
}
if let Some(sl) = &p.spread_level {
out.push_str(&ec2_elem("spreadLevel", sl));
}
out.push_str(&super::tags::tag_set_xml(tags));
out
}
pub(crate) fn create_placement_group(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(
&req.query_params,
"Strategy",
&["cluster", "spread", "partition"],
)?;
validate_enum(&req.query_params, "SpreadLevel", &["host", "rack"])?;
let name = req
.query_params
.get("GroupName")
.cloned()
.unwrap_or_else(|| gen_id("pg"));
let pg = PlacementGroup {
group_id: gen_id("pg"),
group_name: name.clone(),
strategy: req
.query_params
.get("Strategy")
.cloned()
.unwrap_or_else(|| "cluster".to_string()),
state: "available".to_string(),
partition_count: req
.query_params
.get("PartitionCount")
.and_then(|v| v.parse().ok()),
spread_level: req.query_params.get("SpreadLevel").cloned(),
};
let owner = req.account_id.clone();
let region = req.region.clone();
let tags = {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&pg.group_id,
"placement-group",
);
let t = state.tags_for(&pg.group_id).to_vec();
state.placement_groups.insert(name, pg.clone());
t
};
let body = format!(
"<placementGroup>{}</placementGroup>",
pg_xml(&pg, &tags, &owner, ®ion)
);
Ok(Ec2Service::respond(
"CreatePlacementGroup",
&req.request_id,
&body,
))
}
pub(crate) fn delete_placement_group(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let name = require(&req.query_params, "GroupName")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
state.placement_groups.remove(&name);
}
Ok(Ec2Service::respond(
"DeletePlacementGroup",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn describe_placement_groups(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let wanted = indexed_list(&req.query_params, "GroupName");
let owner = req.account_id.clone();
let region = req.region.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
.placement_groups
.values()
.filter(|p| wanted.is_empty() || wanted.contains(&p.group_name))
.map(|p| pg_xml(p, state.tags_for(&p.group_id), &owner, ®ion))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribePlacementGroups",
&req.request_id,
&ec2_list("placementGroupSet", &items),
))
}
pub(crate) fn get_groups_for_capacity_reservation(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 1, 1000)?;
require(&req.query_params, "CapacityReservationId")?;
Ok(Ec2Service::respond(
"GetGroupsForCapacityReservation",
&req.request_id,
&ec2_list("capacityReservationGroupSet", &[]),
))
}