Skip to content

Commit

Permalink
OCPBUGS-44849: Provide fallback Xattr method where not supported in k…
Browse files Browse the repository at this point in the history
…ernel.

Some customers are expereincing breaking issues when their OS does not support xattr, most notably NFS + Xattr
This causes installations not to proceed and needs to be addressed urgently.

This change provides a means of 'falling back' to a filesystem based implementation in the case that xattr is unavailable in the kernel.

This is achieved by the creation of an XattrClient interface and three implementations of this client

OSxattrClient - which wraps the kernel native xattr functions and is responsible for determining if xattr is available or not.
FilesystemBasedXattrClient - Which uses a directory parallel to the root to maintain metadata keys as individual files - one file per key

CompositeXattrClient - Which encapsulates the other two clients and dependent on the availability of xattr in the kernel will call the right client as applicable.

If we need to disable the fallback for any reason then the correct way to do this is to create a custom config map for assisted service.
```
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-config-map
  namespace: multicluster-engine
data:
  ENABLE_XATTR_FALLBACK: "false"
```

This should then be added as an annotation to the AgentServiceConfig
```
kind: AgentServiceConfig
metadata:
  annotations:
    unsupported.agent-install.openshift.io/assisted-service-configmap: custom-config-map

```

Once you have updated the AgentServiceConfig, redeploy the service
```
oc rollout restart deployment -n multicluster-engine assisted-service
```

After the service redeploys, you should see an entry in the assisted service log to indicate that the setting has been picked up

```
oc logs -f assisted-service-66dfcbf689-95v5f -c asisted-service | grep -i FALLBACK
time="2024-12-03T09:46:36Z" level=info msg="Options.EnableXattrFallback:false" func=main.main file="/assisted-service/cmd/main.go:332"
```
  • Loading branch information
paul-maidment committed Dec 4, 2024
1 parent 973249d commit ebb71ba
Show file tree
Hide file tree
Showing 10 changed files with 1,048 additions and 282 deletions.
48 changes: 44 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
_ "net/http/pprof"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -168,6 +169,9 @@ var Options struct {

// EnableSoftTimeouts is a boolean flag to enable Soft timeouts by assisted installer
EnableSoftTimeouts bool `envconfig:"ENABLE_SOFT_TIMEOUTS" default:"false"`

// EnableXattrFallback is a boolean flag to enable en emulated fallback methoid of xattr on systems that do not support xattr.
EnableXattrFallback bool `envconfig:"ENABLE_XATTR_FALLBACK" default:"true"`
}

