*: do not require env.sh

This removes the need to source env.{sh,fish} when working with hscloud.

This is done by:

 1. Implementing a Go library to reliably detect the location of the
    active hscloud checkout. That in turn is enabled by
    BUILD_WORKSPACE_DIRECTORY being now a thing in Bazel.
 2. Creating a tool `hscloud`, with a command `hscloud workspace` that
    returns the workspace path.
 3. Wrapping this tool to be accessible from Python and Bash.
 4. Bumping all users of hscloud_root to use either the Go library or
    one of the two implemented wrappers.

We also drive-by replace tools/install.sh to be a proper sh_binary, and
make it yell at people if it isn't being ran as `bazel run
//tools:install`.

Finally, we also drive-by delete cluster/tools/nixops.sh which was never used.

Change-Id: I7873714319bfc38bbb930b05baa605c5aa36470a
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1169
Reviewed-by: informatic <informatic@hackerspace.pl>
diff --git a/go/workspace/workspace.go b/go/workspace/workspace.go
new file mode 100644
index 0000000..f8f2ef9
--- /dev/null
+++ b/go/workspace/workspace.go
@@ -0,0 +1,101 @@
+package workspace
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"sync"
+	"syscall"
+)
+
+// isWorkspace returns whether a given string is a valid path pointing to a
+// Bazel workspace directory.
+func isWorkspace(dir string) bool {
+	w := path.Join(dir, "WORKSPACE")
+	if _, err := os.Stat(w); err == nil {
+		return true
+	}
+	return false
+}
+
+// getPathFSID returns an opaque filesystem identifier for a given path.
+func getPathFSID(dir string) (uint64, error) {
+	st, err := os.Stat(dir)
+	if err != nil {
+		// No need to wrap err, as stat errors are already quite explicit
+		// (they also include the stat'd path).
+		return 0, err
+	}
+	switch x := st.Sys().(type) {
+	case *syscall.Stat_t:
+		return x.Dev, nil
+	default:
+		return 0, fmt.Errorf("unsupported operating system (got stat type %+v)", st.Sys())
+	}
+}
+
+// lookForWorkspace recurses up a directory until it finds a WORKSPACE, the
+// root, or crosses a filesystem boundary.
+func lookForWorkspace(dir string) (string, error) {
+	fsid, err := getPathFSID(dir)
+	if err != nil {
+		return "", fmt.Errorf("could not get initial FSID: %w", err)
+	}
+	for {
+		if dir == "." || dir == "/" || dir == "" {
+			return "", fmt.Errorf("got up to root before finding workspace")
+		}
+
+		fsid2, err := getPathFSID(dir)
+		if err != nil {
+			return "", fmt.Errorf("could not get parent FWID: %w", err)
+		}
+		if fsid2 != fsid {
+			return "", fmt.Errorf("crossed filesystem boundaries before finding workspace")
+		}
+
+		if isWorkspace(dir) {
+			return dir, nil
+		}
+		dir = path.Dir(dir)
+	}
+}
+
+// Get returns the workspace directory from which a given
+// command line tool is running. This handles the following
+// cases:
+//
+// 1. The command line tool was invoked via `bazel run`.
+// 2. The command line tool was started in the workspace directory or a
+//    subdirectory.
+//
+// If the workspace directory path cannot be inferred based on the above
+// assumptions, an error is returned.
+func Get() (string, error) {
+	workspaceOnce.Do(func() {
+		workspace, workspaceErr = get()
+	})
+	return workspace, workspaceErr
+}
+
+var (
+	workspace     string
+	workspaceErr  error
+	workspaceOnce sync.Once
+)
+
+func get() (string, error) {
+	if p := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); p != "" && isWorkspace(p) {
+		return p, nil
+	}
+
+	wd, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	p, err := lookForWorkspace(wd)
+	if err != nil {
+		return "", fmt.Errorf("not invoked from `bazel run` and could not find workspace root by traversing upwards: %w", err)
+	}
+	return p, nil
+}