mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 08:20:42 +01:00
* admin: support setting volume read-only and read/write modes
* admin: address PR review on volume access-mode persistence
Reject trailing JSON values in the SetVolumeReadOnly handler so
requests like {"read_only":true}{} no longer pass validation, and add
a trailing-value case to the invalid-request test.
Propagate .vif persistence failures through the access-mode chain.
PersistReadOnly now returns the SaveVolumeInfo error and rolls back
the in-memory volumeInfo on failure; Store.MarkVolumeReadonly and
Store.MarkVolumeWritable propagate that error and roll back their
noWrite flags, so the API reports failure instead of success while
restart would revert the mode.
* admin: make .vif persistence atomic and preserve error chain
SaveVolumeInfo now writes to a .vif.tmp file, syncs it, renames it
over the target, and fsyncs the directory. A write/sync/close failure
leaves the existing .vif intact, so the PersistReadOnly in-memory
rollback matches the durable state instead of diverging from a
partially written file that restart would apply.
Switch the error wrappers in PersistReadOnly, MarkVolumeReadonly, and
MarkVolumeWritable from %v to %w so callers can use errors.Is and
errors.As to classify persistence failures.
* admin: treat post-rename dir fsync failure as a warning
After os.Rename commits the new .vif, the on-disk file already holds
the requested mode. A directory fsync failure only risks losing the
rename across a crash; returning an error here would make
PersistReadOnly roll back in-memory state while the durable file keeps
the new mode, splitting the replica. Log the failure as a warning
instead, matching the best-effort nature of FsyncDir (already skipped
on Windows).
* admin: distinguish post-rename durability failures and use unique temp files
SaveVolumeInfo now uses os.CreateTemp for the staging file, preventing
concurrent saves for the same volume from colliding on a shared .tmp
path.
A directory fsync failure after os.Rename returns a
NotCrashDurableError instead of being silently swallowed. The rename
already committed the new metadata to disk, so PersistReadOnly,
MarkVolumeReadonly, and MarkVolumeWritable skip the in-memory rollback
for this error type (keeping state aligned with the durable file) while
still propagating the failure to the API. Pre-commit failures continue
to roll back as before.
* admin: continue post-commit work after NotCrashDurableError
MarkVolumeWritable now clears the EIO quarantine and the gRPC handlers
(makeVolumeReadonly step 3, makeVolumeWritable master notification)
proceed with their post-commit work when SaveVolumeInfo returns a
NotCrashDurableError, instead of aborting and leaving the volume
unavailable or the master unaware of the mode change. The durability
warning is still propagated to the API caller. Pre-commit failures
continue to abort early as before.
* admin: handle NotCrashDurableError in tier and EC callers
VolumeTierMoveDatFromRemote and VolumeEcShardsGenerate now check for
NotCrashDurableError from SaveVolumeInfo. When the rename has already
committed the new .vif, they continue with their post-commit work
(backend switch, remote deletion, keeping generated EC shards) instead
of aborting and leaving the on-disk metadata inconsistent with the
file layout. The durability warning is logged for the operator.
63 lines
2.2 KiB
Go
63 lines
2.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSetVolumeReadOnlyInvalidRequests(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, id, server, body string
|
|
}{
|
|
{"missing volume", "", "node-a", `{"read_only":true}`},
|
|
{"negative volume", "-1", "node-a", `{"read_only":true}`},
|
|
{"overflow volume", "4294967296", "node-a", `{"read_only":true}`},
|
|
{"invalid volume", "abc", "node-a", `{"read_only":true}`},
|
|
{"missing server", "7", "", `{"read_only":true}`},
|
|
{"missing mode", "7", "node-a", `{}`},
|
|
{"null mode", "7", "node-a", `{"read_only":null}`},
|
|
{"string mode", "7", "node-a", `{"read_only":"false"}`},
|
|
{"malformed JSON", "7", "node-a", `{"read_only":`},
|
|
{"trailing JSON", "7", "node-a", `{"read_only":true}{}`},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tc.body))
|
|
req = mux.SetURLVars(req, map[string]string{"id": tc.id, "server": tc.server})
|
|
w := httptest.NewRecorder()
|
|
// Invalid input must be rejected before accessing the cluster.
|
|
(&ClusterHandlers{}).SetVolumeReadOnly(w, req)
|
|
require.Equal(t, http.StatusBadRequest, w.Code, w.Body.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetVolumeReadOnlyRoutePermissions(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, role string
|
|
enforce bool
|
|
wantStatus int
|
|
}{
|
|
{"read-only user", dash.RoleReadOnly, true, http.StatusForbidden},
|
|
{"admin", "admin", true, http.StatusBadRequest},
|
|
{"no authentication", "", false, http.StatusBadRequest},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
router := mux.NewRouter()
|
|
newRouteTestAdminHandlers().registerAPIRoutes(router.PathPrefix("/api").Subrouter(), tc.enforce)
|
|
// Deliberately omit the mode: allowed callers reach validation, while
|
|
// read-only callers must be rejected by the route's write guard.
|
|
req := httptest.NewRequest(http.MethodPost, "/api/volumes/7/node-a/read-only", strings.NewReader(`{}`))
|
|
req = req.WithContext(dash.WithAuthContext(req.Context(), "", tc.role, ""))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, tc.wantStatus, w.Code, w.Body.String())
|
|
})
|
|
}
|
|
}
|