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)