1: | <?php declare(strict_types=1); |
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | use Daphnie\Collector; |
16: | use Module\Skeleton\Contracts\Tasking; |
17: | use Module\Support\Webapps\App\Loader; |
18: | use Module\Support\Webapps\Finder; |
19: | use Module\Support\Webapps\Updater; |
20: | use Opcenter\Account\Enumerate; |
21: | use Opcenter\Admin\Bootstrapper; |
22: | use Opcenter\Admin\Bootstrapper\Config; |
23: | use Opcenter\Apnscp; |
24: | use Opcenter\Bandwidth\Bulk; |
25: | use Opcenter\CliParser; |
26: | use Opcenter\Filesystem\Quota; |
27: | use Opcenter\License; |
28: | use Opcenter\Map; |
29: | use Opcenter\Process; |
30: | use Opcenter\Service\Plans; |
31: | use Opcenter\SiteConfiguration; |
32: | use Opcenter\System\Cgroup\Controller; |
33: | use Opcenter\System\Cgroup\Group; |
34: | use Opcenter\System\Cgroup\MetricsLogging; |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | class Admin_Module extends Module_Skeleton implements Tasking |
42: | { |
43: | use ImpersonableTrait; |
44: | |
45: | const ADMIN_HOME = '/etc/opcenter/webhost'; |
46: | |
47: | const ADMIN_CONFIG = '.config/'; |
48: | const ADMIN_CONFIG_LEGACY = '/etc/appliance/appliance.ini'; |
49: | |
50: | const CGROUP_CACHE_KEY = 'acct.cgroup'; |
51: | |
52: | const CLI_OOB_FD = \Util_Account_Editor::OOB_FD; |
53: | |
54: | protected $exportedFunctions = [ |
55: | '*' => PRIVILEGE_ADMIN |
56: | ]; |
57: | |
58: | public function __construct() |
59: | { |
60: | parent::__construct(); |
61: | if (!AUTH_ADMIN_API) { |
62: | $this->exportedFunctions = array_merge($this->exportedFunctions, |
63: | array_fill_keys([ |
64: | 'activate_site', |
65: | 'deactivate_site', |
66: | 'suspend_site', |
67: | 'add_site', |
68: | 'edit_site', |
69: | 'delete_site', |
70: | 'hijack', |
71: | 'bless' |
72: | ], PRIVILEGE_NONE) |
73: | ); |
74: | } |
75: | } |
76: | |
77: | |
78: | |
79: | |
80: | |
81: | |
82: | |
83: | |
84: | public function get_domains(): array |
85: | { |
86: | |
87: | $q = PostgreSQL::initialize()->query('SELECT domain,site_id FROM siteinfo ORDER BY domain'); |
88: | $domains = array(); |
89: | while (null !== ($row = $q->fetch_object())) { |
90: | $domains[$row->site_id] = $row->domain; |
91: | } |
92: | |
93: | return $domains; |
94: | } |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | |
102: | |
103: | public function get_site_id_from_admin(string $admin): ?int |
104: | { |
105: | if (!preg_match(Regex::USERNAME, $admin)) { |
106: | return null; |
107: | } |
108: | $db = PostgreSQL::initialize(); |
109: | $rs = $db->query('SELECT site_id FROM siteinfo WHERE admin_user = \'' . $db->escape_string($admin) . '\''); |
110: | return $rs->num_rows() > 0 ? (int)$rs->fetch_object()->site_id : null; |
111: | } |
112: | |
113: | |
114: | |
115: | |
116: | |
117: | |
118: | |
119: | |
120: | public function get_address_from_domain(string $domain) |
121: | { |
122: | if (!preg_match(Regex::DOMAIN, $domain)) { |
123: | return error("invalid domain `%s'", $domain); |
124: | } |
125: | $siteid = $this->get_site_id_from_domain($domain); |
126: | if (!$siteid) { |
127: | return false; |
128: | } |
129: | $pgdb = PostgreSQL::initialize(); |
130: | $q = $pgdb->query('SELECT email FROM siteinfo WHERE site_id = ' . (int)$siteid); |
131: | if ($pgdb->num_rows() > 0) { |
132: | return $q->fetch_object()->email; |
133: | } |
134: | |
135: | return false; |
136: | } |
137: | |
138: | |
139: | |
140: | |
141: | |
142: | |
143: | |
144: | |
145: | public function get_site_id_from_domain($domain): ?int |
146: | { |
147: | if (!preg_match(Regex::DOMAIN, $domain)) { |
148: | error("invalid domain `%s'", $domain); |
149: | |
150: | return null; |
151: | } |
152: | $pgdb = PostgreSQL::initialize(); |
153: | $q = $pgdb->query("SELECT site_id FROM siteinfo WHERE domain = '" . $domain . "'"); |
154: | if ($pgdb->num_rows() > 0) { |
155: | return (int)$q->fetch_object()->site_id; |
156: | } |
157: | |
158: | return Auth::get_site_id_from_domain($domain); |
159: | |
160: | } |
161: | |
162: | |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | public function get_email(): ?string |
170: | { |
171: | if (!IS_CLI) { |
172: | return $this->query('admin_get_email'); |
173: | } |
174: | $ini = $this->_get_admin_config(); |
175: | |
176: | return $ini['adminemail'] ?? ($ini['email'] ?? null); |
177: | } |
178: | |
179: | protected function _get_admin_config(): array |
180: | { |
181: | $file = $this->getAdminConfigFile(); |
182: | if (!file_exists($file)) { |
183: | return []; |
184: | } |
185: | |
186: | return Util_PHP::unserialize(file_get_contents($file)); |
187: | } |
188: | |
189: | private function getAdminConfigFile(): string |
190: | { |
191: | return self::ADMIN_HOME . DIRECTORY_SEPARATOR . self::ADMIN_CONFIG . |
192: | DIRECTORY_SEPARATOR . $this->username; |
193: | } |
194: | |
195: | |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | public function set_email(string $email): bool |
202: | { |
203: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
204: | return error("Email may not be changed in demo mode"); |
205: | } |
206: | |
207: | if (!IS_CLI) { |
208: | $handler = Preferences::factory($this->getAuthContext())->unlock($this->getApnscpFunctionInterceptor()); |
209: | $handler->sync(); |
210: | $ret = $this->query('admin_set_email', $email); |
211: | $handler->freshen(); |
212: | |
213: | return $ret; |
214: | } |
215: | |
216: | if (!preg_match(Regex::EMAIL, $email)) { |
217: | return error("invalid email `%s'", $email); |
218: | } |
219: | |
220: | $prefs = Preferences::factory($this->getAuthContext())->unlock($this->getApnscpFunctionInterceptor()); |
221: | $prefs['email'] = $email; |
222: | $cfg = new Config(); |
223: | |
224: | if ($cfg['apnscp_admin_email'] && $email !== $cfg['apnscp_admin_email']) { |
225: | |
226: | $cfg['apnscp_admin_email'] = $email; |
227: | $cfg->sync(); |
228: | Bootstrapper::run('apnscp/create-admin', 'software/etckeeper'); |
229: | } |
230: | |
231: | return $prefs->sync(); |
232: | } |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | public function list_plans(): array |
240: | { |
241: | return Plans::list(); |
242: | } |
243: | |
244: | |
245: | |
246: | |
247: | |
248: | |
249: | |
250: | public function get_plan(string $plan = null): ?array |
251: | { |
252: | if (null === $plan) { |
253: | $plan = Plans::default(); |
254: | } |
255: | if (!Plans::exists($plan)) { |
256: | return null; |
257: | } |
258: | |
259: | return (new SiteConfiguration(null))->setPlanName($plan) |
260: | ->getDefaultConfiguration(); |
261: | } |
262: | |
263: | |
264: | |
265: | |
266: | |
267: | |
268: | |
269: | |
270: | public function get_service_info(string $service = null, string $plan = null): ?array |
271: | { |
272: | $plan = $plan ?? Plans::default(); |
273: | if (null === ($data = Plans::get($plan))) { |
274: | error("Unknown plan `%s'", $plan); |
275: | return null; |
276: | } |
277: | |
278: | return $service ? ($data[$service] ?? null) : $data; |
279: | } |
280: | |
281: | |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | public function update_webapps(array $options = []): bool |
288: | { |
289: | $launcher = Updater::launch(); |
290: | foreach ($options as $k => $v) { |
291: | switch ($k) { |
292: | case 'limit': |
293: | $launcher->batch((int)$v); |
294: | break; |
295: | case 'type': |
296: | $launcher->limitType($v); |
297: | break; |
298: | case 'assets': |
299: | $launcher->enableAssetUpdates((bool)$v); |
300: | break; |
301: | case 'core': |
302: | $launcher->enableCoreUpdates((bool)$v); |
303: | break; |
304: | case 'site': |
305: | $launcher->limitSite($v); |
306: | break; |
307: | default: |
308: | fatal("unknown option `%s'", $k); |
309: | } |
310: | } |
311: | |
312: | return (bool)$launcher->run(); |
313: | } |
314: | |
315: | |
316: | |
317: | |
318: | |
319: | |
320: | |
321: | public function list_failed_webapps(string $site = null): array |
322: | { |
323: | $failed = []; |
324: | if ($site) { |
325: | if (!($sites = (array)Auth::get_site_id_from_anything($site))) { |
326: | warn("Unknown site `%s'", $site); |
327: | |
328: | return []; |
329: | } |
330: | $sites = array_map(static function ($s) { |
331: | return 'site' . $s; |
332: | }, $sites); |
333: | } else { |
334: | $sites = Enumerate::active(); |
335: | } |
336: | $getLatest = function ($app) { |
337: | static $index; |
338: | if (!isset($index[$app])) { |
339: | $instance = Loader::fromDocroot($app, null, $this->getAuthContext()); |
340: | $versions = $instance->getVersions(); |
341: | $version = $versions ? array_pop($versions) : null; |
342: | $index[$app] = $version; |
343: | } |
344: | |
345: | return $index[$app]; |
346: | }; |
347: | |
348: | foreach ($sites as $s) { |
349: | if (!Auth::get_domain_from_site_id($s)) { |
350: | continue; |
351: | } |
352: | |
353: | $auth = Auth::nullableContext(null, $s); |
354: | if (null === $auth) { |
355: | continue; |
356: | } |
357: | |
358: | $finder = new Finder($auth); |
359: | $apps = $finder->getActiveApplications(static function ($appmeta) { |
360: | return !empty($appmeta['failed']); |
361: | }); |
362: | if (!$apps) { |
363: | continue; |
364: | } |
365: | $list = []; |
366: | foreach ($apps as $path => $app) { |
367: | $type = $app['type'] ?? null; |
368: | $latest = $type ? $getLatest($app['type']) : null; |
369: | $list[$path] = [ |
370: | 'type' => $type, |
371: | 'version' => $app['version'] ?? null, |
372: | 'hostname' => $app['hostname'] ?? null, |
373: | 'path' => $app['path'] ?? '', |
374: | 'latest' => $latest |
375: | ]; |
376: | } |
377: | $failed[$s] = $list; |
378: | } |
379: | |
380: | return $failed; |
381: | } |
382: | |
383: | |
384: | |
385: | |
386: | |
387: | |
388: | |
389: | public function reset_webapp_failure(array $constraints = []): int |
390: | { |
391: | $known = ['site', 'version', 'type']; |
392: | if ($bad = array_diff(array_keys($constraints), $known)) { |
393: | error("unknown constraints: `%s'", implode(', ', $bad)); |
394: | |
395: | return 0; |
396: | } |
397: | if (isset($constraints['site'])) { |
398: | $siteid = Auth::get_site_id_from_anything($constraints['site']); |
399: | if (!$siteid) { |
400: | error("unknown site `%s'", $constraints['site']); |
401: | |
402: | return 0; |
403: | } |
404: | $sites = ['site' . $siteid]; |
405: | } else { |
406: | $sites = Enumerate::active(); |
407: | } |
408: | $versionFilter = static function (array $appmeta) use ($constraints) { |
409: | if (!isset($constraints['version'])) { |
410: | return true; |
411: | } |
412: | if (!isset($appmeta['version'])) { |
413: | return false; |
414: | } |
415: | |
416: | $vercon = explode(' ', $constraints['version'], 2); |
417: | if (count($vercon) === 1) { |
418: | $vercon = ['=', $vercon[0]]; |
419: | } |
420: | |
421: | return version_compare($appmeta['version'], ...array_reverse($vercon)); |
422: | }; |
423: | $typeFilter = static function (array $appmeta) use ($constraints) { |
424: | if (!isset($constraints['type'])) { |
425: | return true; |
426: | } |
427: | |
428: | return $appmeta['type'] === $constraints['type']; |
429: | }; |
430: | $count = 0; |
431: | foreach ($sites as $site) { |
432: | $auth = Auth::context(null, $site); |
433: | $finder = new Finder($auth); |
434: | $apps = $finder->getActiveApplications(static function ($appmeta) { |
435: | return !empty($appmeta['failed']); |
436: | }); |
437: | foreach ($apps as $path => $app) { |
438: | if (!$typeFilter($app)) { |
439: | continue; |
440: | } |
441: | if (!$versionFilter($app)) { |
442: | continue; |
443: | } |
444: | $instance = Loader::fromDocroot(null, $path, $auth); |
445: | $instance->clearFailed(); |
446: | info("Reset failed status on `%s/%s'", $instance->getHostname(), $instance->getPath()); |
447: | $count++; |
448: | } |
449: | } |
450: | |
451: | return $count; |
452: | } |
453: | |
454: | |
455: | |
456: | |
457: | |
458: | |
459: | |
460: | public function locate_webapps($site = null): array |
461: | { |
462: | return Finder::find($site); |
463: | } |
464: | |
465: | |
466: | |
467: | |
468: | |
469: | |
470: | |
471: | public function prune_webapps($site = null): void |
472: | { |
473: | Finder::prune($site); |
474: | } |
475: | |
476: | |
477: | |
478: | |
479: | |
480: | |
481: | |
482: | |
483: | public function delete_site(?string $site, array $flags = []): bool |
484: | { |
485: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
486: | return error("Sites may not be modified in demo mode"); |
487: | } |
488: | |
489: | if (!IS_CLI) { |
490: | return $this->query('admin_delete_site', $site, $flags); |
491: | } |
492: | $args = CliParser::buildFlags($flags); |
493: | $proc = new Util_Process_Safe; |
494: | $proc->setDescriptor(self::CLI_OOB_FD, 'pipe', 'w', 'oob'); |
495: | $ret = $proc->run(INCLUDE_PATH . "/bin/DeleteDomain ${args} --fd=%d --output=json %s", self::CLI_OOB_FD, $site); |
496: | |
497: | if (!Error_Reporter::merge_json($ret['oob'])) { |
498: | return warn('Failed to read response - output received: %s', $ret['oob']); |
499: | } |
500: | |
501: | return $ret['success']; |
502: | } |
503: | |
504: | |
505: | |
506: | |
507: | |
508: | |
509: | |
510: | |
511: | |
512: | |
513: | public function add_site(string $domain, string $admin, array $opts = [], array $flags = []): bool |
514: | { |
515: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
516: | return error("Sites may not be modified in demo mode"); |
517: | } |
518: | |
519: | if (!IS_CLI) { |
520: | return $this->query('admin_add_site', $domain, $admin, $opts, $flags); |
521: | } |
522: | array_set($opts, 'siteinfo.admin_user', $admin); |
523: | array_set($opts, 'siteinfo.domain', $domain); |
524: | $plan = array_pull($opts, 'siteinfo.plan'); |
525: | $args = CliParser::commandifyConfiguration($opts); |
526: | |
527: | if ($plan) { |
528: | $args = '--plan=' . escapeshellarg($plan) . ' ' . $args; |
529: | } |
530: | $args = CliParser::buildFlags($flags) . ' ' . $args; |
531: | $cmd = INCLUDE_PATH . "/bin/AddDomain --fd=%d --output=json ${args}"; |
532: | info('%(bin)s command: %(command)s', ['bin' => 'AddDomain', 'command' => $cmd]); |
533: | $proc = new Util_Process_Safe; |
534: | $proc->setDescriptor(self::CLI_OOB_FD, 'pipe', 'w', 'oob'); |
535: | $ret = $proc->run($cmd, self::CLI_OOB_FD); |
536: | if (!Error_Reporter::merge_json($ret['oob'])) { |
537: | return error('Failed to read response - output received: %s', $ret['oob']); |
538: | } |
539: | |
540: | return $ret['success']; |
541: | } |
542: | |
543: | |
544: | |
545: | |
546: | |
547: | |
548: | |
549: | |
550: | |
551: | public function edit_site(string $site, array $opts = [], array $flags = []): bool |
552: | { |
553: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
554: | return error("Sites may not be modified in demo mode"); |
555: | } |
556: | |
557: | if (!IS_CLI) { |
558: | return $this->query('admin_edit_site', $site, $opts, $flags); |
559: | } |
560: | |
561: | $plan = array_pull($opts, 'siteinfo.plan'); |
562: | $args = CliParser::commandifyConfiguration($opts); |
563: | |
564: | if ($plan) { |
565: | $args = '--plan=' . escapeshellarg($plan) . ' ' . $args; |
566: | } |
567: | $args = CliParser::buildFlags($flags) . ' ' . $args; |
568: | $cmd = INCLUDE_PATH . "/bin/EditDomain --fd=%d --output=json ${args} %s"; |
569: | info('%(bin)s command: %(command)s', ['bin' => 'EditDomain', 'command' => $cmd]); |
570: | $proc = new Util_Process_Safe; |
571: | $proc->setDescriptor(self::CLI_OOB_FD, 'pipe', 'w', 'oob'); |
572: | $ret = $proc->run($cmd, self::CLI_OOB_FD, $site); |
573: | if (!Error_Reporter::merge_json($ret['oob'])) { |
574: | return error('Failed to read response - output received: %s', $ret['oob']); |
575: | } |
576: | |
577: | return $ret['success']; |
578: | } |
579: | |
580: | |
581: | |
582: | |
583: | |
584: | |
585: | |
586: | public function activate_site($site): bool |
587: | { |
588: | if (!IS_CLI) { |
589: | return $this->query('admin_activate_site', $site); |
590: | } |
591: | |
592: | $site = implode(' ', array_map('escapeshellarg', (array)$site)); |
593: | $proc = new Util_Process; |
594: | $proc->setDescriptor(self::CLI_OOB_FD, 'pipe', 'w', 'oob'); |
595: | $ret = $proc->run(INCLUDE_PATH . '/bin/ActivateDomain --fd=%d --output=json %s', self::CLI_OOB_FD, $site); |
596: | Error_Reporter::merge_json($ret['oob']); |
597: | |
598: | return $ret['success']; |
599: | } |
600: | |
601: | |
602: | |
603: | |
604: | |
605: | |
606: | |
607: | |
608: | public function deactivate_site($site, array $flags = []): bool |
609: | { |
610: | return $this->suspend_site($site, $flags); |
611: | } |
612: | |
613: | |
614: | |
615: | |
616: | |
617: | |
618: | |
619: | |
620: | public function suspend_site($site, array $flags = []): bool |
621: | { |
622: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
623: | return error("Sites may not be modified in demo mode"); |
624: | } |
625: | |
626: | if (!IS_CLI) { |
627: | return $this->query('admin_suspend_site', $site, $flags); |
628: | } |
629: | |
630: | $site = implode(' ', array_map('escapeshellarg', (array)$site)); |
631: | $args = CliParser::buildFlags($flags); |
632: | $proc = new Util_Process; |
633: | $proc->setDescriptor(self::CLI_OOB_FD, 'pipe', 'w', 'oob'); |
634: | $ret = $proc->run(INCLUDE_PATH . '/bin/SuspendDomain --fd=%d --output=json %s %s', self::CLI_OOB_FD, $args, $site); |
635: | Error_Reporter::merge_json($ret['oob']); |
636: | |
637: | return $ret['success']; |
638: | } |
639: | |
640: | |
641: | |
642: | |
643: | |
644: | |
645: | |
646: | |
647: | |
648: | |
649: | |
650: | public function hijack(string $site, string $user = null, string $gate = null): ?string |
651: | { |
652: | return $this->impersonateRole($site, $user, $gate); |
653: | } |
654: | |
655: | |
656: | |
657: | |
658: | |
659: | |
660: | public function get_storage(): array |
661: | { |
662: | $mounts = $this->stats_get_partition_information(); |
663: | foreach ($mounts as $mount) { |
664: | if ($mount['mount'] !== '/') { |
665: | continue; |
666: | } |
667: | |
668: | return [ |
669: | 'qused' => $mount['used'], |
670: | 'qhard' => $mount['size'], |
671: | 'qsoft' => $mount['size'], |
672: | 'fused' => 0, |
673: | 'fsoft' => PHP_INT_MAX, |
674: | 'fhard' => PHP_INT_MAX |
675: | ]; |
676: | |
677: | } |
678: | warn('Failed to locate root partition / - storage information incomplete'); |
679: | |
680: | return []; |
681: | } |
682: | |
683: | |
684: | |
685: | |
686: | |
687: | |
688: | |
689: | public function get_site_storage(array $sites = []): array |
690: | { |
691: | return $this->get_usage('storage', $sites); |
692: | |
693: | } |
694: | |
695: | |
696: | |
697: | |
698: | |
699: | |
700: | |
701: | |
702: | public function get_usage(string $field = 'storage', array $sites = []): array |
703: | { |
704: | if (!IS_CLI) { |
705: | return $this->query('admin_get_usage', $field, $sites); |
706: | } |
707: | if (!$sites) { |
708: | $sites = Enumerate::sites(); |
709: | } else { |
710: | $sites = array_filter(array_map(static function ($site) { |
711: | $id = Auth::get_site_id_from_anything($site); |
712: | if (null === $id) { |
713: | return null; |
714: | } |
715: | |
716: | return "site$id"; |
717: | }, $sites)); |
718: | } |
719: | |
720: | switch ($field) { |
721: | case 'storage': |
722: | return $this->getStorageUsage($sites); |
723: | case 'bandwidth': |
724: | return $this->getBandwidthUsage($sites); |
725: | case 'cgroup': |
726: | case 'cgroups': |
727: | return $this->getCgroupCacheWrapper($sites); |
728: | default: |
729: | fatal('Unknown resource spec %s', $field); |
730: | } |
731: | |
732: | return []; |
733: | } |
734: | |
735: | private function getStorageUsage(array $sites): array |
736: | { |
737: | $groups = array_filter(array_combine($sites, array_map(static function ($site) { |
738: | $group = Auth::get_group_from_site($site); |
739: | if (false === ($grp = posix_getgrnam($group))) { |
740: | return null; |
741: | } |
742: | |
743: | return $grp['gid']; |
744: | }, $sites))); |
745: | |
746: | $quotas = Quota::getGroup($groups); |
747: | |
748: | foreach ($groups as $site => $gid) { |
749: | $groups[$site] = $quotas[$gid]; |
750: | } |
751: | |
752: | return array_filter($groups); |
753: | } |
754: | |
755: | |
756: | |
757: | |
758: | |
759: | |
760: | |
761: | private function getBandwidthUsage(array $sites): array |
762: | { |
763: | $overage = Bulk::getCurrentUsage(); |
764: | $sites = array_flip($sites); |
765: | $built = []; |
766: | foreach ($overage as $o) { |
767: | $site = 'site' . $o['site_id']; |
768: | if (!isset($sites[$site])) { |
769: | continue; |
770: | } |
771: | $built[$site] = $o; |
772: | } |
773: | |
774: | return $built; |
775: | } |
776: | |
777: | |
778: | |
779: | |
780: | |
781: | |
782: | |
783: | private function getCgroupCacheWrapper(array $sites): array |
784: | { |
785: | $cache = Cache_Global::spawn(); |
786: | $existing = []; |
787: | if (false !== ($tmp = $cache->get(self::CGROUP_CACHE_KEY))) { |
788: | $existing = (array)$tmp; |
789: | } |
790: | |
791: | $search = array_flip($sites); |
792: | if (!($missing = array_diff_key($search, $existing))) { |
793: | return array_intersect_key($existing, $search); |
794: | } |
795: | |
796: | $tokens = []; |
797: | $controllers = $this->cgroup_get_controllers(); |
798: | $dummyGroup = new Group(null); |
799: | foreach ($controllers as $c) { |
800: | $controller = (Controller::make($dummyGroup, $c)); |
801: | $attrs = new MetricsLogging($controller); |
802: | $controllerTokens = $attrs->getMetricTokensFromAttributes($attrs->getLoggableAttributes()); |
803: | $tokens = array_values($tokens + append_config($controllerTokens)); |
804: | } |
805: | |
806: | |
807: | $existing += $this->getCgroupUsage(array_keys($missing), $tokens); |
808: | $cache->set(self::CGROUP_CACHE_KEY, $existing, CGROUP_PREFETCH_TTL); |
809: | |
810: | return array_intersect_key($existing, $search); |
811: | } |
812: | |
813: | |
814: | |
815: | |
816: | |
817: | |
818: | |
819: | |
820: | private function getCgroupUsage(array $sites, array $tokens): array |
821: | { |
822: | if (!TELEMETRY_ENABLED) { |
823: | warn('[telemetry] => enabled is set to false'); |
824: | |
825: | return []; |
826: | } |
827: | |
828: | $sum = (new Collector(PostgreSQL::pdo()))->range( |
829: | $tokens, |
830: | time() - 86400, |
831: | null, |
832: | array_map(static function ($s) { |
833: | |
834: | return (int)substr($s, 4); |
835: | }, $sites) |
836: | ); |
837: | |
838: | $built = []; |
839: | $limits = $this->collect(['cgroup']); |
840: | $sites = Enumerate::sites(); |
841: | |
842: | foreach ($sites as $site) { |
843: | $siteid = (int)substr($site, 4); |
844: | $cpuUsed = $sum['c-cpuacct-usage'][$siteid] ?? null; |
845: | if (null !== $cpuUsed) { |
846: | |
847: | $cpuUsed /= 100; |
848: | } |
849: | $memlimit = $limits[$site]['cgroup']['memory'] ?? null; |
850: | if (null !== $memlimit) { |
851: | $memlimit *= 1024; |
852: | } |
853: | |
854: | $built[$site] = [ |
855: | 'site_id' => $siteid, |
856: | 'cpu' => [ |
857: | 'used' => $cpuUsed, |
858: | 'threshold' => $limits[$site]['cgroup']['cpu'] ?? null |
859: | ], |
860: | 'memory' => [ |
861: | 'used' => $sum['c-memory-used'][$siteid] ?? null, |
862: | 'peak' => $sum['c-memory-peak'][$siteid] ?? null, |
863: | 'threshold' => $memlimit |
864: | ], |
865: | 'pids' => [ |
866: | 'used' => $sum['c-pids-used'][$siteid] ?? null, |
867: | 'threshold' => $limits[$site]['cgroup']['proclimit'] ?? null |
868: | ], |
869: | 'io' => [ |
870: | 'read' => $sum['c-blkio-bw-read'][$siteid] ?? null, |
871: | 'write' => $sum['c-blkio-bw-write'][$siteid] ?? null, |
872: | 'threshold' => $limits[$site]['cgroup']['io'] ?? null |
873: | ] |
874: | ]; |
875: | } |
876: | |
877: | return $built; |
878: | } |
879: | |
880: | |
881: | |
882: | |
883: | |
884: | |
885: | |
886: | |
887: | |
888: | |
889: | |
890: | public function collect(?array $params = [], array $query = null, array $sites = []): array |
891: | { |
892: | if ([] === $params) { |
893: | $params = [ |
894: | 'siteinfo.email', |
895: | 'siteinfo.admin_user', |
896: | 'aliases.aliases', |
897: | 'billing.invoice', |
898: | 'billing.parent_invoice' |
899: | ]; |
900: | } else if ($params && is_array(current($params))) { |
901: | $tmp = []; |
902: | foreach ($params as $k => $items) { |
903: | foreach ($items as $v) { |
904: | $tmp[] = "${k}.${v}"; |
905: | } |
906: | } |
907: | $params = $tmp; |
908: | } |
909: | |
910: | if ($query && !is_array(current($query))) { |
911: | |
912: | |
913: | |
914: | $tmp = []; |
915: | foreach ($query as $k => $v) { |
916: | $k = str_replace(',', '.', $k); |
917: | array_set($tmp, $k, $v); |
918: | } |
919: | $query = $tmp; |
920: | } |
921: | |
922: | $sites = $sites ? \Opcenter\Account\Enumerate::freeform($sites) : \Opcenter\Account\Enumerate::sites(); |
923: | |
924: | $built = array_build(array_filter($sites), static function ($k, $s) use ($params, $query) { |
925: | $oldex = Error_Reporter::exception_upgrade(Error_Reporter::E_FATAL | Error_Reporter::E_ERROR); |
926: | try { |
927: | $id = Auth::get_site_id_from_anything($s); |
928: | $ctx = Auth::context(null, "site${id}"); |
929: | } catch (apnscpException $e) { |
930: | return null; |
931: | } finally { |
932: | Error_Reporter::exception_upgrade($oldex); |
933: | } |
934: | |
935: | |
936: | $account = $ctx->getAccount(); |
937: | $meta = [ |
938: | 'active' => (bool)$account->active |
939: | ]; |
940: | if ((bool)($query['active'] ?? $meta['active']) !== $meta['active']) { |
941: | return null; |
942: | } |
943: | |
944: | unset($query['active']); |
945: | if ($query && Util_PHP::array_diff_assoc_recursive($query, $account->cur)) { |
946: | return null; |
947: | } |
948: | if ($params === null) { |
949: | $params = array_dot(array_keys($account->cur)); |
950: | } |
951: | foreach ($params as $p) { |
952: | array_set($meta, $p, array_get($account->cur, $p)); |
953: | } |
954: | $meta['domain'] = $account->cur['siteinfo']['domain']; |
955: | |
956: | return [$s, $meta]; |
957: | }); |
958: | unset($built['']); |
959: | |
960: | return $built; |
961: | } |
962: | |
963: | |
964: | |
965: | |
966: | |
967: | |
968: | |
969: | public function kill_site(string $site): bool |
970: | { |
971: | if (!IS_CLI) { |
972: | return $this->query('admin_kill_site', $site); |
973: | } |
974: | if (!($admin = $this->get_meta_from_site($site, 'siteinfo', 'admin'))) { |
975: | return error("Failed to lookup admin for site `%s'", $site); |
976: | } |
977: | |
978: | foreach (Process::matchGroup($admin) as $pid) { |
979: | Process::kill($pid, SIGKILL); |
980: | } |
981: | |
982: | return true; |
983: | } |
984: | |
985: | |
986: | |
987: | |
988: | |
989: | |
990: | |
991: | |
992: | |
993: | |
994: | public function get_meta_from_site(string $site, string $service, string $class = null) |
995: | { |
996: | if (!IS_CLI) { |
997: | return $this->query('admin_get_meta_from_site', $site, $service, $class); |
998: | } |
999: | if (!$context = Auth::context(null, $site)) { |
1000: | return error("Unknown domain `%s'", $site); |
1001: | } |
1002: | if (null === ($conf = $context->conf($service))) { |
1003: | return error("Unknown service `%s'", $service); |
1004: | } |
1005: | |
1006: | return array_get($conf, $class, null); |
1007: | } |
1008: | |
1009: | public function get_meta_from_domain(string $domain, string $service, string $class = null) |
1010: | { |
1011: | deprecated_func('Use %(name)s', 'admin_get_meta_from_site'); |
1012: | return $this->get_meta_from_site($domain, $service, $class); |
1013: | } |
1014: | |
1015: | |
1016: | |
1017: | |
1018: | |
1019: | |
1020: | |
1021: | public function activate_license(string $key): bool |
1022: | { |
1023: | if (!IS_CLI) { |
1024: | return $this->query('admin_activate_license', $key); |
1025: | } |
1026: | |
1027: | return (License::get()->issue($key) && Apnscp::restart()) || error('Failed to activate license'); |
1028: | } |
1029: | |
1030: | |
1031: | |
1032: | |
1033: | |
1034: | |
1035: | public function renew_license(): bool |
1036: | { |
1037: | if (!IS_CLI) { |
1038: | return $this->query('admin_renew_license'); |
1039: | } |
1040: | $license = License::get(); |
1041: | if (!$license->installed()) { |
1042: | return error('License is not installed'); |
1043: | } |
1044: | if ($license->isTrial()) { |
1045: | return error('Cannot renew trial licenses'); |
1046: | } |
1047: | if ($license->isLifetime()) { |
1048: | return warn('Lifetime licenses never need renewal'); |
1049: | } |
1050: | if (!$license->needsReissue()) { |
1051: | return warn('License does not need reissue. %d days until expiry', $license->daysUntilExpire()); |
1052: | } |
1053: | if (!$license->reissue()) { |
1054: | return false; |
1055: | } |
1056: | info('Restarting ' . PANEL_BRAND); |
1057: | |
1058: | return Apnscp::restart(); |
1059: | } |
1060: | |
1061: | |
1062: | |
1063: | |
1064: | |
1065: | |
1066: | |
1067: | public function read_map(string $map): array |
1068: | { |
1069: | if (!IS_CLI) { |
1070: | return $this->query('admin_read_map', $map); |
1071: | } |
1072: | if (!$map || $map[0] === '.' || $map[0] === '/') { |
1073: | error("Invalid map specified `%s'", $map); |
1074: | |
1075: | return []; |
1076: | } |
1077: | $path = Map::home() . DIRECTORY_SEPARATOR . $map; |
1078: | |
1079: | if (!file_exists($path)) { |
1080: | error("Invalid map specified `%s'", $map); |
1081: | |
1082: | return []; |
1083: | } |
1084: | |
1085: | return Map::load($map)->fetchAll(); |
1086: | } |
1087: | |
1088: | |
1089: | |
1090: | |
1091: | |
1092: | |
1093: | |
1094: | |
1095: | public function create_from_meta(string $site, array $meta = []): ?string |
1096: | { |
1097: | $createConfig = []; |
1098: | foreach (dot($meta) as $k => $v) { |
1099: | array_set($createConfig, $k, $v); |
1100: | } |
1101: | if (null === ($ctx = \Auth::nullableContext(null, $site))) { |
1102: | error("Site `%s' not found - pass \$site as null to generate fresh command", $site); |
1103: | return null; |
1104: | } |
1105: | |
1106: | $createConfig = array_replace_recursive($ctx->getAccount()->cur, $createConfig); |
1107: | $ctx = $ctx->getAccount(); |
1108: | |
1109: | $editor = new Util_Account_Editor($ctx); |
1110: | |
1111: | foreach ($createConfig as $svc => $v) { |
1112: | foreach ($v as $var => $val) { |
1113: | $editor->setConfig($svc, $var, $val); |
1114: | } |
1115: | } |
1116: | return $editor->setMode('add')->getCommand(); |
1117: | } |
1118: | |
1119: | public function _housekeeping() |
1120: | { |
1121: | $configHome = static::ADMIN_HOME . '/' . self::ADMIN_CONFIG; |
1122: | if (!is_dir($configHome)) { |
1123: | Opcenter\Filesystem::mkdir($configHome, APNSCP_SYSTEM_USER, APNSCP_SYSTEM_USER, 0700); |
1124: | } |
1125: | |
1126: | $defplan = Plans::path(Plans::default()); |
1127: | if (!is_dir($defplan)) { |
1128: | $base = Plans::path(''); |
1129: | |
1130: | $dh = opendir($base); |
1131: | if (!$dh) { |
1132: | return error("Plan path `%s' missing, account creation will fail until fixed", |
1133: | $base |
1134: | ); |
1135: | } |
1136: | while (false !== ($f = readdir($dh))) { |
1137: | if ($f === '..' || $f === '.') { |
1138: | continue; |
1139: | } |
1140: | $path = $base . DIRECTORY_SEPARATOR . $f; |
1141: | if (is_link($path)) { |
1142: | unlink($path); |
1143: | break; |
1144: | } |
1145: | } |
1146: | if ($f !== false) { |
1147: | info("old default plan `%s' renamed to `%s'", |
1148: | $f, Plans::default() |
1149: | ); |
1150: | } |
1151: | symlink(dirname($defplan) . '/.skeleton', $defplan); |
1152: | } |
1153: | |
1154: | |
1155: | $themepath = public_path('images/themes/current'); |
1156: | if (is_link($themepath) && basename(readlink($themepath)) === STYLE_THEME) { |
1157: | return; |
1158: | } |
1159: | is_link($themepath) && unlink($themepath); |
1160: | $curpath = dirname($themepath) . '/current'; |
1161: | symlink(STYLE_THEME, $curpath); |
1162: | if (!is_dir(readlink($curpath))) { |
1163: | Opcenter\Filesystem::mkdir($curpath, APNSCP_SYSTEM_USER, APNSCP_SYSTEM_USER); |
1164: | } |
1165: | } |
1166: | |
1167: | |
1168: | |
1169: | |
1170: | |
1171: | |
1172: | |
1173: | |
1174: | |
1175: | public function bless(string $domain, string $user, array $creation = []): bool |
1176: | { |
1177: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
1178: | return error("Demo may not bless sites"); |
1179: | } |
1180: | |
1181: | if (!\Auth::domain_exists($domain)) { |
1182: | return error("Domain `%s' does not exist", $domain); |
1183: | } |
1184: | |
1185: | $args = [ |
1186: | 'siteinfo.admin_user' => $user |
1187: | ]; |
1188: | return (new \Opcenter\Account\Bless($domain, $this->getAuthContext()))->create($creation + $args); |
1189: | } |
1190: | |
1191: | public function _cron(Cronus $c) |
1192: | { |
1193: | |
1194: | $exception = Error_Reporter::exception_upgrade(Error_Reporter::E_FATAL); |
1195: | foreach (Enumerate::sites() as $site) { |
1196: | try { |
1197: | Auth::context(null, $site); |
1198: | } catch (apnscpException $e) { |
1199: | continue; |
1200: | } |
1201: | } |
1202: | Error_Reporter::exception_upgrade($exception); |
1203: | |
1204: | $c->schedule( |
1205: | (int)(Auth_Info_Account::CACHE_DURATION - ceil(Auth_Info_Account::CACHE_DURATION / CRON_RESOLUTION)), |
1206: | 'account.cache', |
1207: | static function () { |
1208: | |
1209: | $exception = Error_Reporter::exception_upgrade(Error_Reporter::E_FATAL); |
1210: | foreach (Enumerate::sites() as $site) { |
1211: | try { |
1212: | Auth::context(null, $site); |
1213: | } catch (apnscpException $e) { |
1214: | continue; |
1215: | } |
1216: | } |
1217: | Error_Reporter::exception_upgrade($exception); |
1218: | }); |
1219: | |
1220: | if (CGROUP_SHOW_USAGE) { |
1221: | $cache = Cache_Global::spawn(); |
1222: | if (!$cache->exists(self::CGROUP_CACHE_KEY)) { |
1223: | $this->getCgroupCacheWrapper(Enumerate::sites()); |
1224: | } |
1225: | } |
1226: | } |
1227: | } |