Merge "veyron/services/mgmt/node/impl: add self-installation functionality to node manager."
diff --git a/lib/exec/util.go b/lib/exec/util.go
index 8ecbeb2..ab68838 100644
--- a/lib/exec/util.go
+++ b/lib/exec/util.go
@@ -28,3 +28,30 @@
}
return append(env, newValue)
}
+
+// Mergeenv merges the values for the variables contained in 'other' with the
+// values contained in 'base'. If a variable exists in both, the value in
+// 'other' takes precedence.
+func Mergeenv(base, other []string) []string {
+ otherValues := make(map[string]string)
+ otherUsed := make(map[string]bool)
+ for _, v := range other {
+ if parts := strings.SplitN(v, "=", 2); len(parts) == 2 {
+ otherValues[parts[0]] = parts[1]
+ }
+ }
+ for i, v := range base {
+ if parts := strings.SplitN(v, "=", 2); len(parts) == 2 {
+ if otherValue, ok := otherValues[parts[0]]; ok {
+ base[i] = parts[0] + "=" + otherValue
+ otherUsed[parts[0]] = true
+ }
+ }
+ }
+ for k, v := range otherValues {
+ if !otherUsed[k] {
+ base = append(base, k+"="+v)
+ }
+ }
+ return base
+}
diff --git a/lib/exec/util_test.go b/lib/exec/util_test.go
index a247524..8167493 100644
--- a/lib/exec/util_test.go
+++ b/lib/exec/util_test.go
@@ -32,3 +32,22 @@
t.Fatalf("Expected error when looking up environment variable value, got none", value)
}
}
+
+func TestMerge(t *testing.T) {
+ env := []string{"ANIMAL=GOPHER", "METAL=CHROMIUM"}
+ env = Mergeenv(env, []string{"CAR=VEYRON", "METAL=VANADIUM"})
+ if want, got := 3, len(env); want != got {
+ t.Fatalf("Expected %d env vars, got %d instead", want, got)
+ }
+ for n, want := range map[string]string{
+ "ANIMAL": "GOPHER",
+ "CAR": "VEYRON",
+ "METAL": "VANADIUM",
+ } {
+ if got, err := Getenv(env, n); err != nil {
+ t.Fatalf("Expected a value when looking up %q, got none", n)
+ } else if got != want {
+ t.Fatalf("Unexpected value of environment variable %q: expected %q, got %q", n, want, got)
+ }
+ }
+}
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index 8e4cdb6..7dc6543 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -263,7 +263,7 @@
return "", errOperationFailed
}
// TODO(caprita): Share binaries if already existing locally.
- if err := generateBinary(versionDir, "bin", envelope, true); err != nil {
+ if err := downloadBinary(versionDir, "bin", envelope.Binary); err != nil {
return versionDir, err
}
if err := saveEnvelope(versionDir, envelope); err != nil {
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 32458f4..b3012f5 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1,3 +1,6 @@
+// TODO(caprita): This file is becoming unmanageable; split into several test
+// files.
+
package impl_test
import (
@@ -54,6 +57,7 @@
blackbox.CommandTable["execScript"] = execScript
blackbox.CommandTable["nodeManager"] = nodeManager
blackbox.CommandTable["app"] = app
+ blackbox.CommandTable["installer"] = install
blackbox.HelperProcess(t)
}
@@ -258,6 +262,9 @@
}
}
+// TODO(caprita): Move this to util_test.go, and add a flag to allow persisting
+// the root dir even when the test succeeds (helpful while developing tests).
+
// setupRootDir sets up and returns the local filesystem location that the node
// manager is told to use, as well as a cleanup function.
func setupRootDir(t *testing.T) (string, func()) {
@@ -317,6 +324,7 @@
defer cleanup()
// Current link does not have to live in the root dir.
+ // TODO(caprita): This should be in a unique test tempdir.
currLink := filepath.Join(os.TempDir(), "testcurrent")
os.Remove(currLink) // Start out with a clean slate.
defer os.Remove(currLink)
@@ -922,6 +930,61 @@
}
}
+// install installs the node manager.
+func install(args []string) {
+ if err := impl.SelfInstall(args); err != nil {
+ vlog.Fatalf("SelfInstall failed: %v", err)
+ }
+}
+
+// TestNodeManagerInstall verifies the 'self install' functionality of the node
+// manager: it runs SelfInstall in a child process, then runs the executable
+// from the soft link that the installation created. This should bring up a
+// functioning node manager.
+func TestNodeManagerInstall(t *testing.T) {
+ defer setupLocalNamespace(t)()
+ root, cleanup := setupRootDir(t)
+ defer cleanup()
+
+ currLink := filepath.Join(os.TempDir(), "testcurrent")
+ os.Remove(currLink) // Start out with a clean slate.
+
+ // We create this command not to run it, but to harvest the flags and
+ // env vars from it. We need these to pass to the installer, to ensure
+ // that the node manager that the installer configures can run.
+ nm := blackbox.HelperCommand(t, "nodeManager", "nm")
+ defer setupChildCommand(nm)()
+
+ argsForNodeManager := append([]string{"--"}, nm.Cmd.Args[1:]...)
+ installer := blackbox.HelperCommand(t, "installer", argsForNodeManager...)
+ installer.Cmd.Env = exec.Mergeenv(installer.Cmd.Env, nm.Cmd.Env)
+
+ // Add vars to instruct the installer how to configure the node manager.
+ installer.Cmd.Env = exec.Setenv(installer.Cmd.Env, config.RootEnv, root)
+ installer.Cmd.Env = exec.Setenv(installer.Cmd.Env, config.CurrentLinkEnv, currLink)
+
+ if err := installer.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ installer.ExpectEOFAndWait()
+ installer.Cleanup()
+
+ // CurrLink should now be pointing to a node manager script that
+ // can start up a node manager.
+
+ runNM := blackbox.HelperCommand(t, "execScript", currLink)
+ if err := runNM.Cmd.Start(); err != nil {
+ t.Fatalf("Start() failed: %v", err)
+ }
+ pid := readPID(t, runNM)
+ resolve(t, "nm", 1)
+ revertNodeExpectError(t, "nm", verror.NoExist) // No previous version available.
+ syscall.Kill(pid, syscall.SIGINT)
+ runNM.Expect("nm terminating")
+ runNM.ExpectEOFAndWait()
+ runNM.Cleanup()
+}
+
func TestNodeManagerGlob(t *testing.T) {
// Set up mount table, application, and binary repositories.
defer setupLocalNamespace(t)()
diff --git a/services/mgmt/node/impl/node_installer.go b/services/mgmt/node/impl/node_installer.go
new file mode 100644
index 0000000..af0206e
--- /dev/null
+++ b/services/mgmt/node/impl/node_installer.go
@@ -0,0 +1,67 @@
+package impl
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "veyron.io/veyron/veyron2/services/mgmt/application"
+ "veyron.io/veyron/veyron2/vlog"
+
+ "veyron.io/veyron/veyron/lib/testutil/blackbox" // For VeyronEnvironment, see TODO.
+ "veyron.io/veyron/veyron/services/mgmt/node/config"
+)
+
+// InstallFrom takes a veyron object name denoting an application service where
+// a node manager application envelope can be obtained. It downloads the latest
+// version of the node manager and installs it.
+func InstallFrom(origin string) error {
+ // TODO(caprita): Implement.
+ return nil
+}
+
+// SelfInstall installs the node manager and configures it using the environment
+// and the supplied command-line flags.
+func SelfInstall(args []string) error {
+ configState, err := config.Load()
+ if err != nil {
+ return fmt.Errorf("failed to load config: %v", err)
+ }
+ vlog.VI(1).Infof("Config for node manager: %v", configState)
+ configState.Name = "dummy" // Just so that Validate passes.
+ if err := configState.Validate(); err != nil {
+ return fmt.Errorf("invalid config %v: %v", configState, err)
+ }
+
+ // Create node manager directory tree.
+ nmDir := filepath.Join(configState.Root, "node-manager", "base")
+ if err := os.RemoveAll(nmDir); err != nil {
+ return fmt.Errorf("RemoveAll(%v) failed: %v", nmDir, err)
+ }
+ perm := os.FileMode(0700)
+ if err := os.MkdirAll(nmDir, perm); err != nil {
+ return fmt.Errorf("MkdirAll(%v, %v) failed: %v", nmDir, perm, err)
+ }
+ envelope := &application.Envelope{
+ Args: args,
+ // TODO(caprita): Cleaning up env vars to avoid picking up all
+ // the garbage from the user's env. Move VeyronEnvironment
+ // outside of blackbox if we stick with this strategy.
+ // Alternatively, pass the env vars meant specifically for the
+ // node manager in a different way.
+ Env: blackbox.VeyronEnvironment(os.Environ()),
+ }
+ if err := linkSelf(nmDir, "noded"); err != nil {
+ return err
+ }
+ // We don't pass in the config state setting, since they're already
+ // contained in the environment.
+ if err := generateScript(nmDir, nil, envelope); err != nil {
+ return err
+ }
+
+ // TODO(caprita): Test the node manager we just installed.
+
+ return updateLink(filepath.Join(nmDir, "noded.sh"), configState.CurrentLink)
+ // TODO(caprita): Update system management daemon.
+}
diff --git a/services/mgmt/node/impl/node_invoker.go b/services/mgmt/node/impl/node_invoker.go
index 243f2d1..92bbb80 100644
--- a/services/mgmt/node/impl/node_invoker.go
+++ b/services/mgmt/node/impl/node_invoker.go
@@ -49,7 +49,7 @@
vexec "veyron.io/veyron/veyron/lib/exec"
"veyron.io/veyron/veyron/lib/glob"
- iconfig "veyron.io/veyron/veyron/services/mgmt/node/config"
+ "veyron.io/veyron/veyron/services/mgmt/node/config"
"veyron.io/veyron/veyron/services/mgmt/profile"
)
@@ -86,7 +86,7 @@
type nodeInvoker struct {
updating *updatingState
callback *callbackState
- config *iconfig.State
+ config *config.State
disp *dispatcher
}
@@ -228,14 +228,19 @@
return nil
}
+// TODO(caprita): Move this to util.go since node_installer is also using it now.
+
func generateScript(workspace string, configSettings []string, envelope *application.Envelope) error {
+ // TODO(caprita): Remove this snippet of code, it doesn't seem to serve
+ // any purpose.
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 += strings.Join(iconfig.QuoteEnv(append(envelope.Env, configSettings...)), " ") + " "
+ output += strings.Join(config.QuoteEnv(append(envelope.Env, configSettings...)), " ") + " "
output += filepath.Join(workspace, "noded") + " "
output += strings.Join(envelope.Args, " ")
path = filepath.Join(workspace, "noded.sh")
@@ -279,8 +284,14 @@
// 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, "noded", envelope, !sameBinary); err != nil {
- return err
+ if sameBinary {
+ if err := linkSelf(workspace, "noded"); err != nil {
+ return err
+ }
+ } else {
+ if err := downloadBinary(workspace, "noded", envelope.Binary); err != nil {
+ return err
+ }
}
// Populate the new workspace with a node manager script.
configSettings, err := i.config.Save(envelope)
@@ -293,7 +304,6 @@
if err := i.testNodeManager(ctx, workspace, envelope); err != nil {
return err
}
- // If the binary has changed, update the node manager symlink.
if err := updateLink(filepath.Join(workspace, "noded.sh"), i.config.CurrentLink); err != nil {
return err
}
diff --git a/services/mgmt/node/impl/util.go b/services/mgmt/node/impl/util.go
index 106d4a4..5f719b4 100644
--- a/services/mgmt/node/impl/util.go
+++ b/services/mgmt/node/impl/util.go
@@ -45,15 +45,12 @@
return &envelope, nil
}
-func generateBinary(workspace, fileName string, envelope *application.Envelope, newBinary bool) error {
- if newBinary {
- // Download the new binary.
- return downloadBinary(workspace, fileName, envelope.Binary)
- }
- // Link the current binary.
+// linkSelf creates a link to the current binary.
+func linkSelf(workspace, fileName string) error {
path := filepath.Join(workspace, fileName)
- if err := os.Link(os.Args[0], path); err != nil {
- vlog.Errorf("Link(%v, %v) failed: %v", os.Args[0], path, err)
+ self := os.Args[0]
+ if err := os.Link(self, path); err != nil {
+ vlog.Errorf("Link(%v, %v) failed: %v", self, path, err)
return errOperationFailed
}
return nil
diff --git a/services/mgmt/node/noded/main.go b/services/mgmt/node/noded/main.go
index 3ef8fa4..3292dfe 100644
--- a/services/mgmt/node/noded/main.go
+++ b/services/mgmt/node/noded/main.go
@@ -2,6 +2,7 @@
import (
"flag"
+ "os"
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/rt"
@@ -14,13 +15,32 @@
)
var (
- publishAs = flag.String("name", "", "name to publish the node manager at")
+ publishAs = flag.String("name", "", "name to publish the node manager at")
+ installSelf = flag.Bool("install_self", false, "perform installation using environment and command-line flags")
+ installFrom = flag.String("install_from", "", "if not-empty, perform installation from the provided application envelope object name")
)
func main() {
flag.Parse()
runtime := rt.Init()
defer runtime.Cleanup()
+
+ if len(*installFrom) > 0 {
+ if err := impl.InstallFrom(*installFrom); err != nil {
+ vlog.Errorf("InstallFrom failed: %v", err)
+ os.Exit(1)
+ }
+ return
+ }
+
+ if *installSelf {
+ if err := impl.SelfInstall(flag.Args()); err != nil {
+ vlog.Errorf("SelfInstall failed: %v", err)
+ os.Exit(1)
+ }
+ return
+ }
+
server, err := runtime.NewServer()
if err != nil {
vlog.Fatalf("NewServer() failed: %v", err)