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 #include <android-base/result.h>
26
27 #include "common/libs/fs/shared_buf.h"
28
29 using android::base::Error;
30 using android::base::Result;
31
32 namespace cuttlefish {
33
PrivKey(const std::string & privkey_path)34 SshCommand& SshCommand::PrivKey(const std::string& privkey_path) & {
35 privkey_path_ = privkey_path;
36 return *this;
37 }
PrivKey(const std::string & privkey_path)38 SshCommand SshCommand::PrivKey(const std::string& privkey_path) && {
39 privkey_path_ = privkey_path;
40 return *this;
41 }
42
WithoutKnownHosts()43 SshCommand& SshCommand::WithoutKnownHosts() & {
44 without_known_hosts_ = true;
45 return *this;
46 }
WithoutKnownHosts()47 SshCommand SshCommand::WithoutKnownHosts() && {
48 without_known_hosts_ = true;
49 return *this;
50 }
51
Username(const std::string & username)52 SshCommand& SshCommand::Username(const std::string& username) & {
53 username_ = username;
54 return *this;
55 }
Username(const std::string & username)56 SshCommand SshCommand::Username(const std::string& username) && {
57 username_ = username;
58 return *this;
59 }
60
Host(const std::string & host)61 SshCommand& SshCommand::Host(const std::string& host) & {
62 host_ = host;
63 return *this;
64 }
Host(const std::string & host)65 SshCommand SshCommand::Host(const std::string& host) && {
66 host_ = host;
67 return *this;
68 }
69
RemotePortForward(uint16_t remote,uint16_t local)70 SshCommand& SshCommand::RemotePortForward(uint16_t remote, uint16_t local) & {
71 remote_port_forwards_.push_back({remote, local});
72 return *this;
73 }
RemotePortForward(uint16_t remote,uint16_t local)74 SshCommand SshCommand::RemotePortForward(uint16_t remote, uint16_t local) && {
75 remote_port_forwards_.push_back({remote, local});
76 return *this;
77 }
78
RemoteParameter(const std::string & param)79 SshCommand& SshCommand::RemoteParameter(const std::string& param) & {
80 parameters_.push_back(param);
81 return *this;
82 }
RemoteParameter(const std::string & param)83 SshCommand SshCommand::RemoteParameter(const std::string& param) && {
84 parameters_.push_back(param);
85 return *this;
86 }
87
Build() const88 Command SshCommand::Build() const {
89 Command remote_cmd{"/usr/bin/ssh"};
90 if (privkey_path_) {
91 remote_cmd.AddParameter("-i");
92 remote_cmd.AddParameter(*privkey_path_);
93 }
94 if (without_known_hosts_) {
95 remote_cmd.AddParameter("-o");
96 remote_cmd.AddParameter("StrictHostKeyChecking=no");
97 remote_cmd.AddParameter("-o");
98 remote_cmd.AddParameter("UserKnownHostsFile=/dev/null");
99 }
100 for (const auto& fwd : remote_port_forwards_) {
101 remote_cmd.AddParameter("-R");
102 remote_cmd.AddParameter(fwd.remote_port, ":127.0.0.1:", fwd.local_port);
103 }
104 if (host_) {
105 remote_cmd.AddParameter(username_ ? *username_ + "@" : "", *host_);
106 }
107 for (const auto& param : parameters_) {
108 remote_cmd.AddParameter(param);
109 }
110 return remote_cmd;
111 }
112
CreateDefault(GceApi & gce,const std::string & zone,const std::string & instance_name,bool internal)113 Result<std::unique_ptr<ScopedGceInstance>> ScopedGceInstance::CreateDefault(
114 GceApi& gce, const std::string& zone, const std::string& instance_name,
115 bool internal) {
116 auto ssh_key = KeyPair::CreateRsa(4096);
117 if (!ssh_key.ok()) {
118 return Error() << "Could not create ssh key pair: " << ssh_key.error();
119 }
120
121 auto ssh_pubkey = (*ssh_key)->OpenSshPublicKey();
122 if (!ssh_pubkey.ok()) {
123 return Error() << "Could get openssh format key: " << ssh_pubkey.error();
124 }
125
126 auto default_instance_info =
127 GceInstanceInfo()
128 .Name(instance_name)
129 .Zone(zone)
130 .MachineType("zones/us-west1-a/machineTypes/n1-standard-4")
131 .AddMetadata("ssh-keys", "vsoc-01:" + *ssh_pubkey)
132 .AddNetworkInterface(GceNetworkInterface::Default())
133 .AddDisk(
134 GceInstanceDisk::EphemeralBootDisk()
135 .SourceImage(
136 "projects/cloud-android-releases/global/images/family/"
137 "cuttlefish-google")
138 .SizeGb(30))
139 .AddScope("https://www.googleapis.com/auth/androidbuild.internal")
140 .AddScope("https://www.googleapis.com/auth/devstorage.read_only")
141 .AddScope("https://www.googleapis.com/auth/logging.write");
142
143 auto creation = gce.Insert(default_instance_info).Future().get();
144 if (!creation.ok()) {
145 return Error() << "Failed to create instance: " << creation.error();
146 }
147
148 auto privkey = CF_EXPECT((*ssh_key)->PemPrivateKey());
149 std::unique_ptr<TemporaryFile> privkey_file(CF_EXPECT(new TemporaryFile()));
150 auto fd_dup = SharedFD::Dup(privkey_file->fd);
151 CF_EXPECT(fd_dup->IsOpen());
152 CF_EXPECT(WriteAll(fd_dup, privkey) == privkey.size());
153 fd_dup->Close();
154
155 std::unique_ptr<ScopedGceInstance> instance(new ScopedGceInstance(
156 gce, default_instance_info, std::move(privkey_file), internal));
157
158 auto created_info = gce.Get(default_instance_info).get();
159 if (!created_info.ok()) {
160 return Error() << "Failed to get instance info: " << created_info.error();
161 }
162 instance->instance_ = *created_info;
163
164 auto ssh_ready = instance->EnforceSshReady();
165 if (!ssh_ready.ok()) {
166 return Error() << "Failed to access SSH on instance: " << ssh_ready.error();
167 }
168 return instance;
169 }
170
EnforceSshReady()171 Result<void> ScopedGceInstance::EnforceSshReady() {
172 std::string out;
173 std::string err;
174 for (int i = 0; i < 100; i++) {
175 auto ssh = Ssh();
176 if (!ssh.ok()) {
177 return Error() << "Failed to create ssh command: " << ssh.error();
178 }
179
180 ssh->RemoteParameter("ls");
181 ssh->RemoteParameter("/");
182 auto command = ssh->Build();
183
184 out = "";
185 err = "";
186 int ret = RunWithManagedStdio(std::move(command), nullptr, &out, &err);
187 if (ret == 0) {
188 return {};
189 }
190 }
191
192 return Error() << "Failed to ssh to the instance. stdout=\"" << out
193 << "\", stderr = \"" << err << "\"";
194 }
195
ScopedGceInstance(GceApi & gce,const GceInstanceInfo & instance,std::unique_ptr<TemporaryFile> privkey,bool use_internal_address)196 ScopedGceInstance::ScopedGceInstance(GceApi& gce,
197 const GceInstanceInfo& instance,
198 std::unique_ptr<TemporaryFile> privkey,
199 bool use_internal_address)
200 : gce_(gce),
201 instance_(instance),
202 privkey_(std::move(privkey)),
203 use_internal_address_(use_internal_address) {}
204
~ScopedGceInstance()205 ScopedGceInstance::~ScopedGceInstance() {
206 auto delete_ins = gce_.Delete(instance_).Future().get();
207 if (!delete_ins.ok()) {
208 LOG(ERROR) << "Failed to delete instance: " << delete_ins.error();
209 }
210 }
211
Ssh()212 Result<SshCommand> ScopedGceInstance::Ssh() {
213 const auto& network_interfaces = instance_.NetworkInterfaces();
214 CF_EXPECT(!network_interfaces.empty());
215 auto iface = network_interfaces[0];
216 auto ip = use_internal_address_ ? iface.InternalIp() : iface.ExternalIp();
217 CF_EXPECT(ip.has_value());
218 return SshCommand()
219 .PrivKey(privkey_->path)
220 .WithoutKnownHosts()
221 .Username("vsoc-01")
222 .Host(*ip);
223 }
224
225 } // namespace cuttlefish
226