github.com/coreos/mantle@v0.13.0/cmd/kola/updatepayload.go (about) 1 // Copyright 2016 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 "bufio" 19 "bytes" 20 "fmt" 21 "net" 22 "os" 23 "path/filepath" 24 "strings" 25 "text/template" 26 "time" 27 28 "github.com/spf13/cobra" 29 "golang.org/x/crypto/ssh/agent" 30 31 "github.com/coreos/mantle/kola" 32 "github.com/coreos/mantle/platform" 33 "github.com/coreos/mantle/platform/conf" 34 "github.com/coreos/mantle/platform/machine/qemu" 35 "github.com/coreos/mantle/sdk" 36 sdkomaha "github.com/coreos/mantle/sdk/omaha" 37 ) 38 39 var ( 40 updateTimeout time.Duration 41 updatePayload string 42 cmdUpdatePayload = &cobra.Command{ 43 Run: runUpdatePayload, 44 PreRun: preRun, 45 Use: "updatepayload", 46 Short: "test serving a update_engine payload", 47 Long: ` 48 Boot a CoreOS instance and serve an update payload to its update_engine. 49 50 This command must run inside of the SDK as root, e.g. 51 52 sudo kola updatepayload 53 `, 54 } 55 ) 56 57 type userdataParams struct { 58 Port int 59 Keys []*agent.Key 60 } 61 62 // The user data is a bash script executed by cloudinit to ensure 63 // compatibility with all versions of CoreOS. 64 const userdataTmpl = `#!/bin/bash -ex 65 66 # add ssh key on exit to avoid racing w/ test harness 67 do_ssh_keys() { 68 update-ssh-keys -u core -a updatepayload <<-EOF 69 {{range .Keys}}{{.}} 70 {{end}} 71 EOF 72 } 73 trap do_ssh_keys EXIT 74 75 # update atomicly so nothing reading update.conf fails 76 cat >/etc/coreos/update.conf.new <<EOF 77 GROUP=developer 78 SERVER=http://10.0.0.1:{{printf "%d" .Port}}/v1/update/ 79 EOF 80 mv /etc/coreos/update.conf{.new,} 81 82 # inject the dev key so official images can be used for testing 83 cat >/etc/coreos/update-payload-key.pub.pem <<EOF 84 -----BEGIN PUBLIC KEY----- 85 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzFS5uVJ+pgibcFLD3kbY 86 k02Edj0HXq31ZT/Bva1sLp3Ysv+QTv/ezjf0gGFfASdgpz6G+zTipS9AIrQr0yFR 87 +tdp1ZsHLGxVwvUoXFftdapqlyj8uQcWjjbN7qJsZu0Ett/qo93hQ5nHW7Sv5dRm 88 /ZsDFqk2Uvyaoef4bF9r03wYpZq7K3oALZ2smETv+A5600mj1Xg5M52QFU67UHls 89 EFkZphrGjiqiCdp9AAbAvE7a5rFcJf86YR73QX08K8BX7OMzkn3DsqdnWvLB3l3W 90 6kvIuP+75SrMNeYAcU8PI1+bzLcAG3VN3jA78zeKALgynUNH50mxuiiU3DO4DZ+p 91 5QIDAQAB 92 -----END PUBLIC KEY----- 93 EOF 94 mount --bind /etc/coreos/update-payload-key.pub.pem \ 95 /usr/share/update_engine/update-payload-key.pub.pem 96 97 # disable reboot so we have explicit control 98 systemctl mask locksmithd.service 99 systemctl stop locksmithd.service 100 systemctl reset-failed locksmithd.service 101 102 # off we go! 103 systemctl restart update-engine.service 104 ` 105 106 func init() { 107 cmdUpdatePayload.Flags().DurationVar( 108 &updateTimeout, "timeout", 120*time.Second, 109 "maximum time to wait for update") 110 cmdUpdatePayload.Flags().StringVar( 111 &updatePayload, "payload", "", 112 "update payload") 113 root.AddCommand(cmdUpdatePayload) 114 } 115 116 func runUpdatePayload(cmd *cobra.Command, args []string) { 117 if len(args) != 0 { 118 plog.Fatal("No args accepted") 119 } 120 121 if updatePayload == "" { 122 updatePayload = newPayload() 123 } 124 125 start := time.Now() 126 plog.Notice("=== Running CoreOS upgrade test") 127 if err := runUpdateTest(); err != nil { 128 plog.Fatalf("--- FAIL: %v (%s)", err, time.Since(start)) 129 } 130 plog.Noticef("--- PASS: CoreOS upgrade test (%s)", time.Since(start)) 131 } 132 133 func runUpdateTest() error { 134 outputDir, err := kola.SetupOutputDir(outputDir, "qemu") 135 if err != nil { 136 fmt.Fprintf(os.Stderr, "Setup failed: %v\n", err) 137 os.Exit(1) 138 } 139 140 flight, err := qemu.NewFlight(&kola.QEMUOptions) 141 if err != nil { 142 return fmt.Errorf("new flight: %v", err) 143 } 144 defer flight.Destroy() 145 146 cluster, err := flight.NewCluster(&platform.RuntimeConfig{ 147 OutputDir: outputDir, 148 }) 149 if err != nil { 150 return fmt.Errorf("new cluster: %v", err) 151 } 152 defer cluster.Destroy() 153 qc := cluster.(*qemu.Cluster) 154 155 if err := qc.OmahaServer.AddPackage(updatePayload, "update.gz"); err != nil { 156 return fmt.Errorf("bad payload: %v", err) 157 } 158 159 userdata, err := newUserdata(qc) 160 if err != nil { 161 return fmt.Errorf("bad userdata: %v", err) 162 } 163 164 plog.Infof("Spawning test machine") 165 166 m, err := cluster.NewMachine(userdata) 167 if err != nil { 168 return fmt.Errorf("new machine: %v", err) 169 } 170 171 // initial boot 172 if err := checkUsrA(m); err != nil { 173 return fmt.Errorf("initial boot: %v", err) 174 } 175 176 if err := tryUpdate(m); err != nil { 177 return fmt.Errorf("first update: %v", err) 178 } 179 180 // second boot 181 if err := checkUsrB(m); err != nil { 182 return fmt.Errorf("second boot: %v", err) 183 } 184 185 // Invalidate USR-A to ensure the update is legit. 186 if out, stderr, err := m.SSH("sudo coreos-setgoodroot && " + 187 "sudo wipefs /dev/disk/by-partlabel/USR-A"); err != nil { 188 return fmt.Errorf("invalidating USR-A failed: %s: %v: %s", out, err, stderr) 189 } 190 191 if err := tryUpdate(m); err != nil { 192 return fmt.Errorf("second update: %v", err) 193 } 194 195 // third boot 196 if err := checkUsrA(m); err != nil { 197 return fmt.Errorf("third boot: %v", err) 198 } 199 200 return nil 201 } 202 203 func tryUpdate(m platform.Machine) error { 204 plog.Infof("Triggering update_engine") 205 206 /* trigger update, monitor the progress. */ 207 out, stderr, err := m.SSH("update_engine_client -check_for_update") 208 if err != nil { 209 return fmt.Errorf("Executing update_engine_client failed: %v: %v: %s", out, err, stderr) 210 } 211 212 start := time.Now() 213 status := "unknown" 214 for status != "UPDATE_STATUS_UPDATED_NEED_REBOOT" && time.Since(start) < updateTimeout { 215 time.Sleep(10 * time.Second) 216 217 envs, stderr, err := m.SSH("update_engine_client -status 2>/dev/null") 218 if err != nil { 219 return fmt.Errorf("checking status failed: %v: %s", err, stderr) 220 } 221 222 em := splitNewlineEnv(string(envs)) 223 status = em["CURRENT_OP"] 224 } 225 226 if status != "UPDATE_STATUS_UPDATED_NEED_REBOOT" { 227 return fmt.Errorf("failed to complete within %s, current status %s", updateTimeout, status) 228 } 229 230 plog.Info("Rebooting test machine") 231 232 /* reboot it */ 233 if err := m.Reboot(); err != nil { 234 return fmt.Errorf("reboot failed: %v", err) 235 } 236 237 return nil 238 } 239 240 func newPayload() string { 241 plog.Info("Generating update payload") 242 243 // check for update file, generate if it doesn't exist 244 dir := sdk.BuildImageDir(kola.QEMUOptions.Board, "latest") 245 if err := sdkomaha.GenerateFullUpdate(dir); err != nil { 246 plog.Fatalf("Building full update failed: %v", err) 247 } 248 249 return filepath.Join(dir, "coreos_production_update.gz") 250 } 251 252 func newUserdata(qc *qemu.Cluster) (*conf.UserData, error) { 253 keys, err := qc.Keys() 254 if err != nil { 255 return nil, err 256 } 257 258 params := userdataParams{ 259 Port: qc.OmahaServer.Addr().(*net.TCPAddr).Port, 260 Keys: keys, 261 } 262 tmpl, err := template.New("userdata").Parse(userdataTmpl) 263 if err != nil { 264 return nil, err 265 } 266 267 var buf bytes.Buffer 268 if err := tmpl.Execute(&buf, ¶ms); err != nil { 269 return nil, err 270 } 271 272 return conf.Script(buf.String()), nil 273 } 274 275 func checkUsrA(m platform.Machine) error { 276 plog.Info("Checking for boot from USR-A partition") 277 return checkUsrPartition(m, []string{ 278 "PARTUUID=" + sdk.USRAUUID.String(), 279 "PARTLABEL=USR-A"}) 280 } 281 282 func checkUsrB(m platform.Machine) error { 283 plog.Info("Checking for boot from USR-B partition") 284 return checkUsrPartition(m, []string{ 285 "PARTUUID=" + sdk.USRBUUID.String(), 286 "PARTLABEL=USR-B"}) 287 } 288 289 // checkUsrPartition inspects /proc/cmdline of the machine, looking for the 290 // expected partition mounted at /usr. 291 func checkUsrPartition(m platform.Machine, accept []string) error { 292 out, stderr, err := m.SSH("cat /proc/cmdline") 293 if err != nil { 294 return fmt.Errorf("cat /proc/cmdline: %v: %v: %s", out, err, stderr) 295 } 296 plog.Debugf("Kernel cmdline: %s", out) 297 298 vars := splitSpaceEnv(string(out)) 299 for _, a := range accept { 300 if vars["mount.usr"] == a { 301 return nil 302 } 303 if vars["verity.usr"] == a { 304 return nil 305 } 306 if vars["usr"] == a { 307 return nil 308 } 309 } 310 311 return fmt.Errorf("mount.usr not one of %q", strings.Join(accept, " ")) 312 } 313 314 // split space-seperated KEY=VAL pairs into a map 315 func splitSpaceEnv(envs string) map[string]string { 316 m := make(map[string]string) 317 pairs := strings.Fields(envs) 318 for _, p := range pairs { 319 spl := strings.SplitN(p, "=", 2) 320 if len(spl) == 2 { 321 m[spl[0]] = spl[1] 322 } 323 } 324 return m 325 } 326 327 // splits newline-delimited KEY=VAL pairs into a map 328 func splitNewlineEnv(envs string) map[string]string { 329 m := make(map[string]string) 330 sc := bufio.NewScanner(strings.NewReader(envs)) 331 for sc.Scan() { 332 spl := strings.SplitN(sc.Text(), "=", 2) 333 m[spl[0]] = spl[1] 334 } 335 return m 336 }