From 99ca38e7d26b29528700887f9d4b4d00d91c997e Mon Sep 17 00:00:00 2001 From: Lucas Kent Date: Mon, 27 Nov 2023 11:16:00 +1100 Subject: [PATCH] add serialize/deserialize functionality to Ec2Instance --- aws-throwaway/Cargo.toml | 2 - aws-throwaway/src/ec2_instance.rs | 46 ++++++++++++++++++-- aws-throwaway/src/ec2_instance_definition.rs | 2 +- aws-throwaway/src/lib.rs | 2 +- readme.md | 5 ++- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/aws-throwaway/Cargo.toml b/aws-throwaway/Cargo.toml index 52eee0d..bd02038 100644 --- a/aws-throwaway/Cargo.toml +++ b/aws-throwaway/Cargo.toml @@ -26,8 +26,6 @@ anyhow = "1.0.42" uuid = { version = "1.0.0", features = ["serde", "v4"] } tracing = "0.1.15" async-trait = "0.1.30" -# TODO: avoid pulling in these dependencies when use_sdk is enabled, -# will need to introduce a use_cli feature to do so serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" futures = "0.3.30" diff --git a/aws-throwaway/src/ec2_instance.rs b/aws-throwaway/src/ec2_instance.rs index 730d19c..0fc6f53 100644 --- a/aws-throwaway/src/ec2_instance.rs +++ b/aws-throwaway/src/ec2_instance.rs @@ -1,9 +1,15 @@ use crate::ssh::SshConnection; +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr}; use std::time::Duration; use tokio::{net::TcpStream, time::Instant}; /// Represents a currently running EC2 instance and provides various methods for interacting with the instance. +/// +/// This type implements serde Serialize/Deserialize to allow you to save and restore the instance from disk. +/// After restoring Ec2Instance in this way you need to call the [`Ec2Instance::init`] method. +#[derive(Serialize, Deserialize)] pub struct Ec2Instance { connect_ip: IpAddr, public_ip: Option, @@ -11,10 +17,12 @@ pub struct Ec2Instance { client_private_key: String, host_public_key_bytes: Vec, host_public_key: String, - ssh: SshConnection, + #[serde(skip)] + ssh: Option, network_interfaces: Vec, } +#[derive(Serialize, Deserialize)] pub struct NetworkInterface { pub private_ipv4: Ipv4Addr, pub device_index: i32, @@ -32,7 +40,7 @@ impl Ec2Instance { } /// Use this address to get the private or public IP that aws-throwaway is using to ssh to the instance. - /// Whether or not this is public is decided by AwsBuilder::use_public_addresses. + /// Whether or not this is public is decided by [`crate::AwsBuilder::use_public_addresses`]. /// /// You should use this address if you want to connect to the instance from your local machine pub fn connect_ip(&self) -> IpAddr { @@ -62,7 +70,9 @@ impl Ec2Instance { /// Returns an object that allows commands to be sent over ssh pub fn ssh(&self) -> &SshConnection { - &self.ssh + self.ssh + .as_ref() + .expect("Make sure to call `Ec2Instance::init` after deserializing `Ec2Instance`") } /// Get a list of commands that the user can paste into bash to manually open an ssh connection to this instance. @@ -132,7 +142,7 @@ TERM=xterm ssh -i key ubuntu@{} -o "UserKnownHostsFile known_hosts" Ok(ssh) => { break Ec2Instance { connect_ip, - ssh, + ssh: Some(ssh), public_ip, private_ip, host_public_key_bytes, @@ -146,4 +156,32 @@ TERM=xterm ssh -i key ubuntu@{} -o "UserKnownHostsFile known_hosts" }; } } + + /// After deserializing [`Ec2Instance`] this method must be called to recreate the ssh connection. + /// + /// No need to call after creating via [`crate::Aws::create_ec2_instance`] + pub async fn init(&mut self) -> Result<()> { + let connect_ip = self.connect_ip; + + // We use a drastically simplifed initialization approach here compared to `Ec2Instance::new`. + // Since we can assume that the server has either already started up or is now terminated we + // avoid retries and tailor our error messages in order to provide better error reporting. + let stream = + tokio::time::timeout(Duration::from_secs(5), TcpStream::connect((connect_ip, 22))) + .await + .map_err(|_| anyhow!("Timed out connecting to {connect_ip}:22"))? + .with_context(|| format!("Failed to connect to {connect_ip}:22"))?; + + let ssh = SshConnection::new( + stream, + connect_ip, + self.host_public_key_bytes.clone(), + &self.client_private_key, + ) + .await + .with_context(|| format!("Failed to make ssh connection to {connect_ip}:22"))?; + self.ssh = Some(ssh); + + Ok(()) + } } diff --git a/aws-throwaway/src/ec2_instance_definition.rs b/aws-throwaway/src/ec2_instance_definition.rs index e671fe6..b91e691 100644 --- a/aws-throwaway/src/ec2_instance_definition.rs +++ b/aws-throwaway/src/ec2_instance_definition.rs @@ -1,6 +1,6 @@ use crate::InstanceType; -/// Defines an instance that can be launched via [`Aws::create_ec2_instance`] +/// Defines an instance that can be launched via [`crate::Aws::create_ec2_instance`] pub struct Ec2InstanceDefinition { pub(crate) instance_type: InstanceType, pub(crate) volume_size_gb: u32, diff --git a/aws-throwaway/src/lib.rs b/aws-throwaway/src/lib.rs index 788423f..798a8fc 100644 --- a/aws-throwaway/src/lib.rs +++ b/aws-throwaway/src/lib.rs @@ -122,6 +122,6 @@ pub enum CleanupResources { /// Cleanup resources created by all [`Aws`] instances that use [`CleanupResources::WithAppTag`] of the same tag. /// It is highly reccomended that this tag is hardcoded, generating this tag could easily lead to forgotten resources. WithAppTag(String), - /// Cleanup resources created by all [`Aws`] instances regardless of whether it was created via [`CleanupResources::AllResources`] or [`CleanupResources::ResourcesMatchingTag`] + /// Cleanup resources created by all [`Aws`] instances regardless of whether it was created via [`CleanupResources::AllResources`] or [`CleanupResources::WithAppTag`] AllResources, } diff --git a/readme.md b/readme.md index e9cf808..14b83ee 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,10 @@ aws-throwaway makes it trivial to spin up an instance, interact with it, and the ```rust let aws = Aws::builder(CleanupResources::AllResources).build().await; -let instance = aws.create_ec2_instance(Ec2InstanceDefinition::new(InstanceType::T2Micro)).await; +let instance = aws.create_ec2_instance( + Ec2InstanceDefinition::new(InstanceType::T2Micro) +).await; + let output = instance.ssh().shell("echo 'Hello world!'").await; println!("output from ec2 instance: {}", output.stdout);