losetup.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package losetup
  15. import (
  16. "fmt"
  17. "path/filepath"
  18. "strconv"
  19. "strings"
  20. "sync"
  21. "yunion.io/x/jsonutils"
  22. "yunion.io/x/log"
  23. "yunion.io/x/pkg/errors"
  24. "yunion.io/x/onecloud/pkg/util/mountutils"
  25. "yunion.io/x/onecloud/pkg/util/procutils"
  26. )
  27. const (
  28. LOSETUP_COMMAND = "losetup"
  29. )
  30. var (
  31. attachDeviceLock = sync.Mutex{}
  32. )
  33. type Command struct {
  34. Path string
  35. Args []string
  36. output string
  37. }
  38. func NewCommand(path string, args ...string) *Command {
  39. return &Command{
  40. Path: path,
  41. Args: args,
  42. output: "",
  43. }
  44. }
  45. func (cmd *Command) AddArgs(args ...string) *Command {
  46. cmd.Args = append(cmd.Args, args...)
  47. return cmd
  48. }
  49. func (cmd *Command) Run() (*Command, error) {
  50. ecmd := procutils.NewRemoteCommandAsFarAsPossible(cmd.Path, cmd.Args...)
  51. out, err := ecmd.Output()
  52. if err != nil {
  53. err = errors.Wrapf(err, "%s %v: %s", cmd.Path, cmd.Args, out)
  54. }
  55. cmd.output = string(out)
  56. return cmd, err
  57. }
  58. func (cmd *Command) Output() string {
  59. return cmd.output
  60. }
  61. type LosetupCommand struct {
  62. *Command
  63. }
  64. func NewLosetupCommand() *LosetupCommand {
  65. return &LosetupCommand{
  66. Command: NewCommand(LOSETUP_COMMAND),
  67. }
  68. }
  69. func parseJsonOutput(content string) (*Devices, error) {
  70. obj, err := jsonutils.ParseString(content)
  71. if err != nil {
  72. return nil, errors.Wrapf(err, "parse json: %s", content)
  73. }
  74. devs := new(Devices)
  75. if err := obj.Unmarshal(devs); err != nil {
  76. return nil, errors.Wrapf(err, "unmarshal json: %s", content)
  77. }
  78. return devs, nil
  79. }
  80. func ListDevices() (*Devices, error) {
  81. cmd, err := NewLosetupCommand().AddArgs("--json").Run()
  82. errs := make([]error, 0)
  83. if err != nil {
  84. errs = append(errs, errors.Wrap(err, "list by json"))
  85. devs, err2 := listDevicesOldVersion()
  86. if err2 != nil {
  87. errs = append(errs, errors.Wrap(err, "list by using old way"))
  88. } else {
  89. return devs, nil
  90. }
  91. return nil, errors.NewAggregate(errs)
  92. }
  93. output := cmd.Output()
  94. return parseJsonOutput(output)
  95. }
  96. func listDevicesOldVersion() (*Devices, error) {
  97. cmd, err := NewLosetupCommand().AddArgs("-l", "-O", "NAME,BACK-FILE").Run()
  98. if err != nil {
  99. return nil, err
  100. }
  101. output := cmd.Output()
  102. devs, err := parseDevices(output)
  103. return devs, err
  104. }
  105. /*func GetUnusedDevice() (string, error) {
  106. // find first unused device
  107. cmd, err := NewLosetupCommand().AddArgs("-f").Run()
  108. if err != nil {
  109. return "", err
  110. }
  111. return strings.TrimSuffix(cmd.Output(), "\n"), nil
  112. }*/
  113. func attachDevice(devPath string, filePath string, partScan bool) (*Device, error) {
  114. // See man-page: https://man7.org/linux/man-pages/man8/losetup.8.html
  115. // The loop device setup is not an atomic operation when used with
  116. // --find, and losetup does not protect this operation by any lock.
  117. attachDeviceLock.Lock()
  118. defer attachDeviceLock.Unlock()
  119. oldDevs, err := ListDevices()
  120. if err != nil {
  121. return nil, err
  122. }
  123. oldDev := oldDevs.GetDeviceByFile(filePath)
  124. if oldDev != nil {
  125. //return nil, fmt.Errorf("file %q already attached to %q, attached twice???", oldDev.BackFile, oldDev.Name)
  126. return oldDev, nil
  127. }
  128. args := []string{}
  129. if partScan {
  130. args = append(args, "-P")
  131. }
  132. if devPath != "" {
  133. args = append(args, []string{devPath, filePath}...)
  134. } else {
  135. // args = append(args, []string{"--find", "--nooverlap", filePath}...)
  136. args = append(args, []string{"--find", filePath}...)
  137. }
  138. _, err = NewLosetupCommand().AddArgs(args...).Run()
  139. if err != nil {
  140. return nil, err
  141. }
  142. devs, err := ListDevices()
  143. if err != nil {
  144. return nil, err
  145. }
  146. dev := devs.GetDeviceByFile(filePath)
  147. if dev == nil {
  148. return nil, fmt.Errorf("Not found loop device by file: %s", filePath)
  149. }
  150. return dev, nil
  151. }
  152. func AttachDeviceWithPath(devPath string, filePath string, partScan bool) (*Device, error) {
  153. return attachDevice(devPath, filePath, partScan)
  154. }
  155. func AttachDevice(filePath string, partScan bool) (*Device, error) {
  156. return attachDevice("", filePath, partScan)
  157. }
  158. // converts a raw key value pair string into a map of key value pairs
  159. // example raw string of `foo="0" bar="1" baz="biz"` is returned as:
  160. // map[string]string{"foo":"0", "bar":"1", "baz":"biz"}
  161. func parseKeyValuePairString(propsRaw string) map[string]string {
  162. // first split the single raw string on spaces and initialize a map of
  163. // a length equal to the number of pairs
  164. props := strings.Split(propsRaw, " ")
  165. propMap := make(map[string]string, len(props))
  166. for _, kvpRaw := range props {
  167. // split each individual key value pair on the equals sign
  168. kvp := strings.Split(kvpRaw, "=")
  169. if len(kvp) == 2 {
  170. // first element is the final key, second element is the final value
  171. // (don't forget to remove surrounding quotes from the value)
  172. propMap[kvp[0]] = strings.Replace(kvp[1], `"`, "", -1)
  173. }
  174. }
  175. return propMap
  176. }
  177. const (
  178. // DiskType is a disk type
  179. DiskType = "disk"
  180. // SSDType is an sdd type
  181. SSDType = "ssd"
  182. // PartType is a partition type
  183. PartType = "part"
  184. // CryptType is an encrypted type
  185. CryptType = "crypt"
  186. // LVMType is an LVM type
  187. LVMType = "lvm"
  188. // MultiPath is for multipath devices
  189. MultiPath = "mpath"
  190. // LinearType is a linear type
  191. LinearType = "linear"
  192. // LoopType is a loop device type
  193. LoopType = "loop"
  194. sgdiskCmd = "sgdisk"
  195. // CephLVPrefix is the prefix of a LV owned by ceph-volume
  196. CephLVPrefix = "ceph--"
  197. // DeviceMapperPrefix is the prefix of a LV from the device mapper interface
  198. DeviceMapperPrefix = "dm-"
  199. )
  200. // Partition represents a partition metadata
  201. type Partition struct {
  202. Name string
  203. Size uint64
  204. Label string
  205. Filesystem string
  206. }
  207. // ref: https://github.com/rook/rook/blob/master/pkg/util/sys/device.go#L135
  208. func GetDevicePartions(devicePath string) ([]string, error) {
  209. cmd, err := NewCommand("lsblk", devicePath, "--bytes", "--pairs", "--output", "NAME,SIZE,TYPE,PKNAME").Run()
  210. if err != nil {
  211. return nil, errors.Wrapf(err, "get device %s partions", devicePath)
  212. }
  213. device := filepath.Base(devicePath)
  214. output := cmd.Output()
  215. partInfo := strings.Split(output, "\n")
  216. var partitions = make([]string, 0)
  217. var totalPartitionSize uint64
  218. for _, info := range partInfo {
  219. props := parseKeyValuePairString(info)
  220. name := props["NAME"]
  221. if name == device {
  222. // found the main device
  223. log.Infof("Device found - %s", name)
  224. _, err = strconv.ParseUint(props["SIZE"], 10, 64)
  225. if err != nil {
  226. return nil, fmt.Errorf("failed to get device %s size. %+v", device, err)
  227. }
  228. } else if props["PKNAME"] == device && props["TYPE"] == PartType {
  229. // found a partition
  230. p := Partition{Name: name}
  231. p.Size, err = strconv.ParseUint(props["SIZE"], 10, 64)
  232. if err != nil {
  233. return nil, fmt.Errorf("failed to get partition %s size. %+v", name, err)
  234. }
  235. totalPartitionSize += p.Size
  236. partitions = append(partitions, name)
  237. } else if strings.HasPrefix(name, CephLVPrefix) && props["TYPE"] == LVMType {
  238. partitions = append(partitions, name)
  239. }
  240. }
  241. return partitions, nil
  242. }
  243. func DetachDevice(devPath string) error {
  244. getDev := func() (*Device, error) {
  245. devs, err := ListDevices()
  246. if err != nil {
  247. return nil, errors.Wrapf(err, "list devices")
  248. }
  249. dev := devs.GetDeviceByName(devPath)
  250. return dev, nil
  251. }
  252. dev, err := getDev()
  253. if err != nil {
  254. return err
  255. }
  256. if dev == nil {
  257. return nil
  258. }
  259. // check mountpoints
  260. partions, err := GetDevicePartions(devPath)
  261. if err != nil {
  262. return errors.Wrapf(err, "get device %s partions", devPath)
  263. }
  264. for _, part := range partions {
  265. checkMntCmd, _ := NewCommand("sh", "-c", fmt.Sprintf("mount | grep %s | awk '{print $3}'", part)).Run()
  266. if out := checkMntCmd.Output(); out != "" {
  267. mntPoints := strings.Split(out, "\n")
  268. for _, mntPoint := range mntPoints {
  269. if mntPoint != "" {
  270. if err := mountutils.Unmount(mntPoint, false); err != nil {
  271. return errors.Wrapf(err, "umount %s of dev: %s, part: %s", mntPoint, dev.Name, part)
  272. }
  273. }
  274. }
  275. }
  276. }
  277. _, err = NewLosetupCommand().AddArgs("-d", dev.Name).Run()
  278. if err != nil {
  279. return errors.Wrapf(err, "detach device")
  280. }
  281. // recheck
  282. /*dev, err = getDev()
  283. if err != nil {
  284. return errors.Wrapf(err, "get device by %s for rechecking", devPath)
  285. }
  286. if dev != nil {
  287. return errors.Errorf("device %s still exists, %s", devPath, jsonutils.Marshal(dev))
  288. }*/
  289. return nil
  290. }
  291. func DetachDeviceByFile(filePath string) error {
  292. devs, err := ListDevices()
  293. if err != nil {
  294. return err
  295. }
  296. dev := devs.GetDeviceByFile(filePath)
  297. if dev == nil {
  298. return nil
  299. }
  300. _, err = NewLosetupCommand().AddArgs("-d", dev.Name).Run()
  301. return err
  302. }
  303. func ResizeLoopDevice(loopDev string) error {
  304. _, err := NewLosetupCommand().AddArgs("-c", loopDev).Run()
  305. return err
  306. }