github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/build/sources/conveyorPacker_yum.go (about)

     1  // Copyright (c) 2018, Sylabs Inc. All rights reserved.
     2  // This software is licensed under a 3-clause BSD license. Please consult the
     3  // LICENSE.md file distributed with the sources of this project regarding your
     4  // rights to use or distribute this software.
     5  
     6  package sources
     7  
     8  import (
     9  	"bufio"
    10  	"bytes"
    11  	"fmt"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"regexp"
    17  	"runtime"
    18  	"strings"
    19  
    20  	"github.com/sylabs/singularity/internal/pkg/sylog"
    21  	"github.com/sylabs/singularity/pkg/build/types"
    22  )
    23  
    24  const (
    25  	yumConf = "/etc/bootstrap-yum.conf"
    26  )
    27  
    28  // YumConveyor holds stuff that needs to be packed into the bundle
    29  type YumConveyor struct {
    30  	b         *types.Bundle
    31  	rpmPath   string
    32  	mirrorurl string
    33  	updateurl string
    34  	osversion string
    35  	include   string
    36  	gpg       string
    37  	httpProxy string
    38  }
    39  
    40  // YumConveyorPacker only needs to hold the conveyor to have the needed data to pack
    41  type YumConveyorPacker struct {
    42  	YumConveyor
    43  }
    44  
    45  // Get downloads container information from the specified source
    46  func (c *YumConveyor) Get(b *types.Bundle) (err error) {
    47  	c.b = b
    48  
    49  	// check for dnf or yum on system
    50  	var installCommandPath string
    51  	if installCommandPath, err = exec.LookPath("dnf"); err == nil {
    52  		sylog.Debugf("Found dnf at: %v", installCommandPath)
    53  	} else if installCommandPath, err = exec.LookPath("yum"); err == nil {
    54  		sylog.Debugf("Found yum at: %v", installCommandPath)
    55  	} else {
    56  		return fmt.Errorf("Neither yum nor dnf in PATH")
    57  	}
    58  
    59  	// check for rpm on system
    60  	err = c.getRPMPath()
    61  	if err != nil {
    62  		return fmt.Errorf("While checking rpm path: %v", err)
    63  	}
    64  
    65  	err = c.getBootstrapOptions()
    66  	if err != nil {
    67  		return fmt.Errorf("While getting bootstrap options: %v", err)
    68  	}
    69  
    70  	err = c.genYumConfig()
    71  	if err != nil {
    72  		return fmt.Errorf("While generating Yum config: %v", err)
    73  	}
    74  
    75  	err = c.copyPseudoDevices()
    76  	if err != nil {
    77  		return fmt.Errorf("While copying pseudo devices: %v", err)
    78  	}
    79  
    80  	args := []string{`--noplugins`, `-c`, filepath.Join(c.b.Rootfs(), yumConf), `--installroot`, c.b.Rootfs(), `--releasever=` + c.osversion, `-y`, `install`}
    81  	args = append(args, strings.Fields(c.include)...)
    82  
    83  	// Do the install
    84  	sylog.Debugf("\n\tInstall Command Path: %s\n\tDetected Arch: %s\n\tOSVersion: %s\n\tMirrorURL: %s\n\tUpdateURL: %s\n\tIncludes: %s\n", installCommandPath, runtime.GOARCH, c.osversion, c.mirrorurl, c.updateurl, c.include)
    85  	cmd := exec.Command(installCommandPath, args...)
    86  	// cmd.Stdout = os.Stdout
    87  	cmd.Stderr = os.Stderr
    88  	if err = cmd.Run(); err != nil {
    89  		return fmt.Errorf("While bootstrapping: %v", err)
    90  	}
    91  
    92  	// clean up bootstrap packages
    93  	os.RemoveAll(filepath.Join(c.b.Rootfs(), "/var/cache/yum-bootstrap"))
    94  
    95  	return nil
    96  }
    97  
    98  // Pack puts relevant objects in a Bundle!
    99  func (cp *YumConveyorPacker) Pack() (b *types.Bundle, err error) {
   100  	err = cp.insertBaseEnv()
   101  	if err != nil {
   102  		return nil, fmt.Errorf("While inserting base environment: %v", err)
   103  	}
   104  
   105  	err = cp.insertRunScript()
   106  	if err != nil {
   107  		return nil, fmt.Errorf("While inserting runscript: %v", err)
   108  	}
   109  
   110  	return cp.b, nil
   111  }
   112  
   113  func (c *YumConveyor) getRPMPath() (err error) {
   114  	var output, stderr bytes.Buffer
   115  
   116  	c.rpmPath, err = exec.LookPath("rpm")
   117  	if err != nil {
   118  		return fmt.Errorf("RPM is not in PATH: %v", err)
   119  	}
   120  
   121  	cmd := exec.Command("rpm", "--showrc")
   122  	cmd.Stdout = &output
   123  	cmd.Stderr = &stderr
   124  
   125  	if err = cmd.Run(); err != nil {
   126  		return fmt.Errorf("%v: %v", err, stderr.String())
   127  	}
   128  
   129  	rpmDBPath := ""
   130  	scanner := bufio.NewScanner(&output)
   131  	scanner.Split(bufio.ScanLines)
   132  
   133  	for scanner.Scan() {
   134  		// search for dbpath from showrc output
   135  		if strings.Contains(scanner.Text(), "_dbpath\t") {
   136  			// second field in the string is the path
   137  			rpmDBPath = strings.Fields(scanner.Text())[2]
   138  		}
   139  	}
   140  
   141  	if rpmDBPath == "" {
   142  		return fmt.Errorf("Could not find dbpath")
   143  	} else if rpmDBPath != `%{_var}/lib/rpm` {
   144  		return fmt.Errorf("RPM database is using a weird path: %s\n"+
   145  			"You are probably running this bootstrap on Debian or Ubuntu.\n"+
   146  			"There is a way to work around this problem:\n"+
   147  			"Create a file at path %s/.rpmmacros.\n"+
   148  			"Place the following lines into the '.rpmmacros' file:\n"+
   149  			"%s\n"+
   150  			"%s\n"+
   151  			"After creating the file, re-run the bootstrap.\n"+
   152  			"More info: https://github.com/sylabs/singularity/issues/241\n",
   153  			rpmDBPath, os.Getenv("HOME"), `%_var /var`, `%_dbpath %{_var}/lib/rpm`)
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func (c *YumConveyor) getBootstrapOptions() (err error) {
   160  	var ok bool
   161  
   162  	// look for http_proxy and gpg environment vars
   163  	c.gpg = os.Getenv("GPG")
   164  	c.httpProxy = os.Getenv("http_proxy")
   165  
   166  	// get mirrorURL, updateURL, OSVerison, and Includes components to definition
   167  	c.mirrorurl, ok = c.b.Recipe.Header["mirrorurl"]
   168  	if !ok {
   169  		return fmt.Errorf("Invalid yum header, no MirrorURL specified")
   170  	}
   171  
   172  	c.updateurl, _ = c.b.Recipe.Header["updateurl"]
   173  
   174  	// look for an OS version if a mirror specifies it
   175  	regex := regexp.MustCompile(`(?i)%{OSVERSION}`)
   176  	if regex.MatchString(c.mirrorurl) || regex.MatchString(c.updateurl) {
   177  		c.osversion, ok = c.b.Recipe.Header["osversion"]
   178  		if !ok {
   179  			return fmt.Errorf("Invalid yum header, OSVersion referenced in mirror but no OSVersion specified")
   180  		}
   181  		c.mirrorurl = regex.ReplaceAllString(c.mirrorurl, c.osversion)
   182  		c.updateurl = regex.ReplaceAllString(c.updateurl, c.osversion)
   183  	}
   184  
   185  	include, _ := c.b.Recipe.Header["include"]
   186  
   187  	// check for include environment variable and add it to requires string
   188  	include += ` ` + os.Getenv("INCLUDE")
   189  
   190  	// trim leading and trailing whitespace
   191  	include = strings.TrimSpace(include)
   192  
   193  	// add aa_base to start of include list by default
   194  	include = `/etc/redhat-release coreutils ` + include
   195  
   196  	c.include = include
   197  
   198  	return nil
   199  }
   200  
   201  func (c *YumConveyor) genYumConfig() (err error) {
   202  	fileContent := "[main]\n"
   203  	// http proxy
   204  	if c.httpProxy != "" {
   205  		fileContent += "proxy=" + c.httpProxy + "\n"
   206  	}
   207  	fileContent += "cachedir=/var/cache/yum-bootstrap\n"
   208  	fileContent += "keepcache=0\n"
   209  	fileContent += "debuglevel=2\n"
   210  	fileContent += "logfile=/var/log/yum.log\n"
   211  	fileContent += "syslog_device=/dev/null\n"
   212  	fileContent += "exactarch=1\n"
   213  	fileContent += "obsoletes=1\n"
   214  	// gpg
   215  	if c.gpg != "" {
   216  		fileContent += "gpgcheck=1\n"
   217  	} else {
   218  		fileContent += "gpgcheck=0\n"
   219  	}
   220  	fileContent += "plugins=1\n"
   221  	fileContent += "reposdir=0\n"
   222  	fileContent += "deltarpm=0\n"
   223  	fileContent += "\n"
   224  	fileContent += "[base]\n"
   225  	fileContent += "name=Linux $releasever - $basearch\n"
   226  	// mirror
   227  	if c.mirrorurl != "" {
   228  		fileContent += "baseurl=" + c.mirrorurl + "\n"
   229  	}
   230  	fileContent += "enabled=1\n"
   231  	// gpg
   232  	if c.gpg != "" {
   233  		fileContent += "gpgcheck=1\n"
   234  	} else {
   235  		fileContent += "gpgcheck=0\n"
   236  	}
   237  
   238  	// add update section if updateurl is specified
   239  	if c.updateurl != "" {
   240  		fileContent += "[updates]\n"
   241  		fileContent += "name=Linux $releasever - $basearch updates\n"
   242  		fileContent += "baseurl=" + c.updateurl + "\n"
   243  		fileContent += "enabled=1\n"
   244  		// gpg
   245  		if c.gpg != "" {
   246  			fileContent += "gpgcheck=1\n"
   247  		} else {
   248  			fileContent += "gpgcheck=0\n"
   249  		}
   250  		fileContent += "\n"
   251  	}
   252  
   253  	err = os.Mkdir(filepath.Join(c.b.Rootfs(), "/etc"), 0775)
   254  	if err != nil {
   255  		return fmt.Errorf("While creating %v: %v", filepath.Join(c.b.Rootfs(), "/etc"), err)
   256  	}
   257  
   258  	err = ioutil.WriteFile(filepath.Join(c.b.Rootfs(), yumConf), []byte(fileContent), 0664)
   259  	if err != nil {
   260  		return fmt.Errorf("While creating %v: %v", filepath.Join(c.b.Rootfs(), yumConf), err)
   261  	}
   262  
   263  	// if gpg key is specified, import it
   264  	if c.gpg != "" {
   265  		err = c.importGPGKey()
   266  		if err != nil {
   267  			return fmt.Errorf("While importing GPG key: %v", err)
   268  		}
   269  	} else {
   270  		sylog.Infof("Skipping GPG Key Import")
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  func (c *YumConveyor) importGPGKey() (err error) {
   277  	sylog.Infof("We have a GPG key!  Preparing RPM database.")
   278  
   279  	// make sure gpg is being imported over https
   280  	if strings.HasPrefix(c.gpg, "https://") == false {
   281  		return fmt.Errorf("GPG key must be fetched with https")
   282  	}
   283  
   284  	// make sure curl is installed so rpm can import gpg key
   285  	if _, err = exec.LookPath("curl"); err != nil {
   286  		return fmt.Errorf("Neither yum nor dnf in PATH")
   287  	}
   288  
   289  	cmd := exec.Command(c.rpmPath, "--root", c.b.Rootfs(), "--initdb")
   290  	cmd.Stdout = os.Stdout
   291  	cmd.Stderr = os.Stderr
   292  	if err = cmd.Run(); err != nil {
   293  		return fmt.Errorf("While initializing new rpm db: %v", err)
   294  	}
   295  
   296  	cmd = exec.Command(c.rpmPath, "--root", c.b.Rootfs(), "--import", c.gpg)
   297  	cmd.Stdout = os.Stdout
   298  	cmd.Stderr = os.Stderr
   299  	if err = cmd.Run(); err != nil {
   300  		return fmt.Errorf("While importing GPG key with rpm: %v", err)
   301  	}
   302  
   303  	sylog.Infof("GPG key import complete!")
   304  
   305  	return nil
   306  }
   307  
   308  func (c *YumConveyor) copyPseudoDevices() (err error) {
   309  	err = os.Mkdir(filepath.Join(c.b.Rootfs(), "/dev"), 0775)
   310  	if err != nil {
   311  		return fmt.Errorf("While creating %v: %v", filepath.Join(c.b.Rootfs(), "/dev"), err)
   312  	}
   313  
   314  	devs := []string{"/dev/null", "/dev/zero", "/dev/random", "/dev/urandom"}
   315  
   316  	for _, dev := range devs {
   317  		cmd := exec.Command("cp", "-a", dev, filepath.Join(c.b.Rootfs(), "/dev"))
   318  		cmd.Stdout = os.Stdout
   319  		cmd.Stderr = os.Stderr
   320  		if err = cmd.Run(); err != nil {
   321  			f, err := os.Create(c.b.Rootfs() + "/.singularity.d/runscript")
   322  			if err != nil {
   323  				return fmt.Errorf("While creating %v: %v", filepath.Join(c.b.Rootfs(), dev), err)
   324  			}
   325  
   326  			defer f.Close()
   327  		}
   328  	}
   329  
   330  	return nil
   331  }
   332  
   333  func (cp *YumConveyorPacker) insertBaseEnv() (err error) {
   334  	if err = makeBaseEnv(cp.b.Rootfs()); err != nil {
   335  		return
   336  	}
   337  	return nil
   338  }
   339  
   340  func (cp *YumConveyorPacker) insertRunScript() (err error) {
   341  	ioutil.WriteFile(filepath.Join(cp.b.Rootfs(), "/.singularity.d/runscript"), []byte("#!/bin/sh\n"), 0755)
   342  	if err != nil {
   343  		return
   344  	}
   345  
   346  	return nil
   347  }