From 2dd1aeb8aae18bc754f1230f48cab64b385aabf7 Mon Sep 17 00:00:00 2001 From: Jian Qiu Date: Thu, 28 Apr 2022 09:26:45 +0800 Subject: [PATCH] fix: mount disk readonly (#13152) Co-authored-by: Qiu Jian --- pkg/apis/identity/consts.go | 2 + pkg/apis/image/consts.go | 1 + pkg/cloudcommon/consts/deployer.go | 27 +++++ pkg/cloudcommon/options/options.go | 2 + pkg/hostman/diskutils/interface.go | 9 +- pkg/hostman/diskutils/kvm.go | 46 +++++++- pkg/hostman/diskutils/vddk.go | 22 +++- .../hostdeployer/deployserver/deployserver.go | 109 +++++++++++------- .../hostdeployer/deployserver/options.go | 26 ++++- pkg/hostman/options/options.go | 9 +- pkg/image/models/image_subs.go | 12 +- pkg/image/models/images.go | 98 +++++++++------- pkg/image/options/options.go | 4 +- pkg/image/service/service.go | 42 ++++++- pkg/util/procutils/procutils_test.go | 53 +++++++++ pkg/util/procutils/remote_readdir.go | 100 ++++++++++++++++ pkg/util/procutils/remote_readdir_test.go | 54 +++++++++ pkg/util/procutils/remote_stat.go | 78 +++++++++++++ pkg/util/qemuimg/qemuimg.go | 3 +- pkg/util/qemutils/qemutils.go | 29 +++-- 20 files changed, 602 insertions(+), 124 deletions(-) create mode 100644 pkg/cloudcommon/consts/deployer.go create mode 100644 pkg/util/procutils/procutils_test.go create mode 100644 pkg/util/procutils/remote_readdir.go create mode 100644 pkg/util/procutils/remote_readdir_test.go create mode 100644 pkg/util/procutils/remote_stat.go diff --git a/pkg/apis/identity/consts.go b/pkg/apis/identity/consts.go index aec04610bf..c4fba5598e 100644 --- a/pkg/apis/identity/consts.go +++ b/pkg/apis/identity/consts.go @@ -220,6 +220,8 @@ var ( // glance blacklist options // ############################ "deploy_server_socket_path", + "enable_remote_executor", + "executor_socket_path", }, } ) diff --git a/pkg/apis/image/consts.go b/pkg/apis/image/consts.go index e10a24071a..bc7380ace6 100644 --- a/pkg/apis/image/consts.go +++ b/pkg/apis/image/consts.go @@ -30,6 +30,7 @@ const ( IMAGE_STATUS_SAVED = "saved" IMAGE_STATUS_ACTIVE = "active" IMAGE_STATUS_CONVERTING = "converting" + IMAGE_STATUS_PROBING = "probing" IMAGE_ENCRYPT_STATUS_UNENCRYPTED = "" IMAGE_ENCRYPT_STATUS_ENCRYPTED = "encrypted" diff --git a/pkg/cloudcommon/consts/deployer.go b/pkg/cloudcommon/consts/deployer.go new file mode 100644 index 0000000000..d7526f2012 --- /dev/null +++ b/pkg/cloudcommon/consts/deployer.go @@ -0,0 +1,27 @@ +// 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 consts + +var ( + deployTempDir = "" +) + +func DeployTempDir() string { + return deployTempDir +} + +func SetDeployTempDir(dir string) { + deployTempDir = dir +} diff --git a/pkg/cloudcommon/options/options.go b/pkg/cloudcommon/options/options.go index fa40535ab3..9fde0e5a76 100644 --- a/pkg/cloudcommon/options/options.go +++ b/pkg/cloudcommon/options/options.go @@ -140,6 +140,8 @@ type HostCommonOptions struct { ExecutorSocketPath string `help:"Executor socket path" default:"/var/run/onecloud/exec.sock"` DeployServerSocketPath string `help:"Deploy server listen socket path" default:"/var/run/onecloud/deploy.sock"` + + EnableRemoteExecutor bool `help:"Enable remote executor" default:"false"` } type DBOptions struct { diff --git a/pkg/hostman/diskutils/interface.go b/pkg/hostman/diskutils/interface.go index fbbde928b1..4c7e61a821 100644 --- a/pkg/hostman/diskutils/interface.go +++ b/pkg/hostman/diskutils/interface.go @@ -26,6 +26,7 @@ type IDisk interface { MountRootfs() (fsdriver.IRootFsDriver, error) UmountRootfs(driver fsdriver.IRootFsDriver) error ResizePartition() error + Cleanup() } type DiskParams struct { @@ -34,15 +35,15 @@ type DiskParams struct { VddkInfo *apis.VDDKConInfo } -func GetIDisk(params DiskParams, driver string) IDisk { +func GetIDisk(params DiskParams, driver string, readOnly bool) (IDisk, error) { hypervisor := params.Hypervisor switch hypervisor { case comapi.HYPERVISOR_KVM: - return NewKVMGuestDisk(params.DiskPath, driver) + return NewKVMGuestDisk(params.DiskPath, driver, readOnly) case comapi.HYPERVISOR_ESXI: - return NewVDDKDisk(params.VddkInfo, params.DiskPath, driver) + return NewVDDKDisk(params.VddkInfo, params.DiskPath, driver, readOnly) default: - return NewKVMGuestDisk(params.DiskPath, driver) + return NewKVMGuestDisk(params.DiskPath, driver, readOnly) } } diff --git a/pkg/hostman/diskutils/kvm.go b/pkg/hostman/diskutils/kvm.go index 6a9dac6a60..1a4346d0dd 100644 --- a/pkg/hostman/diskutils/kvm.go +++ b/pkg/hostman/diskutils/kvm.go @@ -16,24 +16,64 @@ package diskutils import ( "fmt" + "io/ioutil" + "os" + "path/filepath" "yunion.io/x/log" "yunion.io/x/pkg/errors" + cloudconsts "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/hostman/diskutils/libguestfs" "yunion.io/x/onecloud/pkg/hostman/diskutils/nbd" "yunion.io/x/onecloud/pkg/hostman/guestfs" "yunion.io/x/onecloud/pkg/hostman/guestfs/fsdriver" "yunion.io/x/onecloud/pkg/hostman/hostdeployer/consts" + "yunion.io/x/onecloud/pkg/util/qemuimg" ) type SKVMGuestDisk struct { - deployer IDeployer + readOnly bool + kvmImagePath string + topImagePath string + deployer IDeployer } -func NewKVMGuestDisk(imagePath, driver string) *SKVMGuestDisk { +func NewKVMGuestDisk(imagePath, driver string, readOnly bool) (*SKVMGuestDisk, error) { + originImage := imagePath + if readOnly { + // if readonly, create a top image over the original image, open device as RW + tmpFileDir, err := ioutil.TempDir(cloudconsts.DeployTempDir(), "kvm_disks") + if err != nil { + log.Errorf("fail to obtain tempFile for readonly kvm disk: %s", err) + return nil, errors.Wrap(err, "ioutil.TempDir") + } + tmpFileName := filepath.Join(tmpFileDir, "disk") + img, err := qemuimg.NewQemuImage(tmpFileName) + if err != nil { + log.Errorf("fail to init qemu image %s", tmpFileName) + return nil, errors.Wrap(err, "NewQemuImage") + } + err = img.CreateQcow2(0, false, imagePath, "", "", "") + if err != nil { + log.Errorf("fail to create overlay qcow2 for kvm disk readonly access") + return nil, errors.Wrap(err, "CreateQcow2") + } + originImage = imagePath + imagePath = tmpFileName + } return &SKVMGuestDisk{ - deployer: newDeployer(imagePath, driver), + readOnly: readOnly, + kvmImagePath: originImage, + topImagePath: imagePath, + deployer: newDeployer(imagePath, driver), + }, nil +} + +func (d *SKVMGuestDisk) Cleanup() { + if d.readOnly { + // if readonly, discard the top image when cleanup + os.RemoveAll(filepath.Dir(d.topImagePath)) } } diff --git a/pkg/hostman/diskutils/vddk.go b/pkg/hostman/diskutils/vddk.go index 099340c054..ddd0dfd1cd 100644 --- a/pkg/hostman/diskutils/vddk.go +++ b/pkg/hostman/diskutils/vddk.go @@ -63,10 +63,11 @@ type VDDKDisk struct { Pid int kvmDisk *SKVMGuestDisk + readOnly bool deployDriver string } -func NewVDDKDisk(vddkInfo *apis.VDDKConInfo, diskPath, deployDriver string) *VDDKDisk { +func NewVDDKDisk(vddkInfo *apis.VDDKConInfo, diskPath, deployDriver string, readOnly bool) (*VDDKDisk, error) { return &VDDKDisk{ Host: vddkInfo.Host, Port: int(vddkInfo.Port), @@ -75,7 +76,8 @@ func NewVDDKDisk(vddkInfo *apis.VDDKConInfo, diskPath, deployDriver string) *VDD VmRef: vddkInfo.Vmref, DiskPath: diskPath, deployDriver: deployDriver, - } + readOnly: readOnly, + }, nil } type Command struct { @@ -137,12 +139,22 @@ func logpath(pid int) string { return fmt.Sprintf("%s/vixDiskLib-%d.log", TMPDIR, pid) } +func (vd *VDDKDisk) Cleanup() { + if vd.kvmDisk != nil { + vd.kvmDisk.Cleanup() + vd.kvmDisk = nil + } +} + func (vd *VDDKDisk) Connect() error { flatFile, err := vd.ConnectBlockDevice() if err != nil { - return err + return errors.Wrap(err, "ConnectBlockDevice") + } + vd.kvmDisk, err = NewKVMGuestDisk(flatFile, vd.deployDriver, vd.readOnly) + if err != nil { + return errors.Wrap(err, "NewKVMGuestDisk") } - vd.kvmDisk = NewKVMGuestDisk(flatFile, vd.deployDriver) return vd.kvmDisk.Connect() } @@ -151,6 +163,8 @@ func (vd *VDDKDisk) Disconnect() error { if err := vd.kvmDisk.Disconnect(); err != nil { log.Errorf("kvm disk disconnect failed %s", err) } + vd.kvmDisk.Cleanup() + vd.kvmDisk = nil } return vd.DisconnectBlockDevice() } diff --git a/pkg/hostman/hostdeployer/deployserver/deployserver.go b/pkg/hostman/hostdeployer/deployserver/deployserver.go index 2550d36580..cbd6ea4f71 100644 --- a/pkg/hostman/hostdeployer/deployserver/deployserver.go +++ b/pkg/hostman/hostdeployer/deployserver/deployserver.go @@ -63,21 +63,27 @@ func (*DeployerServer) DeployGuestFs(ctx context.Context, req *deployapi.DeployP } }() log.Infof("********* Deploy guest fs on %s", req.DiskPath) - var disk = diskutils.GetIDisk(diskutils.DiskParams{ + disk, err := diskutils.GetIDisk(diskutils.DiskParams{ Hypervisor: req.GuestDesc.Hypervisor, DiskPath: req.DiskPath, VddkInfo: req.VddkInfo, - }, DeployOption.ImageDeployDriver) + }, DeployOption.ImageDeployDriver, false) + if err != nil { + log.Errorf("diskutils.GetIDisk fail %s", err) + return new(deployapi.DeployGuestFsResponse), errors.Wrap(err, "GetIDisk") + } + defer disk.Cleanup() if len(req.GuestDesc.Hypervisor) == 0 { req.GuestDesc.Hypervisor = comapi.HYPERVISOR_KVM } - defer disk.Disconnect() if err := disk.Connect(); err != nil { log.Infof("Failed to connect %s disk: %s", req.GuestDesc.Hypervisor, err) - return new(deployapi.DeployGuestFsResponse), nil + return new(deployapi.DeployGuestFsResponse), errors.Wrap(err, "Connect") } + defer disk.Disconnect() root, err := disk.MountRootfs() if err != nil { + log.Infof("Failed mounting rootfs for %s disk", req.GuestDesc.Hypervisor) return new(deployapi.DeployGuestFsResponse), err } defer disk.UmountRootfs(root) @@ -104,16 +110,19 @@ func (*DeployerServer) ResizeFs(ctx context.Context, req *deployapi.ResizeFsPara } }() log.Infof("********* Resize fs on %s", req.DiskPath) - var disk = diskutils.GetIDisk(diskutils.DiskParams{ + disk, err := diskutils.GetIDisk(diskutils.DiskParams{ Hypervisor: req.Hypervisor, DiskPath: req.DiskPath, VddkInfo: req.VddkInfo, - }, DeployOption.ImageDeployDriver) - defer disk.Disconnect() - err = disk.Connect() + }, DeployOption.ImageDeployDriver, false) if err != nil { + return new(deployapi.Empty), errors.Wrap(err, "GetIDisk fail") + } + defer disk.Cleanup() + if err := disk.Connect(); err != nil { return new(deployapi.Empty), errors.Wrap(err, "disk connect failed") } + defer disk.Disconnect() unmount := func(root fsdriver.IRootFsDriver) error { err := disk.UmountRootfs(root) @@ -149,16 +158,21 @@ func (*DeployerServer) ResizeFs(ctx context.Context, req *deployapi.ResizeFsPara func (*DeployerServer) FormatFs(ctx context.Context, req *deployapi.FormatFsParams) (*deployapi.Empty, error) { log.Infof("********* Format fs on %s", req.DiskPath) - gd := diskutils.NewKVMGuestDisk(req.DiskPath, DeployOption.ImageDeployDriver) - defer gd.Disconnect() + gd, err := diskutils.NewKVMGuestDisk(req.DiskPath, DeployOption.ImageDeployDriver, false) + if err != nil { + return new(deployapi.Empty), errors.Wrap(err, "NewKVMGuestDisk") + } + defer gd.Cleanup() + if err := gd.Connect(); err == nil { + defer gd.Disconnect() if err := gd.MakePartition(req.FsFormat); err == nil { err = gd.FormatPartition(req.FsFormat, req.Uuid) if err != nil { - return new(deployapi.Empty), err + return new(deployapi.Empty), errors.Wrap(err, "FormatPartition") } } else { - return new(deployapi.Empty), err + return new(deployapi.Empty), errors.Wrap(err, "MakePartition") } } else { log.Errorf("failed connect kvm disk %s: %s", req.DiskPath, err) @@ -169,34 +183,44 @@ func (*DeployerServer) FormatFs(ctx context.Context, req *deployapi.FormatFsPara func (*DeployerServer) SaveToGlance(ctx context.Context, req *deployapi.SaveToGlanceParams) (*deployapi.SaveToGlanceResponse, error) { log.Infof("********* %s save to glance", req.DiskPath) var ( - kvmDisk = diskutils.NewKVMGuestDisk(req.DiskPath, DeployOption.ImageDeployDriver) osInfo string relInfo *deployapi.ReleaseInfo ) - err := func() error { - err := kvmDisk.Connect() - if err != nil { - return errors.Wrapf(err, "kvmDisk.Connect") - } + kvmDisk, err := diskutils.NewKVMGuestDisk(req.DiskPath, DeployOption.ImageDeployDriver, false) + if err != nil { + return new(deployapi.SaveToGlanceResponse), errors.Wrap(err, "NewKVMGuestDisk") + } + defer kvmDisk.Cleanup() + + err = kvmDisk.Connect() + if err != nil { + log.Errorf("failed connect kvm disk %s: %s", req.DiskPath, err) + } else { defer kvmDisk.Disconnect() - root, err := kvmDisk.MountKvmRootfs() - if err != nil { - return errors.Wrapf(err, "kvmDisk.MountKvmRootfs") - } - defer kvmDisk.UmountKvmRootfs(root) + err = func() error { + var err error + root, err := kvmDisk.MountKvmRootfs() + if err == nil { + defer kvmDisk.UmountKvmRootfs(root) - osInfo = root.GetOs() - relInfo = root.GetReleaseInfo(root.GetPartition()) - if req.Compress { - err = root.PrepareFsForTemplate(root.GetPartition()) - } - - if req.Compress { - kvmDisk.Zerofree() - } - return err - }() + osInfo = root.GetOs() + relInfo = root.GetReleaseInfo(root.GetPartition()) + if req.Compress { + err = root.PrepareFsForTemplate(root.GetPartition()) + if err != nil { + log.Errorf("PrepareFsForTemplate %s", err) + } + } + if req.Compress { + kvmDisk.Zerofree() + } + } else { + log.Errorf("") + } + return err + }() + } return &deployapi.SaveToGlanceResponse{ OsInfo: osInfo, @@ -230,12 +254,17 @@ func (*DeployerServer) getImageInfo(kvmDisk *diskutils.SKVMGuestDisk) (*deployap func (s *DeployerServer) ProbeImageInfo(ctx context.Context, req *deployapi.ProbeImageInfoPramas) (*deployapi.ImageInfo, error) { log.Infof("********* %s probe image info", req.DiskPath) - kvmDisk := diskutils.NewKVMGuestDisk(req.DiskPath, DeployOption.ImageDeployDriver) - defer kvmDisk.Disconnect() - if err := kvmDisk.Connect(); err != nil { - log.Infof("Failed to connect kvm disk %s: %s", req.DiskPath, err) - return new(deployapi.ImageInfo), errors.Error("Disk connector failed to connect image") + kvmDisk, err := diskutils.NewKVMGuestDisk(req.DiskPath, DeployOption.ImageDeployDriver, true) + if err != nil { + return new(deployapi.ImageInfo), errors.Wrap(err, "NewKVMGuestDisk") } + defer kvmDisk.Cleanup() + + if err := kvmDisk.Connect(); err != nil { + log.Errorf("Failed to connect kvm disk %s: %s", req.DiskPath, err) + return new(deployapi.ImageInfo), errors.Wrap(err, "Disk connector failed to connect image") + } + defer kvmDisk.Disconnect() return s.getImageInfo(kvmDisk) } @@ -253,7 +282,7 @@ func (*DeployerServer) ConnectEsxiDisks( ) ret.Disks = make([]*deployapi.EsxiDiskInfo, len(req.AccessInfo)) for i := 0; i < len(req.AccessInfo); i++ { - disk := diskutils.NewVDDKDisk(req.VddkInfo, req.AccessInfo[i].DiskPath, DeployOption.ImageDeployDriver) + disk, _ := diskutils.NewVDDKDisk(req.VddkInfo, req.AccessInfo[i].DiskPath, DeployOption.ImageDeployDriver, false) flatFilePath, err = disk.ConnectBlockDevice() if err != nil { err = errors.Wrapf(err, "disk %s connect block device", req.AccessInfo[i].DiskPath) diff --git a/pkg/hostman/hostdeployer/deployserver/options.go b/pkg/hostman/hostdeployer/deployserver/options.go index ceb423f953..a746b7f61e 100644 --- a/pkg/hostman/hostdeployer/deployserver/options.go +++ b/pkg/hostman/hostdeployer/deployserver/options.go @@ -17,18 +17,24 @@ package deployserver import ( "os" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/cloudcommon/consts" common_options "yunion.io/x/onecloud/pkg/cloudcommon/options" + "yunion.io/x/onecloud/pkg/util/fileutils2" ) type SDeployOptions struct { common_options.HostCommonOptions - PrivatePrefixes []string `help:"IPv4 private prefixes"` - ChntpwPath string `help:"path to chntpw tool" default:"/usr/local/bin/chntpw.static"` - EnableRemoteExecutor bool `help:"Enable remote executor" default:"false"` - CloudrootDir string `help:"User cloudroot home dir" default:"/opt"` - ImageDeployDriver string `help:"Image deploy driver" default:"nbd" choices:"nbd|libguestfs"` - CommonConfigFile string `help:"common config file for container"` + PrivatePrefixes []string `help:"IPv4 private prefixes"` + ChntpwPath string `help:"path to chntpw tool" default:"/usr/local/bin/chntpw.static"` + + CloudrootDir string `help:"User cloudroot home dir" default:"/opt"` + ImageDeployDriver string `help:"Image deploy driver" default:"nbd" choices:"nbd|libguestfs"` + CommonConfigFile string `help:"common config file for container"` + + DeployTempDir string `help:"temp dir for deployer" default:"/opt/cloud/workspace/run/deploy"` } var DeployOption SDeployOptions @@ -44,6 +50,14 @@ func Parse() (hostOpts SDeployOptions) { // keep base options hostOpts.BaseOptions.BaseOptions = baseOpt } + if !fileutils2.Exists(hostOpts.DeployTempDir) { + err := os.MkdirAll(hostOpts.DeployTempDir, 0755) + if err != nil { + log.Fatalf("fail to create %s: %s", hostOpts.DeployTempDir, err) + return + } + } + consts.SetDeployTempDir(hostOpts.DeployTempDir) return hostOpts } diff --git a/pkg/hostman/options/options.go b/pkg/hostman/options/options.go index 1e3d3a1db5..78ac1c85b0 100644 --- a/pkg/hostman/options/options.go +++ b/pkg/hostman/options/options.go @@ -142,11 +142,10 @@ type SHostOptions struct { OvnEipBridge string `help:"name of bridge for eip traffic management" default:"$HOST_OVN_EIP_BRIDGE|breip"` OvnUnderlayMtu int `help:"mtu of ovn underlay network" default:"1500"` - EnableRemoteExecutor bool `help:"Enable remote executor" default:"false"` - EnableHealthChecker bool `help:"enable host health checker" default:"false"` - HealthDriver string `help:"Component save host health state" default:"etcd"` - HostHealthTimeout int `help:"host health timeout" default:"30"` - HostLeaseTimeout int `help:"lease timeout" default:"10"` + EnableHealthChecker bool `help:"enable host health checker" default:"false"` + HealthDriver string `help:"Component save host health state" default:"etcd"` + HostHealthTimeout int `help:"host health timeout" default:"30"` + HostLeaseTimeout int `help:"lease timeout" default:"10"` SyncStorageInfoDurationSecond int `help:"sync storage size duration, unit is second" default:"60"` StartHostIgnoreSysError bool `help:"start host agent ignore sys error" default:"false"` diff --git a/pkg/image/models/image_subs.go b/pkg/image/models/image_subs.go index 2857b4adb5..0d44c8cd1b 100644 --- a/pkg/image/models/image_subs.go +++ b/pkg/image/models/image_subs.go @@ -201,7 +201,7 @@ func (self *SImageSubformat) SaveTorrent() error { } _, err = torrentutils.GenerateTorrent(imgPath, torrent.GetTrackers(), torrentPath) if err != nil { - log.Errorf("torrentutils.GenerateTorrent fail %s", err) + log.Errorf("torrentutils.GenerateTorrent %s fail %s", imgPath, err) return err } checksum, err := fileutils2.MD5(torrentPath) @@ -314,8 +314,8 @@ func (self *SImageSubformat) GetDetails() SImageSubformatDetails { return details } -func (self *SImageSubformat) isActive(useFast bool) bool { - active, reason := isActive(self.GetLocalLocation(), self.Size, self.Checksum, self.FastHash, useFast) +func (self *SImageSubformat) isActive(useFast bool, noCheckum bool) bool { + active, reason := isActive(self.GetLocalLocation(), self.Size, self.Checksum, self.FastHash, useFast, noCheckum) if active || reason != FileChecksumMismatch { return active } @@ -326,7 +326,7 @@ func (self *SImageSubformat) isActive(useFast bool) bool { } func (self *SImageSubformat) isTorrentActive() bool { - active, _ := isActive(self.getLocalTorrentLocation(), self.TorrentSize, self.TorrentChecksum, "", false) + active, _ := isActive(self.getLocalTorrentLocation(), self.TorrentSize, self.TorrentChecksum, "", false, false) return active } @@ -346,9 +346,9 @@ func (self *SImageSubformat) setTorrentStatus(status string) error { return err } -func (self *SImageSubformat) checkStatus(useFast bool) { +func (self *SImageSubformat) checkStatus(useFast bool, noChecksum bool) { if strings.HasPrefix(self.Location, LocalFilePrefix) { - if self.isActive(useFast) { + if self.isActive(useFast, noChecksum) { if self.Status != api.IMAGE_STATUS_ACTIVE { self.SetStatus(api.IMAGE_STATUS_ACTIVE) } diff --git a/pkg/image/models/images.go b/pkg/image/models/images.go index 29456404d7..de8c98c32c 100644 --- a/pkg/image/models/images.go +++ b/pkg/image/models/images.go @@ -1356,7 +1356,7 @@ const ( Others ) -func isActive(localPath string, size int64, chksum string, fastHash string, useFastHash bool) (bool, sUnactiveReason) { +func isActive(localPath string, size int64, chksum string, fastHash string, useFastHash bool, noChecksum bool) (bool, sUnactiveReason) { if len(localPath) == 0 || !fileutils2.Exists(localPath) { log.Errorf("invalid file: %s", localPath) return false, FileNoExists @@ -1365,7 +1365,7 @@ func isActive(localPath string, size int64, chksum string, fastHash string, useF log.Errorf("size mistmatch: %s", localPath) return false, FileSizeMismatch } - if len(chksum) == 0 || len(fastHash) == 0 { + if len(chksum) == 0 || len(fastHash) == 0 || noChecksum { return true, Others } if useFastHash && len(fastHash) > 0 { @@ -1396,8 +1396,8 @@ func (self *SImage) IsIso() bool { return self.DiskFormat == string(api.ImageTypeISO) } -func (self *SImage) isActive(useFast bool) bool { - active, reason := isActive(self.GetLocalLocation(), self.Size, self.Checksum, self.FastHash, useFast) +func (self *SImage) isActive(useFast bool, noChecksum bool) bool { + active, reason := isActive(self.GetLocalLocation(), self.Size, self.Checksum, self.FastHash, useFast, noChecksum) if active || reason != FileChecksumMismatch { return active } @@ -1412,7 +1412,7 @@ func (self *SImage) DoCheckStatus(ctx context.Context, userCred mcclient.TokenCr return } if IsCheckStatusEnabled(self) { - if self.isActive(useFast) { + if self.isActive(useFast, true) { if self.Status != api.IMAGE_STATUS_ACTIVE { self.SetStatus(userCred, api.IMAGE_STATUS_ACTIVE, "check active") } @@ -1613,10 +1613,11 @@ func (img *SImage) PerformPrivate(ctx context.Context, userCred mcclient.TokenCr } func (img *SImage) PerformProbe(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.PerformProbeInput) (jsonutils.JSONObject, error) { - if img.Status != api.IMAGE_STATUS_ACTIVE { + if img.Status != api.IMAGE_STATUS_ACTIVE && img.Status != api.IMAGE_STATUS_SAVED { return nil, httperrors.NewInvalidStatusError("cannot probe in status %s", img.Status) } - err := img.StartImagePipeline(ctx, userCred, true) + img.SetStatus(userCred, api.IMAGE_STATUS_PROBING, "perform probe") + err := img.StartImagePipeline(ctx, userCred, false) if err != nil { return nil, errors.Wrap(err, "ImageProbeAndCustomization") } @@ -1668,6 +1669,7 @@ func (image *SImage) doProbeImageInfo(ctx context.Context, userCred mcclient.Tok if err != nil { return false, errors.Wrap(err, "ProbeImageInfo") } + log.Infof("image probe info: %s", jsonutils.Marshal(imageInfo)) err = image.updateImageInfo(ctx, userCred, imageInfo) if err != nil { return false, errors.Wrap(err, "updateImageInfo") @@ -1680,14 +1682,18 @@ func (image *SImage) updateImageInfo( userCred mcclient.TokenCredential, imageInfo *deployapi.ImageInfo, ) error { - if image.OsArch != imageInfo.OsInfo.Arch { - db.Update(image, func() error { - image.OsArch = imageInfo.OsInfo.Arch - return nil - }) - } + db.Update(image, func() error { + image.OsArch = imageInfo.OsInfo.Arch + return nil + }) + imageProperties := jsonutils.Marshal(imageInfo.OsInfo).(*jsonutils.JSONDict) + imageProperties.Set(api.IMAGE_OS_ARCH, jsonutils.NewString(imageInfo.OsInfo.Arch)) + imageProperties.Set("os_version", jsonutils.NewString(imageInfo.OsInfo.Version)) + imageProperties.Set("os_distribution", jsonutils.NewString(imageInfo.OsInfo.Distro)) + imageProperties.Set("os_language", jsonutils.NewString(imageInfo.OsInfo.Language)) + imageProperties.Set(api.IMAGE_OS_TYPE, jsonutils.NewString(imageInfo.OsType)) imageProperties.Set(api.IMAGE_PARTITION_TYPE, jsonutils.NewString(imageInfo.PhysicalPartitionType)) if imageInfo.IsUefiSupport { @@ -1770,8 +1776,8 @@ func (image *SImage) isLocal() bool { return strings.HasPrefix(image.Location, LocalFilePrefix) } -func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mcclient.TokenCredential) error { - oldStatus := image.Status +func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mcclient.TokenCredential) (bool, error) { + uploaded := false if image.isLocal() { imagePath := image.GetLocalLocation() image.SetStatus(userCred, api.IMAGE_STATUS_SAVING, "save image to specific storage") @@ -1781,9 +1787,10 @@ func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mccl log.Errorf("Failed save image to specific storage %s", err) errStr := fmt.Sprintf("save image to storage %s: %v", storage.Type(), err) image.SetStatus(userCred, api.IMAGE_STATUS_SAVE_FAIL, errStr) - return errors.Wrapf(err, "save image to storage %s", storage.Type()) + return false, errors.Wrapf(err, "save image to storage %s", storage.Type()) } if location != image.Location { + uploaded = true // save success! to update the location _, err = db.Update(image, func() error { image.Location = location @@ -1791,7 +1798,7 @@ func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mccl }) if err != nil { log.Errorf("failed update image location %s", err) - return errors.Wrap(err, "update image location") + return false, errors.Wrap(err, "update image location") } // update location success, remove local copy if err = procutils.NewCommand("rm", "-f", imagePath).Run(); err != nil { @@ -1800,16 +1807,6 @@ func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mccl } image.SetStatus(userCred, api.IMAGE_STATUS_ACTIVE, "save image to specific storage complete") } - if oldStatus != api.IMAGE_STATUS_ACTIVE { - kwargs := jsonutils.NewDict() - kwargs.Set("name", jsonutils.NewString(image.GetName())) - osType, err := ImagePropertyManager.GetProperty(image.Id, api.IMAGE_OS_TYPE) - if err == nil { - kwargs.Set("os_type", jsonutils.NewString(osType.Value)) - } - notifyclient.SystemNotifyWithCtx(ctx, notify.NotifyPriorityNormal, notifyclient.IMAGE_ACTIVED, kwargs) - notifyclient.NotifyImportantWithCtx(ctx, []string{userCred.GetUserId()}, false, notifyclient.IMAGE_ACTIVED, kwargs) - } subimgs := ImageSubformatManager.GetAllSubImages(image.Id) for i := 0; i < len(subimgs); i++ { @@ -1832,8 +1829,9 @@ func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mccl if err != nil { log.Errorf("Failed save image to sepcific storage %s", err) subimgs[i].SetStatus(api.IMAGE_STATUS_SAVE_FAIL) - return errors.Wrapf(err, "save sub image %s to storage %s", subimgs[i].Format, storage.Type()) + return false, errors.Wrapf(err, "save sub image %s to storage %s", subimgs[i].Format, storage.Type()) } else if subimgs[i].Location != location { + uploaded = true _, err := db.Update(&subimgs[i], func() error { subimgs[i].Location = location return nil @@ -1851,13 +1849,13 @@ func (image *SImage) doUploadPermanentStorage(ctx context.Context, userCred mccl }) } } - return nil + return uploaded, nil } -func (img *SImage) doConvert(ctx context.Context, userCred mcclient.TokenCredential) error { +func (img *SImage) doConvert(ctx context.Context, userCred mcclient.TokenCredential) (bool, error) { if img.IsGuestImage.IsTrue() { // for image the part of a guest image, convert is not necessary. - return nil + return false, nil } needConvert := false subimgs := ImageSubformatManager.GetAllSubImages(img.Id) @@ -1869,10 +1867,10 @@ func (img *SImage) doConvert(ctx context.Context, userCred mcclient.TokenCredent // no need to have this subformat err := subimgs[i].cleanup(ctx, userCred) if err != nil { - return errors.Wrap(err, "cleanup sub image") + return false, errors.Wrap(err, "cleanup sub image") } } - subimgs[i].checkStatus(true) + subimgs[i].checkStatus(true, false) if subimgs[i].Status != api.IMAGE_STATUS_ACTIVE { needConvert = true } @@ -1882,18 +1880,18 @@ func (img *SImage) doConvert(ctx context.Context, userCred mcclient.TokenCredent if (img.Status == api.IMAGE_STATUS_SAVED || img.Status == api.IMAGE_STATUS_ACTIVE) && needConvert { err := img.migrateSubImage(ctx) if err != nil { - return errors.Wrap(err, "migrateSubImage") + return false, errors.Wrap(err, "migrateSubImage") } err = img.makeSubImages(ctx) if err != nil { - return errors.Wrap(err, "makeSubImages") + return false, errors.Wrap(err, "makeSubImages") } err = img.doConvertAllSubformats() if err != nil { - return errors.Wrap(err, "doConvertAllSubformats") + return false, errors.Wrap(err, "doConvertAllSubformats") } } - return nil + return needConvert, nil } func (img *SImage) doEncrypt(ctx context.Context, userCred mcclient.TokenCredential) (bool, error) { @@ -1961,6 +1959,7 @@ func (img *SImage) setEncryptStatus(userCred mcclient.TokenCredential, status st } func (img *SImage) Pipeline(ctx context.Context, userCred mcclient.TokenCredential, skipProbe bool) error { + updated := false needChecksum := false // do probe if !skipProbe { @@ -1971,6 +1970,8 @@ func (img *SImage) Pipeline(ctx context.Context, userCred mcclient.TokenCredenti if alterd { needChecksum = true } + } else { + log.Debugf("skipProbe image...") } // do encrypt { @@ -1990,17 +1991,36 @@ func (img *SImage) Pipeline(ctx context.Context, userCred mcclient.TokenCredenti } { // do conert - err := img.doConvert(ctx, userCred) + converted, err := img.doConvert(ctx, userCred) if err != nil { return errors.Wrap(err, "doConvert") } + if converted { + updated = true + } } { // do doUploadPermanent - err := img.doUploadPermanentStorage(ctx, userCred) + uploaded, err := img.doUploadPermanentStorage(ctx, userCred) if err != nil { return errors.Wrap(err, "doUploadPermanentStorage") } + if uploaded { + updated = true + } + } + if img.Status != api.IMAGE_STATUS_ACTIVE { + img.SetStatus(userCred, api.IMAGE_STATUS_ACTIVE, "image pipeline complete") + } + if updated { + kwargs := jsonutils.NewDict() + kwargs.Set("name", jsonutils.NewString(img.GetName())) + osType, err := ImagePropertyManager.GetProperty(img.Id, api.IMAGE_OS_TYPE) + if err == nil { + kwargs.Set("os_type", jsonutils.NewString(osType.Value)) + } + notifyclient.SystemNotifyWithCtx(ctx, notify.NotifyPriorityNormal, notifyclient.IMAGE_ACTIVED, kwargs) + notifyclient.NotifyImportantWithCtx(ctx, []string{userCred.GetUserId()}, false, notifyclient.IMAGE_ACTIVED, kwargs) } return nil } diff --git a/pkg/image/options/options.go b/pkg/image/options/options.go index 24d9c16d3b..649f4981c3 100644 --- a/pkg/image/options/options.go +++ b/pkg/image/options/options.go @@ -20,7 +20,7 @@ import ( ) type SImageOptions struct { - common_options.CommonOptions + common_options.HostCommonOptions common_options.DBOptions @@ -40,7 +40,7 @@ type SImageOptions struct { TorrentClientPath string `help:"path to torrent executable" default:"/opt/yunion/bin/torrent"` - DeployServerSocketPath string `help:"Deploy server listen socket path" default:"/var/run/onecloud/deploy.sock"` + // DeployServerSocketPath string `help:"Deploy server listen socket path" default:"/var/run/onecloud/deploy.sock"` StorageDriver string `help:"image backend storage" default:"local" choices:"s3|local"` diff --git a/pkg/image/service/service.go b/pkg/image/service/service.go index 8ec996ea7a..8187dd7791 100644 --- a/pkg/image/service/service.go +++ b/pkg/image/service/service.go @@ -21,6 +21,7 @@ import ( "strings" "time" + execlient "yunion.io/x/executor/client" "yunion.io/x/log" _ "yunion.io/x/sqlchemy/backends" @@ -80,6 +81,12 @@ func StartService() { } } + log.Infof("exec socket path: %s", options.Options.ExecutorSocketPath) + if options.Options.EnableRemoteExecutor { + execlient.Init(options.Options.ExecutorSocketPath) + procutils.SetRemoteExecutor() + } + log.Infof("Target image formats %#v", opts.TargetImageFormats) app_common.InitAuth(commonOpts, func() { @@ -103,17 +110,18 @@ func StartService() { common_options.StartOptionManager(opts, opts.ConfigSyncPeriodSeconds, api.SERVICE_TYPE, api.SERVICE_VERSION, options.OnOptionsChange) models.Init(options.Options.StorageDriver) - if options.Options.StorageDriver == api.IMAGE_STORAGE_DRIVER_S3 { - initS3() - } if len(options.Options.DeployServerSocketPath) > 0 { log.Infof("deploy server socket path: %s", options.Options.DeployServerSocketPath) deployclient.Init(options.Options.DeployServerSocketPath) } - // Check the images after everything is ready - go models.CheckImages() + if options.Options.StorageDriver == api.IMAGE_STORAGE_DRIVER_S3 { + go initS3() + } else { + // Check the images after everything is ready + go models.CheckImages() + } if !opts.IsSlaveNode { cron := cronman.InitCronJobManager(true, options.Options.CronJobWorkerCount) @@ -175,6 +183,17 @@ func initS3() { log.Fatalf("fail to create %s: %s", options.Options.S3MountPoint, err) } } + // check the s3 mount point has been mounted by previous glance instance + // if it is mounted, just wait + for { + if err := procutils.NewRemoteCommandAsFarAsPossible("mountpoint", options.Options.S3MountPoint).Run(); err == nil { + // sleep 1 second + procutils.NewRemoteCommandAsFarAsPossible("umount", options.Options.S3MountPoint).Run() + time.Sleep(time.Second) + } else { + break + } + } out, err := procutils.NewCommand("s3fs", options.Options.S3BucketName, options.Options.S3MountPoint, @@ -182,4 +201,17 @@ func initS3() { if err != nil { log.Fatalf("failed mount s3fs %s %s", err, out) } + log.Infof("s3fs: %s", out) + + for { + if err := procutils.NewRemoteCommandAsFarAsPossible("mountpoint", options.Options.S3MountPoint).Run(); err != nil { + // sleep 1 second + time.Sleep(time.Second) + } else { + break + } + } + + // check image after s3 mounted + models.CheckImages() } diff --git a/pkg/util/procutils/procutils_test.go b/pkg/util/procutils/procutils_test.go new file mode 100644 index 0000000000..c54aad2fe3 --- /dev/null +++ b/pkg/util/procutils/procutils_test.go @@ -0,0 +1,53 @@ +// 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 procutils + +import ( + "os" + "testing" +) + +func TestStat(t *testing.T) { + cases := []struct { + filename string + exists bool + isdir bool + }{ + { + filename: "/", + exists: true, + isdir: true, + }, + { + filename: "/tmp/__0_1_2_3_a_b_c_d____", + exists: false, + isdir: false, + }, + } + for _, c := range cases { + fi, err := RemoteStat(c.filename) + if err != nil { + if !c.exists && os.IsNotExist(err) { + // ok + } else { + t.Errorf("expect exists: %v err: %s", c.exists, err) + } + } else { + if fi.IsDir() != c.isdir { + t.Errorf("isdir want: %v got: %v", c.isdir, fi.IsDir()) + } + } + } +} diff --git a/pkg/util/procutils/remote_readdir.go b/pkg/util/procutils/remote_readdir.go new file mode 100644 index 0000000000..c9bb276e0f --- /dev/null +++ b/pkg/util/procutils/remote_readdir.go @@ -0,0 +1,100 @@ +// 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 procutils + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/timeutils" +) + +var ( + splitReg = regexp.MustCompile(`\s+`) +) + +func split(line string) []string { + return splitReg.Split(line, -1) +} + +func parseLsLine(line string) (os.FileInfo, error) { + // drwxr-xr-x. 7 1000 20 4096 2022-02-16 08:10:21.660000000 +0800 .vim + // -rw-------. 1 0 0 23658 2022-04-21 17:31:04.320995359 +0800 .viminfo + line = strings.TrimSpace(line) + if len(line) < 10 { + return nil, errors.Error("invalid ls line: too short") + } + parts := split(line) + if len(parts) < 9 { + return nil, errors.Error(fmt.Sprintf("invalid ls line: parts %d", len(parts))) + } + ftype := "file" + switch parts[0][0] { + case 'd': + ftype = "directory" + case 'l': + ftype = "link" + } + size, err := strconv.ParseInt(parts[4], 10, 64) + if err != nil { + return nil, errors.Wrap(err, "Parse size") + } + tmstr := fmt.Sprintf("%sT%s%s:00", parts[5], parts[6], parts[7][:3]) + atime, err := timeutils.ParseTimeStr(tmstr) + if err != nil { + return nil, errors.Wrap(err, "parse time") + } + tmstr = strings.Join(parts[5:8], " ") + nameIndex := strings.Index(line, tmstr) + len(tmstr) + 1 + name := strings.TrimSpace(line[nameIndex:]) + if ftype == "link" { + arrowPos := strings.Index(name, "->") + if arrowPos > 0 { + name = strings.TrimSpace(name[:arrowPos]) + } + } + fs := &sFileStat{ + FileSize: size, + FileType: ftype, + FileName: name, + LastModAt: atime, + } + return fs, nil +} + +func RemoteReadDir(dirname string) ([]os.FileInfo, error) { + output, err := NewRemoteCommandAsFarAsPossible("ls", "-la1n", "--full-time", dirname).Output() + if err != nil { + if strings.Contains(strings.ToLower(string(output)), "no such file or directory") { + return nil, os.ErrNotExist + } + return nil, errors.Wrap(err, "NewRemoteCommandAsFarAsPossible") + } + files := make([]os.FileInfo, 0) + lines := strings.Split(string(output), "\n") + for _, line := range lines { + f, err := parseLsLine(line) + if err != nil { + // log.Errorf("parseLsLine %s fail %s", line, err) + } else { + files = append(files, f) + } + } + return files, nil +} diff --git a/pkg/util/procutils/remote_readdir_test.go b/pkg/util/procutils/remote_readdir_test.go new file mode 100644 index 0000000000..741d763cb7 --- /dev/null +++ b/pkg/util/procutils/remote_readdir_test.go @@ -0,0 +1,54 @@ +// 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 procutils + +import ( + "testing" +) + +func TestReadDir(t *testing.T) { + files, err := RemoteReadDir(".") + if err != nil { + t.Errorf("RemoteReadDIr %s", err) + } else { + for _, f := range files { + t.Logf("%s %d %v %s", f.Name(), f.Size(), f.IsDir(), f.ModTime()) + } + } +} + +func TestParseLsLine(t *testing.T) { + cases := []string{ + "dr-x------. 45 0 0 16384 2022-04-26 19:48:03.811985235 +0800 .", + "drwxr-xr-x. 26 0 0 4096 2022-04-20 10:06:16.339991488 +0800 ..", + "drwxr-xr-x. 2 0 0 4096 2022-04-01 14:20:53.293839273 +0800 0401", + "drwxr-xr-x. 2 0 0 4096 2022-04-05 15:39:23.634908120 +0800 0405image", + "-rw-r--r--. 1 0 0 335 2022-01-10 09:12:34.924898855 +0800 1.c", + "-rw-r--r--. 1 0 0 304 2022-03-04 11:10:00.803987595 +0800 2.c", + "drwx------. 4 0 0 4096 2021-12-08 11:08:04.304950170 +0800 .ansible", + "-rw-r--r--. 1 0 0 26115746 2022-03-22 19:00:45.368976212 +0800 apigateway-ee-200200321.tgz", + "lrwxrwxrwx. 1 0 0 7 2021-12-16 10:19:58.924982587 +0800 .bash_profile -> .bashrc", + "-rw-------. 1 0 0 4107 2022-04-18 17:26:56.108984702 +0800 .bashrc", + "drwx------. 2 0 0 4096 2021-12-20 20:02:30.483709077 +0800 build", + } + for _, c := range cases { + f, err := parseLsLine(c) + if err != nil { + t.Errorf("parseLsLine %s fail %s", c, err) + } else { + t.Logf("%s %d %v %s", f.Name(), f.Size(), f.IsDir(), f.ModTime()) + } + } +} diff --git a/pkg/util/procutils/remote_stat.go b/pkg/util/procutils/remote_stat.go new file mode 100644 index 0000000000..18845fac7c --- /dev/null +++ b/pkg/util/procutils/remote_stat.go @@ -0,0 +1,78 @@ +// 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 procutils + +import ( + "os" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" +) + +type sFileStat struct { + FileSize int64 `json:"file_size"` + FileType string `json:"file_type"` + FileName string `json:"file_name"` + LastModAt time.Time `json:"last_mod_at"` +} + +func (s *sFileStat) Name() string { + return s.FileName +} + +func (s *sFileStat) Size() int64 { + return s.FileSize +} + +func (s *sFileStat) Mode() os.FileMode { + if s.IsDir() { + return os.ModeDir + } + return os.FileMode(0) +} + +func (s *sFileStat) ModTime() time.Time { + return s.LastModAt +} + +func (s *sFileStat) IsDir() bool { + return s.FileType == "directory" +} + +func (s *sFileStat) Sys() interface{} { + return nil +} + +func RemoteStat(filename string) (os.FileInfo, error) { + output, err := NewRemoteCommandAsFarAsPossible("stat", "-c", `{"file_size":%s,"file_name":"%n","file_type":"%F"}`, filename).Output() + if err != nil { + if strings.Contains(strings.ToLower(string(output)), "no such file or directory") { + return nil, os.ErrNotExist + } + return nil, errors.Wrap(err, "NewRemoteCommandAsFarAsPossible") + } + json, err := jsonutils.Parse(output) + if err != nil { + return nil, errors.Error(output) + } + fs := &sFileStat{} + err = json.Unmarshal(fs) + if err != nil { + return nil, errors.Wrap(err, "json.Unmarshal") + } + return fs, nil +} diff --git a/pkg/util/qemuimg/qemuimg.go b/pkg/util/qemuimg/qemuimg.go index 72d4786d70..a781ea3b3f 100644 --- a/pkg/util/qemuimg/qemuimg.go +++ b/pkg/util/qemuimg/qemuimg.go @@ -94,7 +94,8 @@ func (img *SQemuImage) parse() error { } else if strings.HasPrefix(img.Path, api.STORAGE_RBD) { img.ActualSizeBytes = 0 } else { - fileInfo, err := os.Stat(img.Path) + // check file existence + fileInfo, err := procutils.RemoteStat(img.Path) if err != nil { if !os.IsNotExist(err) { return err diff --git a/pkg/util/qemutils/qemutils.go b/pkg/util/qemutils/qemutils.go index ddede56e3b..dff78d4f6c 100644 --- a/pkg/util/qemutils/qemutils.go +++ b/pkg/util/qemutils/qemutils.go @@ -16,13 +16,14 @@ package qemutils import ( "fmt" - "io/ioutil" - "os" "path" "regexp" "sort" "strings" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/util/procutils" "yunion.io/x/onecloud/pkg/util/version" ) @@ -59,17 +60,23 @@ func getQemuCmd(cmd, version string) string { func getQemuCmdByVersion(cmd, version string) string { p := path.Join(fmt.Sprintf("/usr/local/qemu-%s/bin", version), cmd) - if _, err := os.Stat(p); !os.IsNotExist(err) { + if _, err := procutils.RemoteStat(p); err == nil { return p + } else { + log.Errorf("stat %s: %s", p, err) } cmd = cmd + "_" + version p = path.Join(USER_LOCAL_BIN, cmd) - if _, err := os.Stat(p); !os.IsNotExist(err) { + if _, err := procutils.RemoteStat(p); err == nil { return p + } else { + log.Errorf("stat %s: %s", p, err) } p = path.Join(USER_BIN, cmd) - if _, err := os.Stat(p); !os.IsNotExist(err) { + if _, err := procutils.RemoteStat(p); err == nil { return p + } else { + log.Errorf("stat %s: %s", p, err) } return "" } @@ -92,7 +99,7 @@ func getCmdVersion(cmd string) string { func getQemuDefaultCmd(cmd string) string { var qemus = make([]string, 0) - if files, err := ioutil.ReadDir("/usr/local"); err == nil { + if files, err := procutils.RemoteReadDir("/usr/local"); err == nil { for i := 0; i < len(files); i++ { if strings.HasPrefix(files[i].Name(), "qemu-") { qemus = append(qemus, files[i].Name()) @@ -104,15 +111,17 @@ func getQemuDefaultCmd(cmd string) string { getQemuVersion(qemus[j])) }) p := fmt.Sprintf("/usr/local/%s/bin/%s", qemus[len(qemus)-1], cmd) - if _, err := os.Stat(p); !os.IsNotExist(err) { + if _, err := procutils.RemoteStat(p); err == nil { return p + } else { + log.Errorf("stat %s: %s", p, err) } } } cmds := make([]string, 0) for _, dir := range []string{USER_LOCAL_BIN, USER_BIN} { - if files, err := ioutil.ReadDir(dir); err == nil { + if files, err := procutils.RemoteReadDir(dir); err == nil { for i := 0; i < len(files); i++ { if strings.HasPrefix(files[i].Name(), cmd) { cmds = append(cmds, files[i].Name()) @@ -124,8 +133,10 @@ func getQemuDefaultCmd(cmd string) string { getCmdVersion(cmds[j])) }) p := path.Join(dir, cmds[len(cmds)-1]) - if _, err := os.Stat(p); !os.IsNotExist(err) { + if _, err := procutils.RemoteStat(p); err == nil { return p + } else { + log.Errorf("stat %s: %s", p, err) } } }