device.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. /*
  2. Copyright 2016 The Rook Authors. All rights reserved.
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package sys
  14. import (
  15. "encoding/json"
  16. "fmt"
  17. osexec "os/exec"
  18. "strconv"
  19. "strings"
  20. "github.com/pkg/errors"
  21. "github.com/google/uuid"
  22. "github.com/rook/rook/pkg/util/exec"
  23. )
  24. const (
  25. // DiskType is a disk type
  26. DiskType = "disk"
  27. // SSDType is an sdd type
  28. SSDType = "ssd"
  29. // PartType is a partition type
  30. PartType = "part"
  31. // CryptType is an encrypted type
  32. CryptType = "crypt"
  33. // LVMType is an LVM type
  34. LVMType = "lvm"
  35. // MultiPath is for multipath devices
  36. MultiPath = "mpath"
  37. // LinearType is a linear type
  38. LinearType = "linear"
  39. // LoopType is a loop device type
  40. LoopType = "loop"
  41. sgdiskCmd = "sgdisk"
  42. // CephLVPrefix is the prefix of a LV owned by ceph-volume
  43. CephLVPrefix = "ceph--"
  44. // DeviceMapperPrefix is the prefix of a LV from the device mapper interface
  45. DeviceMapperPrefix = "dm-"
  46. )
  47. // CephVolumeInventory represents the output of the ceph-volume inventory command
  48. type CephVolumeInventory struct {
  49. Path string `json:"path"`
  50. Available bool `json:"available"`
  51. RejectedReasons json.RawMessage `json:"rejected_reasons"`
  52. SysAPI json.RawMessage `json:"sys_api"`
  53. LVS json.RawMessage `json:"lvs"`
  54. }
  55. // CephVolumeLVMList represents the output of the ceph-volume lvm list command
  56. type CephVolumeLVMList map[string][]map[string]interface{}
  57. // Partition represents a partition metadata
  58. type Partition struct {
  59. Name string
  60. Size uint64
  61. Label string
  62. Filesystem string
  63. }
  64. // LocalDisk contains information about an unformatted block device
  65. type LocalDisk struct {
  66. // Name is the device name
  67. Name string `json:"name"`
  68. // Parent is the device parent's name
  69. Parent string `json:"parent"`
  70. // HasChildren is whether the device has a children device
  71. HasChildren bool `json:"hasChildren"`
  72. // DevLinks is the persistent device path on the host
  73. DevLinks string `json:"devLinks"`
  74. // Size is the device capacity in byte
  75. Size uint64 `json:"size"`
  76. // UUID is used by /dev/disk/by-uuid
  77. UUID string `json:"uuid"`
  78. // Serial is the disk serial used by /dev/disk/by-id
  79. Serial string `json:"serial"`
  80. // Type is disk type
  81. Type string `json:"type"`
  82. // Rotational is the boolean whether the device is rotational: true for hdd, false for ssd and nvme
  83. Rotational bool `json:"rotational"`
  84. // ReadOnly is the boolean whether the device is readonly
  85. Readonly bool `json:"readOnly"`
  86. // Partitions is a partition slice
  87. Partitions []Partition
  88. // Filesystem is the filesystem currently on the device
  89. Filesystem string `json:"filesystem"`
  90. // Mountpoint is the mountpoint of the filesystem's on the device
  91. Mountpoint string `json:"mountpoint"`
  92. // Vendor is the device vendor
  93. Vendor string `json:"vendor"`
  94. // Model is the device model
  95. Model string `json:"model"`
  96. // WWN is the world wide name of the device
  97. WWN string `json:"wwn"`
  98. // WWNVendorExtension is the WWN_VENDOR_EXTENSION from udev info
  99. WWNVendorExtension string `json:"wwnVendorExtension"`
  100. // Empty checks whether the device is completely empty
  101. Empty bool `json:"empty"`
  102. // Information provided by Ceph Volume Inventory
  103. CephVolumeData string `json:"cephVolumeData,omitempty"`
  104. // RealPath is the device pathname behind the PVC, behind /mnt/<pvc>/name
  105. RealPath string `json:"real-path,omitempty"`
  106. // KernelName is the kernel name of the device
  107. KernelName string `json:"kernel-name,omitempty"`
  108. // Whether this device should be encrypted
  109. Encrypted bool `json:"encrypted,omitempty"`
  110. }
  111. // ListDevices list all devices available on a machine
  112. func ListDevices(executor exec.Executor) ([]string, error) {
  113. devices, err := executor.ExecuteCommandWithOutput("lsblk", "--all", "--noheadings", "--list", "--output", "KNAME")
  114. if err != nil {
  115. return nil, fmt.Errorf("failed to list all devices: %+v", err)
  116. }
  117. return strings.Split(devices, "\n"), nil
  118. }
  119. // GetDevicePartitions gets partitions on a given device
  120. func GetDevicePartitions(device string, executor exec.Executor) (partitions []Partition, unusedSpace uint64, err error) {
  121. var devicePath string
  122. splitDevicePath := strings.Split(device, "/")
  123. if len(splitDevicePath) == 1 {
  124. devicePath = fmt.Sprintf("/dev/%s", device) //device path for OSD on devices.
  125. } else {
  126. devicePath = device //use the exact device path (like /mnt/<pvc-name>) in case of PVC block device
  127. }
  128. output, err := executor.ExecuteCommandWithOutput("lsblk", devicePath,
  129. "--bytes", "--pairs", "--output", "NAME,SIZE,TYPE,PKNAME")
  130. logger.Infof("Output: %+v", output)
  131. if err != nil {
  132. return nil, 0, fmt.Errorf("failed to get device %s partitions. %+v", device, err)
  133. }
  134. partInfo := strings.Split(output, "\n")
  135. var deviceSize uint64
  136. var totalPartitionSize uint64
  137. for _, info := range partInfo {
  138. props := parseKeyValuePairString(info)
  139. name := props["NAME"]
  140. if name == device {
  141. // found the main device
  142. logger.Info("Device found - ", name)
  143. deviceSize, err = strconv.ParseUint(props["SIZE"], 10, 64)
  144. if err != nil {
  145. return nil, 0, fmt.Errorf("failed to get device %s size. %+v", device, err)
  146. }
  147. } else if props["PKNAME"] == device && props["TYPE"] == PartType {
  148. // found a partition
  149. p := Partition{Name: name}
  150. p.Size, err = strconv.ParseUint(props["SIZE"], 10, 64)
  151. if err != nil {
  152. return nil, 0, fmt.Errorf("failed to get partition %s size. %+v", name, err)
  153. }
  154. totalPartitionSize += p.Size
  155. info, err := GetUdevInfo(name, executor)
  156. if err != nil {
  157. return nil, 0, err
  158. }
  159. if v, ok := info["PARTNAME"]; ok {
  160. p.Label = v
  161. }
  162. if v, ok := info["ID_PART_ENTRY_NAME"]; ok {
  163. p.Label = v
  164. }
  165. if v, ok := info["ID_FS_TYPE"]; ok {
  166. p.Filesystem = v
  167. }
  168. partitions = append(partitions, p)
  169. } else if strings.HasPrefix(name, CephLVPrefix) && props["TYPE"] == LVMType {
  170. p := Partition{Name: name}
  171. partitions = append(partitions, p)
  172. }
  173. }
  174. if deviceSize > 0 {
  175. unusedSpace = deviceSize - totalPartitionSize
  176. }
  177. return partitions, unusedSpace, nil
  178. }
  179. // GetDeviceProperties gets device properties
  180. func GetDeviceProperties(device string, executor exec.Executor) (map[string]string, error) {
  181. // As we are mounting the block mode PVs on /mnt we use the entire path,
  182. // e.g., if the device path is /mnt/example-pvc then its taken completely
  183. // else if its just vdb then the following is used
  184. devicePath := strings.Split(device, "/")
  185. if len(devicePath) == 1 {
  186. device = fmt.Sprintf("/dev/%s", device)
  187. }
  188. return GetDevicePropertiesFromPath(device, executor)
  189. }
  190. // GetDevicePropertiesFromPath gets a device property from a path
  191. func GetDevicePropertiesFromPath(devicePath string, executor exec.Executor) (map[string]string, error) {
  192. output, err := executor.ExecuteCommandWithOutput("lsblk", devicePath,
  193. "--bytes", "--nodeps", "--pairs", "--paths", "--output", "SIZE,ROTA,RO,TYPE,PKNAME,NAME,KNAME,MOUNTPOINT,FSTYPE")
  194. if err != nil {
  195. logger.Errorf("failed to execute lsblk. output: %s", output)
  196. return nil, err
  197. }
  198. logger.Debugf("lsblk output: %q", output)
  199. return parseKeyValuePairString(output), nil
  200. }
  201. // IsLV returns if a device is owned by LVM, is a logical volume
  202. func IsLV(devicePath string, executor exec.Executor) (bool, error) {
  203. devProps, err := GetDevicePropertiesFromPath(devicePath, executor)
  204. if err != nil {
  205. return false, fmt.Errorf("failed to get device properties for %q: %+v", devicePath, err)
  206. }
  207. diskType, ok := devProps["TYPE"]
  208. if !ok {
  209. return false, fmt.Errorf("TYPE property is not found for %q", devicePath)
  210. }
  211. return diskType == LVMType, nil
  212. }
  213. // GetUdevInfo gets udev information
  214. func GetUdevInfo(device string, executor exec.Executor) (map[string]string, error) {
  215. output, err := executor.ExecuteCommandWithOutput("udevadm", "info", "--query=property", fmt.Sprintf("/dev/%s", device))
  216. if err != nil {
  217. return nil, err
  218. }
  219. logger.Debugf("udevadm info output: %q", output)
  220. return parseUdevInfo(output), nil
  221. }
  222. // GetDeviceFilesystems get the file systems available
  223. func GetDeviceFilesystems(device string, executor exec.Executor) (string, error) {
  224. devicePath := strings.Split(device, "/")
  225. if len(devicePath) == 1 {
  226. device = fmt.Sprintf("/dev/%s", device)
  227. }
  228. output, err := executor.ExecuteCommandWithOutput("udevadm", "info", "--query=property", device)
  229. if err != nil {
  230. return "", err
  231. }
  232. return parseFS(output), nil
  233. }
  234. // GetDiskUUID look up the UUID for a disk.
  235. func GetDiskUUID(device string, executor exec.Executor) (string, error) {
  236. if _, err := osexec.LookPath(sgdiskCmd); err != nil {
  237. return "", errors.Wrap(err, "sgdisk not found")
  238. }
  239. devicePath := strings.Split(device, "/")
  240. if len(devicePath) == 1 {
  241. device = fmt.Sprintf("/dev/%s", device)
  242. }
  243. output, err := executor.ExecuteCommandWithOutput(sgdiskCmd, "--print", device)
  244. if err != nil {
  245. return "", errors.Wrapf(err, "sgdisk failed. output=%s", output)
  246. }
  247. return parseUUID(device, output)
  248. }
  249. func GetDiskDeviceClass(disk *LocalDisk) string {
  250. if disk.Rotational {
  251. return "hdd"
  252. }
  253. if strings.Contains(disk.RealPath, "nvme") {
  254. return "nvme"
  255. }
  256. return "ssd"
  257. }
  258. // CheckIfDeviceAvailable checks if a device is available for consumption. The caller
  259. // needs to decide based on the return values whether it is available.
  260. func CheckIfDeviceAvailable(executor exec.Executor, devicePath string, pvcBacked bool) (bool, string, error) {
  261. checker := isDeviceAvailable
  262. isLV, err := IsLV(devicePath, executor)
  263. if err != nil {
  264. return false, "", fmt.Errorf("failed to determine if the device was LV. %v", err)
  265. }
  266. if isLV {
  267. checker = isLVAvailable
  268. }
  269. isAvailable, rejectedReason, err := checker(executor, devicePath)
  270. if err != nil {
  271. return false, "", fmt.Errorf("failed to determine if the device was available. %v", err)
  272. }
  273. return isAvailable, rejectedReason, nil
  274. }
  275. // GetLVName returns the LV name of the device in the form of "VG/LV".
  276. func GetLVName(executor exec.Executor, devicePath string) (string, error) {
  277. devInfo, err := executor.ExecuteCommandWithOutput("dmsetup", "info", "-c", "--noheadings", "-o", "name", devicePath)
  278. if err != nil {
  279. return "", fmt.Errorf("failed to execute dmsetup info for %q. %v", devicePath, err)
  280. }
  281. out, err := executor.ExecuteCommandWithOutput("dmsetup", "splitname", "--noheadings", devInfo)
  282. if err != nil {
  283. return "", fmt.Errorf("failed to execute dmsetup splitname for %q. %v", devInfo, err)
  284. }
  285. split := strings.Split(out, ":")
  286. if len(split) < 2 {
  287. return "", fmt.Errorf("dmsetup splitname returned unexpected result for %q. output: %q", devInfo, out)
  288. }
  289. return fmt.Sprintf("%s/%s", split[0], split[1]), nil
  290. }
  291. // finds the disk uuid in the output of sgdisk
  292. func parseUUID(device, output string) (string, error) {
  293. // find the line with the uuid
  294. lines := strings.Split(output, "\n")
  295. for _, line := range lines {
  296. // If GPT is not found in a disk, sgdisk creates a new GPT in memory and reports its UUID.
  297. // This ID changes each call and is not appropriate to identify the device.
  298. if strings.Contains(line, "Creating new GPT entries in memory.") {
  299. break
  300. }
  301. if strings.Contains(line, "Disk identifier (GUID)") {
  302. words := strings.Split(line, " ")
  303. for _, word := range words {
  304. // we expect most words in the line not to be a uuid, but will return the first one that is
  305. result, err := uuid.Parse(word)
  306. if err == nil {
  307. return result.String(), nil
  308. }
  309. }
  310. }
  311. }
  312. return "", fmt.Errorf("uuid not found for device %s. output=%s", device, output)
  313. }
  314. // converts a raw key value pair string into a map of key value pairs
  315. // example raw string of `foo="0" bar="1" baz="biz"` is returned as:
  316. // map[string]string{"foo":"0", "bar":"1", "baz":"biz"}
  317. func parseKeyValuePairString(propsRaw string) map[string]string {
  318. // first split the single raw string on spaces and initialize a map of
  319. // a length equal to the number of pairs
  320. props := strings.Split(propsRaw, " ")
  321. propMap := make(map[string]string, len(props))
  322. for _, kvpRaw := range props {
  323. // split each individual key value pair on the equals sign
  324. kvp := strings.Split(kvpRaw, "=")
  325. if len(kvp) == 2 {
  326. // first element is the final key, second element is the final value
  327. // (don't forget to remove surrounding quotes from the value)
  328. propMap[kvp[0]] = strings.Replace(kvp[1], `"`, "", -1)
  329. }
  330. }
  331. return propMap
  332. }
  333. // find fs from udevadm info
  334. func parseFS(output string) string {
  335. m := parseUdevInfo(output)
  336. if v, ok := m["ID_FS_TYPE"]; ok {
  337. return v
  338. }
  339. return ""
  340. }
  341. func parseUdevInfo(output string) map[string]string {
  342. lines := strings.Split(output, "\n")
  343. result := make(map[string]string, len(lines))
  344. for _, v := range lines {
  345. pairs := strings.Split(v, "=")
  346. if len(pairs) > 1 {
  347. result[pairs[0]] = pairs[1]
  348. }
  349. }
  350. return result
  351. }
  352. func isDeviceAvailable(executor exec.Executor, devicePath string) (bool, string, error) {
  353. CVInventory, err := inventoryDevice(executor, devicePath)
  354. if err != nil {
  355. return false, "", fmt.Errorf("failed to determine if the device %q is available. %v", devicePath, err)
  356. }
  357. if CVInventory.Available {
  358. return true, "", nil
  359. }
  360. return false, string(CVInventory.RejectedReasons), nil
  361. }
  362. func inventoryDevice(executor exec.Executor, devicePath string) (CephVolumeInventory, error) {
  363. var CVInventory CephVolumeInventory
  364. args := []string{"inventory", "--format", "json", devicePath}
  365. inventory, err := executor.ExecuteCommandWithOutput("ceph-volume", args...)
  366. if err != nil {
  367. return CVInventory, fmt.Errorf("failed to execute ceph-volume inventory on disk %q. %s. %v", devicePath, inventory, err)
  368. }
  369. bInventory := []byte(inventory)
  370. err = json.Unmarshal(bInventory, &CVInventory)
  371. if err != nil {
  372. return CVInventory, fmt.Errorf("failed to unmarshal json data coming from ceph-volume inventory %q. %q. %v", devicePath, inventory, err)
  373. }
  374. return CVInventory, nil
  375. }
  376. func isLVAvailable(executor exec.Executor, devicePath string) (bool, string, error) {
  377. lv, err := GetLVName(executor, devicePath)
  378. if err != nil {
  379. return false, "", fmt.Errorf("failed to get the LV name for the device %q. %v", devicePath, err)
  380. }
  381. cvLVMList, err := lvmList(executor, lv)
  382. if err != nil {
  383. return false, "", fmt.Errorf("failed to determine if the device %q is available. %v", devicePath, err)
  384. }
  385. if len(cvLVMList) == 0 {
  386. return true, "", nil
  387. }
  388. return false, "Used by Ceph", nil
  389. }
  390. func lvmList(executor exec.Executor, lv string) (CephVolumeLVMList, error) {
  391. args := []string{"lvm", "list", "--format", "json", lv}
  392. output, err := executor.ExecuteCommandWithOutput("ceph-volume", args...)
  393. if err != nil {
  394. return nil, fmt.Errorf("failed to execute ceph-volume lvm list on LV %q. %v", lv, err)
  395. }
  396. var cvLVMList CephVolumeLVMList
  397. err = json.Unmarshal([]byte(output), &cvLVMList)
  398. if err != nil {
  399. return nil, fmt.Errorf("error unmarshalling json data coming from ceph-volume lvm list %q. %v", lv, err)
  400. }
  401. return cvLVMList, nil
  402. }
  403. // ListDevicesChild list all child available on a device
  404. // For an encrypted device, it will return the encrypted device like so:
  405. // lsblk --noheadings --output NAME --path --list /dev/sdd
  406. // /dev/sdd
  407. // /dev/mapper/ocs-deviceset-thin-1-data-0hmfgp-block-dmcrypt
  408. func ListDevicesChild(executor exec.Executor, device string) ([]string, error) {
  409. childListRaw, err := executor.ExecuteCommandWithOutput("lsblk", "--noheadings", "--path", "--list", "--output", "NAME", device)
  410. if err != nil {
  411. return []string{}, fmt.Errorf("failed to list child devices of %q. %v", device, err)
  412. }
  413. return strings.Split(childListRaw, "\n"), nil
  414. }
  415. // IsDeviceEncrypted returns whether the disk has a "crypt" label on it
  416. func IsDeviceEncrypted(executor exec.Executor, device string) (bool, error) {
  417. deviceType, err := executor.ExecuteCommandWithOutput("lsblk", "--noheadings", "--output", "TYPE", device)
  418. if err != nil {
  419. return false, fmt.Errorf("failed to get devices type of %q. %v", device, err)
  420. }
  421. return deviceType == "crypt", nil
  422. }