Passed
Push — develop ( 1c8c15...0cf33e )
by Nikolay
05:48
created

UpgradeFromImageAction::main()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 51
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 30
c 2
b 0
f 0
dl 0
loc 51
rs 8.5066
cc 7
nc 7
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2024 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\PBXCoreREST\Lib\System;
21
22
use MikoPBX\Core\System\Processes;
23
use MikoPBX\Core\System\Storage;
24
use MikoPBX\Core\System\System;
25
use MikoPBX\Core\System\Util;
26
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
27
28
/**
29
 * Upgrade the PBX using uploaded IMG file.
30
 * @package MikoPBX\PBXCoreREST\Lib\System
31
 */
32
class UpgradeFromImageAction extends \Phalcon\Di\Injectable
33
{
34
    const CF_DEVICE = '/var/etc/cfdevice';
35
    const STORAGE_DEVICE = '/var/etc/storage_device';
36
37
    const MIN_SPACE_MB = 400;
38
39
40
    /**
41
     * Upgrade the PBX using uploaded IMG file.
42
     * @param string $imageFileLocation The location of the IMG file previously uploaded to the system.
43
     *
44
     * @return PBXApiResult An object containing the result of the API call.
45
     */
46
    public static function main(string $imageFileLocation): PBXApiResult
47
    {
48
        $res = new PBXApiResult();
49
        $res->processor = __METHOD__;
50
        $res->success = true;
51
        $res->data['message'] = 'In progress...';
52
53
        // Validate input parameters.
54
        list($res->success, $res->messages) = self::validateParameters($imageFileLocation);
55
        if (!$res->success) {
56
            return $res;
57
        }
58
59
        // Check free space
60
        list($res->success, $res->messages) = self::calculateFreeSpace();
61
        if (!$res->success) {
62
            return $res;
63
        }
64
65
        $res->data['imageFileLocation'] = $imageFileLocation;
66
67
        // Generate update script
68
        $res->data['storage_uuid'] = self::getStorageUID();
69
        if (empty($res->data['storage_uuid'])) {
70
            $res->success = false;
71
            $res->messages[] = "The storage disk uid is empty!";
72
            return $res;
73
        }
74
75
        // Get CF disk UID
76
        $res->data['cf_uuid'] = self::getCfUID();
77
        if (empty($res->data['cf_uuid'])) {
78
            $res->success = false;
79
            $res->messages[] = "The CF disk uid is empty!";
80
            return $res;
81
        }
82
83
        // Get Boot device name
84
        $res->data['bootPartitionName'] = self::getBootPartitionName($res->data['cf_uuid']);
85
        if (empty($res->data['bootPartitionName'])) {
86
            $res->success = false;
87
            $res->messages[] = "The Boot partition name is empty!";
88
            return $res;
89
        }
90
91
        // Write update script
92
        list($res->success, $res->messages) = self::writeUpdateScript($res->data);
93
        if ($res->success) {
94
            System::rebootSyncBg();
95
        }
96
        return $res;
97
    }
98
99
    /**
100
     * Validate input parameters.
101
     * @param string $imageFileLocation The location of the IMG file previously uploaded to the system.
102
     * @return array
103
     */
104
    private static function validateParameters(string $imageFileLocation): array
105
    {
106
        $success = true;
107
        $messages = [];
108
        if (!file_exists($imageFileLocation)) {
109
            $success = false;
110
            $messages[] = "The update file '{$imageFileLocation}' could not be found.";
111
        }
112
113
        if (!file_exists(self::CF_DEVICE)) {
114
            $success = false;
115
            $messages[] = "The system setup has not been initiated.";
116
        }
117
118
        if (!file_exists(self::STORAGE_DEVICE)) {
119
            $success = false;
120
            $messages[] = "The storage disk has not been mounted yet!";
121
        }
122
123
        return [$success, $messages];
124
    }
125
126
    /*
127
     * Get Storage disk UID
128
     *
129
     * @return string Storage disk UID
130
     */
131
132
    /**
133
     * Calculates the free space on the storage disk before upgrade.
134
     * @return array
135
     */
136
    private static function calculateFreeSpace(): array
137
    {
138
        $success = true;
139
        $messages = [];
140
        $storageDevice = file_get_contents(self::STORAGE_DEVICE);
141
        if (Storage::getFreeSpace($storageDevice) < self::MIN_SPACE_MB) {
142
            $success = false;
143
            $messages[] = "The storage disk has less than " . self::MIN_SPACE_MB . " MB free space.";
144
        }
145
        return [$success, $messages];
146
    }
147
148
    private static function getStorageUID(): string
149
    {
150
        $grep = Util::which('grep');
151
        $cat = Util::which('cat');
152
        $awk = Util::which('awk');
153
        $storageDeviceFile = self::STORAGE_DEVICE;
154
        $cmd = "$grep $($cat $storageDeviceFile) < /etc/fstab | $awk -F'[= ]' '{ print \$2}'";
155
        return trim(shell_exec($cmd));
156
    }
157
158
    /**
159
     * Get configuration disk UID
160
     *
161
     * @return string configuration disk UID
162
     */
163
    private static function getCfUID(): string
164
    {
165
        $grep = Util::which('grep');
166
        $awk = Util::which('awk');
167
        $cmd = "$grep '/cf' < /etc/fstab | $awk -F'[= ]' '{ print \$2}'";
168
        return trim(shell_exec($cmd));
169
    }
170
171
    /**
172
     * Get boot disk partition name
173
     *
174
     * @return string boot disk partition name
175
     */
176
    private static function getBootPartitionName(string $cf_uuid): string
177
    {
178
        $lsblk = Util::which('lsblk');
179
        $grep = Util::which('grep');
180
        $cut = Util::which('cut');
181
        $cmd = "$lsblk -o UUID,PKNAME -p | $grep '$cf_uuid' | $cut -f 2 -d ' '";
182
        $bootDeviceName = trim(shell_exec($cmd));
183
        return Storage::getDevPartName($bootDeviceName, 1);
184
    }
185
186
    /**
187
     * Prepare an update script, mount the boot partition and write the update script to the partition.
188
     *
189
     * @param array $parameters An array containing the parameters for the script.
190
     * @return array
191
     */
192
    private static function writeUpdateScript(array $parameters): array
193
    {
194
        $res = new PBXApiResult();
195
        $res->processor = __METHOD__;
196
        $res->success = true;
197
198
        // Mount boot partition
199
        $systemDir = '/system';
200
        Util::mwMkdir($systemDir);
201
        $mount = Util::which('mount');
202
        $result = Processes::mwExec("$mount /dev/{$parameters['bootPartitionName']} $systemDir");
203
        if ($result === 0) {
204
            $upgradeScriptDir = "$systemDir/upgrade";
205
            Util::mwMkdir($upgradeScriptDir);
206
            self::prepareEnvironmentFile($upgradeScriptDir, $parameters);
207
208
            // Write the future release update script to the boot partition
209
            $res = self::extractNewUpdateScript($parameters['imageFileLocation'], $upgradeScriptDir);
210
            if (!$res->success) {
211
                $res = self::extractCurrentUpdateScript($upgradeScriptDir);
212
                if ($res->success) {
213
                    $res->messages[] = "The update script has been written to the boot partition.";
214
                }
215
            }
216
        } else {
217
            $res->messages[] = "Failed to mount the boot partition /dev/{$parameters['bootPartitionName']}";
218
            $res->success = false;
219
        }
220
221
        return [$res->success, $res->messages];
222
    }
223
224
    /**
225
     * Prepares and executes a script to handle an IMG file upgrade.
226
     *
227
     * @param string $imageFileLocation The location of the IMG file.
228
     * @param string $desiredLocation The desired location for the extracted files.
229
     * @return PBXApiResult An object containing the result of the operation.
230
     */
231
    private static function extractNewUpdateScript(string $imageFileLocation, string $desiredLocation): PBXApiResult
232
    {
233
        $res = new PBXApiResult();
234
        $res->processor = __METHOD__;
235
        $res->success = true;
236
237
        $decompressedImg = $imageFileLocation . '-decompressed.img';
238
        $mountPoint = '/mnt/image_partition';
239
240
        // Ensure mount point directory exists
241
        Util::mwMkdir($mountPoint);
242
        Util::mwMkdir($desiredLocation);
243
244
        // Decompress the IMG file
245
        $gunzip = Util::which('gunzip');
246
        $decompressCmd = "$gunzip -c '{$imageFileLocation}' > '{$decompressedImg}'";
247
        Processes::mwExec($decompressCmd);
248
249
        // Setup loop device with the correct offset
250
        $offset = 1024 * 512;
251
        $loopDev = self::setupLoopDevice($decompressedImg, $offset);
252
253
        if (empty($loopDev)) {
254
            $res->success = false;
255
            $res->messages[] = "Failed to set up the loop device.";
256
            return $res;
257
        }
258
259
        // Mount the first partition as FAT16
260
        $mount = Util::which('mount');
261
        $result = Processes::mwExec("$mount -t vfat $loopDev $mountPoint -o ro,umask=0000");
262
        if ($result !== 0) {
263
            $res->success = false;
264
            $res->messages[] = "Failed to mount the first partition. Check filesystem and options.";
265
            return $res;
266
        }
267
268
        // Extract files from initramfs.igz
269
        $initramfsPath = "{$mountPoint}/boot/initramfs.igz";
270
        self::extractFileFromInitramfs($initramfsPath, 'sbin/firmware_upgrade.sh', "{$desiredLocation}/firmware_upgrade.sh");
271
        self::extractFileFromInitramfs($initramfsPath, 'sbin/pbx_firmware', "{$desiredLocation}/pbx_firmware");
272
        self::extractFileFromInitramfs($initramfsPath, 'etc/version', "{$desiredLocation}/version");
273
274
        // Clean-up
275
        $umount = Util::which('umount');
276
        Processes::mwExec("$umount $mountPoint");
277
        self::destroyLoopDevice($loopDev);
278
        unlink($decompressedImg);
279
280
        $res->data['message'] = "Upgrade process completed successfully.";
281
        return $res;
282
    }
283
284
    /**
285
     * Copy current release upgrade script to the desired location.
286
     * @param string $desiredLocation The desired location for the extracted files.
287
     * @return PBXApiResult
288
     */
289
    private static function extractCurrentUpdateScript(string $desiredLocation): PBXApiResult
290
    {
291
        $res = new PBXApiResult();
292
        $res->processor = __METHOD__;
293
        $res->success = true;
294
        $upgradeScript = '/sbin/firmware_upgrade.sh';
295
        copy($upgradeScript, $desiredLocation);
296
297
        $pbx_firmware = '/sbin/pbx_firmware';
298
        copy($pbx_firmware, $desiredLocation);
299
300
        return $res;
301
    }
302
303
    /**
304
     * Sets up a loop device for a specified image file at a given offset.
305
     *
306
     * @param string $filePath The path to the file that needs a loop device.
307
     * @param int $offset The byte offset at which to start the loop device.
308
     * @return string|null The path to the loop device, or null if the setup failed.
309
     */
310
    private static function setupLoopDevice(string $filePath, int $offset): ?string
311
    {
312
        $losetup = Util::which('losetup');
313
        $cmd = "{$losetup} --show -f -o {$offset} {$filePath}";
314
315
        // Execute the command and capture the output
316
        Processes::mwExec($cmd, $output, $returnVar);
317
        if ($returnVar === 0 && !empty($output[0])) {
318
            return $output[0];  // Returns the path to the loop device, e.g., /dev/loop0
319
        }
320
321
        return null;  // Return null if the command failed
322
    }
323
324
    /**
325
     * Extracts a specific file from an initramfs image.
326
     *
327
     * @param string $initramfsPath The path to the initramfs file.
328
     * @param string $filePath The path of the file inside the initramfs.
329
     * @param string $outputPath Where to save the extracted file.
330
     *
331
     * @return void
332
     */
333
    private static function extractFileFromInitramfs(string $initramfsPath, string $filePath, string $outputPath): void
334
    {
335
        $gunzip = Util::which('gunzip');
336
        $cpio = Util::which('cpio');
337
        $cmd = "$gunzip -c '{$initramfsPath}' | $cpio -i --to-stdout '{$filePath}' > '{$outputPath}'";
338
        Processes::mwExec($cmd);
339
    }
340
341
    /**
342
     * Destroys a loop device, freeing it up for other uses.
343
     *
344
     * @param string $loopDevice The path to the loop device (e.g., /dev/loop0).
345
     * @return void
346
     */
347
    private static function destroyLoopDevice(string $loopDevice): void
348
    {
349
        $losetup = Util::which('losetup');
350
        $cmd = "{$losetup} -d {$loopDevice}";
351
        Processes::mwExec($cmd, $output, $returnVar);
352
    }
353
354
    /**
355
     * Prepares the .env file for the upgrade process.
356
     *
357
     * @param string $path The path to the directory containing the .env file.
358
     * @param array $parameters An array containing the parameters for the .env file.
359
     * @return void
360
     */
361
    private static function prepareEnvironmentFile(string $path, array $parameters): void
362
    {
363
        $envFilePath = "$path/.env";
364
        $config = [
365
            'STORAGE_UUID' => $parameters['storage_uuid'],
366
            'CF_UUID' => $parameters['cf_uuid'],
367
            'UPDATE_IMG_FILE' => $parameters['imageFileLocation'],
368
        ];
369
        $file = fopen($envFilePath, 'w');
370
371
        foreach ($config as $key => $value) {
372
            fwrite($file, "$key='$value'\n");
373
        }
374
        fclose($file);
375
    }
376
}