From f45b81dbe2157fc7561862a471d140fc3519312e Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:00:13 -0500 Subject: [PATCH] Prevent overwriting existing host_uuid file (#48012) In some circumstances, multiple Teleport processes may be trying to write the host_uuid file in the same data directory simultaneously. The last of the writers would win, and any process using a host UUID that did not match what ended up on disk could get into a perpertual state of being unable to connect to the cluster. To avoid the raciness, the host_uuid file writing process is no longer a blind upsert. Instead, special care is taken to ensure that there can only be a single writer, and that any subsequent updates to the file are aborted and the first value written is used instead. --- lib/service/service.go | 15 +-- lib/service/service_test.go | 3 +- lib/srv/regular/sshserver.go | 3 +- .../connectmycomputer/connectmycomputer.go | 7 +- .../connectmycomputer_test.go | 4 +- lib/utils/hostid/hostid.go | 61 +++++++++ lib/utils/hostid/hostid_test.go | 116 ++++++++++++++++++ lib/utils/hostid/hostid_unix.go | 105 ++++++++++++++++ lib/utils/hostid/hostid_windows.go | 30 +++++ lib/utils/utils.go | 72 ----------- lib/utils/utils_test.go | 50 -------- tool/tctl/common/admin_action_test.go | 3 +- tool/tctl/common/tctl.go | 7 +- tool/teleport/testenv/test_server.go | 3 +- 14 files changed, 338 insertions(+), 141 deletions(-) create mode 100644 lib/utils/hostid/hostid.go create mode 100644 lib/utils/hostid/hostid_test.go create mode 100644 lib/utils/hostid/hostid_unix.go create mode 100644 lib/utils/hostid/hostid_windows.go diff --git a/lib/service/service.go b/lib/service/service.go index 7f0b89e1bc3b1..83c11173898ba 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -160,6 +160,7 @@ import ( "github.com/gravitational/teleport/lib/utils" awsutils "github.com/gravitational/teleport/lib/utils/aws" "github.com/gravitational/teleport/lib/utils/cert" + "github.com/gravitational/teleport/lib/utils/hostid" logutils "github.com/gravitational/teleport/lib/utils/log" vc "github.com/gravitational/teleport/lib/versioncontrol" "github.com/gravitational/teleport/lib/versioncontrol/endpoint" @@ -2830,7 +2831,7 @@ func (process *TeleportProcess) initSSH() error { storagePresence := local.NewPresenceService(process.storage.BackendStorage) // read the host UUID: - serverID, err := utils.ReadOrMakeHostUUID(cfg.DataDir) + serverID, err := hostid.ReadOrCreateFile(cfg.DataDir) if err != nil { return trace.Wrap(err) } @@ -4307,7 +4308,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { } // read the host UUID: - serverID, err := utils.ReadOrMakeHostUUID(cfg.DataDir) + serverID, err := hostid.ReadOrCreateFile(cfg.DataDir) if err != nil { return trace.Wrap(err) } @@ -6307,7 +6308,7 @@ func readOrGenerateHostID(ctx context.Context, cfg *servicecfg.Config, kubeBacke if err := persistHostIDToStorages(ctx, cfg, kubeBackend); err != nil { return trace.Wrap(err) } - } else if kubeBackend != nil && utils.HostUUIDExistsLocally(cfg.DataDir) { + } else if kubeBackend != nil && hostid.ExistsLocally(cfg.DataDir) { // This case is used when loading a Teleport pre-11 agent with storage attached. // In this case, we have to copy the "host_uuid" from the agent to the secret // in case storage is removed later. @@ -6346,14 +6347,14 @@ func readHostIDFromStorages(ctx context.Context, dataDir string, kubeBackend kub } // Even if running in Kubernetes fallback to local storage if `host_uuid` was // not found in secret. - hostID, err := utils.ReadHostUUID(dataDir) + hostID, err := hostid.ReadFile(dataDir) return hostID, trace.Wrap(err) } // persistHostIDToStorages writes the cfg.HostUUID to local data and to // Kubernetes Secret if this process is running on a Kubernetes Cluster. func persistHostIDToStorages(ctx context.Context, cfg *servicecfg.Config, kubeBackend kubernetesBackend) error { - if err := utils.WriteHostUUID(cfg.DataDir, cfg.HostUUID); err != nil { + if err := hostid.WriteFile(cfg.DataDir, cfg.HostUUID); err != nil { if errors.Is(err, fs.ErrPermission) { cfg.Logger.ErrorContext(ctx, "Teleport does not have permission to write to the data directory. Ensure that you are running as a user with appropriate permissions.", "data_dir", cfg.DataDir) } @@ -6372,7 +6373,7 @@ func persistHostIDToStorages(ctx context.Context, cfg *servicecfg.Config, kubeBa // loadHostIDFromKubeSecret reads the host_uuid from the Kubernetes secret with // the expected key: `/host_uuid`. func loadHostIDFromKubeSecret(ctx context.Context, kubeBackend kubernetesBackend) (string, error) { - item, err := kubeBackend.Get(ctx, backend.NewKey(utils.HostUUIDFile)) + item, err := kubeBackend.Get(ctx, backend.NewKey(hostid.FileName)) if err != nil { return "", trace.Wrap(err) } @@ -6385,7 +6386,7 @@ func writeHostIDToKubeSecret(ctx context.Context, kubeBackend kubernetesBackend, _, err := kubeBackend.Put( ctx, backend.Item{ - Key: backend.NewKey(utils.HostUUIDFile), + Key: backend.NewKey(hostid.FileName), Value: []byte(id), }, ) diff --git a/lib/service/service_test.go b/lib/service/service_test.go index b94d776627999..6c5cb7b606e4e 100644 --- a/lib/service/service_test.go +++ b/lib/service/service_test.go @@ -69,6 +69,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/local" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" ) func TestMain(m *testing.M) { @@ -1177,7 +1178,7 @@ func Test_readOrGenerateHostID(t *testing.T) { dataDir := t.TempDir() // write host_uuid file to temp dir. if len(tt.args.hostIDContent) > 0 { - err := utils.WriteHostUUID(dataDir, tt.args.hostIDContent) + err := hostid.WriteFile(dataDir, tt.args.hostIDContent) require.NoError(t, err) } diff --git a/lib/srv/regular/sshserver.go b/lib/srv/regular/sshserver.go index df0e87f4abb48..6425e1dab27b7 100644 --- a/lib/srv/regular/sshserver.go +++ b/lib/srv/regular/sshserver.go @@ -72,6 +72,7 @@ import ( "github.com/gravitational/teleport/lib/sshutils/x11" "github.com/gravitational/teleport/lib/teleagent" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" "github.com/gravitational/teleport/lib/utils/uds" ) @@ -726,7 +727,7 @@ func New( options ...ServerOption, ) (*Server, error) { // read the host UUID: - uuid, err := utils.ReadOrMakeHostUUID(dataDir) + uuid, err := hostid.ReadOrCreateFile(dataDir) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/teleterm/services/connectmycomputer/connectmycomputer.go b/lib/teleterm/services/connectmycomputer/connectmycomputer.go index 1cc0f8914a052..26ecc8aafe8d9 100644 --- a/lib/teleterm/services/connectmycomputer/connectmycomputer.go +++ b/lib/teleterm/services/connectmycomputer/connectmycomputer.go @@ -41,6 +41,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/teleterm/clusters" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" ) type RoleSetup struct { @@ -395,7 +396,7 @@ func (n *NodeJoinWait) getNodeNameFromHostUUIDFile(ctx context.Context, cluster // the file is empty. // // Here we need to be able to distinguish between both of those two cases. - out, err := utils.ReadPath(utils.GetHostUUIDPath(dataDir)) + out, err := utils.ReadPath(hostid.GetPath(dataDir)) if err != nil { if trace.IsNotFound(err) { continue @@ -536,7 +537,7 @@ type NodeDelete struct { // Run grabs the host UUID of an agent from a disk and deletes the node with that name. func (n *NodeDelete) Run(ctx context.Context, presence Presence, cluster *clusters.Cluster) error { - hostUUID, err := utils.ReadHostUUID(getAgentDataDir(n.cfg.AgentsDir, cluster.ProfileName)) + hostUUID, err := hostid.ReadFile(getAgentDataDir(n.cfg.AgentsDir, cluster.ProfileName)) if trace.IsNotFound(err) { return nil } @@ -585,7 +586,7 @@ type NodeName struct { // Get returns the host UUID of the agent from a disk. func (n *NodeName) Get(cluster *clusters.Cluster) (string, error) { - hostUUID, err := utils.ReadHostUUID(getAgentDataDir(n.cfg.AgentsDir, cluster.ProfileName)) + hostUUID, err := hostid.ReadFile(getAgentDataDir(n.cfg.AgentsDir, cluster.ProfileName)) return hostUUID, trace.Wrap(err) } diff --git a/lib/teleterm/services/connectmycomputer/connectmycomputer_test.go b/lib/teleterm/services/connectmycomputer/connectmycomputer_test.go index 9a0af0b749edf..e7b453b94b2bc 100644 --- a/lib/teleterm/services/connectmycomputer/connectmycomputer_test.go +++ b/lib/teleterm/services/connectmycomputer/connectmycomputer_test.go @@ -35,7 +35,7 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/teleterm/api/uri" "github.com/gravitational/teleport/lib/teleterm/clusters" - "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" ) func TestRoleSetupRun_WithNonLocalUser(t *testing.T) { @@ -472,7 +472,7 @@ func mustMakeHostUUIDFile(t *testing.T, agentsDir string, profileName string) st err = os.MkdirAll(dataDir, agentsDirStat.Mode()) require.NoError(t, err) - hostUUID, err := utils.ReadOrMakeHostUUID(dataDir) + hostUUID, err := hostid.ReadOrCreateFile(dataDir) require.NoError(t, err) return hostUUID diff --git a/lib/utils/hostid/hostid.go b/lib/utils/hostid/hostid.go new file mode 100644 index 0000000000000..094e4cf9547ae --- /dev/null +++ b/lib/utils/hostid/hostid.go @@ -0,0 +1,61 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package hostid + +import ( + "errors" + "io/fs" + "path/filepath" + "strings" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/utils" +) + +const ( + // FileName is the file name where the host UUID file is stored + FileName = "host_uuid" +) + +// GetPath returns the path to the host UUID file given the data directory. +func GetPath(dataDir string) string { + return filepath.Join(dataDir, FileName) +} + +// ExistsLocally checks if dataDir/host_uuid file exists in local storage. +func ExistsLocally(dataDir string) bool { + _, err := ReadFile(dataDir) + return err == nil +} + +// ReadFile reads host UUID from the file in the data dir +func ReadFile(dataDir string) (string, error) { + out, err := utils.ReadPath(GetPath(dataDir)) + if err != nil { + if errors.Is(err, fs.ErrPermission) { + //do not convert to system error as this loses the ability to compare that it is a permission error + return "", trace.Wrap(err) + } + return "", trace.ConvertSystemError(err) + } + id := strings.TrimSpace(string(out)) + if id == "" { + return "", trace.NotFound("host uuid is empty") + } + return id, nil +} diff --git a/lib/utils/hostid/hostid_test.go b/lib/utils/hostid/hostid_test.go new file mode 100644 index 0000000000000..2ea22c4e71e7f --- /dev/null +++ b/lib/utils/hostid/hostid_test.go @@ -0,0 +1,116 @@ +//go:build !windows + +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package hostid_test + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" +) + +func TestMain(m *testing.M) { + utils.InitLoggerForTests() + os.Exit(m.Run()) +} + +func TestReadOrCreate(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + var wg errgroup.Group + concurrency := 10 + ids := make([]string, concurrency) + barrier := make(chan struct{}) + + for i := 0; i < concurrency; i++ { + i := i + wg.Go(func() error { + <-barrier + id, err := hostid.ReadOrCreateFile(dir) + ids[i] = id + return err + }) + } + + close(barrier) + + require.NoError(t, wg.Wait()) + for _, id := range ids { + assert.Equal(t, ids[0], id) + } +} + +func TestIdempotence(t *testing.T) { + t.Parallel() + + // call twice, get same result + dir := t.TempDir() + id, err := hostid.ReadOrCreateFile(dir) + require.Len(t, id, 36) + require.NoError(t, err) + uuidCopy, err := hostid.ReadOrCreateFile(dir) + require.NoError(t, err) + require.Equal(t, id, uuidCopy) +} + +func TestBadLocation(t *testing.T) { + t.Parallel() + + // call with a read-only dir, make sure to get an error + id, err := hostid.ReadOrCreateFile("/bad-location") + require.Empty(t, id) + require.Error(t, err) + require.Regexp(t, "^.*no such file or directory.*$", err.Error()) +} + +func TestIgnoreWhitespace(t *testing.T) { + t.Parallel() + + // newlines are getting ignored + dir := t.TempDir() + id := fmt.Sprintf("%s\n", uuid.NewString()) + err := os.WriteFile(filepath.Join(dir, hostid.FileName), []byte(id), 0666) + require.NoError(t, err) + out, err := hostid.ReadFile(dir) + require.NoError(t, err) + require.Equal(t, strings.TrimSpace(id), out) +} + +func TestRegenerateEmpty(t *testing.T) { + t.Parallel() + + // empty UUID in file is regenerated + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, hostid.FileName), nil, 0666) + require.NoError(t, err) + out, err := hostid.ReadOrCreateFile(dir) + require.NoError(t, err) + require.Len(t, out, 36) +} diff --git a/lib/utils/hostid/hostid_unix.go b/lib/utils/hostid/hostid_unix.go new file mode 100644 index 0000000000000..b5334e641c232 --- /dev/null +++ b/lib/utils/hostid/hostid_unix.go @@ -0,0 +1,105 @@ +//go:build !windows + +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package hostid + +import ( + "errors" + "io/fs" + "time" + + "github.com/google/renameio/v2" + "github.com/google/uuid" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/utils" +) + +// WriteFile writes host UUID into a file +func WriteFile(dataDir string, id string) error { + err := renameio.WriteFile(GetPath(dataDir), []byte(id), 0o400) + if err != nil { + if errors.Is(err, fs.ErrPermission) { + //do not convert to system error as this loses the ability to compare that it is a permission error + return trace.Wrap(err) + } + return trace.ConvertSystemError(err) + } + return nil +} + +// ReadOrCreateFile looks for a hostid file in the data dir. If present, +// returns the UUID from it, otherwise generates one +func ReadOrCreateFile(dataDir string) (string, error) { + hostUUIDFileLock := GetPath(dataDir) + ".lock" + const iterationLimit = 3 + + for i := 0; i < iterationLimit; i++ { + if read, err := ReadFile(dataDir); err == nil { + return read, nil + } else if !trace.IsNotFound(err) { + return "", trace.Wrap(err) + } + + // Checking error instead of the usual uuid.New() in case uuid generation + // fails due to not enough randomness. It's been known to happen happen when + // Teleport starts very early in the node initialization cycle and /dev/urandom + // isn't ready yet. + rawID, err := uuid.NewRandom() + if err != nil { + return "", trace.BadParameter("" + + "Teleport failed to generate host UUID. " + + "This may happen if randomness source is not fully initialized when the node is starting up. " + + "Please try restarting Teleport again.") + } + + writeFile := func(potentialID string) (string, error) { + unlock, err := utils.FSTryWriteLock(hostUUIDFileLock) + if err != nil { + return "", trace.Wrap(err) + } + defer unlock() + + if read, err := ReadFile(dataDir); err == nil { + return read, nil + } else if !trace.IsNotFound(err) { + return "", trace.Wrap(err) + } + + if err := WriteFile(dataDir, potentialID); err != nil { + return "", trace.Wrap(err) + } + + return potentialID, nil + } + + id, err := writeFile(rawID.String()) + if err != nil { + if errors.Is(err, utils.ErrUnsuccessfulLockTry) { + time.Sleep(100 * time.Millisecond) + continue + } + + return "", trace.Wrap(err) + } + + return id, nil + } + + return "", trace.LimitExceeded("failed to obtain host uuid") +} diff --git a/lib/utils/hostid/hostid_windows.go b/lib/utils/hostid/hostid_windows.go new file mode 100644 index 0000000000000..ab2a5a55e56d7 --- /dev/null +++ b/lib/utils/hostid/hostid_windows.go @@ -0,0 +1,30 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package hostid + +import "github.com/gravitational/trace" + +// WriteFile writes host UUID into a file +func WriteFile(dataDir string, id string) error { + return trace.NotImplemented("host id writing is not supported on windows") +} + +// ReadOrCreateFile looks for a hostid file in the data dir. If present, +// returns the UUID from it, otherwise generates one +func ReadOrCreateFile(dataDir string) (string, error) { + return "", trace.NotImplemented("host id writing is not supported on windows") +} diff --git a/lib/utils/utils.go b/lib/utils/utils.go index b1931e2ae8cf4..5da5b39d05685 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -37,7 +37,6 @@ import ( "time" "unicode" - "github.com/google/uuid" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/validation" @@ -468,75 +467,6 @@ func GetFreeTCPPorts(n int, offset ...int) (PortList, error) { return PortList{ports: list}, nil } -// GetHostUUIDPath returns the path to the host UUID file given the data directory. -func GetHostUUIDPath(dataDir string) string { - return filepath.Join(dataDir, HostUUIDFile) -} - -// HostUUIDExistsLocally checks if dataDir/host_uuid file exists in local storage. -func HostUUIDExistsLocally(dataDir string) bool { - _, err := ReadHostUUID(dataDir) - return err == nil -} - -// ReadHostUUID reads host UUID from the file in the data dir -func ReadHostUUID(dataDir string) (string, error) { - out, err := ReadPath(GetHostUUIDPath(dataDir)) - if err != nil { - if errors.Is(err, fs.ErrPermission) { - //do not convert to system error as this loses the ability to compare that it is a permission error - return "", err - } - return "", trace.ConvertSystemError(err) - } - id := strings.TrimSpace(string(out)) - if id == "" { - return "", trace.NotFound("host uuid is empty") - } - return id, nil -} - -// WriteHostUUID writes host UUID into a file -func WriteHostUUID(dataDir string, id string) error { - err := os.WriteFile(GetHostUUIDPath(dataDir), []byte(id), os.ModeExclusive|0400) - if err != nil { - if errors.Is(err, fs.ErrPermission) { - //do not convert to system error as this loses the ability to compare that it is a permission error - return err - } - return trace.ConvertSystemError(err) - } - return nil -} - -// ReadOrMakeHostUUID looks for a hostid file in the data dir. If present, -// returns the UUID from it, otherwise generates one -func ReadOrMakeHostUUID(dataDir string) (string, error) { - id, err := ReadHostUUID(dataDir) - if err == nil { - return id, nil - } - if !trace.IsNotFound(err) { - return "", trace.Wrap(err) - } - // Checking error instead of the usual uuid.New() in case uuid generation - // fails due to not enough randomness. It's been known to happen happen when - // Teleport starts very early in the node initialization cycle and /dev/urandom - // isn't ready yet. - rawID, err := uuid.NewRandom() - if err != nil { - return "", trace.BadParameter("" + - "Teleport failed to generate host UUID. " + - "This may happen if randomness source is not fully initialized when the node is starting up. " + - "Please try restarting Teleport again.") - } - id = rawID.String() - if err = WriteHostUUID(dataDir, id); err != nil { - return "", trace.Wrap(err) - } - return id, nil -} - // StringSliceSubset returns true if b is a subset of a. func StringSliceSubset(a []string, b []string) error { aset := make(map[string]bool) @@ -712,8 +642,6 @@ const ( // CertExtensionAuthority specifies teleport authority's name // that signed this domain CertExtensionAuthority = "x-teleport-authority" - // HostUUIDFile is the file name where the host UUID file is stored - HostUUIDFile = "host_uuid" // CertTeleportClusterName is a name of the teleport cluster CertTeleportClusterName = "x-teleport-cluster-name" // CertTeleportUserCertificate is the certificate of the authenticated in user. diff --git a/lib/utils/utils_test.go b/lib/utils/utils_test.go index 42ca172f35b78..e1625915bb204 100644 --- a/lib/utils/utils_test.go +++ b/lib/utils/utils_test.go @@ -20,14 +20,12 @@ package utils import ( "bytes" - "fmt" "os" "path/filepath" "strings" "testing" "time" - "github.com/google/uuid" "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,54 +39,6 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestHostUUIDIdempotent(t *testing.T) { - t.Parallel() - - // call twice, get same result - dir := t.TempDir() - id, err := ReadOrMakeHostUUID(dir) - require.Len(t, id, 36) - require.NoError(t, err) - uuidCopy, err := ReadOrMakeHostUUID(dir) - require.NoError(t, err) - require.Equal(t, id, uuidCopy) -} - -func TestHostUUIDBadLocation(t *testing.T) { - t.Parallel() - - // call with a read-only dir, make sure to get an error - id, err := ReadOrMakeHostUUID("/bad-location") - require.Empty(t, id) - require.Error(t, err) - require.Regexp(t, "^.*no such file or directory.*$", err.Error()) -} - -func TestHostUUIDIgnoreWhitespace(t *testing.T) { - t.Parallel() - - // newlines are getting ignored - dir := t.TempDir() - id := fmt.Sprintf("%s\n", uuid.NewString()) - err := os.WriteFile(filepath.Join(dir, HostUUIDFile), []byte(id), 0666) - require.NoError(t, err) - out, err := ReadHostUUID(dir) - require.NoError(t, err) - require.Equal(t, strings.TrimSpace(id), out) -} - -func TestHostUUIDRegenerateEmpty(t *testing.T) { - t.Parallel() - - // empty UUID in file is regenerated - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, HostUUIDFile), nil, 0666) - require.NoError(t, err) - out, err := ReadOrMakeHostUUID(dir) - require.NoError(t, err) - require.Len(t, out, 36) -} - func TestSelfSignedCert(t *testing.T) { t.Parallel() diff --git a/tool/tctl/common/admin_action_test.go b/tool/tctl/common/admin_action_test.go index 7e9ff52ceae3a..b910cf239b5ab 100644 --- a/tool/tctl/common/admin_action_test.go +++ b/tool/tctl/common/admin_action_test.go @@ -56,6 +56,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" tctl "github.com/gravitational/teleport/tool/tctl/common" testserver "github.com/gravitational/teleport/tool/teleport/testenv" tsh "github.com/gravitational/teleport/tool/tsh/common" @@ -1076,7 +1077,7 @@ func newAdminActionTestSuite(t *testing.T) *adminActionTestSuite { }) require.NoError(t, err) - hostUUID, err := utils.ReadHostUUID(process.Config.DataDir) + hostUUID, err := hostid.ReadFile(process.Config.DataDir) require.NoError(t, err) localAdmin, err := storage.ReadLocalIdentity( filepath.Join(process.Config.DataDir, teleport.ComponentProcess), diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index b4d0883c8464b..6ee12979b73bc 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -52,6 +52,7 @@ import ( "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" "github.com/gravitational/teleport/tool/common" ) @@ -380,16 +381,16 @@ func ApplyConfig(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authclient.Confi authConfig := new(authclient.Config) // read the host UUID only in case the identity was not provided, // because it will be used for reading local auth server identity - cfg.HostUUID, err = utils.ReadHostUUID(cfg.DataDir) + cfg.HostUUID, err = hostid.ReadFile(cfg.DataDir) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, trace.Wrap(err, "Could not load Teleport host UUID file at %s. "+ "Please make sure that a Teleport Auth Service instance is running on this host prior to using tctl or provide credentials by logging in with tsh first.", - filepath.Join(cfg.DataDir, utils.HostUUIDFile)) + filepath.Join(cfg.DataDir, hostid.FileName)) } else if errors.Is(err, fs.ErrPermission) { return nil, trace.Wrap(err, "Teleport does not have permission to read Teleport host UUID file at %s. "+ "Ensure that you are running as a user with appropriate permissions or provide credentials by logging in with tsh first.", - filepath.Join(cfg.DataDir, utils.HostUUIDFile)) + filepath.Join(cfg.DataDir, hostid.FileName)) } return nil, trace.Wrap(err) } diff --git a/tool/teleport/testenv/test_server.go b/tool/teleport/testenv/test_server.go index 759abe0d56a4e..234ad8296792b 100644 --- a/tool/teleport/testenv/test_server.go +++ b/tool/teleport/testenv/test_server.go @@ -62,6 +62,7 @@ import ( "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/hostid" "github.com/gravitational/teleport/tool/teleport/common" ) @@ -695,7 +696,7 @@ func MakeDefaultAuthClient(t *testing.T, process *service.TeleportProcess) *auth t.Helper() cfg := process.Config - hostUUID, err := utils.ReadHostUUID(process.Config.DataDir) + hostUUID, err := hostid.ReadFile(process.Config.DataDir) require.NoError(t, err) identity, err := storage.ReadLocalIdentity(