Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions db/knex_migrations/2023-10-23-add-nut-monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
exports.up = function (knex) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the description you mentioned the following, what are your questions?

Some questions I had were regarding the database schema,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to know if these column names are OK and in keeping with the conventions, I think I did the right thing based on what else I saw.

// Add new column
return knex.schema
.alterTable("monitor", function (table) {
table.text("ups_name");
table.text("nut_username");
table.text("nut_password");
});

};

exports.down = function (knex) {
// Drop nut variable column
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("ups_name");
table.dropColumn("nut_username");
table.dropColumn("nut_password");
});
};
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"favico.js": "~0.3.10",
"jest": "~29.6.1",
"marked": "~4.2.5",
"node-nut": "^1.0.3",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if this library is a good idea…
skarcha/node-nut#9 (comment)

"node-ssh": "~13.1.0",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
Expand Down
3 changes: 3 additions & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class Monitor extends BeanModel {
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
upsName: this.upsName,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
grpcUrl: this.grpcUrl,
Expand Down Expand Up @@ -173,6 +174,8 @@ class Monitor extends BeanModel {
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
nutUsername: this.nutUsername,
nutPassword: this.nutPassword,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
tlsCa: this.tlsCa,
Expand Down
64 changes: 64 additions & 0 deletions server/monitor-types/nut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN } = require("../../src/util");
const dayjs = require("dayjs");
const jsonata = require("jsonata");
const Nut = require("node-nut");

const { log } = require("../../src/util");

class NutMonitorType extends MonitorType {

name = "nut";

/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node-nut does not seem to include a timeout function.
Given that this is doing network calls, adding such an option would be a nice touch.

let startTime = dayjs().valueOf();
let expression = jsonata(monitor.jsonPath);

const nut = new Nut(monitor.port, monitor.hostname);

nut.on("ready", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be me not knowing js enough, but I think this function is racable (have not debugged this, just a hunch)

  • First observation is, that none of the functions which are called are actually blocking (): https://www.npmjs.com/package/node-nut?activeTab=code
  • if Nut does not immediatly become ready, heartbeat being set might be missed as the event loop has already exited this function

I think this might be why you are seeing

WARN: The ping is not effective when the status is DOWN

I think the following code looks more promissing:
https://github.com/skarcha/node-nut/blob/2729b2f3a6d1bb9d9105c68c0b1cdc62933c0f81/examples/polling.js

nut.GetUPSList((upslist, err) => {
if (err) {
nut.close();
log.error("NUT Error: " + err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not returning here intentional?
Further code assumes that there has not been an error and that nut is still open

}

let upsname = upslist[monitor.upsName] && monitor.upsName || Object.keys(upslist)[0];

nut.GetUPSVars(upsname, async (vars, err) => {
nut.close();
if (err) {
throw new Error("Error getting UPS variables");
} else {
// convert data to object
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify that data can be a json-string or object according to the usage?

if (typeof vars === "string") {
vars = JSON.parse(vars);
}
const data = ({ ...vars }); // copy vars
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you clarify the comment why this is nessesary?


// Check device status
let result = await expression.evaluate(data);

if (result.toString() === monitor.expectedValue) {
heartbeat.status = UP;
heartbeat.msg = "";
heartbeat.ping = dayjs().valueOf() - startTime;
} else {
heartbeat.status = DOWN;
heartbeat.msg = `Value not expected, value was: [${result}]`;
}
}
});
});
});

nut.start();
}
}

module.exports = {
NutMonitorType,
};
3 changes: 3 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,8 @@ let needSetup = false;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword;
bean.nutUsername = monitor.nutUsername;
bean.nutPassword = monitor.nutPassword;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.databaseConnectionString = monitor.databaseConnectionString;
Expand Down Expand Up @@ -824,6 +826,7 @@ let needSetup = false;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.upsName = monitor.upsName;

bean.validate();

Expand Down
2 changes: 2 additions & 0 deletions server/uptime-kuma-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["nut"] = new NutMonitorType();

this.io = new Server(this.httpServer);
}
Expand Down Expand Up @@ -433,3 +434,4 @@ module.exports = {
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { NutMonitorType } = require("./monitor-types/nut");
1 change: 1 addition & 0 deletions src/pages/Details.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<span v-if="monitor.type === 'redis'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'sqlserver'">SQL Server: {{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'nut'">NUT: {{monitor.upsName}} on {{ monitor.hostname }}:{{ monitor.port }}</span>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a translation for on using named-interpolation or a similar mechanism allowing our translators to translate this ^^

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are some linting warnings associated with this line

</p>

<div class="functions">
Expand Down
39 changes: 31 additions & 8 deletions src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
<option value="mqtt">
MQTT
</option>
<option value="nut">
Network UPS Tools
</option>
<option value="kafka-producer">
Kafka Producer
</option>
Expand Down Expand Up @@ -144,8 +147,26 @@
</div>
</div>

<!-- Json Query -->
<div v-if="monitor.type === 'json-query'" class="my-3">
<!-- NUT -->
<!-- For NUT Type -->
<template v-if="monitor.type === 'nut'">
<div class="my-3">
<label for="upsName" class="form-label">UPS {{ $t("Name") }}</label>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding a description on where to obtain the correct ups name might be worth it to prevent other users spending excessive time on this field

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I agree, do you think it makes sense to add it on the field description? If it's too long, where would that sort of documentation go?

Screenshot as an example of where it could live:
image

Copy link
Collaborator

@CommanderStorm CommanderStorm Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unshure if a longer name like name of the uninterruptible power supply might be clearer and more importantly translate better.
In any case, I think it would be better if the word order ups name is correct in all languages (some are rtl) => It might be saver to include this in the translated string
Thoughts?

<input id="upsName" v-model="monitor.upsName" type="text" class="form-control" required>
</div>
<div class="my-3">
<label for="nutUsername" class="form-label">{{ $t("Username") }}</label>
<input id="nutUsername" v-model="monitor.nutUsername" type="text" class="form-control">
</div>

<div class="my-3">
<label for="nutPassword" class="form-label">{{ $t("Password") }}</label>
<input id="nutPassword" v-model="monitor.nutPassword" type="password" class="form-control">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you utilise the HiddenInput component instead for all secret fields?

</div>
</template>

<!-- Json Query and NUT-->
<div v-if="monitor.type === 'json-query' || monitor.type === 'nut'" class="my-3">
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>

Expand Down Expand Up @@ -221,15 +242,15 @@
</template>

<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping'" class="my-3">
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / NUT only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'nut'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
</div>

<!-- Port -->
<!-- For TCP Port / Steam / MQTT / Radius Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<!-- For TCP Port / Steam / MQTT / Radius Type / NUT only -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'nut'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
Expand Down Expand Up @@ -1156,12 +1177,14 @@ message HealthCheckResponse {
}
}

// Set default port for DNS if not already defined
if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") {
// Set default port for DNS, RADIUS, NUT if not already defined
if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812" || this.monitor.port === "3493") {
if (this.monitor.type === "dns") {
this.monitor.port = "53";
} else if (this.monitor.type === "radius") {
this.monitor.port = "1812";
} else if (this.monitor.type === "nut") {
this.monitor.port = "3493";
} else {
this.monitor.port = undefined;
}
Expand Down