1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use crate::ssh::SshConnection;
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.
pub struct Ec2Instance {
    connect_ip: IpAddr,
    public_ip: Option<IpAddr>,
    private_ip: IpAddr,
    client_private_key: String,
    host_public_key_bytes: Vec<u8>,
    host_public_key: String,
    ssh: SshConnection,
    network_interfaces: Vec<NetworkInterface>,
}

pub struct NetworkInterface {
    pub private_ipv4: Ipv4Addr,
    pub device_index: i32,
}

impl Ec2Instance {
    /// Use this address to connect to this instance from outside of AWS
    pub fn public_ip(&self) -> Option<IpAddr> {
        self.public_ip
    }

    /// Use this address to connect to this instance from within AWS
    pub fn private_ip(&self) -> IpAddr {
        self.private_ip
    }

    /// List of all network interfaces attached to this instance.
    /// Includes the primary interface that has the ip returned by [`Ec2Instance::private_ip`] as well as all other interfaces attached to this instance at the time it was created.
    pub fn network_interfaces(&self) -> &[NetworkInterface] {
        &self.network_interfaces
    }

    /// Use this as the private key of your machine when connecting to this instance
    pub fn client_private_key(&self) -> &str {
        &self.client_private_key
    }

    /// Use this for authenticating a host programmatically
    pub fn host_public_key_bytes(&self) -> &[u8] {
        &self.host_public_key_bytes
    }

    /// Insert this into your known_hosts file to avoid errors due to unknown fingerprints
    pub fn openssh_known_hosts_line(&self) -> String {
        format!("{} {}", &self.connect_ip, &self.host_public_key)
    }

    /// Returns an object that allows commands to be sent over ssh
    pub fn ssh(&self) -> &SshConnection {
        &self.ssh
    }

    /// Get a list of commands that the user can paste into bash to manually open an ssh connection to this instance.
    pub fn ssh_instructions(&self) -> String {
        format!(
            r#"```
chmod 700 key 2> /dev/null || true
echo '{}' > key
echo '{}' > known_hosts
chmod 400 key
TERM=xterm ssh -i key ubuntu@{} -o "UserKnownHostsFile known_hosts"
```"#,
            self.client_private_key(),
            self.openssh_known_hosts_line(),
            self.connect_ip
        )
    }

    /// It is gauranteed that public_ip will be Some if use_public_address is true
    pub(crate) async fn new(
        connect_ip: IpAddr,
        public_ip: Option<IpAddr>,
        private_ip: IpAddr,
        host_public_key_bytes: Vec<u8>,
        host_public_key: String,
        client_private_key: &str,
        network_interfaces: Vec<NetworkInterface>,
    ) -> Self {
        loop {
            let start = Instant::now();
            // We retry many times before we are able to succesfully make an ssh connection.
            // Each error is expected and so is logged as a `info!` that describes the underlying startup process that is supposed to cause the error.
            // A numbered comment is left before each `info!` to demonstrate the order each error occurs in.
            match tokio::time::timeout(
                Duration::from_secs(10),
                TcpStream::connect((connect_ip, 22)),
            )
            .await
            {
                Err(_) => {
                    // 1.
                    tracing::info!("Timed out connecting to {connect_ip} over ssh, the host is probably not accessible yet, retrying");
                    continue;
                }
                Ok(Err(e)) => {
                    // 2.
                    tracing::info!("failed to connect to {connect_ip}:22, the host probably hasnt started their ssh service yet, retrying, error was {e}");
                    tokio::time::sleep_until(start + Duration::from_secs(1)).await;
                    continue;
                }
                Ok(Ok(stream)) => {
                    match SshConnection::new(
                        stream,
                        connect_ip,
                        host_public_key_bytes.clone(),
                        client_private_key,
                    )
                    .await
                    {
                        Err(err) => {
                            // 3.
                            tracing::info!("Failed to make ssh connection to server, the host has probably not run its user-data script yet, retrying, error was: {err:?}");
                            tokio::time::sleep_until(start + Duration::from_secs(1)).await;
                            continue;
                        }
                        // 4. Then finally we have a working ssh connection.
                        Ok(ssh) => {
                            break Ec2Instance {
                                connect_ip,
                                ssh,
                                public_ip,
                                private_ip,
                                host_public_key_bytes,
                                host_public_key,
                                client_private_key: client_private_key.to_owned(),
                                network_interfaces,
                            };
                        }
                    };
                }
            };
        }
    }
}