projectquota.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. // +build linux
  2. //
  3. // projectquota.go - implements XFS project quota controls
  4. // for setting quota limits on a newly created directory.
  5. // It currently supports the legacy XFS specific ioctls.
  6. //
  7. // TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR
  8. // for both xfs/ext4 for kernel version >= v4.5
  9. //
  10. package quota
  11. /*
  12. #include <stdlib.h>
  13. #include <dirent.h>
  14. #include <linux/fs.h>
  15. #include <linux/quota.h>
  16. #include <linux/dqblk_xfs.h>
  17. #ifndef FS_XFLAG_PROJINHERIT
  18. struct fsxattr {
  19. __u32 fsx_xflags;
  20. __u32 fsx_extsize;
  21. __u32 fsx_nextents;
  22. __u32 fsx_projid;
  23. unsigned char fsx_pad[12];
  24. };
  25. #define FS_XFLAG_PROJINHERIT 0x00000200
  26. #endif
  27. #ifndef FS_IOC_FSGETXATTR
  28. #define FS_IOC_FSGETXATTR _IOR ('X', 31, struct fsxattr)
  29. #endif
  30. #ifndef FS_IOC_FSSETXATTR
  31. #define FS_IOC_FSSETXATTR _IOW ('X', 32, struct fsxattr)
  32. #endif
  33. #ifndef PRJQUOTA
  34. #define PRJQUOTA 2
  35. #endif
  36. #ifndef XFS_PROJ_QUOTA
  37. #define XFS_PROJ_QUOTA 2
  38. #endif
  39. #ifndef Q_XSETPQLIM
  40. #define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA)
  41. #endif
  42. #ifndef Q_XGETPQUOTA
  43. #define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA)
  44. #endif
  45. const int Q_XGETQSTAT_PRJQUOTA = QCMD(Q_XGETQSTAT, PRJQUOTA);
  46. */
  47. import "C"
  48. import (
  49. "fmt"
  50. "io/ioutil"
  51. "path"
  52. "path/filepath"
  53. "unsafe"
  54. rsystem "github.com/opencontainers/runc/libcontainer/system"
  55. "github.com/sirupsen/logrus"
  56. "golang.org/x/sys/unix"
  57. )
  58. // Quota limit params - currently we only control blocks hard limit
  59. type Quota struct {
  60. Size uint64
  61. }
  62. // Control - Context to be used by storage driver (e.g. overlay)
  63. // who wants to apply project quotas to container dirs
  64. type Control struct {
  65. backingFsBlockDev string
  66. nextProjectID uint32
  67. quotas map[string]uint32
  68. }
  69. // NewControl - initialize project quota support.
  70. // Test to make sure that quota can be set on a test dir and find
  71. // the first project id to be used for the next container create.
  72. //
  73. // Returns nil (and error) if project quota is not supported.
  74. //
  75. // First get the project id of the home directory.
  76. // This test will fail if the backing fs is not xfs.
  77. //
  78. // xfs_quota tool can be used to assign a project id to the driver home directory, e.g.:
  79. // echo 999:/var/lib/docker/overlay2 >> /etc/projects
  80. // echo docker:999 >> /etc/projid
  81. // xfs_quota -x -c 'project -s docker' /<xfs mount point>
  82. //
  83. // In that case, the home directory project id will be used as a "start offset"
  84. // and all containers will be assigned larger project ids (e.g. >= 1000).
  85. // This is a way to prevent xfs_quota management from conflicting with docker.
  86. //
  87. // Then try to create a test directory with the next project id and set a quota
  88. // on it. If that works, continue to scan existing containers to map allocated
  89. // project ids.
  90. //
  91. func NewControl(basePath string) (*Control, error) {
  92. //
  93. // If we are running in a user namespace quota won't be supported for
  94. // now since makeBackingFsDev() will try to mknod().
  95. //
  96. if rsystem.RunningInUserNS() {
  97. return nil, ErrQuotaNotSupported
  98. }
  99. //
  100. // create backing filesystem device node
  101. //
  102. backingFsBlockDev, err := makeBackingFsDev(basePath)
  103. if err != nil {
  104. return nil, err
  105. }
  106. // check if we can call quotactl with project quotas
  107. // as a mechanism to determine (early) if we have support
  108. hasQuotaSupport, err := hasQuotaSupport(backingFsBlockDev)
  109. if err != nil {
  110. return nil, err
  111. }
  112. if !hasQuotaSupport {
  113. return nil, ErrQuotaNotSupported
  114. }
  115. //
  116. // Get project id of parent dir as minimal id to be used by driver
  117. //
  118. minProjectID, err := getProjectID(basePath)
  119. if err != nil {
  120. return nil, err
  121. }
  122. minProjectID++
  123. //
  124. // Test if filesystem supports project quotas by trying to set
  125. // a quota on the first available project id
  126. //
  127. quota := Quota{
  128. Size: 0,
  129. }
  130. if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
  131. return nil, err
  132. }
  133. q := Control{
  134. backingFsBlockDev: backingFsBlockDev,
  135. nextProjectID: minProjectID + 1,
  136. quotas: make(map[string]uint32),
  137. }
  138. //
  139. // get first project id to be used for next container
  140. //
  141. err = q.findNextProjectID(basePath)
  142. if err != nil {
  143. return nil, err
  144. }
  145. logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, q.nextProjectID)
  146. return &q, nil
  147. }
  148. // SetQuota - assign a unique project id to directory and set the quota limits
  149. // for that project id
  150. func (q *Control) SetQuota(targetPath string, quota Quota) error {
  151. projectID, ok := q.quotas[targetPath]
  152. if !ok {
  153. projectID = q.nextProjectID
  154. //
  155. // assign project id to new container directory
  156. //
  157. err := setProjectID(targetPath, projectID)
  158. if err != nil {
  159. return err
  160. }
  161. q.quotas[targetPath] = projectID
  162. q.nextProjectID++
  163. }
  164. //
  165. // set the quota limit for the container's project id
  166. //
  167. logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
  168. return setProjectQuota(q.backingFsBlockDev, projectID, quota)
  169. }
  170. // setProjectQuota - set the quota for project id on xfs block device
  171. func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
  172. var d C.fs_disk_quota_t
  173. d.d_version = C.FS_DQUOT_VERSION
  174. d.d_id = C.__u32(projectID)
  175. d.d_flags = C.XFS_PROJ_QUOTA
  176. d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT
  177. d.d_blk_hardlimit = C.__u64(quota.Size / 512)
  178. d.d_blk_softlimit = d.d_blk_hardlimit
  179. var cs = C.CString(backingFsBlockDev)
  180. defer C.free(unsafe.Pointer(cs))
  181. _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM,
  182. uintptr(unsafe.Pointer(cs)), uintptr(d.d_id),
  183. uintptr(unsafe.Pointer(&d)), 0, 0)
  184. if errno != 0 {
  185. return fmt.Errorf("Failed to set quota limit for projid %d on %s: %v",
  186. projectID, backingFsBlockDev, errno.Error())
  187. }
  188. return nil
  189. }
  190. // GetQuota - get the quota limits of a directory that was configured with SetQuota
  191. func (q *Control) GetQuota(targetPath string, quota *Quota) error {
  192. projectID, ok := q.quotas[targetPath]
  193. if !ok {
  194. return fmt.Errorf("quota not found for path : %s", targetPath)
  195. }
  196. //
  197. // get the quota limit for the container's project id
  198. //
  199. var d C.fs_disk_quota_t
  200. var cs = C.CString(q.backingFsBlockDev)
  201. defer C.free(unsafe.Pointer(cs))
  202. _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA,
  203. uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
  204. uintptr(unsafe.Pointer(&d)), 0, 0)
  205. if errno != 0 {
  206. return fmt.Errorf("Failed to get quota limit for projid %d on %s: %v",
  207. projectID, q.backingFsBlockDev, errno.Error())
  208. }
  209. quota.Size = uint64(d.d_blk_hardlimit) * 512
  210. return nil
  211. }
  212. // getProjectID - get the project id of path on xfs
  213. func getProjectID(targetPath string) (uint32, error) {
  214. dir, err := openDir(targetPath)
  215. if err != nil {
  216. return 0, err
  217. }
  218. defer closeDir(dir)
  219. var fsx C.struct_fsxattr
  220. _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
  221. uintptr(unsafe.Pointer(&fsx)))
  222. if errno != 0 {
  223. return 0, fmt.Errorf("Failed to get projid for %s: %v", targetPath, errno.Error())
  224. }
  225. return uint32(fsx.fsx_projid), nil
  226. }
  227. // setProjectID - set the project id of path on xfs
  228. func setProjectID(targetPath string, projectID uint32) error {
  229. dir, err := openDir(targetPath)
  230. if err != nil {
  231. return err
  232. }
  233. defer closeDir(dir)
  234. var fsx C.struct_fsxattr
  235. _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
  236. uintptr(unsafe.Pointer(&fsx)))
  237. if errno != 0 {
  238. return fmt.Errorf("Failed to get projid for %s: %v", targetPath, errno.Error())
  239. }
  240. fsx.fsx_projid = C.__u32(projectID)
  241. fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT
  242. _, _, errno = unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR,
  243. uintptr(unsafe.Pointer(&fsx)))
  244. if errno != 0 {
  245. return fmt.Errorf("Failed to set projid for %s: %v", targetPath, errno.Error())
  246. }
  247. return nil
  248. }
  249. // findNextProjectID - find the next project id to be used for containers
  250. // by scanning driver home directory to find used project ids
  251. func (q *Control) findNextProjectID(home string) error {
  252. files, err := ioutil.ReadDir(home)
  253. if err != nil {
  254. return fmt.Errorf("read directory failed : %s", home)
  255. }
  256. for _, file := range files {
  257. if !file.IsDir() {
  258. continue
  259. }
  260. path := filepath.Join(home, file.Name())
  261. projid, err := getProjectID(path)
  262. if err != nil {
  263. return err
  264. }
  265. if projid > 0 {
  266. q.quotas[path] = projid
  267. }
  268. if q.nextProjectID <= projid {
  269. q.nextProjectID = projid + 1
  270. }
  271. }
  272. return nil
  273. }
  274. func free(p *C.char) {
  275. C.free(unsafe.Pointer(p))
  276. }
  277. func openDir(path string) (*C.DIR, error) {
  278. Cpath := C.CString(path)
  279. defer free(Cpath)
  280. dir := C.opendir(Cpath)
  281. if dir == nil {
  282. return nil, fmt.Errorf("Can't open dir")
  283. }
  284. return dir, nil
  285. }
  286. func closeDir(dir *C.DIR) {
  287. if dir != nil {
  288. C.closedir(dir)
  289. }
  290. }
  291. func getDirFd(dir *C.DIR) uintptr {
  292. return uintptr(C.dirfd(dir))
  293. }
  294. // Get the backing block device of the driver home directory
  295. // and create a block device node under the home directory
  296. // to be used by quotactl commands
  297. func makeBackingFsDev(home string) (string, error) {
  298. var stat unix.Stat_t
  299. if err := unix.Stat(home, &stat); err != nil {
  300. return "", err
  301. }
  302. backingFsBlockDev := path.Join(home, "backingFsBlockDev")
  303. // Re-create just in case someone copied the home directory over to a new device
  304. unix.Unlink(backingFsBlockDev)
  305. err := unix.Mknod(backingFsBlockDev, unix.S_IFBLK|0600, int(stat.Dev))
  306. switch err {
  307. case nil:
  308. return backingFsBlockDev, nil
  309. case unix.ENOSYS:
  310. return "", ErrQuotaNotSupported
  311. default:
  312. return "", fmt.Errorf("Failed to mknod %s: %v", backingFsBlockDev, err)
  313. }
  314. }
  315. func hasQuotaSupport(backingFsBlockDev string) (bool, error) {
  316. var cs = C.CString(backingFsBlockDev)
  317. defer free(cs)
  318. var qstat C.fs_quota_stat_t
  319. _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(C.Q_XGETQSTAT_PRJQUOTA), uintptr(unsafe.Pointer(cs)), 0, uintptr(unsafe.Pointer(&qstat)), 0, 0)
  320. if errno == 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ENFD > 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ACCT > 0 {
  321. return true, nil
  322. }
  323. switch errno {
  324. // These are the known fatal errors, consider all other errors (ENOTTY, etc.. not supporting quota)
  325. case unix.EFAULT, unix.ENOENT, unix.ENOTBLK, unix.EPERM:
  326. default:
  327. return false, nil
  328. }
  329. return false, errno
  330. }