From 8f249b8a713ab64b1ee8d023e327147a0544abe6 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Sun, 1 Oct 2023 22:18:20 +0800 Subject: [PATCH] feature: SUSE guest support --- build/docker/Dockerfile.host-deployer | 2 +- pkg/hostman/guestfs/fsdriver/drivers.go | 1 + pkg/hostman/guestfs/fsdriver/linux.go | 12 +- pkg/hostman/guestfs/fsdriver/linux_init.go | 85 ++++++ pkg/hostman/guestfs/fsdriver/suse.go | 272 ++++++++++++++++++ pkg/hostman/guestfs/fsdriver/userdata.go | 51 +--- pkg/hostman/guestfs/kvmpart/kvmpart.go | 32 ++- pkg/hostman/guestfs/kvmpart/localfs.go | 12 + pkg/hostman/guestfs/sshpart/sshpart.go | 12 + pkg/hostman/hostdeployer/apis/deploy.pb.go | 5 +- .../hostdeployer/apis/deploy_grpc.pb.go | 1 + pkg/util/btrfsutils/btrfsutils.go | 90 ++++++ pkg/util/btrfsutils/btrfsutils_test.go | 48 ++++ pkg/util/btrfsutils/doc.go | 15 + 14 files changed, 589 insertions(+), 49 deletions(-) create mode 100644 pkg/hostman/guestfs/fsdriver/linux_init.go create mode 100644 pkg/hostman/guestfs/fsdriver/suse.go create mode 100644 pkg/util/btrfsutils/btrfsutils.go create mode 100644 pkg/util/btrfsutils/btrfsutils_test.go create mode 100644 pkg/util/btrfsutils/doc.go diff --git a/build/docker/Dockerfile.host-deployer b/build/docker/Dockerfile.host-deployer index b3d4569611..948cf9e3d2 100644 --- a/build/docker/Dockerfile.host-deployer +++ b/build/docker/Dockerfile.host-deployer @@ -1,4 +1,4 @@ -FROM registry.cn-beijing.aliyuncs.com/yunionio/host-deployer-base:1.4 +FROM registry.cn-beijing.aliyuncs.com/yunionio/host-deployer-base:1.5 MAINTAINER "Yaoqi Wan wanyaoqi@yunionyun.com" diff --git a/pkg/hostman/guestfs/fsdriver/drivers.go b/pkg/hostman/guestfs/fsdriver/drivers.go index 2aa69e4ce3..a3e5246a47 100644 --- a/pkg/hostman/guestfs/fsdriver/drivers.go +++ b/pkg/hostman/guestfs/fsdriver/drivers.go @@ -47,6 +47,7 @@ func Init(initPrivatePrefixes []string, cloudrootDir string) error { NewFangdeDeskRootfs, NewUKylinRootfs, NewCentosRootFs, NewFedoraRootFs, NewRhelRootFs, + NewOpenSuseRootFs, NewDebianRootFs, NewCirrosRootFs, NewCirrosNewRootFs, NewUbuntuRootFs, NewGentooRootFs, NewArchLinuxRootFs, NewOpenWrtRootFs, NewCoreOsRootFs, NewOpenEulerRootFs, diff --git a/pkg/hostman/guestfs/fsdriver/linux.go b/pkg/hostman/guestfs/fsdriver/linux.go index 223f3e8d36..a35d786a2e 100644 --- a/pkg/hostman/guestfs/fsdriver/linux.go +++ b/pkg/hostman/guestfs/fsdriver/linux.go @@ -228,11 +228,12 @@ func (l *sLinuxRootFs) DeployYunionroot(rootFs IDiskPartition, pubkeys *deployap l.DisableCloudinit(rootFs) } var yunionroot = YUNIONROOT_USER - rootdir := path.Join(cloudrootDirectory, yunionroot) + var rootdir string // := path.Join(cloudrootDirectory, yunionroot) var err error if rootdir, err = rootFs.CheckOrAddUser(yunionroot, cloudrootDirectory, true); err != nil { return errors.Wrap(err, "unable to CheckOrAddUser") } + log.Infof("DeployYunionroot %s home %s", yunionroot, rootdir) err = DeployAuthorizedKeys(rootFs, rootdir, pubkeys, true) if err != nil { log.Infof("DeployAuthorizedKeys error: %s", err.Error()) @@ -738,7 +739,12 @@ func (d *sLinuxRootFs) DeployTelegraf(config string) (bool, error) { if err != nil { return false, errors.Wrap(err, "chmod supervise run script") } - // add crontab: start telegraf on guest boot + initCmd := fmt.Sprintf("%s/supervise %s", cloudMonitorPath, telegrafPath) + err = d.installInitScript("telegraf", initCmd) + if err != nil { + return false, errors.Wrap(err, "installInitScript") + } + /* // add crontab: start telegraf on guest boot cronJob := fmt.Sprintf("@reboot %s/supervise %s", cloudMonitorPath, telegrafPath) if procutils.NewCommand("chroot", part.GetMountPath(), "crontab", "-l", "|", "grep", cronJob).Run() == nil { // if cronjob exist, return success @@ -749,7 +755,7 @@ func (d *sLinuxRootFs) DeployTelegraf(config string) (bool, error) { ).Output() if err != nil { return false, errors.Wrapf(err, "add crontab %s", output) - } + }*/ return true, nil } diff --git a/pkg/hostman/guestfs/fsdriver/linux_init.go b/pkg/hostman/guestfs/fsdriver/linux_init.go new file mode 100644 index 0000000000..4b59c18c9f --- /dev/null +++ b/pkg/hostman/guestfs/fsdriver/linux_init.go @@ -0,0 +1,85 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fsdriver + +import ( + "fmt" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/util/procutils" +) + +const ( + unitDirPath = "/usr/lib/systemd/system" + enableDirPath = "/etc/systemd/system/multi-user.target.wants" +) + +func (d *sLinuxRootFs) isSupportSystemd() bool { + return d.rootFs.Exists(unitDirPath, false) && d.rootFs.Exists(enableDirPath, false) +} + +func (d *sLinuxRootFs) installInitScript(name, cmd string) error { + if d.isSupportSystemd() { + return d.installSystemd(name, cmd) + } else { + return d.installCrond(cmd) + } +} + +func (d *sLinuxRootFs) installCrond(cmd string) error { + cronJob := fmt.Sprintf("@reboot %s", cmd) + if procutils.NewCommand("chroot", d.rootFs.GetMountPath(), "crontab", "-l", "|", "grep", cronJob).Run() == nil { + // if cronjob exist, return success + return nil + } + output, err := procutils.NewCommand("chroot", d.rootFs.GetMountPath(), "sh", "-c", + fmt.Sprintf("(crontab -l 2>/dev/null; echo '%s') |crontab -", cronJob), + ).Output() + if err != nil { + return errors.Wrapf(err, "add crontab %s", output) + } + return nil +} + +func (d *sLinuxRootFs) installSystemd(name, cmd string) error { + var serviceName = fmt.Sprintf("%s.service", name) + var unitPath = fmt.Sprintf("%s/%s", unitDirPath, serviceName) + var enablePath = fmt.Sprintf("%s/%s", enableDirPath, serviceName) + var unitContent = fmt.Sprintf(`[Unit] +Description=Run once +After=local-fs.target +After=network.target + +[Service] +ExecStart=%s +RemainAfterExit=true +Type=oneshot + +[Install] +WantedBy=multi-user.target +`, cmd) + err := d.rootFs.FilePutContents(unitPath, unitContent, false, false) + if err != nil { + return errors.Wrap(err, "save user_data unit fail") + } + + err = d.rootFs.Symlink(unitPath, enablePath, false) + if err != nil { + return errors.Wrap(err, "create user_data symlink fail") + } + + return nil +} diff --git a/pkg/hostman/guestfs/fsdriver/suse.go b/pkg/hostman/guestfs/fsdriver/suse.go new file mode 100644 index 0000000000..485e4c30c7 --- /dev/null +++ b/pkg/hostman/guestfs/fsdriver/suse.go @@ -0,0 +1,272 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fsdriver + +import ( + "fmt" + "path" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/utils" + + "yunion.io/x/onecloud/pkg/cloudcommon/types" + deployapi "yunion.io/x/onecloud/pkg/hostman/hostdeployer/apis" + "yunion.io/x/onecloud/pkg/util/netutils2" +) + +type sSuseLikeRootFs struct { + *sLinuxRootFs +} + +func newSuseLikeRootFs(part IDiskPartition) *sSuseLikeRootFs { + return &sSuseLikeRootFs{ + sLinuxRootFs: newLinuxRootFs(part), + } +} + +func (r *sSuseLikeRootFs) PrepareFsForTemplate(rootFs IDiskPartition) error { + if err := r.sLinuxRootFs.PrepareFsForTemplate(rootFs); err != nil { + return err + } + return r.CleanNetworkScripts(rootFs) +} + +func (r *sSuseLikeRootFs) CleanNetworkScripts(rootFs IDiskPartition) error { + networkPath := "/etc/sysconfig/network" + files := rootFs.ListDir(networkPath, false) + for i := 0; i < len(files); i++ { + if strings.HasPrefix(files[i], "ifcfg-") && files[i] != "ifcfg-lo" { + rootFs.Remove(path.Join(networkPath, files[i]), false) + continue + } + if strings.HasPrefix(files[i], "ifroute-") { + rootFs.Remove(path.Join(networkPath, files[i]), false) + } + } + return nil +} + +func (r *sSuseLikeRootFs) RootSignatures() []string { + sig := r.sLinuxRootFs.RootSignatures() + return append([]string{"etc/SUSE-brand", "/etc/os-release"}, sig...) +} + +func (r *sSuseLikeRootFs) DeployHostname(rootFs IDiskPartition, hn, domain string) error { + if rootFs.Exists("/etc/HOSTNAME", false) { + return rootFs.FilePutContents("/etc/HOSTNAME", getHostname(hn, domain), false, false) + } + return nil +} + +func (r *sSuseLikeRootFs) enableBondingModule(rootFs IDiskPartition, bondNics []*types.SServerNic) error { + var content strings.Builder + for i := range bondNics { + content.WriteString("alias ") + content.WriteString(bondNics[i].Name) + content.WriteString(" bonding\n options ") + content.WriteString(bondNics[i].Name) + content.WriteString(" miimon=100 mode=4 lacp_rate=1 xmit_hash_policy=1\n") + } + return rootFs.FilePutContents("/etc/modprobe.d/bonding.conf", content.String(), false, false) +} + +func (r *sSuseLikeRootFs) deployNetworkingScripts(rootFs IDiskPartition, nics []*types.SServerNic) error { + if err := r.sLinuxRootFs.DeployNetworkingScripts(rootFs, nics); err != nil { + return err + } + + // ToServerNics(nics) + allNics, bondNics := convertNicConfigs(nics) + if len(bondNics) > 0 { + err := r.enableBondingModule(rootFs, bondNics) + if err != nil { + return err + } + } + + var dnsSrv []string + mainNic, err := getMainNic(allNics) + if err != nil { + return err + } + var mainIp string + if mainNic != nil { + mainIp = mainNic.Ip + } + for i := range allNics { + nicDesc := allNics[i] + var cmds strings.Builder + cmds.WriteString("STARTMODE=auto\n") + if nicDesc.Mtu > 0 { + cmds.WriteString(fmt.Sprintf("MTU=%d\n", nicDesc.Mtu)) + } + if len(nicDesc.Mac) > 0 { + cmds.WriteString("LLADDR=") + cmds.WriteString(nicDesc.Mac) + cmds.WriteString("\n") + } + if len(nicDesc.TeamingSlaves) != 0 { + cmds.WriteString(`BONDING_OPTS="mode=4 miimon=100"\n`) + } + if nicDesc.TeamingMaster != nil { + cmds.WriteString("BOOTPROTO=none\n") + cmds.WriteString("MASTER=") + cmds.WriteString(nicDesc.TeamingMaster.Name) + cmds.WriteString("\n") + cmds.WriteString("SLAVE=yes\n") + } else if nicDesc.Virtual { + cmds.WriteString("BOOTPROTO=none\n") + cmds.WriteString("NETMASK=255.255.255.255\n") + cmds.WriteString("IPADDR=") + cmds.WriteString(netutils2.PSEUDO_VIP) + cmds.WriteString("\n") + } else if nicDesc.Manual { + netmask := netutils2.Netlen2Mask(int(nicDesc.Masklen)) + cmds.WriteString("BOOTPROTO=statuc\n") + cmds.WriteString("NETMASK=") + cmds.WriteString(netmask) + cmds.WriteString("\n") + cmds.WriteString("IPADDR=") + cmds.WriteString(nicDesc.Ip) + cmds.WriteString("\n") + + var routes = make([][]string, 0) + netutils2.AddNicRoutes(&routes, nicDesc, mainIp, len(nics), privatePrefixes) + if len(nicDesc.Gateway) > 0 && nicDesc.Ip == mainIp { + routes = append(routes, []string{ + "0.0.0.0/0", + nicDesc.Gateway, + }) + } + var rtbl strings.Builder + for _, r := range routes { + rtbl.WriteString(r[0]) + rtbl.WriteString(" ") + rtbl.WriteString(r[1]) + rtbl.WriteString(" - ") + rtbl.WriteString(nicDesc.Name) + rtbl.WriteString("\n") + } + rtblStr := rtbl.String() + if len(rtblStr) > 0 { + var fn = fmt.Sprintf("/etc/sysconfig/network/ifroute-%s", nicDesc.Name) + if err := rootFs.FilePutContents(fn, rtblStr, false, false); err != nil { + return err + } + } + + dnslist := netutils2.GetNicDns(nicDesc) + for i := 0; i < len(dnslist); i++ { + if !utils.IsInArray(dnslist[i], dnsSrv) { + dnsSrv = append(dnsSrv, dnslist[i]) + } + } + } else { + cmds.WriteString("BOOTPROTO=dhcp\n") + } + var fn = fmt.Sprintf("/etc/sysconfig/network/ifcfg-%s", nicDesc.Name) + log.Debugf("%s: %s", fn, cmds.String()) + if err := rootFs.FilePutContents(fn, cmds.String(), false, false); err != nil { + return err + } + } + if len(dnsSrv) > 0 { + cont, err := rootFs.FileGetContents("/etc/sysconfig/network/config", false) + if err != nil { + return errors.Wrap(err, "FileGetContents config") + } + + lines := strings.Split(string(cont), "\n") + for i := range lines { + line := strings.TrimSpace(lines[i]) + if strings.HasPrefix(line, "NETCONFIG_DNS_STATIC_SERVERS=") { + lines[i] = fmt.Sprintf("NETCONFIG_DNS_STATIC_SERVERS=\"%s\"", strings.Join(dnsSrv, " ")) + } + } + err = rootFs.FilePutContents("/etc/sysconfig/network/config", strings.Join(lines, "\n"), false, false) + if err != nil { + return errors.Wrap(err, "FilePutContents config") + } + } + return nil +} + +func (r *sSuseLikeRootFs) DeployStandbyNetworkingScripts(rootFs IDiskPartition, nics, nicsStandby []*types.SServerNic) error { + if err := r.sLinuxRootFs.DeployStandbyNetworkingScripts(rootFs, nics, nicsStandby); err != nil { + return err + } + for _, nic := range nicsStandby { + var cmds string + if len(nic.NicType) == 0 || nic.NicType != "ipmi" { + cmds += fmt.Sprintf("LLADDR=%s\n", nic.Mac) + cmds += "STARTMODE=off\n" + var fn = fmt.Sprintf("/etc/sysconfig/network/ifcfg-%s%d", NetDevPrefix, nic.Index) + if err := rootFs.FilePutContents(fn, cmds, false, false); err != nil { + return err + } + } + } + return nil +} + +func (r *sSuseLikeRootFs) enableSerialConsole(drv IRootFsDriver, rootFs IDiskPartition, sysInfo *jsonutils.JSONDict) error { + return r.enableSerialConsoleSystemd(rootFs) +} + +func (r *sSuseLikeRootFs) disableSerialConcole(drv IRootFsDriver, rootFs IDiskPartition) error { + r.disableSerialConsoleSystemd(rootFs) + return nil +} + +type SOpenSuseRootFs struct { + *sSuseLikeRootFs +} + +func NewOpenSuseRootFs(part IDiskPartition) IRootFsDriver { + return &SOpenSuseRootFs{sSuseLikeRootFs: newSuseLikeRootFs(part)} +} + +func (c *SOpenSuseRootFs) String() string { + return "OpenSuseRootFs" +} + +func (c *SOpenSuseRootFs) GetName() string { + return "OpenSUSE" +} + +func (c *SOpenSuseRootFs) GetReleaseInfo(rootFs IDiskPartition) *deployapi.ReleaseInfo { + rel, _ := rootFs.FileGetContents("/etc/os-release", false) + var version string + if len(rel) > 0 { + lines := strings.Split(string(rel), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "VERSION=") { + version = strings.Trim(line[len("VERSION="):], " \"'") + break + } + } + } + return deployapi.NewReleaseInfo(c.GetName(), version, c.GetArch(rootFs)) +} + +func (c *SOpenSuseRootFs) DeployNetworkingScripts(rootFs IDiskPartition, nics []*types.SServerNic) error { + if err := c.sSuseLikeRootFs.deployNetworkingScripts(rootFs, nics); err != nil { + return err + } + return nil +} diff --git a/pkg/hostman/guestfs/fsdriver/userdata.go b/pkg/hostman/guestfs/fsdriver/userdata.go index 57866b3477..84731a4335 100644 --- a/pkg/hostman/guestfs/fsdriver/userdata.go +++ b/pkg/hostman/guestfs/fsdriver/userdata.go @@ -64,8 +64,7 @@ func (d *sLinuxRootFs) deployUserDataByCron(userData string) error { func (d *sLinuxRootFs) deployUserDataBySystemd(userData string) error { const scriptPath = "/etc/userdata.sh" - const runScriptPath = "/etc/run-userdata.sh" - const serviceName = "cloud-userdata.service" + const serviceName = "cloud-userdata" { config, err := cloudinit.ParseUserData(userData) @@ -76,55 +75,25 @@ func (d *sLinuxRootFs) deployUserDataBySystemd(userData string) error { scripts = config.UserDataScript() } scripts += "\n" - scripts += fmt.Sprintf("systemctl disable --now %s\n", serviceName) + if d.isSupportSystemd() { + // diable systemd service + scripts += fmt.Sprintf("systemctl disable --now %s\n", serviceName) + } else { + // cleanup crontab + scripts += fmt.Sprintf("crontab -l 2>/dev/null | grep -v '%s') |crontab -\n", scriptPath) + } err = d.rootFs.FilePutContents(scriptPath, scripts, false, false) if err != nil { return errors.Wrap(err, "save user_data fail") } } - { - runScripts := fmt.Sprintf(`#!/bin/bash -/bin/bash %s >> /var/log/cloud-userdata.log 2>&1 -`, scriptPath) - err := d.rootFs.FilePutContents(runScriptPath, runScripts, false, false) + err := d.installInitScript(serviceName, scriptPath) if err != nil { - return errors.Wrap(err, "save user_data fail") - } - err = d.rootFs.Chmod(runScriptPath, 0755, false) - if err != nil { - return errors.Wrap(err, "chmod user_data fail") + return errors.Wrap(err, "installInitScript") } } - - { - var unitPath = fmt.Sprintf("/usr/lib/systemd/system/%s", serviceName) - var enablePath = fmt.Sprintf("/etc/systemd/system/multi-user.target.wants/%s", serviceName) - var unitContent = fmt.Sprintf(`[Unit] -Description=Run once -After=local-fs.target -After=network.target - -[Service] -ExecStart=%s -RemainAfterExit=true -Type=oneshot - -[Install] -WantedBy=multi-user.target -`, runScriptPath) - err := d.rootFs.FilePutContents(unitPath, unitContent, false, false) - if err != nil { - return errors.Wrap(err, "save user_data unit fail") - } - - err = d.rootFs.Symlink(unitPath, enablePath, false) - if err != nil { - return errors.Wrap(err, "create user_data symlink fail") - } - } - return nil } diff --git a/pkg/hostman/guestfs/kvmpart/kvmpart.go b/pkg/hostman/guestfs/kvmpart/kvmpart.go index aac3f57740..ebd7c5aa79 100644 --- a/pkg/hostman/guestfs/kvmpart/kvmpart.go +++ b/pkg/hostman/guestfs/kvmpart/kvmpart.go @@ -28,6 +28,7 @@ import ( "yunion.io/x/onecloud/pkg/hostman/guestfs" "yunion.io/x/onecloud/pkg/hostman/guestfs/fsdriver" + "yunion.io/x/onecloud/pkg/util/btrfsutils" "yunion.io/x/onecloud/pkg/util/fileutils2" "yunion.io/x/onecloud/pkg/util/procutils" ) @@ -110,18 +111,24 @@ func (p *SKVMGuestDiskPartition) MountPartReadOnly() bool { } func (p *SKVMGuestDiskPartition) Mount() bool { - if len(p.fs) == 0 || utils.IsInStringArray(p.fs, []string{"swap", "btrfs"}) { + if len(p.fs) == 0 || utils.IsInStringArray(p.fs, []string{"swap"}) { log.Errorf("Mount fs failed: unsupport fs %s on %s", p.fs, p.partDev) return false } err := p.fsck() if err != nil { log.Errorf("SKVMGuestDiskPartition fsck error: %s", err) - return false + // return false } err = p.mount(false) if err != nil { log.Errorf("SKVMGuestDiskPartition mount error: %s", err) + } else if p.fs == "btrfs" { + // try mount btrfs subvoume + err := btrfsutils.MountSubvols(p.partDev, p.mountPath) + if err != nil { + log.Errorf("SKVMGuestDiskPartition mount btrfs error %s", err) + } } if p.IsReadonly() { @@ -197,6 +204,9 @@ func (p *SKVMGuestDiskPartition) mount(readonly bool) error { func (p *SKVMGuestDiskPartition) fsck() error { var checkCmd, fixCmd []string switch p.fs { + case "btrfs": + checkCmd = []string{"fsck.btrfs", "--readonly", p.partDev} + fixCmd = []string{"fsck.btrfs", "--repair", p.partDev} case "hfsplus": checkCmd = []string{"fsck.hfsplus", "-q", p.partDev} fixCmd = []string{"fsck.hfsplus", "-fpy", p.partDev} @@ -257,6 +267,24 @@ func (p *SKVMGuestDiskPartition) Umount() error { } }() + if p.fs == "btrfs" { + // first try unmount btrfs subvols + err := btrfsutils.UnmountSubvols(p.mountPath) + if err != nil { + log.Errorf("SKVMGuestDiskPartition unmount btrfs error %s", err) + } + } + + // check lsof + { + out, err := procutils.NewCommand("lsof", p.mountPath).Output() + if err != nil { + log.Warningf("lsof %s fail %s", p.mountPath, err) + } else { + log.Infof("unmount path %s lsof %s", p.mountPath, string(out)) + } + } + var tries = 0 var err error var out []byte diff --git a/pkg/hostman/guestfs/kvmpart/localfs.go b/pkg/hostman/guestfs/kvmpart/localfs.go index c169d2d10f..29a945342d 100644 --- a/pkg/hostman/guestfs/kvmpart/localfs.go +++ b/pkg/hostman/guestfs/kvmpart/localfs.go @@ -235,6 +235,18 @@ func (f *SLocalGuestFS) CheckOrAddUser(user, homeDir string, isSys bool) (realHo err = errors.Wrap(err, "chage") return } + if !f.Exists(realHomeDir, false) { + err = f.Mkdir(realHomeDir, 0700, false) + if err != nil { + err = errors.Wrapf(err, "Mkdir %s", realHomeDir) + } else { + cmd := []string{"chroot", f.mountPath, "chown", user, realHomeDir} + err = procutils.NewCommand(cmd[0], cmd[1:]...).Run() + if err != nil { + err = errors.Wrap(err, "chown") + } + } + } } return } diff --git a/pkg/hostman/guestfs/sshpart/sshpart.go b/pkg/hostman/guestfs/sshpart/sshpart.go index ccaea480f2..9149c7e147 100644 --- a/pkg/hostman/guestfs/sshpart/sshpart.go +++ b/pkg/hostman/guestfs/sshpart/sshpart.go @@ -415,6 +415,18 @@ func (p *SSHPartition) CheckOrAddUser(user, homeDir string, isSys bool) (realHom err = nil } } + if !p.Exists(realHomeDir, false) { + err = p.Mkdir(realHomeDir, 0700, false) + if err != nil { + err = errors.Wrapf(err, "Mkdir %s", realHomeDir) + } else { + cmd := []string{"/usr/sbin/chroot", p.mountPath, "chown", user, realHomeDir} + _, err = p.term.Run(strings.Join(cmd, " ")) + if err != nil { + err = errors.Wrap(err, "chown") + } + } + } } return } diff --git a/pkg/hostman/hostdeployer/apis/deploy.pb.go b/pkg/hostman/hostdeployer/apis/deploy.pb.go index 6101441d51..bcb68e6672 100644 --- a/pkg/hostman/hostdeployer/apis/deploy.pb.go +++ b/pkg/hostman/hostdeployer/apis/deploy.pb.go @@ -11,10 +11,11 @@ package apis import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go b/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go index f4bc46296b..6d0845193b 100644 --- a/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go +++ b/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go @@ -8,6 +8,7 @@ package apis import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/pkg/util/btrfsutils/btrfsutils.go b/pkg/util/btrfsutils/btrfsutils.go new file mode 100644 index 0000000000..48fe0084c9 --- /dev/null +++ b/pkg/util/btrfsutils/btrfsutils.go @@ -0,0 +1,90 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package btrfsutils + +import ( + "path/filepath" + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/util/procutils" +) + +/* +ID 256 gen 32 top level 5 path @ +ID 257 gen 2426 top level 256 path @/var +ID 258 gen 870 top level 256 path @/usr/local +ID 259 gen 2426 top level 256 path @/tmp +ID 260 gen 35 top level 256 path @/srv +ID 261 gen 2426 top level 256 path @/root +ID 262 gen 874 top level 256 path @/opt +ID 263 gen 35 top level 256 path @/home +ID 264 gen 26 top level 256 path @/boot/grub2/x86_64-efi +ID 265 gen 64 top level 256 path @/boot/grub2/i386-pc +ID 266 gen 881 top level 256 path @/.snapshots +ID 267 gen 2426 top level 266 path @/.snapshots/1/snapshot +ID 272 gen 65 top level 266 path @/.snapshots/2/snapshot +*/ +func parseBtrfsSubvols(lines string) []string { + log.Debugf("parseBtrfsSubvols %s", lines) + var ret []string + for _, l := range strings.Split(lines, "\n") { + parts := strings.Split(l, " ") + if len(parts) >= 9 && strings.HasPrefix(parts[8], "@/") { + ret = append(ret, strings.TrimSpace(parts[8])) + } + } + return ret +} + +func MountSubvols(dev string, mnt string) error { + output, err := procutils.NewCommand("btrfs", "subvolume", "list", mnt).Output() + if err != nil { + return errors.Wrap(err, "btrfs subvolume list") + } + subvols := parseBtrfsSubvols(string(output)) + log.Debugf("mount btrfs subvols %s", strings.Join(subvols, ",")) + for _, subvol := range subvols { + err := procutils.NewCommand("mount", dev, filepath.Join(mnt, subvol[2:]), "-o", "subvol=/"+subvol).Run() + if err != nil { + return errors.Wrapf(err, "btrfs mount subvolume %s", subvol) + } + } + return nil +} + +func UnmountSubvols(mnt string) error { + output, err := procutils.NewCommand("btrfs", "subvolume", "list", mnt).Output() + if err != nil { + return errors.Wrap(err, "btrfs subvolume list") + } + subvols := parseBtrfsSubvols(string(output)) + log.Debugf("unmount btrfs subvols %s", strings.Join(subvols, ",")) + // scan in reverse order + var errs []error + for i := len(subvols) - 1; i >= 0; i-- { + subvol := subvols[i] + err := procutils.NewCommand("umount", filepath.Join(mnt, subvol[2:])).Run() + if err != nil { + errs = append(errs, errors.Wrapf(err, "btrfs unmount subvolume %s", subvol)) + } + } + if len(errs) > 0 { + return errors.NewAggregate(errs) + } + return nil +} diff --git a/pkg/util/btrfsutils/btrfsutils_test.go b/pkg/util/btrfsutils/btrfsutils_test.go new file mode 100644 index 0000000000..9b0092a1a5 --- /dev/null +++ b/pkg/util/btrfsutils/btrfsutils_test.go @@ -0,0 +1,48 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package btrfsutils + +import ( + "reflect" + "testing" +) + +const ( + lines = `ID 256 gen 32 top level 5 path @ +ID 257 gen 872 top level 256 path @/var +ID 258 gen 869 top level 256 path @/usr/local +ID 259 gen 872 top level 256 path @/tmp +ID 260 gen 35 top level 256 path @/srv +ID 261 gen 872 top level 256 path @/root +ID 262 gen 871 top level 256 path @/opt +ID 263 gen 35 top level 256 path @/home +ID 264 gen 26 top level 256 path @/boot/grub2/x86_64-efi +ID 265 gen 64 top level 256 path @/boot/grub2/i386-pc +ID 266 gen 66 top level 256 path @/.snapshots +ID 267 gen 873 top level 266 path @/.snapshots/1/snapshot +ID 272 gen 65 top level 266 path @/.snapshots/2/snapshot +` +) + +func TestParseBtrfsSubvols(t *testing.T) { + subvols := parseBtrfsSubvols(lines) + want := []string{"@/var", "@/usr/local", "@/tmp", "@/srv", "@/root", "@/opt", "@/home", + "@/boot/grub2/x86_64-efi", "@/boot/grub2/i386-pc", "@/.snapshots", + "@/.snapshots/1/snapshot", "@/.snapshots/2/snapshot", + } + if !reflect.DeepEqual(subvols, want) { + t.Errorf("expect %s got %s", want, subvols) + } +} diff --git a/pkg/util/btrfsutils/doc.go b/pkg/util/btrfsutils/doc.go new file mode 100644 index 0000000000..27e87f1901 --- /dev/null +++ b/pkg/util/btrfsutils/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package btrfsutils // import "yunion.io/x/onecloud/pkg/util/btrfsutils"