@silentjma/n8n-nodes-unraid 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/dist/credentials/UnraidApi.credentials.js +65 -0
- package/dist/lib/analysis.js +398 -0
- package/dist/lib/client.js +46 -0
- package/dist/lib/containers.js +54 -0
- package/dist/lib/logs.js +41 -0
- package/dist/lib/queries.js +86 -0
- package/dist/nodes/Unraid/Unraid.node.js +712 -0
- package/dist/nodes/Unraid/Unraid.node.json +9 -0
- package/dist/nodes/Unraid/unraid.svg +1 -0
- package/dist/nodes/UnraidTrigger/UnraidTrigger.node.js +283 -0
- package/dist/nodes/UnraidTrigger/UnraidTrigger.node.json +9 -0
- package/dist/nodes/UnraidTrigger/unraid.svg +1 -0
- package/package.json +75 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unraidGraphql = unraidGraphql;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
async function unraidGraphql(query, variables = {}) {
|
|
6
|
+
const credentials = await this.getCredentials('unraidApi');
|
|
7
|
+
const baseUrl = String(credentials.serverUrl ?? '').replace(/\/+$/, '');
|
|
8
|
+
if (!/^https?:\/\//i.test(baseUrl)) {
|
|
9
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid Unraid server URL "${baseUrl}". It must start with http:// or https://.`);
|
|
10
|
+
}
|
|
11
|
+
let response;
|
|
12
|
+
try {
|
|
13
|
+
response = (await this.helpers.httpRequestWithAuthentication.call(this, 'unraidApi', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
url: `${baseUrl}/graphql`,
|
|
16
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
17
|
+
body: { query, variables },
|
|
18
|
+
json: true,
|
|
19
|
+
skipSslCertificateValidation: credentials.allowUnauthorizedCerts === true,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
|
|
24
|
+
message: 'Unraid API request failed',
|
|
25
|
+
description: baseUrl.startsWith('http://')
|
|
26
|
+
? 'If SSL is enabled on Unraid, http:// is redirected to https:// and the request body is lost. Use the https:// URL (with Ignore SSL Issues for the self-signed certificate).'
|
|
27
|
+
: 'Check the server URL, that the API is enabled (Unraid 7.2+ or the Unraid Connect plugin) and that the API key is valid.',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (response?.errors?.length) {
|
|
31
|
+
const message = response.errors.map((e) => (e.path?.length ? `${e.path.join('.')}: ${e.message}` : e.message)).join('; ');
|
|
32
|
+
const forbidden = response.errors.some((e) => e.extensions?.code === 'FORBIDDEN' || /forbidden|permission/i.test(e.message));
|
|
33
|
+
const unauthenticated = response.errors.some((e) => e.extensions?.code === 'UNAUTHENTICATED');
|
|
34
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unraid API error: ${message}`, {
|
|
35
|
+
description: unauthenticated
|
|
36
|
+
? 'The API key is invalid or was revoked. Create a new one in Settings › Management Access › API Keys and update the Unraid API credential.'
|
|
37
|
+
: forbidden
|
|
38
|
+
? 'The API key lacks the permission for this operation. Give it the admin role, or the matching resource permission.'
|
|
39
|
+
: undefined,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!response?.data) {
|
|
43
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Unraid API returned no data');
|
|
44
|
+
}
|
|
45
|
+
return response.data;
|
|
46
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listContainerRefs = listContainerRefs;
|
|
4
|
+
exports.matchContainer = matchContainer;
|
|
5
|
+
exports.commandSafeName = commandSafeName;
|
|
6
|
+
exports.resolveContainerIds = resolveContainerIds;
|
|
7
|
+
exports.containerOptions = containerOptions;
|
|
8
|
+
exports.withName = withName;
|
|
9
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
10
|
+
const analysis_1 = require("./analysis");
|
|
11
|
+
const client_1 = require("./client");
|
|
12
|
+
const queries_1 = require("./queries");
|
|
13
|
+
async function listContainerRefs() {
|
|
14
|
+
const data = await client_1.unraidGraphql.call(this, queries_1.Q.containerNames);
|
|
15
|
+
return data.docker.containers ?? [];
|
|
16
|
+
}
|
|
17
|
+
function matchContainer(refs, value) {
|
|
18
|
+
const v = value.trim();
|
|
19
|
+
const lower = v.replace(/^\//, '').toLowerCase();
|
|
20
|
+
return (refs.find((c) => c.id === v || c.id.endsWith(`:${v}`)) ??
|
|
21
|
+
refs.find((c) => (0, analysis_1.containerName)(c).toLowerCase() === lower) ??
|
|
22
|
+
refs.find((c) => c.names.some((n) => n.replace(/^\//, '').toLowerCase() === lower)) ??
|
|
23
|
+
refs.find((c) => commandSafeName((0, analysis_1.containerName)(c)) === commandSafeName(v)));
|
|
24
|
+
}
|
|
25
|
+
/** Telegram commands allow only [a-z0-9_], so "Plex-Media-Server" is sent as plex_media_server. */
|
|
26
|
+
function commandSafeName(name) {
|
|
27
|
+
return name
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
30
|
+
.replace(/^_+|_+$/g, '');
|
|
31
|
+
}
|
|
32
|
+
/** Resolves names/IDs, reusing one container listing per execution via the cache object. */
|
|
33
|
+
async function resolveContainerIds(values, cache) {
|
|
34
|
+
cache.refs ??= await listContainerRefs.call(this);
|
|
35
|
+
return values.map((value) => {
|
|
36
|
+
const match = matchContainer(cache.refs, value);
|
|
37
|
+
if (!match) {
|
|
38
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No Docker container named or with ID "${value}"`, {
|
|
39
|
+
description: `Known containers: ${cache.refs.map((c) => (0, analysis_1.containerName)(c)).join(', ') || 'none'}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return match.id;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function containerOptions() {
|
|
46
|
+
const refs = await listContainerRefs.call(this);
|
|
47
|
+
return refs
|
|
48
|
+
.map((c) => ({ name: `${(0, analysis_1.containerName)(c)} (${c.state.toLowerCase()})`, value: (0, analysis_1.containerName)(c) }))
|
|
49
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
50
|
+
}
|
|
51
|
+
/** Adds a plain `name` field next to Docker's `names` array. */
|
|
52
|
+
function withName(container) {
|
|
53
|
+
return { name: (0, analysis_1.containerName)(container), ...container };
|
|
54
|
+
}
|
package/dist/lib/logs.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitLogContent = splitLogContent;
|
|
4
|
+
exports.compilePattern = compilePattern;
|
|
5
|
+
exports.filterLogLines = filterLogLines;
|
|
6
|
+
// Log text -> structured lines, optionally filtered by regex and classified with suggested next steps.
|
|
7
|
+
const analysis_1 = require("./analysis");
|
|
8
|
+
function splitLogContent(content, startLine) {
|
|
9
|
+
const raw = content.split(/\r?\n/);
|
|
10
|
+
if (raw.length && raw[raw.length - 1] === '')
|
|
11
|
+
raw.pop();
|
|
12
|
+
return raw.map((line, i) => ({ lineNumber: startLine ? startLine + i : undefined, line }));
|
|
13
|
+
}
|
|
14
|
+
function compilePattern(pattern) {
|
|
15
|
+
if (!pattern?.trim())
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
return new RegExp(pattern, 'i');
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new Error(`Invalid filter pattern "${pattern}": ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function filterLogLines(lines, filter) {
|
|
25
|
+
const regex = compilePattern(filter.pattern);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const l of lines) {
|
|
28
|
+
if (regex && !regex.test(l.line))
|
|
29
|
+
continue;
|
|
30
|
+
if (filter.analyze) {
|
|
31
|
+
const match = (0, analysis_1.classifyLogLine)(l.line);
|
|
32
|
+
if (filter.minSeverity && filter.minSeverity !== 'all' && (!match || !(0, analysis_1.severityAtLeast)(match.severity, filter.minSeverity)))
|
|
33
|
+
continue;
|
|
34
|
+
out.push(match ? { ...l, ...match } : l);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
out.push(l);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// GraphQL documents used by the Unraid nodes, written against api/generated-schema.graphql from github.com/unraid/api.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.M = exports.Q = exports.METRICS_FIELDS = exports.NOTIFICATION_FIELDS = exports.PHYSICAL_DISK_FIELDS = exports.ARRAY_FIELDS = exports.PARITY_FIELDS = exports.ARRAY_DISK_FIELDS = exports.CONTAINER_FIELDS = void 0;
|
|
5
|
+
exports.CONTAINER_FIELDS = `
|
|
6
|
+
id names image imageId state status created autoStart autoStartOrder
|
|
7
|
+
isUpdateAvailable isRebuildReady isOrphaned webUiUrl iconUrl projectUrl
|
|
8
|
+
ports { ip privatePort publicPort type }`;
|
|
9
|
+
exports.ARRAY_DISK_FIELDS = `
|
|
10
|
+
id idx name device size status type temp warning critical rotational isSpinning
|
|
11
|
+
numReads numWrites numErrors fsSize fsFree fsUsed fsType comment`;
|
|
12
|
+
exports.PARITY_FIELDS = 'date duration speed status errors progress correcting paused running';
|
|
13
|
+
exports.ARRAY_FIELDS = `
|
|
14
|
+
id state
|
|
15
|
+
capacity { kilobytes { free used total } disks { free used total } }
|
|
16
|
+
parityCheckStatus { ${exports.PARITY_FIELDS} }
|
|
17
|
+
parities { ${exports.ARRAY_DISK_FIELDS} }
|
|
18
|
+
disks { ${exports.ARRAY_DISK_FIELDS} }
|
|
19
|
+
caches { ${exports.ARRAY_DISK_FIELDS} }
|
|
20
|
+
boot { ${exports.ARRAY_DISK_FIELDS} }`;
|
|
21
|
+
exports.PHYSICAL_DISK_FIELDS = 'id device type name vendor size firmwareRevision serialNum interfaceType smartStatus temperature isSpinning';
|
|
22
|
+
exports.NOTIFICATION_FIELDS = 'id title subject description importance link type timestamp formattedTimestamp';
|
|
23
|
+
exports.METRICS_FIELDS = `
|
|
24
|
+
cpu { percentTotal }
|
|
25
|
+
memory { total used free available percentTotal swapTotal swapUsed percentSwapTotal }`;
|
|
26
|
+
exports.Q = {
|
|
27
|
+
online: '{ online }',
|
|
28
|
+
info: `{
|
|
29
|
+
online
|
|
30
|
+
info {
|
|
31
|
+
time
|
|
32
|
+
os { platform distro release kernel arch hostname uptime }
|
|
33
|
+
cpu { manufacturer brand cores threads speed speedmax }
|
|
34
|
+
system { manufacturer model virtual }
|
|
35
|
+
}
|
|
36
|
+
}`,
|
|
37
|
+
metrics: `{ metrics { ${exports.METRICS_FIELDS} } }`,
|
|
38
|
+
array: `{ array { ${exports.ARRAY_FIELDS} } }`,
|
|
39
|
+
parityHistory: `{ parityHistory { ${exports.PARITY_FIELDS} } }`,
|
|
40
|
+
disks: `{ disks { ${exports.PHYSICAL_DISK_FIELDS} } }`,
|
|
41
|
+
shares: '{ shares { id name free used size comment cache include exclude splitLevel } }',
|
|
42
|
+
containers: `{ docker { containers { ${exports.CONTAINER_FIELDS} } } }`,
|
|
43
|
+
containerNames: '{ docker { containers { id names state } } }',
|
|
44
|
+
container: `query ($id: PrefixedID!) { docker { container(id: $id) { ${exports.CONTAINER_FIELDS} } } }`,
|
|
45
|
+
containerLogs: `query ($id: PrefixedID!, $since: DateTime, $tail: Int) {
|
|
46
|
+
docker { logs(id: $id, since: $since, tail: $tail) { containerId cursor lines { timestamp message } } }
|
|
47
|
+
}`,
|
|
48
|
+
containerUpdateStatuses: '{ docker { containerUpdateStatuses { name updateStatus } } }',
|
|
49
|
+
vms: '{ vms { domains { id name state } } }',
|
|
50
|
+
logFiles: '{ logFiles { name path size modifiedAt } }',
|
|
51
|
+
logFile: `query ($path: String!, $lines: Int, $startLine: Int) {
|
|
52
|
+
logFile(path: $path, lines: $lines, startLine: $startLine) { path content totalLines startLine }
|
|
53
|
+
}`,
|
|
54
|
+
notifications: `query ($filter: NotificationFilter!) { notifications { list(filter: $filter) { ${exports.NOTIFICATION_FIELDS} } } }`,
|
|
55
|
+
notificationOverview: `{ notifications { overview {
|
|
56
|
+
unread { info warning alert total }
|
|
57
|
+
archive { info warning alert total }
|
|
58
|
+
} } }`,
|
|
59
|
+
warningsAndAlerts: `{ notifications { warningsAndAlerts { ${exports.NOTIFICATION_FIELDS} } } }`,
|
|
60
|
+
health: `{
|
|
61
|
+
info { os { hostname uptime } }
|
|
62
|
+
array { ${exports.ARRAY_FIELDS} }
|
|
63
|
+
disks { ${exports.PHYSICAL_DISK_FIELDS} }
|
|
64
|
+
docker { containers { ${exports.CONTAINER_FIELDS} } }
|
|
65
|
+
notifications { warningsAndAlerts { ${exports.NOTIFICATION_FIELDS} } }
|
|
66
|
+
metrics { ${exports.METRICS_FIELDS} }
|
|
67
|
+
}`,
|
|
68
|
+
};
|
|
69
|
+
exports.M = {
|
|
70
|
+
arraySetState: `mutation ($input: ArrayStateInput!) { array { setState(input: $input) { id state } } }`,
|
|
71
|
+
parityStart: 'mutation ($correct: Boolean!) { parityCheck { start(correct: $correct) } }',
|
|
72
|
+
parityPause: 'mutation { parityCheck { pause } }',
|
|
73
|
+
parityResume: 'mutation { parityCheck { resume } }',
|
|
74
|
+
parityCancel: 'mutation { parityCheck { cancel } }',
|
|
75
|
+
dockerAction: (action) => `mutation ($id: PrefixedID!) { docker { ${action}(id: $id) { ${exports.CONTAINER_FIELDS} } } }`,
|
|
76
|
+
dockerUpdateMany: `mutation ($ids: [PrefixedID!]!) { docker { updateContainers(ids: $ids) { ${exports.CONTAINER_FIELDS} } } }`,
|
|
77
|
+
dockerUpdateAll: `mutation { docker { updateAllContainers { ${exports.CONTAINER_FIELDS} } } }`,
|
|
78
|
+
dockerRefreshDigests: 'mutation { refreshDockerDigests }',
|
|
79
|
+
vmAction: (action) => `mutation ($id: PrefixedID!) { vm { ${action}(id: $id) } }`,
|
|
80
|
+
createNotification: `mutation ($input: NotificationData!) { createNotification(input: $input) { ${exports.NOTIFICATION_FIELDS} } }`,
|
|
81
|
+
notifyIfUnique: `mutation ($input: NotificationData!) { notifyIfUnique(input: $input) { ${exports.NOTIFICATION_FIELDS} } }`,
|
|
82
|
+
archiveNotification: `mutation ($id: PrefixedID!) { archiveNotification(id: $id) { ${exports.NOTIFICATION_FIELDS} } }`,
|
|
83
|
+
archiveAll: `mutation ($importance: NotificationImportance) { archiveAll(importance: $importance) {
|
|
84
|
+
unread { info warning alert total } archive { info warning alert total }
|
|
85
|
+
} }`,
|
|
86
|
+
};
|