project.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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 fsquota
  15. import (
  16. "bufio"
  17. "fmt"
  18. "io/ioutil"
  19. "os"
  20. "path/filepath"
  21. "regexp"
  22. "strconv"
  23. "sync"
  24. "golang.org/x/sys/unix"
  25. "k8s.io/kubernetes/pkg/volume/util/fsquota/common"
  26. )
  27. var projectsFile = "/etc/projects"
  28. var projidFile = "/etc/projid"
  29. var projectsParseRegexp = regexp.MustCompilePOSIX("^([[:digit:]]+):(.*)$")
  30. var projidParseRegexp = regexp.MustCompilePOSIX("^([^#][^:]*):([[:digit:]]+)$")
  31. var quotaIDLock sync.RWMutex
  32. const maxUnusedQuotasToSearch = 128 // Don't go into an infinite loop searching for an unused quota
  33. type projectType struct {
  34. isValid bool // False if we need to remove this line
  35. id common.QuotaID
  36. data string // Project name (projid) or directory (projects)
  37. line string
  38. }
  39. type projectsList struct {
  40. projects []projectType
  41. projid []projectType
  42. }
  43. func projFilesAreOK() error {
  44. if sf, err := os.Lstat(projectsFile); err != nil || sf.Mode().IsRegular() {
  45. if sf, err := os.Lstat(projidFile); err != nil || sf.Mode().IsRegular() {
  46. return nil
  47. }
  48. return fmt.Errorf("%s exists but is not a plain file, cannot continue", projidFile)
  49. }
  50. return fmt.Errorf("%s exists but is not a plain file, cannot continue", projectsFile)
  51. }
  52. func lockFile(file *os.File) error {
  53. return unix.Flock(int(file.Fd()), unix.LOCK_EX)
  54. }
  55. func unlockFile(file *os.File) error {
  56. return unix.Flock(int(file.Fd()), unix.LOCK_UN)
  57. }
  58. // openAndLockProjectFiles opens /etc/projects and /etc/projid locked.
  59. // Creates them if they don't exist
  60. func openAndLockProjectFiles() (*os.File, *os.File, error) {
  61. // Make sure neither project-related file is a symlink!
  62. if err := projFilesAreOK(); err != nil {
  63. return nil, nil, fmt.Errorf("system project files failed verification: %v", err)
  64. }
  65. // We don't actually modify the original files; we create temporaries and
  66. // move them over the originals
  67. fProjects, err := os.OpenFile(projectsFile, os.O_RDONLY|os.O_CREATE, 0644)
  68. if err != nil {
  69. err = fmt.Errorf("unable to open %s: %v", projectsFile, err)
  70. return nil, nil, err
  71. }
  72. fProjid, err := os.OpenFile(projidFile, os.O_RDONLY|os.O_CREATE, 0644)
  73. if err == nil {
  74. // Check once more, to ensure nothing got changed out from under us
  75. if err = projFilesAreOK(); err == nil {
  76. err = lockFile(fProjects)
  77. if err == nil {
  78. err = lockFile(fProjid)
  79. if err == nil {
  80. return fProjects, fProjid, nil
  81. }
  82. // Nothing useful we can do if we get an error here
  83. err = fmt.Errorf("unable to lock %s: %v", projidFile, err)
  84. unlockFile(fProjects)
  85. } else {
  86. err = fmt.Errorf("unable to lock %s: %v", projectsFile, err)
  87. }
  88. } else {
  89. err = fmt.Errorf("system project files failed re-verification: %v", err)
  90. }
  91. fProjid.Close()
  92. } else {
  93. err = fmt.Errorf("unable to open %s: %v", projidFile, err)
  94. }
  95. fProjects.Close()
  96. return nil, nil, err
  97. }
  98. func closeProjectFiles(fProjects *os.File, fProjid *os.File) error {
  99. // Nothing useful we can do if either of these fail,
  100. // but we have to close (and thereby unlock) the files anyway.
  101. var err error
  102. var err1 error
  103. if fProjid != nil {
  104. err = fProjid.Close()
  105. }
  106. if fProjects != nil {
  107. err1 = fProjects.Close()
  108. }
  109. if err == nil {
  110. return err1
  111. }
  112. return err
  113. }
  114. func parseProject(l string) projectType {
  115. if match := projectsParseRegexp.FindStringSubmatch(l); match != nil {
  116. i, err := strconv.Atoi(match[1])
  117. if err == nil {
  118. return projectType{true, common.QuotaID(i), match[2], l}
  119. }
  120. }
  121. return projectType{true, common.BadQuotaID, "", l}
  122. }
  123. func parseProjid(l string) projectType {
  124. if match := projidParseRegexp.FindStringSubmatch(l); match != nil {
  125. i, err := strconv.Atoi(match[2])
  126. if err == nil {
  127. return projectType{true, common.QuotaID(i), match[1], l}
  128. }
  129. }
  130. return projectType{true, common.BadQuotaID, "", l}
  131. }
  132. func parseProjFile(f *os.File, parser func(l string) projectType) []projectType {
  133. var answer []projectType
  134. scanner := bufio.NewScanner(f)
  135. for scanner.Scan() {
  136. answer = append(answer, parser(scanner.Text()))
  137. }
  138. return answer
  139. }
  140. func readProjectFiles(projects *os.File, projid *os.File) projectsList {
  141. return projectsList{parseProjFile(projects, parseProject), parseProjFile(projid, parseProjid)}
  142. }
  143. func findAvailableQuota(path string, idMap map[common.QuotaID]bool) (common.QuotaID, error) {
  144. unusedQuotasSearched := 0
  145. for id := common.FirstQuota; id == id; id++ {
  146. if _, ok := idMap[id]; !ok {
  147. isInUse, err := getApplier(path).QuotaIDIsInUse(id)
  148. if err != nil {
  149. return common.BadQuotaID, err
  150. } else if !isInUse {
  151. return id, nil
  152. }
  153. unusedQuotasSearched++
  154. if unusedQuotasSearched > maxUnusedQuotasToSearch {
  155. break
  156. }
  157. }
  158. }
  159. return common.BadQuotaID, fmt.Errorf("Cannot find available quota ID")
  160. }
  161. func addDirToProject(path string, id common.QuotaID, list *projectsList) (common.QuotaID, bool, error) {
  162. idMap := make(map[common.QuotaID]bool)
  163. for _, project := range list.projects {
  164. if project.data == path {
  165. if id != project.id {
  166. return common.BadQuotaID, false, fmt.Errorf("Attempt to reassign project ID for %s", path)
  167. }
  168. // Trying to reassign a directory to the project it's
  169. // already in. Maybe this should be an error, but for
  170. // now treat it as an idempotent operation
  171. return id, false, nil
  172. }
  173. idMap[project.id] = true
  174. }
  175. var needToAddProjid = true
  176. for _, projid := range list.projid {
  177. idMap[projid.id] = true
  178. if projid.id == id && id != common.BadQuotaID {
  179. needToAddProjid = false
  180. }
  181. }
  182. var err error
  183. if id == common.BadQuotaID {
  184. id, err = findAvailableQuota(path, idMap)
  185. if err != nil {
  186. return common.BadQuotaID, false, err
  187. }
  188. needToAddProjid = true
  189. }
  190. if needToAddProjid {
  191. name := fmt.Sprintf("volume%v", id)
  192. line := fmt.Sprintf("%s:%v", name, id)
  193. list.projid = append(list.projid, projectType{true, id, name, line})
  194. }
  195. line := fmt.Sprintf("%v:%s", id, path)
  196. list.projects = append(list.projects, projectType{true, id, path, line})
  197. return id, needToAddProjid, nil
  198. }
  199. func removeDirFromProject(path string, id common.QuotaID, list *projectsList) (bool, error) {
  200. if id == common.BadQuotaID {
  201. return false, fmt.Errorf("Attempt to remove invalid quota ID from %s", path)
  202. }
  203. foundAt := -1
  204. countByID := make(map[common.QuotaID]int)
  205. for i, project := range list.projects {
  206. if project.data == path {
  207. if id != project.id {
  208. return false, fmt.Errorf("Attempting to remove quota ID %v from path %s, but expecting ID %v", id, path, project.id)
  209. } else if foundAt != -1 {
  210. return false, fmt.Errorf("Found multiple quota IDs for path %s", path)
  211. }
  212. // Faster and easier than deleting an element
  213. list.projects[i].isValid = false
  214. foundAt = i
  215. }
  216. countByID[project.id]++
  217. }
  218. if foundAt == -1 {
  219. return false, fmt.Errorf("Cannot find quota associated with path %s", path)
  220. }
  221. if countByID[id] <= 1 {
  222. // Removing the last entry means that we're no longer using
  223. // the quota ID, so remove that as well
  224. for i, projid := range list.projid {
  225. if projid.id == id {
  226. list.projid[i].isValid = false
  227. }
  228. }
  229. return true, nil
  230. }
  231. return false, nil
  232. }
  233. func writeProjectFile(base *os.File, projects []projectType) (string, error) {
  234. oname := base.Name()
  235. stat, err := base.Stat()
  236. if err != nil {
  237. return "", err
  238. }
  239. mode := stat.Mode() & os.ModePerm
  240. f, err := ioutil.TempFile(filepath.Dir(oname), filepath.Base(oname))
  241. if err != nil {
  242. return "", err
  243. }
  244. filename := f.Name()
  245. if err := os.Chmod(filename, mode); err != nil {
  246. return "", err
  247. }
  248. for _, proj := range projects {
  249. if proj.isValid {
  250. if _, err := f.WriteString(fmt.Sprintf("%s\n", proj.line)); err != nil {
  251. f.Close()
  252. os.Remove(filename)
  253. return "", err
  254. }
  255. }
  256. }
  257. if err := f.Close(); err != nil {
  258. os.Remove(filename)
  259. return "", err
  260. }
  261. return filename, nil
  262. }
  263. func writeProjectFiles(fProjects *os.File, fProjid *os.File, writeProjid bool, list projectsList) error {
  264. tmpProjects, err := writeProjectFile(fProjects, list.projects)
  265. if err == nil {
  266. // Ensure that both files are written before we try to rename either.
  267. if writeProjid {
  268. tmpProjid, err := writeProjectFile(fProjid, list.projid)
  269. if err == nil {
  270. err = os.Rename(tmpProjid, fProjid.Name())
  271. if err != nil {
  272. os.Remove(tmpProjid)
  273. }
  274. }
  275. }
  276. if err == nil {
  277. err = os.Rename(tmpProjects, fProjects.Name())
  278. if err == nil {
  279. return nil
  280. }
  281. // We're in a bit of trouble here; at this
  282. // point we've successfully renamed tmpProjid
  283. // to the real thing, but renaming tmpProject
  284. // to the real file failed. There's not much we
  285. // can do in this position. Anything we could do
  286. // to try to undo it would itself be likely to fail.
  287. }
  288. os.Remove(tmpProjects)
  289. }
  290. return fmt.Errorf("Unable to write project files: %v", err)
  291. }
  292. func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) {
  293. quotaIDLock.Lock()
  294. defer quotaIDLock.Unlock()
  295. fProjects, fProjid, err := openAndLockProjectFiles()
  296. if err == nil {
  297. defer closeProjectFiles(fProjects, fProjid)
  298. list := readProjectFiles(fProjects, fProjid)
  299. var writeProjid bool
  300. ID, writeProjid, err = addDirToProject(path, ID, &list)
  301. if err == nil && ID != common.BadQuotaID {
  302. if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil {
  303. return ID, nil
  304. }
  305. }
  306. }
  307. return common.BadQuotaID, fmt.Errorf("createProjectID %s %v failed %v", path, ID, err)
  308. }
  309. func removeProjectID(path string, ID common.QuotaID) error {
  310. if ID == common.BadQuotaID {
  311. return fmt.Errorf("attempting to remove invalid quota ID %v", ID)
  312. }
  313. quotaIDLock.Lock()
  314. defer quotaIDLock.Unlock()
  315. fProjects, fProjid, err := openAndLockProjectFiles()
  316. if err == nil {
  317. defer closeProjectFiles(fProjects, fProjid)
  318. list := readProjectFiles(fProjects, fProjid)
  319. var writeProjid bool
  320. writeProjid, err = removeDirFromProject(path, ID, &list)
  321. if err == nil {
  322. if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil {
  323. return nil
  324. }
  325. }
  326. }
  327. return fmt.Errorf("removeProjectID %s %v failed %v", path, ID, err)
  328. }