blob: 01bd1b143753b63817ca277fac8a6f9331177856 [file] [log] [blame]
// 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 "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");
}
std::vector<uint8_t> sendHostCommand(
sdbusplus::bus::bus& bus, std::string_view hoth_id,
const std::span<const uint8_t> command,
std::optional<sdbusplus::SdBusDuration> timeout = std::nullopt)
{
sdbusplus::message::message msg =
hothMessage(bus, hoth_id, "SendHostCommand");
msg.append(command);
sdbusplus::message::message resp =
bus.call(msg, timeout.value_or(kCallTimeout));
std::vector<uint8_t> result;
resp.read(result);
return result;
}
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
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();
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)
{
const std::vector<uint8_t> versionStringsCommand = {0x03, 0xfb, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> resp_bytes =
sendHostCommand(bus, hoth_id, versionStringsCommand);
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();
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();
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 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); });
}
} // namespace google::hoth::tools