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 }