veyron/services/mgmt/node: refactor the code and clean up the unit test.
Main changes:
- separate out the 'contract' for the node manager (in terms of what it expects as configuration, what it passes along to its next version, and how the soft link is supposed to be used); make it easier to replace the environment variable based mechanism to pass this config state with e.g. cmd-line flags or a file. Make it clear what node manager implementation can hide and what is public API
- make the soft link point to the actual node manager script rather than its containing workspace. This is again to clean up the contract -- whoever invokes node manager shouldn't assume node manager internally uses workspaces with noded.sh scripts inside of them
- make unit test setup simpler, and make the unit test do more things
- along the way, fixing some small issues that I noticed
Other than that, behavior-wise nothing much changes as far as how node manager works.
Change-Id: I4684c4de7f5e9ab952ca7f7ac0a239f4810d8b8c
diff --git a/services/mgmt/node/config/config.go b/services/mgmt/node/config/config.go
new file mode 100644
index 0000000..dc1fb78
--- /dev/null
+++ b/services/mgmt/node/config/config.go
@@ -0,0 +1,139 @@
+// Package config handles configuration state passed across instances of the
+// node manager.
+//
+// The State object captures setting that the node manager needs to be aware of
+// when it starts. This is passed to the first invocation of the node manager,
+// and then passed down from old node manager to new node manager upon update.
+// The node manager has an implementation-dependent mechanism for parsing and
+// passing state, which is encapsulated by the state sub-package (currently, the
+// mechanism uses environment variables). When instantiating a new instance of
+// the node manager service, the developer needs to pass in a copy of State.
+// They can obtain this by calling Load, which captures any config state passed
+// by a previous version of node manager during update. Any new version of the
+// node manager must be able to decode a previous version's config state, even
+// if the new version changes the mechanism for passing this state (that is,
+// node manager implementations must be backward-compatible as far as accepting
+// and passing config state goes). TODO(caprita): add config state versioning?
+package config
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "veyron2/services/mgmt/application"
+)
+
+// State specifies how the node manager is configured. This should encapsulate
+// what the node manager needs to know and/or be able to mutate about its
+// environment.
+type State struct {
+ // Name is the node manager's object name. Must be non-empty.
+ Name string
+ // Envelope is the node manager's application envelope. If nil, any
+ // envelope fetched from the application repository will trigger an
+ // update.
+ Envelope *application.Envelope
+ // Previous holds the local path to the previous version of the node
+ // manager. If empty, revert is disabled.
+ Previous string
+ // Root is the directory on the local filesystem that contains
+ // the applications' workspaces. Must be non-empty.
+ Root string
+ // Origin is the application repository object name for the node
+ // manager application. If empty, update is disabled.
+ Origin string
+ // CurrentLink is the local filesystem soft link that should point to
+ // the version of the node manager binary/script through which node
+ // manager is started. Node manager is expected to mutate this during
+ // a self-update. Must be non-empty.
+ CurrentLink string
+}
+
+// Validate checks the config state.
+func (c *State) Validate() error {
+ if c.Name == "" {
+ return fmt.Errorf("Name cannot be empty")
+ }
+ if c.Root == "" {
+ return fmt.Errorf("Root cannot be empty")
+ }
+ if c.CurrentLink == "" {
+ return fmt.Errorf("CurrentLink cannot be empty")
+ }
+ return nil
+}
+
+// Load reconstructs the config state passed to the node manager (presumably by
+// the parent node manager during an update). Currently, this is done via
+// environment variables.
+func Load() (*State, error) {
+ var env *application.Envelope
+ if jsonEnvelope := os.Getenv(EnvelopeEnv); jsonEnvelope != "" {
+ env = new(application.Envelope)
+ if err := json.Unmarshal([]byte(jsonEnvelope), env); err != nil {
+ return nil, fmt.Errorf("failed to decode envelope from %v: %v", jsonEnvelope, err)
+ }
+ }
+ return &State{
+ Envelope: env,
+ Previous: os.Getenv(PreviousEnv),
+ Root: os.Getenv(RootEnv),
+ Origin: os.Getenv(OriginEnv),
+ CurrentLink: os.Getenv(CurrentLinkEnv),
+ }, nil
+}
+
+// Save serializes the config state meant to be passed to a child node manager
+// during an update, returning a slice of "key=value" strings, which are
+// expected to be stuffed into environment variable settings by the caller.
+func (c *State) Save(envelope *application.Envelope) ([]string, error) {
+ jsonEnvelope, err := json.Marshal(envelope)
+ if err != nil {
+ return nil, fmt.Errorf("failed to encode envelope %v: %v", envelope, err)
+ }
+ currScript, err := filepath.EvalSymlinks(c.CurrentLink)
+ if err != nil {
+ return nil, fmt.Errorf("EvalSymlink failed: %v", err)
+ }
+ settings := map[string]string{
+ EnvelopeEnv: string(jsonEnvelope),
+ PreviousEnv: currScript,
+ RootEnv: c.Root,
+ OriginEnv: c.Origin,
+ CurrentLinkEnv: c.CurrentLink,
+ }
+ // We need to manually pass the namespace roots to the child, since we
+ // currently don't have a way for the child to obtain this information
+ // from a config service at start-up.
+ for _, ev := range os.Environ() {
+ p := strings.SplitN(ev, "=", 2)
+ if len(p) != 2 {
+ continue
+ }
+ k, v := p[0], p[1]
+ if strings.HasPrefix(k, "NAMESPACE_ROOT") {
+ settings[k] = v
+ }
+ }
+ var ret []string
+ for k, v := range settings {
+ ret = append(ret, k+"="+v)
+ }
+ return ret, nil
+}
+
+// QuoteEnv wraps environment variable values in double quotes, making them
+// suitable for inclusion in a bash script.
+func QuoteEnv(env []string) (ret []string) {
+ for _, e := range env {
+ if eqIdx := strings.Index(e, "="); eqIdx > 0 {
+ ret = append(ret, fmt.Sprintf("%s=%q", e[:eqIdx], e[eqIdx+1:]))
+ } else {
+ ret = append(ret, e)
+ }
+ }
+ return
+}
diff --git a/services/mgmt/node/config/config_test.go b/services/mgmt/node/config/config_test.go
new file mode 100644
index 0000000..44264b3
--- /dev/null
+++ b/services/mgmt/node/config/config_test.go
@@ -0,0 +1,112 @@
+package config_test
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "veyron/services/mgmt/node/config"
+
+ "veyron2/services/mgmt/application"
+)
+
+// TestState checks that encoding/decoding State to child/from parent works
+// as expected.
+func TestState(t *testing.T) {
+ currScript := filepath.Join(os.TempDir(), "fido/was/here")
+ if err := os.MkdirAll(currScript, 0700); err != nil {
+ t.Fatalf("MkdirAll failed: %v", err)
+ }
+ defer os.RemoveAll(currScript)
+ currLink := filepath.Join(os.TempDir(), "familydog")
+ if err := os.Symlink(currScript, currLink); err != nil {
+ t.Fatalf("Symlink failed: %v", err)
+ }
+ defer os.Remove(currLink)
+ state := &config.State{
+ Name: "fido",
+ Previous: "doesn't matter",
+ Root: "fidos/doghouse",
+ Origin: "pet/store",
+ CurrentLink: currLink,
+ }
+ if err := state.Validate(); err != nil {
+ t.Errorf("Config state %v failed to validate: %v", state, err)
+ }
+ encoded, err := state.Save(&application.Envelope{
+ Title: "dog",
+ Args: []string{"roll-over", "play-dead"},
+ })
+ if err != nil {
+ t.Errorf("Config state %v Save failed: %v", state, err)
+ }
+ for _, e := range encoded {
+ pair := strings.SplitN(e, "=", 2)
+ os.Setenv(pair[0], pair[1])
+ }
+ decodedState, err := config.Load()
+ if err != nil {
+ t.Errorf("Config state Load failed: %v", err)
+ }
+ expectedState := state
+ expectedState.Envelope = &application.Envelope{
+ Title: "dog",
+ Args: []string{"roll-over", "play-dead"},
+ }
+ expectedState.Name = ""
+ expectedState.Previous = currScript
+ if !reflect.DeepEqual(decodedState, expectedState) {
+ t.Errorf("Decode state: want %v, got %v", expectedState, decodedState)
+ }
+}
+
+// TestValidate checks the Validate method of State.
+func TestValidate(t *testing.T) {
+ state := &config.State{
+ Name: "schweinsteiger",
+ Previous: "a",
+ Root: "b",
+ Origin: "c",
+ CurrentLink: "d",
+ }
+ if err := state.Validate(); err != nil {
+ t.Errorf("Config state %v failed to validate: %v", state, err)
+ }
+ state.Root = ""
+ if err := state.Validate(); err == nil {
+ t.Errorf("Config state %v should have failed to validate.", state)
+ }
+ state.Root, state.CurrentLink = "a", ""
+ if err := state.Validate(); err == nil {
+ t.Errorf("Confi stateg %v should have failed to validate.", state)
+ }
+ state.CurrentLink, state.Name = "d", ""
+ if err := state.Validate(); err == nil {
+ t.Errorf("Config state %v should have failed to validate.", state)
+ }
+}
+
+// TestQuoteEnv checks the QuoteEnv method.
+func TestQuoteEnv(t *testing.T) {
+ cases := []struct {
+ before, after string
+ }{
+ {`a=b`, `a="b"`},
+ {`a=`, `a=""`},
+ {`a`, `a`},
+ {`a=x y`, `a="x y"`},
+ {`a="x y"`, `a="\"x y\""`},
+ {`a='x y'`, `a="'x y'"`},
+ }
+ var input []string
+ var want []string
+ for _, c := range cases {
+ input = append(input, c.before)
+ want = append(want, c.after)
+ }
+ if got := config.QuoteEnv(input); !reflect.DeepEqual(want, got) {
+ t.Errorf("QuoteEnv(%v) wanted %v, got %v instead", input, want, got)
+ }
+}
diff --git a/services/mgmt/node/config/const.go b/services/mgmt/node/config/const.go
new file mode 100644
index 0000000..c57cd98
--- /dev/null
+++ b/services/mgmt/node/config/const.go
@@ -0,0 +1,21 @@
+package config
+
+const (
+ // EnvelopeEnv is the name of the environment variable that holds the
+ // serialized node manager application envelope.
+ EnvelopeEnv = "VEYRON_NM_ENVELOPE"
+ // PreviousEnv is the name of the environment variable that holds the
+ // path to the previous version of the node manager.
+ PreviousEnv = "VEYRON_NM_PREVIOUS"
+ // OriginEnv is the name of the environment variable that holds the
+ // object name of the application repository that can be used to
+ // retrieve the node manager application envelope.
+ OriginEnv = "VEYRON_NM_ORIGIN"
+ // RootEnv is the name of the environment variable that holds the
+ // path to the directory in which node manager workspaces are
+ // created.
+ RootEnv = "VEYRON_NM_ROOT"
+ // CurrentLinkEnv is the name of the environment variable that holds
+ // the path to the soft link that points to the current node manager.
+ CurrentLinkEnv = "VEYRON_NM_CURRENT"
+)
diff --git a/services/mgmt/node/doc.go b/services/mgmt/node/doc.go
new file mode 100644
index 0000000..2f98bea
--- /dev/null
+++ b/services/mgmt/node/doc.go
@@ -0,0 +1,34 @@
+// Package node contains the implementation for the veyron2/mgmt/node APIs.
+//
+// The node manager is a server that is expected to run on every Veyron-enabled
+// node, and it handles both node management and management of the applications
+// running on the node.
+//
+// The node manager is responsible for installing, updating, and launching
+// applications. It therefore sets up a footprint on the local filesystem, both
+// to maintain its internal state, and to provide applications with their own
+// private workspaces.
+//
+// The node manager is responsible for updating itself. The mechanism to do so
+// is implementation-dependent, though each node manager expects to be supplied
+// with the file path of a symbolic link file, which the node manager will then
+// update to point to the updated version of itself before terminating itself.
+// The node manager should therefore be launched via this symbolic link to
+// enable auto-updates. To enable updates, in addition to the symbolic link
+// path, the node manager needs to be told what its application metadata is
+// (such as command-line arguments and environment variables, i.e. the
+// application envelope defined in the veyron2/services/mgmt/application
+// package), as well as the object name for where it can fetch an updated
+// envelope, and the local filesystem path for its previous version (for
+// rollbacks).
+//
+// Finally, the node manager needs to know its own object name, so it can pass
+// that along to the applications that it starts.
+//
+// The impl subpackage contains the implementation of the node manager service.
+//
+// The config subpackage encapsulates the configuration settings that form the
+// node manager service's 'contract' with its environment.
+//
+// The noded subpackage contains the main driver.
+package node
diff --git a/services/mgmt/node/impl/callback.go b/services/mgmt/node/impl/callback.go
new file mode 100644
index 0000000..787425d
--- /dev/null
+++ b/services/mgmt/node/impl/callback.go
@@ -0,0 +1,34 @@
+package impl
+
+import (
+ "veyron/services/mgmt/lib/exec"
+ inode "veyron/services/mgmt/node"
+
+ "veyron2/mgmt"
+ "veyron2/rt"
+ "veyron2/vlog"
+)
+
+// InvokeCallback provides the parent node manager with the given name (which is
+// expected to be this node manager's object name).
+func InvokeCallback(name string) {
+ handle, err := exec.GetChildHandle()
+ switch err {
+ case nil:
+ // Node manager was started by self-update, notify the parent.
+ callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
+ if err != nil {
+ vlog.Fatalf("Failed to get callback name from config: %v", err)
+ }
+ nmClient, err := inode.BindNode(callbackName)
+ if err != nil {
+ vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
+ }
+ if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
+ vlog.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
+ }
+ case exec.ErrNoVersion:
+ default:
+ vlog.Fatalf("GetChildHandle() failed: %v", err)
+ }
+}
diff --git a/services/mgmt/node/impl/const.go b/services/mgmt/node/impl/const.go
deleted file mode 100644
index c3c3d7b..0000000
--- a/services/mgmt/node/impl/const.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package impl
-
-const (
- // BinaryEnv is the name of the environment variable that holds the
- // object name used for obtaining the node manager binary.
- BinaryEnv = "VEYRON_NM_BINARY"
- // PreviousEnv is the name of the environment variable that holds
- // the path to the workspace that contains the previous version of
- // the node manager.
- PreviousEnv = "VEYRON_NM_PREVIOUS"
- // OriginEnv is the name of the environment variable that holds the
- // object name of the application repository that can be used to
- // retrieve the node manager application envelope.
- OriginEnv = "VEYRON_NM_ORIGIN"
- // RootEnv is the name of the environment variable that holds the
- // path to the directory in which node manager workspaces are
- // created.
- RootEnv = "VEYRON_NM_ROOT"
-)
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index 19147f6..33abf91 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -1,39 +1,47 @@
package impl
import (
+ "fmt"
"sync"
"veyron/services/mgmt/node"
+ "veyron/services/mgmt/node/config"
"veyron2/ipc"
"veyron2/security"
- "veyron2/services/mgmt/application"
)
// dispatcher holds the state of the node manager dispatcher.
type dispatcher struct {
- auth security.Authorizer
- state *state
+ auth security.Authorizer
+ internal *internalState
+ config *config.State
}
// NewDispatcher is the node manager dispatcher factory.
-func NewDispatcher(auth security.Authorizer, envelope *application.Envelope, name, previous string) *dispatcher {
+func NewDispatcher(auth security.Authorizer, config *config.State) (*dispatcher, error) {
+ if err := config.Validate(); err != nil {
+ return nil, fmt.Errorf("Invalid config %v: %v", config, err)
+ }
+
return &dispatcher{
auth: auth,
- state: &state{
+ internal: &internalState{
channels: make(map[string]chan string),
channelsMutex: new(sync.Mutex),
- envelope: envelope,
- name: name,
- previous: previous,
updating: false,
updatingMutex: new(sync.Mutex),
},
- }
+ config: config,
+ }, nil
}
// DISPATCHER INTERFACE IMPLEMENTATION
func (d *dispatcher) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
- return ipc.ReflectInvoker(node.NewServerNode(NewInvoker(d.state, suffix))), d.auth, nil
+ return ipc.ReflectInvoker(node.NewServerNode(&invoker{
+ internal: d.internal,
+ config: d.config,
+ suffix: suffix,
+ })), d.auth, nil
}
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index adb226c..2146596 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1,451 +1,341 @@
package impl_test
import (
- "crypto/md5"
- "encoding/hex"
- "errors"
"fmt"
- "io"
"io/ioutil"
"os"
+ goexec "os/exec"
"path/filepath"
"strings"
- "syscall"
"testing"
- "time"
"veyron/lib/signals"
- _ "veyron/lib/testutil"
"veyron/lib/testutil/blackbox"
- "veyron/lib/testutil/security"
"veyron/services/mgmt/lib/exec"
- "veyron/services/mgmt/node"
+ "veyron/services/mgmt/node/config"
"veyron/services/mgmt/node/impl"
- mtlib "veyron/services/mounttable/lib"
- "veyron2"
- "veyron2/ipc"
- "veyron2/mgmt"
"veyron2/naming"
"veyron2/rt"
"veyron2/services/mgmt/application"
- "veyron2/services/mgmt/binary"
- "veyron2/services/mgmt/repository"
"veyron2/verror"
"veyron2/vlog"
)
-const (
- testEnv = "VEYRON_NM_TEST"
-)
-
-var (
- errOperationFailed = errors.New("operation failed")
-)
-
-func init() {
- blackbox.CommandTable["nodeManager"] = nodeManager
-}
-
-// arInvoker holds the state of an application repository invocation
-// mock. On its first invocation of Match, this mock will return a bogus
-// application title. On the second invocation, it will return the correct
-// node manager app title. We make use of this behavior to test that Update
-// fails when the app title mismatches.
-type arInvoker struct {
- firstInvocation bool
-}
-
-// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
-
-func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
- vlog.VI(0).Infof("Match(), first invocation: %t", i.firstInvocation)
- envelope := generateEnvelope()
- if i.firstInvocation {
- i.firstInvocation = false
- envelope.Title = "gibberish"
- } else {
- envelope.Title = application.NodeManagerTitle
- }
- envelope.Env = exec.Setenv(envelope.Env, testEnv, "child")
- envelope.Binary = "cr"
- return *envelope, nil
-}
-
-// crInvoker holds the state of a binary repository invocation mock.
-type crInvoker struct{}
-
-// BINARY REPOSITORY INTERFACE IMPLEMENTATION
-
-func (*crInvoker) Create(ipc.ServerContext, int32) error {
- vlog.VI(0).Infof("Create()")
- return nil
-}
-
-func (i *crInvoker) Delete(ipc.ServerContext) error {
- vlog.VI(0).Infof("Delete()")
- return nil
-}
-
-func (i *crInvoker) Download(_ ipc.ServerContext, _ int32, stream repository.BinaryServiceDownloadStream) error {
- vlog.VI(0).Infof("Download()")
- file, err := os.Open(os.Args[0])
- if err != nil {
- vlog.Errorf("Open() failed: %v", err)
- return errOperationFailed
- }
- defer file.Close()
- bufferLength := 4096
- buffer := make([]byte, bufferLength)
- for {
- n, err := file.Read(buffer)
- switch err {
- case io.EOF:
- return nil
- case nil:
- if err := stream.Send(buffer[:n]); err != nil {
- vlog.Errorf("Send() failed: %v", err)
- return errOperationFailed
- }
- default:
- vlog.Errorf("Read() failed: %v", err)
- return errOperationFailed
- }
- }
-}
-
-func (*crInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
- vlog.VI(0).Infof("DownloadURL()")
- return "", 0, nil
-}
-
-func (*crInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, error) {
- vlog.VI(0).Infof("Stat()")
- h := md5.New()
- bytes, err := ioutil.ReadFile(os.Args[0])
- if err != nil {
- return []binary.PartInfo{}, errOperationFailed
- }
- h.Write(bytes)
- part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
- return []binary.PartInfo{part}, nil
-}
-
-func (i *crInvoker) Upload(ipc.ServerContext, int32, repository.BinaryServiceUploadStream) error {
- vlog.VI(0).Infof("Upload()")
- return nil
-}
-
-func generateBinary(workspace string) string {
- path := filepath.Join(workspace, "noded")
- if err := os.Link(os.Args[0], path); err != nil {
- vlog.Fatalf("Link(%v, %v) failed: %v", os.Args[0], path, err)
- }
- return path
-}
-
-func generateEnvelope() *application.Envelope {
- envelope := &application.Envelope{}
- envelope.Args = os.Args[1:]
- for _, env := range os.Environ() {
- i := strings.Index(env, "=")
- envelope.Env = append(envelope.Env, fmt.Sprintf("%s=%q", env[:i], env[i+1:]))
- }
- return envelope
-}
-
-func generateLink(root, workspace string) {
- link := filepath.Join(root, impl.CurrentWorkspace)
- newLink := link + ".new"
- fi, err := os.Lstat(newLink)
- if err == nil {
- if err := os.Remove(fi.Name()); err != nil {
- vlog.Fatalf("Remove(%v) failed: %v", fi.Name(), err)
- }
- }
- if err := os.Symlink(workspace, newLink); err != nil {
- vlog.Fatalf("Symlink(%v, %v) failed: %v", workspace, newLink, err)
- }
- if err := os.Rename(newLink, link); err != nil {
- vlog.Fatalf("Rename(%v, %v) failed: %v", newLink, link, err)
- }
-}
-
-func generateScript(workspace, binary string) string {
- envelope := generateEnvelope()
- envelope.Env = exec.Setenv(envelope.Env, testEnv, "parent")
- output := "#!/bin/bash\n"
- output += strings.Join(envelope.Env, " ") + " "
- output += binary + " " + strings.Join(envelope.Args, " ")
- path := filepath.Join(workspace, "noded.sh")
- if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
- vlog.Fatalf("WriteFile(%v) failed: %v", path, err)
- }
- return path
-}
-
-func invokeCallback(name string) {
- handle, err := exec.GetChildHandle()
- switch err {
- case nil:
- // Node manager was started by self-update, notify the parent
- // process that you are ready.
- handle.SetReady()
- callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
- if err != nil {
- vlog.Fatalf("Failed to get callback name from config: %v", err)
- }
- nmClient, err := node.BindNode(callbackName)
- if err != nil {
- vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
- }
- if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
- vlog.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
- }
- case exec.ErrNoVersion:
- vlog.Fatalf("invokeCallback should only be called from child node manager")
- default:
- vlog.Fatalf("NewChildHandle() failed: %v", err)
- }
-}
-
-func invokeUpdate(t *testing.T, name string, expectFail bool) {
- address := naming.JoinAddressName(name, "nm")
- stub, err := node.BindNode(address)
- if err != nil {
- t.Fatalf("BindNode(%v) failed: %v", address, err)
- }
- err = stub.Update(rt.R().NewContext())
- if expectFail {
- if !verror.Is(err, verror.BadArg) {
- t.Fatalf("Unexpected update error: %v", err)
- }
- } else if err != nil {
- t.Fatalf("Update() failed: %v", err)
- }
-}
-
-// nodeManager is an enclosure for setting up and starting the parent
-// and child node manager used by the TestUpdate() method.
-func nodeManager(argv []string) {
- root := os.Getenv(impl.RootEnv)
- switch os.Getenv(testEnv) {
- case "setup":
- workspace := filepath.Join(root, fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
- perm := os.FileMode(0755)
- if err := os.MkdirAll(workspace, perm); err != nil {
- vlog.Fatalf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
- }
- binary := generateBinary(workspace)
- script := generateScript(workspace, binary)
- generateLink(root, workspace)
- argv, envv := []string{}, []string{}
- if err := syscall.Exec(script, argv, envv); err != nil {
- vlog.Fatalf("Exec(%v, %v, %v) failed: %v", script, argv, envv, err)
- }
- case "parent":
- runtime := rt.Init()
- defer runtime.Cleanup()
- // Set up a mock binary repository, a mock application repository, and a node manager.
- _, crCleanup := startBinaryRepository()
- defer crCleanup()
- _, arCleanup := startApplicationRepository()
- defer arCleanup()
- _, nmCleanup := startNodeManager()
- defer nmCleanup()
- // Wait until shutdown.
- <-signals.ShutdownOnSignals()
- blackbox.WaitForEOFOnStdin()
- case "child":
- runtime := rt.Init()
- defer runtime.Cleanup()
- // Set up a node manager.
- name, nmCleanup := startNodeManager()
- defer nmCleanup()
- invokeCallback(name)
- // Wait until shutdown.
- <-signals.ShutdownOnSignals()
- blackbox.WaitForEOFOnStdin()
- }
-}
-
-func spawnNodeManager(t *testing.T, mtName string, idFile string) *blackbox.Child {
- root := filepath.Join(os.TempDir(), "noded")
- child := blackbox.HelperCommand(t, "nodeManager")
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, "NAMESPACE_ROOT", mtName)
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, "VEYRON_IDENTITY", idFile)
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, impl.OriginEnv, "ar")
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, impl.RootEnv, root)
- child.Cmd.Env = exec.Setenv(child.Cmd.Env, testEnv, "setup")
- if err := child.Cmd.Start(); err != nil {
- t.Fatalf("Start() failed: %v", err)
- }
- return child
-}
-
-func startApplicationRepository() (string, func()) {
- server, err := rt.R().NewServer()
- if err != nil {
- vlog.Fatalf("NewServer() failed: %v", err)
- }
- dispatcher := ipc.SoloDispatcher(repository.NewServerApplication(&arInvoker{firstInvocation: true}), nil)
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- vlog.VI(1).Infof("Application repository running at endpoint: %s", endpoint)
- name := "ar"
- if err := server.Serve(name, dispatcher); err != nil {
- vlog.Fatalf("Serve(%v) failed: %v", name, err)
- }
- return name, func() {
- if err := server.Stop(); err != nil {
- vlog.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
-func startBinaryRepository() (string, func()) {
- server, err := rt.R().NewServer()
- if err != nil {
- vlog.Fatalf("NewServer() failed: %v", err)
- }
- dispatcher := ipc.SoloDispatcher(repository.NewServerBinary(&crInvoker{}), nil)
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- vlog.VI(1).Infof("Binary repository running at endpoint: %s", endpoint)
- name := "cr"
- if err := server.Serve(name, dispatcher); err != nil {
- vlog.Fatalf("Serve(%v) failed: %v", name, err)
- }
- return name, func() {
- if err := server.Stop(); err != nil {
- vlog.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
-func startMountTable(t *testing.T) (string, func()) {
- server, err := rt.R().NewServer(veyron2.ServesMountTableOpt(true))
- if err != nil {
- t.Fatalf("NewServer() failed: %v", err)
- }
- dispatcher, err := mtlib.NewMountTable("")
- if err != nil {
- t.Fatalf("NewMountTable() failed: %v", err)
- }
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- t.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- if err := server.Serve("", dispatcher); err != nil {
- t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
- }
- name := naming.JoinAddressName(endpoint.String(), "")
- vlog.VI(1).Infof("Mount table name: %v", name)
- return name, func() {
- if err := server.Stop(); err != nil {
- t.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
-func startNodeManager() (string, func()) {
- server, err := rt.R().NewServer()
- if err != nil {
- vlog.Fatalf("NewServer() failed: %v", err)
- }
- protocol, hostname := "tcp", "localhost:0"
- endpoint, err := server.Listen(protocol, hostname)
- if err != nil {
- vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
- }
- envelope := &application.Envelope{}
- name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), ""))
- vlog.VI(0).Infof("Node manager name: %v", name)
- // TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
- // command-line flags in tests are supported.
- dispatcher := impl.NewDispatcher(nil, envelope, name, os.Getenv(impl.PreviousEnv))
- publishAs := "nm"
- if err := server.Serve(publishAs, dispatcher); err != nil {
- vlog.Fatalf("Serve(%v) failed: %v", publishAs, err)
- }
- fmt.Printf("ready\n")
- return name, func() {
- if err := server.Stop(); err != nil {
- vlog.Fatalf("Stop() failed: %v", err)
- }
- }
-}
-
+// TestHelperProcess is blackbox boilerplate.
func TestHelperProcess(t *testing.T) {
blackbox.HelperProcess(t)
}
-// TestUpdate checks that the node manager Update() method works as
-// expected. To that end, this test spawns a new process that invokes
-// the nodeManager() method. The behavior of this method depends on
-// the value of the VEYRON_NM_TEST environment variable:
-//
-// 1) Initially, the value of VEYRON_NM_TEST is set to be "setup",
-// which prompts the nodeManager() method to setup a new workspace
-// that mimics the structure used for storing the node manager
-// binary. The method then sets VEYRON_NM_TEST to "parent" and
-// restarts itself using syscall.Exec(), effectively becoming the
-// process described next.
-//
-// 2) The "parent" branch sets up a mock application and binary
-// repository and a node manager that is pointed to the mock
-// application repository for updates. When all three services start,
-// the TestUpdate() method is notified and it proceeds to invoke
-// Update() on the node manager. This in turn results in the node
-// manager downloading an application envelope from the mock
-// application repository and a binary from the mock binary
-// repository. These are identical to the application envelope of the
-// "parent" node manager, except for the VEYRON_NM_TEST variable,
-// which is set to "child". The Update() method then spawns the child
-// node manager, checks that it is a valid node manager, and
-// terminates.
-//
-// 3) The "child" branch sets up a node manager and then calls back to
-// the "parent" node manager. This prompts the parent node manager to
-// invoke the Revert() method on the child node manager, which
-// terminates the child.
-func TestUpdate(t *testing.T) {
- // Set up a mount table.
- runtime := rt.Init()
- mtName, mtCleanup := startMountTable(t)
- defer mtCleanup()
- ns := runtime.Namespace()
- // The local, client-side Namespace is now relative to the
- // MountTable server started above.
- ns.SetRoots(mtName)
- // Spawn a node manager with an identity blessed by the MountTable's
- // identity under the name "test", and obtain its address.
- //
- // TODO(ataly): Eventually we want to use the same identity the node
- // manager would have if it was running in production.
- idFile := security.SaveIdentityToFile(security.NewBlessedIdentity(runtime.Identity(), "test"))
- defer os.Remove(idFile)
- child := spawnNodeManager(t, mtName, idFile)
- defer child.Cleanup()
- child.Expect("ready")
- ctx, name := runtime.NewContext(), naming.Join(mtName, "nm")
- results, err := ns.Resolve(ctx, name)
+func init() {
+ // All the tests and the subprocesses they start require a runtime; so just
+ // create it here.
+ rt.Init()
+
+ blackbox.CommandTable["nodeManager"] = nodeManager
+ blackbox.CommandTable["execScript"] = execScript
+}
+
+// execScript launches the script passed as argument.
+func execScript(args []string) {
+ if want, got := 1, len(args); want != got {
+ vlog.Fatalf("execScript expected %d arguments, got %d instead", want, got)
+ }
+ script := args[0]
+ env := []string{}
+ if os.Getenv("PAUSE_BEFORE_STOP") == "1" {
+ env = append(env, "PAUSE_BEFORE_STOP=1")
+ }
+ cmd := goexec.Cmd{
+ Path: script,
+ Env: env,
+ Stderr: os.Stderr,
+ Stdout: os.Stdout,
+ }
+ go func() {
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ vlog.Fatalf("Failed to get stdin pipe: %v", err)
+ }
+ blackbox.WaitForEOFOnStdin()
+ stdin.Close()
+ }()
+ if err := cmd.Run(); err != nil {
+ vlog.Fatalf("Run cmd %v failed: %v", cmd, err)
+ }
+}
+
+// nodeManager sets up a node manager server. It accepts the name to publish
+// the server under as an argument. Additional arguments can optionally specify
+// node manager config settings.
+func nodeManager(args []string) {
+ if len(args) == 0 {
+ vlog.Fatalf("nodeManager expected at least an argument")
+ }
+ publishName := args[0]
+
+ defer fmt.Printf("%v terminating\n", publishName)
+ defer rt.R().Cleanup()
+ server, endpoint := newServer()
+ defer server.Stop()
+ name := naming.MakeTerminal(naming.JoinAddressName(endpoint, ""))
+ vlog.VI(1).Infof("Node manager name: %v", name)
+
+ // Satisfy the contract described in doc.go by passing the config state
+ // through to the node manager dispatcher constructor.
+ configState, err := config.Load()
if err != nil {
- t.Fatalf("Resolve(%v) failed: %v", name, err)
+ vlog.Fatalf("Failed to decode config state: %v", err)
}
- if expected, got := 1, len(results); expected != got {
- t.Fatalf("Unexpected number of results: expected %d, got %d", expected, got)
+ configState.Name = name
+
+ // This exemplifies how to override or set specific config fields, if,
+ // for example, the node manager is invoked 'by hand' instead of via a
+ // script prepared by a previous version of the node manager.
+ if len(args) > 1 {
+ if want, got := 3, len(args)-1; want != got {
+ vlog.Fatalf("expected %d additional arguments, got %d instead", want, got)
+ }
+ configState.Root, configState.Origin, configState.CurrentLink = args[1], args[2], args[3]
}
- // First invocation will cause app repository mock to return a bogus
- // app title and hence the update should fail.
- invokeUpdate(t, name, true)
- invokeUpdate(t, name, false)
- child.Expect("ready")
+
+ dispatcher, err := impl.NewDispatcher(nil, configState)
+ if err != nil {
+ vlog.Fatalf("Failed to create node manager dispatcher: %v", err)
+ }
+ if err := server.Serve(publishName, dispatcher); err != nil {
+ vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
+ }
+
+ impl.InvokeCallback(name)
+
+ fmt.Println("ready")
+ <-signals.ShutdownOnSignals()
+ if os.Getenv("PAUSE_BEFORE_STOP") == "1" {
+ blackbox.WaitForEOFOnStdin()
+ }
+}
+
+// generateScript is very similar in behavior to its namesake in invoker.go.
+// However, we chose to re-implement it here for two reasons: (1) avoid making
+// generateScript public; and (2) how the test choses to invoke the node manager
+// subprocess the first time should be independent of how node manager
+// implementation sets up its updated versions.
+func generateScript(t *testing.T, root string, cmd *goexec.Cmd) string {
+ output := "#!/bin/bash\n"
+ output += strings.Join(config.QuoteEnv(cmd.Env), " ") + " "
+ output += cmd.Args[0] + " " + strings.Join(cmd.Args[1:], " ")
+ if err := os.MkdirAll(filepath.Join(root, "factory"), 0755); err != nil {
+ t.Fatalf("MkdirAll failed: %v", err)
+ }
+ // Why pigeons? To show that the name we choose for the initial script
+ // doesn't matter and in particular is independent of how node manager
+ // names its updated version scripts (noded.sh).
+ path := filepath.Join(root, "factory", "pigeons.sh")
+ if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
+ t.Fatalf("WriteFile(%v) failed: %v", path, err)
+ }
+ return path
+}
+
+// envelopeFromCmd returns an envelope that describes the given command object.
+func envelopeFromCmd(cmd *goexec.Cmd) *application.Envelope {
+ return &application.Envelope{
+ Title: application.NodeManagerTitle,
+ Args: cmd.Args[1:],
+ Env: cmd.Env,
+ Binary: "br",
+ }
+}
+
+// TestUpdateAndRevert makes the node manager go through the motions of updating
+// itself to newer versions (twice), and reverting itself back (twice). It also
+// checks that update and revert fail when they're supposed to. The initial
+// node manager is started 'by hand' via a blackbox command. Further versions
+// are started through the soft link that the node manager itself updates.
+func TestUpdateAndRevert(t *testing.T) {
+ // Set up mount table, application, and binary repositories.
+ defer setupLocalNamespace(t)()
+ envelope, cleanup := startApplicationRepository()
+ defer cleanup()
+ defer startBinaryRepository()()
+
+ // This is the local filesystem location that the node manager is told
+ // to use.
+ root := filepath.Join(os.TempDir(), "nodemanager")
+ defer os.RemoveAll(root)
+
+ // Current link does not have to live in the root dir.
+ currLink := filepath.Join(os.TempDir(), "testcurrent")
+ defer os.Remove(currLink)
+
+ // Set up the initial version of the node manager, the so-called
+ // "factory" version.
+ nm := blackbox.HelperCommand(t, "nodeManager", "factoryNM", root, "ar", currLink)
+ defer setupChildCommand(nm)()
+
+ // This is the script that we'll point the current link to initially.
+ scriptPathFactory := generateScript(t, root, nm.Cmd)
+
+ if err := os.Symlink(scriptPathFactory, currLink); err != nil {
+ t.Fatalf("Symlink(%q, %q) failed: %v", scriptPathFactory, currLink, err)
+ }
+ // We instruct the initial node manager that we run to pause before
+ // stopping its service, so that we get a chance to verify that
+ // attempting an update while another one is ongoing will fail.
+ nm.Cmd.Env = exec.Setenv(nm.Cmd.Env, "PAUSE_BEFORE_STOP", "1")
+
+ resolveExpectError(t, "factoryNM", verror.NotFound) // Ensure a clean slate.
+
+ // Start the node manager -- we use the blackbox-generated command to
+ // start it. We could have also used the scriptPathFactory to start it, but
+ // this demonstrates that the initial node manager could be started by
+ // hand as long as the right initial configuration is passed into the
+ // node manager implementation.
+ if err := nm.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer := nm.Cleanup
+ defer func() {
+ if deferrer != nil {
+ deferrer()
+ }
+ }()
+ nm.Expect("ready")
+ resolve(t, "factoryNM") // Verify the node manager has published itself.
+
+ // Simulate an invalid envelope in the application repository.
+ *envelope = *envelopeFromCmd(nm.Cmd)
+ envelope.Title = "bogus"
+ updateExpectError(t, "factoryNM", verror.BadArg) // Incorrect title.
+ revertExpectError(t, "factoryNM", verror.NotFound) // No previous version available.
+
+ // Set up a second version of the node manager. We use the blackbox
+ // command solely to collect the args and env we need to provide the
+ // application repository with an envelope that will actually run the
+ // node manager subcommand. The blackbox command is never started by
+ // hand -- instead, the information in the envelope will be used by the
+ // node manager to stage the next version.
+ nmV2 := blackbox.HelperCommand(t, "nodeManager", "v2NM")
+ defer setupChildCommand(nmV2)()
+ *envelope = *envelopeFromCmd(nmV2.Cmd)
+ update(t, "factoryNM")
+
+ // Current link should have been updated to point to v2.
+ evalLink := func() string {
+ path, err := filepath.EvalSymlinks(currLink)
+ if err != nil {
+ t.Fatalf("EvalSymlinks(%v) failed: %v", currLink, err)
+ }
+ return path
+ }
+ scriptPathV2 := evalLink()
+ if scriptPathFactory == scriptPathV2 {
+ t.Errorf("current link didn't change")
+ }
+
+ // This is from the child node manager started by the node manager
+ // as an update test.
+ nm.Expect("ready")
+ nm.Expect("v2NM terminating")
+
+ updateExpectError(t, "factoryNM", verror.Exists) // Update already in progress.
+
+ nm.CloseStdin()
+ nm.Expect("factoryNM terminating")
+ deferrer = nil
+ nm.Cleanup()
+
+ // A successful update means the node manager has stopped itself. We
+ // relaunch it from the current link.
+ runNM := blackbox.HelperCommand(t, "execScript", currLink)
+ resolveExpectError(t, "v2NM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "v2NM") // Current link should have been launching v2.
+
+ // Try issuing an update without changing the envelope in the application
+ // repository: this should fail, and current link should be unchanged.
+ updateExpectError(t, "v2NM", verror.NotFound)
+ if evalLink() != scriptPathV2 {
+ t.Errorf("script changed")
+ }
+
+ // Create a third version of the node manager and issue an update.
+ nmV3 := blackbox.HelperCommand(t, "nodeManager", "v3NM")
+ defer setupChildCommand(nmV3)()
+ *envelope = *envelopeFromCmd(nmV3.Cmd)
+ update(t, "v2NM")
+
+ scriptPathV3 := evalLink()
+ if scriptPathV3 == scriptPathV2 {
+ t.Errorf("current link didn't change")
+ }
+
+ // This is from the child node manager started by the node manager
+ // as an update test.
+ runNM.Expect("ready")
+ // Both the parent and child node manager should terminate upon successful
+ // update.
+ runNM.ExpectSet([]string{"v3NM terminating", "v2NM terminating"})
+
+ deferrer = nil
+ runNM.Cleanup()
+
+ // Re-lanuch the node manager from current link.
+ runNM = blackbox.HelperCommand(t, "execScript", currLink)
+ // We instruct the node manager to pause before stopping its server, so
+ // that we can verify that a second revert fails while a revert is in
+ // progress.
+ runNM.Cmd.Env = exec.Setenv(nm.Cmd.Env, "PAUSE_BEFORE_STOP", "1")
+ resolveExpectError(t, "v3NM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "v3NM") // Current link should have been launching v3.
+
+ // Revert the node manager to its previous version (v2).
+ revert(t, "v3NM")
+ revertExpectError(t, "v3NM", verror.Exists) // Revert already in progress.
+ nm.CloseStdin()
+ runNM.Expect("v3NM terminating")
+ if evalLink() != scriptPathV2 {
+ t.Errorf("current link didn't change")
+ }
+ deferrer = nil
+ runNM.Cleanup()
+
+ // Re-launch the node manager from current link.
+ runNM = blackbox.HelperCommand(t, "execScript", currLink)
+ resolveExpectError(t, "v2NM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "v2NM") // Current link should have been launching v2.
+
+ // Revert the node manager to its previous version (factory).
+ revert(t, "v2NM")
+ runNM.Expect("v2NM terminating")
+ if evalLink() != scriptPathFactory {
+ t.Errorf("current link didn't change")
+ }
+ deferrer = nil
+ runNM.Cleanup()
+
+ // Re-launch the node manager from current link.
+ runNM = blackbox.HelperCommand(t, "execScript", currLink)
+ resolveExpectError(t, "factoryNM", verror.NotFound) // Ensure a clean slate.
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ deferrer = runNM.Cleanup
+ runNM.Expect("ready")
+ resolve(t, "factoryNM") // Current link should have been launching factory version.
}
diff --git a/services/mgmt/node/impl/invoker.go b/services/mgmt/node/impl/invoker.go
index ed450c6..b7b4611 100644
--- a/services/mgmt/node/impl/invoker.go
+++ b/services/mgmt/node/impl/invoker.go
@@ -1,11 +1,9 @@
package impl
-// The implementation of the node manager expects that the node
-// manager installations are all organized in the following directory
-// structure:
+// The implementation of the node manager expects that the node manager
+// installations are all organized in the following directory structure:
//
// VEYRON_NM_ROOT/
-// current # symlink to one of the workspaces
// workspace-1/
// noded - the node manager binary
// noded.sh - a shell script to start the binary
@@ -14,15 +12,14 @@
// noded - the node manager binary
// noded.sh - a shell script to start the binary
//
-// The node manager is always expected to be started through
-// VEYRON_NM_ROOT/current/noded.sh, which is monitored by an init
-// daemon. This provides for simple and robust updates.
+// 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
+// provides for simple and robust updates.
//
-// To update the node manager to a newer version, a new workspace is
-// created and the "current" symlink is updated
-// accordingly. Similarly, to revert the node manager to a previous
-// version, all that is required is to update the symlink to point to
-// the previous workspace.
+// To update the node manager to a newer version, a new workspace is created and
+// the symlink is updated to the new noded.sh script. Similarly, to revert the
+// node manager to a previous version, all that is required is to update the
+// symlink to point to the previous noded.sh script.
import (
"bytes"
@@ -43,6 +40,7 @@
"veyron/lib/config"
blib "veyron/services/mgmt/lib/binary"
vexec "veyron/services/mgmt/lib/exec"
+ iconfig "veyron/services/mgmt/node/config"
"veyron/services/mgmt/profile"
"veyron2/ipc"
@@ -58,24 +56,15 @@
"veyron2/vlog"
)
-const CurrentWorkspace = "current"
-
-// state wraps state shared between different node manager
+// internalState wraps state shared between different node manager
// invocations.
-type state struct {
+type internalState struct {
// channels maps callback identifiers to channels that are used to
// communicate information from child processes.
channels map[string]chan string
// channelsMutex is a lock for coordinating concurrent access to
// <channels>.
channelsMutex *sync.Mutex
- // envelope is the node manager application envelope.
- envelope *application.Envelope
- // name is the node manager name.
- name string
- // previous holds the local path to the previous version of the node
- // manager.
- previous string
// updating is a flag that records whether this instance of node
// manager is being updated.
updating bool
@@ -86,7 +75,11 @@
// invoker holds the state of a node manager invocation.
type invoker struct {
- state *state
+ // internal holds the node manager's internal state that persists across
+ // RPC requests.
+ internal *internalState
+ // config holds the node manager's (immutable) configuration state.
+ config *iconfig.State
// suffix is the suffix of the current invocation that is assumed to
// be used as a relative object name to identify an application,
// installation, or instance.
@@ -100,16 +93,9 @@
errOperationFailed = verror.Internalf("operation failed")
errUpdateInProgress = verror.Existsf("update in progress")
errIncompatibleUpdate = verror.BadArgf("update failed: mismatching app title")
+ errUpdateNoOp = verror.NotFoundf("no different version available")
)
-// NewInvoker is the invoker factory.
-func NewInvoker(state *state, suffix string) *invoker {
- return &invoker{
- state: state,
- suffix: suffix,
- }
-}
-
// NODE INTERFACE IMPLEMENTATION
// computeNodeProfile generates a description of the runtime
@@ -290,7 +276,7 @@
}
func (i *invoker) Describe(call ipc.ServerContext) (node.Description, error) {
- vlog.VI(0).Infof("%v.Describe()", i.suffix)
+ vlog.VI(1).Infof("%v.Describe()", i.suffix)
empty := node.Description{}
nodeProfile, err := i.computeNodeProfile()
if err != nil {
@@ -305,7 +291,7 @@
}
func (i *invoker) IsRunnable(call ipc.ServerContext, description binary.Description) (bool, error) {
- vlog.VI(0).Infof("%v.IsRunnable(%v)", i.suffix, description)
+ vlog.VI(1).Infof("%v.IsRunnable(%v)", i.suffix, description)
nodeProfile, err := i.computeNodeProfile()
if err != nil {
return false, err
@@ -323,7 +309,7 @@
}
func (i *invoker) Reset(call ipc.ServerContext, deadline uint64) error {
- vlog.VI(0).Infof("%v.Reset(%v)", i.suffix, deadline)
+ vlog.VI(1).Infof("%v.Reset(%v)", i.suffix, deadline)
// TODO(jsimsa): Implement.
return nil
}
@@ -377,16 +363,14 @@
// TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
// command-line flags in tests are supported.
-func generateScript(workspace string, envelope *application.Envelope) error {
+func generateScript(workspace string, configSettings []string, envelope *application.Envelope) error {
path, err := filepath.EvalSymlinks(os.Args[0])
if err != nil {
vlog.Errorf("EvalSymlinks(%v) failed: %v", os.Args[0], err)
return errOperationFailed
}
output := "#!/bin/bash\n"
- output += BinaryEnv + "=" + envelope.Binary + " "
- output += PreviousEnv + "=" + filepath.Dir(path) + " "
- output += strings.Join(envelope.Env, " ") + " "
+ output += strings.Join(iconfig.QuoteEnv(append(envelope.Env, configSettings...)), " ") + " "
output += filepath.Join(workspace, "noded") + " "
output += strings.Join(envelope.Args, " ")
path = filepath.Join(workspace, "noded.sh")
@@ -397,25 +381,25 @@
return nil
}
-// getCurrentFileInfo returns the os.FileInfo for both the symbolic
-// link $VEYRON_NM_ROOT/current and the workspace this link points to.
-func getCurrentFileInfo() (os.FileInfo, os.FileInfo, error) {
- path := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace)
+// getCurrentFileInfo returns the os.FileInfo for both the symbolic link
+// CurrentLink, and the node script in the workspace that this link points to.
+func (i *invoker) getCurrentFileInfo() (os.FileInfo, string, error) {
+ path := i.config.CurrentLink
link, err := os.Lstat(path)
if err != nil {
vlog.Errorf("Lstat(%v) failed: %v", path, err)
- return nil, nil, err
+ return nil, "", err
}
- workspace, err := os.Stat(path)
+ scriptPath, err := filepath.EvalSymlinks(path)
if err != nil {
- vlog.Errorf("Stat(%v) failed: %v", path, err)
- return nil, nil, err
+ vlog.Errorf("EvalSymlinks(%v) failed: %v", path, err)
+ return nil, "", err
}
- return link, workspace, nil
+ return link, scriptPath, nil
}
-func updateLink(workspace string) error {
- link := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace)
+func (i *invoker) updateLink(newScript string) error {
+ link := i.config.CurrentLink
newLink := link + ".new"
fi, err := os.Lstat(newLink)
if err == nil {
@@ -424,8 +408,8 @@
return errOperationFailed
}
}
- if err := os.Symlink(workspace, newLink); err != nil {
- vlog.Errorf("Symlink(%v, %v) failed: %v", workspace, newLink, err)
+ if err := os.Symlink(newScript, newLink); err != nil {
+ vlog.Errorf("Symlink(%v, %v) failed: %v", newScript, newLink, err)
return errOperationFailed
}
if err := os.Rename(newLink, link); err != nil {
@@ -436,13 +420,13 @@
}
func (i *invoker) registerCallback(id string, channel chan string) {
- i.state.channelsMutex.Lock()
- defer i.state.channelsMutex.Unlock()
- i.state.channels[id] = channel
+ i.internal.channelsMutex.Lock()
+ defer i.internal.channelsMutex.Unlock()
+ i.internal.channels[id] = channel
}
func (i *invoker) revertNodeManager() error {
- if err := updateLink(i.state.previous); err != nil {
+ if err := i.updateLink(i.config.Previous); err != nil {
return err
}
rt.R().Stop()
@@ -457,7 +441,7 @@
// Setup up the child process callback.
id := fmt.Sprintf("%d", rand.Int())
cfg := config.New()
- cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.JoinAddressName(i.state.name, id))
+ cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.MakeTerminal(naming.Join(i.config.Name, id)))
handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg})
callbackChan := make(chan string)
i.registerCallback(id, callbackChan)
@@ -478,18 +462,18 @@
}
// Wait for the child process to invoke the Callback().
select {
- case address := <-callbackChan:
+ case childName := <-callbackChan:
// Check that invoking Update() succeeds.
- address = naming.JoinAddressName(address, "nm")
- nmClient, err := node.BindNode(address)
+ childName = naming.MakeTerminal(naming.Join(childName, "nm"))
+ nmClient, err := node.BindNode(childName)
if err != nil {
- vlog.Errorf("BindNode(%v) failed: %v", address, err)
+ vlog.Errorf("BindNode(%v) failed: %v", childName, err)
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
}
return errOperationFailed
}
- linkOld, workspaceOld, err := getCurrentFileInfo()
+ linkOld, pathOld, err := i.getCurrentFileInfo()
if err != nil {
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
@@ -498,7 +482,7 @@
}
// Since the resolution of mtime for files is seconds,
// the test sleeps for a second to make sure it can
- // check whether the "current" symlink is updated.
+ // check whether the current symlink is updated.
time.Sleep(time.Second)
if err := nmClient.Revert(rt.R().NewContext()); err != nil {
if err := handle.Clean(); err != nil {
@@ -506,23 +490,23 @@
}
return errOperationFailed
}
- linkNew, workspaceNew, err := getCurrentFileInfo()
+ linkNew, pathNew, err := i.getCurrentFileInfo()
if err != nil {
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
}
return errOperationFailed
}
- // Check that the new node manager updated the symbolic link
- // $VEYRON_NM_ROOT/current.
+ // Check that the new node manager updated the current symbolic
+ // link.
if !linkOld.ModTime().Before(linkNew.ModTime()) {
vlog.Errorf("new node manager test failed")
return errOperationFailed
}
- // Check that the symbolic link $VEYRON_NM_ROOT/current points to
- // the same workspace.
- if workspaceOld.Name() != workspaceNew.Name() {
- updateLink(workspaceOld.Name())
+ // Ensure that the current symbolic link points to the same
+ // script.
+ if pathNew != pathOld {
+ i.updateLink(pathOld)
vlog.Errorf("new node manager test failed")
return errOperationFailed
}
@@ -537,144 +521,162 @@
}
func (i *invoker) unregisterCallback(id string) {
- i.state.channelsMutex.Lock()
- defer i.state.channelsMutex.Unlock()
- delete(i.state.channels, id)
+ i.internal.channelsMutex.Lock()
+ defer i.internal.channelsMutex.Unlock()
+ delete(i.internal.channels, id)
}
func (i *invoker) updateNodeManager() error {
- envelope, err := fetchEnvelope(os.Getenv(OriginEnv))
+ if len(i.config.Origin) == 0 {
+ return errUpdateNoOp
+ }
+ envelope, err := fetchEnvelope(i.config.Origin)
if err != nil {
return err
}
if envelope.Title != application.NodeManagerTitle {
return errIncompatibleUpdate
}
- if !reflect.DeepEqual(envelope, i.state.envelope) {
- // Create new workspace.
- workspace := filepath.Join(os.Getenv(RootEnv), fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
- perm := os.FileMode(0755)
- if err := os.MkdirAll(workspace, perm); err != nil {
- vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
- return errOperationFailed
- }
- // Populate the new workspace with a node manager binary.
- if err := generateBinary(workspace, envelope, envelope.Binary != i.state.envelope.Binary); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- // Populate the new workspace with a node manager script.
- if err := generateScript(workspace, envelope); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- if err := i.testNodeManager(workspace, envelope); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- // If the binary has changed, update the node manager symlink.
- if err := updateLink(workspace); err != nil {
- if err := os.RemoveAll(workspace); err != nil {
- vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
- }
- return err
- }
- rt.R().Stop()
+ if i.config.Envelope != nil && reflect.DeepEqual(envelope, i.config.Envelope) {
+ return errUpdateNoOp
}
+ // Create new workspace.
+ workspace := filepath.Join(i.config.Root, fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
+ perm := os.FileMode(0755)
+ if err := os.MkdirAll(workspace, perm); err != nil {
+ vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
+ return errOperationFailed
+ }
+ // Populate the new workspace with a node manager binary.
+ // TODO(caprita): match identical binaries on binary metadata
+ // rather than binary object name.
+ sameBinary := i.config.Envelope != nil && envelope.Binary == i.config.Envelope.Binary
+ if err := generateBinary(workspace, envelope, !sameBinary); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ // Populate the new workspace with a node manager script.
+ configSettings, err := i.config.Save(envelope)
+ if err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return errOperationFailed
+ }
+ if err := generateScript(workspace, configSettings, envelope); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ if err := i.testNodeManager(workspace, envelope); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ // If the binary has changed, update the node manager symlink.
+ if err := i.updateLink(filepath.Join(workspace, "noded.sh")); err != nil {
+ if err := os.RemoveAll(workspace); err != nil {
+ vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+ }
+ return err
+ }
+ rt.R().Stop()
return nil
}
func (i *invoker) Install(call ipc.ServerContext, von string) (string, error) {
- vlog.VI(0).Infof("%v.Install(%q)", i.suffix, von)
+ vlog.VI(1).Infof("%v.Install(%q)", i.suffix, von)
// TODO(jsimsa): Implement.
return "", nil
}
func (i *invoker) Refresh(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Refresh()", i.suffix)
+ vlog.VI(1).Infof("%v.Refresh()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Restart(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Restart()", i.suffix)
+ vlog.VI(1).Infof("%v.Restart()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Resume(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Resume()", i.suffix)
+ vlog.VI(1).Infof("%v.Resume()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Revert(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Revert()", i.suffix)
- if i.state.previous == "" {
- return errOperationFailed
+ vlog.VI(1).Infof("%v.Revert()", i.suffix)
+ if i.config.Previous == "" {
+ return errUpdateNoOp
}
- i.state.updatingMutex.Lock()
- if i.state.updating {
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Lock()
+ if i.internal.updating {
+ i.internal.updatingMutex.Unlock()
return errUpdateInProgress
} else {
- i.state.updating = true
+ i.internal.updating = true
}
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Unlock()
err := i.revertNodeManager()
- i.state.updatingMutex.Lock()
- i.state.updating = false
- i.state.updatingMutex.Unlock()
+ if err != nil {
+ i.internal.updatingMutex.Lock()
+ i.internal.updating = false
+ i.internal.updatingMutex.Unlock()
+ }
return err
}
func (i *invoker) Start(call ipc.ServerContext) ([]string, error) {
- vlog.VI(0).Infof("%v.Start()", i.suffix)
+ vlog.VI(1).Infof("%v.Start()", i.suffix)
// TODO(jsimsa): Implement.
return make([]string, 0), nil
}
func (i *invoker) Stop(call ipc.ServerContext, deadline uint64) error {
- vlog.VI(0).Infof("%v.Stop(%v)", i.suffix, deadline)
+ vlog.VI(1).Infof("%v.Stop(%v)", i.suffix, deadline)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Suspend(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Suspend()", i.suffix)
+ vlog.VI(1).Infof("%v.Suspend()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Uninstall(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Uninstall()", i.suffix)
+ vlog.VI(1).Infof("%v.Uninstall()", i.suffix)
// TODO(jsimsa): Implement.
return nil
}
func (i *invoker) Update(call ipc.ServerContext) error {
- vlog.VI(0).Infof("%v.Update()", i.suffix)
+ vlog.VI(1).Infof("%v.Update()", i.suffix)
switch {
case i.suffix == "nm":
// This branch attempts to update the node manager itself.
- i.state.updatingMutex.Lock()
- if i.state.updating {
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Lock()
+ if i.internal.updating {
+ i.internal.updatingMutex.Unlock()
return errUpdateInProgress
} else {
- i.state.updating = true
+ i.internal.updating = true
}
- i.state.updatingMutex.Unlock()
+ i.internal.updatingMutex.Unlock()
err := i.updateNodeManager()
- i.state.updatingMutex.Lock()
- i.state.updating = false
- i.state.updatingMutex.Unlock()
+ if err != nil {
+ i.internal.updatingMutex.Lock()
+ i.internal.updating = false
+ i.internal.updatingMutex.Unlock()
+ }
return err
case appsSuffix.MatchString(i.suffix):
// TODO(jsimsa): Implement.
@@ -686,7 +688,7 @@
}
func (i *invoker) UpdateTo(call ipc.ServerContext, von string) error {
- vlog.VI(0).Infof("%v.UpdateTo(%q)", i.suffix, von)
+ vlog.VI(1).Infof("%v.UpdateTo(%q)", i.suffix, von)
// TODO(jsimsa): Implement.
return nil
}
@@ -694,15 +696,15 @@
// CONFIG INTERFACE IMPLEMENTATION
func (i *invoker) Set(_ ipc.ServerContext, key, value string) error {
- vlog.VI(0).Infof("%v.Set(%v, %v)", i.suffix, key, value)
+ vlog.VI(1).Infof("%v.Set(%v, %v)", i.suffix, key, value)
// For now, only handle the child node manager name. We'll add handling
// for the child's app cycle manager name later on.
if key != mgmt.ChildNodeManagerConfigKey {
return nil
}
- i.state.channelsMutex.Lock()
- channel, ok := i.state.channels[i.suffix]
- i.state.channelsMutex.Unlock()
+ i.internal.channelsMutex.Lock()
+ channel, ok := i.internal.channels[i.suffix]
+ i.internal.channelsMutex.Unlock()
if !ok {
return errInvalidSuffix
}
diff --git a/services/mgmt/node/impl/mock_repo_test.go b/services/mgmt/node/impl/mock_repo_test.go
new file mode 100644
index 0000000..28eaf4a
--- /dev/null
+++ b/services/mgmt/node/impl/mock_repo_test.go
@@ -0,0 +1,130 @@
+package impl_test
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "errors"
+ "io"
+ "io/ioutil"
+ "os"
+
+ "veyron2/ipc"
+ "veyron2/services/mgmt/application"
+ "veyron2/services/mgmt/binary"
+ "veyron2/services/mgmt/repository"
+ "veyron2/vlog"
+)
+
+// arInvoker holds the state of an application repository invocation mock. The
+// mock returns the value of the wrapped envelope, which can be subsequently be
+// changed at any time. Client is responsible for synchronization if desired.
+type arInvoker struct {
+ envelope application.Envelope
+}
+
+// startApplicationRepository sets up a server running the application
+// repository. It returns a pointer to the envelope that the repository returns
+// to clients (so that it can be changed). It also returns a cleanup function.
+func startApplicationRepository() (*application.Envelope, func()) {
+ server, _ := newServer()
+ invoker := new(arInvoker)
+ dispatcher := ipc.SoloDispatcher(repository.NewServerApplication(invoker), nil)
+ name := "ar"
+ if err := server.Serve(name, dispatcher); err != nil {
+ vlog.Fatalf("Serve(%v) failed: %v", name, err)
+ }
+ return &invoker.envelope, func() {
+ if err := server.Stop(); err != nil {
+ vlog.Fatalf("Stop() failed: %v", err)
+ }
+ }
+}
+
+// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
+func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
+ vlog.VI(1).Infof("Match()")
+ return i.envelope, nil
+}
+
+// brInvoker holds the state of a binary repository invocation mock. It always
+// serves the current running binary.
+type brInvoker struct{}
+
+// startBinaryRepository sets up a server running the binary repository and
+// returns a cleanup function.
+func startBinaryRepository() func() {
+ server, _ := newServer()
+ dispatcher := ipc.SoloDispatcher(repository.NewServerBinary(new(brInvoker)), nil)
+ name := "br"
+ if err := server.Serve(name, dispatcher); err != nil {
+ vlog.Fatalf("Serve(%q) failed: %v", name, err)
+ }
+ return func() {
+ if err := server.Stop(); err != nil {
+ vlog.Fatalf("Stop() failed: %v", err)
+ }
+ }
+}
+
+// BINARY REPOSITORY INTERFACE IMPLEMENTATION
+
+func (*brInvoker) Create(ipc.ServerContext, int32) error {
+ vlog.VI(1).Infof("Create()")
+ return nil
+}
+
+func (i *brInvoker) Delete(ipc.ServerContext) error {
+ vlog.VI(1).Infof("Delete()")
+ return nil
+}
+
+var errOperationFailed = errors.New("operation failed")
+
+func (i *brInvoker) Download(_ ipc.ServerContext, _ int32, stream repository.BinaryServiceDownloadStream) error {
+ vlog.VI(1).Infof("Download()")
+ file, err := os.Open(os.Args[0])
+ if err != nil {
+ vlog.Errorf("Open() failed: %v", err)
+ return errOperationFailed
+ }
+ defer file.Close()
+ bufferLength := 4096
+ buffer := make([]byte, bufferLength)
+ for {
+ n, err := file.Read(buffer)
+ switch err {
+ case io.EOF:
+ return nil
+ case nil:
+ if err := stream.Send(buffer[:n]); err != nil {
+ vlog.Errorf("Send() failed: %v", err)
+ return errOperationFailed
+ }
+ default:
+ vlog.Errorf("Read() failed: %v", err)
+ return errOperationFailed
+ }
+ }
+}
+
+func (*brInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
+ vlog.VI(1).Infof("DownloadURL()")
+ return "", 0, nil
+}
+
+func (*brInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, error) {
+ vlog.VI(1).Infof("Stat()")
+ h := md5.New()
+ bytes, err := ioutil.ReadFile(os.Args[0])
+ if err != nil {
+ return []binary.PartInfo{}, errOperationFailed
+ }
+ h.Write(bytes)
+ part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
+ return []binary.PartInfo{part}, nil
+}
+
+func (i *brInvoker) Upload(ipc.ServerContext, int32, repository.BinaryServiceUploadStream) error {
+ vlog.VI(1).Infof("Upload()")
+ return nil
+}
diff --git a/services/mgmt/node/impl/util_test.go b/services/mgmt/node/impl/util_test.go
new file mode 100644
index 0000000..3388b63
--- /dev/null
+++ b/services/mgmt/node/impl/util_test.go
@@ -0,0 +1,155 @@
+package impl_test
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ mtlib "veyron/services/mounttable/lib"
+
+ "veyron/lib/testutil/blackbox"
+ "veyron/lib/testutil/security"
+ "veyron/services/mgmt/lib/exec"
+
+ "veyron2"
+ "veyron2/ipc"
+ "veyron2/naming"
+ "veyron2/rt"
+ "veyron2/services/mgmt/node"
+ "veyron2/verror"
+ "veyron2/vlog"
+)
+
+// TODO(caprita): I've had to write one too many of these, let's move it to some
+// central utility library.
+
+// setupLocalNamespace sets up a mounttable and sets the local namespace root
+// to point to it. Returns a cleanup function.
+func setupLocalNamespace(t *testing.T) func() {
+ server, err := rt.R().NewServer(veyron2.ServesMountTableOpt(true))
+ if err != nil {
+ t.Fatalf("NewServer() failed: %v", err)
+ }
+ dispatcher, err := mtlib.NewMountTable("")
+ if err != nil {
+ t.Fatalf("NewMountTable() failed: %v", err)
+ }
+ protocol, hostname := "tcp", "localhost:0"
+ endpoint, err := server.Listen(protocol, hostname)
+ if err != nil {
+ t.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
+ }
+ if err := server.Serve("", dispatcher); err != nil {
+ t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
+ }
+ name := naming.JoinAddressName(endpoint.String(), "")
+ vlog.VI(1).Infof("Mount table object name: %v", name)
+ ns := rt.R().Namespace()
+ // Make the runtime's namespace rooted at the MountTable server started
+ // above.
+ ns.SetRoots(name)
+ return func() {
+ if err := server.Stop(); err != nil {
+ t.Fatalf("Stop() failed: %v", err)
+ }
+ // The runtime outlives the particular test case that invokes
+ // setupLocalNamespace. It's good practice to reset the
+ // runtime's state before the next test uses it.
+ ns.SetRoots()
+ }
+}
+
+// TODO(caprita): Move this setup into the blackbox lib.
+
+// setupChildCommand configures the child to use the right mounttable root
+// and identity. It returns a cleanup function.
+func setupChildCommand(child *blackbox.Child) func() {
+ cmd := child.Cmd
+ for i, root := range rt.R().Namespace().Roots() {
+ cmd.Env = exec.Setenv(cmd.Env, fmt.Sprintf("NAMESPACE_ROOT%d", i), root)
+ }
+ idFile := security.SaveIdentityToFile(security.NewBlessedIdentity(rt.R().Identity(), "test"))
+ cmd.Env = exec.Setenv(cmd.Env, "VEYRON_IDENTITY", idFile)
+ return func() {
+ os.Remove(idFile)
+ }
+}
+
+func newServer() (ipc.Server, string) {
+ server, err := rt.R().NewServer()
+ if err != nil {
+ vlog.Fatalf("NewServer() failed: %v", err)
+ }
+ protocol, hostname := "tcp", "localhost:0"
+ endpoint, err := server.Listen(protocol, hostname)
+ if err != nil {
+ vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
+ }
+ return server, endpoint.String()
+}
+
+// resolveExpectError verifies that the given name is not in the mounttable.
+func resolveExpectError(t *testing.T, name string, errID verror.ID) {
+ if results, err := rt.R().Namespace().Resolve(rt.R().NewContext(), name); err == nil {
+ t.Errorf("Resolve(%v) succeeded with results %v when it was expected to fail", name, results)
+ } else if !verror.Is(err, errID) {
+ t.Errorf("Resolve(%v) failed with error %v, expected error ID %v", err, errID)
+ }
+}
+
+// resolve looks up the given name in the mounttable.
+func resolve(t *testing.T, name string) string {
+ results, err := rt.R().Namespace().Resolve(rt.R().NewContext(), name)
+ if err != nil {
+ t.Fatalf("Resolve(%v) failed: %v", name, err)
+ }
+ if want, got := 1, len(results); want != got {
+ t.Fatalf("Resolve(%v) expected %d result(s), got %d instead", name, want, got)
+ }
+ return results[0]
+}
+
+// The following set of functions are convenience wrappers around Update and
+// Revert.
+
+func updateExpectError(t *testing.T, name string, errID verror.ID) {
+ if err := invokeUpdate(t, name); !verror.Is(err, errID) {
+ t.Errorf("Unexpected update error %v, expected error ID %v", err, errID)
+ }
+}
+
+func update(t *testing.T, name string) {
+ if err := invokeUpdate(t, name); err != nil {
+ t.Errorf("Update() failed: %v", err)
+ }
+}
+
+func invokeUpdate(t *testing.T, name string) error {
+ address := naming.Join(name, "nm")
+ stub, err := node.BindNode(address)
+ if err != nil {
+ t.Fatalf("BindNode(%v) failed: %v", address, err)
+ }
+ return stub.Update(rt.R().NewContext())
+}
+
+func revertExpectError(t *testing.T, name string, errID verror.ID) {
+ if err := invokeRevert(t, name); !verror.Is(err, errID) {
+ t.Errorf("Unexpected revert error %v, expected error ID %v", err, errID)
+ }
+}
+
+func revert(t *testing.T, name string) {
+ if err := invokeRevert(t, name); err != nil {
+ t.Errorf("Revert() failed: %v", err)
+ }
+}
+
+func invokeRevert(t *testing.T, name string) error {
+ address := naming.Join(name, "nm")
+ stub, err := node.BindNode(address)
+ if err != nil {
+ t.Fatalf("BindNode(%v) failed: %v", address, err)
+ }
+ return stub.Revert(rt.R().NewContext())
+}
diff --git a/services/mgmt/node/noded/main.go b/services/mgmt/node/noded/main.go
index 9deb603..1fe7ff4 100644
--- a/services/mgmt/node/noded/main.go
+++ b/services/mgmt/node/noded/main.go
@@ -2,30 +2,17 @@
import (
"flag"
- "os"
"veyron/lib/signals"
vflag "veyron/security/flag"
- "veyron/services/mgmt/lib/exec"
- "veyron/services/mgmt/node"
+ "veyron/services/mgmt/node/config"
"veyron/services/mgmt/node/impl"
- "veyron2/mgmt"
"veyron2/naming"
"veyron2/rt"
- "veyron2/services/mgmt/application"
"veyron2/vlog"
)
-func generateEnvelope() *application.Envelope {
- return &application.Envelope{
- Args: os.Args,
- Binary: os.Getenv(impl.BinaryEnv),
- Env: os.Environ(),
- Title: application.NodeManagerTitle,
- }
-}
-
func main() {
// TODO(rthellend): Remove the address and protocol flags when the config manager is working.
var address, protocol, publishAs string
@@ -33,9 +20,6 @@
flag.StringVar(&protocol, "protocol", "tcp", "network type to listen on")
flag.StringVar(&publishAs, "name", "", "name to publish the node manager at")
flag.Parse()
- if os.Getenv(impl.OriginEnv) == "" {
- vlog.Fatalf("Specify the node manager origin as environment variable %s=<name>", impl.OriginEnv)
- }
runtime := rt.Init()
defer runtime.Cleanup()
server, err := runtime.NewServer()
@@ -47,29 +31,26 @@
if err != nil {
vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, address, err)
}
- suffix, envelope := "", generateEnvelope()
- name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), suffix))
- vlog.VI(0).Infof("Node manager name: %v", name)
- // TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
- // command-line flags are supported in tests.
- dispatcher := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), envelope, name, os.Getenv(impl.PreviousEnv))
+ name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), ""))
+ vlog.VI(0).Infof("Node manager object name: %v", name)
+ configState, err := config.Load()
+ if err != nil {
+ vlog.Fatalf("Failed to load config passed from parent: %v", err)
+ return
+ }
+ configState.Name = name
+ // TODO(caprita): We need a way to set config fields outside of the
+ // update mechanism (since that should ideally be an opaque
+ // implementation detail).
+ dispatcher, err := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), configState)
+ if err != nil {
+ vlog.Fatalf("Failed to create dispatcher: %v", err)
+ }
if err := server.Serve(publishAs, dispatcher); err != nil {
vlog.Fatalf("Serve(%v) failed: %v", publishAs, err)
}
- handle, _ := exec.GetChildHandle()
- if handle != nil {
- callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
- if err != nil {
- vlog.Fatalf("Couldn't get callback name from config: %v", err)
- }
- nmClient, err := node.BindConfig(callbackName)
- if err != nil {
- vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
- }
- if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
- vlog.Fatalf("Callback(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
- }
- }
+ impl.InvokeCallback(name)
+
// Wait until shutdown.
<-signals.ShutdownOnSignals()
}