Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add SNMP Monitor #4717

Merged
merged 75 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
d92003e
SNMP Initial Commits
mattv8 Apr 27, 2024
a3cdd69
Use net-snmp instead of snmp-native
mattv8 Apr 29, 2024
ff5890a
Updated a comment
mattv8 Apr 29, 2024
4a882be
Further SNMP monitor development
mattv8 Apr 29, 2024
99dc4cf
Wrong variable used
mattv8 Apr 30, 2024
138075a
Update db migration: allow nulls
mattv8 Apr 30, 2024
9c8024c
Update db migration: down function
mattv8 Apr 30, 2024
9d28fcf
Update bean model backend
mattv8 Apr 30, 2024
4593afb
Frontend input validation
mattv8 Apr 30, 2024
9848ce4
Minor frontend styling
mattv8 Apr 30, 2024
704ffd3
Finalized SNMP monitor
mattv8 Apr 30, 2024
b4bd003
Merge branch 'master' into snmp-monitor
CommanderStorm Apr 30, 2024
ba47aca
Apply suggestions from code review
mattv8 Apr 30, 2024
7459654
ES Lint Compliant
mattv8 May 1, 2024
e944492
Corrected down function
mattv8 May 1, 2024
97a9094
ES Lint Compliant
mattv8 May 1, 2024
9ba0f68
Remove supurfluous log.debug
mattv8 May 1, 2024
ba84f01
Delete .EditMonitor.vue.swp
mattv8 May 1, 2024
4699a1c
ES Lint Compliant
mattv8 May 1, 2024
d83c2b9
Revert unintentional changes to EditMonitor.vue
mattv8 May 2, 2024
f059d54
Use frontend timeout
mattv8 May 2, 2024
8e56a81
Refactor how strings/numerics are parsed
mattv8 May 2, 2024
c87ac2f
Move getKey() to util.ts
mattv8 May 3, 2024
09fd816
Updated code comments
mattv8 May 3, 2024
407f729
New dependency for net-snmp
mattv8 May 3, 2024
9053b48
Merge branch 'louislam:master' into snmp-monitor
mattv8 May 3, 2024
4386d0a
Apply suggestions from code review
mattv8 May 5, 2024
0280b2a
A comment about varbinds[0] for clarification
mattv8 May 6, 2024
86b997c
Limit to <= SNMPv2c for now
mattv8 May 6, 2024
0384b34
Remove unnecessary func getKey
mattv8 May 6, 2024
997791b
Default: invalid condition error
mattv8 May 6, 2024
1fe1bb5
Given that above throws, the else case is not nessesary
mattv8 May 6, 2024
433e317
Simplify error catch
mattv8 May 6, 2024
6037912
Consistent placeholder text
mattv8 May 6, 2024
c68b1c6
Remove unnecessary func getKey
mattv8 May 6, 2024
e9b52eb
Separate error cases for SNMP varbind returns
mattv8 May 6, 2024
4ef66b3
SNMP version helptext
mattv8 May 6, 2024
19f21a9
SNMP OID helptext
mattv8 May 6, 2024
56e7fa8
Helptext ALL THE THINGS
mattv8 May 6, 2024
f4842ea
Translation key for OID
mattv8 May 6, 2024
2b5d100
Ensure SNMP session is closed properly
mattv8 May 6, 2024
e5fb726
Missed changes leftover from removal of getKey()
mattv8 May 7, 2024
2015142
Maybe don't helptext all the things...
mattv8 May 7, 2024
8b4b27f
Final cleanup of changes to EditMonitor.vue
mattv8 May 7, 2024
da8f0d1
Apply suggestions from code review
mattv8 May 8, 2024
1c47407
Re-use monitor.radiusPassword for community string
mattv8 May 8, 2024
c475994
Fix ES Lint
mattv8 May 8, 2024
d25ee8f
Using JSON Query Expressions
mattv8 May 10, 2024
7eee5db
Variable changes
mattv8 Jun 5, 2024
b2d76bc
Refactor line for conciseness
mattv8 Jun 5, 2024
2d2c186
Fix: a typo
mattv8 Jun 5, 2024
efb1642
Blend json-query and snmp monitors
mattv8 Jun 5, 2024
36dc94b
Better type handling
mattv8 Jun 6, 2024
10d3188
Query json directly rather than with $.value
mattv8 Jun 6, 2024
eaa935c
Also return result of the evaluation
mattv8 Jun 6, 2024
fdc145b
Added Robustness
mattv8 Jun 7, 2024
23f844d
Error handling robustness
mattv8 Jun 7, 2024
8235291
Fix: Cast to string then eval
mattv8 Jun 10, 2024
e2e8109
Helpful error when query returns object or array
mattv8 Jun 10, 2024
43bd09b
Update 2024-04-26-0000-snmp-monitor.js
mattv8 Jun 10, 2024
69c22ed
Removed "Control Value"
mattv8 Jun 12, 2024
5dc4bb6
Merge branch 'master' into snmp-monitor
CommanderStorm Jun 12, 2024
b5a73e5
Apply suggestions from code review
mattv8 Jun 12, 2024
248aec8
Formtting fix
CommanderStorm Jun 12, 2024
c124f3a
Formtting fix
CommanderStorm Jun 12, 2024
9820f57
Truncate long responses
mattv8 Jun 13, 2024
6fc0cbf
ES Lint
mattv8 Jun 13, 2024
092688a
ES Lint
mattv8 Jun 13, 2024
46ecb82
"Hostname or IP Address" back to "Hostname"
mattv8 Jun 13, 2024
a1f31f9
Remove jsonQueryDescription from lang files
mattv8 Jun 13, 2024
e237d66
"Hostname or IP Address" back to "Hostname"
mattv8 Jun 13, 2024
a037448
C&P typo from review
mattv8 Jun 14, 2024
8d8ce23
Robustness and edge-case handling
mattv8 Jun 14, 2024
71f9384
Merge branch 'master' into snmp-monitor
CommanderStorm Jul 15, 2024
c82de20
fixed merge-typo
CommanderStorm Jul 15, 2024
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
16 changes: 16 additions & 0 deletions db/knex_migrations/2024-04-26-0000-snmp-monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("snmp_oid").defaultTo(null);
table.enum("snmp_version", [ "1", "2c", "3" ]).defaultTo("2c");
table.string("json_path_operator").defaultTo(null);
});
};

exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("snmp_oid");
table.dropColumn("snmp_version");
table.dropColumn("json_path_operator");
});
};
31 changes: 31 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 @@ -119,6 +119,7 @@
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
Expand Down
26 changes: 9 additions & 17 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
SQL_DATETIME_FORMAT, evaluateJsonQuery
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
Expand All @@ -17,7 +17,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
Expand Down Expand Up @@ -161,6 +160,9 @@ class Monitor extends BeanModel {
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
remote_browser: this.remote_browser,
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
};

if (includeSensitiveData) {
Expand Down Expand Up @@ -598,25 +600,15 @@ class Monitor extends BeanModel {
} else if (this.type === "json-query") {
let data = res.data;

// convert data to object
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
try {
data = JSON.parse(data);
} catch (_) {
// Failed to parse as JSON, just process it as a string
}
}
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);

let expression = jsonata(this.jsonPath);

let result = await expression.evaluate(data);

if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
if (status) {
bean.status = UP;
bean.msg = `JSON query passes (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
throw new Error(`JSON query does not pass (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`);
}

}

} else if (this.type === "port") {
Expand Down
63 changes: 63 additions & 0 deletions server/monitor-types/snmp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const { MonitorType } = require("./monitor-type");
const { UP, log, evaluateJsonQuery } = require("../../src/util");
const snmp = require("net-snmp");

class SNMPMonitorType extends MonitorType {
name = "snmp";

/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let session;
try {
const sessionOptions = {
port: monitor.port || "161",
retries: monitor.maxretries,
timeout: monitor.timeout * 1000,
version: snmp.Version[monitor.snmpVersion],
};
session = snmp.createSession(monitor.hostname, monitor.radiusPassword, sessionOptions);

// Handle errors during session creation
session.on("error", (error) => {
throw new Error(`Error creating SNMP session: ${error.message}`);
});

const varbinds = await new Promise((resolve, reject) => {
mattv8 marked this conversation as resolved.
Show resolved Hide resolved
session.get([ monitor.snmpOid ], (error, varbinds) => {
error ? reject(error) : resolve(varbinds);
});
});
log.debug("monitor", `SNMP: Received varbinds (Type: ${snmp.ObjectType[varbinds[0].type]} Value: ${varbinds[0].value})`);

if (varbinds.length === 0) {
throw new Error(`No varbinds returned from SNMP session (OID: ${monitor.snmpOid})`);
mattv8 marked this conversation as resolved.
Show resolved Hide resolved
}

if (varbinds[0].type === snmp.ObjectType.NoSuchInstance) {
throw new Error(`The SNMP query returned that no instance exists for OID ${monitor.snmpOid}`);
}

// We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in.
const value = varbinds[0].value;

const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue);

if (status) {
heartbeat.status = UP;
heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`;
} else {
throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`);
}
} finally {
if (session) {
session.close();
}
}
}
}

module.exports = {
SNMPMonitorType,
};
4 changes: 4 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,10 @@ let needSetup = false;
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;
bean.snmpVersion = monitor.snmpVersion;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout;

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 @@ -113,6 +113,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();

// Allow all CORS origins (polling) in development
Expand Down Expand Up @@ -517,4 +518,5 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
1 change: 0 additions & 1 deletion src/lang/bg-BG.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,6 @@
"twilioApiKey": "API ключ (по избор)",
"Expected Value": "Очаквана стойност",
"Json Query": "Заявка тип JSON",
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте {0} за документация относно езика на заявката. Имате възможност да тествате {1}.",
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
"Badge Preview": "Преглед на баджа",
"Notify Channel": "Канал за известяване",
Expand Down
1 change: 0 additions & 1 deletion src/lang/cs-CZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,6 @@
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na {0}, a využít můžete též {1}.",
"Select": "Vybrat",
"selectedMonitorCount": "Vybráno: {0}",
"Check/Uncheck": "Vybrat/Zrušit výběr",
Expand Down
1 change: 0 additions & 1 deletion src/lang/de-CH.json
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,6 @@
"Json Query": "Json-Abfrage",
"filterActive": "Aktiv",
"filterActivePaused": "Pausiert",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
"Badge Duration (in hours)": "Abzeichen Dauer (in Stunden)",
"Badge Preview": "Abzeichen Vorschau",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
Expand Down
1 change: 0 additions & 1 deletion src/lang/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,6 @@
"filterActivePaused": "Pausiert",
"Expected Value": "Erwarteter Wert",
"Json Query": "Json-Abfrage",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
"pushDeerServerDescription": "Leer lassen um den offiziellen Server zu verwenden",
Expand Down
11 changes: 9 additions & 2 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"Keyword": "Keyword",
"Invert Keyword": "Invert Keyword",
"Expected Value": "Expected Value",
"Json Query": "Json Query",
"Json Query Expression": "Json Query Expression",
"Friendly Name": "Friendly Name",
"URL": "URL",
"Hostname": "Hostname",
Expand Down Expand Up @@ -588,7 +588,7 @@
"notificationDescription": "Notifications must be assigned to a monitor to function.",
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out {0} for the documentation about the query language. A playground can be found {1}.",
"jsonQueryDescription": "Parse and extract specific data from the server's JSON response using JSON query or use \"$\" for the raw response, if not expecting JSON. The result is then compared to the expected value, as strings. See {0} for documentation and use {1} to experiment with queries.",
mattv8 marked this conversation as resolved.
Show resolved Hide resolved
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
"backupDescription2": "Note: history and event data is not included.",
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
Expand Down Expand Up @@ -943,6 +943,13 @@
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
"max 15 digits": "max 15 digits",
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
"Community String": "Community String",
"snmpCommunityStringHelptext": "This string functions as a password to authenticate and control access to SNMP-enabled devices. Match it with your SNMP device's configuration.",
"OID (Object Identifier)": "OID (Object Identifier)",
"snmpOIDHelptext": "Enter the OID for the sensor or status you want to monitor. Use network management tools like MIB browsers or SNMP software if you're unsure about the OID.",
"Condition": "Condition",
"SNMP Version": "SNMP Version",
"Please enter a valid OID.": "Please enter a valid OID.",
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
"threemaRecipient": "Recipient",
"threemaRecipientType": "Recipient Type",
Expand Down
1 change: 0 additions & 1 deletion src/lang/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,6 @@
"Json Query": "Consulta Json",
"invertKeywordDescription": "Comprobar si la palabra clave está ausente en vez de presente.",
"enableNSCD": "Habilitar NSCD (Demonio de Caché de Servicio de Nombres) para almacenar en caché todas las solicitudes DNS",
"jsonQueryDescription": "Realiza una consulta JSON contra la respuesta y verifica el valor esperado (el valor de retorno se convertirá a una cadena para la comparación). Consulta {0} para obtener documentación sobre el lenguaje de consulta. Puede encontrar un espacio de prueba {1}.",
"Request Timeout": "Tiempo de espera máximo de petición",
"timeoutAfter": "Expirar después de {0} segundos",
"chromeExecutableDescription": "Para usuarios de Docker, si Chromium no está instalado, puede que tarde unos minutos en ser instalado y mostrar el resultado de la prueba. Usa 1GB de espacio.",
Expand Down
1 change: 0 additions & 1 deletion src/lang/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,6 @@
"filterActive": "فعال",
"webhookCustomBodyDesc": "یک بدنه HTTP سفارشی برای ریکوئست تعریف کنید. متغیر های قابل استفاده: {msg}, {heartbeat}, {monitor}.",
"tailscalePingWarning": "برای استفاده از Tailscale Ping monitor، شما باید آپتایم کوما را بدون استفاده از داکر و همچنین Tailscale client را نیز بر روی سرور خود نصب داشته باشید.",
"jsonQueryDescription": "یک کوئری json در برابر پاسخ انجام دهید و مقدار مورد انتظار را (مقدار برگشتی برای مقایسه به رشته تبدیل می شود). برای مستندات درباره زبان کوئری، {0} مشاهده کنید. همچنین محیط تست را میتوانید در {1} پیدا کنید.",
"Enter the list of brokers": "لیست بروکر هارا وارد کنید",
"Enable Kafka Producer Auto Topic Creation": "فعال سازی ایجاپ موضوع اتوماتیک تهیه کننده",
"Secret AccessKey": "کلید محرمانه AccessKey",
Expand Down
1 change: 0 additions & 1 deletion src/lang/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,6 @@
"emailTemplateLimitedToUpDownNotification": "saatavilla vain YLÖS/ALAS sydämensykkeille, muulloin null",
"Your User ID": "Käyttäjätunnuksesi",
"invertKeywordDescription": "Etsi puuttuvaa avainsanaa.",
"jsonQueryDescription": "Suorita JSON-kysely vastaukselle ja tarkista odotettu arvo (Paluuarvo muutetaan merkkijonoksi vertailua varten). Katso kyselykielen ohjeita osoitteesta {0}. Leikkikenttä löytyy osoitteesta {1}.",
"Bark API Version": "Bark API-versio",
"Notify Channel": "Ilmoitus kanavalle",
"aboutNotifyChannel": "Ilmoitus kanavalle antaa työpöytä- tai mobiili-ilmoituksen kaikille kanavan jäsenille; riippumatta ovatko he paikalla vai poissa.",
Expand Down
1 change: 0 additions & 1 deletion src/lang/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,6 @@
"twilioApiKey": "Clé API (facultatif)",
"Expected Value": "Valeur attendue",
"Json Query": "Requête Json",
"jsonQueryDescription": "Faites une requête json contre la réponse et vérifiez la valeur attendue (la valeur de retour sera convertie en chaîne pour comparaison). Consultez {0} pour la documentation sur le langage de requête. Une aire de jeux peut être trouvée {1}.",
"Badge Duration (in hours)": "Durée du badge (en heures)",
"Badge Preview": "Aperçu du badge",
"aboutNotifyChannel": "Notifier le canal déclenchera une notification de bureau ou mobile pour tous les membres du canal, que leur disponibilité soit active ou absente.",
Expand Down
1 change: 0 additions & 1 deletion src/lang/ga.json
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,6 @@
"confirmDisableTwoFAMsg": "An bhfuil tú cinnte gur mhaith leat 2FA a dhíchumasú?",
"affectedStatusPages": "Taispeáin an teachtaireacht cothabhála seo ar leathanaigh stádais roghnaithe",
"keywordDescription": "Cuardaigh eochairfhocal i ngnáthfhreagra HTML nó JSON. Tá an cuardach cás-íogair.",
"jsonQueryDescription": "Déan Iarratas json in aghaidh an fhreagra agus seiceáil an luach a bhfuiltear ag súil leis (Déanfar an luach fillte a thiontú ina theaghrán le haghaidh comparáide). Seiceáil {0} le haghaidh na gcáipéisí faoin teanga iarratais. Is féidir clós súgartha a aimsiú {1}.",
"backupDescription": "Is féidir leat gach monatóir agus fógra a chúltaca isteach i gcomhad JSON.",
"backupDescription2": "Nóta: níl sonraí staire agus imeachta san áireamh.",
"octopushAPIKey": "\"Eochair API\" ó dhintiúir API HTTP sa phainéal rialaithe",
Expand Down
Loading
Loading