//------------------------------------------------------------------------------
// @file: NodeCmd.cc
// @author: Fabio Luchetti - CERN
//------------------------------------------------------------------------------
/************************************************************************
* EOS - the CERN Disk Storage System *
* Copyright (C) 2018 CERN/Switzerland *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see .*
************************************************************************/
#include "NodeCmd.hh"
#include "mgm/proc/ProcInterface.hh"
#include "mgm/XrdMgmOfs.hh"
#include "mgm/config/IConfigEngine.hh"
#include "mq/MessagingRealm.hh"
#include "namespace/interface/IFsView.hh"
EOSMGMNAMESPACE_BEGIN
//------------------------------------------------------------------------------
// Method implementing the specific behavior of the command executed by the
// asynchronous thread
//------------------------------------------------------------------------------
eos::console::ReplyProto
NodeCmd::ProcessRequest() noexcept
{
eos::console::ReplyProto reply;
eos::console::NodeProto node = mReqProto.node();
switch (mReqProto.node().subcmd_case()) {
case eos::console::NodeProto::kLs:
LsSubcmd(node.ls(), reply);
break;
case eos::console::NodeProto::kRm:
RmSubcmd(node.rm(), reply);
break;
case eos::console::NodeProto::kStatus:
StatusSubcmd(node.status(), reply);
break;
case eos::console::NodeProto::kConfig:
ConfigSubcmd(node.config(), reply);
break;
case eos::console::NodeProto::kSet:
SetSubcmd(node.set(), reply);
break;
case eos::console::NodeProto::kProxygroup:
ProxygroupSubcmd(node.proxygroup(), reply);
break;
default:
reply.set_std_err("error: not supported");
reply.set_retc(EINVAL);
}
return reply;
}
//------------------------------------------------------------------------------
// Execute ls subcommand
//------------------------------------------------------------------------------
void NodeCmd::LsSubcmd(const eos::console::NodeProto_LsProto& ls,
eos::console::ReplyProto& reply)
{
using eos::console::NodeProto;
bool json_output = false;
std::string list_format;
std::string format;
auto format_case = ls.outformat();
if ((format_case == NodeProto::LsProto::NONE) && WantsJsonOutput()) {
format_case = NodeProto::LsProto::MONITORING;
}
switch (format_case) {
case NodeProto::LsProto::LISTING:
format = FsView::GetNodeFormat("l");
list_format = FsView::GetFileSystemFormat("l");
break;
case NodeProto::LsProto::MONITORING:
format = FsView::GetNodeFormat("m");
json_output = WantsJsonOutput();
break;
case NodeProto::LsProto::IO:
format = FsView::GetNodeFormat("io");
break;
case NodeProto::LsProto::SYS:
format = FsView::GetNodeFormat("sys");
break;
case NodeProto::LsProto::FSCK:
format = FsView::GetNodeFormat("fsck");
break;
default : // NONE
format = FsView::GetNodeFormat("");
break;
}
if (!ls.outhost()) {
if (format.find('S') != std::string::npos) {
format.replace(format.find('S'), 1, "s");
}
if (list_format.find('S') != std::string::npos) {
list_format.replace(list_format.find('S'), 1, "s");
}
}
std::string output;
eos::common::RWMutexReadLock rd_lock(FsView::gFsView.ViewMutex);
FsView::gFsView.PrintNodes(output, format, list_format, 0,
ls.selection().c_str(), mReqProto.dontcolor());
if (json_output) {
output = ResponseToJsonString(output);
}
reply.set_std_out(output);
reply.set_retc(0);
}
//------------------------------------------------------------------------------
// Execute rm subcommand
//------------------------------------------------------------------------------
void NodeCmd::RmSubcmd(const eos::console::NodeProto_RmProto& rm,
eos::console::ReplyProto& reply)
{
if (mVid.uid != 0 && mVid.prot != "sss") {
reply.set_std_err("error: you have to take role 'root' to execute this command");
reply.set_retc(EPERM);
return;
}
if (rm.node().empty()) {
reply.set_std_err("error: illegal parameter 'node'");
reply.set_retc(EINVAL);
return;
}
std::string nodename = rm.node();
if ((nodename.find(':') == std::string::npos)) {
nodename += ":1095"; // default eos fst port
}
if ((nodename.find("/eos/") == std::string::npos)) {
nodename.insert(0, "/eos/");
nodename.append("/fst");
}
eos::common::RWMutexWriteLock wr_lock(FsView::gFsView.ViewMutex);
if (!FsView::gFsView.mNodeView.count(nodename)) {
reply.set_std_err("error: no such node '" + nodename + "'");
reply.set_retc(ENOENT);
return;
}
// Remove a node only if it has no heartbeat anymore
if ((time(nullptr) - FsView::gFsView.mNodeView[nodename]->GetHeartBeat()) < 5) {
reply.set_std_err("error: this node was still sending a heartbeat < 5 "
"seconds ago - stop the FST daemon first!");
reply.set_retc(EBUSY);
return;
}
// Remove a node only if all filesystems are in empty state
for (auto it = FsView::gFsView.mNodeView[nodename]->begin();
it != FsView::gFsView.mNodeView[nodename]->end(); ++it) {
FileSystem* fs = FsView::gFsView.mIdView.lookupByID(*it);
if (fs) {
// check the empty state
if ((fs->GetConfigStatus(false) != eos::common::ConfigStatus::kEmpty)) {
reply.set_std_err("error: unable to remove node '" + nodename +
"' - filesystems are not all in empty state");
reply.set_retc(EBUSY);
return;
}
}
}
common::SharedHashLocator nodeLocator = common::SharedHashLocator::makeForNode(
nodename);
if (!mq::SharedHashWrapper::deleteHash(gOFS->mMessagingRealm.get(),
nodeLocator)) {
reply.set_std_err("error: unable to remove config of node '" + nodename + "'");
reply.set_retc(EIO);
} else {
if (FsView::gFsView.UnRegisterNode(nodename.c_str())) {
reply.set_std_out("success: removed node '" + nodename + "'");
} else {
reply.set_std_err("error: unable to unregister node '" + nodename + "'");
}
}
// Delete also the entry from the configuration
eos_info("msg=\"delete from configuration\" node_name=%s",
nodeLocator.getConfigQueue().c_str());
gOFS->ConfEngine->DeleteConfigValueByMatch("global",
nodeLocator.getConfigQueue().c_str());
gOFS->ConfEngine->AutoSave();
}
//------------------------------------------------------------------------------
// Execute status subcommand
//------------------------------------------------------------------------------
void NodeCmd::StatusSubcmd(const eos::console::NodeProto_StatusProto& status,
eos::console::ReplyProto& reply)
{
std::string nodename = status.node();
if ((nodename.find(':') == std::string::npos)) {
nodename += ":1095"; // default eos fst port
}
if ((nodename.find("/eos/") == std::string::npos)) {
nodename.insert(0, "/eos/");
nodename.append("/fst");
}
if (!FsView::gFsView.mNodeView.count(nodename)) {
reply.set_std_err("error: cannot find node - no node with name '" + nodename +
"'");
reply.set_retc(ENOENT);
return;
}
eos::common::RWMutexWriteLock wr_lock(FsView::gFsView.ViewMutex);
std::string std_out;
std::vector keylist;
std_out +=
"# ------------------------------------------------------------------------------------\n";
std_out += "# Node Variables\n";
std_out +=
"# ....................................................................................\n";
FsView::gFsView.mNodeView[nodename]->GetConfigKeys(keylist);
std::sort(keylist.begin(), keylist.end());
for (auto& i : keylist) {
char line[2048];
std::string val = FsView::gFsView.mNodeView[nodename]->GetConfigMember(i);
if (val.substr(0, 7) == "base64:") {
val = "base64:...";
}
if (val.length() > 1024) {
val = "...";
}
snprintf(line, sizeof(line) - 1, "%-32s := %s\n", i.c_str(), val.c_str());
std_out += line;
}
reply.set_std_out(std_out);
reply.set_retc(0);
}
//------------------------------------------------------------------------------
// Execute config subcommand
//------------------------------------------------------------------------------
void NodeCmd::ConfigSubcmd(const eos::console::NodeProto_ConfigProto& config,
eos::console::ReplyProto& reply)
{
if (mVid.uid != 0 && mVid.prot != "sss") {
reply.set_std_err("error: you have to take role 'root' to execute this command");
reply.set_retc(EPERM);
return;
}
if (!config.node_name().length() ||
!config.node_key().length() ||
!config.node_value().length()) {
reply.set_std_err("error: invalid parameters");
reply.set_retc(EINVAL);
return;
}
std::set set_nodes;
{
// Collect the path of the nodes concerned
eos::common::RWMutexReadLock lock(FsView::gFsView.ViewMutex);
if ((config.node_name().find('*') != std::string::npos)) {
for (auto& it : FsView::gFsView.mNodeView) {
set_nodes.insert(it.first);
}
} else {
// by host:port name
std::string path = config.node_name();
if ((path.find(':') == std::string::npos)) {
path += ":1095"; // default eos fst port
}
if ((path.find("/eos/") == std::string::npos)) {
path.insert(0, "/eos/");
path.append("/fst");
}
if (FsView::gFsView.mNodeView.count(path)) {
set_nodes.insert(path);
}
}
}
if (set_nodes.empty()) {
reply.set_retc(EINVAL);
reply.set_std_err("error: cannot find node <" + config.node_name() + ">");
return;
}
// Handle file system specific configurations
if (config.node_key() == "configstatus") {
return ConfigFsSpecific(set_nodes, config.node_key(), config.node_value(),
reply);
}
// Handle note specific configurations
for (auto& node_path : set_nodes) {
eos::common::RWMutexReadLock lock(FsView::gFsView.ViewMutex);
auto it = FsView::gFsView.mNodeView.find(node_path);
if (it == FsView::gFsView.mNodeView.end()) {
continue;
}
FsNode* node = it->second;
if (config.node_key() == "error.simulation") {
if (node->SetConfigMember(config.node_key(), config.node_value(), false)) {
reply.set_std_out("success: setting error simulation tag '" +
config.node_value() += "'");
} else {
reply.set_std_err("error: failed to store the error simulation tag");
reply.set_retc(EFAULT);
}
} else if (config.node_key() == "publish.interval") {
if (node->SetConfigMember(config.node_key(), config.node_value(), false)) {
reply.set_std_out("success: setting publish interval to '" +
config.node_value() + "'");
} else {
reply.set_std_err("error: failed to store publish interval");
reply.set_retc(EFAULT);
}
} else if (config.node_key() == "debug.level") {
if (node->SetConfigMember(config.node_key(), config.node_value(), false)) {
reply.set_std_out("success: setting debug level to '" +
config.node_value() + "'");
} else {
reply.set_std_err("error: failed to store debug level interval");
reply.set_retc(EFAULT);
}
} else {
reply.set_std_err("error: the specified key is not known - consult the "
"usage information of the command");
reply.set_retc(EINVAL);
}
}
}
//----------------------------------------------------------------------------
// Execute config operation affecting the file system parameters
//----------------------------------------------------------------------------
void
NodeCmd::ConfigFsSpecific(const std::set& nodes,
const std::string& key, const std::string& value,
eos::console::ReplyProto& reply)
{
using eos::common::FileSystem;
if ((FileSystem::GetConfigStatusFromString(value.c_str()) ==
eos::common::ConfigStatus::kUnknown)) {
reply.set_std_err("error: not an allowed parameter <" + value + ">");
reply.set_retc(EINVAL);
return;
}
for (const auto& node_path : nodes) {
std::set fsids;
{
// Collect the list of file systems concerned
eos::common::RWMutexReadLock lock(FsView::gFsView.ViewMutex);
auto it = FsView::gFsView.mNodeView.find(node_path);
if (it == FsView::gFsView.mNodeView.end()) {
continue;
}
for (eos::common::FileSystem::fsid_t fsid : *it->second) {
fsids.insert(fsid);
}
}
for (auto fsid : fsids) {
eos::common::RWMutexReadLock lock(FsView::gFsView.ViewMutex);
auto* fs = FsView::gFsView.mIdView.lookupByID(fsid);
if (!fs) {
continue;
}
if (value == "empty") {
// Check if the file system is really empty
if (gOFS->eosFsView->getNumFilesOnFs(fs->GetId())) {
eos_static_info("msg=\"trying to set a file system that still "
"contains files to empty state\" fsid=%lu",
fs->GetId());
reply.set_std_err("error: some file systems are not empty");
reply.set_retc(EINVAL);
break;
}
}
fs->SetString(key.c_str(), value.c_str());
FsView::gFsView.StoreFsConfig(fs, false);
}
gOFS->ConfEngine->AutoSave();
}
}
//------------------------------------------------------------------------------
// Execute set subcommand
//------------------------------------------------------------------------------
void NodeCmd::SetSubcmd(const eos::console::NodeProto_SetProto& set,
eos::console::ReplyProto& reply)
{
std::string nodename = set.node();
const std::string& status = set.node_state_switch();
std::string key = "status";
if (!nodename.length() || !status.length()) {
reply.set_std_err("error: illegal parameter");
reply.set_retc(EINVAL);
return;
}
if ((nodename.find(':') == std::string::npos)) {
nodename += ":1095"; // default eos fst port
}
if ((nodename.find("/eos/") == std::string::npos)) {
nodename.insert(0, "/eos/");
nodename.append("/fst");
}
std::string tident = mVid.tident.c_str();
std::string rnodename = nodename;
{
// for sss + node identification
rnodename.erase(0, 5);
size_t dpos;
if ((dpos = rnodename.find(':')) != std::string::npos) {
rnodename.erase(dpos);
}
if ((dpos = rnodename.find('.')) != std::string::npos) {
rnodename.erase(dpos);
}
size_t addpos = 0;
if ((addpos = tident.find('@')) != std::string::npos) {
tident.erase(0, addpos + 1);
}
}
eos::common::RWMutexWriteLock lock(FsView::gFsView.ViewMutex);
// If EOS_SKIP_SSS_HOSTNAME_MATCH env variable is set then we skip
// the check below as this currently breaks the Kubernetes setup.
bool skip_hostname_match = getenv("EOS_SKIP_SSS_HOSTNAME_MATCH") ? true : false;
if (mVid.uid == 0 || mVid.prot == "sss") {
if (mVid.uid != 0 && mVid.prot == "sss") {
if (!skip_hostname_match &&
tident.compare(0, tident.length(), rnodename, 0, tident.length())) {
reply.set_std_err("error: nodes can only be configured as 'root' or by "
"connecting from the node itself using the sss protocol(1)");
reply.set_retc(EPERM);
return;
}
}
} else {
reply.set_std_err("error: nodes can only be configured as 'root' or by "
"connecting from the node itself using the sss protocol(2)");
reply.set_retc(EPERM);
return;
}
if (!FsView::gFsView.mNodeView.count(nodename)) {
reply.set_std_out("info: creating node '" + nodename + "'");
// reply.set_std_err("error: no such node '" + nodename + "'");
// reply.set_retc(ENOENT);
if (!FsView::gFsView.RegisterNode(nodename.c_str())) {
reply.set_std_err("error: cannot register node <" + nodename + ">");
reply.set_retc(EIO);
return;
}
}
if (!FsView::gFsView.mNodeView[nodename]->SetConfigMember(key, status)) {
reply.set_std_err("error: cannot set node config value");
reply.set_retc(EIO);
return;
}
// set also the manager name
if (!FsView::gFsView.mNodeView[nodename]->SetConfigMember("manager",
gOFS->mMaster->GetMasterId(), true)) {
reply.set_std_err("error: cannot set the manager name");
reply.set_retc(EIO);
return;
}
}
//------------------------------------------------------------------------------
// Execute proxygroup subcommand
//------------------------------------------------------------------------------
void NodeCmd::ProxygroupSubcmd(const eos::console::NodeProto_ProxygroupProto&
proxygroup, eos::console::ReplyProto& reply)
{
std::string nodename = proxygroup.node();
std::string status = (proxygroup.node_proxygroup().length()) ?
proxygroup.node_proxygroup() : "clear";
std::string key = "proxygroup";
eos::console::NodeProto_ProxygroupProto::Action action =
proxygroup.node_action();
if (status.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890._-")
!= std::string::npos) {
status.clear();
}
if (!nodename.length() || !status.length()) {
reply.set_std_err("error: illegal parameter");
reply.set_retc(EINVAL);
return;
}
if ((nodename.find(':') == std::string::npos)) {
nodename += ":1095"; // default eos fst port
}
if ((nodename.find("/eos/") == std::string::npos)) {
nodename.insert(0, "/eos/");
nodename.append("/fst");
}
std::string tident = mVid.tident.c_str();
std::string rnodename = nodename;
{
// for sss + node identification
rnodename.erase(0, 5);
size_t dpos;
if ((dpos = rnodename.find(':')) != std::string::npos) {
rnodename.erase(dpos);
}
if ((dpos = rnodename.find('.')) != std::string::npos) {
rnodename.erase(dpos);
}
size_t addpos = 0;
if ((addpos = tident.find('@')) != std::string::npos) {
tident.erase(0, addpos + 1);
}
}
eos::common::RWMutexWriteLock lock(FsView::gFsView.ViewMutex);
// If EOS_SKIP_SSS_HOSTNAME_MATCH env variable is set then we skip
// the check below as this currently breaks the Kubernetes setup.
bool skip_hostname_match = (getenv("EOS_SKIP_SSS_HOSTNAME_MATCH")) ? true :
false;
if (mVid.uid == 0 || mVid.prot == "sss") {
if (mVid.uid != 0 && mVid.prot == "sss") {
if (!skip_hostname_match &&
tident.compare(0, tident.length(), rnodename, 0, tident.length())) {
reply.set_std_err("error: nodes can only be configured as 'root' or by "
"connecting from the node itself using the sss protocol(1)");
reply.set_retc(EPERM);
return;
}
}
} else {
reply.set_std_err("error: nodes can only be configured as 'root' or by "
"connecting from the node itself using the sss protocol(2)");
reply.set_retc(EPERM);
return;
}
if (!FsView::gFsView.mNodeView.count(nodename)) {
reply.set_std_out("info: creating node '" + nodename + "'");
// reply.set_std_err("error: no such node '" + nodename + "'");
// reply.set_retc(ENOENT);
if (!FsView::gFsView.RegisterNode(nodename.c_str())) {
reply.set_std_err("error: cannot register node <" + nodename + ">");
reply.set_retc(EIO);
return;
}
}
// we need to take the previous version of groupproxys to update it
std::string proxygroups = FsView::gFsView.mNodeView[nodename]->GetConfigMember(
key);
eos_static_debug(" old proxygroups value %s", proxygroups.c_str());
// find a previous occurence
std::set groups;
std::string::size_type pos1 = 0, pos2 = 0;
if (!proxygroups.empty()) {
do {
pos2 = proxygroups.find(',', pos1);
groups.insert(proxygroups.substr(pos1,
pos2 == std::string::npos ? std::string::npos : pos2 - pos1));
pos1 = pos2;
if (pos1 != std::string::npos) {
pos1++;
}
} while (pos2 != std::string::npos);
}
if (action == eos::console::NodeProto_ProxygroupProto::CLEAR) {
proxygroups = "";
} else {
if (action == eos::console::NodeProto_ProxygroupProto::ADD) {
groups.insert(status);
} else if (action == eos::console::NodeProto_ProxygroupProto::RM) {
groups.erase(status);
}
proxygroups.clear();
for (const auto& group : groups) {
proxygroups.append(group + ",");
}
if (!proxygroups.empty()) {
proxygroups.resize(proxygroups.size() - 1);
}
}
eos_static_debug(" new proxygroups value %s", proxygroups.c_str());
status = proxygroups;
if (!FsView::gFsView.mNodeView[nodename]->SetConfigMember(key, status)) {
reply.set_std_err("error: cannot set node config value");
reply.set_retc(EIO);
return;
}
// set also the manager name
if (!FsView::gFsView.mNodeView[nodename]->SetConfigMember("manager",
gOFS->mMaster->GetMasterId(), true)) {
reply.set_std_err("error: cannot set the manager name");
reply.set_retc(EIO);
return;
}
}
EOSMGMNAMESPACE_END