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  }