package devmapper // import "github.com/docker/docker/daemon/graphdriver/devmapper" import ( "bufio" "bytes" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "reflect" "strings" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) type directLVMConfig struct { Device string ThinpPercent uint64 ThinpMetaPercent uint64 AutoExtendPercent uint64 AutoExtendThreshold uint64 } var ( errThinpPercentMissing = errors.New("must set both `dm.thinp_percent` and `dm.thinp_metapercent` if either is specified") errThinpPercentTooBig = errors.New("combined `dm.thinp_percent` and `dm.thinp_metapercent` must not be greater than 100") errMissingSetupDevice = errors.New("must provide device path in `dm.directlvm_device` in order to configure direct-lvm") ) func validateLVMConfig(cfg directLVMConfig) error { if reflect.DeepEqual(cfg, directLVMConfig{}) { return nil } if cfg.Device == "" { return errMissingSetupDevice } if (cfg.ThinpPercent > 0 && cfg.ThinpMetaPercent == 0) || cfg.ThinpMetaPercent > 0 && cfg.ThinpPercent == 0 { return errThinpPercentMissing } if cfg.ThinpPercent+cfg.ThinpMetaPercent > 100 { return errThinpPercentTooBig } return nil } func checkDevAvailable(dev string) error { lvmScan, err := exec.LookPath("lvmdiskscan") if err != nil { logrus.Debug("could not find lvmdiskscan") return nil } out, err := exec.Command(lvmScan).CombinedOutput() if err != nil { logrus.WithError(err).Error(string(out)) return nil } if !bytes.Contains(out, []byte(dev)) { return errors.Errorf("%s is not available for use with devicemapper", dev) } return nil } func checkDevInVG(dev string) error { pvDisplay, err := exec.LookPath("pvdisplay") if err != nil { logrus.Debug("could not find pvdisplay") return nil } out, err := exec.Command(pvDisplay, dev).CombinedOutput() if err != nil { logrus.WithError(err).Error(string(out)) return nil } scanner := bufio.NewScanner(bytes.NewReader(bytes.TrimSpace(out))) for scanner.Scan() { fields := strings.SplitAfter(strings.TrimSpace(scanner.Text()), "VG Name") if len(fields) > 1 { // got "VG Name" line" vg := strings.TrimSpace(fields[1]) if len(vg) > 0 { return errors.Errorf("%s is already part of a volume group %q: must remove this device from any volume group or provide a different device", dev, vg) } logrus.Error(fields) break } } return nil } func checkDevHasFS(dev string) error { blkid, err := exec.LookPath("blkid") if err != nil { logrus.Debug("could not find blkid") return nil } out, err := exec.Command(blkid, dev).CombinedOutput() if err != nil { logrus.WithError(err).Error(string(out)) return nil } fields := bytes.Fields(out) for _, f := range fields { kv := bytes.Split(f, []byte{'='}) if bytes.Equal(kv[0], []byte("TYPE")) { v := bytes.Trim(kv[1], "\"") if len(v) > 0 { return errors.Errorf("%s has a filesystem already, use dm.directlvm_device_force=true if you want to wipe the device", dev) } return nil } } return nil } func verifyBlockDevice(dev string, force bool) error { if err := checkDevAvailable(dev); err != nil { return err } if err := checkDevInVG(dev); err != nil { return err } if force { return nil } return checkDevHasFS(dev) } func readLVMConfig(root string) (directLVMConfig, error) { var cfg directLVMConfig p := filepath.Join(root, "setup-config.json") b, err := os.ReadFile(p) if err != nil { if os.IsNotExist(err) { return cfg, nil } return cfg, errors.Wrap(err, "error reading existing setup config") } // check if this is just an empty file, no need to produce a json error later if so if len(b) == 0 { return cfg, nil } err = json.Unmarshal(b, &cfg) return cfg, errors.Wrap(err, "error unmarshaling previous device setup config") } func writeLVMConfig(root string, cfg directLVMConfig) error { p := filepath.Join(root, "setup-config.json") b, err := json.Marshal(cfg) if err != nil { return errors.Wrap(err, "error marshalling direct lvm config") } err = os.WriteFile(p, b, 0600) return errors.Wrap(err, "error writing direct lvm config to file") } func setupDirectLVM(cfg directLVMConfig) error { lvmProfileDir := "/etc/lvm/profile" binaries := []string{"pvcreate", "vgcreate", "lvcreate", "lvconvert", "lvchange", "thin_check"} for _, bin := range binaries { if _, err := exec.LookPath(bin); err != nil { return errors.Wrap(err, "error looking up command `"+bin+"` while setting up direct lvm") } } err := os.MkdirAll(lvmProfileDir, 0755) if err != nil { return errors.Wrap(err, "error creating lvm profile directory") } if cfg.AutoExtendPercent == 0 { cfg.AutoExtendPercent = 20 } if cfg.AutoExtendThreshold == 0 { cfg.AutoExtendThreshold = 80 } if cfg.ThinpPercent == 0 { cfg.ThinpPercent = 95 } if cfg.ThinpMetaPercent == 0 { cfg.ThinpMetaPercent = 1 } out, err := exec.Command("pvcreate", "-f", cfg.Device).CombinedOutput() if err != nil { return errors.Wrap(err, string(out)) } out, err = exec.Command("vgcreate", "docker", cfg.Device).CombinedOutput() if err != nil { return errors.Wrap(err, string(out)) } out, err = exec.Command("lvcreate", "--wipesignatures", "y", "-n", "thinpool", "docker", "--extents", fmt.Sprintf("%d%%VG", cfg.ThinpPercent)).CombinedOutput() if err != nil { return errors.Wrap(err, string(out)) } out, err = exec.Command("lvcreate", "--wipesignatures", "y", "-n", "thinpoolmeta", "docker", "--extents", fmt.Sprintf("%d%%VG", cfg.ThinpMetaPercent)).CombinedOutput() if err != nil { return errors.Wrap(err, string(out)) } out, err = exec.Command("lvconvert", "-y", "--zero", "n", "-c", "512K", "--thinpool", "docker/thinpool", "--poolmetadata", "docker/thinpoolmeta").CombinedOutput() if err != nil { return errors.Wrap(err, string(out)) } profile := fmt.Sprintf("activation{\nthin_pool_autoextend_threshold=%d\nthin_pool_autoextend_percent=%d\n}", cfg.AutoExtendThreshold, cfg.AutoExtendPercent) err = os.WriteFile(lvmProfileDir+"/docker-thinpool.profile", []byte(profile), 0600) if err != nil { return errors.Wrap(err, "error writing docker thinp autoextend profile") } out, err = exec.Command("lvchange", "--metadataprofile", "docker-thinpool", "docker/thinpool").CombinedOutput() return errors.Wrap(err, string(out)) }