1: <?php
2:
3: use Opcenter\Filesystem\Quota\Project;
4:
5: /**
6: * Copyright (C) Apis Networks, Inc - All Rights Reserved.
7: *
8: * Unauthorized copying of this file, via any medium, is
9: * strictly prohibited without consent. Any dissemination of
10: * material herein is prohibited.
11: *
12: * For licensing inquiries email <licensing@apisnetworks.com>
13: *
14: * Written by Matt Saladna <matt@apisnetworks.com>, July 2017
15: */
16:
17: #[Route(
18: prefix: 'quota'
19: )]
20: class Diskquota_Module extends Module_Skeleton
21: {
22: const DEPENDENCY_MAP = [
23: 'siteinfo',
24: ];
25:
26: const MIN_STORAGE_AMNESTY = QUOTA_STORAGE_WAIT;
27: // time in seconds between amnesty requests
28: const AMNESTY_DURATION = QUOTA_STORAGE_DURATION;
29: // 24 hours
30: const AMNESTY_MULTIPLIER = QUOTA_STORAGE_BOOST;
31: const AMNESTY_JOB_MARKER = 'amnesty';
32:
33: const ERR_PROJECT_NAME_RESERVED = [':err_project_name_reserved', "Project name begins with reserved prefix"];
34:
35: public $exportedFunctions = [
36: '*' => PRIVILEGE_SITE,
37: 'amnesty' => PRIVILEGE_SITE | PRIVILEGE_ADMIN,
38: 'project_create' => PRIVILEGE_ADMIN,
39: 'projects' => PRIVILEGE_ADMIN,
40: 'project_delete' => PRIVILEGE_ADMIN,
41: 'project_get' => PRIVILEGE_ADMIN,
42: 'project_supported' => PRIVILEGE_ADMIN,
43: 'project_set' => PRIVILEGE_ADMIN,
44: ];
45:
46: /**
47: * Request a temporary bump to account storage
48: *
49: * @param string $site optional site to apply amnesty
50: * @return bool
51: * @see MIN_STORAGE_AMNESTY
52: */
53: #[Route(
54: as: 'amnesty',
55: methods: ['post']
56: )]
57: public function amnesty(string $site = null): bool
58: {
59: if (posix_getuid() && !IS_CLI) {
60: return $this->query('diskquota_amnesty');
61: }
62:
63: $last = $this->getServiceValue('diskquota', 'amnesty');
64: $now = coalesce($_SERVER['REQUEST_TIME'], time());
65: if (!$site) {
66: $site = $this->site;
67: } else if ($site && !($this->permission_level && PRIVILEGE_SITE)) {
68: if (!($site = \Auth::get_site_id_from_anything($site))) {
69: return error("Unknown site identifier `%s'", $site);
70: }
71:
72: $site = "site" . $site;
73: }
74:
75: if (($this->permission_level & PRIVILEGE_SITE) && self::MIN_STORAGE_AMNESTY > ($now - $last)) {
76: $aday = self::MIN_STORAGE_AMNESTY / 86400;
77:
78: return error('storage amnesty may be requested once every %(period)d days, %(remaining)d days remaining', [
79: 'period' => $aday,
80: 'remaining' => $aday - ceil(($now - $last) / 86400)
81: ]);
82: }
83:
84: // @TODO handling dynamic quota overages with xfs?
85: $ctx = \Auth::context(null, $site);
86: $storage = $ctx->getAccount()->conf['diskquota']['quota'];
87: $newstorage = $storage * self::AMNESTY_MULTIPLIER;
88: $acct = new Util_Account_Editor($ctx->getAccount(), $ctx);
89: $acct->setConfig('diskquota', 'quota', $newstorage)->setConfig('diskquota', 'amnesty', $now);
90: $ret = $acct->edit();
91: if ($ret !== true) {
92: Error_Reporter::report(var_export($ret, true));
93: return error('failed to set amnesty on account');
94: }
95: $acct->setConfig('diskquota', 'quota', $storage);
96: $cmd = $acct->getCommand();
97: $proc = new Util_Process_Schedule('+' . self::AMNESTY_DURATION . ' seconds');
98: if (($this->permission_level & PRIVILEGE_ADMIN)) {
99: if ($id = $proc->preempted(self::AMNESTY_JOB_MARKER, $ctx)) {
100: // @xxx potential unbounded storage growth
101: $proc->cancelJob($id);
102: // @todo report duplicate request
103: }
104:
105: }
106: $proc->setID(self::AMNESTY_JOB_MARKER, $ctx);
107: $ret = $proc->run($cmd);
108:
109: // @todo extract to event
110: $msg = sprintf("Domain: %s\r\nSite: %d\r\nServer: %s", $this->domain, $this->site_id, SERVER_NAME_SHORT);
111: Mail::send(Crm_Module::COPY_ADMIN, 'Amnesty Request', $msg);
112:
113: return $ret['success'];
114: }
115:
116: /**
117: * Account is under amnesty
118: *
119: * @return bool
120: */
121: #[Route(
122: as: 'amnesty',
123: methods: ['get']
124: )]
125: public function amnesty_active(): bool
126: {
127: $time = $_SERVER['REQUEST_TIME'] ?? time();
128: $amnesty = $this->getServiceValue('diskquota', 'amnesty');
129: if (!$amnesty) {
130: return false;
131: }
132:
133: return ($time - $amnesty) <= self::AMNESTY_DURATION;
134: }
135:
136: /**
137: * Project feature supported
138: *
139: * @return bool
140: */
141: public function project_supported(): bool
142: {
143: return Project::supported();
144: }
145:
146: /**
147: * Get project quota
148: *
149: * @param string $name
150: * @return array
151: */
152: public function project_get(string $name): array
153: {
154: if (!IS_CLI) {
155: return $this->query('diskquota_project_get', $name);
156: }
157:
158: return (new Project($name))->get();
159: }
160:
161: /**
162: * Set project quota
163: *
164: * @param string $name
165: * @param int $bhard hard limit in KB
166: * @param int $ihard hard limit in inodes
167: * @param int $bsoft soft limit in KB (unenforced)
168: * @param int $isoft soft limit in inodes (unenforced)
169: * @return bool
170: */
171: public function project_set(string $name, int $bhard, int $ihard, int $bsoft = 0, int $isoft = 0): bool
172: {
173: if (!IS_CLI) {
174: return $this->query('diskquota_project_set', $name, $bhard, $ihard, $bsoft, $isoft);
175: }
176:
177: foreach (['b', 'i'] as $prefix) {
178: foreach (['soft', 'hard'] as $type) {
179: if (${"{$prefix}{$type}"} < 0) {
180: return error("Parameter `%(name)s' value out of bounds", ['name' => "\${$prefix}{$type}"]);
181: }
182: }
183: }
184:
185: if (str_starts_with($name, Opcenter\Reseller::RESELLER_PREFIX)) {
186: return error(self::ERR_PROJECT_NAME_RESERVED);
187: }
188:
189: return (new Project($name))->set($bhard, $ihard, $bsoft, $isoft);
190: }
191:
192: /**
193: * List known projects
194: *
195: * @return array
196: */
197: public function projects(): array
198: {
199: if (!file_exists(Project::PROJID_MAP)) {
200: return [];
201: }
202:
203: $map = new Opcenter\Map\Textfile(Project::PROJID_MAP, 'r', ':');
204: return array_keys($map->fetchAll());
205: }
206:
207: /**
208: * Delete project
209: *
210: * @param string $name
211: * @return bool
212: */
213: public function project_delete(string $name): bool
214: {
215: if (!IS_CLI) {
216: return $this->query('diskquota_project_delete', $name);
217: }
218:
219: if (str_starts_with($name, Opcenter\Reseller::RESELLER_PREFIX)) {
220: return error(self::ERR_PROJECT_NAME_RESERVED);
221: }
222:
223: return (new Project($name))->remove();
224: }
225:
226: /**
227: * Create new project
228: *
229: * @param string $name
230: * @return bool
231: * @throws ReflectionException
232: */
233: public function project_create(string $name): bool
234: {
235: if (!IS_CLI) {
236: return $this->query('diskquota_project_create', $name);
237: }
238:
239: if (Project::exists($name)) {
240: return error(Project::ERR_PROJECT_EXISTS, ['name' => $name]);
241: }
242:
243: if (str_starts_with($name, Opcenter\Reseller::RESELLER_PREFIX)) {
244: return error(self::ERR_PROJECT_NAME_RESERVED);
245: }
246:
247: return (bool)(new Project($name))->create();
248: }
249: }