@lossless.org/objectstorage 11.0.0 → 11.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/.smartconfig.json +19 -1
- package/dist_rust/ruststorage_linux_amd64 +0 -0
- package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +4 -4
- package/dist_rust/ruststorage_linux_amd64_musl +0 -0
- package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/ruststorage_linux_arm64 +0 -0
- package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +4 -4
- package/dist_rust/ruststorage_linux_arm64_musl +0 -0
- package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_rust/ruststorage_macos_amd64 +0 -0
- package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +4 -4
- package/dist_rust/ruststorage_macos_arm64 +0 -0
- package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dashboard.d.ts +49 -0
- package/dist_ts/classes.dashboard.js +234 -0
- package/dist_ts/dashboard.bundled.d.ts +4 -0
- package/dist_ts/dashboard.bundled.js +20 -0
- package/dist_ts/dashboard.interfaces.d.ts +132 -0
- package/dist_ts/dashboard.interfaces.js +2 -0
- package/dist_ts/dashboard.plugins.d.ts +5 -0
- package/dist_ts/dashboard.plugins.js +10 -0
- package/dist_ts/index.d.ts +94 -1
- package/dist_ts/index.js +63 -1
- package/package.json +11 -7
- package/readme.md +192 -9
- package/third-party-notices.md +20 -4
- package/third_party_licenses/borsh-1.8.1-license.txt +23 -0
- package/third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt +201 -0
- package/third_party_licenses/cargo-dependencies.html +420 -0
- package/third_party_licenses/dyn-clone-1.0.20-license.txt +23 -0
- package/third_party_licenses/equivalent-1.0.3-license.txt +25 -0
- package/third_party_licenses/hashbrown-0.17.1-license.txt +25 -0
- package/third_party_licenses/indexmap-2.14.2-license.txt +25 -0
- package/third_party_licenses/inventory.json +310 -8
- package/third_party_licenses/proc-macro-crate-3.5.0-license.txt +23 -0
- package/third_party_licenses/ref-cast-1.0.27-license.txt +23 -0
- package/third_party_licenses/ref-cast-impl-1.0.27-license.txt +23 -0
- package/third_party_licenses/schemars-1.2.3-license.txt +21 -0
- package/third_party_licenses/syn-3.0.9-license.txt +23 -0
- package/third_party_licenses/toml_datetime-1.1.2-license.txt +19 -0
- package/third_party_licenses/toml_edit-0.25.17-license.txt +19 -0
- package/third_party_licenses/toml_parser-1.1.5-license.txt +19 -0
- package/third_party_licenses/winnow-1.0.4-license.txt +18 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dashboard.ts +280 -0
- package/ts/dashboard.bundled.ts.third-party-notices.json +1741 -0
- package/ts/dashboard.bundled.ts.third-party-notices.txt +5015 -0
- package/ts/dashboard.interfaces.ts +148 -0
- package/ts/dashboard.plugins.ts +13 -0
- package/ts/index.ts +162 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ITypedRequest, implementsTR } from '@api.global/typedrequest-interfaces';
|
|
2
|
+
import type { ITypedSocketEvent } from '@api.global/typedsocket';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Engine snapshot shapes, as `ObjectStorage.getClusterSnapshot()` returns
|
|
6
|
+
* them. The engine builds them from memory only: the drive states the last
|
|
7
|
+
* heartbeat probe left, runtime stats, healing state, S3 listener counters
|
|
8
|
+
* and a bounded ring of recent events. Times are milliseconds since the Unix
|
|
9
|
+
* epoch; absent values are `null`.
|
|
10
|
+
*/
|
|
11
|
+
export interface IObjectStorageTelemetryEvent {
|
|
12
|
+
/** Numbered per node from 1; a gap means older events left the ring. */
|
|
13
|
+
seq: number;
|
|
14
|
+
at: number;
|
|
15
|
+
kind: 'engine' | 'peer' | 'drive' | 'healing';
|
|
16
|
+
severity: 'info' | 'warning' | 'error';
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** S3 listener counters since the engine started; bytes include HTTP headers. */
|
|
21
|
+
export interface IObjectStorageTrafficCounters {
|
|
22
|
+
requestsTotal: number;
|
|
23
|
+
errorResponses: number;
|
|
24
|
+
bytesIn: number;
|
|
25
|
+
bytesOut: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IObjectStorageStorageTotals {
|
|
29
|
+
bucketCount: number;
|
|
30
|
+
objectCount: number;
|
|
31
|
+
totalBytes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IObjectStorageDriveSnapshot {
|
|
35
|
+
index: number;
|
|
36
|
+
path: string;
|
|
37
|
+
status: 'online' | 'degraded' | 'offline' | 'healing';
|
|
38
|
+
totalBytes: number;
|
|
39
|
+
usedBytes: number;
|
|
40
|
+
availableBytes: number;
|
|
41
|
+
errorCount: number;
|
|
42
|
+
lastError: string | null;
|
|
43
|
+
lastCheck: number | null;
|
|
44
|
+
erasureSetId: number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IObjectStorageHealingSnapshot {
|
|
48
|
+
active: boolean;
|
|
49
|
+
scanIntervalMs: number;
|
|
50
|
+
lastRunStartedAt: number | null;
|
|
51
|
+
lastRunCompletedAt: number | null;
|
|
52
|
+
lastDurationMs: number | null;
|
|
53
|
+
shardsChecked: number | null;
|
|
54
|
+
shardsHealed: number | null;
|
|
55
|
+
shardsPending: number | null;
|
|
56
|
+
failed: number | null;
|
|
57
|
+
lastError: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What one node reports about itself; local and remote nodes share this shape. */
|
|
61
|
+
export interface IObjectStorageNodeSnapshot {
|
|
62
|
+
/** Absent in standalone mode. */
|
|
63
|
+
nodeId: string | null;
|
|
64
|
+
mode: 'standalone' | 'cluster';
|
|
65
|
+
startedAt: number;
|
|
66
|
+
capturedAt: number;
|
|
67
|
+
traffic: IObjectStorageTrafficCounters;
|
|
68
|
+
storage: IObjectStorageStorageTotals;
|
|
69
|
+
/** Local drives (cluster mode); empty in standalone mode. */
|
|
70
|
+
drives: IObjectStorageDriveSnapshot[];
|
|
71
|
+
healing: IObjectStorageHealingSnapshot | null;
|
|
72
|
+
/** Recent events, oldest first. */
|
|
73
|
+
events: IObjectStorageTelemetryEvent[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface IObjectStoragePeerSnapshot {
|
|
77
|
+
nodeId: string;
|
|
78
|
+
status: 'online' | 'suspect' | 'offline';
|
|
79
|
+
quicAddress: string;
|
|
80
|
+
s3Address: string;
|
|
81
|
+
driveCount: number;
|
|
82
|
+
lastHeartbeat: number;
|
|
83
|
+
missedHeartbeats: number;
|
|
84
|
+
/** The peer's own snapshot, when it answered within a second. */
|
|
85
|
+
snapshot: IObjectStorageNodeSnapshot | null;
|
|
86
|
+
unavailableReason: string | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface IObjectStorageErasureSnapshot {
|
|
90
|
+
dataShards: number;
|
|
91
|
+
parityShards: number;
|
|
92
|
+
chunkSizeBytes: number;
|
|
93
|
+
totalShards: number;
|
|
94
|
+
readQuorum: number;
|
|
95
|
+
writeQuorum: number;
|
|
96
|
+
erasureSetCount: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface IObjectStorageClusterViewSnapshot {
|
|
100
|
+
nodeId: string;
|
|
101
|
+
quorumHealthy: boolean;
|
|
102
|
+
majorityHealthy: boolean;
|
|
103
|
+
erasure: IObjectStorageErasureSnapshot;
|
|
104
|
+
peers: IObjectStoragePeerSnapshot[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface IObjectStorageClusterSnapshot {
|
|
108
|
+
capturedAt: number;
|
|
109
|
+
clusterEnabled: boolean;
|
|
110
|
+
local: IObjectStorageNodeSnapshot;
|
|
111
|
+
/** Absent in standalone mode. */
|
|
112
|
+
cluster: IObjectStorageClusterViewSnapshot | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Dashboard configuration. The dashboard is off unless `enabled` is true.
|
|
117
|
+
*/
|
|
118
|
+
export interface IObjectStorageDashboardConfig {
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
/** Interface the dashboard listens on. Default `127.0.0.1`. */
|
|
121
|
+
address?: string;
|
|
122
|
+
/** TCP port; `0` asks the OS for one. Default 3001; must differ from the S3 port. */
|
|
123
|
+
port?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One dashboard sample, as pushed to every connected dashboard. */
|
|
127
|
+
export interface IObjectStorageDashboardSample {
|
|
128
|
+
sampledAt: number;
|
|
129
|
+
/** The snapshot, or `null` when the engine could not answer. */
|
|
130
|
+
snapshot: IObjectStorageClusterSnapshot | null;
|
|
131
|
+
error: string | null;
|
|
132
|
+
/** Events were left out of peer snapshots to keep the sample within the push limit. */
|
|
133
|
+
eventsTruncated: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Answers with the latest sample, taking one when none exists yet. */
|
|
137
|
+
export interface IReq_ObjectStorageDashboardGetSample
|
|
138
|
+
extends implementsTR<ITypedRequest, IReq_ObjectStorageDashboardGetSample> {
|
|
139
|
+
method: 'objectStorageDashboardGetSample';
|
|
140
|
+
request: Record<string, never>;
|
|
141
|
+
response: IObjectStorageDashboardSample;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Pushed every two seconds while at least one dashboard is connected. */
|
|
145
|
+
export interface IEvent_ObjectStorageDashboardSample
|
|
146
|
+
extends ITypedSocketEvent<IObjectStorageDashboardSample> {
|
|
147
|
+
name: 'objectStorageDashboardSample';
|
|
148
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Loaded only by the dashboard module, which `ObjectStorage` imports
|
|
2
|
+
// dynamically when the dashboard is enabled.
|
|
3
|
+
|
|
4
|
+
// @api.global scope
|
|
5
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
6
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
7
|
+
|
|
8
|
+
export { typedrequest, typedsocket };
|
|
9
|
+
|
|
10
|
+
// @push.rocks scope
|
|
11
|
+
import * as smartserve from '@push.rocks/smartserve';
|
|
12
|
+
|
|
13
|
+
export { smartserve };
|
package/ts/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import * as paths from './paths.js';
|
|
3
|
+
import type { ObjectStorageDashboard } from './classes.dashboard.js';
|
|
4
|
+
import type {
|
|
5
|
+
IObjectStorageClusterSnapshot,
|
|
6
|
+
IObjectStorageDashboardConfig,
|
|
7
|
+
} from './dashboard.interfaces.js';
|
|
8
|
+
|
|
9
|
+
export * from './dashboard.interfaces.js';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Authentication configuration
|
|
@@ -145,11 +152,34 @@ export interface IDriveConfig {
|
|
|
145
152
|
allowSharedDevice?: boolean;
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Mutual TLS for the cluster transport. Every file is read once at startup and
|
|
157
|
+
* must lie outside the control and drive roots, at an absolute path without
|
|
158
|
+
* symbolic links; a restart reloads them.
|
|
159
|
+
*/
|
|
160
|
+
export interface IClusterTlsConfig {
|
|
161
|
+
/** PEM file with one or two CA certificates (two only during a CA rotation). */
|
|
162
|
+
caFile: string;
|
|
163
|
+
/**
|
|
164
|
+
* PEM file with this node's certificate, optionally followed by intermediates. It has one DNS SAN,
|
|
165
|
+
* equal to nodeId, and is valid for server and client authentication: an extended key usage
|
|
166
|
+
* extension listing serverAuth and clientAuth, or no extended key usage extension.
|
|
167
|
+
*/
|
|
168
|
+
certFile: string;
|
|
169
|
+
/** PEM private key of certFile; owned by the engine's user, mode 0600 or 0400. */
|
|
170
|
+
keyFile: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
/**
|
|
149
174
|
* Cluster configuration for distributed mode
|
|
150
175
|
*/
|
|
151
176
|
export interface IClusterConfig {
|
|
152
177
|
enabled: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* This node's id. Generated and persisted when omitted, except with `tls`:
|
|
180
|
+
* then it is required and must be a lowercase DNS name equal to the node
|
|
181
|
+
* certificate's DNS subject alternative name.
|
|
182
|
+
*/
|
|
153
183
|
nodeId?: string;
|
|
154
184
|
quicPort?: number;
|
|
155
185
|
seedNodes?: string[];
|
|
@@ -167,12 +197,20 @@ export interface IClusterConfig {
|
|
|
167
197
|
* 60,000 (1 minute) to 604,800,000 (7 days).
|
|
168
198
|
*/
|
|
169
199
|
getDeadlineMs?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Mutual TLS for the cluster transport. Without it the transport is
|
|
202
|
+
* encrypted but authenticates no peer. Every node of a cluster sets it, or
|
|
203
|
+
* none does: enabling it is a full stop of every node, not a rolling change.
|
|
204
|
+
*/
|
|
205
|
+
tls?: IClusterTlsConfig;
|
|
170
206
|
}
|
|
171
207
|
|
|
172
208
|
/**
|
|
173
209
|
* Configuration as the engine receives it: every section resolved.
|
|
174
210
|
*/
|
|
175
|
-
export type TObjectStorageResolvedConfig = Required<
|
|
211
|
+
export type TObjectStorageResolvedConfig = Required<
|
|
212
|
+
Omit<IObjectStorageConfig, 'binaryPath' | 'dashboard'>
|
|
213
|
+
>;
|
|
176
214
|
|
|
177
215
|
/**
|
|
178
216
|
* Complete objectstorage configuration
|
|
@@ -193,6 +231,11 @@ export interface IObjectStorageConfig {
|
|
|
193
231
|
limits?: ILimitsConfig;
|
|
194
232
|
multipart?: IMultipartConfig;
|
|
195
233
|
cluster?: IClusterConfig;
|
|
234
|
+
/**
|
|
235
|
+
* The read-only visibility dashboard, served by this process on its own
|
|
236
|
+
* port. Off unless `enabled` is true; never sent to the engine.
|
|
237
|
+
*/
|
|
238
|
+
dashboard?: IObjectStorageDashboardConfig;
|
|
196
239
|
}
|
|
197
240
|
|
|
198
241
|
/**
|
|
@@ -1194,6 +1237,35 @@ export interface IClusterPeerHealth {
|
|
|
1194
1237
|
missedHeartbeats?: number;
|
|
1195
1238
|
}
|
|
1196
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* A drive's medium: `'rotational'` and `'flash'` (network volumes included),
|
|
1242
|
+
* `'smr'` for drive-managed SMR, which reports itself as rotational and is
|
|
1243
|
+
* never detected, and `'zoned'` for host-managed or host-aware SMR and ZNS.
|
|
1244
|
+
*/
|
|
1245
|
+
export type TDriveMedia = 'rotational' | 'smr' | 'flash' | 'zoned';
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* A drive's media.
|
|
1249
|
+
*/
|
|
1250
|
+
export interface IClusterDriveMedia {
|
|
1251
|
+
/**
|
|
1252
|
+
* The media the configuration declares for the drive. Not reported yet:
|
|
1253
|
+
* drives cannot declare their media.
|
|
1254
|
+
*/
|
|
1255
|
+
declared?: TDriveMedia;
|
|
1256
|
+
/**
|
|
1257
|
+
* What the engine detected once at startup, from the whole disk behind the
|
|
1258
|
+
* drive root's filesystem (Linux sysfs `queue/zoned` and `queue/rotational`;
|
|
1259
|
+
* a partition counts as its disk). `'unknown'` for filesystems without a
|
|
1260
|
+
* block device of their own (tmpfs, overlay, network filesystems),
|
|
1261
|
+
* device-mapper, md, loop and other virtual block devices, and on every
|
|
1262
|
+
* platform but Linux. Virtual and network block devices report the
|
|
1263
|
+
* rotational flag their driver or hypervisor sets (QEMU disks show
|
|
1264
|
+
* `'rotational'` unless configured with `rotation_rate=1`).
|
|
1265
|
+
*/
|
|
1266
|
+
detected: TDriveMedia | 'unknown';
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1197
1269
|
/**
|
|
1198
1270
|
* Local drive health as measured by objectstorage's runtime probes.
|
|
1199
1271
|
*/
|
|
@@ -1208,6 +1280,7 @@ export interface IClusterDriveHealth {
|
|
|
1208
1280
|
lastError?: string;
|
|
1209
1281
|
lastCheck?: number;
|
|
1210
1282
|
erasureSetId?: number;
|
|
1283
|
+
media: IClusterDriveMedia;
|
|
1211
1284
|
}
|
|
1212
1285
|
|
|
1213
1286
|
export interface IClusterErasureHealth {
|
|
@@ -1234,6 +1307,25 @@ export interface IClusterRepairHealth {
|
|
|
1234
1307
|
lastError?: string;
|
|
1235
1308
|
}
|
|
1236
1309
|
|
|
1310
|
+
export interface IClusterTransportCertificate {
|
|
1311
|
+
/** SHA-256 of the DER certificate, lowercase hex. */
|
|
1312
|
+
sha256: string;
|
|
1313
|
+
/** Milliseconds since the Unix epoch. */
|
|
1314
|
+
notAfter: number;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* How the cluster transport authenticates peers, and the certificates it
|
|
1319
|
+
* loaded at startup.
|
|
1320
|
+
*/
|
|
1321
|
+
export interface IClusterTransportHealth {
|
|
1322
|
+
mode: 'mutualTls' | 'unauthenticated';
|
|
1323
|
+
/** This node's certificate; only in mode 'mutualTls'. */
|
|
1324
|
+
certificate?: IClusterTransportCertificate;
|
|
1325
|
+
/** The loaded CA bundle in file order; only in mode 'mutualTls'. */
|
|
1326
|
+
caCertificates?: IClusterTransportCertificate[];
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1237
1329
|
/**
|
|
1238
1330
|
* Cluster runtime health from the Rust core.
|
|
1239
1331
|
* When clustering is disabled, the response is `{ enabled: false }`.
|
|
@@ -1249,6 +1341,7 @@ export interface IClusterHealth {
|
|
|
1249
1341
|
drives?: IClusterDriveHealth[];
|
|
1250
1342
|
erasure?: IClusterErasureHealth;
|
|
1251
1343
|
repairs?: IClusterRepairHealth;
|
|
1344
|
+
transport?: IClusterTransportHealth;
|
|
1252
1345
|
}
|
|
1253
1346
|
|
|
1254
1347
|
/**
|
|
@@ -1450,6 +1543,33 @@ function mergeConfig(userConfig: IObjectStorageConfig): TObjectStorageResolvedCo
|
|
|
1450
1543
|
} as TObjectStorageResolvedConfig;
|
|
1451
1544
|
}
|
|
1452
1545
|
|
|
1546
|
+
const DEFAULT_DASHBOARD_ADDRESS = '127.0.0.1';
|
|
1547
|
+
const DEFAULT_DASHBOARD_PORT = 3001;
|
|
1548
|
+
|
|
1549
|
+
function resolveDashboardConfig(
|
|
1550
|
+
dashboardArg: IObjectStorageDashboardConfig | undefined,
|
|
1551
|
+
s3PortArg: number,
|
|
1552
|
+
): Required<IObjectStorageDashboardConfig> | undefined {
|
|
1553
|
+
if (dashboardArg === undefined) {
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof dashboardArg.enabled !== 'boolean') {
|
|
1557
|
+
throw new Error('dashboard.enabled must be a boolean.');
|
|
1558
|
+
}
|
|
1559
|
+
const address = dashboardArg.address ?? DEFAULT_DASHBOARD_ADDRESS;
|
|
1560
|
+
if (typeof address !== 'string' || address.length === 0) {
|
|
1561
|
+
throw new Error('dashboard.address must be a non-empty string.');
|
|
1562
|
+
}
|
|
1563
|
+
const port = dashboardArg.port ?? DEFAULT_DASHBOARD_PORT;
|
|
1564
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1565
|
+
throw new Error('dashboard.port must be an integer from 0 to 65535.');
|
|
1566
|
+
}
|
|
1567
|
+
if (dashboardArg.enabled && port !== 0 && port === s3PortArg) {
|
|
1568
|
+
throw new Error(`dashboard.port ${port} is the S3 port; the dashboard needs its own port.`);
|
|
1569
|
+
}
|
|
1570
|
+
return { enabled: dashboardArg.enabled, address, port };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1453
1573
|
function createAccessKeyId(): string {
|
|
1454
1574
|
return `SS${plugins.crypto.randomBytes(10).toString('hex').toUpperCase()}`;
|
|
1455
1575
|
}
|
|
@@ -1608,6 +1728,7 @@ type TRustStorageCommands = {
|
|
|
1608
1728
|
result: { available: boolean; credentialCount: number; tombstoneCount: number };
|
|
1609
1729
|
};
|
|
1610
1730
|
getClusterHealth: { params: {}; result: IClusterHealth };
|
|
1731
|
+
getClusterSnapshot: { params: {}; result: IObjectStorageClusterSnapshot };
|
|
1611
1732
|
runHealingScan: { params: {}; result: IClusterHealingScanResult };
|
|
1612
1733
|
};
|
|
1613
1734
|
|
|
@@ -1692,6 +1813,8 @@ export class ObjectStorage {
|
|
|
1692
1813
|
private bridgeOwned = false;
|
|
1693
1814
|
private stopRequested = false;
|
|
1694
1815
|
private stopCompleted = false;
|
|
1816
|
+
private readonly dashboardConfig: Required<IObjectStorageDashboardConfig> | undefined;
|
|
1817
|
+
private dashboard: ObjectStorageDashboard | undefined;
|
|
1695
1818
|
|
|
1696
1819
|
constructor(configArg: IObjectStorageConfig = {}) {
|
|
1697
1820
|
const binaryPath = configArg.binaryPath;
|
|
@@ -1702,6 +1825,7 @@ export class ObjectStorage {
|
|
|
1702
1825
|
throw new Error('binaryPath must be a non-empty executable path.');
|
|
1703
1826
|
}
|
|
1704
1827
|
this.config = mergeConfig(configArg);
|
|
1828
|
+
this.dashboardConfig = resolveDashboardConfig(configArg.dashboard, this.config.server.port!);
|
|
1705
1829
|
this.bridge = new plugins.RustBridge<TRustStorageCommands>({
|
|
1706
1830
|
binaryName: 'ruststorage',
|
|
1707
1831
|
...(binaryPath !== undefined
|
|
@@ -1767,6 +1891,23 @@ export class ObjectStorage {
|
|
|
1767
1891
|
throw new Error('Storage engine did not report a valid bound TCP port.');
|
|
1768
1892
|
}
|
|
1769
1893
|
this.boundPort = result.port;
|
|
1894
|
+
if (this.dashboardConfig?.enabled) {
|
|
1895
|
+
// Enabled but unable to start (a port in use, say) fails startup:
|
|
1896
|
+
// the catch below stops the engine.
|
|
1897
|
+
const { ObjectStorageDashboard } = await import('./classes.dashboard.js');
|
|
1898
|
+
const dashboard = new ObjectStorageDashboard({
|
|
1899
|
+
address: this.dashboardConfig.address,
|
|
1900
|
+
port: this.dashboardConfig.port,
|
|
1901
|
+
readSnapshot: () => this.getClusterSnapshot(),
|
|
1902
|
+
});
|
|
1903
|
+
try {
|
|
1904
|
+
await dashboard.start();
|
|
1905
|
+
} catch (dashboardError) {
|
|
1906
|
+
await dashboard.stop();
|
|
1907
|
+
throw dashboardError;
|
|
1908
|
+
}
|
|
1909
|
+
this.dashboard = dashboard;
|
|
1910
|
+
}
|
|
1770
1911
|
} catch (error) {
|
|
1771
1912
|
try {
|
|
1772
1913
|
if (this.bridgeOwned) {
|
|
@@ -1794,6 +1935,11 @@ export class ObjectStorage {
|
|
|
1794
1935
|
return this.running ? this.boundPort : this.config.server.port!;
|
|
1795
1936
|
}
|
|
1796
1937
|
|
|
1938
|
+
/** The dashboard's bound TCP port while it runs; `undefined` otherwise. */
|
|
1939
|
+
public get dashboardPort(): number | undefined {
|
|
1940
|
+
return this.dashboard?.port;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1797
1943
|
public async getStorageDescriptor(
|
|
1798
1944
|
optionsArg?: Partial<plugins.tsclass.storage.IS3Descriptor>,
|
|
1799
1945
|
): Promise<plugins.tsclass.storage.IS3Descriptor> {
|
|
@@ -2182,6 +2328,17 @@ export class ObjectStorage {
|
|
|
2182
2328
|
return this.bridge.sendCommand('getClusterHealth', {});
|
|
2183
2329
|
}
|
|
2184
2330
|
|
|
2331
|
+
/**
|
|
2332
|
+
* The dashboard's snapshot, built by the engine from memory only: it never
|
|
2333
|
+
* probes a drive, syncs or walks storage. A cluster node also asks each
|
|
2334
|
+
* online peer for its own snapshot, waiting up to a second per peer, in
|
|
2335
|
+
* parallel; a peer that does not answer is reported unavailable. One
|
|
2336
|
+
* snapshot runs at a time: a call while another runs rejects.
|
|
2337
|
+
*/
|
|
2338
|
+
public async getClusterSnapshot(): Promise<IObjectStorageClusterSnapshot> {
|
|
2339
|
+
return this.bridge.sendCommand('getClusterSnapshot', {});
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2185
2342
|
/**
|
|
2186
2343
|
* Runs a healing scan on this cluster node and resolves with its result.
|
|
2187
2344
|
* The scan covers the manifests this node coordinated. The request joins
|
|
@@ -2332,6 +2489,10 @@ export class ObjectStorage {
|
|
|
2332
2489
|
// Startup keeps its own rejection; stop joins only its resource ownership.
|
|
2333
2490
|
await Promise.allSettled([this.startPromise]);
|
|
2334
2491
|
}
|
|
2492
|
+
if (this.dashboard) {
|
|
2493
|
+
await this.dashboard.stop();
|
|
2494
|
+
this.dashboard = undefined;
|
|
2495
|
+
}
|
|
2335
2496
|
if (this.bridgeOwned) {
|
|
2336
2497
|
if (this.running) {
|
|
2337
2498
|
this.completedStopResult ??= await this.bridge.sendCommand('stop', {});
|