1 //
2 // Copyright (C) 2022 The Android Open Source Project
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15
16 #include "host/commands/test_gce_driver/scoped_instance.h"
17
18 #include <netinet/ip.h>
19
20 #include <random>
21 #include <sstream>
22 #include <string>
23
24 #include <android-base/file.h>
25
26 #include "common/libs/fs/shared_buf.h"
27 #include "common/libs/utils/result.h"
28 #include "host/commands/test_gce_driver/gce_api.h"
29 #include "host/commands/test_gce_driver/key_pair.h"
30
31 namespace cuttlefish {
32
PrivKey(const std::string & privkey_path)33 SshCommand& SshCommand::PrivKey(const std::string& privkey_path) & {
34 privkey_path_ = privkey_path;
35 return *this;
36 }
PrivKey(const std::string & privkey_path)37 SshCommand SshCommand::PrivKey(const std::string& privkey_path) && {
38 privkey_path_ = privkey_path;
39 return *this;
40 }
41
WithoutKnownHosts()42 SshCommand& SshCommand::WithoutKnownHosts() & {
43 without_known_hosts_ = true;
44 return *this;
45 }
WithoutKnownHosts()46 SshCommand SshCommand::WithoutKnownHosts() && {
47 without_known_hosts_ = true;
48 return *this;
49 }
50
Username(const std::string & username)51 SshCommand& SshCommand::Username(const std::string& username) & {
52 username_ = username;
53 return *this;
54 }
Username(const std::string & username)55 SshCommand SshCommand::Username(const std::string& username) && {
56 username_ = username;
57 return *this;
58 }
59
Host(const std::string & host)60 SshCommand& SshCommand::Host(const std::string& host) & {
61 host_ = host;
62 return *this;
63 }
Host(const std::string & host)64 SshCommand SshCommand::Host(const std::string& host) && {
65 host_ = host;
66 return *this;
67 }
68
RemotePortForward(uint16_t remote,uint16_t local)69 SshCommand& SshCommand::RemotePortForward(uint16_t remote, uint16_t local) & {
70 remote_port_forwards_.push_back({remote, local});
71 return *this;
72 }
RemotePortForward(uint16_t remote,uint16_t local)73 SshCommand SshCommand::RemotePortForward(uint16_t remote, uint16_t local) && {
74 remote_port_forwards_.push_back({remote, local});
75 return *this;
76 }
77
RemoteParameter(const std::string & param)78 SshCommand& SshCommand::RemoteParameter(const std::string& param) & {
79 parameters_.push_back(param);
80 return *this;
81 }
RemoteParameter(const std::string & param)82 SshCommand SshCommand::RemoteParameter(const std::string& param) && {
83 parameters_.push_back(param);
84 return *this;
85 }
86
Build() const87 Command SshCommand::Build() const {
88 Command remote_cmd{"/usr/bin/ssh"};
89 if (privkey_path_) {
90 remote_cmd.AddParameter("-i");
91 remote_cmd.AddParameter(*privkey_path_);
92 }
93 if (without_known_hosts_) {
94 remote_cmd.AddParameter("-o");
95 remote_cmd.AddParameter("StrictHostKeyChecking=no");
96 remote_cmd.AddParameter("-o");
97 remote_cmd.AddParameter("UserKnownHostsFile=/dev/null");
98 }
99 for (const auto& fwd : remote_port_forwards_) {
100 remote_cmd.AddParameter("-R");
101 remote_cmd.AddParameter(fwd.remote_port, ":127.0.0.1:", fwd.local_port);
102 }
103 if (host_) {
104 remote_cmd.AddParameter(username_ ? *username_ + "@" : "", *host_);
105 }
106 for (const auto& param : parameters_) {
107 remote_cmd.AddParameter(param);
108 }
109 return remote_cmd;
110 }
111
CreateDefault(GceApi & gce,const std::string & zone,const std::string & instance_name,bool internal)112 Result<std::unique_ptr<ScopedGceInstance>> ScopedGceInstance::CreateDefault(
113 GceApi& gce, const std::string& zone, const std::string& instance_name,
114 bool internal) {
115 auto ssh_key =
116 CF_EXPECT(KeyPair::CreateRsa(4096), "Could not create ssh key pair");
117 auto ssh_pubkey =
118 CF_EXPECT(ssh_key->OpenSshPublicKey(), "Could get openssh format key: ");
119
120 // TODO(schuffelen): Pass this through more layers to make it more general.
121 auto network_interface = GceNetworkInterface::Default();
122 if (internal) {
123 network_interface.Network(
124 "https://www.googleapis.com/compute/v1/projects/android-treehugger/"
125 "global/networks/cloud-tf-vpc");
126 network_interface.Subnetwork(
127 "https://www.googleapis.com/compute/v1/projects/android-treehugger/"
128 "regions/us-west1/subnetworks/cloud-tf-vpc");
129 }
130
131 auto default_instance_info =
132 GceInstanceInfo()
133 .Name(instance_name)
134 .Zone(zone)
135 .MachineType("zones/us-west1-a/machineTypes/n1-standard-4")
136 .AddMetadata("ssh-keys", "vsoc-01:" + ssh_pubkey)
137 .AddNetworkInterface(std::move(network_interface))
138 .AddDisk(
139 GceInstanceDisk::EphemeralBootDisk()
140 .SourceImage(
141 "projects/cloud-android-releases/global/images/family/"
142 "cuttlefish-google")
143 .SizeGb(30))
144 .AddScope("https://www.googleapis.com/auth/androidbuild.internal")
145 .AddScope("https://www.googleapis.com/auth/devstorage.read_only")
146 .AddScope("https://www.googleapis.com/auth/logging.write");
147
148 CF_EXPECT(gce.Insert(default_instance_info).Future().get(),
149 "Failed to create instance");
150
151 auto privkey = CF_EXPECT(ssh_key->PemPrivateKey());
152 std::unique_ptr<TemporaryFile> privkey_file(CF_EXPECT(new TemporaryFile()));
153 auto fd_dup = SharedFD::Dup(privkey_file->fd);
154 CF_EXPECT(fd_dup->IsOpen());
155 CF_EXPECT(WriteAll(fd_dup, privkey) == privkey.size());
156 fd_dup->Close();
157
158 std::unique_ptr<ScopedGceInstance> instance(new ScopedGceInstance(
159 gce, default_instance_info, std::move(privkey_file), internal));
160
161 auto created_info = CF_EXPECT(gce.Get(default_instance_info).get(),
162 "Failed to get instance info: ");
163
164 CF_EXPECT(instance->EnforceSshReady(), "Failed to access SSH on instance");
165 return instance;
166 }
167
EnforceSshReady()168 Result<void> ScopedGceInstance::EnforceSshReady() {
169 std::string out;
170 std::string err;
171 for (int i = 0; i < 100; i++) {
172 auto ssh = CF_EXPECT(Ssh(), "Failed to create ssh command");
173
174 ssh.RemoteParameter("ls");
175 ssh.RemoteParameter("/");
176 auto command = ssh.Build();
177
178 out = "";
179 err = "";
180 int ret = RunWithManagedStdio(std::move(command), nullptr, &out, &err);
181 if (ret == 0) {
182 return {};
183 }
184 }
185
186 return CF_ERR("Failed to ssh to the instance. stdout=\""
187 << out << "\", stderr = \"" << err << "\"");
188 }
189
ScopedGceInstance(GceApi & gce,const GceInstanceInfo & instance,std::unique_ptr<TemporaryFile> privkey,bool use_internal_address)190 ScopedGceInstance::ScopedGceInstance(GceApi& gce,
191 const GceInstanceInfo& instance,
192 std::unique_ptr<TemporaryFile> privkey,
193 bool use_internal_address)
194 : gce_(gce),
195 instance_(instance),
196 privkey_(std::move(privkey)),
197 use_internal_address_(use_internal_address) {}
198
~ScopedGceInstance()199 ScopedGceInstance::~ScopedGceInstance() {
200 auto delete_ins = gce_.Delete(instance_).Future().get();
201 if (!delete_ins.ok()) {
202 LOG(ERROR) << "Failed to delete instance: " << delete_ins.error().Message();
203 LOG(DEBUG) << "Failed to delete instance: " << delete_ins.error().Trace();
204 }
205 }
206
Ssh()207 Result<SshCommand> ScopedGceInstance::Ssh() {
208 const auto& network_interfaces = instance_.NetworkInterfaces();
209 CF_EXPECT(!network_interfaces.empty());
210 auto iface = network_interfaces[0];
211 auto ip = use_internal_address_ ? iface.InternalIp() : iface.ExternalIp();
212 CF_EXPECT(ip.has_value());
213 return SshCommand()
214 .PrivKey(privkey_->path)
215 .WithoutKnownHosts()
216 .Username("vsoc-01")
217 .Host(*ip);
218 }
219
220 } // namespace cuttlefish
221