blob: 6e43b928993ad9c7d24fcb8c8a58942bdb37543f [file] [log] [blame] [edit]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "hoth_updater_cli.hpp"
#include "google3/host_commands.h"
#include "message_util.hpp"
#include <boost/endian/arithmetic.hpp>
#include <sdbusplus/bus.hpp>
#include <sdbusplus/message.hpp>
#include <stdplus/print.hpp>
#include <stdplus/raw.hpp>
#include <chrono>
#include <exception>
#include <format>
#include <fstream>
#include <functional>
#include <iostream>
#include <iterator>
#include <span>
#include <string_view>
#include <thread>
#include <variant>
#include <vector>
using namespace std::chrono_literals;
namespace google::hoth::tools
{
namespace
{
// Normal update time:
// root@mvbbj12-nfd01:/tmp# time ./hoth_updater --hoth_id=<HOTH_ID> update
// --image=<IMAGE_FILE_NAME>
// --address=0x01ef0000 real 0m8.952s user 0m0.290s sys 0m0.030s
constexpr auto kCallTimeout = 15s;
constexpr auto kRetryDelay = 30s;
constexpr auto kAttemptLimit = 3;
std::string getHothService(std::string_view hoth_id)
{
std::string service = "xyz.openbmc_project.Control.Hoth";
if (!hoth_id.empty() && hoth_id != "bmc")
{
service += ".";
service += hoth_id;
}
return service;
}
std::string getHothObject(std::string_view hoth_id)
{
std::string object = "/xyz/openbmc_project/Control/Hoth";
if (!hoth_id.empty() && hoth_id != "bmc")
{
object += "/";
object += hoth_id;
}
return object;
}
sdbusplus::message::message hothMessage(sdbusplus::bus::bus& bus,
std::string_view hoth_id,
const char* method)
{
std::string service = getHothService(hoth_id);
std::string object = getHothObject(hoth_id);
return bus.new_method_call(service.c_str(), object.c_str(),
"xyz.openbmc_project.Control.Hoth", method);
}
sdbusplus::message::message hothPropertyMessage(sdbusplus::bus::bus& bus,
std::string_view hoth_id)
{
std::string service = getHothService(hoth_id);
std::string object = getHothObject(hoth_id);
return bus.new_method_call(service.c_str(), object.c_str(),
"org.freedesktop.DBus.Properties", "Get");
}
template <typename T>
std::optional<T> getHothStateProperty(
sdbusplus::bus::bus& bus, std::string_view hoth_id,
std::string_view property,
std::optional<sdbusplus::SdBusDuration> timeout = std::nullopt)
{
try
{
sdbusplus::message::message msg = hothPropertyMessage(bus, hoth_id);
msg.append("xyz.openbmc_project.Control.Hoth.State", property);
std::variant<T> value{};
sdbusplus::message::message resp =
bus.call(msg, timeout.value_or(kCallTimeout));
resp.read(value);
return std::get<T>(value);
}
catch (const std::exception& ex)
{
return std::nullopt;
}
}
template <typename T>
inline std::string optionalToString(const std::optional<T>& value)
{
return value.has_value() ? std::format("{}", *value) : "n/a";
}
} // namespace
std::vector<uint8_t> HothUpdaterCLI::sendHostCommand(
sdbusplus::bus::bus& bus, std::string_view hoth_id,
const std::span<const uint8_t> command)
{
sdbusplus::message::message msg =
hothMessage(bus, hoth_id, "SendHostCommand");
msg.append(command);
sdbusplus::message::message resp =
bus.call(msg, kCallTimeout);
std::vector<uint8_t> result;
resp.read(result);
return result;
}
void HothUpdaterCLI::updateFirmware(sdbusplus::bus::bus& bus,
std::string_view hoth_id,
const std::span<const uint8_t> image)
{
sdbusplus::message::message msg =
hothMessage(bus, hoth_id, "UpdateFirmware");
msg.append(image);
bus.call(msg, kCallTimeout);
}
void HothUpdaterCLI::spiWrite(sdbusplus::bus::bus& bus,
std::string_view hoth_id,
const std::span<const uint8_t> image,
std::optional<uint32_t> address)
{
for (int attempt = 0; attempt < kAttemptLimit; attempt++)
{
try
{
sdbusplus::message::message msg =
hothMessage(bus, hoth_id, "SpiWrite");
if (address)
{
msg.append(*address); // u
msg.append(image); // ay
}
bus.call(msg, kCallTimeout);
return;
}
catch (const std::exception& ex)
{
std::cout << "Exception caught: " << ex.what() << '\n';
std::cout << "Will retry in " << kRetryDelay.count() << " seconds"
<< '\n';
std::this_thread::sleep_for(kRetryDelay);
}
}
throw std::runtime_error("Retry attempt limit exhausted.");
}
FirmwareUpdateStatus
HothUpdaterCLI::getFirmwareUpdateStatus(sdbusplus::bus::bus& bus,
std::string_view hoth_id)
{
sdbusplus::message::message msg =
hothMessage(bus, hoth_id, "GetFirmwareUpdateStatus");
sdbusplus::message::message reply = bus.call(msg, kCallTimeout);
std::string rsp;
reply.read(rsp);
return sdbusplus::xyz::openbmc_project::Control::server::Hoth::
convertFirmwareUpdateStatusFromString(rsp);
}
std::vector<uint8_t>
HothUpdaterCLI::readFileIntoByteArray(std::string_view filename)
{
std::ifstream image_file;
image_file.exceptions(std::ios::failbit);
image_file.open(filename.data(), std::ios::binary);
std::vector<uint8_t> image(std::istreambuf_iterator<char>(image_file), {});
return image;
}
void HothUpdaterCLI::doUpdate(const Args& args)
{
sdbusplus::bus::bus bus = sdbusplus::bus::new_default();
doUpdateLogic(bus, args);
}
void HothUpdaterCLI::doUpdateLogic(sdbusplus::bus::bus& bus, const Args& args)
{
auto end_time = std::chrono::steady_clock::now() + 5min;
std::vector<uint8_t> image = readFileIntoByteArray(args.imageFilename);
if (args.updateMethod == "spi")
{
spiWrite(bus, args.hothId, image, args.address);
}
else if (args.updateMethod == "update_firmware")
{
updateFirmware(bus, args.hothId, image);
}
else if (args.updateMethod == "payload_update")
{
throw std::runtime_error("Payload update is not supported yet");
}
FirmwareUpdateStatus status = getFirmwareUpdateStatus(bus, args.hothId);
while (status == FirmwareUpdateStatus::InProgress)
{
if (std::chrono::steady_clock::now() > end_time)
{
throw std::runtime_error("Timed out updating firmware");
}
std::this_thread::sleep_for(1s);
status = getFirmwareUpdateStatus(bus, args.hothId);
}
if (status != FirmwareUpdateStatus::Done)
{
throw std::runtime_error("Update failed");
}
}
HothVersionStringsRsp HothUpdaterCLI::getHothVersion(sdbusplus::bus::bus& bus,
std::string_view hoth_id)
{
google::hoth::internal::ReqHeader header;
google::hoth::internal::populateReqHeader(EC_CMD_GET_VERSION, 0, nullptr, 0,
&header);
auto header_span = stdplus::raw::asSpan<uint8_t>(header);
std::vector<uint8_t> command(header_span.begin(), header_span.end());
std::vector<uint8_t> resp_bytes = sendHostCommand(bus, hoth_id, command);
auto response = stdplus::raw::copyFrom<HothVersionStringsRsp>(resp_bytes);
if (response.header.result != 0)
{
throw std::runtime_error("Failed to get versions");
}
return response;
}
void HothUpdaterCLI::doFirmwareVersion(const Args& args)
{
sdbusplus::bus::bus bus = sdbusplus::bus::new_default();
doFirmwareVersionLogic(bus, args);
}
void HothUpdaterCLI::doFirmwareVersionLogic(sdbusplus::bus::bus& bus,
const Args& args)
{
auto response = getHothVersion(bus, args.hothId);
if (args.ro)
{
std::cout << response.version.version_string_ro;
}
else
{
std::cout << response.version.version_string_rw;
}
}
std::vector<std::string> splitString(const std::string& s, const char delim)
{
size_t idx = 0;
std::vector<std::string> ret;
while (idx < s.size())
{
std::size_t delim_pos = s.find_first_of(delim, idx);
if (delim_pos == std::string::npos)
{
break;
}
ret.emplace_back(s.substr(idx, delim_pos - idx));
idx = delim_pos + 1;
}
if (idx < s.size())
{
ret.emplace_back(s.substr(idx));
}
return ret;
}
HothActivationStatistics
HothUpdaterCLI::getHothActivationStatistics(sdbusplus::bus::bus& bus,
std::string_view hoth_id)
{
HothActivationStatistics status;
// Try query the statistics properties on dBus.
// Each individual property could fail. Ignore errors and try populate as
// much as possible.
status.rw_failure_code = getHothStateProperty<uint32_t>(
bus, hoth_id, "FirmwareUpdateFailureCode");
status.rw_failed_minor = getHothStateProperty<uint32_t>(
bus, hoth_id, "FirmwareUpdateFailedMinor");
status.ro_failure_code = getHothStateProperty<uint32_t>(
bus, hoth_id, "BootloaderUpdateFailureCode");
status.reset_flags =
getHothStateProperty<uint32_t>(bus, hoth_id, "ResetFlags");
status.uptime_us = getHothStateProperty<uint64_t>(bus, hoth_id, "UpTime");
return status;
}
void HothUpdaterCLI::doActivationCheck(const Args& args)
{
sdbusplus::bus::bus bus = sdbusplus::bus::new_default();
doActivationCheckLogic(bus, args);
}
void HothUpdaterCLI::doActivationCheckLogic(sdbusplus::bus::bus& bus,
const Args& args)
{
stdplus::print(stdout, "installed_version: \"{}\"\n",
args.expectedRwVersion);
std::vector<std::string> hoth_ids = splitString(args.hothId, ',');
if (hoth_ids.empty())
{
hoth_ids.push_back("");
}
for (const std::string& hoth_id : hoth_ids)
{
auto response = getHothVersion(bus, hoth_id);
// Strip out the hoth family in the version string, because it's not
// guaranteed to be returned completely.
std::string_view version_stripped(response.version.version_string_rw);
size_t split_pos = version_stripped.find_first_of('/');
if (split_pos != std::string::npos)
{
version_stripped = version_stripped.substr(0, split_pos);
}
stdplus::print(stdout,
"activated_versions {{\n"
" key: \"{}\"\n"
" value: \"{}\"\n"
"}}\n",
(hoth_id.empty() ? "active" : hoth_id),
version_stripped);
if (args.expectedRwVersion != version_stripped)
{
HothActivationStatistics actv_status =
getHothActivationStatistics(bus, hoth_id);
stdplus::print(stdout,
"notes: \"Running RW version {}"
" does not match expected version {}, Status: rw={} "
"(ver={}) ro={} rst_flags={}, uptime={}us\"\n",
version_stripped, args.expectedRwVersion,
optionalToString(actv_status.rw_failure_code),
optionalToString(actv_status.rw_failed_minor),
optionalToString(actv_status.ro_failure_code),
optionalToString(actv_status.reset_flags),
optionalToString(actv_status.uptime_us));
throw std::runtime_error("Activation check failed");
}
}
}
void HothUpdaterCLI::doEnableSecurebootEnforcement(const Args& args)
{
sdbusplus::bus::bus bus = sdbusplus::bus::new_default();
doEnableSecurebootEnforcementLogic(bus, args);
}
void HothUpdaterCLI::doEnableSecurebootEnforcementLogic(
sdbusplus::bus::bus& bus, const Args& args)
{
google::hoth::internal::ReqHeader header;
secure_boot_enforcement_state payload = {.enabled = 1, .reserved0 = {0}};
google::hoth::internal::populateReqHeader(
EC_CMD_BOARD_SPECIFIC_BASE + EC_PRV_CMD_HOTH_SET_SECURE_BOOT_ENFORCEMENT,
0, &payload, sizeof(payload), &header);
auto header_span = stdplus::raw::asSpan<uint8_t>(header);
auto payload_span = stdplus::raw::asSpan<uint8_t>(payload);
std::vector<uint8_t> command;
command.insert(command.end(), header_span.begin(), header_span.end());
command.insert(command.end(), payload_span.begin(), payload_span.end());
std::vector<std::string> hoth_ids = splitString(args.hothId, ',');
if (hoth_ids.empty())
{
hoth_ids.push_back("");
}
for (const std::string& hoth_id : hoth_ids)
{
std::vector<uint8_t> resp_bytes =
sendHostCommand(bus, hoth_id, command);
auto response = stdplus::raw::copyFrom<
google::hoth::internal::RspHeader>(resp_bytes);
if (response.result != 0)
{
throw std::runtime_error(
"Failed to enable secure boot enforcement");
}
}
}
void setupCLIApp(CLI::App& app, HothUpdaterCLI& cli, Args& args)
{
app.require_subcommand(1);
app.add_option("--hoth_id", args.hothId,
"Hoth IDs, comma-delimited for activation-check");
auto* update = app.add_subcommand("update", "Update Hoth image");
update->add_option("--image", args.imageFilename, "Firmware image path")
->required()
->check(CLI::ExistingFile);
update
->add_option("--method", args.updateMethod,
"Update method, can be spi|update_firmware|payload_update")
->required();
update->add_option("--address", args.address, "SPI address for data")
->check(CLI::NonNegativeNumber);
update->callback([&args, &cli] { cli.doUpdate(args); });
auto* version =
app.add_subcommand("firmware-version", "Get firmware version");
version->add_flag("--ro,!--rw", args.ro, "Select ro/rw partition")
->required();
version->callback([&args, &cli] { cli.doFirmwareVersion(args); });
auto* activation_check = app.add_subcommand(
"activation-check", "Generate package activation check textproto");
activation_check->add_flag("--rw", args.expectedRwVersion,
"Expected RW version");
// RO version check not implemented yet.
activation_check->callback([&args, &cli] { cli.doActivationCheck(args); });
auto* enable_secureboot = app.add_subcommand(
"enable-secureboot-enforcement", "Enable secure boot enforcement");
enable_secureboot->callback(
[&args, &cli] { cli.doEnableSecurebootEnforcement(args); });
}
} // namespace google::hoth::tools