Commit a7aee5ba authored by jan.koester's avatar jan.koester
Browse files

test

parent 4eba6675
Loading
Loading
Loading
Loading
+5 −1
Changes for CMakeLists.txt: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ project(webctl VERSION 0.2.3 LANGUAGES CXX)
#                    after binding
#   modules/         one shared object per area (dashboard, users, network, shares,
#                    raid, lvm, filesystems, services, journal, updates, snmp,
#                    time, system),
#                    time, ups, system),
#                    dlopen()ed by the backend from its module directory
#   www/             the AJAX UI; www/modules/<name>.js per module

@@ -129,6 +129,10 @@ target_link_libraries(webctl_mod_snmp PRIVATE sysbus::sysbus)
# (ntpplus-dev)
webctl_module(time modules/time/time.cpp)
target_link_libraries(webctl_mod_time PRIVATE sysbus::sysbus)
# UPS: an APC UPS behind its network management card, a client of upsd
# over sysbus; needs upsd/api.h (upsd-dev)
webctl_module(ups modules/ups/ups.cpp)
target_link_libraries(webctl_mod_ups PRIVATE sysbus::sysbus)

install(TARGETS webctl-backend webctl-frontend RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
install(DIRECTORY www/ DESTINATION ${WEBCTL_WWWDIR})
+17 −6
Changes for modules/network/network.cpp: 17 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -36,10 +36,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   start | stop | restart  {interface}
 *   reload | forceReload    {interface?}          (none: all)
 *
 * Bonds, bridges and WireGuard interfaces (.netdev files; reload without
 * an interface creates them, adds their members, configures WireGuard):
 *   createNetdev  {netdev, kind: bond|bridge|wireguard, mode?, miimon?, lacpRate?, hashPolicy?, stp?, members?[],
 *                  privateKey?, generateKey?, listenPort?, routes?, peers?[]}
 * Bonds, bridges, VLANs and WireGuard interfaces (.netdev files; reload
 * without an interface creates them, adds their members, configures
 * WireGuard):
 *   createNetdev  {netdev, kind: bond|bridge|vlan|wireguard, mode?, miimon?, lacpRate?, hashPolicy?, stp?, members?[],
 *                  vlanId?, parent?, privateKey?, generateKey?, listenPort?, routes?, peers?[]}
 *   updateNetdev  {netdev, ...the same, without kind}
 *   deleteNetdev  {netdev}                        (also removes the device)
 *   generateKey                                   -> {privateKey, publicKey, presharedKey}
@@ -92,7 +93,11 @@ json_object *toJson(const sysbus::InterfaceInfo &i) {
    json_object_object_add(o, "dns", dns);
    json_object_object_add(o, "kind", jstr(i.kind));
    json_object_object_add(o, "master", jstr(i.master));
    json_object_object_add(o, "parent", jstr(i.parent));
    json_object_object_add(o, "memberOf", jstr(i.memberOf));
    json_object *vlans = json_object_new_array();
    for (auto &v : i.vlans) json_object_array_add(vlans, jstr(v));
    json_object_object_add(o, "vlans", vlans);
    return o;
}

@@ -105,6 +110,10 @@ json_object *netdevJson(const sysbus::NetDevInfo &n) {
    json_object_object_add(o, "lacpRate", jstr(n.lacpRate));
    json_object_object_add(o, "hashPolicy", jstr(n.hashPolicy));
    json_object_object_add(o, "stp", json_object_new_boolean(n.stp));
    if (n.kind == "vlan") {
        json_object_object_add(o, "vlanId", json_object_new_int(n.vlanId));
        json_object_object_add(o, "parent", jstr(n.parent));
    }
    json_object_object_add(o, "config", jstr(n.config));
    json_object_object_add(o, "configPath", jstr(n.configPath));
    json_object_object_add(o, "exists", json_object_new_boolean(n.exists));
@@ -188,6 +197,8 @@ bool netdevSettings(json_object *a, sysbus::NetDevSettings &s, std::string &err)
    if (webctl::has(a, "lacpRate")) s.lacpRate = webctl::str(a, "lacpRate");
    if (webctl::has(a, "hashPolicy")) s.hashPolicy = webctl::str(a, "hashPolicy");
    if (webctl::has(a, "stp")) s.stp = webctl::boolean(a, "stp");
    if (webctl::has(a, "vlanId")) s.vlanId = (int)webctl::integer(a, "vlanId", 0);
    if (webctl::has(a, "parent")) s.parent = webctl::str(a, "parent");
    json_object *members = nullptr;
    if (json_object_object_get_ex(a, "members", &members) && members) {
        std::vector<std::string> list;
@@ -268,8 +279,8 @@ json_object *call(const webctl_request *req) {
        sysbus::NetDevSettings s;
        std::string err;
        if (!netdevSettings(a, s, err)) return error(err);
        if (action == "createNetdev" && s.kind != "bond" && s.kind != "bridge" && s.kind != "wireguard")
            return error(tr("kind must be bond, bridge or wireguard", "Art muss bond, bridge oder wireguard sein"));
        if (action == "createNetdev" && s.kind != "bond" && s.kind != "bridge" && s.kind != "vlan" && s.kind != "wireguard")
            return error(tr("kind must be bond, bridge, vlan or wireguard", "Art muss bond, bridge, vlan oder wireguard sein"));
        bool done = action == "createNetdev" ? bus.createNetDev(s) : bus.editNetDev(s);
        return done ? webctl::ok() : busError(bus);
    }

modules/ups/ups.cpp

0 → 100644
+237 −0
Changes for modules/ups/ups.cpp: 237 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* webctl module "ups": an APC UPS behind its network management card,
 * entirely over upsd's sysbus API (include/upsd/api.h). upsd's fields
 * become camelCase keys (battery_runtime -> batteryRuntime); numbers are
 * numbers (null: not known), yes/no are booleans.
 *
 *   info                -> {settings, state}
 *       settings: address, port, version, community, writeCommunitySet,
 *           user, level, auth, authPasswordSet, priv, privPasswordSet,
 *           interval, shutdown, shutdownDelay, shutdownRuntime,
 *           shutdownCapacity, upsOff
 *       state: what GetStatus says (state, error, lastPoll, model, ...,
 *           shutdown, shutdownIn, shutdownReason)
 *   save {settings}     as above; writeCommunity, authPassword,
 *                       privPassword: empty or missing keeps them
 *   test {settings}     the connection with these settings, not kept ->
 *                       {model, name, firmware, serial, outputStatus,
 *                       engineId}
 *   ups                 the UPS's own settings, read from it ->
 *                       {values{name, batteryReplaced, ...}, allowed{key:
 *                       "253,264,..."}}
 *   upsSave {values}    the UPS settings to change (strings or numbers)
 *   selftest, beep      commands to the UPS
 *   calibrate {cancel}  runtime calibration
 */

#include <cctype>
#include <cstdlib>
#include <set>
#include <string>
#include <vector>

#include <sysbus/client.h>
#include <upsd/api.h>
#include <webctl/module.h>

using webctl::error;
using webctl::jstr;
using webctl::tr;

namespace {

using Fields = std::vector<std::pair<std::string, std::string>>;

const char *yesNo(bool b) { return b ? "yes" : "no"; }

// battery_runtime -> batteryRuntime
std::string camel(const std::string &k) {
    std::string out;
    bool up = false;
    for (char c : k) {
        if (c == '_') up = true;
        else out += up ? (char)toupper((unsigned char)c) : c, up = false;
    }
    return out;
}

// batteryRuntime -> battery_runtime
std::string snake(const std::string &k) {
    std::string out;
    for (char c : k) {
        if (isupper((unsigned char)c)) out += '_', out += (char)tolower((unsigned char)c);
        else out += c;
    }
    return out;
}

// a call to upsd; on failure the error object to return
json_object *callUpsd(const char *method, const Fields &args, sysbus::Message &reply) {
    sysbus::Client bus;
    if (!bus.connect())
        return error(tr("sysbus isn't reachable: %1", "sysbus ist nicht erreichbar: %1", {bus.lastError()}), 503);
    Fields all = args;
    all.emplace_back("lang", webctl::language());
    // SNMP to the card: a few seconds when it doesn't answer, the SNMPv3
    // key localization on top
    reply = bus.callService(upsd::Service, method, all, 60000);
    if (reply.type != sysbus::MessageType::Error) return nullptr;
    const std::string &name = reply.member;
    if (name == sysbus::error::ServiceUnknown)
        return error(tr("upsd isn't running (Services: upsd)", "upsd läuft nicht (Dienste: upsd)"), 503);
    int status = name == sysbus::error::AccessDenied ? 403 : name == sysbus::error::InvalidArgs ? 400 : 500;
    return error(reply.get("message", name), status);
}

// the reply's fields as an object: the keys in numbers become numbers
// (null if empty), those in flags booleans, the rest strings
json_object *fieldsJson(const sysbus::Message &r, const std::set<std::string> &numbers,
                        const std::set<std::string> &flags) {
    json_object *o = json_object_new_object();
    for (auto &f : r.fields) {
        const std::string &k = f.first, &v = f.second;
        if (k == "lang" || k.compare(0, 7, "sysbus.") == 0) continue;
        json_object *val;
        if (numbers.count(k)) val = v.empty() ? nullptr : json_object_new_double(std::strtod(v.c_str(), nullptr));
        else if (flags.count(k)) val = v.empty() ? nullptr : json_object_new_boolean(v == "yes");
        else val = jstr(v);
        json_object_object_add(o, camel(k).c_str(), val);
    }
    return o;
}

const std::set<std::string> StatusNumbers = {
    "last_poll", "battery_capacity", "battery_runtime", "battery_temperature", "battery_voltage",
    "battery_nominal_voltage", "battery_packs", "battery_bad_packs", "time_on_battery", "input_voltage",
    "input_voltage_min", "input_voltage_max", "input_frequency", "output_voltage", "output_frequency", "output_load",
    "output_current", "shutdown_in"};
const std::set<std::string> StatusFlags = {"battery_replace"};
const std::set<std::string> SettingsNumbers = {"port", "interval", "shutdown_delay", "shutdown_runtime",
                                               "shutdown_capacity"};
const std::set<std::string> SettingsFlags = {"write_community_set", "auth_password_set", "priv_password_set",
                                             "shutdown", "ups_off"};

// the settings from the UI as upsd's fields; "" or why not
std::string settingsFields(json_object *s, Fields &out) {
    if (!s || !json_object_is_type(s, json_type_object)) return tr("no settings given", "keine Einstellungen angegeben");
    json_object_object_foreach(s, key, val) {
        std::string k = snake(key);
        if (k == "write_community_set" || k == "auth_password_set" || k == "priv_password_set") continue;
        // secrets: empty keeps the one upsd has
        if (k == "write_community" || k == "auth_password" || k == "priv_password") {
            std::string v = val ? json_object_get_string(val) : "";
            if (!v.empty()) out.emplace_back(k, v);
        } else if (k == "shutdown" || k == "ups_off") {
            out.emplace_back(k, yesNo(val && json_object_get_boolean(val)));
        } else if (k == "address" || k == "port" || k == "version" || k == "community" || k == "user" ||
                   k == "level" || k == "auth" || k == "priv" || k == "interval" || k == "shutdown_delay" ||
                   k == "shutdown_runtime" || k == "shutdown_capacity") {
            if (val && json_object_is_type(val, json_type_double))
                out.emplace_back(k, std::to_string((long long)json_object_get_double(val)));
            else
                out.emplace_back(k, val ? json_object_get_string(val) : "");
        } else {
            return tr("unknown setting %1", "unbekannte Einstellung %1", {key});
        }
    }
    return "";
}

json_object *simple(const char *method, const Fields &f = {}) {
    sysbus::Message r;
    if (json_object *e = callUpsd(method, f, r)) return e;
    return webctl::ok();
}

json_object *call(const webctl_request *req) {
    webctl::LanguageScope lang(req->lang);
    std::string action = req->action;
    sysbus::Message reply;

    if (action == "info") {
        if (json_object *err = callUpsd(upsd::method::GetSettings, {}, reply)) return err;
        json_object *settings = fieldsJson(reply, SettingsNumbers, SettingsFlags);
        if (json_object *err = callUpsd(upsd::method::GetStatus, {}, reply)) {
            json_object_put(settings);
            return err;
        }
        json_object *out = json_object_new_object();
        json_object_object_add(out, "settings", settings);
        json_object_object_add(out, "state", fieldsJson(reply, StatusNumbers, StatusFlags));
        return out;
    }
    if (action == "save" || action == "test") {
        json_object *s = nullptr;
        json_object_object_get_ex(req->args, "settings", &s);
        Fields f;
        std::string bad = settingsFields(s, f);
        if (!bad.empty()) return error(bad);
        if (action == "save") return simple(upsd::method::SetSettings, f);
        if (json_object *err = callUpsd(upsd::method::TestConnection, f, reply)) return err;
        return fieldsJson(reply, {}, {});
    }
    if (action == "ups") {
        if (json_object *err = callUpsd(upsd::method::GetUpsSettings, {}, reply)) return err;
        json_object *values = json_object_new_object(), *allowed = json_object_new_object();
        for (auto &f : reply.fields) {
            if (f.first == "lang" || f.first.compare(0, 7, "sysbus.") == 0) continue;
            if (f.first.compare(0, 8, "allowed_") == 0)
                json_object_object_add(allowed, camel(f.first.substr(8)).c_str(), jstr(f.second));
            else
                json_object_object_add(values, camel(f.first).c_str(), jstr(f.second));
        }
        json_object *out = json_object_new_object();
        json_object_object_add(out, "values", values);
        json_object_object_add(out, "allowed", allowed);
        return out;
    }
    if (action == "upsSave") {
        json_object *values = nullptr;
        json_object_object_get_ex(req->args, "values", &values);
        if (!values || !json_object_is_type(values, json_type_object))
            return error(tr("no settings given", "keine Einstellungen angegeben"));
        Fields f;
        json_object_object_foreach(values, key, val) {
            if (val && json_object_is_type(val, json_type_double))
                f.emplace_back(snake(key), std::to_string((long long)json_object_get_double(val)));
            else
                f.emplace_back(snake(key), val ? json_object_get_string(val) : "");
        }
        return simple(upsd::method::SetUpsSettings, f);
    }
    if (action == "selftest") return simple(upsd::method::SelfTest);
    if (action == "beep") return simple(upsd::method::Beep);
    if (action == "calibrate")
        return simple(upsd::method::Calibrate, {{"cancel", yesNo(webctl::boolean(req->args, "cancel"))}});
    return error(tr("unknown action %1", "unbekannte Aktion %1", {action}), 404);
}

} // namespace

WEBCTL_MODULE({WEBCTL_MODULE_ABI, "ups", "UPS", 39, nullptr, nullptr, call});
+144 −8

File changed.

Preview size limit exceeded, changes collapsed.

www/modules/ups.js

0 → 100644
+442 −0

File added.

Preview size limit exceeded, changes collapsed.