// Copyright Epic Games, Inc. All Rights Reserved. #include "admin_cmd.h" #include "zenserviceclient.h" #include #include #include #include #include #include #include using namespace std::literals; namespace zen { ScrubCommand::ScrubCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "n", "dry", "Dry run (do not delete any data)", cxxopts::value(m_DryRun), ""); m_Options.add_option("", "", "no-gc", "Do not perform GC after scrub pass", cxxopts::value(m_NoGc), ""); m_Options.add_option("", "", "no-cas", "Do not scrub CAS stores", cxxopts::value(m_NoCas), ""); m_Options.add_option("", "", "maxtimeslice", "Number of second Scrub is allowed to run before stopping in seconds (default 300s)", cxxopts::value(m_MaxTimeSliceSeconds), ""); } ScrubCommand::~ScrubCommand() = default; void ScrubCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient& Http = Service.Http(); HttpClient::KeyValueMap Params{{"skipdelete", ToString(m_DryRun)}, {"skipgc", ToString(m_NoGc)}, {"skipcid", ToString(m_NoCas)}, {"maxtimeslice", fmt::format("{}", m_MaxTimeSliceSeconds)}}; if (HttpClient::Response Response = Http.Post("/admin/scrub"sv, /* headers */ HttpClient::KeyValueMap{}, Params)) { ZEN_CONSOLE("Scrub started OK: {}", Response.ToText()); } else { Response.ThrowError("Scrub start failed"); } } ////////////////////////////////////////////////////////////////////////// GcCommand::GcCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "s", "smallobjects", "Collect small objects", cxxopts::value(m_SmallObjects)->default_value("false"), ""); m_Options.add_option("", "", "skipcid", "Skip collection of CAS data", cxxopts::value(m_SkipCid)->default_value("false"), ""); m_Options.add_option("", "n", "skipdelete", "Skip deletion of data (dryrun)", cxxopts::value(m_SkipDelete)->default_value("false"), ""); m_Options.add_option("", "m", "maxcacheduration", "Max cache lifetime (in seconds)", cxxopts::value(m_MaxCacheDuration)->default_value("0"), ""); m_Options.add_option("", "d", "disksizesoftlimit", "Max disk usage size (in bytes)", cxxopts::value(m_DiskSizeSoftLimit)->default_value("0"), ""); m_Options.add_option("", "", "usegcv1", "Force use of GC version 1. Deprecated, will do nothing.", cxxopts::value(m_ForceUseGCV1)->default_value("false"), ""); m_Options .add_option("", "", "usegcv2", "Force use of GC version 2", cxxopts::value(m_ForceUseGCV2)->default_value("false"), ""); m_Options.add_option("", "", "compactblockthreshold", "How much of a compact block should be used to skip compacting the block. 0 - compact only empty eligible blocks, " "100 - compact all non-full eligible blocks.", cxxopts::value(m_CompactBlockThreshold)->default_value("60"), ""); m_Options .add_option("", "", "verbose", "Enable verbose logging for GC", cxxopts::value(m_Verbose)->default_value("false"), ""); m_Options.add_option("", "", "single-threaded", "Force GC to run single threaded", cxxopts::value(m_SingleThreaded)->default_value("false"), ""); m_Options.add_option("", "", "reference-low", "Reference filter lower limit - defaults to no limit", cxxopts::value(m_ReferenceHashLow), ""); m_Options.add_option("", "", "reference-high", "Reference filter higher limit - defaults to no limit", cxxopts::value(m_ReferenceHashHigh), ""); m_Options.add_option("", "", "cache-attachment-store", "Enable storing attachments referenced by a cache record in block store meta data", cxxopts::value(m_StoreCacheAttachmentMetaData)->default_value("false"), ""); m_Options.add_option("", "", "projectstore-attachment-store", "Enable storing attachments referenced by project oplogs in meta data", cxxopts::value(m_StoreProjectAttachmentMetaData)->default_value("false"), ""); m_Options.add_option("", "", "gc-validation", "Enable validation of references after full GC.", cxxopts::value(m_EnableValidation)->default_value("true"), ""); } GcCommand::~GcCommand() { } void GcCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient::KeyValueMap Params; Params.Entries.insert({"smallobjects", m_SmallObjects ? "true" : "false"}); if (m_MaxCacheDuration != 0) { Params.Entries.insert({"maxcacheduration", fmt::format("{}", m_MaxCacheDuration)}); } if (m_DiskSizeSoftLimit != 0) { Params.Entries.insert({"disksizesoftlimit", fmt::format("{}", m_DiskSizeSoftLimit)}); } Params.Entries.insert({"skipcid", m_SkipCid ? "true" : "false"}); Params.Entries.insert({"skipdelete", m_SkipDelete ? "true" : "false"}); if (m_ForceUseGCV1) { throw OptionParseException("'--usegcv1' is deprecated and can no longer be used", m_Options.help()); } if (m_ForceUseGCV2) { Params.Entries.insert({"forceusegcv2", "true"}); } if (m_CompactBlockThreshold) { Params.Entries.insert({"compactblockthreshold", fmt::format("{}", m_CompactBlockThreshold)}); } IoHash LowRef = IoHash::Zero; if (!m_ReferenceHashLow.empty()) { if (m_ReferenceHashLow.length() != IoHash::StringLength) { throw OptionParseException(fmt::format("'--reference-low' ('{}') is malformed, it must be a {} character hex string", m_ReferenceHashLow, IoHash::StringLength), m_Options.help()); } if (!IoHash::TryParse(m_ReferenceHashLow, LowRef)) { throw OptionParseException(fmt::format("'--reference-low' ('{}') is malformed", m_ReferenceHashLow), m_Options.help()); } } IoHash HighRef = IoHash::Max; if (!m_ReferenceHashHigh.empty()) { if (m_ReferenceHashHigh.length() != IoHash::StringLength) { throw OptionParseException(fmt::format("''--reference-high' ('{}') is malformed, it must be a {} character hex string", m_ReferenceHashHigh, IoHash::StringLength), m_Options.help()); } if (!IoHash::TryParse(m_ReferenceHashHigh, HighRef)) { throw OptionParseException(fmt::format("'--reference-high' ('{}') is malformed", m_ReferenceHashHigh), m_Options.help()); } } if (HighRef < LowRef) { throw OptionParseException( fmt::format("'--reference-high' ('{}') is invalid, it must be a higher value than '--reference-low' ('{}')", m_ReferenceHashHigh, m_ReferenceHashLow), m_Options.help()); } if (!m_ReferenceHashLow.empty() || !m_ReferenceHashHigh.empty()) { Params.Entries.insert({"referencehashlow", LowRef.ToHexString()}); Params.Entries.insert({"referencehashhigh", HighRef.ToHexString()}); } Params.Entries.insert({"verbose", m_Verbose ? "true" : "false"}); Params.Entries.insert({"singlethreaded", m_SingleThreaded ? "true" : "false"}); if (m_StoreCacheAttachmentMetaData) { Params.Entries.insert({"storecacheattachmentmetadata", m_StoreCacheAttachmentMetaData ? "true" : "false"}); } if (m_StoreProjectAttachmentMetaData) { Params.Entries.insert({"storeprojectattachmentmetadata", m_StoreProjectAttachmentMetaData ? "true" : "false"}); } Params.Entries.insert({"enablevalidation", m_EnableValidation ? "true" : "false"}); HttpClient& Http = Service.Http(); if (HttpClient::Response Response = Http.Post("/admin/gc"sv, HttpClient::Accept(HttpContentType::kJSON), Params)) { ZEN_CONSOLE("OK: {}", Response.ToText()); } else { Response.ThrowError("GC start failed"); } } GcStatusCommand::GcStatusCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "d", "details", "Show detailed GC report", cxxopts::value(m_Details)->default_value("false"), "
"); } GcStatusCommand::~GcStatusCommand() { } void GcStatusCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient& Http = Service.Http(); if (HttpClient::Response Response = Http.Get("/admin/gc"sv, HttpClient::Accept(HttpContentType::kJSON))) { ZEN_CONSOLE("OK: {}", Response.ToText()); } else { Response.ThrowError("Gc status failed"); } } GcStopCommand::GcStopCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); } GcStopCommand::~GcStopCommand() { } void GcStopCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient& Http = Service.Http(); if (HttpClient::Response Response = Http.Post("/admin/gc-stop"sv, HttpClient::Accept(HttpContentType::kJSON))) { if (Response.StatusCode == HttpResponseCode::Accepted) { ZEN_CONSOLE("OK: {}", "Cancel request accepted"); } else { ZEN_CONSOLE("OK: {}", "No GC running"); } } else { Response.ThrowError("Gc stop failed"); } } //////////////////////////////////////////// JobCommand::JobCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "j", "jobid", "Job id", cxxopts::value(m_JobId), ""); m_Options.add_option("", "c", "cancel", "Cancel job id", cxxopts::value(m_Cancel), ""); } JobCommand::~JobCommand() = default; void JobCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); using namespace std::literals; if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient& Http = Service.Http(); if (m_Cancel) { if (m_JobId == 0) { throw OptionParseException("'--jobid' is required", m_Options.help()); } } std::string Url = m_JobId != 0 ? fmt::format("/admin/jobs/{}", m_JobId) : "/admin/jobs"; if (m_Cancel) { if (HttpClient::Response Result = Http.Delete(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("failed cancelling job"sv); } } else if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed fetching job info"sv); } } //////////////////////////////////////////// LoggingCommand::LoggingCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "", "cache-write-log", "Enable cache write logging", cxxopts::value(m_CacheWriteLog), ""); m_Options.add_option("", "", "cache-access-log", "Enable cache access logging", cxxopts::value(m_CacheAccessLog), ""); m_Options .add_option("", "", "set-log-level", "Set zenserver log level", cxxopts::value(m_SetLogLevel), ""); m_Options.add_option("", "", "copy-log", "Copy the server log file from a local zenserver instance", cxxopts::value(m_ServerLogTarget), ""); m_Options.add_option("", "", "copy-cache-log", "Copy the server cache log file from a local zenserver instance", cxxopts::value(m_CacheLogTarget), ""); m_Options.add_option("", "", "copy-http-log", "Copy the server http log file from a local zenserver instance", cxxopts::value(m_HttpLogTarget), ""); } LoggingCommand::~LoggingCommand() = default; void LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); using namespace std::literals; if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient& Http = Service.Http(); HttpClient::KeyValueMap Parameters; if (!m_CacheWriteLog.empty()) { if (m_CacheWriteLog == "enable") { (*Parameters)["cacheenablewritelog"] = "true"; } else if (m_CacheWriteLog == "disable") { (*Parameters)["cacheenablewritelog"] = "false"; } else { throw OptionParseException(fmt::format("'--cache-write-log' ('{}') is invalid, use 'enable' or 'disable'", m_CacheWriteLog), m_Options.help()); } } if (!m_CacheAccessLog.empty()) { if (m_CacheAccessLog == "enable") { (*Parameters)["cacheenableaccesslog"] = "true"; } else if (m_CacheAccessLog == "disable") { (*Parameters)["cacheenableaccesslog"] = "false"; } else { throw OptionParseException(fmt::format("'--cache-access-log' ('{}') is invalid, use 'enable' or 'disable'", m_CacheAccessLog), m_Options.help()); } } if (!m_SetLogLevel.empty()) { (*Parameters)["loglevel"] = m_SetLogLevel; } if ((*Parameters).empty()) { if (HttpClient::Response Result = Http.Get("/admin/logs", HttpClient::Accept(ZenContentType::kCbObject))) { ZEN_CONSOLE("{}", Result.ToText()); const CbObject LogsResponse = Result.AsObject(); auto CopyLog = [](std::string_view SourceName, std::string_view SourcePath, std::string_view TargetPath) { if (SourcePath.empty()) { throw std::runtime_error(fmt::format("Failed to retrieve {} log path", SourceName)); } if (std::error_code Ec = CopyFile(SourcePath, TargetPath, {}); Ec) { throw std::system_error( Ec, fmt::format("Failed to copy {} log file {} to output file '{}'", SourceName, SourcePath, TargetPath)); } }; if (!m_ServerLogTarget.empty()) { CopyLog("server", LogsResponse["Logfile"].AsString(), m_ServerLogTarget); } if (!m_CacheLogTarget.empty()) { CopyLog("cache", LogsResponse["cache"].AsObjectView()["Logfile"].AsString(), m_CacheLogTarget); } if (!m_HttpLogTarget.empty()) { CopyLog("http", LogsResponse["http"].AsObjectView()["Logfile"].AsString(), m_HttpLogTarget); } } else { Result.ThrowError("failed fetching log info"sv); } return; } if (HttpClient::Response Result = Http.Post("/admin/logs", HttpClient::KeyValueMap{}, Parameters)) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed setting log info"sv); } } ////////////////////////////////////////////////////////////////////////// FlushCommand::FlushCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); } FlushCommand::~FlushCommand() = default; void FlushCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); HttpClient& Http = Service.Http(); if (HttpClient::Response Response = Http.Post("/admin/flush"sv)) { ZEN_CONSOLE("OK: {}", Response.ToText()); return; } else { Response.ThrowError("Flush failed"); } } ////////////////////////////////////////////////////////////////////////// CopyStateCommand::CopyStateCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "", "data-path", "Zen server source data path", cxxopts::value(m_DataPath), ""); m_Options.add_option("", "", "target-path", "Target data path", cxxopts::value(m_TargetPath), ""); m_Options.add_option("", "", "skip-logs", "Only copy state index files, not log files (recommended to issue a zen flush before using this option)", cxxopts::value(m_SkipLogs)->default_value("false"), ""); m_Options.parse_positional({"data-path", "target-path"}); } CopyStateCommand::~CopyStateCommand() = default; static void Copy(const std::filesystem::path& Source, const std::filesystem::path& Target) { CreateDirectories(Target.parent_path()); CopyFileOptions Options; if (std::error_code Ec = CopyFile(Source, Target, Options); Ec) { throw std::system_error(Ec, fmt::format("Failed to copy '{}' to '{}'", Source, Target)); } } static bool TryCopy(const std::filesystem::path& Source, const std::filesystem::path& Target) { if (!IsFile(Source)) { return false; } CreateDirectories(Target.parent_path()); CopyFileOptions Options; std::error_code Ec = CopyFile(Source, Target, Options); return !Ec; } void CopyStateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } if (m_DataPath.empty()) { throw OptionParseException("'--data-path' is required", m_Options.help()); } if (!IsDir(m_DataPath)) { throw std::runtime_error(fmt::format("'--data-path' '{}' must exist", m_DataPath)); } if (m_TargetPath.empty()) { throw OptionParseException("'--target-path' is required", m_Options.help()); } std::filesystem::path RootManifestPath = m_DataPath / "root_manifest"; std::filesystem::path TargetRootManifestPath = m_TargetPath / "root_manifest"; if (!TryCopy(RootManifestPath, TargetRootManifestPath)) { throw std::runtime_error( fmt::format("'--data-path' ('{}') is invalid, missing root_manifest at '{}'", m_DataPath, RootManifestPath)); } std::filesystem::path CachePath = m_DataPath / "cache"; std::filesystem::path TargetCachePath = m_TargetPath / "cache"; // Copy cache state DirectoryContent CacheDirectoryContent; GetDirectoryContent(CachePath, DirectoryContentFlags::IncludeDirs, CacheDirectoryContent); for (const std::filesystem::path& NamespacePath : CacheDirectoryContent.Directories) { std::filesystem::path NamespaceName = NamespacePath.filename(); std::filesystem::path TargetNamespacePath = TargetCachePath / NamespaceName; DirectoryContent CacheNamespaceContent; GetDirectoryContent(NamespacePath, DirectoryContentFlags::IncludeDirs, CacheNamespaceContent); for (const std::filesystem::path& BucketPath : CacheNamespaceContent.Directories) { std::filesystem::path BucketName = BucketPath.filename(); std::filesystem::path TargetBucketPath = TargetNamespacePath / BucketName; // TODO: make these use file naming helpers from cache implementation? std::filesystem::path ManifestPath = BucketPath / "zen_manifest"; std::filesystem::path TargetManifestPath = TargetBucketPath / "zen_manifest"; if (TryCopy(ManifestPath, TargetManifestPath)) { if (!m_SkipLogs) { std::filesystem::path LogName = fmt::format("{}.{}", BucketName.string(), "slog"); std::filesystem::path LogPath = BucketPath / LogName; std::filesystem::path TargetLogPath = TargetBucketPath / LogName; Copy(LogPath, TargetLogPath); } std::filesystem::path IndexName = fmt::format("{}.{}", BucketName.string(), "uidx"); std::filesystem::path IndexPath = BucketPath / IndexName; std::filesystem::path TargetIndexPath = TargetBucketPath / IndexName; TryCopy(IndexPath, TargetIndexPath); std::filesystem::path MetaName = fmt::format("{}.{}", BucketName.string(), "meta"); std::filesystem::path MetaPath = BucketPath / MetaName; std::filesystem::path TargetMetaPath = TargetBucketPath / MetaName; TryCopy(MetaPath, TargetMetaPath); } } } std::filesystem::path CasPath = m_DataPath / "cas"; std::filesystem::path TargetCasPath = m_TargetPath / "cas"; { std::filesystem::path UCasRootPath = CasPath / ".ucas_root"; std::filesystem::path TargetUCasRootPath = TargetCasPath / ".ucas_root"; Copy(UCasRootPath, TargetUCasRootPath); } if (!m_SkipLogs) { std::filesystem::path LogPath = CasPath / "cas.ulog"; std::filesystem::path TargetLogPath = TargetCasPath / "cas.ulog"; Copy(LogPath, TargetLogPath); } { std::filesystem::path IndexPath = CasPath / "cas.uidx"; std::filesystem::path TargetIndexPath = TargetCasPath / "cas.uidx"; TryCopy(IndexPath, TargetIndexPath); } { std::filesystem::path SobsRootPath = CasPath / "sobs"; std::filesystem::path TargetSobsRootPath = TargetCasPath / "sobs"; { std::filesystem::path SobsIndexPath = SobsRootPath / "sobs.uidx"; std::filesystem::path TargetSobsIndexPath = TargetSobsRootPath / "sobs.uidx"; TryCopy(SobsIndexPath, TargetSobsIndexPath); if (!m_SkipLogs) { std::filesystem::path SobsLogPath = SobsRootPath / "sobs.ulog"; std::filesystem::path TargetSobsLogPath = TargetSobsRootPath / "sobs.ulog"; Copy(SobsLogPath, TargetSobsLogPath); } } } { std::filesystem::path TobsRootPath = CasPath / "tobs"; std::filesystem::path TargetTobsRootPath = TargetCasPath / "tobs"; { std::filesystem::path TobsIndexPath = TobsRootPath / "tobs.uidx"; std::filesystem::path TargetTobsIndexPath = TargetTobsRootPath / "tobs.uidx"; TryCopy(TobsIndexPath, TargetTobsIndexPath); if (!m_SkipLogs) { std::filesystem::path TobsLogPath = TobsRootPath / "tobs.ulog"; std::filesystem::path TargetTobsLogPath = TargetTobsRootPath / "tobs.ulog"; Copy(TobsLogPath, TargetTobsLogPath); } } } } } // namespace zen