github.com/coreos/mantle@v0.13.0/platform/machine/unprivqemu/cluster.go (about) 1 // Copyright 2019 Red Hat 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 unprivqemu 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "regexp" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/pborman/uuid" 29 30 "github.com/coreos/mantle/platform" 31 "github.com/coreos/mantle/platform/conf" 32 "github.com/coreos/mantle/system/exec" 33 "github.com/coreos/mantle/util" 34 ) 35 36 // Cluster is a local cluster of QEMU-based virtual machines. 37 // 38 // XXX: must be exported so that certain QEMU tests can access struct members 39 // through type assertions. 40 type Cluster struct { 41 *platform.BaseCluster 42 flight *flight 43 44 mu sync.Mutex 45 } 46 47 func (qc *Cluster) NewMachine(userdata *conf.UserData) (platform.Machine, error) { 48 return qc.NewMachineWithOptions(userdata, platform.MachineOptions{}) 49 } 50 51 func (qc *Cluster) NewMachineWithOptions(userdata *conf.UserData, options platform.MachineOptions) (platform.Machine, error) { 52 id := uuid.New() 53 54 dir := filepath.Join(qc.RuntimeConf().OutputDir, id) 55 if err := os.Mkdir(dir, 0777); err != nil { 56 return nil, err 57 } 58 59 // hacky solution for cloud config ip substitution 60 // NOTE: escaping is not supported 61 qc.mu.Lock() 62 63 conf, err := qc.RenderUserData(userdata, map[string]string{}) 64 if err != nil { 65 qc.mu.Unlock() 66 return nil, err 67 } 68 qc.mu.Unlock() 69 70 var confPath string 71 if conf.IsIgnition() { 72 confPath = filepath.Join(dir, "ignition.json") 73 if err := conf.WriteFile(confPath); err != nil { 74 return nil, err 75 } 76 } else if conf.IsEmpty() { 77 } else { 78 return nil, fmt.Errorf("unprivileged qemu only supports Ignition or empty configs") 79 } 80 81 journal, err := platform.NewJournal(dir) 82 if err != nil { 83 return nil, err 84 } 85 86 qm := &machine{ 87 qc: qc, 88 id: id, 89 journal: journal, 90 consolePath: filepath.Join(dir, "console.txt"), 91 } 92 93 qmCmd, extraFiles, err := platform.CreateQEMUCommand(qc.flight.opts.Board, qm.id, qc.flight.opts.BIOSImage, qm.consolePath, confPath, qc.flight.diskImagePath, conf.IsIgnition(), options) 94 if err != nil { 95 return nil, err 96 } 97 98 for _, file := range extraFiles { 99 defer file.Close() 100 } 101 102 qc.mu.Lock() 103 104 qmCmd = append(qmCmd, "-netdev", "user,id=eth0,restrict=yes,hostfwd=tcp:127.0.0.1:0-:22", "-device", platform.Virtio(qc.flight.opts.Board, "net", "netdev=eth0")) 105 106 plog.Debugf("NewMachine: %q", qmCmd) 107 108 qm.qemu = exec.Command(qmCmd[0], qmCmd[1:]...) 109 110 qc.mu.Unlock() 111 112 cmd := qm.qemu.(*exec.ExecCmd) 113 cmd.Stderr = os.Stderr 114 115 cmd.ExtraFiles = append(cmd.ExtraFiles, extraFiles...) 116 117 if err = qm.qemu.Start(); err != nil { 118 return nil, err 119 } 120 121 pid := strconv.Itoa(qm.qemu.Pid()) 122 err = util.Retry(6, 5*time.Second, func() error { 123 var err error 124 qm.ip, err = getAddress(pid) 125 if err != nil { 126 return err 127 } 128 return nil 129 }) 130 if err != nil { 131 return nil, err 132 } 133 134 if err := platform.StartMachine(qm, qm.journal); err != nil { 135 qm.Destroy() 136 return nil, err 137 } 138 139 qc.AddMach(qm) 140 141 return qm, nil 142 } 143 144 func (qc *Cluster) Destroy() { 145 qc.BaseCluster.Destroy() 146 qc.flight.DelCluster(qc) 147 } 148 149 // parse /proc/net/tcp to determine the port selected by QEMU 150 func getAddress(pid string) (string, error) { 151 data, err := ioutil.ReadFile("/proc/net/tcp") 152 if err != nil { 153 return "", fmt.Errorf("reading /proc/net/tcp: %v", err) 154 } 155 156 for _, line := range strings.Split(string(data), "\n")[1:] { 157 fields := strings.Fields(line) 158 if len(fields) < 10 { 159 // at least 10 fields are neeeded for the local & remote address and the inode 160 continue 161 } 162 localAddress := fields[1] 163 remoteAddress := fields[2] 164 inode := fields[9] 165 166 isLocalPat := regexp.MustCompile("0100007F:[[:xdigit:]]{4}") 167 if !isLocalPat.MatchString(localAddress) || remoteAddress != "00000000:0000" { 168 continue 169 } 170 171 dir := fmt.Sprintf("/proc/%s/fd/", pid) 172 fds, err := ioutil.ReadDir(dir) 173 if err != nil { 174 return "", fmt.Errorf("listing %s: %v", dir, err) 175 } 176 177 for _, f := range fds { 178 link, err := os.Readlink(filepath.Join(dir, f.Name())) 179 if err != nil { 180 continue 181 } 182 socketPattern := regexp.MustCompile("socket:\\[([0-9]+)\\]") 183 match := socketPattern.FindStringSubmatch(link) 184 if len(match) > 1 { 185 if inode == match[1] { 186 // this entry belongs to the QEMU pid, parse the port and return the address 187 portHex := strings.Split(localAddress, ":")[1] 188 port, err := strconv.ParseInt(portHex, 16, 32) 189 if err != nil { 190 return "", fmt.Errorf("decoding port %q: %v", portHex, err) 191 } 192 return fmt.Sprintf("127.0.0.1:%d", port), nil 193 } 194 } 195 } 196 } 197 return "", fmt.Errorf("didn't find an address") 198 }