blob: b1c18e0d538b0dba28937cd8e02d3e15a554b979 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"fmt"
"path/filepath"
"strings"
"time"
"v.io/x/lib/cmdline"
)
var cmdNode = &cmdline.Command{
Name: "node",
Short: "Manage GCE nodes",
Long: "Manage GCE nodes.",
Children: []*cmdline.Command{cmdNodeAuthorize, cmdNodeDeauthorize, cmdNodeCreate, cmdNodeDelete},
}
var cmdNodeAuthorize = &cmdline.Command{
Runner: cmdline.RunnerFunc(runNodeAuthorize),
Name: "authorize",
Short: "Authorize a user to login to a GCE node",
Long: `
Authorizes a user to login to a GCE node (possibly as other user). For
instance, this mechanism is used to give Jenkins slave nodes access to
the GCE mirror of Vanadium repositories.
`,
ArgsName: "<userA>@<hostA> [<userB>@]<hostB>",
ArgsLong: `
<userA>@<hostA> [<userB>@]<hostB> authorizes userA to log into GCE
node hostB from GCE node hostA as user userB. The default value for
userB is userA.
`,
}
var cmdNodeDeauthorize = &cmdline.Command{
Runner: cmdline.RunnerFunc(runNodeDeauthorize),
Name: "deauthorize",
Short: "Deauthorize a user to login to a GCE node",
Long: `
Deuthorizes a user to login to a GCE node (possibly as other
user). For instance, this mechanism is used to revoke access of give
Jenkins slave nodes to the GCE mirror of Vanadium repositories.
`,
ArgsName: "<userA>@<hostA> [<userB>@]<hostB>",
ArgsLong: `
<userA>@<hostA> [<userB>@]<hostB> deauthorizes userA to log into GCE
node hostB from GCE node hostA as user userB. The default value for
userB is userA.
`,
}
func parseUserAndHost(args []string) (string, string, string, string, error) {
if got, want := len(args), 2; got != want {
return "", "", "", "", fmt.Errorf("unexpected number of arguments: got %v, want %d", got, want)
}
parseFn := func(s string) (string, string, error) {
tokens := strings.Split(s, "@")
switch len(tokens) {
case 1:
return "", tokens[0], nil
case 2:
return tokens[0], tokens[1], nil
default:
return "", "", fmt.Errorf("unexpected length of %v: expected at most %d", tokens, 2)
}
}
userA, hostA, err := parseFn(args[0])
if err != nil {
return "", "", "", "", err
}
if userA == "" {
return "", "", "", "", fmt.Errorf("failed to parse user: %v", args[0])
}
userB, hostB, err := parseFn(args[1])
if err != nil {
return "", "", "", "", err
}
if userB == "" {
userB = userA
}
return userA, hostA, userB, hostB, nil
}
// TODO(jsimsa): Add command-line flags for specifying the name of the
// SSH key file to use and whether to create one if it does not
// exist.
func runNodeAuthorize(env *cmdline.Env, args []string) error {
userA, hostA, userB, hostB, err := parseUserAndHost(args)
if err != nil {
return env.UsageErrorf("%v", err)
}
// Copy the public SSH key for <userA> from <hostA> to the local
// machine.
ctx := newContext(env)
s := ctx.NewSeq()
tmpDir, err := s.TempDir("", "")
if err != nil {
return fmt.Errorf("TempDir() failed: %v", err)
}
defer ctx.NewSeq().RemoveAll(tmpDir)
allNodes, err := listAll(ctx)
if err != nil {
return err
}
nodeA, err := allNodes.MatchNames(hostA)
if err != nil {
return err
}
if err := nodeA.RunCopy(ctx, []string{fmt.Sprintf(":/home/%v/.ssh/id_rsa.pub", userA)}, tmpDir); err != nil {
return err
}
// Append the key to the set of authorized keys of <userB> on
// <hostB>.
sshKeyFile := filepath.Join(tmpDir, "id_rsa.pub")
bytes, err := s.ReadFile(sshKeyFile)
if err != nil {
return fmt.Errorf("ReadFile(%v) failed: %v", sshKeyFile, err)
}
nodeB, err := allNodes.MatchNames(hostB)
if err != nil {
return err
}
echoCmd := []string{"echo", strings.TrimSpace(string(bytes)), ">>", fmt.Sprintf("/home/%v/.ssh/authorized_keys", userB)}
if err := nodeB.RunCommand(ctx, userB, echoCmd); err != nil {
return err
}
return nil
}
func runNodeDeauthorize(env *cmdline.Env, args []string) error {
userA, hostA, userB, hostB, err := parseUserAndHost(args)
if err != nil {
return env.UsageErrorf("%v", err)
}
// Remove all keys for <userA>@<hostA> from the set of authorized
// keys of <userB> on <hostB>.
ctx := newContext(env)
allNodes, err := listAll(ctx)
if err != nil {
return err
}
nodeB, err := allNodes.MatchNames(hostB)
if err != nil {
return err
}
authorizedKeysFile := fmt.Sprintf("/home/%v/.ssh/authorized_keys", userB)
tmpKeysFile := authorizedKeysFile + ".tmp"
grepCmd := []string{"grep", "-v", fmt.Sprintf("%v@%v", userA, hostA), authorizedKeysFile, ">", tmpKeysFile}
if err := nodeB.RunCommand(ctx, userB, grepCmd); err != nil {
return err
}
moveCmd := []string{"mv", tmpKeysFile, authorizedKeysFile}
if err := nodeB.RunCommand(ctx, userB, moveCmd); err != nil {
return err
}
return nil
}
var cmdNodeCreate = &cmdline.Command{
Runner: cmdline.RunnerFunc(runNodeCreate),
Name: "create",
Short: "Create GCE nodes",
Long: `
Create GCE nodes. Runs 'gcloud compute instances create'.
`,
ArgsName: "<names>",
ArgsLong: "<names> is a list of names identifying nodes to be created.",
}
var cmdNodeDelete = &cmdline.Command{
Runner: cmdline.RunnerFunc(runNodeDelete),
Name: "delete",
Short: "Delete GCE nodes",
Long: `
Delete GCE nodes. Runs 'gcloud compute instances delete'.
`,
ArgsName: "<names>",
ArgsLong: "<names> is a list of names identifying nodes to be deleted.",
}
func runNodeCreate(env *cmdline.Env, args []string) error {
ctx := newContext(env)
// Create the GCE node(s).
createArgs := []string{
"compute",
"--project", *flagProject,
"instances",
"create",
}
createArgs = append(createArgs, args...)
createArgs = append(createArgs,
"--boot-disk-size", flagBootDiskSize,
"--image", flagImage,
"--machine-type", flagMachineType,
"--zone", flagZone,
"--scopes", flagScopes,
)
if err := ctx.NewSeq().Last("gcloud", createArgs...); err != nil {
return err
}
// Create in-memory representation of node information.
allNodes, err := listAll(ctx)
if err != nil {
return err
}
nodes, err := allNodes.MatchNames(strings.Join(args, ","))
if err != nil {
return err
}
// Wait for the SSH server on all nodes to start up.
const numRetries = 10
const retryPeriod = 5 * time.Second
ready := false
for i := 0; i < numRetries; i++ {
if err := nodes.RunCommand(ctx, *flagUser, []string{"echo"}); err != nil {
fmt.Fprintf(ctx.Stdout(), "attempt #%d to connect failed, will try again later\n", i+1)
time.Sleep(retryPeriod)
continue
}
ready = true
break
}
if !ready {
return fmt.Errorf("timed out waiting for nodes to start")
}
// Execute the setup script.
if flagSetupScript != "" {
if err := nodes.RunCopyAndRun(ctx, *flagUser, []string{flagSetupScript}, nil, ""); err != nil {
return err
}
}
return nil
}
func runNodeDelete(env *cmdline.Env, args []string) error {
ctx := newContext(env)
// Delete the GCE node(s).
var in bytes.Buffer
in.WriteString("Y\n") // answers the [Y/n] prompt
deleteArgs := []string{
"compute",
"--project", *flagProject,
"instances",
"delete",
}
deleteArgs = append(deleteArgs, args...)
deleteArgs = append(deleteArgs, "--zone", flagZone)
return ctx.NewSeq().Read(&in).Last("gcloud", deleteArgs...)
}