github.com/emc-advanced-dev/unik@v0.0.0-20190717152701-a58d3e8e33b7/pkg/compilers/mirage/mirage.go (about)

     1  package mirage
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  	"github.com/emc-advanced-dev/pkg/errors"
    15  
    16  	"github.com/solo-io/unik/pkg/compilers"
    17  	unikos "github.com/solo-io/unik/pkg/os"
    18  	"github.com/solo-io/unik/pkg/types"
    19  	unikutil "github.com/solo-io/unik/pkg/util"
    20  	"gopkg.in/yaml.v2"
    21  )
    22  
    23  type Type int
    24  
    25  const (
    26  	XenType Type = iota
    27  	UKVMType
    28  	VirtioType
    29  )
    30  
    31  type MirageCompiler struct {
    32  	Type Type
    33  }
    34  
    35  func (c *MirageCompiler) CompileRawImage(params types.CompileImageParams) (*types.RawImage, error) {
    36  
    37  	sourcesDir := params.SourcesDir
    38  
    39  	if err := grantOpamPermissions(sourcesDir); err != nil {
    40  		return nil, err
    41  	}
    42  	var containerToUse string
    43  	var args []string
    44  	switch c.Type {
    45  	case XenType:
    46  		var err error
    47  		args, err = parseMirageManifest(sourcesDir)
    48  		if err != nil {
    49  			args, err = introspectArguments(sourcesDir)
    50  			if err != nil {
    51  				return nil, err
    52  			}
    53  		}
    54  		containerToUse = "compilers-mirage-ocaml-xen"
    55  		args = append([]string{"configure", "-t", "xen"}, args...)
    56  
    57  	case VirtioType:
    58  		containerToUse = "compilers-mirage-ocaml-ukvm"
    59  		args = append([]string{"configure", "-t", "virtio"}, args...)
    60  
    61  	case UKVMType:
    62  		containerToUse = "compilers-mirage-ocaml-ukvm"
    63  		args = append([]string{"configure", "-t", "ukvm"}, args...)
    64  	default:
    65  		return nil, errors.New("unknown type", nil)
    66  	}
    67  
    68  	if err := unikutil.NewContainer(containerToUse).WithEntrypoint("mirage").WithVolume(sourcesDir, "/opt/code").Run(args...); err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	if err := unikutil.NewContainer(containerToUse).WithEntrypoint("/usr/bin/make").WithVolume(sourcesDir, "/opt/code").Run(); err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	// Extract volume info
    77  
    78  	// read xl template file, and see what disks are needed
    79  	matches, err := filepath.Glob(filepath.Join(params.SourcesDir, "*.xl.in"))
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	if len(matches) != 1 {
    85  		return nil, errors.New("XL file count is wrong", nil)
    86  	}
    87  
    88  	xlFile := matches[0]
    89  
    90  	disks, err := getDisks(sourcesDir, xlFile)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	switch c.Type {
    95  	case XenType:
    96  		return c.packageForXen(sourcesDir, disks, params.NoCleanup)
    97  	case UKVMType:
    98  		return c.packageForUkvm(sourcesDir, disks, params.NoCleanup)
    99  	case VirtioType:
   100  		return c.packageForVirtio(sourcesDir, disks, params.NoCleanup)
   101  	default:
   102  		return nil, errors.New("unknown type", nil)
   103  	}
   104  }
   105  
   106  func (c *MirageCompiler) packageForXen(sourcesDir string, disks []string, cleanup bool) (*types.RawImage, error) {
   107  	// TODO: ukvm package zipfile for ukvm
   108  	var res types.RawImage
   109  	res.RunSpec.Compiler = compilers.MIRAGE_OCAML_XEN.String()
   110  
   111  	unikernelfile, err := getUnikernelFile(sourcesDir)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	res.RunSpec.DeviceMappings = append(res.RunSpec.DeviceMappings, types.DeviceMapping{MountPoint: "/", DeviceName: "/dev/sda1"})
   117  	for _, disk := range disks {
   118  		res.RunSpec.DeviceMappings = append(res.RunSpec.DeviceMappings, types.DeviceMapping{MountPoint: "xen:" + disk, DeviceName: disk})
   119  	}
   120  
   121  	// TODO: ukvm package zipfile for ukvm
   122  	imgFile, err := compilers.BuildBootableImage(unikernelfile, "", false, cleanup)
   123  
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	res.LocalImagePath = imgFile
   129  	res.StageSpec = types.StageSpec{
   130  		ImageFormat:           types.ImageFormat_RAW,
   131  		XenVirtualizationType: types.XenVirtualizationType_Paravirtual,
   132  	}
   133  	res.RunSpec.DefaultInstanceMemory = 256
   134  
   135  	return &res, nil
   136  }
   137  
   138  func (c *MirageCompiler) packageForUkvm(sourcesDir string, disks []string, cleanup bool) (*types.RawImage, error) {
   139  	return c.packageUnikernel(sourcesDir, disks, cleanup, "ukvm")
   140  }
   141  func (c *MirageCompiler) packageForVirtio(sourcesDir string, disks []string, cleanup bool) (*types.RawImage, error) {
   142  	// for qemu we create an empty cmdline file
   143  	r, err := c.packageUnikernel(sourcesDir, disks, cleanup, "virtio")
   144  	if err != nil {
   145  		return r, err
   146  	}
   147  	return r, err
   148  }
   149  
   150  func (c *MirageCompiler) packageUnikernel(sourcesDir string, disks []string, cleanup bool, unikernel string) (*types.RawImage, error) {
   151  	// find ukvm-bin -> the monitor
   152  	// find *.ukvm -> the unikernel
   153  
   154  	matches, err := filepath.Glob(filepath.Join(sourcesDir, "*."+unikernel))
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	// filter non relevant Makefile.ukvm
   160  	var potentialMatches []string
   161  	for _, m := range matches {
   162  		if !strings.HasSuffix(m, "Makefile."+unikernel) {
   163  			potentialMatches = append(potentialMatches, m)
   164  		}
   165  	}
   166  
   167  	if len(potentialMatches) != 1 {
   168  		return nil, errors.New(fmt.Sprintf("Ukvm kernel file count is wrong: %v", potentialMatches), nil)
   169  	}
   170  
   171  	kernel := potentialMatches[0]
   172  
   173  	// place them in the image directory
   174  
   175  	tmpImageDir, err := ioutil.TempDir("", "")
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	if err := unikos.CopyFile(kernel, filepath.Join(tmpImageDir, "program.bin")); err != nil {
   181  		return nil, errors.New("copying bootable image to image dir", err)
   182  	}
   183  
   184  	if unikernel == "ukvm" {
   185  		monitor := filepath.Join(sourcesDir, "ukvm-bin")
   186  		if err := unikos.CopyFile(monitor, filepath.Join(tmpImageDir, "ukvm-bin")); err != nil {
   187  			return nil, errors.New("copying bootable image to image dir", err)
   188  		}
   189  	} else {
   190  		// it is virtio for qemu
   191  		// mirage doesn't have command line, so create an empty one
   192  		// this is the hint for qemu to use PV mode
   193  		f, err := os.Create(filepath.Join(tmpImageDir, "cmdline"))
   194  		if err != nil {
   195  			return nil, errors.New("creating empty cmdline for image", err)
   196  		}
   197  		f.Close()
   198  	}
   199  
   200  	res := &types.RawImage{}
   201  	for _, disk := range disks {
   202  		res.RunSpec.DeviceMappings = append(res.RunSpec.DeviceMappings, types.DeviceMapping{MountPoint: unikernel + ":" + disk, DeviceName: disk})
   203  	}
   204  	if unikernel == "ukvm" {
   205  		res.RunSpec.Compiler = compilers.MIRAGE_OCAML_UKVM.String()
   206  	} else {
   207  		res.RunSpec.Compiler = compilers.MIRAGE_OCAML_QEMU.String()
   208  	}
   209  	res.LocalImagePath = tmpImageDir
   210  	res.StageSpec = types.StageSpec{
   211  		ImageFormat: types.ImageFormat_Folder,
   212  	}
   213  	res.RunSpec.DefaultInstanceMemory = 256
   214  
   215  	return res, nil
   216  }
   217  
   218  func (r *MirageCompiler) Usage() *compilers.CompilerUsage {
   219  	return nil
   220  }
   221  
   222  var parseRegEx = regexp.MustCompile(`vdev=(\S+),\s.+?target=@\S+?:(\S+?)@`)
   223  
   224  func getUnikernelFile(sourcesDir string) (string, error) {
   225  
   226  	matches, err := filepath.Glob(filepath.Join(sourcesDir, "*.xen"))
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  
   231  	if len(matches) != 1 {
   232  		return "", errors.New("Xen kernel file count is wrong", nil)
   233  	}
   234  
   235  	return matches[0], nil
   236  }
   237  
   238  func getDisks(sourcesDir string, xlFile string) ([]string, error) {
   239  
   240  	xlFileFile, err := os.Open(xlFile)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	defer xlFileFile.Close()
   245  	return getDisksFromReader(sourcesDir, xlFileFile)
   246  }
   247  
   248  func getDiskMatchesFromReader(xlFile io.Reader) ([][]string, error) {
   249  
   250  	scanner := bufio.NewScanner(xlFile)
   251  	const diskPrefix = "disk = "
   252  	var matches [][]string
   253  	for scanner.Scan() {
   254  		line := scanner.Text()
   255  		if strings.HasPrefix(line, diskPrefix) {
   256  			restOfLine := line[len(diskPrefix):]
   257  			matches = parseRegEx.FindAllStringSubmatch(restOfLine, -1)
   258  			break
   259  		}
   260  	}
   261  	if err := scanner.Err(); err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	return matches, nil
   266  
   267  }
   268  
   269  func getDisksFromReader(sourcesDir string, xlFile io.Reader) ([]string, error) {
   270  	matches, err := getDiskMatchesFromReader(xlFile)
   271  
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  
   276  	var disks []string
   277  	for _, match := range matches {
   278  		// match must have length of two!
   279  		if len(match) != 3 {
   280  			return nil, errors.New("Matches mismatch - please update code", nil)
   281  		}
   282  		disk := match[1]
   283  		volFile := match[2]
   284  
   285  		_, err := os.Stat(filepath.Join(sourcesDir, volFile))
   286  		if err != nil {
   287  			if os.IsNotExist(err) {
   288  				continue
   289  			} else {
   290  				return nil, errors.New("unexpected error statting disk file", err)
   291  			}
   292  		}
   293  		disks = append(disks, disk)
   294  	}
   295  
   296  	// only return disks that have an image file with them...
   297  
   298  	return disks, nil
   299  }
   300  
   301  func introspectArguments(sourcesDir string) ([]string, error) {
   302  
   303  	output, err := unikutil.NewContainer("compilers-mirage-ocaml-xen").WithVolume(sourcesDir, "/opt/code").WithEntrypoint("/home/opam/.opam/system/bin/mirage").CombinedOutput("describe", "--color=never")
   304  	if err != nil {
   305  		log.WithError(err).WithFields(log.Fields{"output": string(output)}).Error("Error getting data on mirage code")
   306  		return nil, err
   307  	}
   308  
   309  	keys := getKeys(getKeyStringFromDescribe(string(output)))
   310  
   311  	var args []string
   312  
   313  	if _, ok := keys["kv_ro"]; ok {
   314  		args = append(args, "--kv_ro", "fat")
   315  	}
   316  
   317  	if _, ok := keys["network"]; ok {
   318  		args = append(args, "--network", "0")
   319  		args = append(args, "--net", "direct")
   320  		args = append(args, "--dhcp", "true")
   321  	}
   322  
   323  	// find kv_ro, net, network arguments
   324  	return args, nil
   325  }
   326  
   327  func getKeyStringFromDescribe(input string) string {
   328  	const keys = "Keys "
   329  	index := strings.Index(input, keys)
   330  
   331  	if index < 0 {
   332  		return ""
   333  	}
   334  	return input[index+len(keys):]
   335  }
   336  
   337  var pairs = regexp.MustCompile(`\s*(\S+?)=(.+?)(,|\s*$)`)
   338  
   339  func getKeys(input string) map[string]string {
   340  	res := make(map[string]string)
   341  	matches := pairs.FindAllStringSubmatch(input, -1)
   342  	for _, match := range matches {
   343  		res[match[1]] = match[2]
   344  	}
   345  	return res
   346  }
   347  
   348  type mirageProjectConfig struct {
   349  	Args string `yaml:"arguments"`
   350  }
   351  
   352  func parseMirageManifest(sourcesDir string) ([]string, error) {
   353  	data, err := ioutil.ReadFile(filepath.Join(sourcesDir, "manifest.yaml"))
   354  	if err != nil {
   355  		return nil, errors.New("failed to read manifest.yaml file", err)
   356  	}
   357  
   358  	var config mirageProjectConfig
   359  	if err := yaml.Unmarshal(data, &config); err != nil {
   360  		return nil, errors.New("failed to parse yaml manifest.yaml file", err)
   361  	}
   362  
   363  	return strings.Split(config.Args, " "), nil
   364  }
   365  
   366  func grantOpamPermissions(sourcesDir string) error {
   367  	err := unikutil.NewContainer("compilers-mirage-ocaml-xen").WithVolume(sourcesDir, "/opt/code").WithEntrypoint("sudo").Run("chown", "-R", "opam", ".")
   368  	if err != nil {
   369  		log.WithError(err).Error("Error granting permissions to opam")
   370  		return err
   371  	}
   372  	return nil
   373  }