| // Copyright 2018 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <arpa/inet.h> | |
| #include <fcntl.h> | |
| #include <stdint.h> | |
| #include <string.h> | |
| #include <iostream> | |
| #include <memory> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <base/at_exit.h> | |
| #include <base/check_op.h> | |
| #include <base/files/file_descriptor_watcher_posix.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/functional/bind.h> | |
| #include <base/logging.h> | |
| #include <base/memory/ref_counted.h> | |
| #include <base/message_loop/message_pump_type.h> | |
| #include <base/notreached.h> | |
| #include <base/run_loop.h> | |
| #include <base/strings/string_number_conversions.h> | |
| #include <base/strings/string_split.h> | |
| #include <base/strings/stringprintf.h> | |
| #include <base/system/sys_info.h> | |
| #include <base/task/single_thread_task_executor.h> | |
| #include <base/task/single_thread_task_runner.h> | |
| #include <brillo/flag_helper.h> | |
| #include <brillo/syslog_logging.h> | |
| #include <chromeos/dbus/service_constants.h> | |
| #include <dbus/bus.h> | |
| #include <dbus/message.h> | |
| #include <dbus/object_path.h> | |
| #include <dbus/object_proxy.h> | |
| #include <vm_cicerone/cicerone_service.pb.h> | |
| using std::string; | |
| namespace { | |
| constexpr int kDefaultTimeoutMs = 120 * 1000; | |
| void OnSignalConnected(const std::string& interface_name, | |
| const std::string& signal_name, | |
| bool is_connected) { | |
| LOG(INFO) << "D-Bus signal " << (is_connected ? "" : "NOT ") | |
| << "connected for " << interface_name << ":" << signal_name; | |
| } | |
| void OnContainerCreatedCallback( | |
| base::RunLoop* run_loop, | |
| vm_tools::cicerone::LxdContainerCreatedSignal::Status* final_status, | |
| std::string* failure_reason, | |
| dbus::Signal* signal) { | |
| dbus::MessageReader reader(signal); | |
| vm_tools::cicerone::LxdContainerCreatedSignal lccs; | |
| if (!reader.PopArrayOfBytesAsProto(&lccs)) { | |
| LOG(ERROR) << "Failed parsing LxdContainerCreatedSignal proto"; | |
| return; | |
| } | |
| *final_status = lccs.status(); | |
| *failure_reason = lccs.failure_reason(); | |
| run_loop->Quit(); | |
| } | |
| void OnContainerStartingCallback( | |
| base::RunLoop* run_loop, | |
| vm_tools::cicerone::LxdContainerStartingSignal::Status* final_status, | |
| std::string* failure_reason, | |
| dbus::Signal* signal) { | |
| dbus::MessageReader reader(signal); | |
| vm_tools::cicerone::LxdContainerStartingSignal lcss; | |
| if (!reader.PopArrayOfBytesAsProto(&lcss)) { | |
| LOG(ERROR) << "Failed parsing LxdContainerStartingSignal proto"; | |
| return; | |
| } | |
| switch (lcss.status()) { | |
| case vm_tools::cicerone::LxdContainerStartingSignal::STARTED: | |
| case vm_tools::cicerone::LxdContainerStartingSignal::CANCELLED: | |
| case vm_tools::cicerone::LxdContainerStartingSignal::FAILED: | |
| *final_status = lcss.status(); | |
| *failure_reason = lcss.failure_reason(); | |
| run_loop->Quit(); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| int CreateLxdContainer(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id, | |
| string image_server, | |
| string image_alias, | |
| string rootfs_path, | |
| string metadata_path) { | |
| LOG(INFO) << "Creating LXD container"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kCreateLxdContainerMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::CreateLxdContainerRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| request.set_image_server(std::move(image_server)); | |
| request.set_image_alias(std::move(image_alias)); | |
| request.set_rootfs_path(std::move(rootfs_path)); | |
| request.set_metadata_path(std::move(metadata_path)); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode CreateLxdContainer protobuf"; | |
| return -1; | |
| } | |
| // RunLoop so we can monitor the D-Bus signals coming back to determine when | |
| // container creation has actually finished. | |
| base::RunLoop run_loop; | |
| vm_tools::cicerone::LxdContainerCreatedSignal::Status final_status = | |
| vm_tools::cicerone::LxdContainerCreatedSignal::UNKNOWN; | |
| std::string failure_reason = "Timed out waiting for reply"; | |
| proxy->ConnectToSignal(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kLxdContainerCreatedSignal, | |
| base::BindRepeating(&OnContainerCreatedCallback, | |
| base::Unretained(&run_loop), | |
| &final_status, &failure_reason), | |
| base::BindOnce(&OnSignalConnected)); | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::CreateLxdContainerResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| vm_tools::cicerone::CreateLxdContainerResponse::Status status = | |
| response.status(); | |
| if (status != vm_tools::cicerone::CreateLxdContainerResponse::CREATING && | |
| status != vm_tools::cicerone::CreateLxdContainerResponse::EXISTS) { | |
| LOG(ERROR) << "Failed to create LXD container: " | |
| << response.failure_reason(); | |
| return -1; | |
| } | |
| if (status == vm_tools::cicerone::CreateLxdContainerResponse::EXISTS) { | |
| LOG(INFO) << "Container " << container_name << " already existed"; | |
| return 0; | |
| } else { | |
| // Start the RunLoop which'll get the D-Bus signal callbacks and set the | |
| // final result for us. | |
| LOG(INFO) << "Waiting for D-Bus signal for container creation status"; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, run_loop.QuitClosure(), base::Minutes(10)); | |
| run_loop.Run(); | |
| if (final_status == | |
| vm_tools::cicerone::LxdContainerCreatedSignal::CREATED) { | |
| LOG(INFO) << "Created container " << container_name << " successfully"; | |
| return 0; | |
| } else { | |
| LOG(ERROR) << "Failed to create LXD container: " << failure_reason; | |
| return -1; | |
| } | |
| } | |
| } | |
| int StartLxdContainer(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id) { | |
| LOG(INFO) << "Starting LXD container"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kStartLxdContainerMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::StartLxdContainerRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode StartLxdContainer protobuf"; | |
| return -1; | |
| } | |
| // RunLoop so we can monitor the D-Bus signals coming back to determine when | |
| // starting the container has actually finished. | |
| base::RunLoop run_loop; | |
| vm_tools::cicerone::LxdContainerStartingSignal::Status final_status = | |
| vm_tools::cicerone::LxdContainerStartingSignal::UNKNOWN; | |
| std::string failure_reason = "Timed out waiting for reply"; | |
| proxy->ConnectToSignal(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kLxdContainerStartingSignal, | |
| base::BindRepeating(&OnContainerStartingCallback, | |
| base::Unretained(&run_loop), | |
| &final_status, &failure_reason), | |
| base::BindOnce(&OnSignalConnected)); | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::StartLxdContainerResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| vm_tools::cicerone::StartLxdContainerResponse::Status status = | |
| response.status(); | |
| if (status == vm_tools::cicerone::StartLxdContainerResponse::FAILED || | |
| status == vm_tools::cicerone::StartLxdContainerResponse::UNKNOWN) { | |
| LOG(ERROR) << "Failed to start LXD container: " | |
| << response.failure_reason(); | |
| return -1; | |
| } | |
| if (status == vm_tools::cicerone::StartLxdContainerResponse::RUNNING) { | |
| LOG(INFO) << "Container " << container_name << " already running"; | |
| return 0; | |
| } else { | |
| if (status == vm_tools::cicerone::StartLxdContainerResponse::REMAPPING) { | |
| LOG(INFO) << "Container is remapping filesystem; this can take a while"; | |
| } else { | |
| DCHECK_EQ(status, | |
| vm_tools::cicerone::StartLxdContainerResponse::STARTING); | |
| } | |
| // Start the RunLoop which'll get the D-Bus signal callbacks and set the | |
| // final result for us. | |
| LOG(INFO) << "Waiting for D-Bus signal for container start status"; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, run_loop.QuitClosure(), base::Minutes(10)); | |
| run_loop.Run(); | |
| if (final_status == | |
| vm_tools::cicerone::LxdContainerStartingSignal::STARTED) { | |
| LOG(INFO) << "Started container " << container_name << " successfully"; | |
| return 0; | |
| } else { | |
| LOG(ERROR) << "Failed to start LXD container: " << failure_reason; | |
| return -1; | |
| } | |
| LOG(INFO) << "Started container: " << container_name; | |
| return 0; | |
| } | |
| } | |
| int SetTimezone(dbus::ObjectProxy* proxy, const string& timezone_name) { | |
| LOG(INFO) << "Setting timezone for VMs to " << timezone_name; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kSetTimezoneMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::SetTimezoneRequest request; | |
| request.set_timezone_name(timezone_name); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode SetTimezone protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::SetTimezoneResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Successfully set timezone for " << response.successes() | |
| << " containers to " << timezone_name; | |
| int failure_count = response.failure_reasons_size(); | |
| if (failure_count != 0) { | |
| LOG(ERROR) << "Received " << failure_count << " failures:"; | |
| for (int i = 0; i < failure_count; i++) { | |
| LOG(ERROR) << response.failure_reasons(i); | |
| } | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| int GetLxdContainerUsername(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id) { | |
| LOG(INFO) << "Getting LXD container primary username"; | |
| dbus::MethodCall method_call( | |
| vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kGetLxdContainerUsernameMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::GetLxdContainerUsernameRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode GetLxdContainerUsernameRequest protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::GetLxdContainerUsernameResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| vm_tools::cicerone::GetLxdContainerUsernameResponse::Status status = | |
| response.status(); | |
| if (status != vm_tools::cicerone::GetLxdContainerUsernameResponse::SUCCESS) { | |
| LOG(ERROR) << "Failed to get primary username: " | |
| << response.failure_reason(); | |
| return -1; | |
| } | |
| LOG(INFO) << "Container primary user is: " << response.username(); | |
| return 0; | |
| } | |
| int SetUpLxdContainerUser(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id, | |
| string container_username) { | |
| LOG(INFO) << "Setting up LXD container user"; | |
| dbus::MethodCall method_call( | |
| vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kSetUpLxdContainerUserMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::SetUpLxdContainerUserRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| request.set_container_username(std::move(container_username)); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode SetUpLxdContainerUser protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::SetUpLxdContainerUserResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| vm_tools::cicerone::SetUpLxdContainerUserResponse::Status status = | |
| response.status(); | |
| if (status != vm_tools::cicerone::SetUpLxdContainerUserResponse::EXISTS && | |
| status != vm_tools::cicerone::SetUpLxdContainerUserResponse::SUCCESS) { | |
| LOG(ERROR) << "Failed to set up user: " << response.failure_reason(); | |
| return -1; | |
| } | |
| if (status == vm_tools::cicerone::SetUpLxdContainerUserResponse::EXISTS) { | |
| LOG(INFO) << "Container user already exists"; | |
| return 0; | |
| } else { | |
| LOG(INFO) << "Created user in container"; | |
| return 0; | |
| } | |
| } | |
| int LaunchApplication(dbus::ObjectProxy* proxy, | |
| string owner_id, | |
| string name, | |
| string container_name, | |
| string application) { | |
| if (application.empty()) { | |
| LOG(ERROR) << "--application is required"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Starting application " << application << " in '" << name << ":" | |
| << container_name << "'"; | |
| dbus::MethodCall method_call( | |
| vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kLaunchContainerApplicationMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::LaunchContainerApplicationRequest request; | |
| request.set_owner_id(owner_id); | |
| request.set_vm_name(name); | |
| request.set_container_name(container_name); | |
| request.set_desktop_file_id(application); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode LaunchContainerApplicationRequest protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::LaunchContainerApplicationResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| if (!response.success()) { | |
| LOG(ERROR) << "Failed to launch application: " << response.failure_reason(); | |
| return -1; | |
| } | |
| LOG(INFO) << "Launched application " << application << " in '" << name << ":" | |
| << container_name << "'"; | |
| return 0; | |
| } | |
| void Write(const std::string& output_filepath, const std::string& content) { | |
| int content_size = content.size(); | |
| if (content_size != base::WriteFile(base::FilePath(output_filepath), | |
| content.c_str(), content_size)) { | |
| LOG(ERROR) << "Failed to write to file " << output_filepath; | |
| } | |
| } | |
| int GetIcon(dbus::ObjectProxy* proxy, | |
| string owner_id, | |
| string name, | |
| string container_name, | |
| string application, | |
| int icon_size, | |
| int scale, | |
| string output_filepath) { | |
| if (application.empty()) { | |
| LOG(ERROR) << "--application is required"; | |
| return -1; | |
| } | |
| if (output_filepath.empty()) { | |
| LOG(ERROR) << "--output_filepath is required"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Getting icon for " << application << " in '" << name << ":" | |
| << container_name << "'"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kGetContainerAppIconMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::ContainerAppIconRequest request; | |
| request.set_owner_id(owner_id); | |
| request.set_vm_name(name); | |
| request.set_container_name(container_name); | |
| request.add_desktop_file_ids(application); | |
| request.set_size(icon_size); | |
| request.set_scale(scale); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode ContainerAppIconRequest protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::ContainerAppIconResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| // This should have up to one icon since the input has only one application | |
| // file ID. | |
| CHECK_LE(response.icons_size(), 1); | |
| for (vm_tools::cicerone::DesktopIcon icon : response.icons()) { | |
| if (!icon.icon().empty()) | |
| Write(output_filepath, icon.icon()); | |
| } | |
| return 0; | |
| } | |
| int GetInfo(dbus::ObjectProxy* proxy) { | |
| LOG(INFO) << "Getting information"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kGetDebugInformationMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::GetDebugInformationRequest request; | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode GetDebugInformationRequest protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::GetDebugInformationResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| std::cout << response.debug_information(); | |
| return 0; | |
| } | |
| int GetLinuxPackageInfo(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id, | |
| string file_path) { | |
| if (file_path.empty()) { | |
| LOG(ERROR) << "--file_path is required"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Getting Linux package info"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kGetLinuxPackageInfoMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::LinuxPackageInfoRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| request.set_file_path(file_path); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode LinuxPackageInfoRequest protobuf"; | |
| return -1; | |
| } | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::LinuxPackageInfoResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| if (!response.success()) { | |
| LOG(ERROR) << "Failure getting Linux package info: " | |
| << response.failure_reason(); | |
| return -1; | |
| } | |
| LOG(INFO) << "Linux package info for: " << file_path; | |
| LOG(INFO) << "Package ID: " << response.package_id(); | |
| LOG(INFO) << "License: " << response.license(); | |
| LOG(INFO) << "Description: " << response.description(); | |
| LOG(INFO) << "Project URL: " << response.project_url(); | |
| LOG(INFO) << "Size(bytes): " << response.size(); | |
| LOG(INFO) << "Summary: " << response.summary(); | |
| return 0; | |
| } | |
| void OnApplicationInstalledCallback( | |
| base::RunLoop* run_loop, | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal::Status* final_status, | |
| std::string* failure_details, | |
| dbus::Signal* signal) { | |
| dbus::MessageReader reader(signal); | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal ilpps; | |
| if (!reader.PopArrayOfBytesAsProto(&ilpps)) { | |
| LOG(ERROR) << "Failed parsing InstallLinuxPackageProgressSignal proto"; | |
| return; | |
| } | |
| if (ilpps.status() == | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal::DOWNLOADING) { | |
| LOG(INFO) << "Downloading install packages, progress: " | |
| << ilpps.progress_percent(); | |
| return; | |
| } else if (ilpps.status() == | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal:: | |
| INSTALLING) { | |
| LOG(INFO) << "Installing packages, progress: " << ilpps.progress_percent(); | |
| return; | |
| } | |
| *final_status = ilpps.status(); | |
| *failure_details = ilpps.failure_details(); | |
| run_loop->Quit(); | |
| } | |
| int InstallLinuxPackage(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id, | |
| string file_path) { | |
| if (file_path.empty()) { | |
| LOG(ERROR) << "--file_path is required"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Installing Linux package"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kInstallLinuxPackageMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::InstallLinuxPackageRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| request.set_file_path(std::move(file_path)); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode InstallLinuxPackageRequest protobuf"; | |
| return -1; | |
| } | |
| // RunLoop so we can monitor the D-Bus signals coming back to determine when | |
| // application install has actually finished. | |
| base::RunLoop run_loop; | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal::Status final_status = | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal::FAILED; | |
| std::string failure_details = "Timed out waiting for reply"; | |
| proxy->ConnectToSignal(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kInstallLinuxPackageProgressSignal, | |
| base::BindRepeating(&OnApplicationInstalledCallback, | |
| base::Unretained(&run_loop), | |
| &final_status, &failure_details), | |
| base::BindOnce(&OnSignalConnected)); | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::InstallLinuxPackageResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| switch (response.status()) { | |
| case vm_tools::cicerone::InstallLinuxPackageResponse::STARTED: | |
| LOG(INFO) << "Successfully started the package install"; | |
| // Start the RunLoop which'll get the D-Bus signal callbacks and set the | |
| // final result for us. | |
| LOG(INFO) << "Waiting for D-Bus signal for application install status"; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, run_loop.QuitClosure(), base::Minutes(10)); | |
| run_loop.Run(); | |
| if (final_status == | |
| vm_tools::cicerone::InstallLinuxPackageProgressSignal::SUCCEEDED) { | |
| LOG(INFO) << "Finished application install successfully"; | |
| return 0; | |
| } else { | |
| LOG(ERROR) << "Failed to install application: " << failure_details; | |
| return -1; | |
| } | |
| case vm_tools::cicerone::InstallLinuxPackageResponse:: | |
| INSTALL_ALREADY_ACTIVE: | |
| LOG(ERROR) << "Failed starting the package install because one is " | |
| "already active"; | |
| return -1; | |
| default: | |
| LOG(ERROR) << "Failed starting the package install, reason: " | |
| << response.failure_reason(); | |
| return -1; | |
| } | |
| } | |
| void OnApplicationUninstalledCallback( | |
| base::RunLoop* run_loop, | |
| vm_tools::cicerone::UninstallPackageProgressSignal::Status* final_status, | |
| std::string* failure_details, | |
| dbus::Signal* signal) { | |
| dbus::MessageReader reader(signal); | |
| vm_tools::cicerone::UninstallPackageProgressSignal progress_signal; | |
| if (!reader.PopArrayOfBytesAsProto(&progress_signal)) { | |
| LOG(ERROR) << "Failed parsing UninstallPackageProgressSignal proto"; | |
| return; | |
| } | |
| if (progress_signal.status() == | |
| vm_tools::cicerone::UninstallPackageProgressSignal::UNINSTALLING) { | |
| LOG(INFO) << "Uninstall packages, progress: " | |
| << progress_signal.progress_percent(); | |
| return; | |
| } | |
| *final_status = progress_signal.status(); | |
| *failure_details = progress_signal.failure_details(); | |
| run_loop->Quit(); | |
| } | |
| int UninstallApplication(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id, | |
| const string& application) { | |
| if (application.empty()) { | |
| LOG(ERROR) << "--application is required"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Uninstalling application"; | |
| dbus::MethodCall method_call( | |
| vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kUninstallPackageOwningFileMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::UninstallPackageOwningFileRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| request.set_desktop_file_id(application); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode UninstallPackageOwningFileRequest protobuf"; | |
| return -1; | |
| } | |
| // RunLoop so we can monitor the D-Bus signals coming back to determine when | |
| // application uninstall has actually finished. | |
| base::RunLoop run_loop; | |
| vm_tools::cicerone::UninstallPackageProgressSignal::Status final_status = | |
| vm_tools::cicerone::UninstallPackageProgressSignal::FAILED; | |
| std::string failure_details = "Timed out waiting for reply"; | |
| proxy->ConnectToSignal(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kUninstallPackageProgressSignal, | |
| base::BindRepeating(&OnApplicationUninstalledCallback, | |
| base::Unretained(&run_loop), | |
| &final_status, &failure_details), | |
| base::BindOnce(&OnSignalConnected)); | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::UninstallPackageOwningFileResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| switch (response.status()) { | |
| case vm_tools::cicerone::UninstallPackageOwningFileResponse::STARTED: | |
| LOG(INFO) << "Successfully started the package uninstall"; | |
| // Start the RunLoop which'll get the D-Bus signal callbacks and set the | |
| // final result for us. | |
| LOG(INFO) << "Waiting for D-Bus signal for application uninstall status"; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, run_loop.QuitClosure(), base::Minutes(10)); | |
| run_loop.Run(); | |
| if (final_status == | |
| vm_tools::cicerone::UninstallPackageProgressSignal::SUCCEEDED) { | |
| LOG(INFO) << "Finished application uninstall successfully"; | |
| return 0; | |
| } else { | |
| LOG(ERROR) << "Failed to uninstall application: " << failure_details; | |
| return -1; | |
| } | |
| case vm_tools::cicerone::UninstallPackageOwningFileResponse:: | |
| BLOCKING_OPERATION_IN_PROGRESS: | |
| LOG(ERROR) << "Failed starting the package uninstall because one is " | |
| "already active"; | |
| return -1; | |
| default: | |
| LOG(ERROR) << "Failed starting the package uninstall, reason: " | |
| << response.failure_reason(); | |
| return -1; | |
| } | |
| } | |
| void OnPlaybookAppliedCallback( | |
| base::RunLoop* run_loop, | |
| vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::Status* | |
| final_status, | |
| std::string* failure_details, | |
| dbus::Signal* signal) { | |
| dbus::MessageReader reader(signal); | |
| vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal progress_signal; | |
| if (!reader.PopArrayOfBytesAsProto(&progress_signal)) { | |
| LOG(ERROR) << "Failed parsing ApplyAnsiblePlaybookProgressSignal proto"; | |
| return; | |
| } | |
| if (progress_signal.status() == | |
| vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::IN_PROGRESS) { | |
| LOG(INFO) << "Applying playbook..."; | |
| return; | |
| } | |
| *final_status = progress_signal.status(); | |
| *failure_details = progress_signal.failure_details(); | |
| run_loop->Quit(); | |
| } | |
| int ApplyAnsiblePlaybook(dbus::ObjectProxy* proxy, | |
| const string& vm_name, | |
| const string& container_name, | |
| const string& owner_id, | |
| string playbook) { | |
| if (playbook.empty()) { | |
| LOG(ERROR) << "--playbook is required"; | |
| return -1; | |
| } | |
| LOG(INFO) << "Applying Ansible playbook"; | |
| dbus::MethodCall method_call(vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kApplyAnsiblePlaybookMethod); | |
| dbus::MessageWriter writer(&method_call); | |
| vm_tools::cicerone::ApplyAnsiblePlaybookRequest request; | |
| request.set_vm_name(vm_name); | |
| request.set_container_name(container_name); | |
| request.set_owner_id(owner_id); | |
| request.set_playbook(std::move(playbook)); | |
| if (!writer.AppendProtoAsArrayOfBytes(request)) { | |
| LOG(ERROR) << "Failed to encode ApplyAnsiblePlaybookRequest protobuf"; | |
| return -1; | |
| } | |
| // RunLoop so we can monitor the D-Bus signals coming back to determine when | |
| // playbook application has actually finished. | |
| base::RunLoop run_loop; | |
| vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::Status final_status = | |
| vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::FAILED; | |
| std::string failure_details = "Timed out waiting for reply"; | |
| proxy->ConnectToSignal( | |
| vm_tools::cicerone::kVmCiceroneInterface, | |
| vm_tools::cicerone::kApplyAnsiblePlaybookProgressSignal, | |
| base::BindRepeating(&OnPlaybookAppliedCallback, | |
| base::Unretained(&run_loop), &final_status, | |
| &failure_details), | |
| base::BindOnce(&OnSignalConnected)); | |
| std::unique_ptr<dbus::Response> dbus_response = | |
| proxy->CallMethodAndBlock(&method_call, kDefaultTimeoutMs) | |
| .value_or(nullptr); | |
| if (!dbus_response) { | |
| LOG(ERROR) << "Failed to send dbus message to cicerone service"; | |
| return -1; | |
| } | |
| dbus::MessageReader reader(dbus_response.get()); | |
| vm_tools::cicerone::ApplyAnsiblePlaybookResponse response; | |
| if (!reader.PopArrayOfBytesAsProto(&response)) { | |
| LOG(ERROR) << "Failed to parse response protobuf"; | |
| return -1; | |
| } | |
| switch (response.status()) { | |
| case vm_tools::cicerone::ApplyAnsiblePlaybookResponse::STARTED: | |
| LOG(INFO) << "Successfully started the playbook application"; | |
| // Start the RunLoop which'll get the D-Bus signal callbacks and set the | |
| // final result for us. | |
| LOG(INFO) << "Waiting for D-Bus signal for playbook application status"; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, run_loop.QuitClosure(), base::Minutes(10)); | |
| run_loop.Run(); | |
| if (final_status == | |
| vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::SUCCEEDED) { | |
| LOG(INFO) << "Finished playbook application successfully"; | |
| return 0; | |
| } | |
| LOG(ERROR) << "Failed to apply playbook: " << failure_details; | |
| return -1; | |
| case vm_tools::cicerone::ApplyAnsiblePlaybookResponse::FAILED: | |
| LOG(ERROR) << "Failed starting the playbook application, reason: " | |
| << response.failure_reason(); | |
| return -1; | |
| default: | |
| NOTREACHED(); | |
| return -1; | |
| } | |
| } | |
| } // namespace | |
| int main(int argc, char** argv) { | |
| base::AtExitManager at_exit; | |
| // Operations. | |
| DEFINE_bool(create_lxd_container, false, "Create an LXD container"); | |
| DEFINE_bool(start_lxd_container, false, "Start an LXD container"); | |
| DEFINE_bool(set_timezone, false, "Set timezone for all known LXD containers"); | |
| DEFINE_bool(get_username, false, "Get the primary username in a container"); | |
| DEFINE_bool(set_up_lxd_user, false, "Set up a user in an LXD container"); | |
| DEFINE_bool(launch_application, false, | |
| "Launches an application in a container"); | |
| DEFINE_bool(get_icon, false, "Get an app icon from a container within a VM"); | |
| DEFINE_bool(get_info, false, "Get debug information about all running VMs"); | |
| DEFINE_bool(install_package, false, "Install a Linux package file"); | |
| DEFINE_bool(uninstall_application, false, "Uninstall an application"); | |
| DEFINE_bool(package_info, false, "Gets information on a Linux package file"); | |
| DEFINE_bool(apply_playbook, false, "Apply an Ansible playbook"); | |
| // Parameters. | |
| DEFINE_string(vm_name, "", "VM name"); | |
| DEFINE_string(container_name, "", "Container name"); | |
| DEFINE_string(owner_id, "", "User id"); | |
| DEFINE_string(image_server, "", "Image server to pull a container from"); | |
| DEFINE_string(image_alias, "", "Container image alias"); | |
| DEFINE_string(rootfs_path, "", "Path to rootfs tarball"); | |
| DEFINE_string(metadata_path, "", "Path to metadata tarball"); | |
| DEFINE_string(container_username, "", "Container username"); | |
| DEFINE_string(application, "", "Name of the application to launch"); | |
| DEFINE_string(output_filepath, "", | |
| "Filename with path to write appliction icon to"); | |
| DEFINE_string(timezone_name, "", | |
| "The timezone to set, for example 'America/Denver'. " | |
| "See /usr/share/zoneinfo for other valid names."); | |
| DEFINE_int32(icon_size, 48, | |
| "The size of the icon to get is this icon_size by icon_size"); | |
| DEFINE_int32(scale, 1, "The scale that the icon is designed to use with"); | |
| DEFINE_string(file_path, "", "Package file path"); | |
| DEFINE_string(playbook, "", "Ansible playbook content"); | |
| brillo::FlagHelper::Init(argc, argv, "vm_cicerone client tool"); | |
| brillo::InitLog(brillo::kLogToStderrIfTty); | |
| base::SingleThreadTaskExecutor task_executor(base::MessagePumpType::IO); | |
| base::FileDescriptorWatcher watcher(task_executor.task_runner()); | |
| dbus::Bus::Options opts; | |
| opts.bus_type = dbus::Bus::SYSTEM; | |
| scoped_refptr<dbus::Bus> bus(new dbus::Bus(std::move(opts))); | |
| if (!bus->Connect()) { | |
| LOG(ERROR) << "Failed to connect to system bus"; | |
| return -1; | |
| } | |
| dbus::ObjectProxy* proxy = bus->GetObjectProxy( | |
| vm_tools::cicerone::kVmCiceroneServiceName, | |
| dbus::ObjectPath(vm_tools::cicerone::kVmCiceroneServicePath)); | |
| if (!proxy) { | |
| LOG(ERROR) << "Unable to get dbus proxy for " | |
| << vm_tools::cicerone::kVmCiceroneServiceName; | |
| return -1; | |
| } | |
| // The standard says that bool to int conversion is implicit and that | |
| // false => 0 and true => 1. | |
| // clang-format off | |
| if (FLAGS_create_lxd_container + FLAGS_start_lxd_container + | |
| FLAGS_set_timezone + FLAGS_set_up_lxd_user + FLAGS_get_username + | |
| FLAGS_launch_application + FLAGS_get_icon + FLAGS_get_info + | |
| FLAGS_install_package + FLAGS_uninstall_application + | |
| FLAGS_package_info + FLAGS_apply_playbook != 1) { | |
| // clang-format on | |
| LOG(ERROR) << "Exactly one of --create_lxd_container, " | |
| << "--start_lxd_container, --set_up_lxd_user, --set_timezone, " | |
| << "--get_username, --launch_application, --get_icon, " | |
| << "--get_info, --install_package, --uninstall_application, " | |
| << "--apply_playbook, or --package_info must be provided"; | |
| return -1; | |
| } | |
| // Check for the get_info and set_timezone commands early because they do | |
| // not require owner ID, VM name, or container name. | |
| if (FLAGS_get_info) { | |
| return GetInfo(proxy); | |
| } | |
| if (FLAGS_set_timezone) { | |
| return SetTimezone(proxy, FLAGS_timezone_name); | |
| } | |
| // Every other D-Bus method for cicerone requires owner ID, VM name, and | |
| // container name. | |
| if (FLAGS_owner_id.empty()) { | |
| LOG(ERROR) << "--owner_id is required"; | |
| return -1; | |
| } | |
| if (FLAGS_vm_name.empty()) { | |
| LOG(ERROR) << "--vm_name is required"; | |
| return -1; | |
| } | |
| if (FLAGS_container_name.empty()) { | |
| LOG(ERROR) << "--container_name is required"; | |
| return -1; | |
| } | |
| if (FLAGS_create_lxd_container) { | |
| return CreateLxdContainer( | |
| proxy, FLAGS_vm_name, FLAGS_container_name, FLAGS_owner_id, | |
| std::move(FLAGS_image_server), std::move(FLAGS_image_alias), | |
| std::move(FLAGS_rootfs_path), std::move(FLAGS_metadata_path)); | |
| } else if (FLAGS_start_lxd_container) { | |
| return StartLxdContainer(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id); | |
| } else if (FLAGS_set_up_lxd_user) { | |
| return SetUpLxdContainerUser(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id, | |
| std::move(FLAGS_container_username)); | |
| } else if (FLAGS_get_username) { | |
| return GetLxdContainerUsername(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id); | |
| } else if (FLAGS_launch_application) { | |
| return LaunchApplication( | |
| proxy, std::move(FLAGS_owner_id), std::move(FLAGS_vm_name), | |
| std::move(FLAGS_container_name), std::move(FLAGS_application)); | |
| } else if (FLAGS_get_icon) { | |
| return GetIcon(proxy, std::move(FLAGS_owner_id), std::move(FLAGS_vm_name), | |
| std::move(FLAGS_container_name), | |
| std::move(FLAGS_application), FLAGS_icon_size, FLAGS_scale, | |
| std::move(FLAGS_output_filepath)); | |
| } else if (FLAGS_install_package) { | |
| return InstallLinuxPackage(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id, std::move(FLAGS_file_path)); | |
| } else if (FLAGS_uninstall_application) { | |
| return UninstallApplication(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id, FLAGS_application); | |
| } else if (FLAGS_package_info) { | |
| return GetLinuxPackageInfo(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id, std::move(FLAGS_file_path)); | |
| } else if (FLAGS_apply_playbook) { | |
| return ApplyAnsiblePlaybook(proxy, FLAGS_vm_name, FLAGS_container_name, | |
| FLAGS_owner_id, std::move(FLAGS_playbook)); | |
| } | |
| // Unreachable. | |
| return 0; | |
| } |