ultravisor-beacon-capability 1.0.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/README.md +106 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +103 -0
- package/docs/_brand.json +18 -0
- package/docs/_cover.md +13 -0
- package/docs/_sidebar.md +31 -0
- package/docs/_topbar.md +5 -0
- package/docs/_version.json +7 -0
- package/docs/api/README.md +44 -0
- package/docs/api/action-convention.md +148 -0
- package/docs/api/add-action.md +68 -0
- package/docs/api/beacon-capability.md +89 -0
- package/docs/api/build-action-map.md +88 -0
- package/docs/api/connect.md +81 -0
- package/docs/api/disconnect.md +50 -0
- package/docs/api/is-connected.md +33 -0
- package/docs/api/lifecycle-hooks.md +115 -0
- package/docs/architecture.md +237 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/examples/README.md +58 -0
- package/docs/examples/certificate-expiry-monitor.md +212 -0
- package/docs/examples/docker-container-management.md +265 -0
- package/docs/examples/log-archive-and-upload.md +214 -0
- package/docs/examples/log-file-cleanup.md +199 -0
- package/docs/examples/mysql-maintenance.md +253 -0
- package/docs/examples/postgresql-aggregation.md +247 -0
- package/docs/examples/rest-api-health-check.md +213 -0
- package/docs/examples/rest-endpoint-sync.md +240 -0
- package/docs/examples/server-metrics-collection.md +199 -0
- package/docs/examples/shell-commands.md +176 -0
- package/docs/index.html +39 -0
- package/docs/quickstart.md +199 -0
- package/docs/retold-catalog.json +85 -0
- package/docs/retold-keyword-index.json +10642 -0
- package/package.json +33 -0
- package/source/Ultravisor-Beacon-Capability-ActionMap.cjs +132 -0
- package/source/Ultravisor-Beacon-Capability.cjs +276 -0
- package/test/Ultravisor-Beacon-Capability_tests.js +744 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Example: REST Endpoint Sync
|
|
2
|
+
|
|
3
|
+
A capability that fetches data from one REST API and pushes it to another. Common use case: syncing records between services, pulling data from a vendor API into an internal system, or replicating configuration between environments.
|
|
4
|
+
|
|
5
|
+
## Full Source
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const libFable = require('fable');
|
|
9
|
+
const libBeaconCapability = require('ultravisor-beacon-capability');
|
|
10
|
+
const libHTTPS = require('https');
|
|
11
|
+
const libHTTP = require('http');
|
|
12
|
+
const libURL = require('url');
|
|
13
|
+
|
|
14
|
+
class RESTSync extends libBeaconCapability
|
|
15
|
+
{
|
|
16
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
17
|
+
{
|
|
18
|
+
super(pFable, pOptions, pServiceHash);
|
|
19
|
+
this.serviceType = 'RESTSync';
|
|
20
|
+
this.capabilityName = 'RESTSync';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Internal helper: make an HTTP request and return parsed JSON.
|
|
25
|
+
*/
|
|
26
|
+
_request(pURL, pMethod, pHeaders, pBody, fCallback)
|
|
27
|
+
{
|
|
28
|
+
let tmpParsed = libURL.parse(pURL);
|
|
29
|
+
let tmpLib = (tmpParsed.protocol === 'https:') ? libHTTPS : libHTTP;
|
|
30
|
+
|
|
31
|
+
let tmpHeaders = Object.assign({ 'Content-Type': 'application/json' }, pHeaders || {});
|
|
32
|
+
|
|
33
|
+
let tmpOptions = {
|
|
34
|
+
hostname: tmpParsed.hostname,
|
|
35
|
+
port: tmpParsed.port,
|
|
36
|
+
path: tmpParsed.path,
|
|
37
|
+
method: pMethod,
|
|
38
|
+
headers: tmpHeaders,
|
|
39
|
+
timeout: 30000
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let tmpReq = tmpLib.request(tmpOptions, (pRes) =>
|
|
43
|
+
{
|
|
44
|
+
let tmpData = '';
|
|
45
|
+
pRes.on('data', (pChunk) => { tmpData += pChunk; });
|
|
46
|
+
pRes.on('end', () =>
|
|
47
|
+
{
|
|
48
|
+
let tmpParsedBody = null;
|
|
49
|
+
try { tmpParsedBody = JSON.parse(tmpData); }
|
|
50
|
+
catch (pParseError) { tmpParsedBody = tmpData; }
|
|
51
|
+
|
|
52
|
+
if (pRes.statusCode >= 400)
|
|
53
|
+
{
|
|
54
|
+
return fCallback(new Error(`HTTP ${pRes.statusCode}: ${tmpData.substring(0, 500)}`));
|
|
55
|
+
}
|
|
56
|
+
return fCallback(null, { StatusCode: pRes.statusCode, Body: tmpParsedBody });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
tmpReq.on('error', (pError) => fCallback(pError));
|
|
61
|
+
tmpReq.on('timeout', () => { tmpReq.destroy(); fCallback(new Error('Request timed out')); });
|
|
62
|
+
|
|
63
|
+
if (pBody)
|
|
64
|
+
{
|
|
65
|
+
tmpReq.write(typeof pBody === 'string' ? pBody : JSON.stringify(pBody));
|
|
66
|
+
}
|
|
67
|
+
tmpReq.end();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Action: FetchAndPost ---
|
|
71
|
+
|
|
72
|
+
get actionFetchAndPost_Description()
|
|
73
|
+
{
|
|
74
|
+
return 'Fetch records from a source API and POST each to a destination API';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get actionFetchAndPost_Schema()
|
|
78
|
+
{
|
|
79
|
+
return [
|
|
80
|
+
{ Name: 'SourceURL', DataType: 'String', Required: true, Description: 'GET endpoint that returns an array of records' },
|
|
81
|
+
{ Name: 'SourceHeaders', DataType: 'Object', Required: false },
|
|
82
|
+
{ Name: 'DestinationURL', DataType: 'String', Required: true, Description: 'POST endpoint for each record' },
|
|
83
|
+
{ Name: 'DestinationHeaders', DataType: 'Object', Required: false },
|
|
84
|
+
{ Name: 'RecordsPath', DataType: 'String', Required: false, Description: 'Dot-path to the array in the source response (e.g. "data.items")' },
|
|
85
|
+
{ Name: 'DryRun', DataType: 'Boolean', Required: false, Default: false }
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
actionFetchAndPost(pSettings, pWorkItem, fCallback, fReportProgress)
|
|
90
|
+
{
|
|
91
|
+
fReportProgress({ Percent: 5, Message: 'Fetching from source...' });
|
|
92
|
+
|
|
93
|
+
this._request(pSettings.SourceURL, 'GET', pSettings.SourceHeaders, null, (pFetchError, pFetchResult) =>
|
|
94
|
+
{
|
|
95
|
+
if (pFetchError)
|
|
96
|
+
{
|
|
97
|
+
return fCallback(pFetchError);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract records array
|
|
101
|
+
let tmpRecords = pFetchResult.Body;
|
|
102
|
+
if (pSettings.RecordsPath)
|
|
103
|
+
{
|
|
104
|
+
let tmpParts = pSettings.RecordsPath.split('.');
|
|
105
|
+
for (let i = 0; i < tmpParts.length; i++)
|
|
106
|
+
{
|
|
107
|
+
tmpRecords = tmpRecords ? tmpRecords[tmpParts[i]] : undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!Array.isArray(tmpRecords))
|
|
112
|
+
{
|
|
113
|
+
return fCallback(new Error(`Source did not return an array at path "${pSettings.RecordsPath || '(root)'}"`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fReportProgress({ Percent: 20, Message: `Fetched ${tmpRecords.length} records` });
|
|
117
|
+
|
|
118
|
+
if (pSettings.DryRun)
|
|
119
|
+
{
|
|
120
|
+
return fCallback(null, {
|
|
121
|
+
Outputs: { DryRun: true, RecordCount: tmpRecords.length, SampleRecord: tmpRecords[0] || null },
|
|
122
|
+
Log: [`DRY RUN: Would POST ${tmpRecords.length} records to ${pSettings.DestinationURL}`]
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// POST each record sequentially
|
|
127
|
+
let tmpIndex = 0;
|
|
128
|
+
let tmpSuccessCount = 0;
|
|
129
|
+
let tmpErrors = [];
|
|
130
|
+
|
|
131
|
+
let fnPostNext = () =>
|
|
132
|
+
{
|
|
133
|
+
if (tmpIndex >= tmpRecords.length)
|
|
134
|
+
{
|
|
135
|
+
return fCallback(null, {
|
|
136
|
+
Outputs: {
|
|
137
|
+
TotalRecords: tmpRecords.length,
|
|
138
|
+
SuccessCount: tmpSuccessCount,
|
|
139
|
+
ErrorCount: tmpErrors.length,
|
|
140
|
+
Errors: tmpErrors.slice(0, 20)
|
|
141
|
+
},
|
|
142
|
+
Log: [`Synced ${tmpSuccessCount}/${tmpRecords.length} records to ${pSettings.DestinationURL}`]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let tmpRecord = tmpRecords[tmpIndex];
|
|
147
|
+
tmpIndex++;
|
|
148
|
+
|
|
149
|
+
this._request(pSettings.DestinationURL, 'POST', pSettings.DestinationHeaders, tmpRecord, (pPostError) =>
|
|
150
|
+
{
|
|
151
|
+
if (pPostError)
|
|
152
|
+
{
|
|
153
|
+
tmpErrors.push({ Index: tmpIndex - 1, Error: pPostError.message });
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{
|
|
157
|
+
tmpSuccessCount++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let tmpPercent = 20 + Math.round((tmpIndex / tmpRecords.length) * 75);
|
|
161
|
+
fReportProgress({ Percent: tmpPercent, Message: `Posted ${tmpIndex} / ${tmpRecords.length}` });
|
|
162
|
+
|
|
163
|
+
setImmediate(fnPostNext);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
fnPostNext();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Action: MirrorEndpoint ---
|
|
172
|
+
|
|
173
|
+
get actionMirrorEndpoint_Description()
|
|
174
|
+
{
|
|
175
|
+
return 'GET from source and PUT the entire response body to a destination';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get actionMirrorEndpoint_Schema()
|
|
179
|
+
{
|
|
180
|
+
return [
|
|
181
|
+
{ Name: 'SourceURL', DataType: 'String', Required: true },
|
|
182
|
+
{ Name: 'SourceHeaders', DataType: 'Object', Required: false },
|
|
183
|
+
{ Name: 'DestinationURL', DataType: 'String', Required: true },
|
|
184
|
+
{ Name: 'DestinationHeaders', DataType: 'Object', Required: false }
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
actionMirrorEndpoint(pSettings, pWorkItem, fCallback)
|
|
189
|
+
{
|
|
190
|
+
this._request(pSettings.SourceURL, 'GET', pSettings.SourceHeaders, null, (pFetchError, pFetchResult) =>
|
|
191
|
+
{
|
|
192
|
+
if (pFetchError) return fCallback(pFetchError);
|
|
193
|
+
|
|
194
|
+
this._request(pSettings.DestinationURL, 'PUT', pSettings.DestinationHeaders, pFetchResult.Body, (pPutError, pPutResult) =>
|
|
195
|
+
{
|
|
196
|
+
if (pPutError) return fCallback(pPutError);
|
|
197
|
+
return fCallback(null, {
|
|
198
|
+
Outputs: {
|
|
199
|
+
SourceStatus: pFetchResult.StatusCode,
|
|
200
|
+
DestinationStatus: pPutResult.StatusCode
|
|
201
|
+
},
|
|
202
|
+
Log: [`Mirrored ${pSettings.SourceURL} -> ${pSettings.DestinationURL}`]
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Startup ---
|
|
210
|
+
|
|
211
|
+
let tmpFable = new libFable({ Product: 'RESTSync', ProductVersion: '1.0.0' });
|
|
212
|
+
tmpFable.addServiceType('RESTSync', RESTSync);
|
|
213
|
+
let tmpCap = tmpFable.instantiateServiceProvider('RESTSync');
|
|
214
|
+
|
|
215
|
+
tmpCap.connect(
|
|
216
|
+
{
|
|
217
|
+
ServerURL: process.env.ULTRAVISOR_URL || 'http://localhost:54321',
|
|
218
|
+
Name: 'rest-sync-worker'
|
|
219
|
+
},
|
|
220
|
+
(pError) =>
|
|
221
|
+
{
|
|
222
|
+
if (pError) throw pError;
|
|
223
|
+
console.log('REST sync beacon online');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
process.on('SIGTERM', () => { tmpCap.disconnect(() => process.exit(0)); });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Registered Task Types
|
|
230
|
+
|
|
231
|
+
- `beacon-restsync-fetchandpost`
|
|
232
|
+
- `beacon-restsync-mirrorendpoint`
|
|
233
|
+
|
|
234
|
+
## Key Points
|
|
235
|
+
|
|
236
|
+
- **No external HTTP library** -- uses Node.js built-in modules
|
|
237
|
+
- **RecordsPath** supports dot-notation for extracting arrays from nested JSON (e.g. `data.results`)
|
|
238
|
+
- **Sequential POSTs** avoid overwhelming the destination API; use `MaxConcurrent` on the beacon config for parallelism at the work item level
|
|
239
|
+
- **DryRun** fetches from source but skips destination writes
|
|
240
|
+
- **Error collection** is capped at 20 entries to avoid massive outputs
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Example: Server Metrics Collection
|
|
2
|
+
|
|
3
|
+
A capability that collects CPU, memory, disk, and process metrics from the host machine. Schedule it to run every few minutes to build a lightweight time series of infrastructure health in Ultravisor.
|
|
4
|
+
|
|
5
|
+
## Full Source
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const libFable = require('fable');
|
|
9
|
+
const libBeaconCapability = require('ultravisor-beacon-capability');
|
|
10
|
+
const libChildProcess = require('child_process');
|
|
11
|
+
const libOS = require('os');
|
|
12
|
+
|
|
13
|
+
class ServerMetrics extends libBeaconCapability
|
|
14
|
+
{
|
|
15
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
16
|
+
{
|
|
17
|
+
super(pFable, pOptions, pServiceHash);
|
|
18
|
+
this.serviceType = 'ServerMetrics';
|
|
19
|
+
this.capabilityName = 'ServerMetrics';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- Action: CollectAll ---
|
|
23
|
+
|
|
24
|
+
get actionCollectAll_Description()
|
|
25
|
+
{
|
|
26
|
+
return 'Collect CPU, memory, disk, and load average metrics from the host';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
actionCollectAll(pSettings, pWorkItem, fCallback)
|
|
30
|
+
{
|
|
31
|
+
let tmpMetrics = {
|
|
32
|
+
Timestamp: new Date().toISOString(),
|
|
33
|
+
Hostname: libOS.hostname(),
|
|
34
|
+
Platform: libOS.platform(),
|
|
35
|
+
Arch: libOS.arch(),
|
|
36
|
+
Uptime: libOS.uptime(),
|
|
37
|
+
LoadAverage: libOS.loadavg(),
|
|
38
|
+
CPUs: libOS.cpus().length,
|
|
39
|
+
TotalMemoryMB: Math.round(libOS.totalmem() / (1024 * 1024)),
|
|
40
|
+
FreeMemoryMB: Math.round(libOS.freemem() / (1024 * 1024)),
|
|
41
|
+
MemoryUsagePercent: Math.round((1 - libOS.freemem() / libOS.totalmem()) * 100)
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Get disk usage
|
|
45
|
+
libChildProcess.exec("df -h / | tail -1 | awk '{print $2, $3, $4, $5}'", (pError, pStdOut) =>
|
|
46
|
+
{
|
|
47
|
+
if (!pError && pStdOut.trim())
|
|
48
|
+
{
|
|
49
|
+
let tmpParts = pStdOut.trim().split(/\s+/);
|
|
50
|
+
tmpMetrics.DiskTotal = tmpParts[0] || 'unknown';
|
|
51
|
+
tmpMetrics.DiskUsed = tmpParts[1] || 'unknown';
|
|
52
|
+
tmpMetrics.DiskAvailable = tmpParts[2] || 'unknown';
|
|
53
|
+
tmpMetrics.DiskUsagePercent = tmpParts[3] || 'unknown';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return fCallback(null, {
|
|
57
|
+
Outputs: tmpMetrics,
|
|
58
|
+
Log: [
|
|
59
|
+
`${tmpMetrics.Hostname}: CPU load ${tmpMetrics.LoadAverage[0].toFixed(2)}, ` +
|
|
60
|
+
`Memory ${tmpMetrics.MemoryUsagePercent}%, ` +
|
|
61
|
+
`Disk ${tmpMetrics.DiskUsagePercent || 'N/A'}`
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Action: TopProcesses ---
|
|
68
|
+
|
|
69
|
+
get actionTopProcesses_Description()
|
|
70
|
+
{
|
|
71
|
+
return 'List the top processes by CPU or memory usage';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get actionTopProcesses_Schema()
|
|
75
|
+
{
|
|
76
|
+
return [
|
|
77
|
+
{ Name: 'SortBy', DataType: 'String', Required: false, Default: 'cpu', Description: '"cpu" or "memory"' },
|
|
78
|
+
{ Name: 'Count', DataType: 'Integer', Required: false, Default: 10 }
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
actionTopProcesses(pSettings, pWorkItem, fCallback)
|
|
83
|
+
{
|
|
84
|
+
let tmpSortFlag = (pSettings.SortBy === 'memory') ? '-m' : '-r';
|
|
85
|
+
let tmpCount = parseInt(pSettings.Count, 10) || 10;
|
|
86
|
+
|
|
87
|
+
// macOS and Linux have different ps flags; use a portable approach
|
|
88
|
+
let tmpCmd = `ps aux --sort=${(pSettings.SortBy === 'memory') ? '-%mem' : '-%cpu'} | head -${tmpCount + 1}`;
|
|
89
|
+
|
|
90
|
+
libChildProcess.exec(tmpCmd, (pError, pStdOut) =>
|
|
91
|
+
{
|
|
92
|
+
if (pError)
|
|
93
|
+
{
|
|
94
|
+
// Fallback for macOS
|
|
95
|
+
let tmpFallback = `ps aux | sort -nrk ${(pSettings.SortBy === 'memory') ? '4' : '3'} | head -${tmpCount}`;
|
|
96
|
+
libChildProcess.exec(tmpFallback, (pFallbackError, pFallbackOut) =>
|
|
97
|
+
{
|
|
98
|
+
if (pFallbackError) return fCallback(pFallbackError);
|
|
99
|
+
return fCallback(null, {
|
|
100
|
+
Outputs: { Processes: pFallbackOut, SortedBy: pSettings.SortBy || 'cpu' },
|
|
101
|
+
Log: [`Top ${tmpCount} processes by ${pSettings.SortBy || 'cpu'}`]
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return fCallback(null, {
|
|
108
|
+
Outputs: { Processes: pStdOut, SortedBy: pSettings.SortBy || 'cpu' },
|
|
109
|
+
Log: [`Top ${tmpCount} processes by ${pSettings.SortBy || 'cpu'}`]
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Action: NetworkConnections ---
|
|
115
|
+
|
|
116
|
+
get actionNetworkConnections_Description()
|
|
117
|
+
{
|
|
118
|
+
return 'Count active network connections by state';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
actionNetworkConnections(pSettings, pWorkItem, fCallback)
|
|
122
|
+
{
|
|
123
|
+
let tmpCmd = "netstat -an 2>/dev/null | awk '/tcp/ {print $6}' | sort | uniq -c | sort -rn";
|
|
124
|
+
|
|
125
|
+
libChildProcess.exec(tmpCmd, (pError, pStdOut) =>
|
|
126
|
+
{
|
|
127
|
+
if (pError)
|
|
128
|
+
{
|
|
129
|
+
// Fallback: try ss on Linux
|
|
130
|
+
let tmpFallback = "ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn";
|
|
131
|
+
libChildProcess.exec(tmpFallback, (pFallbackError, pFallbackOut) =>
|
|
132
|
+
{
|
|
133
|
+
if (pFallbackError) return fCallback(pFallbackError);
|
|
134
|
+
return this._parseConnectionCounts(pFallbackOut, fCallback);
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._parseConnectionCounts(pStdOut, fCallback);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_parseConnectionCounts(pOutput, fCallback)
|
|
144
|
+
{
|
|
145
|
+
let tmpStates = {};
|
|
146
|
+
let tmpTotal = 0;
|
|
147
|
+
let tmpLines = pOutput.trim().split('\n');
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
150
|
+
{
|
|
151
|
+
let tmpMatch = tmpLines[i].trim().match(/(\d+)\s+(.*)/);
|
|
152
|
+
if (tmpMatch)
|
|
153
|
+
{
|
|
154
|
+
let tmpCount = parseInt(tmpMatch[1], 10);
|
|
155
|
+
tmpStates[tmpMatch[2]] = tmpCount;
|
|
156
|
+
tmpTotal += tmpCount;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return fCallback(null, {
|
|
161
|
+
Outputs: { States: tmpStates, TotalConnections: tmpTotal },
|
|
162
|
+
Log: [`Network connections: ${tmpTotal} total`]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Startup ---
|
|
168
|
+
|
|
169
|
+
let tmpFable = new libFable({ Product: 'ServerMetrics', ProductVersion: '1.0.0' });
|
|
170
|
+
tmpFable.addServiceType('ServerMetrics', ServerMetrics);
|
|
171
|
+
let tmpCap = tmpFable.instantiateServiceProvider('ServerMetrics');
|
|
172
|
+
|
|
173
|
+
tmpCap.connect(
|
|
174
|
+
{
|
|
175
|
+
ServerURL: process.env.ULTRAVISOR_URL || 'http://localhost:54321',
|
|
176
|
+
Name: `metrics-${libOS.hostname()}`
|
|
177
|
+
},
|
|
178
|
+
(pError) =>
|
|
179
|
+
{
|
|
180
|
+
if (pError) throw pError;
|
|
181
|
+
console.log('Server metrics beacon online');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
process.on('SIGTERM', () => { tmpCap.disconnect(() => process.exit(0)); });
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Registered Task Types
|
|
188
|
+
|
|
189
|
+
- `beacon-servermetrics-collectall`
|
|
190
|
+
- `beacon-servermetrics-topprocesses`
|
|
191
|
+
- `beacon-servermetrics-networkconnections`
|
|
192
|
+
|
|
193
|
+
## Key Points
|
|
194
|
+
|
|
195
|
+
- **CollectAll** uses the Node.js `os` module for cross-platform metrics, with `df` for disk info
|
|
196
|
+
- **TopProcesses** includes a macOS fallback when Linux-style `ps --sort` is unavailable
|
|
197
|
+
- **NetworkConnections** tries `netstat` first, then falls back to `ss`
|
|
198
|
+
- Schedule `CollectAll` every 5 minutes in Ultravisor to build a lightweight metrics history
|
|
199
|
+
- The beacon name includes the hostname, so multiple servers can register the same capability
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Example: Shell Commands
|
|
2
|
+
|
|
3
|
+
Wrap basic shell commands as beacon actions. This is the simplest possible capability -- each action executes a shell command and returns the output.
|
|
4
|
+
|
|
5
|
+
## Full Source
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const libFable = require('fable');
|
|
9
|
+
const libBeaconCapability = require('ultravisor-beacon-capability');
|
|
10
|
+
const libChildProcess = require('child_process');
|
|
11
|
+
|
|
12
|
+
class ShellCommands extends libBeaconCapability
|
|
13
|
+
{
|
|
14
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
15
|
+
{
|
|
16
|
+
super(pFable, pOptions, pServiceHash);
|
|
17
|
+
this.serviceType = 'ShellCommands';
|
|
18
|
+
this.capabilityName = 'ShellCommands';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- Action: Ping ---
|
|
22
|
+
|
|
23
|
+
get actionPing_Description()
|
|
24
|
+
{
|
|
25
|
+
return 'Ping a host and return the result';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get actionPing_Schema()
|
|
29
|
+
{
|
|
30
|
+
return [
|
|
31
|
+
{ Name: 'Host', DataType: 'String', Required: true },
|
|
32
|
+
{ Name: 'Count', DataType: 'Integer', Required: false, Default: 4 }
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
actionPing(pSettings, pWorkItem, fCallback)
|
|
37
|
+
{
|
|
38
|
+
let tmpCount = pSettings.Count || 4;
|
|
39
|
+
let tmpCmd = `ping -c ${tmpCount} ${pSettings.Host}`;
|
|
40
|
+
|
|
41
|
+
this.log.info(`Pinging ${pSettings.Host} (${tmpCount} packets)...`);
|
|
42
|
+
|
|
43
|
+
libChildProcess.exec(tmpCmd, { timeout: 30000 }, (pError, pStdOut, pStdErr) =>
|
|
44
|
+
{
|
|
45
|
+
if (pError)
|
|
46
|
+
{
|
|
47
|
+
return fCallback(null, {
|
|
48
|
+
Outputs: { Success: false, StdOut: pStdOut || '', StdErr: pStdErr || '', ExitCode: pError.code },
|
|
49
|
+
Log: [`Ping failed: ${pError.message}`]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return fCallback(null, {
|
|
53
|
+
Outputs: { Success: true, StdOut: pStdOut, ExitCode: 0 },
|
|
54
|
+
Log: [`Pinged ${pSettings.Host} successfully`]
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Action: Uptime ---
|
|
60
|
+
|
|
61
|
+
get actionUptime_Description()
|
|
62
|
+
{
|
|
63
|
+
return 'Return the system uptime';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
actionUptime(pSettings, pWorkItem, fCallback)
|
|
67
|
+
{
|
|
68
|
+
libChildProcess.exec('uptime', (pError, pStdOut) =>
|
|
69
|
+
{
|
|
70
|
+
if (pError) return fCallback(pError);
|
|
71
|
+
return fCallback(null, {
|
|
72
|
+
Outputs: { Uptime: pStdOut.trim() },
|
|
73
|
+
Log: ['Retrieved system uptime']
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Action: Whoami ---
|
|
79
|
+
|
|
80
|
+
get actionWhoami_Description()
|
|
81
|
+
{
|
|
82
|
+
return 'Return the current user and hostname';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
actionWhoami(pSettings, pWorkItem, fCallback)
|
|
86
|
+
{
|
|
87
|
+
libChildProcess.exec('whoami && hostname', (pError, pStdOut) =>
|
|
88
|
+
{
|
|
89
|
+
if (pError) return fCallback(pError);
|
|
90
|
+
let tmpLines = pStdOut.trim().split('\n');
|
|
91
|
+
return fCallback(null, {
|
|
92
|
+
Outputs: { User: tmpLines[0], Hostname: tmpLines[1] || '' },
|
|
93
|
+
Log: ['Retrieved user and hostname']
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Action: RunCommand ---
|
|
99
|
+
|
|
100
|
+
get actionRunCommand_Description()
|
|
101
|
+
{
|
|
102
|
+
return 'Execute an arbitrary shell command with a timeout';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get actionRunCommand_Schema()
|
|
106
|
+
{
|
|
107
|
+
return [
|
|
108
|
+
{ Name: 'Command', DataType: 'String', Required: true },
|
|
109
|
+
{ Name: 'TimeoutSeconds', DataType: 'Integer', Required: false, Default: 60 },
|
|
110
|
+
{ Name: 'WorkingDirectory', DataType: 'String', Required: false }
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
actionRunCommand(pSettings, pWorkItem, fCallback)
|
|
115
|
+
{
|
|
116
|
+
let tmpOptions = {
|
|
117
|
+
timeout: (pSettings.TimeoutSeconds || 60) * 1000,
|
|
118
|
+
maxBuffer: 10 * 1024 * 1024
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (pSettings.WorkingDirectory)
|
|
122
|
+
{
|
|
123
|
+
tmpOptions.cwd = pSettings.WorkingDirectory;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.log.info(`Executing: ${pSettings.Command}`);
|
|
127
|
+
|
|
128
|
+
libChildProcess.exec(pSettings.Command, tmpOptions, (pError, pStdOut, pStdErr) =>
|
|
129
|
+
{
|
|
130
|
+
let tmpExitCode = pError ? (pError.code || 1) : 0;
|
|
131
|
+
return fCallback(null, {
|
|
132
|
+
Outputs: {
|
|
133
|
+
StdOut: pStdOut || '',
|
|
134
|
+
StdErr: pStdErr || '',
|
|
135
|
+
ExitCode: tmpExitCode,
|
|
136
|
+
Success: tmpExitCode === 0
|
|
137
|
+
},
|
|
138
|
+
Log: [`Command exited with code ${tmpExitCode}`]
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Startup ---
|
|
145
|
+
|
|
146
|
+
let tmpFable = new libFable({ Product: 'ShellCommands', ProductVersion: '1.0.0' });
|
|
147
|
+
tmpFable.addServiceType('ShellCommands', ShellCommands);
|
|
148
|
+
let tmpCap = tmpFable.instantiateServiceProvider('ShellCommands');
|
|
149
|
+
|
|
150
|
+
tmpCap.connect(
|
|
151
|
+
{
|
|
152
|
+
ServerURL: process.env.ULTRAVISOR_URL || 'http://localhost:54321',
|
|
153
|
+
Name: `shell-${require('os').hostname()}`,
|
|
154
|
+
MaxConcurrent: 4
|
|
155
|
+
},
|
|
156
|
+
(pError, pInfo) =>
|
|
157
|
+
{
|
|
158
|
+
if (pError) throw pError;
|
|
159
|
+
console.log(`Shell commands beacon online: ${pInfo.BeaconID}`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
process.on('SIGTERM', () => { tmpCap.disconnect(() => process.exit(0)); });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Registered Task Types
|
|
166
|
+
|
|
167
|
+
- `beacon-shellcommands-ping`
|
|
168
|
+
- `beacon-shellcommands-uptime`
|
|
169
|
+
- `beacon-shellcommands-whoami`
|
|
170
|
+
- `beacon-shellcommands-runcommand`
|
|
171
|
+
|
|
172
|
+
## Key Points
|
|
173
|
+
|
|
174
|
+
- `exec` timeout prevents long-running commands from hanging
|
|
175
|
+
- Failed commands return results with `Success: false` rather than calling `fCallback(pError)` -- this ensures the work item completes (with error data in Outputs) rather than being marked as a hard failure
|
|
176
|
+
- `maxBuffer` is raised to 10MB for commands that produce large output
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
7
|
+
<meta name="description" content="Ultravisor Beacon Capability v0.0.1 Documentation — Convention-based base class for building Ultravisor beacon capabilities with minimal boilerplate">
|
|
8
|
+
|
|
9
|
+
<title>Ultravisor Beacon Capability v0.0.1 Documentation</title>
|
|
10
|
+
|
|
11
|
+
<!-- Application Stylesheet -->
|
|
12
|
+
<link href="css/docuserve.css" rel="stylesheet">
|
|
13
|
+
<!-- KaTeX stylesheet for LaTeX equation rendering -->
|
|
14
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
|
|
15
|
+
<!-- PICT Dynamic View CSS Container -->
|
|
16
|
+
<style id="PICT-CSS"></style>
|
|
17
|
+
|
|
18
|
+
<!-- Load the PICT library from jsDelivr CDN -->
|
|
19
|
+
<script src="https://cdn.jsdelivr.net/npm/pict@1/dist/pict.min.js" type="text/javascript"></script>
|
|
20
|
+
<!-- Bootstrap the Application -->
|
|
21
|
+
<script type="text/javascript">
|
|
22
|
+
//<![CDATA[
|
|
23
|
+
Pict.safeOnDocumentReady(() => { Pict.safeLoadPictApplication(PictDocuserve, 2)});
|
|
24
|
+
//]]>
|
|
25
|
+
</script>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<!-- The root container for the Pict application -->
|
|
29
|
+
<div id="Docuserve-Application-Container"></div>
|
|
30
|
+
|
|
31
|
+
<!-- Mermaid diagram rendering -->
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
33
|
+
<script>mermaid.initialize({ startOnLoad: false, theme: 'default' });</script>
|
|
34
|
+
<!-- KaTeX for LaTeX equation rendering -->
|
|
35
|
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>
|
|
36
|
+
<!-- Load the Docuserve PICT Application Bundle from jsDelivr CDN -->
|
|
37
|
+
<script src="https://cdn.jsdelivr.net/npm/pict-docuserve@0/dist/pict-docuserve.min.js" type="text/javascript"></script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|