From 0f4556fc2343dc0ade0bb1e0d1fc6f85770d77af Mon Sep 17 00:00:00 2001 From: "Andrey V.Kosteltsev" Date: Fri, 15 Jul 2022 10:36:51 +0300 Subject: First commit: Sila SNMP Sub Agent and configuration manager --- snmpcfg/Makefile.am | 41 +++ snmpcfg/sila-snmp-cfg-manager.service.in | 13 + snmpcfg/snmpcfg-server.cpp | 291 +++++++++++++++++++++ snmpcfg/xyz/openbmc_project/SNMPCfg.interface.yaml | 13 + 4 files changed, 358 insertions(+) create mode 100644 snmpcfg/Makefile.am create mode 100644 snmpcfg/sila-snmp-cfg-manager.service.in create mode 100644 snmpcfg/snmpcfg-server.cpp create mode 100644 snmpcfg/xyz/openbmc_project/SNMPCfg.interface.yaml (limited to 'snmpcfg') diff --git a/snmpcfg/Makefile.am b/snmpcfg/Makefile.am new file mode 100644 index 0000000..5741fd0 --- /dev/null +++ b/snmpcfg/Makefile.am @@ -0,0 +1,41 @@ +bin_PROGRAMS = sila-snmpcfg + +nobase_nodist_include_HEADERS = \ + xyz/openbmc_project/SNMPCfg/server.hpp + +sila_snmpcfg_SOURCES = \ + snmpcfg-server.cpp \ + xyz/openbmc_project/SNMPCfg/server.cpp + +sila_snmpcfg_CXXFLAGS = \ + $(SDBUSPLUS_CFLAGS) \ + $(PHOSPHOR_DBUS_INTERFACES_CFLAGS) \ + $(PHOSPHOR_LOGGING_CFLAGS) +sila_snmpcfg_LDADD = \ + $(SDBUSPLUS_LIBS) \ + $(PHOSPHOR_DBUS_INTERFACES_LIBS) \ + $(PHOSPHOR_LOGGING_LIBS) + +# Be sure to build needed files before compiling +BUILT_SOURCES = \ + xyz/openbmc_project/SNMPCfg/server.cpp \ + xyz/openbmc_project/SNMPCfg/server.hpp + +CLEANFILES=${BUILT_SOURCES} + +xyz/openbmc_project/SNMPCfg/server.cpp: \ +xyz/openbmc_project/SNMPCfg.interface.yaml \ +xyz/openbmc_project/SNMPCfg/server.hpp + @mkdir -p $(@D) + $(SDBUSPLUSPLUS) -r $(srcdir) interface server-cpp \ +xyz.openbmc_project.SNMPCfg > $@ + +xyz/openbmc_project/SNMPCfg/server.hpp: \ +xyz/openbmc_project/SNMPCfg.interface.yaml + @mkdir -p $(@D) + $(SDBUSPLUSPLUS) -r $(srcdir) interface server-header \ +xyz.openbmc_project.SNMPCfg > $@ + +if HAVE_SYSTEMD +systemdsystemunit_DATA = sila-snmp-cfg-manager.service +endif diff --git a/snmpcfg/sila-snmp-cfg-manager.service.in b/snmpcfg/sila-snmp-cfg-manager.service.in new file mode 100644 index 0000000..d80b9a6 --- /dev/null +++ b/snmpcfg/sila-snmp-cfg-manager.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Sila SNMP configuration manager +After=snmpd.service + +[Service] +ExecStart=@bindir@/sila-snmpcfg +SyslogIdentifier=sila-snmpcfg +Restart=always +Type=dbus +BusName=xyz.openbmc_project.SNMPCfg + +[Install] +WantedBy=@SYSTEMD_TARGET@ diff --git a/snmpcfg/snmpcfg-server.cpp b/snmpcfg/snmpcfg-server.cpp new file mode 100644 index 0000000..da76555 --- /dev/null +++ b/snmpcfg/snmpcfg-server.cpp @@ -0,0 +1,291 @@ +/** + * @brief SNMP Configuration manager + * + * This file is part of sila-snmp project. + * + * Copyright (c) 2022 SILA + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace phosphor::logging; +using SNMPCfg_inherit = sdbusplus::server::object_t< + sdbusplus::xyz::openbmc_project::server::SNMPCfg>; + +using InternalFailure = + sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure; +using InvalidArgument = + sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument; +using Argument = xyz::openbmc_project::Common::InvalidArgument; + +static constexpr auto snmpdConf = "/etc/snmp/snmpd.conf"; +static constexpr auto whitespace = " \t"; + +/** + * @brief Check if the string contains the token at the position or after + * several spaces. + * + * @param[in] str - string to be checked + * @param[in] token - expected token + * @param[in/out] pos - start position for checking, will keep the last + * checked position. + * + * @return - true if token found. + */ +static bool getToken(const std::string& str, const std::string& token, + size_t& pos) +{ + pos = str.find_first_not_of(whitespace, pos); + if (pos != std::string::npos && 0 == str.compare(pos, token.size(), token)) + { + auto endPos = str.find_first_of(whitespace, pos); + if (endPos == std::string::npos || pos + token.size() == endPos) + { + pos = endPos; + return true; + } + } + return false; +} + +class Configurator : public SNMPCfg_inherit +{ + public: + /** + * @brief Configurator object constructor + * + * @param bus - DBus connection object reference + * @param path - DBus object path + */ + Configurator(sdbusplus::bus::bus& bus, const char* path) : + SNMPCfg_inherit(bus, path), bus(bus) + { + readConfig(); + } + + std::string community(std::string value) override + { + static constexpr auto communityMaxLen = 256; + static constexpr auto communityMinLen = 1; + + auto communityLen = value.length(); + if (communityLen < communityMinLen || communityLen > communityMaxLen) + { + log("Invalid community name length"); + elog(Argument::ARGUMENT_NAME("Community"), + Argument::ARGUMENT_VALUE(value.c_str())); + } + + SNMPCfg_inherit::community(value); + writeConfig(); + return value; + } + + private: + /** + * @brief Parse line and get community name + * + * @param line - the line from configuration file + * + * @return community name if corresponding statement found + * and nullopt otherwise + */ + std::optional getCommunityName(std::string line) const + { + // Required line should be in format: + // 'com2sec readonly default ' + size_t pos = 0; + + for (const auto& token : {"com2sec", "readonly", "default"}) + { + if (!getToken(line, token, pos) || pos == std::string::npos) + { + return std::nullopt; + } + } + + pos = line.find_first_not_of(whitespace, pos); + if (pos == std::string::npos) + { + return std::nullopt; + } + + auto endPos = line.find_last_not_of(whitespace); + if (pos < endPos && line[pos] == '"' && line[endPos] == '"') + { + pos++; + endPos--; + } + + return (pos <= endPos ? line.substr(pos, endPos - pos + 1) + : std::string{}); + } + + /** + * @brief Read actual settings from configuration file + */ + void readConfig() + { + std::ifstream fileToRead(snmpdConf, std::ios::in); + if (!fileToRead.is_open()) + { + log("Failed to open SNMP daemon configuration file", + entry("FILE_NAME=%s", snmpdConf)); + return; + } + + std::string line; + while (std::getline(fileToRead, line)) + { + auto communityName = getCommunityName(line); + if (communityName) + { + SNMPCfg_inherit::community(*communityName); + return; + } + } + log("Community not found", + entry("FILE_NAME=%s", snmpdConf)); + } + + /** + * @brief Write actual settings to the configuration file. + */ + void writeConfig() + { + std::string tmpFileName{snmpdConf}; + tmpFileName += ".tmp"; + + std::ifstream fileToRead(snmpdConf, std::ios::in); + std::ofstream fileToWrite(tmpFileName, std::ios::out); + if (!fileToRead.is_open()) + { + log("Failed to open SNMP daemon configuration file", + entry("FILE_NAME=%s", snmpdConf)); + return; + } + + if (!fileToWrite.is_open()) + { + log("Failed to create new configuration file", + entry("FILE_NAME=%s", tmpFileName.c_str())); + return; + } + + auto value = SNMPCfg_inherit::community(); + bool isQuoteRequired = + (value.find_first_of(whitespace) != std::string::npos); + bool updated = false; + std::string line; + while (std::getline(fileToRead, line)) + { + auto communityName = getCommunityName(line); + if (communityName && *communityName != value) + { + fileToWrite << "com2sec readonly default\t " + << (isQuoteRequired ? "\"" : "") << value + << (isQuoteRequired ? "\"" : "") << std::endl; + updated = true; + } + else + { + fileToWrite << line << std::endl; + } + } + fileToWrite.close(); + fileToRead.close(); + + if (updated) + { + if (0 != std::rename(tmpFileName.c_str(), snmpdConf)) + { + int error = errno; + log("Failed to update SNMP daemon configuarion", + entry("WHAT=%s", strerror(error)), + entry("FILE_NAME=%s", snmpdConf)); + elog(); + } + + reloadSnmpDaemon(); + } + else + { + if (0 != std::remove(tmpFileName.c_str())) + { + int error = errno; + log("Failed to remove temporary file", + entry("WHAT=%s", strerror(error)), + entry("FILE_NAME=%s", tmpFileName.c_str())); + } + } + } + + sdbusplus::bus::bus& bus; + + /** + * @brief Ask SNMP daemon to reread configuration. + */ + void reloadSnmpDaemon() + { + auto m = bus.new_method_call( + "org.freedesktop.systemd1", "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", "ReloadUnit"); + m.append("snmpd.service", "replace"); + try + { + bus.call_noreply(m); + } + catch (const sdbusplus::exception::SdBusError& e) + { + log("Failed to reload SNMP daemon", + entry("WHATE=%s", e.what())); + elog(); + } + } +}; + +/** + * @brief Application entry point + * + * @return exit status + */ +int main() +{ + constexpr auto path = "/xyz/openbmc_project/snmpcfg"; + + auto b = sdbusplus::bus::new_default(); + sdbusplus::server::manager_t m{b, path}; + + b.request_name("xyz.openbmc_project.SNMPCfg"); + Configurator cfg{b, path}; + + while (1) + { + b.process_discard(); + b.wait(); + } + + return 0; +} diff --git a/snmpcfg/xyz/openbmc_project/SNMPCfg.interface.yaml b/snmpcfg/xyz/openbmc_project/SNMPCfg.interface.yaml new file mode 100644 index 0000000..5017f7b --- /dev/null +++ b/snmpcfg/xyz/openbmc_project/SNMPCfg.interface.yaml @@ -0,0 +1,13 @@ +description: > + SNMP daemon configuration manager. + +properties: + - name: Community + type: string + description: > + SNMPv1 and SNMPv2 community that allows read-only (GET and GETNEXT) access + to the data providing by SNMP daemon. + errors: + - xyz.openbmc_project.Common.Error.InternalFailure + - xyz.openbmc_project.Common.Error.InvalidArgument + -- cgit v1.2.3