func (h *Host) getJailStatus(name string, refresh bool) (JailStatus, error) { if refresh || h.jailStatusCache == nil || time.Now().Sub(h.jailStatusTimestamp) > (2*time.Second) { // FIXME: nicer cache/expiry implementation? if lines, err := run.Command("/usr/sbin/jls", "-d", "jid", "dying", "name").OutputLines(); err != nil { return NoJailStatus, errors.Trace(err) } else { stat := make(map[string]JailStatus) for _, line := range lines { fields := strings.SplitN(line, " ", 3) status := NoJailStatus if len(fields) != 3 { return NoJailStatus, errors.Errorf("Cannot parse jls line %#v", line) } if jid, err := strconv.Atoi(fields[0]); err != nil { return NoJailStatus, errors.Annotatef(err, "Cannot parse jls line %#v", line) } else { status.Jid = jid } if dying, err := strconv.Atoi(fields[1]); err != nil { return NoJailStatus, errors.Annotatef(err, "Cannot parse jls line %#v", line) } else { status.Dying = (dying != 0) } stat[fields[2]] = status } h.jailStatusCache = stat } } return h.jailStatusCache[name], nil }
func cmdPodCmd(cmd string, baseArgs ...string) func(*jetpack.Pod, []string) error { return func(pod *jetpack.Pod, args []string) error { jid := pod.Jid() if jid == 0 { return errors.New("Pod is not running") } else { return errors.Trace(run.Command(cmd, append(append(baseArgs, strconv.Itoa(jid)), args...)...).Run()) } } }
func (pod *Pod) runJail(op string) error { if err := pod.prepJail(); err != nil { return err } verbosity := "-q" if Config().GetBool("debug", false) { verbosity = "-v" } pod.ui.Debug("Running: jail", op) return run.Command("jail", "-f", pod.Path("jail.conf"), verbosity, op, pod.jailName()).Run() }
func zfs(command string, args []string) *run.Cmd { quiet := false if command[0] == '@' { quiet = true command = command[1:] } cmd := run.Command("/sbin/zfs", append([]string{command}, args...)...) if quiet { cmd.Cmd.Stderr = nil } return cmd }
func cmdCp(args []string) error { // caches pods := map[string]*jetpack.Pod{} for i, arg := range args { // Leave switches and local paths alone if cmdCpSwitchOrLocalFileRegexp.MatchString(arg) { continue } pieces := strings.SplitN(arg, ":", 3) if len(pieces) != 3 { return ErrUsage } podUUID := uuid.Parse(pieces[0]) if podUUID == nil { return ErrUsage } pod := pods[podUUID.String()] if pod == nil { pod_, err := Host.GetPod(podUUID) if err != nil { return err } pod = pod_ pods[podUUID.String()] = pod_ } isVolume := false if pieces[1] == "" { if len(pod.Manifest.Apps) != 1 { return ErrUsage } pieces[1] = pod.Manifest.Apps[0].Name.String() } else if pieces[1][0] == '@' { isVolume = true pieces[1] = pieces[1][1:] } // TODO: sanity check on paths? if isVolume { args[i] = pod.Path("rootfs", "vol", pieces[1], pieces[2]) } else { args[i] = pod.Path("rootfs", "app", pieces[1], "rootfs", pieces[2]) } } // fmt.Printf("cp %#v\n", args) return errors.Trace(run.Command("/bin/cp", args...).Run()) }
func ZPools() ([]string, error) { return run.Command("/sbin/zpool", "list", "-Hp", "-oname").OutputLines() }
// Write ACI to `, return its hash. If packlist file is nil, writes // flat ACI (and manifest omits the dependencies). func (img *Image) writeACI(w io.Writer, packlist *os.File) (*types.Hash, error) { var sink io.WriteCloser var faucet io.Reader sw := ui.NewSpinningWriter("Writing ACI", w) defer sw.Close() sink = sw tarArgs := []string{"-C", img.Path(), "-c", "--null", "-f", "-"} if packlist != nil { img.ui.Debug("Writing an incremental ACI") tarArgs = append(tarArgs, "-n", "-T", "-") } else { img.ui.Debug("Writing a flat ACI") // no packlist -> flat ACI manifest := img.Manifest manifest.Dependencies = nil manifest.PathWhitelist = nil manifestF, err := ioutil.TempFile(img.Path(), "manifest.flat.") if err != nil { return nil, errors.Trace(err) } defer os.Remove(manifestF.Name()) if manifestB, err := json.Marshal(manifest); err != nil { manifestF.Close() return nil, errors.Trace(err) } else { _, err := manifestF.Write(manifestB) manifestF.Close() if err != nil { return nil, errors.Trace(err) } } manifestN := filepath.Base(manifestF.Name()) tarArgs = append(tarArgs, "-s", "/^"+manifestN+"$/manifest/", manifestN, "rootfs") } tar := run.Command("tar", tarArgs...).ReadFrom(packlist) if tarPipe, err := tar.StdoutPipe(); err != nil { return nil, errors.Trace(err) } else { faucet = tarPipe } hash := sha512.New() faucet = io.TeeReader(faucet, hash) var compressor *run.Cmd = nil if compression := Config().GetString("images.aci.compression", "no"); compression != "none" { switch compression { case "xz": compressor = run.Command("xz", "-z", "-c") case "bzip2": compressor = run.Command("bzip2", "-z", "-c") case "gz": case "gzip": compressor = run.Command("gzip", "-c") default: return nil, errors.Errorf("Invalid setting images.aci.compression=%#v (allowed values: xz, bzip2, gzip, none)", compression) } compressor.Cmd.Stdout = sink if cin, err := compressor.StdinPipe(); err != nil { return nil, errors.Trace(err) } else { sink = cin } } if err := tar.Start(); err != nil { return nil, errors.Trace(err) } if compressor != nil { if err := compressor.Start(); err != nil { tar.Kill() return nil, errors.Trace(err) } } if _, err := io.Copy(sink, faucet); err != nil { sink.Close() tar.Kill() compressor.Kill() return nil, errors.Trace(err) } if err := tar.Wait(); err != nil { sink.Close() compressor.Kill() return nil, errors.Trace(err) } sink.Close() if err := compressor.Wait(); err != nil { return nil, errors.Trace(err) } if hash, err := types.NewHash(fmt.Sprintf("sha512-%x", hash.Sum(nil))); err != nil { // CAN'T HAPPEN, srsly return nil, errors.Trace(err) } else { img.ui.Debug("Saved", hash) return hash, nil } }
func (img *Image) Build(buildDir string, addFiles []string, buildExec []string) (*Image, error) { img.ui.Println("Preparing build pod") abuilddir, _ := filepath.Abs(buildDir) img.ui.Debug("Build dir:", abuilddir) img.ui.Debug("Extra files:", run.ShellEscape(addFiles...)) img.ui.Debug("Build command:", run.ShellEscape(buildExec...)) buildPod, err := img.Host.CreatePod(img.buildPodManifest(buildExec)) if err != nil { return nil, errors.Trace(err) } ui := ui.NewUI("cyan", "build", buildPod.UUID.String()) workDir := buildPod.Manifest.Apps[0].App.WorkingDirectory ui.Debugf("Preparing build environment in %v", workDir) ds, err := img.Host.Dataset.GetDataset(path.Join("pods", buildPod.UUID.String(), "rootfs.0")) if err != nil { return nil, errors.Trace(err) } fullWorkDir := ds.Path(workDir) if err := os.Mkdir(fullWorkDir, 0700); err != nil { return nil, errors.Trace(err) } if buildDir[len(buildDir)-1] != '/' { buildDir += "/" } cpArgs := []string{"-R", buildDir} if addFiles != nil { cpArgs = append(cpArgs, addFiles...) } cpArgs = append(cpArgs, fullWorkDir) if err := run.Command("cp", cpArgs...).Run(); err != nil { return nil, errors.Trace(err) } ui.Println("Running the build") if err := buildPod.Apps()[0].Run(os.Stdin, os.Stdout, os.Stderr); err != nil { return nil, errors.Trace(err) } if err := buildPod.Kill(); err != nil { return nil, errors.Trace(err) } ui.Debug("Reading new image manifest") manifestBytes, err := ioutil.ReadFile(filepath.Join(fullWorkDir, "manifest.json")) if err != nil { return nil, errors.Trace(err) } ui.Debug("Removing work dir") if err := os.RemoveAll(fullWorkDir); err != nil { return nil, errors.Trace(err) } if err := os.Remove(ds.Path("etc/resolv.conf")); err != nil && !os.IsNotExist(err) { return nil, errors.Trace(err) } ui.Println("Pivoting build pod into new image") // Pivot pod into an image childImage := NewImage(img.Host, buildPod.UUID) if err := ds.Set("mountpoint", childImage.Path("rootfs")); err != nil { return nil, errors.Trace(err) } if err := ds.Rename(img.Host.Dataset.ChildName(path.Join("images", childImage.UUID.String()))); err != nil { return nil, errors.Trace(err) } // We don't need build pod anymore if err := buildPod.Destroy(); err != nil { return nil, errors.Trace(err) } buildPod = nil // Construct the child image's manifest ui.Debug("Constructing new image manifest") if err := json.Unmarshal(manifestBytes, &childImage.Manifest); err != nil { savePath := childImage.Path("manifest.err") ioutil.WriteFile(savePath, manifestBytes, 0400) return nil, errors.Annotatef(err, "Parsing new image manifest; tried to save at %v", savePath) } if _, ok := childImage.Manifest.Annotations.Get("timestamp"); !ok { childImage.Manifest.Annotations.Set("timestamp", time.Now().Format(time.RFC3339)) } for _, label := range []types.ACIdentifier{"os", "arch"} { if childValue, ok := childImage.Manifest.GetLabel(string(label)); !ok { // if child has no os/arch, copy from parent if parentValue, ok := img.Manifest.GetLabel(string(label)); ok { childImage.Manifest.Labels = append(childImage.Manifest.Labels, types.Label{Name: label, Value: parentValue}) } } else if childValue == "" { // if child explicitly set to nil or empty string, remove the // label for i, l := range childImage.Manifest.Labels { if l.Name == label { childImage.Manifest.Labels = append( childImage.Manifest.Labels[:i], childImage.Manifest.Labels[i+1:]...) break } } } } childImage.Manifest.Dependencies = append(types.Dependencies{ types.Dependency{ ImageName: img.Manifest.Name, ImageID: img.Hash, Labels: img.Manifest.Labels, }}, childImage.Manifest.Dependencies...) // Get packing list out of `zfs diff` ui.Debug("Generating incremental packing list") packlist, err := ioutil.TempFile(childImage.Path(), "aci.packlist.") if err != nil { return nil, errors.Trace(err) } os.Remove(packlist.Name()) defer packlist.Close() io.WriteString(packlist, "manifest") // To figure out whether a deleted file has been re-added (and // should be kept in PathWhitelist after all), we keep changes in a // map: a false value means there was an addition; true value means // a deletion. False overwrites true, true never overwrites false. deletionMap := make(map[string]bool) if snap, err := ds.GetSnapshot("parent"); err != nil { return nil, errors.Trace(err) } else if diffs, err := snap.ZfsFields("diff"); err != nil { return nil, errors.Trace(err) } else { for _, diff := range diffs { path1 := diff[1][len(ds.Mountpoint):] switch diff[0] { case "+", "M": io.WriteString(packlist, filepath.Join("\000rootfs", path1)) deletionMap[path1] = false case "R": path2 := diff[2][len(ds.Mountpoint):] deletionMap[path2] = false io.WriteString(packlist, filepath.Join("\000rootfs", path2)) fallthrough case "-": if _, ok := deletionMap[path1]; !ok { // if found in map, either already true (no need to set // again), or false (which should stay) deletionMap[path1] = true } default: return nil, errors.Errorf("Unknown `zfs diff` line: %v", diff) } } } packlist.Seek(0, os.SEEK_SET) // Check if there were any deletions. If there weren't any, we don't // need to prepare a path whitelist. haveDeletions := false for _, isDeletion := range deletionMap { if isDeletion { haveDeletions = true break } } // If any files from parent were deleted, fill in path whitelist if haveDeletions { ui.Debug("Some files were deleted, filling in path whitelist") prefixLen := len(ds.Mountpoint) if err := filepath.Walk(ds.Mountpoint, func(path string, _ os.FileInfo, err error) error { if err != nil { return err } if len(path) == prefixLen { // All paths are prefixed with ds.Mountpoint. Cheaper to compare lengths than whole string. return nil } childImage.Manifest.PathWhitelist = append(childImage.Manifest.PathWhitelist, path[prefixLen:]) return nil }); err != nil { return nil, errors.Trace(err) } sort.Strings(childImage.Manifest.PathWhitelist) } if err := childImage.saveManifest(); err != nil { return nil, errors.Trace(err) } // Save the ACI if f, err := os.OpenFile(childImage.Path("aci"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0440); err != nil { return nil, errors.Trace(err) } else { defer f.Close() if hash, err := childImage.writeACI(f, packlist); err != nil { return nil, errors.Trace(err) } else { childImage.Hash = hash } } if err := childImage.sealImage(); err != nil { return nil, errors.Trace(err) } return childImage, nil }
func (app *App) Stage2(stdin io.Reader, stdout, stderr io.Writer, user, group string, cwd string, exec ...string) error { if app.IsRunning() { // One Jetpack process won't need to run multiple commands in the // same app at the same time. It's either sequential // hook-exec-hook, or an individual command, but not both in the // same binary. This assumption may change in the future. // FIXME: race condition between this place and setting app.cmd return errors.New("A stage2 command is already running for this app") } app.killed = false if strings.HasPrefix(user, "/") || strings.HasPrefix(group, "/") { return errors.New("Path-based user/group not supported yet, sorry") } if cwd == "" { cwd = app.app.WorkingDirectory } addSupplementaryGIDs := false if user == "" { user = app.app.User addSupplementaryGIDs = true } if group == "" { group = app.app.Group addSupplementaryGIDs = true } // Ensure jail is created jid := app.Pod.ensureJid() mds, err := app.Pod.MetadataURL() if err != nil { return errors.Trace(err) } pwf, err := passwd.ReadPasswd(app.Path("etc", "passwd")) if err != nil { return errors.Trace(err) } pwent := pwf.Find(user) if pwent == nil { return errors.Errorf("Cannot find user: %#v", user) } if group != "" { grf, err := passwd.ReadGroup(app.Path("etc", "group")) if err != nil { return errors.Trace(err) } pwent.Gid = grf.FindGid(group) if pwent.Gid < 0 { return errors.Errorf("Cannot find group: %#v", group) } } if cwd == "" { cwd = "/" } gids := strconv.Itoa(pwent.Gid) if addSupplementaryGIDs && len(app.app.SupplementaryGIDs) > 0 { gidsArr := make([]string, len(app.app.SupplementaryGIDs)+1) gidsArr[0] = gids for i, gid := range app.app.SupplementaryGIDs { gidsArr[i+1] = strconv.Itoa(gid) } gids = strings.Join(gidsArr, ",") } stage2 := filepath.Join(Config().MustGetString("path.libexec"), "stage2") args := []string{ fmt.Sprintf("%d:%d:%s:%s:%s", jid, pwent.Uid, gids, app.Name, cwd), "AC_METADATA_URL=" + mds, "USER="******"LOGNAME=" + pwent.Username, "HOME=" + pwent.Home, "SHELL=" + pwent.Shell, } // TODO: move TERM= here if stdin (or stdout?) is a terminal args = append(args, app.env()...) args = append(args, exec...) app.cmd = run.Command(stage2, args...) app.cmd.Cmd.Stdin = stdin app.cmd.Cmd.Stdout = stdout app.cmd.Cmd.Stderr = stderr defer func() { app.cmd = nil }() return app.cmd.Run() }
func (h *Host) ImportImage(name types.ACIdentifier, aci, asc *os.File) (_ *Image, erv error) { newId := uuid.NewRandom() newIdStr := newId.String() ui := ui.NewUI("magenta", "import", newIdStr) if name.Empty() { ui.Println("Starting import") } else { ui.Printf("Starting import of %v", name) } if asc != nil { ui.Debug("Checking signature") didKeyDiscovery := false ks := h.Keystore() checkSig: if ety, err := ks.CheckSignature(name, aci, asc); err == openpgp_err.ErrUnknownIssuer && !didKeyDiscovery { ui.Println("Image signed by an unknown issuer, attempting to discover public key...") if err := h.TrustKey(name, "", ""); err != nil { return nil, errors.Trace(err) } didKeyDiscovery = true aci.Seek(0, os.SEEK_SET) asc.Seek(0, os.SEEK_SET) goto checkSig } else if err != nil { return nil, errors.Trace(err) } else { ui.Println("Valid signature for", name, "by:") ui.Println(keystore.KeyDescription(ety)) // FIXME:ui aci.Seek(0, os.SEEK_SET) asc.Seek(0, os.SEEK_SET) } } else { ui.Debug("No signature to check") } img := NewImage(h, newId) defer func() { if erv != nil { img.Destroy() } }() if err := os.MkdirAll(img.Path(), 0700); err != nil { return nil, errors.Trace(err) } // Save copy of the signature if asc != nil { ui.Debug("Saving signature copy") if ascCopy, err := os.OpenFile(img.Path("aci.asc"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400); err != nil { return nil, errors.Trace(err) } else { _, err := io.Copy(ascCopy, asc) ascCopy.Close() if err != nil { return nil, errors.Trace(err) } } } // Load manifest ui.Debug("Loading manifest") manifestBytes, err := run.Command("tar", "-xOqf", "-", "manifest").ReadFrom(aci).Output() if err != nil { return nil, errors.Trace(err) } aci.Seek(0, os.SEEK_SET) if err = json.Unmarshal(manifestBytes, &img.Manifest); err != nil { return nil, errors.Trace(err) } if !name.Empty() && name != img.Manifest.Name { return nil, errors.Errorf("ACI name mismatch: downloaded %#v, got %#v instead", name, img.Manifest.Name) } if len(img.Manifest.Dependencies) == 0 { ui.Debug("No dependencies to fetch") if _, err := h.Dataset.CreateDataset(path.Join("images", newIdStr), "-o", "mountpoint="+h.Dataset.Path("images", newIdStr, "rootfs")); err != nil { return nil, errors.Trace(err) } } else { for i, dep := range img.Manifest.Dependencies { ui.Println("Looking for dependency:", dep.ImageName, dep.Labels, dep.ImageID) if dimg, err := h.getImageDependency(dep); err != nil { return nil, errors.Trace(err) } else { // We get a copy of the dependency struct when iterating, not // a pointer to it. We need to write to the slice's index to // save the hash to the real manifest. img.Manifest.Dependencies[i].ImageID = dimg.Hash if i == 0 { ui.Printf("Cloning parent %v as base rootfs\n", dimg) if ds, err := dimg.Clone(path.Join(h.Dataset.Name, "images", newIdStr), h.Dataset.Path("images", newIdStr, "rootfs")); err != nil { return nil, errors.Trace(err) } else { img.rootfs = ds } } else { return nil, errors.New("Not implemented") } } } } if err := img.saveManifest(); err != nil { return nil, errors.Trace(err) } ui.Println("Unpacking rootfs") // Save us a copy of the original, compressed ACI aciCopy, err := os.OpenFile(img.Path("aci"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) if err != nil { return nil, errors.Trace(err) } defer aciCopy.Close() aciZRd := io.TeeReader(fetch.ProgressBarFileReader(aci), aciCopy) // Decompress tarball for checksum aciRd, err := DecompressingReader(aciZRd) if err != nil { return nil, errors.Trace(err) } hash := sha512.New() aciRd = io.TeeReader(aciRd, hash) // Unpack the image. We trust system's tar, no need to roll our own untarCmd := run.Command("tar", "-C", img.Path(), "-xf", "-", "rootfs") untar, err := untarCmd.StdinPipe() if err != nil { return nil, errors.Trace(err) } if err := untarCmd.Start(); err != nil { return nil, errors.Trace(err) } // FIXME: defer killing process if survived if _, err := io.Copy(untar, aciRd); err != nil { return nil, errors.Trace(err) } if err := untar.Close(); err != nil { return nil, errors.Trace(err) } if err := untarCmd.Wait(); err != nil { return nil, errors.Trace(err) } if hash, err := types.NewHash(fmt.Sprintf("sha512-%x", hash.Sum(nil))); err != nil { // CAN'T HAPPEN return nil, errors.Trace(err) } else { ui.Println("Successfully imported", hash) img.Hash = hash } // TODO: enforce PathWhiteList if err := img.sealImage(); err != nil { return nil, errors.Trace(err) } return img, nil }