Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9054790
[resfssgcp_nfs] Add driver implementation and keyword definitions
cgalibern Jun 29, 2026
223fde4
[resfssgcp_nfs_cg] Add driver implementation with manifest and keywords
cgalibern Jun 29, 2026
e8d876f
[resipsgcp_dnsalias] Add driver implementation with manifest and keyw…
cgalibern Jun 29, 2026
31d03ec
[resfshost] Remove unused keyword group variables to simplify manifes…
cgalibern Jun 29, 2026
62a1687
[core/object] Add `DecodeKeys` method to support batch key decoding
cgalibern Jul 4, 2026
afd82fe
[sgcp] Add AuthInfo struct and GetAuthInfo utility function
cgalibern Jul 4, 2026
9695713
[ageingcache] Add Outputter struct for managing output functions
cgalibern Jul 4, 2026
d66b722
[file] Fix type conversion for unix.Major/Minor function calls (darwin)
cgalibern Jul 4, 2026
ebd1a2d
[pg] Update `ApplyProc` to return additional success status (darwin)
cgalibern Jul 4, 2026
719dedc
[resfshost] Split `KeywordCheckRead` into `Enabled` and `Disabled` va…
cgalibern Jul 6, 2026
b272388
[sgcp] Add `Config` struct and utility functions for SGCP configurati…
cgalibern Jul 6, 2026
0dfe445
[sgcp] Add `Api` struct and HTTP utility methods with tests
cgalibern Jul 6, 2026
2faa7cd
[sgcp] Add `TokenFactory` for token management with caching and tests
cgalibern Jul 6, 2026
c500230
[sgcp] Add `FilesAPI` for managing file operations with tests
cgalibern Jul 6, 2026
3253566
[sgcp] Add `DisabledFlag` support to `Config` and `IsDisabled` utilit…
cgalibern Jul 6, 2026
89c80b3
[sgcp] Refactor `GetAuthInfo` into struct method for improved encapsu…
cgalibern Jul 6, 2026
e2337eb
[resfssgcp_nfs] Add SGCP NFS filesystem driver with client management…
cgalibern Jul 6, 2026
cf0ba25
[resfssgcp_nfs] Validate `secret` and `endpoint` fields with fallback…
cgalibern Jul 6, 2026
44dc326
[resipsgcp_dnsalias] Remove unused default values for `secret` and `e…
cgalibern Jul 6, 2026
e187732
Add `resfssgcp_nfs` driver to driver database
cgalibern Jul 6, 2026
ed65950
[resfssgcp_nfs] Conditionally register driver based on SGCP config fi…
cgalibern Jul 6, 2026
afdefed
Refactor SGCP configuration loading and testing:
cgalibern Jul 6, 2026
01b50d9
[resfssgcp_nfs] Add mandatory config file validation and enhance test…
cgalibern Jul 6, 2026
7e7b23b
[sgcp] Add singleflight for token cache fetch to prevent duplicate re…
cgalibern Jul 6, 2026
3fde4c8
[resfssgcp_nfs] Clear file status cache on start/stop operations
cgalibern Jul 6, 2026
77c98f6
[resfssgcp_nfs] Simplify driver registration and adjust capabilities …
cgalibern Jul 6, 2026
87b8fb9
[resfssgcp_nfs] Skip capability scanning if the SGCP config file is m…
cgalibern Jul 6, 2026
e8cc70e
[resipsgcp_dnsalias] Skip capability scanning if SGCP config file is …
cgalibern Jul 6, 2026
0a52896
[resfssgcp_nfs_cg] Remove unused default values for `secret` and `end…
cgalibern Jul 6, 2026
b10f81e
Add `resfssgcp_nfs_cg` and `resipsgcp_dnsalias` resource drivers with…
cgalibern Jul 6, 2026
3f4b236
[resfssgcp_nfs] Update filesystem configuration to use NFSv4
cgalibern Jul 7, 2026
f643631
[resfssgcp_nfs] Add `StartTimeout` attribute to configure start actio…
cgalibern Jul 7, 2026
e7fa7e0
[resfssgcp_nfs] Remove unused default value for `Secret` attribute in…
cgalibern Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/driverdb/drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
_ "github.com/opensvc/om3/v3/drivers/resfsdir"
_ "github.com/opensvc/om3/v3/drivers/resfsflag"
_ "github.com/opensvc/om3/v3/drivers/resfshost"
_ "github.com/opensvc/om3/v3/drivers/resfssgcp_nfs"
_ "github.com/opensvc/om3/v3/drivers/resfszfs"
_ "github.com/opensvc/om3/v3/drivers/resiphost"
_ "github.com/opensvc/om3/v3/drivers/resiproute"
Expand Down
1 change: 1 addition & 0 deletions core/object/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type (
AddKey(name string, b []byte) error
ChangeKey(name string, b []byte) error
DecodeKey(name string) ([]byte, error)
DecodeKeys(name ...string) ([][]byte, error)
EditKey(name string) error
InstallKey(name string) error
InstallKeyTo(KVInstall) error
Expand Down
13 changes: 13 additions & 0 deletions core/object/datastore_key_decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@ func (t *dataStore) decode(keyname string) ([]byte, error) {
func (t *dataStore) DecodeKey(keyname string) ([]byte, error) {
return t.decode(keyname)
}

// DecodeKeys returns the decoded bytes of the key value
func (t *dataStore) DecodeKeys(keynames ...string) ([][]byte, error) {
var l [][]byte
for _, keyname := range keynames {
if b, err := t.decode(keyname); err != nil {
return nil, fmt.Errorf("decode key %s: %w", keyname, err)
} else {
l = append(l, b)
}
}
return l, nil
}
32 changes: 10 additions & 22 deletions drivers/resfshost/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,45 +66,33 @@ var (
Scopable: true,
Text: keywords.NewText(fs, "text/kw/zone"),
}
KeywordCheckRead = keywords.Keyword{
KeywordCheckReadDisabled = keywords.Keyword{
Attr: "CheckRead",
Converter: "bool",
Option: "check_read",
Default: "false",
Scopable: true,
Text: keywords.NewText(fs, "text/kw/check_read"),
}

KeywordsVirtual = []*keywords.Keyword{
&KeywordMountPoint,
&KeywordMountOptions,
&KeywordDevice,
&KeywordStatTimeout,
&KeywordZone,
&KeywordCheckRead,
KeywordCheckReadEnabled = keywords.Keyword{
Attr: "CheckRead",
Converter: "bool",
Option: "check_read",
Scopable: true,
Default: "true",
Text: keywords.NewText(fs, "text/kw/check_read"),
}

KeywordsBase = []*keywords.Keyword{
&KeywordMountPoint,
&KeywordDevice,
&KeywordMountOptions,
&KeywordStatTimeout,
&manifest.KWSCSIPersistentReservationKey,
&manifest.KWSCSIPersistentReservationEnabled,
&manifest.KWSCSIPersistentReservationNoPreemptAbort,
&KeywordPromoteRW,
&KeywordMKFSOptions,
&KeywordZone,
&KeywordCheckRead,
}

KeywordsPooling = []*keywords.Keyword{
&KeywordMountPoint,
&KeywordDevice,
&KeywordMountOptions,
&KeywordStatTimeout,
&KeywordMKFSOptions,
&KeywordZone,
&KeywordCheckRead,
&KeywordCheckReadDisabled,
}
)

Expand Down
261 changes: 261 additions & 0 deletions drivers/resfssgcp_nfs/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package resfssgcp_nfs

import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"time"

"github.com/opensvc/om3/v3/util/ageingcache"
"github.com/opensvc/om3/v3/util/plog"
"github.com/opensvc/om3/v3/util/sgcp"
)

type (
// nfsClientMgr handles NFS client management for the resource
nfsClientMgr struct {
uuid string
host string
permission string
protocol string
nfsIgnored []string

api *sgcp.FilesAPI
cacheConfig *sgcp.CacheConfig
log *plog.Logger
}
)

// Start adds the NFS client to the filesystem
func (mgr *nfsClientMgr) Start(ctx context.Context, exclusive bool) error {
if exclusive {
return mgr.startExclusive(ctx)
}
return mgr.start(ctx)
}

func (mgr *nfsClientMgr) start(ctx context.Context) error {
// Verify if the client already exists
fileInfo, err := mgr.getFileInfo(ctx)
if err != nil {
return err
}

if fileInfo != nil {
for _, client := range fileInfo.NFSClients {
if client.Host == mgr.host {
if client.Permission == mgr.permission {
mgr.log.Infof("%s already exists", client)
return nil
}
// Wrong permission, delete and recreate
if err := mgr.deleteNFSClient(ctx, client); err != nil {
// Don't stop here, next addNFSClient will retry delete wrong permission
mgr.log.Debugf("continue (will be retried) on failed deletion of NFS client with wrong permission: %s", err)
}
break
}
}
}

// Add the client
_, err = mgr.addNFSClient(ctx, mgr.host)
return err
}

// startExclusive starts with exclusive access (removes other clients first)
func (mgr *nfsClientMgr) startExclusive(ctx context.Context) error {
// Delete all clients except our host
fileInfo, err := mgr.getFileInfo(ctx)
if err != nil {
return err
}

if fileInfo != nil {
for _, client := range fileInfo.NFSClients {
if client.Host != mgr.host && !mgr.isClientIgnored(client.Host) {
if err := mgr.deleteNFSClient(ctx, client); err != nil {
return err
}
}
}
}

// Add our client
_, err = mgr.addNFSClient(ctx, mgr.host)
return err
}

func (mgr *nfsClientMgr) cacheSig(name string) string {
return fmt.Sprintf("%s:%s", name, mgr.uuid)
}

func (mgr *nfsClientMgr) cacheClear(name string) error {
cacheSig := mgr.cacheSig(name)
return ageingcache.Clear(cacheSig)
}

func (mgr *nfsClientMgr) getFileInfo(ctx context.Context) (*FilesystemInfo, error) {
var fileInfo FilesystemInfo

cacheSig := mgr.cacheSig("getFileInfo")
ttl := time.Duration(mgr.cacheConfig.TTLSeconds) * time.Second
o := ageingcache.NewOutputter(mgr.getFileInfoFactory(ctx))
data, err := ageingcache.Output(o, cacheSig, ttl)
if err != nil {
mgr.log.Debugf("getFileInfo failed on missing cache sig %s.out, ttl: %s: %s", cacheSig, ttl, err)
return nil, fmt.Errorf("getFileInfo failed: %w", err)
}

if err := json.Unmarshal(data, &fileInfo); err != nil {
return nil, err
}

return &fileInfo, err
}

func (mgr *nfsClientMgr) getFileInfoFactory(ctx context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
method, url, statusCode, data, err := mgr.api.GetFilesystem(ctx, mgr.uuid)
if err != nil {
return nil, err
}

if err := mgr.api.CheckStatusCode(method, url, statusCode, http.StatusNotFound, http.StatusOK); err != nil {
return nil, err
}
switch statusCode {
case http.StatusOK:
case http.StatusNotFound:
return nil, nil
default:
// paranoid, should never happen
return nil, fmt.Errorf("%s %s got unexpected status code %d", method, url, statusCode)
}
return data, nil
}
}

// Stop removes the NFS client for this host
func (mgr *nfsClientMgr) Stop(ctx context.Context) error {
fileInfo, err := mgr.getFileInfo(ctx)
if err != nil {
return err
}

if fileInfo == nil {
return nil
}

for _, client := range fileInfo.NFSClients {
if client.Host == mgr.host {
return mgr.deleteNFSClient(ctx, client)
}
}

return nil
}

// addNFSClient adds a new NFS client to the filesystem
func (mgr *nfsClientMgr) addNFSClient(ctx context.Context, host string) (*NfsClient, error) {
newClient := &NfsClient{
Host: host,
Permission: mgr.permission,
Protocol: mgr.protocol,
}

mgr.log.Debugf("Verify existence of %s", newClient)

fileInfo, err := mgr.getFileInfo(ctx)
if err != nil {
return nil, err
}

if fileInfo != nil {
for _, client := range fileInfo.NFSClients {
if client.Host == host {
if client.Permission == mgr.permission {
mgr.log.Infof("%s already exists", client)
return &client, nil
}
mgr.log.Infof("%s already exists, with wrong permission", client)
if err := mgr.deleteNFSClient(ctx, client); err != nil {
mgr.log.Warnf("delete existing %s with wrong permissions: %s", client, err)
}
break
}
}
}
payload := &sgcp.NfsClient{
Host: host,
Permission: mgr.permission,
Protocol: mgr.protocol,
}
mgr.log.Infof("grant permission %s for host %s on filesystem %s ...", mgr.permission, host, mgr.uuid)
method, url, statusCode, data, err := mgr.api.PostNFSClients(ctx, mgr.uuid, payload)

if err != nil {
return nil, err
}
if err := mgr.api.CheckStatusCode(method, url, statusCode, http.StatusCreated); err != nil {
return nil, err
}
if statusCode != http.StatusCreated {
return nil, fmt.Errorf("unexpected status code %s %s: %d", method, url, statusCode)
}
var createdClient NfsClient
if err := json.Unmarshal(data, &createdClient); err != nil {
return nil, err
}

mgr.log.Infof("created %s on filesystem %s", createdClient, mgr.uuid)
return &createdClient, nil
}

// deleteNFSClient deletes an NFS client from the filesystem
func (mgr *nfsClientMgr) deleteNFSClient(ctx context.Context, client NfsClient) error {
cgMsg := ""
if client.ConsistencyGroupID != "" {
cgMsg = fmt.Sprintf(" in consistency group %s", client.ConsistencyGroupID)
}

mgr.log.Infof("drop permission %s for host %s on filesystem %s%s ...", client.Permission, client.Host, mgr.uuid, cgMsg)
method, url, statusCode, _, err := mgr.api.DeleteNFSClients(ctx, mgr.uuid, client.UUID)
if err != nil {
return err
}
if err := mgr.api.CheckStatusCode(method, url, statusCode, http.StatusNoContent, http.StatusPreconditionFailed); err != nil {
return err
}
switch statusCode {
case http.StatusNoContent:
case http.StatusPreconditionFailed:
return fmt.Errorf("consistency group is not in ready (status_code %d)", statusCode)
default:
// paranoid, should never happen
return fmt.Errorf("unexpected status code %d", statusCode)
}

mgr.log.Infof("deleted %s on filesystem %s", client, mgr.uuid)
return nil
}

func (mgr *nfsClientMgr) checkStatusCode(method, url string, got int, wanted ...int) error {
mgr.log.Debugf("%s %s status code: %d", method, url, got)
if slices.Contains(wanted, got) {
return nil
}
return fmt.Errorf("unexpected status code for %s %s got %d wanted %v", method, url, got, wanted)
}

// isClientIgnored checks if a client host should be ignored
func (mgr *nfsClientMgr) isClientIgnored(host string) bool {
for _, ignored := range mgr.nfsIgnored {
if host == ignored {
return true
}
}
return false
}
20 changes: 20 additions & 0 deletions drivers/resfssgcp_nfs/caps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package resfssgcp_nfs

import (
"context"

"github.com/opensvc/om3/v3/util/capabilities"
"github.com/opensvc/om3/v3/util/file"
"github.com/opensvc/om3/v3/util/sgcp"
)

func init() {
capabilities.Register(capabilitiesScanner)
}

func capabilitiesScanner(ctx context.Context) ([]string, error) {
if !file.Exists(sgcp.DefaultConfigPath) {
return nil, nil
}
return []string{drvID.Cap()}, nil
}
Loading
Loading