func InitLogs(logLevel, logFormat string) *logrus.Logger {
Expand Down Expand Up @@ -203,6 +207,37 @@ func maxDuration(dur time.Duration, durations ...time.Duration) time.Duration {
return ret
}

func setUpXattrClient(
log *logrus.Logger,
rootDir string,
) s3wrapper.XattrClient {
log.Infof("Options.EnableXattrFallback:%t", Options.EnableXattrFallback)
var xattrClient s3wrapper.XattrClient
var err error
xattrClient = s3wrapper.NewOSxAttrClient(log, rootDir)
if Options.Storage != storage_filesystem {
return xattrClient
}
if Options.EnableXattrFallback {
xattrClient, err = s3wrapper.NewCompositeXattrClient(
log,
s3wrapper.NewOSxAttrClient(log, rootDir),
s3wrapper.NewFilesystemBasedXattrClient(log, rootDir),
)
if err != nil {
log.WithError(err).Fatalf("failed to initialize xattr handling")
}
}
xattrClientSupported, err := xattrClient.IsSupported()
if err != nil {
log.WithError(err).Fatalf("failed to initialize xattr handling")
}
if !xattrClientSupported {
log.Fatalf("Fallback has been disabled and the file system at '%s' doesn't support extended attributes. This happens when using a RHEL NFS server older than 8.4, a NetApp ONTAP older than 9.12.1, or some other file system that doesn't support extended attributes.", rootDir)
}
return xattrClient
}

func main() {
err := envconfig.Process(common.EnvConfigPrefix, &Options)
if err == nil {
Expand Down Expand Up @@ -325,8 +360,13 @@ func main() {
failOnError(err, "failed to create ignition builder")
installConfigBuilder := installcfg.NewInstallConfigBuilder(log.WithField("pkg", "installcfg"), mirrorRegistriesBuilder, providerRegistry)

var xattrClient s3wrapper.XattrClient = setUpXattrClient(
log,
filepath.Join(Options.WorkDir, Options.S3Config.S3Bucket),
)

var objectHandler = createStorageClient(Options.DeployTarget, Options.Storage, &Options.S3Config,
Options.WorkDir, log, metricsManager, Options.FileSystemUsageThreshold)
Options.WorkDir, log, metricsManager, Options.FileSystemUsageThreshold, xattrClient)
createS3Bucket(objectHandler, log)

manifestsApi := manifests.NewManifestsAPI(db, log.WithField("pkg", "manifests"), objectHandler, usageManager)
Expand Down Expand Up @@ -738,7 +778,7 @@ func createS3Bucket(objectHandler s3wrapper.API, log logrus.FieldLogger) {
}

func createStorageClient(deployTarget string, storage string, s3cfg *s3wrapper.Config, fsWorkDir string,
log logrus.FieldLogger, metricsAPI metrics.API, fsThreshold int) s3wrapper.API {
log logrus.FieldLogger, metricsAPI metrics.API, fsThreshold int, xattrClient s3wrapper.XattrClient) s3wrapper.API {
var storageClient s3wrapper.API = nil
if storage != "" {
switch storage {
Expand All @@ -747,7 +787,7 @@ func createStorageClient(deployTarget string, storage string, s3cfg *s3wrapper.C
log.Fatal("failed to create S3 client")
}
case storage_filesystem:
storageClient = s3wrapper.NewFSClient(fsWorkDir, log, metricsAPI, fsThreshold)
storageClient = s3wrapper.NewFSClient(fsWorkDir, log, metricsAPI, fsThreshold, xattrClient)
default:
log.Fatalf("unsupported storage client: %s", storage)
}
Expand All @@ -759,7 +799,7 @@ func createStorageClient(deployTarget string, storage string, s3cfg *s3wrapper.C
log.Fatal("failed to create S3 client")
}
case deployment_type_onprem, deployment_type_ocp:
storageClient = s3wrapper.NewFSClient(fsWorkDir, log, metricsAPI, fsThreshold)
storageClient = s3wrapper.NewFSClient(fsWorkDir, log, metricsAPI, fsThreshold, xattrClient)
default:
log.Fatalf("unsupported deploy target %s", deployTarget)
}
Expand Down
99 changes: 99 additions & 0 deletions pkg/s3wrapper/composite_xattr_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package s3wrapper

import (
"slices"

"github.com/sirupsen/logrus"
)

func NewCompositeXattrClient(
log logrus.FieldLogger,
oSxattrClient XattrClient,
filesystemBasedXattrClient XattrClient,
) (*CompositeXattrClient, error) {
oSxattrClientSupported, err := oSxattrClient.IsSupported()
if err != nil {
return nil, err
}
if !oSxattrClientSupported {
log.Warn("The native xattr client doesn't support extended attributes. A fallback has been enabled.")
}
filesystemBasedXattrClientSupported, err := filesystemBasedXattrClient.IsSupported()
if err != nil {
return nil, err
}
return &CompositeXattrClient{
oSxattrClient: oSxattrClient,
filesystemBasedXattrClient: filesystemBasedXattrClient,
oSxattrClientSupported: oSxattrClientSupported,
filesystemBasedXattrClientSupported: filesystemBasedXattrClientSupported,
}, nil
}

type CompositeXattrClient struct {
oSxattrClient XattrClient
filesystemBasedXattrClient XattrClient
oSxattrClientSupported bool
filesystemBasedXattrClientSupported bool
}

func (c *CompositeXattrClient) IsSupported() (bool, error) {
return true, nil
}

func (c *CompositeXattrClient) Set(tempFileName string, fileName string, key string, value string) error {
// If the native xattr writes are supported then use those
// otherwise fall back to filesystem based writes.
if c.oSxattrClientSupported {
return c.oSxattrClient.Set(tempFileName, fileName, key, value)
}
return c.filesystemBasedXattrClient.Set(tempFileName, fileName, key, value)
}

func (c *CompositeXattrClient) Get(path, key string) (string, bool, error) {
// Search for the record first in the oSxattrClient
// if not found then look in the filesystemBasedXattrClient.
var err error
var ok bool
var result string
if c.oSxattrClientSupported {
result, ok, err = c.oSxattrClient.Get(path, key)
if err != nil {
return "", ok, err
}
if ok {
return result, ok, nil
}
}
return c.filesystemBasedXattrClient.Get(path, key)
}

func (c *CompositeXattrClient) List(path string) ([]string, error) {
// Produce a list that is the union of keys from both clients
// Just in case the user has performed an upgrade from the filesystem based method.
// respect that the oSxattrClient takes priority when available.
var primaryList []string
var secondaryList []string
var err error
if c.oSxattrClientSupported {
primaryList, err = c.oSxattrClient.List(path)
if err != nil {
return nil, err
}
}
secondaryList, err = c.filesystemBasedXattrClient.List(path)
if err != nil {
return nil, err
}
for _, secondaryListItem := range secondaryList {
if !slices.Contains(primaryList, secondaryListItem) {
primaryList = append(primaryList, secondaryListItem)
}
}
return primaryList, nil
}

func (c *CompositeXattrClient) RemoveAll(path string) error {
// Metadata only needs to be removed in this way for the fallback solution.
return c.filesystemBasedXattrClient.RemoveAll(path)
}
139 changes: 139 additions & 0 deletions pkg/s3wrapper/composite_xattr_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package s3wrapper

import (
"fmt"
"os"
"path/filepath"

"github.com/golang/mock/gomock"
"github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
)

var _ = Describe("Composite Xattr Client", func() {

var (
log *logrus.Logger
ctrl *gomock.Controller
baseDir string
osXattrClient *MockXattrClient
fileSystemBasedXattrClient XattrClient
file1path string
file2path string
)

BeforeEach(func() {
var err error
log = logrus.New()
log.SetOutput(ginkgo.GinkgoWriter)
baseDir, err = os.MkdirTemp("", "test")
Expect(err).ToNot(HaveOccurred())
ctrl = gomock.NewController(GinkgoT())
fileSystemBasedXattrClient = NewFilesystemBasedXattrClient(log, baseDir)
err = os.MkdirAll(filepath.Join(baseDir, "manifests", "openshift"), 0o700)
Expect(err).ToNot(HaveOccurred())
err = os.MkdirAll(filepath.Join(baseDir, "manifests", "manifests"), 0o700)
Expect(err).ToNot(HaveOccurred())
file1path = filepath.Join(baseDir, "manifests", "openshift", "file1.yaml")
file2path = filepath.Join(baseDir, "manifests", "manifests", "file2.yaml")
err = os.WriteFile(file1path, []byte{}, 0o600)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(file2path, []byte{}, 0o600)
Expect(err).ToNot(HaveOccurred())
})

getExpectedMetadataPath := func(filePath string, attributeName string) string {
relativePath := filePath[len(baseDir):]
return fmt.Sprintf("%s%s%s%s", filepath.Join(baseDir, filesystemXattrMetaDataDirectoryName, relativePath), delimiter, "user.", attributeName)
}

assertFileMetadataCorrect := func(filepath string, attributeName string, expectedValue string) {
fileMetadataItemPath := getExpectedMetadataPath(filepath, attributeName)
data, err := os.ReadFile(fileMetadataItemPath)
Expect(err).ToNot(HaveOccurred())
Expect(string(data)).To(Equal(expectedValue))
}

assertFileSystemBasedAttributeWrite := func(path string, attribute string, value string, compositeXattrClient XattrClient) {
err := compositeXattrClient.Set("", path, attribute, value)
Expect(err).ToNot(HaveOccurred())
assertFileMetadataCorrect(path, attribute, value)
}

It("Upgrading to xattr supported natively after unsupported", func() {
var (
compositeXattrClient XattrClient
)

By("Native xattr is unsupported", func() {
osXattrClient = NewMockXattrClient(ctrl)
osXattrClient.EXPECT().IsSupported().Return(false, nil).AnyTimes()
})
By("Create composite xattr client", func() {
var err error
compositeXattrClient, err = NewCompositeXattrClient(log, osXattrClient, fileSystemBasedXattrClient)
Expect(err).ToNot(HaveOccurred())
})
By("Filesystem xattr client should be used for writes", func() {
assertFileSystemBasedAttributeWrite(file1path, "arbitrary-attribute", "some-arbitrary-value", compositeXattrClient)
assertFileSystemBasedAttributeWrite(file1path, "another-arbitrary-attribute", "another-arbitrary-value", compositeXattrClient)
assertFileSystemBasedAttributeWrite(file2path, "arbitrary-attribute-a", "some-arbitrary-value-a", compositeXattrClient)
assertFileSystemBasedAttributeWrite(file2path, "arbitrary-attribute-b", "some-arbitrary-value-b", compositeXattrClient)
})
By("Native xattr is now supported - simulated upgrade", func() {
var err error
compositeXattrClient, err = NewCompositeXattrClient(log, NewOSxAttrClient(log, baseDir), fileSystemBasedXattrClient)
Expect(err).ToNot(HaveOccurred())
})
By("Composite client should return previously stored keys from filesystem", func() {
value, ok, err := compositeXattrClient.Get(file1path, "arbitrary-attribute")
Expect(err).ToNot(HaveOccurred())
Expect(ok).To(BeTrue())
Expect(value).To(Equal("some-arbitrary-value"))
})
By("Composite client should write to os native xattr, leaving filesystem based xattr intact", func() {
err := compositeXattrClient.Set(file1path, file1path, "arbitrary-attribute", "a-new-value")
Expect(err).ToNot(HaveOccurred())
assertFileMetadataCorrect(file1path, "arbitrary-attribute", "some-arbitrary-value")
})
By("should fetch newly written value from the composite client", func() {
value, ok, err := compositeXattrClient.Get(file1path, "arbitrary-attribute")
Expect(err).ToNot(HaveOccurred())
Expect(ok).To(BeTrue())
Expect(value).To(Equal("a-new-value"))
})
By("list from composite xattr client should merge keys", func() {
items, err := compositeXattrClient.List(file1path)
Expect(err).ToNot(HaveOccurred())
Expect(items).To(ContainElement("arbitrary-attribute"))
Expect(items).To(ContainElement("another-arbitrary-attribute"))
})
By("RemoveAll should remove filesystem based user keys", func() {
err := compositeXattrClient.Set(file2path, file2path, "additional-os-xattr", "some-value")
Expect(err).ToNot(HaveOccurred())
items, err := compositeXattrClient.List(file2path)
Expect(err).ToNot(HaveOccurred())
Expect(items).To(ContainElement("arbitrary-attribute-a"))
Expect(items).To(ContainElement("arbitrary-attribute-a"))
Expect(items).To(ContainElement("additional-os-xattr"))
err = compositeXattrClient.RemoveAll(file2path)
Expect(err).ToNot(HaveOccurred())
items, err = compositeXattrClient.List(file2path)
Expect(err).ToNot(HaveOccurred())
Expect(items).ToNot(ContainElement("arbitrary-attribute-a"))
Expect(items).ToNot(ContainElement("arbitrary-attribute-a"))
// Placed here by the os native xattr for which we do not delete the keys in this way
// (xattr data is part of the file itself)
Expect(items).To(ContainElement("additional-os-xattr"))
})
})

AfterEach(func() {
ctrl.Finish()
err := os.RemoveAll(baseDir)
Expect(err).ToNot(HaveOccurred())
})

})
Loading

0 comments on commit ebb71ba

Please sign in to comment.