1: | <?php |
2: | declare(strict_types=1); |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | class Redis_Module extends Module_Skeleton |
23: | { |
24: | const PREF_KEY = 'redis'; |
25: | protected $exportedFunctions = [ |
26: | '*' => PRIVILEGE_SITE | PRIVILEGE_USER, |
27: | 'key_memusage' => PRIVILEGE_ADMIN, |
28: | 'system_info' => PRIVILEGE_ADMIN, |
29: | 'version' => PRIVILEGE_ALL |
30: | ]; |
31: | |
32: | public function __construct() |
33: | { |
34: | parent::__construct(); |
35: | if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE) && (!SSH_USER_DAEMONS || !$this->ssh_enabled())) { |
36: | $this->exportedFunctions['*'] = PRIVILEGE_NONE; |
37: | } |
38: | } |
39: | |
40: | |
41: | |
42: | |
43: | |
44: | |
45: | |
46: | |
47: | public function create(string $nickname, array $options = []): bool |
48: | { |
49: | if (!IS_CLI) { |
50: | return $this->query('redis_create', $nickname, $options); |
51: | } |
52: | |
53: | if (strlen($nickname) < 3) { |
54: | return error('Minimum Redis nickname length is 3'); |
55: | } |
56: | |
57: | if (!preg_match(Regex::REDIS_NICKNAME, $nickname)) { |
58: | return error('Invalid connection nickname'); |
59: | } |
60: | if ($this->exists($nickname)) { |
61: | return error('Redis nickname already in use'); |
62: | } |
63: | $options['port'] = $options['port'] ?? 0; |
64: | if (empty($options['unixsocket'])) { |
65: | $port = \Opcenter\Net\Port::firstFree($this->getAuthContext()); |
66: | if (!$port) { |
67: | return error('Unable to locate free port to run Redis service'); |
68: | } |
69: | $options['port'] = $port; |
70: | $options['bind'] = $options['bind'] ?? '127.0.0.1'; |
71: | if ($options['bind'] !== '127.0.0.1' && empty($options['requirepass'])) { |
72: | return error('No password set for Redis connection and connection open to remote connections. A password must be set.'); |
73: | } |
74: | if ($options['bind'] !== '127.0.0.1') { |
75: | return error('External Redis support not supported yet'); |
76: | } |
77: | } |
78: | |
79: | $options['daemonize'] = 'yes'; |
80: | |
81: | $home = $this->user_get_home(); |
82: | $path = $home . '/.redis'; |
83: | |
84: | if (!$this->file_exists($path)) { |
85: | $this->file_create_directory($path, 0700); |
86: | } |
87: | |
88: | if (!isset($options['dir'])) { |
89: | $options['dir'] = $path . '/' . $nickname; |
90: | } |
91: | if (!$this->file_exists($options['dir'])) { |
92: | $this->file_create_directory($options['dir'], 0700); |
93: | } |
94: | |
95: | |
96: | $fstpath = $this->domain_fs_path($this->getRedisConfiguration($nickname)); |
97: | copy(resource_path('templates/redis/redis.conf'), $fstpath); |
98: | \Opcenter\Filesystem::chogp($fstpath, $this->user_id, $this->group_id, 0600); |
99: | $map = \Opcenter\Map::load($fstpath, 'r+', 'textfile'); |
100: | if (empty($options['bind'])) { |
101: | unset($map['bind']); |
102: | } |
103: | $options['daemonize'] = $options['daemonize'] ?? 'yes'; |
104: | $options['pidfile'] = $options['dir'] . '/redis.pid'; |
105: | |
106: | foreach ($options as $k => $v) { |
107: | $map[$k] = $v; |
108: | } |
109: | |
110: | $map->save(); |
111: | |
112: | $cfgfile = $this->getRedisConfiguration($nickname); |
113: | $ret = $this->pman_run('redis-server %(cfg)s', ['cfg' => $cfgfile]); |
114: | if (!$ret['success']) { |
115: | return error('Failed to start redis: %s', $ret['stderr']); |
116: | } |
117: | $prefs = \Preferences::factory($this->getAuthContext()); |
118: | $data = array_get($prefs, self::PREF_KEY, []); |
119: | $data[$nickname] = [ |
120: | 'port' => $options['port'] ?? null, |
121: | 'bind' => $options['bind'] ?? null, |
122: | 'unixsocket' => $options['unixsocket'] ?? null, |
123: | 'type' => isset($options['unixsocket']) ? 'unix' : 'tcp', |
124: | ]; |
125: | $this->common_set_preference(static::PREF_KEY, $data); |
126: | unset($prefs); |
127: | if (!$this->crontab_enabled()) { |
128: | return warn('Cannot create redis-server job on reboot. Cron is not running'); |
129: | } |
130: | |
131: | if (!$this->crontab_add_job('@reboot', null, null, null, null, 'redis-server ' . $cfgfile)) { |
132: | return warn('Failed to create redis-server job for reboot'); |
133: | } |
134: | |
135: | return true; |
136: | } |
137: | |
138: | |
139: | |
140: | |
141: | |
142: | |
143: | |
144: | public function exists(string $nickname): bool |
145: | { |
146: | $prefs = \Preferences::factory($this->getAuthContext()); |
147: | $pdata = array_get($prefs, self::PREF_KEY, []); |
148: | |
149: | return isset($pdata[$nickname]); |
150: | } |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | |
157: | |
158: | protected function getRedisConfiguration(string $nickname): string |
159: | { |
160: | return $this->user_get_home() . '/.redis/' . $nickname . '.conf'; |
161: | } |
162: | |
163: | public function delete(string $nickname): bool |
164: | { |
165: | if (!IS_CLI) { |
166: | return $this->query('redis_delete', $nickname); |
167: | } |
168: | |
169: | if (!$this->exists($nickname)) { |
170: | return error("Unknown Redis instance `%s'", $nickname); |
171: | } |
172: | if ($this->running($nickname) && !$this->stop($nickname)) { |
173: | return error("Failed to stop Redis instance `%s'", $nickname); |
174: | } |
175: | $prefs = \Preferences::factory($this->getAuthContext()); |
176: | $key = static::PREF_KEY; |
177: | $prefs->unlock($this->getApnscpFunctionInterceptor()); |
178: | $redispref = array_get($prefs, $key, []); |
179: | unset($redispref[$nickname]); |
180: | $prefs[$key] = $redispref; |
181: | unset($prefs); |
182: | $home = $this->user_get_home(); |
183: | |
184: | $files = [ |
185: | $cfgfile = $this->getRedisConfiguration($nickname), |
186: | "{$home}/.redis/{$nickname}" |
187: | ]; |
188: | |
189: | foreach ($files as $f) { |
190: | $this->file_delete($f, true); |
191: | } |
192: | $this->crontab_delete_job('@reboot', null, null, null, null, 'redis-server ' . $cfgfile); |
193: | |
194: | return true; |
195: | } |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | public function running(string $name): ?int |
204: | { |
205: | if (!$this->exists($name)) { |
206: | return null; |
207: | } |
208: | |
209: | $config = $this->config($name); |
210: | $pid = $config['pidfile']; |
211: | if (!$pid || !$this->file_exists($pid)) { |
212: | return null; |
213: | } |
214: | $pid = (int)$this->file_get_file_contents($pid); |
215: | |
216: | return \Opcenter\Process::pidMatches($pid, 'redis-server') ? $pid : null; |
217: | } |
218: | |
219: | |
220: | |
221: | |
222: | |
223: | |
224: | |
225: | public function config(string $nickname): ?array |
226: | { |
227: | if (!IS_CLI) { |
228: | return $this->query('redis_config', $nickname); |
229: | } |
230: | $fstcfg = $this->domain_fs_path($this->getRedisConfiguration($nickname)); |
231: | if (!file_exists($fstcfg)) { |
232: | warn("Redis configuration for `%s' missing", $nickname); |
233: | |
234: | return null; |
235: | } |
236: | |
237: | return \Opcenter\Map::load($fstcfg, 'r', 'textfile')->fetchAll(); |
238: | } |
239: | |
240: | |
241: | |
242: | |
243: | |
244: | |
245: | |
246: | public function stop(string $name): bool |
247: | { |
248: | if (!IS_CLI) { |
249: | return $this->query('redis_stop', $name); |
250: | } |
251: | if (!$this->exists($name)) { |
252: | return error("Unknown redis instance `%s'", $name); |
253: | } |
254: | if (!$pid = $this->running($name)) { |
255: | return warn("Instance `%s' not running", $name); |
256: | } |
257: | |
258: | return $this->pman_kill($pid); |
259: | |
260: | |
261: | } |
262: | |
263: | |
264: | |
265: | |
266: | |
267: | |
268: | public function list(): array |
269: | { |
270: | if ($this->permission_level & PRIVILEGE_SITE) { |
271: | $users = array_keys($this->user_get_users()); |
272: | } else { |
273: | $users = [$this->username]; |
274: | |
275: | } |
276: | $instances = []; |
277: | foreach ($users as $user) { |
278: | $prefs = \Preferences::factory(Auth::context($user, $this->site)); |
279: | if (!$config = array_get($prefs, static::PREF_KEY, [])) { |
280: | continue; |
281: | } |
282: | $instances += $config; |
283: | } |
284: | |
285: | return $instances; |
286: | } |
287: | |
288: | |
289: | |
290: | |
291: | |
292: | |
293: | |
294: | public function start(string $name): bool |
295: | { |
296: | if (!$this->exists($name)) { |
297: | return error("Unknown redis instance `%s'", $name); |
298: | } else if ($pid = $this->running($name)) { |
299: | return warn("Redis instance `%s' already running with PID `%s'", $name, $pid); |
300: | } |
301: | $file = $this->getRedisConfiguration($name); |
302: | |
303: | return $this->pman_run('redis-server %s', [$file])['success'] ?? false; |
304: | } |
305: | |
306: | |
307: | |
308: | |
309: | |
310: | |
311: | public function system_info(): ?array |
312: | { |
313: | if (!is_debug()) { |
314: | error('Debug mode must be enabled'); |
315: | |
316: | return null; |
317: | } |
318: | |
319: | return \Cache_Global::spawn()->info(); |
320: | } |
321: | |
322: | |
323: | |
324: | |
325: | |
326: | |
327: | |
328: | public function key_memusage(int $db = 0): ?array { |
329: | if (!is_debug()) { |
330: | error('Debug mode must be enabled'); |
331: | return null; |
332: | } |
333: | |
334: | $cache = \Cache_Global::spawn(); |
335: | if (!version_compare(array_get($cache->info(), 'redis_version', '0.0.0'), '4.0.0', '>=')) { |
336: | error('Key usage requires Redis 4'); |
337: | return null; |
338: | } |
339: | if (!$cache->select($db)) { |
340: | error('Failed to access database %d', $db); |
341: | return null; |
342: | } |
343: | |
344: | $usage = []; |
345: | |
346: | foreach ($cache->rawCommand('KEYS', '*') as $key) { |
347: | $usage[$key] = $cache->rawCommand('MEMORY', 'USAGE', $key); |
348: | } |
349: | |
350: | return $usage; |
351: | } |
352: | |
353: | |
354: | |
355: | |
356: | |
357: | |
358: | public function version(): string |
359: | { |
360: | return strtok(\CLI\Yum\Synchronizer\Utils::getFullVersionFromPackage('redis'), '-'); |
361: | } |
362: | } |