veyron/services/mgmt/node/impl: persist account associations
Serialize blessing to system account names mappings.
Change-Id: I71669a8e95512558696104a8d5450ed65f5e1324
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index 6c2ab54..1171507 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -154,7 +154,7 @@
// suffix. It is used to identify an application, installation, or
// instance.
suffix []string
- uat systemNameIdentityAssociation
+ uat BlessingSystemAssociationStore
}
func saveEnvelope(dir string, envelope *application.Envelope) error {
@@ -404,9 +404,9 @@
// and is probably not a good fit in other contexts. Revisit the design
// as appropriate. This function also internalizes a decision as to when
// it is possible to start an application that needs to be made explicit.
-func systemAccountForHelper(helperStat os.FileInfo, identityNames []string, uat systemNameIdentityAssociation) (systemName string, err error) {
+func systemAccountForHelper(helperStat os.FileInfo, identityNames []string, uat BlessingSystemAssociationStore) (systemName string, err error) {
haveHelper := isSetuid(helperStat)
- systemName, present := uat.associatedSystemAccount(identityNames)
+ systemName, present := uat.SystemAccountForBlessings(identityNames)
switch {
case haveHelper && present:
@@ -434,7 +434,7 @@
// TODO(rjkroege): Turning on the setuid feature of the suidhelper
// requires an installer with root permissions to install it in <config.Root>/helper
-func genCmd(instanceDir string, helperPath string, uat systemNameIdentityAssociation, identityNames []string) (*exec.Cmd, error) {
+func genCmd(instanceDir string, helperPath string, uat BlessingSystemAssociationStore, identityNames []string) (*exec.Cmd, error) {
versionLink := filepath.Join(instanceDir, "version")
versionDir, err := filepath.EvalSymlinks(versionLink)
if err != nil {
diff --git a/services/mgmt/node/impl/association_state.go b/services/mgmt/node/impl/association_state.go
index 04f04f3..8fe738f 100644
--- a/services/mgmt/node/impl/association_state.go
+++ b/services/mgmt/node/impl/association_state.go
@@ -1,50 +1,124 @@
package impl
import (
+ "os"
+ "path/filepath"
"veyron.io/veyron/veyron2/services/mgmt/node"
+ "veyron.io/veyron/veyron2/verror"
+
+ "encoding/json"
+ "sync"
)
-// TODO(rjk): Replace this with disk-backed storage in the node
-// manager's directory hierarchy.
-type systemNameIdentityAssociation map[string]string
+// BlessingSystemAssociationStore manages a persisted association between
+// Veyron blessings and system account names.
+type BlessingSystemAssociationStore interface {
+ // SystemAccountForBlessings returns a system name from the blessing to
+ // system name association store if one exists for any of the listed
+ // blessings.
+ SystemAccountForBlessings(blessings []string) (string, bool)
-// associatedUname returns a system name from the identity to system
-// name association store if one exists for any of the listed
-// identities.
-func (u systemNameIdentityAssociation) associatedSystemAccount(identityNames []string) (string, bool) {
+ // AllBlessingSystemAssociations returns all of the current Blessing to system
+ // account associations.
+ AllBlessingSystemAssociations() ([]node.Association, error)
+
+ // AssociateSystemAccountForBlessings associates the provided systenName with each
+ // provided blessing.
+ AssociateSystemAccountForBlessings(blessings []string, systemName string) error
+
+ // DisassociateSystemAccountForBlessings removes associations for the provided blessings.
+ DisassociateSystemAccountForBlessings(blessings []string) error
+}
+
+type association struct {
+ data map[string]string
+ filename string
+ sync.Mutex
+}
+
+func (u *association) SystemAccountForBlessings(blessings []string) (string, bool) {
+ u.Lock()
+ defer u.Unlock()
+
systemName := ""
present := false
- for _, n := range identityNames {
- if systemName, present = u[n]; present {
+ for _, n := range blessings {
+ if systemName, present = u.data[n]; present {
break
}
}
return systemName, present
}
-func (u systemNameIdentityAssociation) getAssociations() ([]node.Association, error) {
+func (u *association) AllBlessingSystemAssociations() ([]node.Association, error) {
+ u.Lock()
+ defer u.Unlock()
assocs := make([]node.Association, 0)
- for k, v := range u {
+
+ for k, v := range u.data {
assocs = append(assocs, node.Association{k, v})
}
return assocs, nil
}
-func (u systemNameIdentityAssociation) addAssociations(identityNames []string, systemName string) error {
- for _, n := range identityNames {
- u[n] = systemName
+func (u *association) serialize() (err error) {
+ f, err := os.OpenFile(u.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return verror.NoExistf("Could not open association file for writing %s: %v", u.filename, err)
}
- return nil
+ defer func() {
+ if closerr := f.Close(); closerr != nil {
+ err = closerr
+ }
+ }()
+
+ enc := json.NewEncoder(f)
+ return enc.Encode(u.data)
}
-func (u systemNameIdentityAssociation) deleteAssociations(identityNames []string) error {
- for _, n := range identityNames {
- delete(u, n)
+func (u *association) AssociateSystemAccountForBlessings(blessings []string, systemName string) error {
+ u.Lock()
+ defer u.Unlock()
+
+ for _, n := range blessings {
+ u.data[n] = systemName
}
- return nil
+ return u.serialize()
}
-func newSystemNameIdentityAssociation() systemNameIdentityAssociation {
- return make(map[string]string)
+func (u *association) DisassociateSystemAccountForBlessings(blessings []string) error {
+ u.Lock()
+ defer u.Unlock()
+
+ for _, n := range blessings {
+ delete(u.data, n)
+ }
+ return u.serialize()
+}
+
+func NewBlessingSystemAssociationStore(root string) (BlessingSystemAssociationStore, error) {
+ nddir := filepath.Join(root, "node-manager", "node-data")
+ if err := os.MkdirAll(nddir, os.FileMode(0700)); err != nil {
+ return nil, verror.NoExistf("Could not create node-data directory %s: %v\n", nddir, err)
+ }
+ msf := filepath.Join(nddir, "associated.accounts")
+
+ f, err := os.Open(msf)
+ if err != nil && os.IsExist(err) {
+ return nil, verror.NoExistf("Could not open association file %s: %v\n", msf, err)
+
+ }
+ defer f.Close()
+
+ a := &association{filename: msf, data: make(map[string]string)}
+
+ if err == nil {
+ dec := json.NewDecoder(f)
+ err := dec.Decode(&a.data)
+ if err != nil {
+ return nil, verror.NoExistf("Could not read association file %s: %v\n", msf, err)
+ }
+ }
+ return BlessingSystemAssociationStore(a), nil
}
diff --git a/services/mgmt/node/impl/association_state_test.go b/services/mgmt/node/impl/association_state_test.go
new file mode 100644
index 0000000..4d313dc
--- /dev/null
+++ b/services/mgmt/node/impl/association_state_test.go
@@ -0,0 +1,159 @@
+package impl_test
+
+import (
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "testing"
+ "veyron.io/veyron/veyron/services/mgmt/node/impl"
+ "veyron.io/veyron/veyron2/services/mgmt/node"
+)
+
+// TestAssociationPersistance verifies correct operation of association
+// persistance code.
+func TestAssociationPersistance(t *testing.T) {
+ td, err := ioutil.TempDir("", "nmtest")
+ if err != nil {
+ t.Fatalf("TempDir failed: %v", err)
+ }
+ defer os.RemoveAll(td)
+ nbsa1, err := impl.NewBlessingSystemAssociationStore(td)
+ if err != nil {
+ t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err)
+ }
+
+ // Insert an association.
+ err = nbsa1.AssociateSystemAccountForBlessings([]string{"alice", "bob"}, "alice_account")
+ if err != nil {
+ t.Fatalf("AssociateSystemAccount failed: %v", err)
+ }
+
+ got1, err := nbsa1.AllBlessingSystemAssociations()
+ if err != nil {
+ t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+ }
+
+ compareAssociations(t, got1, []node.Association{
+ {
+ "alice",
+ "alice_account",
+ },
+ {
+ "bob",
+ "alice_account",
+ },
+ })
+
+ nbsa2, err := impl.NewBlessingSystemAssociationStore(td)
+ if err != nil {
+ t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err)
+ }
+
+ got2, err := nbsa2.AllBlessingSystemAssociations()
+ if err != nil {
+ t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+ }
+ compareAssociations(t, got1, got2)
+
+ sysacc, have := nbsa2.SystemAccountForBlessings([]string{"bob"})
+ if expected := true; have != expected {
+ t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", have, expected)
+ }
+ if expected := "alice_account"; sysacc != expected {
+ t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", sysacc, expected)
+ }
+
+ sysacc, have = nbsa2.SystemAccountForBlessings([]string{"doug"})
+ if expected := false; have != expected {
+ t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", have, expected)
+ }
+ if expected := ""; sysacc != expected {
+ t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", sysacc, expected)
+ }
+
+ // Remove "bob".
+ err = nbsa1.DisassociateSystemAccountForBlessings([]string{"bob"})
+ if err != nil {
+ t.Fatalf("DisassociateSystemAccountForBlessings failed: %v", err)
+ }
+
+ // Verify that "bob" has been removed.
+ got1, err = nbsa1.AllBlessingSystemAssociations()
+ if err != nil {
+ t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+ }
+ compareAssociations(t, got1, []node.Association{
+ {
+ "alice",
+ "alice_account",
+ },
+ })
+
+ err = nbsa1.AssociateSystemAccountForBlessings([]string{"alice", "bob"}, "alice_other_account")
+ if err != nil {
+ t.Fatalf("AssociateSystemAccount failed: %v", err)
+ }
+ // Verify that "bob" and "alice" have new values.
+ got1, err = nbsa1.AllBlessingSystemAssociations()
+ if err != nil {
+ t.Fatalf("AllBlessingSystemAssociations failed: %v", err)
+ }
+ compareAssociations(t, got1, []node.Association{
+ {
+ "alice",
+ "alice_other_account",
+ },
+ {
+ "bob",
+ "alice_other_account",
+ },
+ })
+
+ // Make future serialization attempts fail.
+ if err := os.RemoveAll(td); err != nil {
+ t.Fatalf("os.RemoveAll: couldn't delete %s: %v", td, err)
+ }
+ err = nbsa1.AssociateSystemAccountForBlessings([]string{"doug"}, "alice_account")
+ if err == nil {
+ t.Fatalf("AssociateSystemAccount should have failed but didn't")
+ }
+}
+
+func TestAssociationPersistanceDetectsBadStartingConditions(t *testing.T) {
+ dir := "/i-am-hoping-that-there-is-no-such-directory"
+ nbsa1, err := impl.NewBlessingSystemAssociationStore(dir)
+ if nbsa1 != nil || err == nil {
+ t.Fatalf("bad root directory %s ought to have caused an error", dir)
+ }
+
+ // Create a NewBlessingSystemAssociationStore directory as a side-effect.
+ nbsa1, err = impl.NewBlessingSystemAssociationStore(os.TempDir())
+ defer os.RemoveAll(path.Join(os.TempDir(), "node-manager"))
+ if err != nil {
+ t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err)
+ }
+
+ tpath := path.Join(os.TempDir(), "node-manager", "node-data", "associated.accounts")
+ f, err := os.Create(tpath)
+ if err != nil {
+ t.Fatalf("could not open backing file for setup: %v", err)
+ }
+
+ if _, err := io.WriteString(f, "bad-json\""); err != nil {
+ t.Fatalf("could not write to test file %s: %v", tpath, err)
+ }
+ f.Close()
+
+ nbsa1, err = impl.NewBlessingSystemAssociationStore(dir)
+ if nbsa1 != nil || err == nil {
+ t.Fatalf("invalid JSON ought to have caused an error")
+ }
+
+ // This test will fail if executed as root or if your system is configured oddly.
+ unreadableFile := "/dev/autofs"
+ nbsa1, err = impl.NewBlessingSystemAssociationStore(unreadableFile)
+ if nbsa1 != nil || err == nil {
+ t.Fatalf("unreadable file %s ought to have caused an error", unreadableFile)
+ }
+}
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index 3631710..ffc2cf6 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -50,7 +50,7 @@
// dispatcherMutex is a lock for coordinating concurrent access to some
// dispatcher methods.
mu sync.RWMutex
- uat systemNameIdentityAssociation
+ uat BlessingSystemAssociationStore
}
var _ ipc.Dispatcher = (*dispatcher)(nil)
@@ -77,6 +77,10 @@
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid config %v: %v", config, err)
}
+ uat, err := NewBlessingSystemAssociationStore(config.Root)
+ if err != nil {
+ return nil, fmt.Errorf("cannot create persistent store for identity to system account associations: %v", err)
+ }
d := &dispatcher{
etag: "default",
internal: &internalState{
@@ -84,7 +88,7 @@
updating: newUpdatingState(),
},
config: config,
- uat: newSystemNameIdentityAssociation(),
+ uat: uat,
}
// If there exists a signed ACL from a previous instance we prefer that.
aclFile, sigFile, _ := d.getACLFilePaths()
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 48fd309..9fcee59 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1077,23 +1077,12 @@
return nil
}
-// Code to make Association lists sortable.
-type byIdentity []node.Association
-
-func (a byIdentity) Len() int { return len(a) }
-func (a byIdentity) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a byIdentity) Less(i, j int) bool { return a[i].IdentityName < a[j].IdentityName }
-
func listAndVerifyAssociations(t *testing.T, stub node.Node, run veyron2.Runtime, expected []node.Association) {
assocs, err := stub.ListAssociations(run.NewContext())
if err != nil {
t.Fatalf("ListAssociations failed %v", err)
}
- sort.Sort(byIdentity(assocs))
- sort.Sort(byIdentity(expected))
- if !reflect.DeepEqual(assocs, expected) {
- t.Fatalf("ListAssociations() got %v, expected %v", assocs, expected)
- }
+ compareAssociations(t, assocs, expected)
}
// TODO(rjkroege): Verify that associations persist across restarts
diff --git a/services/mgmt/node/impl/node_invoker.go b/services/mgmt/node/impl/node_invoker.go
index 22a0431..a751d11 100644
--- a/services/mgmt/node/impl/node_invoker.go
+++ b/services/mgmt/node/impl/node_invoker.go
@@ -14,6 +14,7 @@
// node-data/
// acl.nodemanager
// acl.signature
+// associated.accounts
//
// The node manager is always expected to be started through the symbolic link
// passed in as config.CurrentLink, which is monitored by an init daemon. This
@@ -89,7 +90,7 @@
callback *callbackState
config *config.State
disp *dispatcher
- uat systemNameIdentityAssociation
+ uat BlessingSystemAssociationStore
}
func (i *nodeInvoker) Claim(call ipc.ServerContext) error {
@@ -422,10 +423,10 @@
}
if accountName == "" {
- return i.uat.deleteAssociations(identityNames)
+ return i.uat.DisassociateSystemAccountForBlessings(identityNames)
} else {
// TODO(rjkroege): Optionally verify here that the required uname is a valid.
- return i.uat.addAssociations(identityNames, accountName)
+ return i.uat.AssociateSystemAccountForBlessings(identityNames, accountName)
}
}
@@ -436,5 +437,5 @@
if err := sameMachineCheck(call); err != nil {
return nil, err
}
- return i.uat.getAssociations()
+ return i.uat.AllBlessingSystemAssociations()
}
diff --git a/services/mgmt/node/impl/util_test.go b/services/mgmt/node/impl/util_test.go
index 92be69a..fd543dd 100644
--- a/services/mgmt/node/impl/util_test.go
+++ b/services/mgmt/node/impl/util_test.go
@@ -5,6 +5,8 @@
"io/ioutil"
"os"
"path/filepath"
+ "reflect"
+ "sort"
"testing"
"veyron.io/veyron/veyron2"
@@ -279,3 +281,18 @@
t.Fatalf("Uninstall(%v) failed: %v", appID, err)
}
}
+
+// Code to make Association lists sortable.
+type byIdentity []node.Association
+
+func (a byIdentity) Len() int { return len(a) }
+func (a byIdentity) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a byIdentity) Less(i, j int) bool { return a[i].IdentityName < a[j].IdentityName }
+
+func compareAssociations(t *testing.T, got, expected []node.Association) {
+ sort.Sort(byIdentity(got))
+ sort.Sort(byIdentity(expected))
+ if !reflect.DeepEqual(got, expected) {
+ t.Fatalf("ListAssociations() got %v, expected %v", got, expected)
+ }
+}