blob: 78101f64c80e2bc4f7979cddb282738fb55e2757 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "vm_tools/garcon/ansible_playbook_application.h"
#include <errno.h>
#include <fcntl.h>
#include <map>
#include <sstream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/location.h>
#include <base/posix/safe_strerror.h>
#include <base/synchronization/waitable_event.h>
#include <base/threading/thread_task_runner_handle.h>
#include "vm_tools/common/spawn_util.h"
namespace vm_tools {
namespace garcon {
namespace {
constexpr char kStdoutCallbackEnv[] = "ANSIBLE_STDOUT_CALLBACK";
constexpr char kDefaultCallbackPluginPathEnv[] = "ANSIBLE_CALLBACK_PLUGINS";
constexpr char kStdoutCallbackName[] = "garcon";
constexpr char kDefaultCallbackPluginPath[] =
"/usr/share/ansible/plugins/callback";
// How long we should wait for a ansible-playbook process to finish.
constexpr base::TimeDelta kAnsibleProcessTimeout =
base::TimeDelta::FromHours(1);
bool CreatePipe(base::ScopedFD* read_fd,
base::ScopedFD* write_fd,
std::string* error_msg) {
int fds[2];
if (pipe2(fds, O_CLOEXEC) < 0) {
*error_msg =
"Failed to open target process pipe: " + base::safe_strerror(errno);
return false;
}
read_fd->reset(fds[0]);
write_fd->reset(fds[1]);
return true;
}
} // namespace
AnsiblePlaybookApplication::AnsiblePlaybookApplication()
: task_runner_(base::ThreadTaskRunnerHandle::Get()),
weak_ptr_factory_(this) {}
void AnsiblePlaybookApplication::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void AnsiblePlaybookApplication::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
base::FilePath AnsiblePlaybookApplication::CreateAnsiblePlaybookFile(
const std::string& playbook, std::string* error_msg) {
base::FilePath ansible_dir;
bool success = base::CreateNewTempDirectory("", &ansible_dir);
if (!success) {
*error_msg = "Failed to create directory for ansible playbook file";
return base::FilePath();
}
const base::FilePath ansible_playbook_file_path =
ansible_dir.Append("playbook.yaml");
base::File ansible_playbook_file(
ansible_playbook_file_path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
if (!ansible_playbook_file.created()) {
*error_msg = "Failed to create file for Ansible playbook";
return base::FilePath();
}
if (!ansible_playbook_file.IsValid()) {
*error_msg = "Failed to create valid file for Ansible playbook";
return base::FilePath();
}
int bytes = ansible_playbook_file.WriteAtCurrentPos(playbook.c_str(),
playbook.length());
if (bytes != playbook.length()) {
*error_msg = "Failed to write Ansible playbook content to file";
return base::FilePath();
}
return ansible_playbook_file_path;
}
bool AnsiblePlaybookApplication::ExecuteAnsiblePlaybook(
const base::FilePath& ansible_playbook_file_path, std::string* error_msg) {
std::vector<std::string> argv{"ansible-playbook",
"--become",
"--connection=local",
"--inventory",
"127.0.0.1,",
ansible_playbook_file_path.value(),
"-e",
"ansible_python_interpreter=/usr/bin/python3"};
std::map<std::string, std::string> env;
env[kStdoutCallbackEnv] = kStdoutCallbackName;
env[kDefaultCallbackPluginPathEnv] = kDefaultCallbackPluginPath;
// Set child's process stdout and stderr to write end of pipes.
int stdio_fd[] = {-1, -1, -1};
if (!CreatePipe(&read_stdout_, &write_stdout_, error_msg)) {
return false;
}
if (!CreatePipe(&read_stderr_, &write_stderr_, error_msg)) {
return false;
}
stdio_fd[STDOUT_FILENO] = write_stdout_.get();
stdio_fd[STDERR_FILENO] = write_stderr_.get();
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED);
bool success = task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&AnsiblePlaybookApplication::SetUpStdIOWatchers,
weak_ptr_factory_.GetWeakPtr(), &event, error_msg));
event.Wait();
if (!success) {
*error_msg = "Failed to post task to set up ansible stdio watchers";
return false;
}
if (!error_msg->empty()) {
return false;
}
pid_t spawned_pid;
if (!Spawn(std::move(argv), std::move(env), "", stdio_fd, &spawned_pid)) {
*error_msg = "Failed to spawn ansible-playbook process";
return false;
}
// As we rely on ansible process to finish and close fds, we set up a timeout
// after which process is killed.
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AnsiblePlaybookApplication::KillAnsibleProcess,
weak_ptr_factory_.GetWeakPtr(), spawned_pid),
kAnsibleProcessTimeout);
ClearWriteFDs();
return true;
}
void AnsiblePlaybookApplication::SetUpStdIOWatchers(base::WaitableEvent* event,
std::string* error_msg) {
stdout_watcher_ = base::FileDescriptorWatcher::WatchReadable(
read_stdout_.get(),
base::BindRepeating(&AnsiblePlaybookApplication::OnStdoutReadable,
weak_ptr_factory_.GetWeakPtr()));
if (!stdout_watcher_) {
*error_msg = "Failed to set watcher for ansible-playbook stdout";
event->Signal();
return;
}
stderr_watcher_ = base::FileDescriptorWatcher::WatchReadable(
read_stderr_.get(),
base::BindRepeating(&AnsiblePlaybookApplication::OnStderrReadable,
weak_ptr_factory_.GetWeakPtr()));
if (!stderr_watcher_) {
*error_msg = "Failed to set watcher for ansible-playbook stderr";
event->Signal();
return;
}
event->Signal();
return;
}
void AnsiblePlaybookApplication::OnStdoutReadable() {
char buffer[100];
ssize_t count = read(read_stdout_.get(), buffer, sizeof(buffer));
if (count <= 0) {
stdout_watcher_.reset();
OnStdIOProcessed(false /*is_stderr*/);
return;
}
stdout_.write(buffer, count);
return;
}
void AnsiblePlaybookApplication::OnStderrReadable() {
char buffer[100];
ssize_t count = read(read_stderr_.get(), buffer, sizeof(buffer));
if (count <= 0) {
stderr_watcher_.reset();
OnStdIOProcessed(true /*is_stderr*/);
return;
}
stderr_.write(buffer, count);
return;
}
void AnsiblePlaybookApplication::OnStdIOProcessed(bool is_stderr) {
if (is_stderr)
is_stderr_finished_ = true;
else
is_stdout_finished_ = true;
if (is_stderr_finished_ && is_stdout_finished_) {
std::string failure_reason;
bool success = GetPlaybookApplicationResult(&failure_reason);
for (auto& observer : observers_)
observer.OnApplyAnsiblePlaybookCompletion(success, failure_reason);
}
}
bool AnsiblePlaybookApplication::GetPlaybookApplicationResult(
std::string* failure_reason) {
const std::string stdout = stdout_.str();
const std::string stderr = stderr_.str();
const std::string execution_info =
"Ansible playbook application stdout:\n" + stdout + "\n" +
"Ansible playbook application stderr:\n" + stderr + "\n";
if (stdout.find("MESSAGE TO GARCON: TASK_FAILED") != std::string::npos) {
LOG(INFO) << "Some tasks failed during container configuration";
*failure_reason = execution_info;
return false;
}
if (!stderr.empty()) {
*failure_reason = execution_info;
return false;
}
return true;
}
void AnsiblePlaybookApplication::ClearWriteFDs() {
write_stdout_.reset();
write_stderr_.reset();
}
void AnsiblePlaybookApplication::KillAnsibleProcess(pid_t pid) {
if (kill(pid, SIGTERM) < 0) {
LOG(ERROR) << "Failed to kill ansible process: "
<< base::safe_strerror(errno);
}
for (auto& observer : observers_)
observer.OnApplyAnsiblePlaybookCompletion(false /*success*/,
"ansible process timed out");
}
} // namespace garcon
} // namespace vm_tools