Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the option for a sentinel file for the origin #1003

Merged
merged 5 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 19 additions & 4 deletions docs/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -416,12 +416,27 @@ name: Origin.Exports
description: >-
A list describing the origin's exports. Each item in the list describes a single namespace the origin exports:

- StoragePrefix: <the relevant path from the object store, eg for posix /my/dir>
FederationPrefix: <the namespace prefix that data from StoragePrefix is made available under within the federation>
- StoragePrefix: The relevant path from the object store, e.g. for posix /my/dir
FederationPrefix: The namespace prefix that data from StoragePrefix is made available under within the federation
Capabilities: A list of the capabilities the origin is willing to support for the given export. Capabilities include:
["Reads", "PublicReads", "Writes", "Listings", "DirectReads"]
where each of these has the same effect as the corresponding "Origin.Enable*" configuration, except scoped to the
given export. If "PublicReads" is included, "Reads" is inferred.
where each of these has the same effect as the corresponding "Origin.Enable*" configuration, except scoped to the
given export. If "PublicReads" is included, "Reads" is inferred.
SentinelLocation: A filename under `StoragePrefix` path for Pelican to check the storage directory exists and is correctly mounted.
Leave it empty to skip the check. You should always choose a distinguished name for `SentinelLocation`. It should not be reused for other servers.
If running in a containerized environment it should not be the name of the underlying physical host as that may change and lead to confusion.
You need to manually create a file under path to `StoragePrefix` with the same name as `SentinelLocation`. Note that this parameter will be ignored
if the origin StorageType is S3.

Example:

