@lossless.org/objectstorage 11.0.0 → 11.2.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 +116 -1
- package/dist_ts/index.js +90 -2
- package/package.json +11 -7
- package/readme.md +400 -22
- 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 +217 -2
package/readme.md
CHANGED
|
@@ -69,7 +69,7 @@ described below.
|
|
|
69
69
|
### Clustering Features
|
|
70
70
|
|
|
71
71
|
- 🔗 **Erasure coding** — Reed-Solomon (configurable k data + m parity shards) for storage efficiency and fault tolerance
|
|
72
|
-
- 🚄 **QUIC transport** — multiplexed inter-node communication via `quinn` with zero head-of-line blocking.
|
|
72
|
+
- 🚄 **QUIC transport** — multiplexed inter-node communication via `quinn` with zero head-of-line blocking. Mutual TLS with a cluster CA when `cluster.tls` is set; without it, encrypted but not authenticated, for trusted networks only (see Cluster transport trust).
|
|
73
73
|
- 💽 **Multi-drive awareness** — each node manages multiple independent storage paths with health monitoring
|
|
74
74
|
- 🩺 **Cluster health introspection** — query native node, drive, quorum, and healing status for product dashboards
|
|
75
75
|
- 🤝 **Cluster membership** — static seed config + runtime join, heartbeat-based failure detection
|
|
@@ -184,8 +184,9 @@ Cluster mode requires the control root and every drive root to exist before
|
|
|
184
184
|
startup. The process user must own them, no path component may be a symlink,
|
|
185
185
|
and group/world write permission must be disabled.
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
network where every host that can reach `quicPort` is a
|
|
187
|
+
Without `cluster.tls` the cluster transport does not authenticate nodes: run
|
|
188
|
+
cluster mode only on a network where every host that can reach `quicPort` is a
|
|
189
|
+
cluster node, or set `cluster.tls` on every node (see
|
|
189
190
|
[Cluster transport trust](#cluster-transport-trust)).
|
|
190
191
|
|
|
191
192
|
```typescript
|
|
@@ -286,7 +287,7 @@ const config: IObjectStorageConfig = {
|
|
|
286
287
|
},
|
|
287
288
|
cluster: { // Optional — omit for standalone mode
|
|
288
289
|
enabled: true,
|
|
289
|
-
nodeId: 'node-1', // Auto-generated UUID if omitted
|
|
290
|
+
nodeId: 'node-1', // Auto-generated UUID if omitted; required with tls
|
|
290
291
|
quicPort: 4000, // Default: 4000
|
|
291
292
|
seedNodes: [], // Addresses of existing cluster members
|
|
292
293
|
erasure: {
|
|
@@ -301,6 +302,16 @@ const config: IObjectStorageConfig = {
|
|
|
301
302
|
heartbeatIntervalMs: 5000, // Default: 5000
|
|
302
303
|
heartbeatTimeoutMs: 30000, // Default: 30000
|
|
303
304
|
getDeadlineMs: 21600000, // Default: 6 h; 60000 (1 min) to 604800000 (7 d)
|
|
305
|
+
tls: { // Optional mutual TLS; set on every node or on none
|
|
306
|
+
caFile: '/etc/objectstorage/tls/ca.pem',
|
|
307
|
+
certFile: '/etc/objectstorage/tls/node-1.pem', // its DNS SAN is nodeId
|
|
308
|
+
keyFile: '/etc/objectstorage/tls/node-1.key', // mode 0600 or 0400
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
dashboard: { // Optional read-only dashboard; off unless enabled
|
|
312
|
+
enabled: true,
|
|
313
|
+
address: '127.0.0.1', // Default: '127.0.0.1'
|
|
314
|
+
port: 3001, // Default: 3001; 0 allocates an ephemeral TCP port; never the S3 port
|
|
304
315
|
},
|
|
305
316
|
};
|
|
306
317
|
|
|
@@ -617,7 +628,8 @@ to the prefix (its folder marker) is covered.
|
|
|
617
628
|
(see [Supported S3 Operations](#supported-s3-operations)), so a parameter
|
|
618
629
|
the operation does not implement gets `NotImplemented` (501).
|
|
619
630
|
- **DeleteObjects.** Every member is checked on its own: covered keys are
|
|
620
|
-
deleted, and the others get a per-key `AccessDenied
|
|
631
|
+
deleted, and the others get a per-key `AccessDenied`, as does every member
|
|
632
|
+
that names a `VersionId`.
|
|
621
633
|
- **Listings.** ListObjects V1, ListObjectsV2, ListObjectVersions and
|
|
622
634
|
ListMultipartUploads need a `prefix` parameter that is covered. A missing or
|
|
623
635
|
wider prefix is refused, never clamped.
|
|
@@ -625,7 +637,8 @@ to the prefix (its folder marker) is covered.
|
|
|
625
637
|
- ListBuckets returns only the credential's bucket.
|
|
626
638
|
- HeadBucket and `GET /{bucket}?location` work on that bucket.
|
|
627
639
|
- Any other bucket-level query (`?acl`, `?versioning`, `?lifecycle`, …) is
|
|
628
|
-
refused with `AccessDenied`, and so is any repeated query parameter
|
|
640
|
+
refused with `AccessDenied`, and so is any repeated query parameter, so a
|
|
641
|
+
prefix credential can neither read nor change a bucket's versioning.
|
|
629
642
|
- Creating or deleting buckets and bucket policy operations are refused.
|
|
630
643
|
- **Policies.** The scope check runs before bucket policies, so a policy Allow
|
|
631
644
|
(including `Principal: "*"`) never widens a prefix credential. A policy Deny
|
|
@@ -924,8 +937,10 @@ Absent-bucket fenced ensure removes any residual policy before creating the new
|
|
|
924
937
|
resource; existing-bucket ensure preserves policy and object data. Once a bucket
|
|
925
938
|
has durable fence history, legacy `createBucket()`, `createBucketTenant()`,
|
|
926
939
|
`rotateBucketTenantCredentials()`, `deleteBucketTenant()`, and unfenced
|
|
927
|
-
`importBucket()` mutations fail with `EFENCE_REQUIRED`.
|
|
928
|
-
|
|
940
|
+
`importBucket()` mutations fail with `EFENCE_REQUIRED`. S3 `CreateBucket` and
|
|
941
|
+
`DeleteBucket` on such a bucket get `InvalidBucketState` (`409`), whose message
|
|
942
|
+
is the `EFENCE_REQUIRED: …` text. Use fenced ensure, exact import, and exact
|
|
943
|
+
delete for its lifecycle.
|
|
929
944
|
|
|
930
945
|
`replaceCredentials()` is a trusted process-local administrative override, not
|
|
931
946
|
a fenced lifecycle API. If it changes exact-owned credential material, completed
|
|
@@ -941,7 +956,8 @@ const metrics = await storage.getMetrics();
|
|
|
941
956
|
|
|
942
957
|
- `getHealth()` reports running state, storage directory and pool observation, auth enabled state, credential counts, bucket count, object count, total bytes, and cluster health.
|
|
943
958
|
- `getHealth().auth` also reports `prefixCredentialCount` (live prefix credentials), `prefixCredentialTombstoneCount` (unexpired revocations) and `prefixCredentialStoreAvailable` (`false` while the prefix credential file is unreadable and every prefix credential is refused).
|
|
944
|
-
- `getMetrics()` returns numeric counters and a Prometheus text snippet for bucket, object, byte, tenant credential, prefix credential (`objectstorage_prefix_credentials_total`, `objectstorage_prefix_credential_tombstones_total`, `objectstorage_prefix_credential_store_available`), and cluster-enabled metrics.
|
|
959
|
+
- `getMetrics()` returns numeric counters and a Prometheus text snippet for bucket, object, byte, tenant credential, prefix credential (`objectstorage_prefix_credentials_total`, `objectstorage_prefix_credential_tombstones_total`, `objectstorage_prefix_credential_store_available`), version (`objectstorage_noncurrent_versions_total`, `objectstorage_noncurrent_bytes_total`, `objectstorage_delete_markers_total`) and cluster-enabled metrics.
|
|
960
|
+
- `getHealth().stampClockAheadOfWallMs` is present while version commit times run ahead of the wall clock (a wall clock that went backwards, see [Bucket versioning](#bucket-versioning)).
|
|
945
961
|
- `getHealth()` reads the prefix credential figures through the `getPrefixCredentialStatus` IPC command (no TypeScript method of its own), and `getMetrics()` derives its prefix metrics from `getHealth()`. The command takes no parameters and returns `{ available, credentialCount, tombstoneCount }`: counts and the store state only, never an access key ID, secret, bucket or prefix.
|
|
946
962
|
|
|
947
963
|
## Runtime Stats
|
|
@@ -962,6 +978,11 @@ interface IBucketSummary {
|
|
|
962
978
|
objectCount: number;
|
|
963
979
|
totalSizeBytes: number;
|
|
964
980
|
creationDate?: number;
|
|
981
|
+
// Versioned buckets only:
|
|
982
|
+
versioning?: 'Enabled' | 'Suspended';
|
|
983
|
+
noncurrentVersionCount?: number;
|
|
984
|
+
noncurrentBytes?: number;
|
|
985
|
+
deleteMarkerCount?: number;
|
|
965
986
|
}
|
|
966
987
|
|
|
967
988
|
interface IStorageLocationSummary {
|
|
@@ -979,9 +1000,22 @@ interface IStorageStats {
|
|
|
979
1000
|
buckets: IBucketSummary[];
|
|
980
1001
|
storageDirectory: string;
|
|
981
1002
|
storageLocations?: IStorageLocationSummary[];
|
|
1003
|
+
// With at least one versioned bucket:
|
|
1004
|
+
totalNoncurrentVersionCount?: number;
|
|
1005
|
+
totalNoncurrentBytes?: number;
|
|
1006
|
+
totalDeleteMarkerCount?: number;
|
|
1007
|
+
stampClockAheadOfWallMs?: number;
|
|
982
1008
|
}
|
|
983
1009
|
```
|
|
984
1010
|
|
|
1011
|
+
- Object counts and bytes count current objects only. A versioned bucket also
|
|
1012
|
+
reports its noncurrent data versions and their payload bytes
|
|
1013
|
+
(`noncurrentVersionCount`, `noncurrentBytes`) and its delete markers, current
|
|
1014
|
+
and noncurrent (`deleteMarkerCount`); the stats totals add them up, and
|
|
1015
|
+
`getMetrics()` exports them as `objectstorage_noncurrent_versions_total`,
|
|
1016
|
+
`objectstorage_noncurrent_bytes_total` and `objectstorage_delete_markers_total`.
|
|
1017
|
+
Stored capacity is current plus noncurrent bytes.
|
|
1018
|
+
|
|
985
1019
|
- `bucketCount`, `totalObjectCount`, `totalStorageBytes`, and per-bucket totals are logical object stats maintained by the Rust runtime. They count object payload bytes, not sidecar files or erasure-coded shard overhead.
|
|
986
1020
|
- objectstorage initializes these values from native on-disk state at startup, then keeps them in memory and updates them when bucket/object mutations succeed. Stats reads do not issue S3 `ListObjects` or rescan every object.
|
|
987
1021
|
- Values are exact for mutations performed through objectstorage after startup. Direct filesystem edits outside objectstorage are not watched; restart the server to resync.
|
|
@@ -1011,6 +1045,23 @@ interface IClusterHealth {
|
|
|
1011
1045
|
drives?: IClusterDriveHealth[];
|
|
1012
1046
|
erasure?: IClusterErasureHealth;
|
|
1013
1047
|
repairs?: IClusterRepairHealth;
|
|
1048
|
+
transport?: IClusterTransportHealth;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
interface IClusterTransportHealth {
|
|
1052
|
+
mode: 'mutualTls' | 'unauthenticated';
|
|
1053
|
+
certificate?: { sha256: string; notAfter: number }; // only with mutualTls
|
|
1054
|
+
caCertificates?: { sha256: string; notAfter: number }[]; // file order; only with mutualTls
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
type TDriveMedia = 'rotational' | 'smr' | 'flash' | 'zoned';
|
|
1058
|
+
|
|
1059
|
+
interface IClusterDriveHealth {
|
|
1060
|
+
// index, path, status, capacity, error and erasure set fields, plus:
|
|
1061
|
+
media: {
|
|
1062
|
+
declared?: TDriveMedia; // not reported yet: drives cannot declare their media
|
|
1063
|
+
detected: TDriveMedia | 'unknown'; // detected once at startup
|
|
1064
|
+
};
|
|
1014
1065
|
}
|
|
1015
1066
|
```
|
|
1016
1067
|
|
|
@@ -1018,10 +1069,88 @@ interface IClusterHealth {
|
|
|
1018
1069
|
- Standalone mode returns `{ enabled: false }`.
|
|
1019
1070
|
- Peer status is the local node's current view of cluster membership and heartbeats, so it is best-effort and may lag real network state.
|
|
1020
1071
|
- Drive health is based on live native probe checks on the configured local drive paths. Capacity values are cheap filesystem snapshots.
|
|
1072
|
+
- `drives[].media.detected` is the medium the engine detected for each drive once at startup, never per health call. On Linux it follows the drive root's filesystem to the whole disk behind it (`st_dev` through `/sys/dev/block`; a partition counts as its disk) and reads the disk's `queue/zoned` and `queue/rotational`: `zoned` for host-managed or host-aware devices (SMR or ZNS), otherwise `rotational` or `flash`, as the disk's driver sets `queue/rotational`. Virtual and network block devices report the flag their driver or hypervisor sets: QEMU disks show `rotational` unless they are configured with `rotation_rate=1`, and NVMe volumes show `flash`. Declaring a drive's media, which is planned, is what will correct such a reading. It is `unknown`, never a guess, for filesystems without a block device of their own (tmpfs, overlay, network and FUSE filesystems, btrfs subvolumes), for device-mapper, md, loop and other virtual block devices (LVM, dm-crypt and software RAID included), when sysfs gives no usable answer, and on every platform but Linux. Drive-managed SMR reports itself as rotational and cannot be detected (`smr` can only ever be declared). The detected media change no behaviour today.
|
|
1021
1073
|
- `quorumHealthy` means the local node currently sees majority quorum and enough available placements in every erasure set to satisfy the configured write quorum.
|
|
1074
|
+
- `transport.mode` is `mutualTls` when `cluster.tls` is set and `unauthenticated` otherwise. With mutual TLS, `certificate` and `caCertificates` describe the files the engine loaded at startup: the SHA-256 of each DER certificate (lowercase hex) and its `notAfter` in milliseconds since the Unix epoch. The files are read only at startup, so during a certificate or CA rotation these show what each node actually runs.
|
|
1022
1075
|
- Repair fields expose the background healer's currently available runtime state. They are best-effort and limited to what the engine tracks today, such as whether a scan is active, the last completed run, and the last error.
|
|
1023
1076
|
- `repairs.shardsChecked`, `shardsHealed` and `failed` count the last completed scan's placement checks, shards written and failures. `repairs.shardsPending` counts the pending shards (see [Self-Healing](#self-healing)) that scan left pending because their drive was out of reach, refused the write, or their object was busy. While a scan runs, `lastRunStartedAt` is the running scan's start and the counts are still the previous scan's; `runHealingScan()` returns one scan's start and counts together.
|
|
1024
1077
|
|
|
1078
|
+
## Dashboard
|
|
1079
|
+
|
|
1080
|
+
Every node can serve a small read-only dashboard for quick visibility: the
|
|
1081
|
+
cluster's nodes, drives, erasure layout and quorum, object and byte totals,
|
|
1082
|
+
request, error and byte rates, healing state and recent engine events. It is
|
|
1083
|
+
off by default:
|
|
1084
|
+
|
|
1085
|
+
```typescript
|
|
1086
|
+
const storage = await ObjectStorage.createAndStart({
|
|
1087
|
+
// ...
|
|
1088
|
+
dashboard: { enabled: true, address: '10.0.0.5', port: 3001 },
|
|
1089
|
+
});
|
|
1090
|
+
console.log(storage.dashboardPort); // the bound port; undefined while off
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Open `http://<address>:<port>/` in a browser. The page is bundled into the
|
|
1094
|
+
package (`ts_web/`, built into `ts/dashboard.bundled.ts`), so nothing is
|
|
1095
|
+
fetched from elsewhere, and it updates itself over one TypedSocket connection
|
|
1096
|
+
to the same origin.
|
|
1097
|
+
|
|
1098
|
+
- **Configuration.** `dashboard.enabled` must be set. `address` defaults to
|
|
1099
|
+
`127.0.0.1` and `port` to `3001`; `port: 0` asks the OS for a port. A port
|
|
1100
|
+
equal to `server.port` is refused at construction. A dashboard that is
|
|
1101
|
+
enabled but cannot bind fails `start()`, and the engine is stopped: it never
|
|
1102
|
+
runs silently without it. The `dashboard` section is not sent to the engine.
|
|
1103
|
+
- **What it shows.** Nodes (status and last heartbeat in this node's view,
|
|
1104
|
+
start time); local drives of every node (status, usage, errors,
|
|
1105
|
+
last probe); the erasure layout and quorum; objects and bytes per node and
|
|
1106
|
+
summed; request, error and byte rates per node and summed, with two-minute
|
|
1107
|
+
sparklines kept in the browser; healing per node (last scan, checked,
|
|
1108
|
+
healed, pending shards, failures); and the newest events of each node (peer
|
|
1109
|
+
status changes, drive status changes, healing scan results, engine start).
|
|
1110
|
+
A standalone node shows its totals, rates and events. Rates are derived
|
|
1111
|
+
from counters of the S3 listener; byte counts are taken at the TCP stream
|
|
1112
|
+
and include HTTP headers. Nothing is stored: a reload starts the sparklines
|
|
1113
|
+
again.
|
|
1114
|
+
- **Where the data comes from.** One management command, `getClusterSnapshot`
|
|
1115
|
+
(also `ObjectStorage.getClusterSnapshot()`), built by the engine from memory
|
|
1116
|
+
only: the drive states the last heartbeat probe left, the in-memory runtime
|
|
1117
|
+
stats, healing state, S3 listener counters and a ring of the newest 64
|
|
1118
|
+
events. It never probes a drive, syncs, walks a directory or reads the
|
|
1119
|
+
control store. A cluster node asks each online peer for that peer's own
|
|
1120
|
+
snapshot over QUIC (`NodeSnapshot`), in parallel, with one second per peer;
|
|
1121
|
+
a peer that is not online or does not answer in time shows as unavailable in
|
|
1122
|
+
that sample. Any node with the dashboard on therefore shows the whole
|
|
1123
|
+
cluster, and the other nodes need no dashboard of their own.
|
|
1124
|
+
- **Cost.** With no dashboard connected there is no timer, no IPC and no QUIC
|
|
1125
|
+
traffic. While at least one is connected, one shared sampler takes a
|
|
1126
|
+
snapshot every two seconds, keeps at most one in flight and skips ticks
|
|
1127
|
+
while one runs, and pushes the same sample (a few KB per node) to every
|
|
1128
|
+
connected dashboard. At most 16 dashboard sockets are admitted at once; the
|
|
1129
|
+
17th is refused with `503`. On the engine side, request and byte counting is
|
|
1130
|
+
a relaxed atomic add per response and per socket read or write. An enabled
|
|
1131
|
+
dashboard with nobody watching runs no timer and sends no traffic; on a
|
|
1132
|
+
three-node cluster against 11.0.0 its CPU cost per request was within 0.4 %,
|
|
1133
|
+
and the engine's memory was unchanged. Its server modules and bundled page
|
|
1134
|
+
add about 50 MiB to the Node.js process of each node once, at start, and
|
|
1135
|
+
that does not grow with time or stored objects. With 16 dashboards open on
|
|
1136
|
+
every node, large uploads ran about 1.5 % slower and the p99 latency was
|
|
1137
|
+
about 5 % higher.
|
|
1138
|
+
- **Security.** The dashboard is read-only by construction: its socket
|
|
1139
|
+
registers one read handler and nothing else, so it cannot start a healing
|
|
1140
|
+
scan, change configuration or touch S3 traffic or stored data. It has no
|
|
1141
|
+
authentication: protect it with the bind address and a firewall, for
|
|
1142
|
+
example by binding it to a private interface only. It shows node IDs,
|
|
1143
|
+
addresses, drive paths, counts and event messages, never credentials or
|
|
1144
|
+
object keys.
|
|
1145
|
+
- **Versions.** Run every node on the same version, as clusters already
|
|
1146
|
+
require. An engine that predates `NodeSnapshot` cannot decode the request,
|
|
1147
|
+
logs a stream error and closes the stream, and the asking node shows it as
|
|
1148
|
+
unavailable.
|
|
1149
|
+
|
|
1150
|
+
`getClusterHealth()` probes every local drive (a write, sync and read of a
|
|
1151
|
+
probe file) on each call; poll `getClusterSnapshot()` instead when you need a
|
|
1152
|
+
cheap periodic view.
|
|
1153
|
+
|
|
1025
1154
|
## Supported S3 Operations
|
|
1026
1155
|
|
|
1027
1156
|
The router fails closed, in standalone and cluster mode alike. A request
|
|
@@ -1044,7 +1173,7 @@ current object, and `GET ?versioning` answered with an object listing.
|
|
|
1044
1173
|
| ListObjectVersions | `GET /{bucket}?versions` | `prefix`, `delimiter`, `max-keys`, `key-marker`, `version-id-marker`, `encoding-type` |
|
|
1045
1174
|
| ListMultipartUploads | `GET /{bucket}?uploads` | `prefix`, `encoding-type` |
|
|
1046
1175
|
| GetBucketLocation | `GET /{bucket}?location` | none |
|
|
1047
|
-
| GetBucketVersioning | `GET /{bucket}?versioning` | none |
|
|
1176
|
+
| GetBucketVersioning, PutBucketVersioning | `GET`, `PUT /{bucket}?versioning` | none (PUT: standalone only, `NotImplemented` in cluster mode) |
|
|
1048
1177
|
| GetBucketLifecycleConfiguration | `GET /{bucket}?lifecycle` | none |
|
|
1049
1178
|
| GetBucketPolicy, PutBucketPolicy, DeleteBucketPolicy | `GET`, `PUT`, `DELETE /{bucket}?policy` | none |
|
|
1050
1179
|
| DeleteObjects | `POST /{bucket}?delete` | none |
|
|
@@ -1082,9 +1211,129 @@ override such as `response-content-type=text/html` would let a public object
|
|
|
1082
1211
|
serve script from the storage origin; a presigned URL is such a request. With
|
|
1083
1212
|
auth disabled the overrides apply.
|
|
1084
1213
|
|
|
1085
|
-
|
|
1214
|
+
### Bucket versioning
|
|
1215
|
+
|
|
1216
|
+
Standalone buckets support S3 versioning exactly as S3 defines it: a bucket is
|
|
1217
|
+
unversioned until `PutBucketVersioning` sets `Enabled` or `Suspended`, and it
|
|
1218
|
+
never goes back to unversioned. New buckets are unversioned. A bucket that was
|
|
1219
|
+
never versioned behaves exactly as before versioning existed, on disk and on
|
|
1220
|
+
the wire: no version ids, no `x-amz-version-id` header, `null` as its only
|
|
1221
|
+
version id. In cluster mode `PUT ?versioning` answers `NotImplemented` (501),
|
|
1222
|
+
so every cluster bucket stays unversioned for now.
|
|
1223
|
+
|
|
1224
|
+
- **Version ids.** A version written while versioning is `Enabled` gets a
|
|
1225
|
+
lowercase UUID as its version id. Versions written before versioning was
|
|
1226
|
+
enabled, and every version written while it is `Suspended`, are the key's
|
|
1227
|
+
`null` version; a key has at most one.
|
|
1228
|
+
- **Enabled.** PUT, CopyObject and CompleteMultipartUpload add a new current
|
|
1229
|
+
version and keep the previous one as noncurrent; the response carries
|
|
1230
|
+
`x-amz-version-id`. A DELETE without a version id adds a delete marker as
|
|
1231
|
+
the current version (also for a key with no object, as S3 does): GET and
|
|
1232
|
+
HEAD then answer `NoSuchKey` (404) with `x-amz-delete-marker: true` and the
|
|
1233
|
+
marker's `x-amz-version-id`, and ListObjects no longer lists the key. The
|
|
1234
|
+
DELETE response carries `x-amz-delete-marker: true` and the marker's id.
|
|
1235
|
+
- **Suspended.** Writes create the `null` version and replace any previous
|
|
1236
|
+
`null` version of the key, wherever it is in the history; versions with ids
|
|
1237
|
+
stay. A DELETE replaces the `null` version with a `null` delete marker.
|
|
1238
|
+
Responses carry `x-amz-version-id: null`.
|
|
1239
|
+
- **By version id.** GET, HEAD and DELETE take `?versionId=` (an id or
|
|
1240
|
+
`null`). Reading a delete marker by its id answers `MethodNotAllowed` (405)
|
|
1241
|
+
with `x-amz-delete-marker: true`, its id and its `Last-Modified`. DELETE with
|
|
1242
|
+
a version id removes that version for good; when it was the current one, the
|
|
1243
|
+
next newest version becomes current again (a delete marker, or the object it
|
|
1244
|
+
covered). A well-formed id that names no version of the key is
|
|
1245
|
+
`NoSuchVersion` (404), also on DELETE; a malformed one is `InvalidArgument`.
|
|
1246
|
+
DeleteObjects accepts a `VersionId` per object and reports `VersionId`,
|
|
1247
|
+
`DeleteMarker` and `DeleteMarkerVersionId` per deleted entry.
|
|
1248
|
+
- **Copies.** A copy source may name a version (`?versionId=` after the
|
|
1249
|
+
source); `x-amz-copy-source-if-*` apply to that version, and the response
|
|
1250
|
+
carries `x-amz-copy-source-version-id` (and `x-amz-version-id` for the new
|
|
1251
|
+
version in a versioned bucket). A delete marker as a copy source is
|
|
1252
|
+
`InvalidRequest` (400); a source whose current version is a delete marker is
|
|
1253
|
+
`NoSuchKey`. UploadPartCopy works the same way.
|
|
1254
|
+
- **Conditional requests.** `If-None-Match: *` on PUT and
|
|
1255
|
+
CompleteMultipartUpload succeeds when the key has no current version or its
|
|
1256
|
+
current version is a delete marker. `If-Match` on DELETE is evaluated against
|
|
1257
|
+
the current version, also with a version id; a current delete marker fails
|
|
1258
|
+
it (412).
|
|
1259
|
+
- **ListObjectVersions** lists every version and delete marker in key order,
|
|
1260
|
+
each key newest first, with `IsLatest` on the current one; versions and
|
|
1261
|
+
delete markers, and common prefixes, all count against `max-keys`. A
|
|
1262
|
+
truncated page carries `NextKeyMarker` and, after a version,
|
|
1263
|
+
`NextVersionIdMarker`; `key-marker` alone resumes after every version of
|
|
1264
|
+
that key, with `version-id-marker` after that version of it. A
|
|
1265
|
+
`version-id-marker` naming a version that was deleted between the pages
|
|
1266
|
+
resumes at that key's newest remaining version: no version of the key is
|
|
1267
|
+
skipped, but versions the previous page already listed are listed again.
|
|
1268
|
+
`Last-Modified` of a version is its commit time.
|
|
1269
|
+
- **Permissions.** `PutBucketVersioning` is authorized as
|
|
1270
|
+
`s3:PutBucketVersioning`, `GetBucketVersioning` as `s3:GetBucketVersioning`,
|
|
1271
|
+
ListObjectVersions as `s3:ListBucketVersions`; a `versionId` (also `null`,
|
|
1272
|
+
also in DeleteObjects) as `s3:GetObjectVersion` or `s3:DeleteObjectVersion`.
|
|
1273
|
+
Only root credentials may change a bucket's versioning: bucket tenant
|
|
1274
|
+
credentials get `AccessDenied` for `PutBucketVersioning` (they may read it
|
|
1275
|
+
with `GetBucketVersioning`, list versions, and read and delete versions in
|
|
1276
|
+
their bucket), because the operator's bucket flows (export, import, fenced
|
|
1277
|
+
replacement and deletion, migrations) do not carry version history and
|
|
1278
|
+
refuse versioned buckets. This lifts once a format that carries history
|
|
1279
|
+
exists. Prefix credentials never name a version and cannot change
|
|
1280
|
+
versioning; they can list versions inside their prefix, and their DELETEs
|
|
1281
|
+
add delete markers.
|
|
1282
|
+
- **Not combined with.** A bucket with engine-enforced immutable retention
|
|
1283
|
+
refuses versioning, and a versioned bucket refuses retention
|
|
1284
|
+
(`InvalidBucketState`, 409). Bucket export and import, fenced bucket
|
|
1285
|
+
replacement and deletion, and bucket migrations refuse versioned buckets,
|
|
1286
|
+
because none of them carries version history; a migration whose source
|
|
1287
|
+
became versioned after it was created cannot be sealed
|
|
1288
|
+
(`EMIGRATION_VERSIONED`, the source stays active). `MfaDelete` is not
|
|
1289
|
+
implemented (501). A versioned bucket is empty for DeleteBucket only once
|
|
1290
|
+
every version and delete marker is deleted (`BucketNotEmpty` before);
|
|
1291
|
+
deleting it forgets its versioning, so a new bucket of the same name starts
|
|
1292
|
+
unversioned. `deleteBucketTenant` deletes every version with the bucket.
|
|
1293
|
+
- **Requests by version wait for writes.** Every request whose answer depends
|
|
1294
|
+
on a bucket's versioning state reads that state, and the history, under the
|
|
1295
|
+
bucket's gate: DELETE and each DeleteObjects member, GET and HEAD with a
|
|
1296
|
+
version id (also `null` on a bucket that was never versioned), a copy or
|
|
1297
|
+
UploadPartCopy source with a version id, the delete-marker answer of a key
|
|
1298
|
+
without a current object, and ListObjectVersions. A request that arrives
|
|
1299
|
+
while `PutBucketVersioning` or a write to the bucket is in progress is
|
|
1300
|
+
therefore served by the state that change leaves. A write holds the gate
|
|
1301
|
+
while it receives its body, so these requests, like every listing, wait
|
|
1302
|
+
for uploads to the same bucket that are in progress: measured with one
|
|
1303
|
+
1 GiB upload into the bucket, a GET by version id issued during it
|
|
1304
|
+
answered once the upload had finished, after 2.6 s at full loopback speed
|
|
1305
|
+
and after 20.3 s at 50 MB/s, while GETs without a version id stayed at a
|
|
1306
|
+
1.6 ms median (3.5 ms p99). GET and HEAD without a version id never wait.
|
|
1307
|
+
- **Startup.** A standalone engine checks every version chain once when it
|
|
1308
|
+
starts, reading every version's record and opening its payload (and
|
|
1309
|
+
repairing what a crash left), and takes the bucket counters from that
|
|
1310
|
+
check. Startup time therefore grows with the number of stored versions.
|
|
1311
|
+
Measured with 1,000,000 versions (100,000 keys × 10) on 8 CPUs with a
|
|
1312
|
+
12 GiB memory limit, on a KVM host whose page cache did not keep the root
|
|
1313
|
+
between starts: the first start after writing them took 346 s; six clean
|
|
1314
|
+
starts took 127 to 264 s (127, 145, 150, 193, 203 and 264 s, median about
|
|
1315
|
+
170 s); three starts after a SIGKILL during writes by 32 concurrent
|
|
1316
|
+
writers took 215 to 256 s. A root on which no bucket was ever versioned
|
|
1317
|
+
pays nothing for this and starts exactly as before. `start()` waits for as
|
|
1318
|
+
long as startup takes; once recovery passes 10 s, the engine logs its
|
|
1319
|
+
progress (chains, versions, elapsed time) to the host's stderr every 10 s.
|
|
1320
|
+
- **Commit times.** Version commit times come from one stamp clock per
|
|
1321
|
+
engine: `max(wall clock, last stamp + monotonic time since)`, persisted to
|
|
1322
|
+
`.smartstorage/versioning/stamp-clock.json` by every `PutBucketVersioning`
|
|
1323
|
+
that changes a bucket's state (before the state itself, and before the
|
|
1324
|
+
response), every 60 seconds and on a graceful stop. Within a process, a wall clock
|
|
1325
|
+
that jumps back never produces a commit time earlier than true time. After
|
|
1326
|
+
a restart with the wall clock behind, commit times can lag true time by at
|
|
1327
|
+
most the downtime plus 60 seconds (a suspended host counts as downtime). A
|
|
1328
|
+
forward jump that is corrected later keeps commit times ahead by the jump
|
|
1329
|
+
for the life of the process. While commit times run ahead of the wall clock,
|
|
1330
|
+
`getStorageStats()` and `getHealth()` report `stampClockAheadOfWallMs`.
|
|
1331
|
+
Within a key, commit times never go backwards, and a version becomes
|
|
1332
|
+
noncurrent at its successor's commit time.
|
|
1333
|
+
|
|
1334
|
+
The reads of an unversioned bucket keep their 11.0.0 semantics:
|
|
1086
1335
|
- `GET ?versioning` returns the empty `<VersioningConfiguration/>` of a bucket
|
|
1087
|
-
that was never versioned.
|
|
1336
|
+
that was never versioned.
|
|
1088
1337
|
- `GET ?lifecycle` returns `NoSuchLifecycleConfiguration` (404). `PUT` and
|
|
1089
1338
|
`DELETE ?lifecycle` are `NotImplemented`.
|
|
1090
1339
|
- `GET ?versions` lists every current object as its S3 null version
|
|
@@ -1092,11 +1341,12 @@ Versioning is never enabled on a bucket:
|
|
|
1092
1341
|
`key-marker` and `version-id-marker`. A truncated page returns the last key
|
|
1093
1342
|
or common prefix as `NextKeyMarker`, plus `NextVersionIdMarker` `null` after a
|
|
1094
1343
|
key. A `version-id-marker` needs a `key-marker`.
|
|
1095
|
-
- `versionId` on GET, HEAD and DELETE,
|
|
1096
|
-
|
|
1097
|
-
well-formed id (a lowercase
|
|
1098
|
-
`NoSuchVersion` (404; `NoSuchBucket` when
|
|
1099
|
-
else, the empty string included, returns
|
|
1344
|
+
- `versionId` on GET, HEAD and DELETE, a `VersionId` of a DeleteObjects
|
|
1345
|
+
member, and a `?versionId=` after the copy source of CopyObject and
|
|
1346
|
+
UploadPartCopy: `null` is the current object. A well-formed id (a lowercase
|
|
1347
|
+
UUID) names no version and returns `NoSuchVersion` (404; `NoSuchBucket` when
|
|
1348
|
+
the bucket is missing). Anything else, the empty string included, returns
|
|
1349
|
+
`InvalidArgument` (400); in DeleteObjects both fail only their member. A `?`
|
|
1100
1350
|
inside a copy-source key must be encoded as `%3F`.
|
|
1101
1351
|
- These reads are authorized as `s3:GetBucketVersioning`,
|
|
1102
1352
|
`s3:GetLifecycleConfiguration` and `s3:ListBucketVersions`. A `versionId`,
|
|
@@ -1205,6 +1455,12 @@ durable cluster metadata. Descriptor-confined standalone startup reclaims
|
|
|
1205
1455
|
abandoned private object materializations from interrupted create-only, copy,
|
|
1206
1456
|
or multipart publication without removing an already published hard link.
|
|
1207
1457
|
|
|
1458
|
+
`CompleteMultipartUpload` refuses, in standalone and cluster mode, a part list
|
|
1459
|
+
that names a part that was never uploaded or an ETag that does not match the
|
|
1460
|
+
uploaded part with `InvalidPart` (`400`), and a part listed twice with
|
|
1461
|
+
`InvalidPartOrder` (`400`). The upload stays open, so a completion with the
|
|
1462
|
+
correct parts still succeeds.
|
|
1463
|
+
|
|
1208
1464
|
Standalone `DeleteObject` also honors `If-Match: *` and comma-separated strong
|
|
1209
1465
|
ETags. It compares the current ETag and deletes inside the same bucket mutation
|
|
1210
1466
|
gate used by PUT, COPY, and multipart completion. A mismatch returns the S3
|
|
@@ -1455,16 +1711,79 @@ ahead of the client.
|
|
|
1455
1711
|
### QUIC Transport
|
|
1456
1712
|
|
|
1457
1713
|
Inter-node communication uses [QUIC](https://en.wikipedia.org/wiki/QUIC) via the `quinn` library:
|
|
1458
|
-
- **
|
|
1714
|
+
- **Mutual TLS or unauthenticated** — with `cluster.tls`, every connection is TLS 1.3 with a CA-signed certificate on both ends; without it, each node generates a self-signed certificate at startup and accepts any peer (see [Cluster transport trust](#cluster-transport-trust))
|
|
1459
1715
|
- 🔀 **Multiplexed streams** — concurrent shard transfers without head-of-line blocking
|
|
1460
1716
|
- ⚡ **Connection pooling** — persistent connections to peer nodes
|
|
1461
1717
|
- 🌊 **Natural backpressure** — QUIC flow control prevents overloading slow peers
|
|
1462
1718
|
|
|
1463
1719
|
### Cluster transport trust
|
|
1464
1720
|
|
|
1465
|
-
|
|
1721
|
+
Without `cluster.tls` the cluster transport does not authenticate nodes. Each node generates a self-signed certificate at startup and accepts any peer's certificate without verifying it. QUIC encrypts the traffic against passive eavesdropping, but not against an attacker on the path. Any host that can reach a node's `quicPort` can join the cluster, sync topology, and read, write, tombstone and purge shards. S3 credentials and bucket policies do not apply to this port. Run such a cluster only on a network where every host that can reach `quicPort` (UDP, default 4000) is a cluster node: a private network or VPN between the nodes, with `quicPort` firewalled from everything else.
|
|
1466
1722
|
|
|
1467
|
-
|
|
1723
|
+
With `cluster.tls` set, the transport is mutual TLS (TLS 1.3, no session resumption, no early data) with a cluster CA you run:
|
|
1724
|
+
|
|
1725
|
+
- **Identity.** A node's identity is the single DNS subject alternative name (SAN) of its certificate, and it equals `cluster.nodeId`. With `cluster.tls`, `nodeId` is required and must be a lowercase DNS name such as `node-1` or `node-1.storage.internal`; it is never generated, and it must still match the node id persisted in the control root. One certificate serves as both the server and the client certificate, so it must be valid for both: an extended key usage extension listing `serverAuth` and `clientAuth`, or no extended key usage extension at all (the recipe below issues both). Use a dedicated CA per cluster: every certificate it signs with one lowercase DNS SAN is a member of the cluster.
|
|
1726
|
+
- **Checks.** A node connects to a known peer only if the peer's certificate names exactly that peer's node id (one DNS SAN, no wildcard), and to a seed (`seedNodes`, known only by its address) only if the seed presents a certificate from the cluster CA. A node accepts a connection only with a client certificate from the cluster CA, and a join or heartbeat only for the node id that certificate names. A peer without such a certificate fails the handshake before it can send any request. Every CA-signed node is a full member: it may sync topology and read, write, tombstone and purge any shard, so the way to remove a compromised node is to stop trusting its certificate (a new CA, see the rotation below).
|
|
1727
|
+
- **Startup.** A node checks its own certificate the way its peers will (chain, validity, server and client authentication, the SAN, and that `keyFile` matches `certFile`) and refuses to start if that check or any file check below fails. The error names the file and the reason. A node never falls back to the unauthenticated transport. At startup it logs one line with its certificate's `notAfter` and the SHA-256 of each CA certificate, and `getClusterHealth().transport` reports them (see [Cluster Health](#cluster-health)).
|
|
1728
|
+
- **Files.** `caFile` holds one CA certificate, or two during a CA rotation; anything else is refused, which keeps a system CA bundle out. `certFile` holds the node certificate, optionally followed by intermediates, and nothing but certificates. `keyFile` holds exactly one unencrypted PKCS#8, SEC1 or PKCS#1 key; ECDSA P-256 is recommended. Each path must be absolute and canonical, with no symbolic link anywhere, and lie outside the control root and every drive root (roots are backed up and replaced, and a key must not travel with them). That check compares paths, so a bind mount that makes a root reachable under another path is not detected. Each file must be a regular file of at most 64 KiB. The files and their directories must be owned by the engine's user or by root and must not be group- or world-writable, and `keyFile` must be owned by the engine's user and grant no group or world permission (mode 0600 or 0400).
|
|
1729
|
+
- **Reload.** The files are read once at startup; a restart is the reload. Certificates are checked at every handshake, so a connection established before a certificate's `notAfter` keeps running until it reconnects, and new handshakes after `notAfter` fail. Renew certificates and restart the nodes before they expire.
|
|
1730
|
+
|
|
1731
|
+
#### Making the CA and the node certificates
|
|
1732
|
+
|
|
1733
|
+
The engine ships no CA tooling. With OpenSSL 3 or LibreSSL 3.3 (the `openssl` of macOS), create the CA once and keep `ca.key` offline. `ec_param_enc:named_curve` is required for LibreSSL, which otherwise writes the curve as explicit parameters that the engine refuses; OpenSSL 3 writes the named curve either way:
|
|
1734
|
+
|
|
1735
|
+
```bash
|
|
1736
|
+
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
|
|
1737
|
+
-pkeyopt ec_param_enc:named_curve -out ca.key
|
|
1738
|
+
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
|
|
1739
|
+
-subj "/CN=objectstorage cluster CA" \
|
|
1740
|
+
-addext "basicConstraints=critical,CA:TRUE" \
|
|
1741
|
+
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
1742
|
+
-out ca.pem
|
|
1743
|
+
```
|
|
1744
|
+
|
|
1745
|
+
Then issue one certificate per node, with the node id as its only DNS SAN:
|
|
1746
|
+
|
|
1747
|
+
```bash
|
|
1748
|
+
NODE_ID=node-1
|
|
1749
|
+
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
|
|
1750
|
+
-pkeyopt ec_param_enc:named_curve -out "$NODE_ID.key"
|
|
1751
|
+
openssl req -new -key "$NODE_ID.key" -subj "/CN=$NODE_ID" -out "$NODE_ID.csr"
|
|
1752
|
+
cat > "$NODE_ID.ext" <<EXT
|
|
1753
|
+
basicConstraints = critical,CA:FALSE
|
|
1754
|
+
keyUsage = critical,digitalSignature
|
|
1755
|
+
extendedKeyUsage = serverAuth,clientAuth
|
|
1756
|
+
subjectAltName = DNS:$NODE_ID
|
|
1757
|
+
EXT
|
|
1758
|
+
openssl x509 -req -in "$NODE_ID.csr" -CA ca.pem -CAkey ca.key -CAcreateserial \
|
|
1759
|
+
-days 365 -sha256 -extfile "$NODE_ID.ext" -out "$NODE_ID.pem"
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1762
|
+
Install `ca.pem` and the node's certificate and key on the node, as the user that runs the engine and outside its control and drive roots:
|
|
1763
|
+
|
|
1764
|
+
```bash
|
|
1765
|
+
install -d -m 0755 /etc/objectstorage/tls
|
|
1766
|
+
install -m 0644 ca.pem "$NODE_ID.pem" /etc/objectstorage/tls/
|
|
1767
|
+
install -m 0600 "$NODE_ID.key" /etc/objectstorage/tls/
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
Then set `cluster.tls` to the three paths (see [Configuration](#configuration)). The test suite runs exactly these commands (`test/test.cluster-tls.node.ts`).
|
|
1771
|
+
|
|
1772
|
+
**Docker secrets** are mounted owned by root with mode 0444 by default, which the key check refuses. Set the key secret's `uid` to the engine's user and its `mode` to `0400` in the service's long `secrets` syntax. Docker Compose ignores `uid`, `gid` and `mode` for secrets read from a file, because it bind-mounts the file; there the host file itself must be owned by the engine's uid with mode 0600 or 0400. Kubernetes secret volumes present their files through symbolic links, which the canonical-path rule refuses.
|
|
1773
|
+
|
|
1774
|
+
#### Enabling mutual TLS
|
|
1775
|
+
|
|
1776
|
+
Nodes with and without `cluster.tls` cannot talk to each other: a TLS node refuses peers without a client certificate and refuses self-signed servers. Enabling TLS is therefore not a rolling change: stop every node, configure `cluster.tls` on every node, then start every node. A node whose `nodeId` was generated keeps that id (the control root persists it, and `getClusterHealth().nodeId` reports it): configure it as `nodeId` and issue the node's certificate for it, or the node refuses to start. Disabling TLS takes the same full stop.
|
|
1777
|
+
|
|
1778
|
+
#### Rotating certificates and the CA
|
|
1779
|
+
|
|
1780
|
+
A node certificate is renewed by issuing a new one from the same CA and restarting the node; its peers need no change. A new CA takes three rolling restarts, with no cluster stop:
|
|
1781
|
+
|
|
1782
|
+
1. Put both CAs into every node's `caFile` (two certificates) and restart the nodes one at a time.
|
|
1783
|
+
2. Issue every node a certificate from the new CA and restart the nodes one at a time.
|
|
1784
|
+
3. Remove the old CA from every `caFile` and restart the nodes one at a time.
|
|
1785
|
+
|
|
1786
|
+
Every pair of nodes stays connected throughout, because trust only grows before the certificates change and only shrinks after. The order matters: a node with a certificate from the new CA cannot connect to a node that trusts only the old one.
|
|
1468
1787
|
|
|
1469
1788
|
### Cluster Membership
|
|
1470
1789
|
|
|
@@ -1616,6 +1935,10 @@ Spawn the Rust binary and start the HTTP server. Concurrent starts join the same
|
|
|
1616
1935
|
attempt; starting an already running instance succeeds without restarting it.
|
|
1617
1936
|
A start is rejected while shutdown or unconfirmed startup cleanup remains
|
|
1618
1937
|
pending. After completed shutdown, an explicit start begins a new lifecycle.
|
|
1938
|
+
A start has no time limit, because startup grows with the stored data (a
|
|
1939
|
+
standalone root with bucket versions checks every version chain); it still
|
|
1940
|
+
fails at once when the engine process exits. A long start shows its progress
|
|
1941
|
+
on the host's stderr, where every engine log line is forwarded.
|
|
1619
1942
|
|
|
1620
1943
|
#### `stop(): Promise<void>`
|
|
1621
1944
|
|
|
@@ -1847,7 +2170,7 @@ Atomically replace the active runtime credential set without restarting the serv
|
|
|
1847
2170
|
|
|
1848
2171
|
#### `getClusterHealth(): Promise<IClusterHealth>`
|
|
1849
2172
|
|
|
1850
|
-
Read the Rust core's current cluster, drive, quorum, and
|
|
2173
|
+
Read the Rust core's current cluster, drive, quorum, repair, and transport health snapshot. Standalone mode returns `{ enabled: false }`.
|
|
1851
2174
|
|
|
1852
2175
|
In cluster mode, `quicAddress` is the local QUIC/UDP socket's actual bound
|
|
1853
2176
|
address. With `cluster.quicPort: 0`, the operating system assigns its port; use
|
|
@@ -1855,6 +2178,20 @@ address. With `cluster.quicPort: 0`, the operating system assigns its port; use
|
|
|
1855
2178
|
Membership advertisements and peer `quicAddress` values contain that same bound
|
|
1856
2179
|
address, including after a restart.
|
|
1857
2180
|
|
|
2181
|
+
#### `getClusterSnapshot(): Promise<IObjectStorageClusterSnapshot>`
|
|
2182
|
+
|
|
2183
|
+
The [dashboard](#dashboard)'s snapshot, built by the engine from memory only:
|
|
2184
|
+
`local` is this node's `IObjectStorageNodeSnapshot` (counters, totals, drives
|
|
2185
|
+
from the last heartbeat probe, healing state, recent events) and, in cluster
|
|
2186
|
+
mode, `cluster` adds quorum, the erasure layout and every peer, with the peer's
|
|
2187
|
+
own snapshot when it answered within a second. One snapshot runs at a time; a
|
|
2188
|
+
call while another runs rejects. Standalone mode returns `cluster: null`.
|
|
2189
|
+
|
|
2190
|
+
#### `dashboardPort: number | undefined`
|
|
2191
|
+
|
|
2192
|
+
The dashboard's bound TCP port while it runs; `undefined` while it is off or
|
|
2193
|
+
stopped.
|
|
2194
|
+
|
|
1858
2195
|
#### `runHealingScan(options?: IRunHealingScanOptions): Promise<IClusterHealingScanResult>`
|
|
1859
2196
|
|
|
1860
2197
|
Run a healing scan on this cluster node (see [Self-Healing](#self-healing)) and
|
|
@@ -1913,7 +2250,7 @@ objectstorage uses a **hybrid Rust + TypeScript** architecture:
|
|
|
1913
2250
|
|
|
1914
2251
|
**Why Rust?** The original TypeScript implementation had critical perf issues: OOM on multipart uploads (parts buffered in memory), double stream copying, file descriptor leaks on HEAD requests, full-file reads for range requests, and no backpressure. The Rust binary solves these with bounded-memory streaming I/O, backpressure, and direct `seek()` for range requests.
|
|
1915
2252
|
|
|
1916
|
-
**IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `findOrphanObjectLeaves`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getBucketResourceFenceState`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, `createPrefixCredential`, `revokePrefixCredential`, `listPrefixCredentials`, `getPrefixCredentialStatus`, `getClusterHealth`, and `runHealingScan`. `getStoragePoolDiagnostic` reads a weak, cached diagnostic source and remains responsive while another management command owns the server; it does not retain the mounted root after server ownership completes. `runHealingScan` answers when its scan completes, with the request's `id`; the engine serves other commands meanwhile, and a shutdown answers a pending request with an error before the `stop` response.
|
|
2253
|
+
**IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `findOrphanObjectLeaves`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getBucketResourceFenceState`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, `createPrefixCredential`, `revokePrefixCredential`, `listPrefixCredentials`, `getPrefixCredentialStatus`, `getClusterHealth`, `getClusterSnapshot`, and `runHealingScan`. `getStoragePoolDiagnostic` reads a weak, cached diagnostic source and remains responsive while another management command owns the server; it does not retain the mounted root after server ownership completes. `runHealingScan` answers when its scan completes, with the request's `id`; the engine serves other commands meanwhile, and a shutdown answers a pending request with an error before the `stop` response. `getClusterSnapshot` is answered the same deferred way, because a cluster node waits up to a second for each peer.
|
|
1917
2254
|
|
|
1918
2255
|
### S3-Compatible Operations
|
|
1919
2256
|
|
|
@@ -1965,6 +2302,8 @@ those versions cannot read the canonical v2 objects.
|
|
|
1965
2302
|
| AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId` | |
|
|
1966
2303
|
| ListMultipartUploads | `GET /{bucket}?uploads` | Optional `prefix` |
|
|
1967
2304
|
| GetBucketLocation | `GET /{bucket}?location` | Returns `server.region`; an empty constraint for `us-east-1` |
|
|
2305
|
+
| GetBucketVersioning, PutBucketVersioning | `GET`, `PUT /{bucket}?versioning` | Standalone; see [Bucket versioning](#bucket-versioning) |
|
|
2306
|
+
| ListObjectVersions | `GET /{bucket}?versions` | Every version and delete marker of a versioned bucket |
|
|
1968
2307
|
| GetBucketPolicy | `GET /{bucket}?policy` | |
|
|
1969
2308
|
| PutBucketPolicy | `PUT /{bucket}?policy` | |
|
|
1970
2309
|
| DeleteBucketPolicy | `DELETE /{bucket}?policy` | |
|
|
@@ -2045,8 +2384,47 @@ prefix credentials must supply one inside their prefix.
|
|
|
2045
2384
|
# key leaf appears only with the complete payload
|
|
2046
2385
|
.smartstorage/credentials.json # Runtime and bucket tenant credentials (0600)
|
|
2047
2386
|
.smartstorage/prefix-credentials.json # Prefix credentials and revocations (0600)
|
|
2387
|
+
.smartstorage/versioning/v1/{bucket}.json
|
|
2388
|
+
# Versioning state of a versioned bucket
|
|
2389
|
+
.smartstorage/versioning/stamp-clock.json
|
|
2390
|
+
# Persisted floor of the version stamp clock
|
|
2391
|
+
.smartstorage/versions/{bucket}/{whole-key-hex-frames}/
|
|
2392
|
+
{seq:016x}-{generation} # Record of a noncurrent version or delete marker
|
|
2393
|
+
{seq:016x}-{generation}.object # Payload of a noncurrent data version
|
|
2048
2394
|
```
|
|
2049
2395
|
|
|
2396
|
+
Bucket versioning keeps the layout above for current objects and adds a
|
|
2397
|
+
format that later standalone engines read unchanged:
|
|
2398
|
+
- `.smartstorage/versioning/v1/{bucket}.json` is
|
|
2399
|
+
`{"format":"smartstorage.bucket-versioning.v1","bucket":…,"status":"Enabled"|"Suspended"}`.
|
|
2400
|
+
A bucket without it was never versioned.
|
|
2401
|
+
- The current object of a key stays at its key path. A payload written while
|
|
2402
|
+
its bucket is versioned ends in a trailer record of schema 2 whose `version`
|
|
2403
|
+
holds its generation UUID, whether it is the null version, its sequence
|
|
2404
|
+
number and its commit time in epoch milliseconds; payloads written before
|
|
2405
|
+
keep schema 1 and are the key's null version with sequence 0.
|
|
2406
|
+
- Every other version lives in the key's chain directory below
|
|
2407
|
+
`.smartstorage/versions/{bucket}/`, whose frame directories are the same
|
|
2408
|
+
hexadecimal frames as below `.smartstorage-objects-v2`. A record
|
|
2409
|
+
`{seq:016x}-{generation}` (JSON, `"format":"smartstorage.object-version.v1"`,
|
|
2410
|
+
with the key, sequence, generation, null flag, delete marker flag, commit
|
|
2411
|
+
time, the time it became noncurrent, payload length, ETag and metadata)
|
|
2412
|
+
exists for every noncurrent version and every delete marker; a data version
|
|
2413
|
+
also has `{seq:016x}-{generation}.object`, the payload inode it had while it
|
|
2414
|
+
was current. Sequence order is chain order; the version id is the generation
|
|
2415
|
+
UUID, or `null`.
|
|
2416
|
+
- A payload is linked into the chain before its record exists and unlinked
|
|
2417
|
+
after its record is gone. Startup recovery brings every chain to a
|
|
2418
|
+
consistent state after a crash (it removes payloads without a record,
|
|
2419
|
+
finishes or undoes an interrupted change of the current version, clears the
|
|
2420
|
+
noncurrent time of a delete marker that is current again and keeps one null
|
|
2421
|
+
version per key) and refuses to start on a record that is corrupt or misses
|
|
2422
|
+
its payload, on a chain directory with a foreign entry, and on versions of a
|
|
2423
|
+
bucket without versioning state.
|
|
2424
|
+
- Engines before 11.2.0 cannot read a payload with a schema-2 trailer: a
|
|
2425
|
+
standalone root on which any bucket was ever versioned (`PutBucketVersioning`
|
|
2426
|
+
with `Enabled` or `Suspended`) cannot be rolled back.
|
|
2427
|
+
|
|
2050
2428
|
Rolling a standalone root back from 10.7.0 or later to 8.0.3–10.6.0: a crash
|
|
2051
2429
|
between staging and publication of a PUT, CopyObject or CompleteMultipartUpload
|
|
2052
2430
|
can leave a staged leaf directory
|
package/third-party-notices.md
CHANGED
|
@@ -55,12 +55,28 @@ Developer ID signed or notarized.
|
|
|
55
55
|
## Locked Cargo graph
|
|
56
56
|
|
|
57
57
|
[Cargo dependency notices](./third_party_licenses/cargo-dependencies.html)
|
|
58
|
-
contain the selected license texts and original attributions for
|
|
59
|
-
The complete locked graph also contains
|
|
58
|
+
contain the selected license texts and original attributions for 279 packages.
|
|
59
|
+
The complete locked graph also contains 18 feature-inactive packages:
|
|
60
60
|
[aho-corasick 1.1.4](./third_party_licenses/aho-corasick-1.1.4-license.txt),
|
|
61
|
-
[
|
|
61
|
+
[borsh 1.8.1](./third_party_licenses/borsh-1.8.1-license.txt),
|
|
62
|
+
[borsh-derive 1.8.1](./third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt),
|
|
63
|
+
[dyn-clone 1.0.20](./third_party_licenses/dyn-clone-1.0.20-license.txt),
|
|
64
|
+
[equivalent 1.0.3](./third_party_licenses/equivalent-1.0.3-license.txt),
|
|
65
|
+
[hashbrown 0.17.1](./third_party_licenses/hashbrown-0.17.1-license.txt),
|
|
66
|
+
[indexmap 2.14.2](./third_party_licenses/indexmap-2.14.2-license.txt),
|
|
67
|
+
[proc-macro-crate 3.5.0](./third_party_licenses/proc-macro-crate-3.5.0-license.txt),
|
|
68
|
+
[ref-cast 1.0.27](./third_party_licenses/ref-cast-1.0.27-license.txt),
|
|
69
|
+
[ref-cast-impl 1.0.27](./third_party_licenses/ref-cast-impl-1.0.27-license.txt),
|
|
70
|
+
[schemars 1.2.3](./third_party_licenses/schemars-1.2.3-license.txt),
|
|
71
|
+
[syn 3.0.9](./third_party_licenses/syn-3.0.9-license.txt),
|
|
72
|
+
[toml_datetime 1.1.2](./third_party_licenses/toml_datetime-1.1.2-license.txt),
|
|
73
|
+
[toml_edit 0.25.17](./third_party_licenses/toml_edit-0.25.17-license.txt),
|
|
74
|
+
[toml_parser 1.1.5](./third_party_licenses/toml_parser-1.1.5-license.txt),
|
|
75
|
+
[valuable 0.1.1](./third_party_licenses/valuable-license.txt),
|
|
76
|
+
[winnow 1.0.4](./third_party_licenses/winnow-1.0.4-license.txt) and
|
|
62
77
|
[zerocopy-derive 0.8.47](./third_party_licenses/zerocopy-derive-0.8.47-license.txt).
|
|
63
|
-
Their exact
|
|
78
|
+
Their exact notices (Apache-2.0 for borsh-derive, which offers no MIT
|
|
79
|
+
alternative, MIT for the others) bring the inventory to all 297 locked third-party
|
|
64
80
|
packages, conservatively including build-only, development and inactive targets.
|
|
65
81
|
Listing a package does not claim that it is linked into every distributed executable.
|
|
66
82
|
|