Passed
Push — develop ( 8607aa...bbbb88 )
by Портнов
05:06
created

PBXInstaller::convertDiscLayout()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 29
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
c 0
b 0
f 0
dl 0
loc 29
rs 8.4444
cc 8
nc 12
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use MikoPBX\Common\Models\PbxSettings;
23
use MikoPBX\Common\Providers\ConfigProvider;
24
use Phalcon\Config\Config;
25
use Phalcon\Di\Di;
26
use Phalcon\Di\Injectable;
27
28
/**
29
 * Class PBXInstaller
30
 * Handles the installation of MikoPBX onto a selected drive
31
 * @package MikoPBX\Core\System
32
 */
33
class PBXInstaller extends Injectable
34
{
35
    /**
36
     * Access to the /etc/inc/mikopbx-settings.json values
37
     *
38
     * @var \Phalcon\Config\Config
39
     */
40
    private Config $config;
41
42
    private array $valid_disks = [];
43
    private array $selected_disk = ['size' => 0, 'id' => ''];
44
    private string $target_disk = '';
45
46
    // File pointer
47
    private $fp;
48
49
    /**
50
     * PBXInstaller constructor.
51
     * Initiates the installation process.
52
     */
53
    public function __construct()
54
    {
55
56
        $this->config = Di::getDefault()->getShared(ConfigProvider::SERVICE_NAME);
57
58
        $this->fp = fopen('php://stdin', 'rb');
59
60
    }
61
62
    /**
63
     * Initiates the installation steps.
64
     */
65
    public function run(): void
66
    {
67
        $this->scanAllHdd();
68
        if ($this->processValidDisks()){
69
            $this->promptForTargetDisk();
70
            if ($this->confirmInstallation()) {
71
                $this->proceedInstallation();
72
            }
73
        }
74
    }
75
76
    /**
77
     * Scans all connected HDDs.
78
     */
79
    private function scanAllHdd(): void
80
    {
81
        $storage = new Storage();
82
        $all_hdd = $storage->getAllHdd();
83
        foreach ($all_hdd as $disk) {
84
            $this->processDisk($disk);
85
        }
86
    }
87
88
    /**
89
     * Process each disk and save valid ones.
90
     *
91
     * @param array $disk Information about the disk
92
     */
93
    private function processDisk(array $disk): void
94
    {
95
        // Initialize a variable to hold additional info
96
        $other_info = '';
97
98
        // Add info if the disk is a system disk or is mounted
99
        if (true === $disk['sys_disk']) {
100
            $other_info .= ' System Disk ';
101
        }
102
        if (true === $disk['mounted']) {
103
            $other_info .= ' Mounted ';
104
        }
105
106
        // Add a visual effect to the additional info if it's not empty
107
        if ($other_info !== '') {
108
            $other_info = "[ \033[31;1m$other_info\033[0m ]";
109
        }
110
111
        // Update the selected disk if the current disk's size is smaller
112
        if ($this->selected_disk['size'] === 0 || $this->selected_disk['size'] > $disk['size']) {
113
            $this->selected_disk = $disk;
114
        }
115
116
        // Ignore disks that are less than 400 megabytes
117
        if ($disk['size'] < 400) {
118
            return;
119
        }
120
121
        // Add the disk to the list of valid disks
122
        $this->valid_disks[$disk['id']] = "  - {$disk['id']}, {$disk['size_text']}, {$disk['vendor']} $other_info \n";
123
    }
124
125
    /**
126
     * Process the valid disks and select one for installation.
127
     */
128
    private function processValidDisks(): bool
129
    {
130
        // If no valid disks were found, print a message and sleep for 3 seconds, then return 0
131
        if (count($this->valid_disks) === 0) {
132
            SystemMessages::echoWithSyslog(PHP_EOL." " . Util::translate('Valid disks not found...') . " ".PHP_EOL);
133
            sleep(3);
134
            return false;
135
        }
136
137
        // If valid disks were found, print prompts for the user to select a disk
138
        echo "\n " . Util::translate('Select the drive to install the system.') . ' ';
139
        echo "\n " . Util::translate('Selected disk:') . "\033[33;1m [{$this->selected_disk['id']}] \033[0m \n\n";
140
        echo "\n " . Util::translate('Valid disks are:') . " \n\n";
141
142
        // Print each valid disk
143
        foreach ($this->valid_disks as $disk) {
144
            echo $disk;
145
        }
146
        echo "\n";
147
        return true;
148
    }
149
150
    /**
151
     * Prompt the user to select a target disk.
152
     *
153
     * @return void The selected disk id
154
     */
155
    private function promptForTargetDisk(): void
156
    {
157
        // Prompt the user to enter a device name until a valid device name is entered
158
        do {
159
            echo "\n" . Util::translate('Enter the device name:') . Util::translate('(default value = ') . $this->selected_disk['id'] . ') :';
160
            $this->target_disk = trim(fgets($this->fp));
161
            if ($this->target_disk === '') {
162
                $this->target_disk = $this->selected_disk['id'];
163
            }
164
        } while (!array_key_exists($this->target_disk, $this->valid_disks));
165
166
    }
167
168
    /**
169
     * Confirm the installation from the user.
170
     */
171
    private function confirmInstallation(): bool
172
    {
173
        // Warning and confirmation prompt
174
        echo '
175
176
*******************************************************************************
177
* ' . Util::translate('WARNING') . '!
178
* ' . Util::translate('The PBX is about to be installed onto the') . " \033[33;1m$this->target_disk\033[0m.
179
* - " . Util::translate('everything on this device will be erased!') . '
180
* - ' . Util::translate('this cannot be undone!') . '
181
*******************************************************************************
182
183
' . Util::translate('The PBX will reboot after installation.') . '
184
185
' . Util::translate('Do you want to proceed? (y/n): ');
186
187
        // If the user doesn't confirm, save the system disk info to a temp file and exit
188
        if (strtolower(trim(fgets($this->fp))) !== 'y') {
189
            sleep(3);
190
            return false;
191
        }
192
193
        return true;
194
    }
195
196
    /**
197
     * Start the installation process.
198
     */
199
    private function proceedInstallation(): void
200
    {
201
        // Save the target disk to a file
202
        $varEtcDir = Directories::getDir(Directories::CORE_VAR_ETC_DIR);
203
        file_put_contents($varEtcDir . '/cfdevice', $this->target_disk);
204
205
        // Start the installation process
206
        echo Util::translate("Installing PBX...").PHP_EOL;
207
        $this->unmountPartitions();
208
        $this->unpackImage();
209
        $this->mountStorage();
210
        $this->copyConfiguration();
211
212
        // Reboot
213
        file_put_contents('/tmp/ejectcd', '');
214
        System::reboot();
215
    }
216
217
    /**
218
     * Unmount the partitions of the selected disk.
219
     */
220
    private function unmountPartitions(): void
221
    {
222
        echo Util::translate(" - Unmounting partitions...").PHP_EOL;
223
        $grep = Util::which('grep');
224
        $awk = Util::which('awk');
225
        $mount = Util::which('mount');
226
        $umount = Util::which('umount');
227
228
        // Get all mounted partitions
229
        $mnt_dirs = [];
230
        Processes::mwExec("$mount | $grep '^/dev/$this->target_disk' | $awk '{print $3}'", $mnt_dirs);
231
        foreach ($mnt_dirs as $mnt) {
232
            // Terminate all related processes.
233
            Processes::mwExec("/sbin/shell_functions.sh killprocesses '$mnt' -TERM 0;");
234
            // Unmount.
235
            Processes::mwExec("$umount $mnt");
236
        }
237
    }
238
239
    /**
240
     * Unpack the image to the target disk.
241
     */
242
    private function unpackImage(): void
243
    {
244
        echo Util::translate(" - Unpacking img...").PHP_EOL;
245
        $pv = Util::which('pv');
246
        $dd = Util::which('dd');
247
        $gunzip = Util::which('gunzip');
248
249
        $install_cmd = 'exec < /dev/console > /dev/console 2>/dev/console;' .
250
            "$pv -p /offload/firmware.img.gz | $gunzip | $dd of=/dev/$this->target_disk bs=4M 2> /dev/null";
251
        passthru($install_cmd);
252
    }
253
254
    /**
255
     * Mount the storage partition.
256
     */
257
    private function mountStorage(): void
258
    {
259
        // Connect the disk for data storage.
260
        Storage::selectAndConfigureStorageDisk(false,true);
261
    }
262
263
    /**
264
     * Copy the configuration to the target disk.
265
     */
266
    private function copyConfiguration():void
267
    {
268
        // Back up the table with disk information.
269
        echo Util::translate("Copying configuration...").PHP_EOL;
270
        Util::mwMkdir('/mnttmp');
271
272
        echo "Target disk: $this->target_disk ...".PHP_EOL;
273
        $confPartitionName = Storage::getDevPartName($this->target_disk, '3', true);
274
        if(empty($confPartitionName)){
275
            echo "Target partition not found: $this->target_disk (part 3) ...".PHP_EOL;
276
            return;
277
        }
278
        // Mount the disk with settings.
279
        $mount  = Util::which('mount');
280
        $umount = Util::which('umount');
281
        $resUMount = Processes::mwExec("$umount $confPartitionName");
282
        echo "Umount $confPartitionName: $resUMount ...".PHP_EOL;
283
        $resMount = Processes::mwExec("$mount -w -o noatime $confPartitionName /mnttmp");
284
        echo "Mount $confPartitionName to /mnttmp: $resMount ...".PHP_EOL;
285
        $filename = $this->config->path('database.dbfile');
286
        $result_db_file = '/mnttmp/conf/mikopbx.db';
287
288
        /** Copy the settings database file. */
289
        $cp = Util::which('cp');
290
        $sqlite3 = Util::which('sqlite3');
291
        $dmpDbFile = tempnam('/tmp', 'storage');
292
        // Save dump of settings.
293
        $tables = ['m_Storage', 'm_LanInterfaces'];
294
        file_put_contents($dmpDbFile, '');
295
        foreach ($tables as $table) {
296
            echo "DUMP $table from /cf/conf/mikopbx.db ...".PHP_EOL;
297
            $res = shell_exec("sqlite3 /cf/conf/mikopbx.db '.schema $table' >> $dmpDbFile");
298
            $res .= shell_exec("sqlite3 /cf/conf/mikopbx.db '.dump $table' >> $dmpDbFile");
299
            echo "$res ...".PHP_EOL;
300
        }
301
        // If another language is selected - use another settings file.
302
        $lang = PbxSettings::getValueByKey(PbxSettings::SSH_LANGUAGE);
303
        $filename_lang = "/offload/conf/mikopbx-$lang.db";
304
        if ($lang !== 'en' && file_exists($filename_lang)) {
305
            $filename = $filename_lang;
306
        }
307
        // Replace the settings file.
308
        $resCopy = Processes::mwExec("$cp $filename $result_db_file");
309
        echo "Copy $filename to $result_db_file: $resCopy ...".PHP_EOL;
310
        foreach ($tables as $table) {
311
            echo "DROP $table IF EXISTS in $result_db_file ...".PHP_EOL;
312
            $res = shell_exec("$sqlite3 $result_db_file 'DROP TABLE IF EXISTS $table'");
313
            echo "$res ...".PHP_EOL;
314
        }
315
        // Restore settings from backup file.
316
        $resSaveSettings = Processes::mwExec("$sqlite3 $result_db_file < $dmpDbFile");
317
        echo "Save settings to $result_db_file. Result: $resSaveSettings ...".PHP_EOL;
318
        unlink($dmpDbFile);
319
        Processes::mwExec("$umount /mnttmp");
320
    }
321
}