```yaml
Origin.Exports
- StoragePrefix: /home/foo/bar
FederationPrefix: /demo/project
Capabilities: ["Reads", "PublicReads", "Writes", "Listings", "DirectReads"]
SentinelLocation: demoproject_origin_A
```
type: object
default: none
components: ["origin"]
Expand Down
2 changes: 1 addition & 1 deletion fed_test_utils/fed.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import (

type (
FedTest struct {
Exports *[]server_utils.OriginExports
Exports *[]server_utils.OriginExport
Token string
Ctx context.Context
Egrp *errgroup.Group
Expand Down
5 changes: 5 additions & 0 deletions launchers/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ func LaunchModules(ctx context.Context, modules config.ServerType) (context.Canc
return shutdownCancel, err
}

ok, err := server_utils.CheckSentinelLocation(originExports)
if err != nil && !ok {
return shutdownCancel, err
}

switch mode {
case "posix":
if len(*originExports) == 0 {
Expand Down
36 changes: 28 additions & 8 deletions server_utils/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ package server_utils

import (
"fmt"
"os"
"path"
"path/filepath"
"reflect"
"strings"
Expand All @@ -33,13 +35,14 @@ import (
"github.com/pelicanplatform/pelican/server_structs"
)

var originExports *[]OriginExports
var originExports *[]OriginExport

type (
OriginExports struct {
OriginExport struct {
StoragePrefix string
FederationPrefix string
Capabilities server_structs.Capabilities
SentinelLocation string
}
)

Expand Down Expand Up @@ -115,12 +118,12 @@ func StringListToCapsHookFunc() mapstructure.DecodeHookFuncType {
// convert those values (such as Origin.FederationPrefix, Origin.StoragePrefix, etc.) into the OriginExports
// struct and return a list of one. Otherwise, we'll base things off the list of exports and ignore the single-prefix
// style of configuration.
func GetOriginExports() (*[]OriginExports, error) {
func GetOriginExports() (*[]OriginExport, error) {
if originExports != nil {
return originExports, nil
}

originExports = &[]OriginExports{}
originExports = &[]OriginExport{}

viper.SetDefault("Origin.StorageType", "posix")
StorageType := param.Origin_StorageType.GetString()
Expand Down Expand Up @@ -152,7 +155,7 @@ func GetOriginExports() (*[]OriginExports, error) {
}

reads := param.Origin_EnableReads.GetBool() || param.Origin_EnablePublicReads.GetBool()
originExport := OriginExports{
originExport := OriginExport{
FederationPrefix: federationPrefix,
StoragePrefix: storagePrefix,
Capabilities: server_structs.Capabilities{
Expand All @@ -175,7 +178,7 @@ func GetOriginExports() (*[]OriginExports, error) {

log.Warningln("Passing export volumes via -v at the command line causes Pelican to ignore exports configured via the yaml file")
log.Warningln("However, namespaces exported this way will inherit the Origin.Enable* settings from your configuration")
log.Warningln("For finer-grained control of each export, please configure them in your pelican.yaml file")
log.Warningln("For finer-grained control of each export, please configure them in your pelican.yaml file via Origin.Exports")
return originExports, nil
}

Expand Down Expand Up @@ -204,7 +207,7 @@ func GetOriginExports() (*[]OriginExports, error) {
log.Infoln("Configuring single-export origin")

reads := (param.Origin_EnableReads.GetBool() || param.Origin_EnablePublicReads.GetBool())
originExport := OriginExports{
originExport := OriginExport{
FederationPrefix: param.Origin_FederationPrefix.GetString(),
StoragePrefix: param.Origin_StoragePrefix.GetString(),
Capabilities: server_structs.Capabilities{
Expand All @@ -227,7 +230,7 @@ func GetOriginExports() (*[]OriginExports, error) {

federationPrefix := filepath.Join("/", param.Origin_S3ServiceName.GetString(),
param.Origin_S3Region.GetString(), param.Origin_S3Bucket.GetString())
originExport := OriginExports{
originExport := OriginExport{
FederationPrefix: federationPrefix,
StoragePrefix: "",
Capabilities: server_structs.Capabilities{
Expand All @@ -246,6 +249,23 @@ func GetOriginExports() (*[]OriginExports, error) {
return originExports, nil
}

func CheckSentinelLocation(exports *[]OriginExport) (ok bool, err error) {
for _, export := range *exports {
if export.SentinelLocation != "" {
sentinelPath := path.Clean(export.SentinelLocation)
if path.Base(sentinelPath) != sentinelPath {
return false, errors.Errorf("invalid SentinelLocation path for StoragePrefix %s, file must not contain a directory. Got %s", export.StoragePrefix, export.SentinelLocation)
}
fullPath := filepath.Join(export.StoragePrefix, sentinelPath)
_, err := os.Stat(fullPath)
if err != nil {
return false, errors.Wrapf(err, "fail to open SentinelLocation %s for StoragePrefix %s. Directory check failed", export.SentinelLocation, export.StoragePrefix)
}
}
}
return true, nil
}

func ResetOriginExports() {
originExports = nil
}
74 changes: 67 additions & 7 deletions server_utils/origin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ package server_utils

import (
_ "embed"
"os"
"path/filepath"
"strings"
"testing"

Expand All @@ -48,7 +50,7 @@ var (
exportSingleVolumeConfig string
)

func setup(t *testing.T, config string) *[]OriginExports {
func setup(t *testing.T, config string) *[]OriginExport {
viper.SetConfigType("yaml")
// Use viper to read in the embedded config
err := viper.ReadConfig(strings.NewReader(config))
Expand Down Expand Up @@ -93,7 +95,7 @@ func TestGetExports(t *testing.T) {
exports := setup(t, multiExportValidConfig)
assert.Len(t, *exports, 2, "expected 2 exports")

expectedExport1 := OriginExports{
expectedExport1 := OriginExport{
StoragePrefix: "/test1",
FederationPrefix: "/first/namespace",
Capabilities: server_structs.Capabilities{
Expand All @@ -106,7 +108,7 @@ func TestGetExports(t *testing.T) {
}
assert.Equal(t, expectedExport1, (*exports)[0])

expectedExport2 := OriginExports{
expectedExport2 := OriginExport{
StoragePrefix: "/test2",
FederationPrefix: "/second/namespace",
Capabilities: server_structs.Capabilities{
Expand All @@ -126,7 +128,7 @@ func TestGetExports(t *testing.T) {
exports := setup(t, exportVolumesValidConfig)
assert.Len(t, *exports, 2, "expected 2 exports")

expectedExport1 := OriginExports{
expectedExport1 := OriginExport{
StoragePrefix: "/test1",
FederationPrefix: "/first/namespace",
Capabilities: server_structs.Capabilities{
Expand All @@ -139,7 +141,7 @@ func TestGetExports(t *testing.T) {
}
assert.Equal(t, expectedExport1, (*exports)[0])

expectedExport2 := OriginExports{
expectedExport2 := OriginExport{
StoragePrefix: "/test2",
FederationPrefix: "/second/namespace",
Capabilities: server_structs.Capabilities{
Expand All @@ -161,7 +163,7 @@ func TestGetExports(t *testing.T) {
exports := setup(t, exportSingleVolumeConfig)
assert.Len(t, *exports, 1, "expected 1 export")

expectedExport := OriginExports{
expectedExport := OriginExport{
StoragePrefix: "/test1",
FederationPrefix: "/first/namespace",
Capabilities: server_structs.Capabilities{
Expand Down Expand Up @@ -190,7 +192,7 @@ func TestGetExports(t *testing.T) {
exports := setup(t, singleExportBlockConfig)
assert.Len(t, *exports, 1, "expected 1 export")

expectedExport := OriginExports{
expectedExport := OriginExport{
StoragePrefix: "/test1",
FederationPrefix: "/first/namespace",
Capabilities: server_structs.Capabilities{
Expand All @@ -213,3 +215,61 @@ func TestGetExports(t *testing.T) {
assert.True(t, viper.GetBool("Origin.EnableDirectReads"))
})
}

func TestCheckSentinelLocation(t *testing.T) {
tmpDir := t.TempDir()
tempStn := filepath.Join(tmpDir, "mock_sentinel")
file, err := os.Create(tempStn)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)

mockExportNoStn := OriginExport{
StoragePrefix: "/foo/bar",
FederationPrefix: "/demo/foo/bar",
Capabilities: server_structs.Capabilities{Reads: true},
}
mockExportValidStn := OriginExport{
StoragePrefix: tmpDir,
FederationPrefix: "/demo/foo/bar",
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "mock_sentinel",
}
mockExportInvalidStn := OriginExport{
StoragePrefix: tmpDir,
FederationPrefix: "/demo/foo/bar",
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "sentinel_dne",
}

t.Run("empty-sentinel-return-ok", func(t *testing.T) {
exports := make([]OriginExport, 0)
exports = append(exports, mockExportNoStn)
exports = append(exports, mockExportNoStn)

ok, err := CheckSentinelLocation(&exports)
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("valid-sentinel-return-ok", func(t *testing.T) {
exports := make([]OriginExport, 0)
exports = append(exports, mockExportNoStn)
exports = append(exports, mockExportValidStn)

ok, err := CheckSentinelLocation(&exports)
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("invalid-sentinel-return-error", func(t *testing.T) {
exports := make([]OriginExport, 0)
exports = append(exports, mockExportNoStn)
exports = append(exports, mockExportValidStn)
exports = append(exports, mockExportInvalidStn)

ok, err := CheckSentinelLocation(&exports)
assert.Error(t, err)
assert.False(t, ok)
})
}
7 changes: 6 additions & 1 deletion web_ui/frontend/components/Config/ObjectField/ExportForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const ExportForm = ({ onSubmit, value }: FormProps<Export>) => {
const [storagePrefix, setStoragePrefix] = React.useState<string>(value?.storageprefix || "")
const [federationPrefix, setFederationPrefix] = React.useState<string>(value?.federationprefix || "")
const [capabilities, setCapabilities] = React.useState<Capability[]>(value?.capabilities || [])
const [sentinelLocation, setSentinelLocation] = React.useState<string>(value?.sentinellocation || "")

const submitHandler = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

const exportValue = {
storageprefix: storagePrefix,
federationprefix: federationPrefix,
capabilities: capabilities
capabilities: capabilities,
sentinellocation: sentinelLocation
}


Expand All @@ -44,6 +46,9 @@ const ExportForm = ({ onSubmit, value }: FormProps<Export>) => {
<Box mb={2}>
<MultiSelectField<Capability> name={"Capabilities"} value={capabilities} onChange={setCapabilities} possibleValues={["PublicReads", "DirectReads", "Writes", "Listings", "Reads"]}/>
</Box>
<Box mb={2}>
<StringField name={"SentinelLocation"} value={sentinelLocation} onChange={setSentinelLocation} />
</Box>
<Button type={"submit"}>Submit</Button>
</form>
)
Expand Down
1 change: 1 addition & 0 deletions web_ui/frontend/components/Config/index.d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,5 @@ export interface Export {
storageprefix: string;
federationprefix: string;
capabilities: Capability[];
sentinellocation: string;
}
2 changes: 1 addition & 1 deletion xrootd/xrootd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type (
S3AccessKeyfile string
S3SecretKeyfile string
S3UrlStyle string
Exports []server_utils.OriginExports
Exports []server_utils.OriginExport
}

CacheConfig struct {
Expand Down
Loading