quota_linux_common_impl.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // +build linux
  2. /*
  3. Copyright 2018 The Kubernetes Authors.
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. package common
  15. import (
  16. "bufio"
  17. "fmt"
  18. "io/ioutil"
  19. "os"
  20. "os/exec"
  21. "regexp"
  22. "strconv"
  23. "strings"
  24. "sync"
  25. "syscall"
  26. "k8s.io/klog"
  27. )
  28. var quotaCmd string
  29. var quotaCmdInitialized bool
  30. var quotaCmdLock sync.RWMutex
  31. // If we later get a filesystem that uses project quota semantics other than
  32. // XFS, we'll need to change this.
  33. // Higher levels don't need to know what's inside
  34. type linuxFilesystemType struct {
  35. name string
  36. typeMagic int64 // Filesystem magic number, per statfs(2)
  37. maxQuota int64
  38. allowEmptyOutput bool // Accept empty output from "quota" command
  39. }
  40. const (
  41. bitsPerWord = 32 << (^uint(0) >> 63) // either 32 or 64
  42. )
  43. var (
  44. linuxSupportedFilesystems = []linuxFilesystemType{
  45. {
  46. name: "XFS",
  47. typeMagic: 0x58465342,
  48. maxQuota: 1<<(bitsPerWord-1) - 1,
  49. allowEmptyOutput: true, // XFS filesystems report nothing if a quota is not present
  50. }, {
  51. name: "ext4fs",
  52. typeMagic: 0xef53,
  53. maxQuota: (1<<(bitsPerWord-1) - 1) & (1<<58 - 1),
  54. allowEmptyOutput: false, // ext4 filesystems always report something even if a quota is not present
  55. },
  56. }
  57. )
  58. // VolumeProvider supplies a quota applier to the generic code.
  59. type VolumeProvider struct {
  60. }
  61. var quotaCmds = []string{"/sbin/xfs_quota",
  62. "/usr/sbin/xfs_quota",
  63. "/bin/xfs_quota"}
  64. var quotaParseRegexp = regexp.MustCompilePOSIX("^[^ \t]*[ \t]*([0-9]+)")
  65. var lsattrCmd = "/usr/bin/lsattr"
  66. var lsattrParseRegexp = regexp.MustCompilePOSIX("^ *([0-9]+) [^ ]+ (.*)$")
  67. // GetQuotaApplier -- does this backing device support quotas that
  68. // can be applied to directories?
  69. func (*VolumeProvider) GetQuotaApplier(mountpoint string, backingDev string) LinuxVolumeQuotaApplier {
  70. for _, fsType := range linuxSupportedFilesystems {
  71. if isFilesystemOfType(mountpoint, backingDev, fsType.typeMagic) {
  72. return linuxVolumeQuotaApplier{mountpoint: mountpoint,
  73. maxQuota: fsType.maxQuota,
  74. allowEmptyOutput: fsType.allowEmptyOutput,
  75. }
  76. }
  77. }
  78. return nil
  79. }
  80. type linuxVolumeQuotaApplier struct {
  81. mountpoint string
  82. maxQuota int64
  83. allowEmptyOutput bool
  84. }
  85. func getXFSQuotaCmd() (string, error) {
  86. quotaCmdLock.Lock()
  87. defer quotaCmdLock.Unlock()
  88. if quotaCmdInitialized {
  89. return quotaCmd, nil
  90. }
  91. for _, program := range quotaCmds {
  92. fileinfo, err := os.Stat(program)
  93. if err == nil && ((fileinfo.Mode().Perm() & (1 << 6)) != 0) {
  94. klog.V(3).Infof("Found xfs_quota program %s", program)
  95. quotaCmd = program
  96. quotaCmdInitialized = true
  97. return quotaCmd, nil
  98. }
  99. }
  100. quotaCmdInitialized = true
  101. return "", fmt.Errorf("No xfs_quota program found")
  102. }
  103. func doRunXFSQuotaCommand(mountpoint string, mountsFile, command string) (string, error) {
  104. quotaCmd, err := getXFSQuotaCmd()
  105. if err != nil {
  106. return "", err
  107. }
  108. // We're using numeric project IDs directly; no need to scan
  109. // /etc/projects or /etc/projid
  110. klog.V(4).Infof("runXFSQuotaCommand %s -t %s -P/dev/null -D/dev/null -x -f %s -c %s", quotaCmd, mountsFile, mountpoint, command)
  111. cmd := exec.Command(quotaCmd, "-t", mountsFile, "-P/dev/null", "-D/dev/null", "-x", "-f", mountpoint, "-c", command)
  112. data, err := cmd.Output()
  113. if err != nil {
  114. return "", err
  115. }
  116. klog.V(4).Infof("runXFSQuotaCommand output %q", string(data))
  117. return string(data), nil
  118. }
  119. // Extract the mountpoint we care about into a temporary mounts file so that xfs_quota does
  120. // not attempt to scan every mount on the filesystem, which could hang if e. g.
  121. // a stuck NFS mount is present.
  122. // See https://bugzilla.redhat.com/show_bug.cgi?id=237120 for an example
  123. // of the problem that could be caused if this were to happen.
  124. func runXFSQuotaCommand(mountpoint string, command string) (string, error) {
  125. tmpMounts, err := ioutil.TempFile("", "mounts")
  126. if err != nil {
  127. return "", fmt.Errorf("Cannot create temporary mount file: %v", err)
  128. }
  129. tmpMountsFileName := tmpMounts.Name()
  130. defer tmpMounts.Close()
  131. defer os.Remove(tmpMountsFileName)
  132. mounts, err := os.Open(MountsFile)
  133. if err != nil {
  134. return "", fmt.Errorf("Cannot open mounts file %s: %v", MountsFile, err)
  135. }
  136. defer mounts.Close()
  137. scanner := bufio.NewScanner(mounts)
  138. for scanner.Scan() {
  139. match := MountParseRegexp.FindStringSubmatch(scanner.Text())
  140. if match != nil {
  141. mount := match[2]
  142. if mount == mountpoint {
  143. if _, err := tmpMounts.WriteString(fmt.Sprintf("%s\n", scanner.Text())); err != nil {
  144. return "", fmt.Errorf("Cannot write temporary mounts file: %v", err)
  145. }
  146. if err := tmpMounts.Sync(); err != nil {
  147. return "", fmt.Errorf("Cannot sync temporary mounts file: %v", err)
  148. }
  149. return doRunXFSQuotaCommand(mountpoint, tmpMountsFileName, command)
  150. }
  151. }
  152. }
  153. return "", fmt.Errorf("Cannot run xfs_quota: cannot find mount point %s in %s", mountpoint, MountsFile)
  154. }
  155. // SupportsQuotas determines whether the filesystem supports quotas.
  156. func SupportsQuotas(mountpoint string, qType QuotaType) (bool, error) {
  157. data, err := runXFSQuotaCommand(mountpoint, "state -p")
  158. if err != nil {
  159. return false, err
  160. }
  161. if qType == FSQuotaEnforcing {
  162. return strings.Contains(data, "Enforcement: ON"), nil
  163. }
  164. return strings.Contains(data, "Accounting: ON"), nil
  165. }
  166. func isFilesystemOfType(mountpoint string, backingDev string, typeMagic int64) bool {
  167. var buf syscall.Statfs_t
  168. err := syscall.Statfs(mountpoint, &buf)
  169. if err != nil {
  170. klog.Warningf("Warning: Unable to statfs %s: %v", mountpoint, err)
  171. return false
  172. }
  173. if int64(buf.Type) != typeMagic {
  174. return false
  175. }
  176. if answer, _ := SupportsQuotas(mountpoint, FSQuotaAccounting); answer {
  177. return true
  178. }
  179. return false
  180. }
  181. // GetQuotaOnDir retrieves the quota ID (if any) associated with the specified directory
  182. // If we can't make system calls, all we can say is that we don't know whether
  183. // it has a quota, and higher levels have to make the call.
  184. func (v linuxVolumeQuotaApplier) GetQuotaOnDir(path string) (QuotaID, error) {
  185. cmd := exec.Command(lsattrCmd, "-pd", path)
  186. data, err := cmd.Output()
  187. if err != nil {
  188. return BadQuotaID, fmt.Errorf("cannot run lsattr: %v", err)
  189. }
  190. match := lsattrParseRegexp.FindStringSubmatch(string(data))
  191. if match == nil {
  192. return BadQuotaID, fmt.Errorf("Unable to parse lsattr -pd %s output %s", path, string(data))
  193. }
  194. if match[2] != path {
  195. return BadQuotaID, fmt.Errorf("Mismatch between supplied and returned path (%s != %s)", path, match[2])
  196. }
  197. projid, err := strconv.ParseInt(match[1], 10, 32)
  198. if err != nil {
  199. return BadQuotaID, fmt.Errorf("Unable to parse project ID from %s (%v)", match[1], err)
  200. }
  201. return QuotaID(projid), nil
  202. }
  203. // SetQuotaOnDir applies a quota to the specified directory under the specified mountpoint.
  204. func (v linuxVolumeQuotaApplier) SetQuotaOnDir(path string, id QuotaID, bytes int64) error {
  205. if bytes < 0 || bytes > v.maxQuota {
  206. bytes = v.maxQuota
  207. }
  208. _, err := runXFSQuotaCommand(v.mountpoint, fmt.Sprintf("limit -p bhard=%v bsoft=%v %v", bytes, bytes, id))
  209. if err != nil {
  210. return err
  211. }
  212. _, err = runXFSQuotaCommand(v.mountpoint, fmt.Sprintf("project -s -p %s %v", path, id))
  213. return err
  214. }
  215. func getQuantity(mountpoint string, id QuotaID, xfsQuotaArg string, multiplier int64, allowEmptyOutput bool) (int64, error) {
  216. data, err := runXFSQuotaCommand(mountpoint, fmt.Sprintf("quota -p -N -n -v %s %v", xfsQuotaArg, id))
  217. if err != nil {
  218. return 0, fmt.Errorf("Unable to run xfs_quota: %v", err)
  219. }
  220. if data == "" && allowEmptyOutput {
  221. return 0, nil
  222. }
  223. match := quotaParseRegexp.FindStringSubmatch(data)
  224. if match == nil {
  225. return 0, fmt.Errorf("Unable to parse quota output '%s'", data)
  226. }
  227. size, err := strconv.ParseInt(match[1], 10, 64)
  228. if err != nil {
  229. return 0, fmt.Errorf("Unable to parse data size '%s' from '%s': %v", match[1], data, err)
  230. }
  231. klog.V(4).Infof("getQuantity %s %d %s %d => %d %v", mountpoint, id, xfsQuotaArg, multiplier, size, err)
  232. return size * multiplier, nil
  233. }
  234. // GetConsumption returns the consumption in bytes if available via quotas
  235. func (v linuxVolumeQuotaApplier) GetConsumption(_ string, id QuotaID) (int64, error) {
  236. return getQuantity(v.mountpoint, id, "-b", 1024, v.allowEmptyOutput)
  237. }
  238. // GetInodes returns the inodes in use if available via quotas
  239. func (v linuxVolumeQuotaApplier) GetInodes(_ string, id QuotaID) (int64, error) {
  240. return getQuantity(v.mountpoint, id, "-i", 1, v.allowEmptyOutput)
  241. }
  242. // QuotaIDIsInUse checks whether the specified quota ID is in use on the specified
  243. // filesystem
  244. func (v linuxVolumeQuotaApplier) QuotaIDIsInUse(id QuotaID) (bool, error) {
  245. bytes, err := v.GetConsumption(v.mountpoint, id)
  246. if err != nil {
  247. return false, err
  248. }
  249. if bytes > 0 {
  250. return true, nil
  251. }
  252. inodes, err := v.GetInodes(v.mountpoint, id)
  253. return inodes > 0, err
  254. }