github.com/coreos/mantle@v0.13.0/cmd/kola/spawn.go (about)

     1  // Copyright 2015-2018 CoreOS, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net"
    23  	"os"
    24  	"os/user"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	"golang.org/x/crypto/ssh"
    30  	"golang.org/x/crypto/ssh/agent"
    31  
    32  	"github.com/coreos/mantle/kola"
    33  	"github.com/coreos/mantle/platform"
    34  	"github.com/coreos/mantle/platform/conf"
    35  	"github.com/coreos/mantle/platform/machine/qemu"
    36  	"github.com/coreos/mantle/sdk"
    37  	"github.com/coreos/mantle/sdk/omaha"
    38  )
    39  
    40  var (
    41  	cmdSpawn = &cobra.Command{
    42  		Run:    runSpawn,
    43  		PreRun: preRun,
    44  		Use:    "spawn",
    45  		Short:  "spawn a CoreOS instance",
    46  	}
    47  
    48  	spawnNodeCount      int
    49  	spawnUserData       string
    50  	spawnDetach         bool
    51  	spawnOmahaPackage   string
    52  	spawnShell          bool
    53  	spawnRemove         bool
    54  	spawnVerbose        bool
    55  	spawnMachineOptions string
    56  	spawnSetSSHKeys     bool
    57  	spawnSSHKeys        []string
    58  )
    59  
    60  func init() {
    61  	cmdSpawn.Flags().IntVarP(&spawnNodeCount, "nodecount", "c", 1, "number of nodes to spawn")
    62  	cmdSpawn.Flags().StringVarP(&spawnUserData, "userdata", "u", "", "file containing userdata to pass to the instances")
    63  	cmdSpawn.Flags().BoolVarP(&spawnDetach, "detach", "t", false, "-kv --shell=false --remove=false")
    64  	cmdSpawn.Flags().StringVar(&spawnOmahaPackage, "omaha-package", "", "add an update payload to the Omaha server, referenced by image version (e.g. 'latest')")
    65  	cmdSpawn.Flags().BoolVarP(&spawnShell, "shell", "s", true, "spawn a shell in an instance before exiting")
    66  	cmdSpawn.Flags().BoolVarP(&spawnRemove, "remove", "r", true, "remove instances after shell exits")
    67  	cmdSpawn.Flags().BoolVarP(&spawnVerbose, "verbose", "v", false, "output information about spawned instances")
    68  	cmdSpawn.Flags().StringVar(&spawnMachineOptions, "qemu-options", "", "experimental: path to QEMU machine options json")
    69  	cmdSpawn.Flags().BoolVarP(&spawnSetSSHKeys, "keys", "k", false, "add SSH keys from --key options")
    70  	cmdSpawn.Flags().StringSliceVar(&spawnSSHKeys, "key", nil, "path to SSH public key (default: SSH agent + ~/.ssh/id_{rsa,dsa,ecdsa,ed25519}.pub)")
    71  	root.AddCommand(cmdSpawn)
    72  }
    73  
    74  func runSpawn(cmd *cobra.Command, args []string) {
    75  	if err := doSpawn(cmd, args); err != nil {
    76  		fmt.Fprintf(os.Stderr, "%s\n", err)
    77  		os.Exit(1)
    78  	}
    79  }
    80  
    81  func doSpawn(cmd *cobra.Command, args []string) error {
    82  	var err error
    83  
    84  	if spawnDetach {
    85  		spawnSetSSHKeys = true
    86  		spawnVerbose = true
    87  		spawnShell = false
    88  		spawnRemove = false
    89  	}
    90  
    91  	if spawnNodeCount <= 0 {
    92  		return fmt.Errorf("Cluster Failed: nodecount must be one or more")
    93  	}
    94  
    95  	var userdata *conf.UserData
    96  	if spawnUserData != "" {
    97  		userbytes, err := ioutil.ReadFile(spawnUserData)
    98  		if err != nil {
    99  			return fmt.Errorf("Reading userdata failed: %v", err)
   100  		}
   101  		userdata = conf.Unknown(string(userbytes))
   102  	}
   103  	if spawnSetSSHKeys {
   104  		if userdata == nil {
   105  			userdata = conf.Ignition(`{"ignition": {"version": "2.0.0"}}`)
   106  		}
   107  		// If the user explicitly passed empty userdata, the userdata
   108  		// will be non-nil but Empty, and adding SSH keys will
   109  		// silently fail.
   110  		userdata, err = addSSHKeys(userdata)
   111  		if err != nil {
   112  			return err
   113  		}
   114  	}
   115  
   116  	outputDir, err = kola.SetupOutputDir(outputDir, kolaPlatform)
   117  	if err != nil {
   118  		return fmt.Errorf("Setup failed: %v", err)
   119  	}
   120  
   121  	flight, err := kola.NewFlight(kolaPlatform)
   122  	if err != nil {
   123  		return fmt.Errorf("Flight failed: %v", err)
   124  	}
   125  	if spawnRemove {
   126  		defer flight.Destroy()
   127  	}
   128  
   129  	cluster, err := flight.NewCluster(&platform.RuntimeConfig{
   130  		OutputDir:        outputDir,
   131  		AllowFailedUnits: true,
   132  	})
   133  	if err != nil {
   134  		return fmt.Errorf("Cluster failed: %v", err)
   135  	}
   136  
   137  	if spawnRemove {
   138  		defer cluster.Destroy()
   139  	}
   140  
   141  	var updateConf *strings.Reader
   142  	if spawnOmahaPackage != "" {
   143  		qc, ok := cluster.(*qemu.Cluster)
   144  		if !ok {
   145  			//TODO(lucab): expand platform support
   146  			return errors.New("--omaha-package is currently only supported on qemu")
   147  		}
   148  		dir := sdk.BuildImageDir(kola.QEMUOptions.Board, spawnOmahaPackage)
   149  		if err := omaha.GenerateFullUpdate(dir); err != nil {
   150  			return fmt.Errorf("Building full update failed: %v", err)
   151  		}
   152  		updatePayload := filepath.Join(dir, "coreos_production_update.gz")
   153  		if err := qc.OmahaServer.AddPackage(updatePayload, "update.gz"); err != nil {
   154  			return fmt.Errorf("bad payload: %v", err)
   155  		}
   156  		hostport, err := qc.GetOmahaHostPort()
   157  		if err != nil {
   158  			return fmt.Errorf("getting Omaha server address: %v", err)
   159  		}
   160  		updateConf = strings.NewReader(fmt.Sprintf("GROUP=developer\nSERVER=http://%s/v1/update/\n", hostport))
   161  	}
   162  
   163  	var someMach platform.Machine
   164  	for i := 0; i < spawnNodeCount; i++ {
   165  		var mach platform.Machine
   166  		var err error
   167  		if spawnVerbose {
   168  			fmt.Println("Spawning machine...")
   169  		}
   170  		if kolaPlatform == "qemu" && spawnMachineOptions != "" {
   171  			var b []byte
   172  			b, err = ioutil.ReadFile(spawnMachineOptions)
   173  			if err != nil {
   174  				return fmt.Errorf("Could not read machine options: %v", err)
   175  			}
   176  
   177  			var machineOpts platform.MachineOptions
   178  			err = json.Unmarshal(b, &machineOpts)
   179  			if err != nil {
   180  				return fmt.Errorf("Could not unmarshal machine options: %v", err)
   181  			}
   182  
   183  			mach, err = cluster.(*qemu.Cluster).NewMachineWithOptions(userdata, machineOpts)
   184  		} else {
   185  			mach, err = cluster.NewMachine(userdata)
   186  		}
   187  		if err != nil {
   188  			return fmt.Errorf("Spawning instance failed: %v", err)
   189  		}
   190  		if updateConf != nil {
   191  			if err := platform.InstallFile(updateConf, mach, "/etc/coreos/update.conf"); err != nil {
   192  				return fmt.Errorf("Setting update.conf: %v", err)
   193  			}
   194  		}
   195  
   196  		if spawnVerbose {
   197  			fmt.Printf("Machine %v spawned at %v\n", mach.ID(), mach.IP())
   198  		}
   199  
   200  		someMach = mach
   201  	}
   202  
   203  	if spawnShell {
   204  		if spawnRemove {
   205  			reader := strings.NewReader(`PS1="\[\033[0;31m\][bound]\[\033[0m\] $PS1"` + "\n")
   206  			if err := platform.InstallFile(reader, someMach, "/etc/profile.d/kola-spawn-bound.sh"); err != nil {
   207  				return fmt.Errorf("Setting shell prompt failed: %v", err)
   208  			}
   209  		}
   210  		if err := platform.Manhole(someMach); err != nil {
   211  			return fmt.Errorf("Manhole failed: %v", err)
   212  		}
   213  	}
   214  	return nil
   215  }
   216  
   217  func addSSHKeys(userdata *conf.UserData) (*conf.UserData, error) {
   218  	// if no keys specified, use keys from agent plus ~/.ssh/id_{rsa,dsa,ecdsa,ed25519}.pub
   219  	if len(spawnSSHKeys) == 0 {
   220  		// add keys directly from the agent
   221  		agentEnv := os.Getenv("SSH_AUTH_SOCK")
   222  		if agentEnv != "" {
   223  			f, err := net.Dial("unix", agentEnv)
   224  			if err != nil {
   225  				return nil, fmt.Errorf("Couldn't connect to unix socket %q: %v", agentEnv, err)
   226  			}
   227  			defer f.Close()
   228  
   229  			agent := agent.NewClient(f)
   230  			keys, err := agent.List()
   231  			if err != nil {
   232  				return nil, fmt.Errorf("Couldn't talk to ssh-agent: %v", err)
   233  			}
   234  			for _, key := range keys {
   235  				userdata = userdata.AddKey(*key)
   236  			}
   237  		}
   238  
   239  		// populate list of key files
   240  		userInfo, err := user.Current()
   241  		if err != nil {
   242  			return nil, err
   243  		}
   244  		for _, name := range []string{"id_rsa.pub", "id_dsa.pub", "id_ecdsa.pub", "id_ed25519.pub"} {
   245  			path := filepath.Join(userInfo.HomeDir, ".ssh", name)
   246  			if _, err := os.Stat(path); err == nil {
   247  				spawnSSHKeys = append(spawnSSHKeys, path)
   248  			}
   249  		}
   250  	}
   251  
   252  	// read key files, failing if any are missing
   253  	for _, path := range spawnSSHKeys {
   254  		keybytes, err := ioutil.ReadFile(path)
   255  		if err != nil {
   256  			return nil, err
   257  		}
   258  		pkey, comment, _, _, err := ssh.ParseAuthorizedKey(keybytes)
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  		key := agent.Key{
   263  			Format:  pkey.Type(),
   264  			Blob:    pkey.Marshal(),
   265  			Comment: comment,
   266  		}
   267  		userdata = userdata.AddKey(key)
   268  	}
   269  	return userdata, nil
   270  }