Passed
Push — develop ( 074444...1c8c15 )
by Nikolay
05:52 queued 24s
created

UpgradeFromImageAction::getCfUID()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 4
c 3
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 0
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
        $upgradeScript = '/etc/rc/upgrade/firmware_upgrade.sh';
199
200
        // Mount boot partition
201
        $systemDir = '/system';
202
        Util::mwMkdir($systemDir);
203
        $mount = Util::which('mount');
204
        $result = Processes::mwExec("$mount /dev/{$parameters['bootPartitionName']} $systemDir");
205
        if ($result === 0) {
206
            $upgradeScriptDir = "$systemDir/upgrade";
207
            Util::mwMkdir($upgradeScriptDir);
208
            // Write the future release update script to the boot partition
209
            $res = self::extractNewUpdateScript($parameters['imageFileLocation'], $upgradeScriptDir);
210
            if ($res->success) {
211
                copy("$upgradeScript", "$upgradeScriptDir");
212
                self::prepareEnvironmentFile($upgradeScriptDir, $parameters);
213
                $res->messages[] = "The update script has been written to the boot partition.";
214
            }
215
        } else {
216
            $res->messages[] = "Failed to mount the boot partition /dev/{$parameters['bootPartitionName']}";
217
            $res->success = false;
218
        }
219
220
        return [$res->success, $res->messages];
221
    }
222
223
    /**
224
     * Prepares and executes a script to handle an IMG file upgrade.
225
     *
226
     * @param string $imageFileLocation The location of the IMG file.
227
     * @param string $desiredLocation The desired location for the extracted files.
228
     * @return PBXApiResult An object containing the result of the operation.
229
     */
230
    private static function extractNewUpdateScript(string $imageFileLocation, string $desiredLocation): PBXApiResult
231
    {
232
        $res = new PBXApiResult();
233
        $res->processor = __METHOD__;
234
        $res->success = true;
235
236
        $decompressedImg = $imageFileLocation . '-decompressed.img';
237
        $mountPoint = '/mnt/image_partition';
238
239
        // Ensure mount point directory exists
240
        Util::mwMkdir($mountPoint);
241
        Util::mwMkdir($desiredLocation);
242
243
        // Decompress the IMG file
244
        $gunzip = Util::which('gunzip');
245
        $decompressCmd = "$gunzip -c '{$imageFileLocation}' > '{$decompressedImg}'";
246
        Processes::mwExec($decompressCmd);
247
248
        // Setup loop device with the correct offset
249
        $offset = 1024 * 512;
250
        $loopDev = self::setupLoopDevice($decompressedImg, $offset);
251
252
        if (empty($loopDev)) {
253
            $res->success = false;
254
            $res->messages[] = "Failed to set up the loop device.";
255
            return $res;
256
        }
257
258
        // Mount the first partition as FAT16
259
        $mount = Util::which('mount');
260
        $result = Processes::mwExec("$mount -t vfat $loopDev $mountPoint -o ro,umask=0000");
261
        if ($result !== 0) {
262
            $res->success = false;
263
            $res->messages[] = "Failed to mount the first partition. Check filesystem and options.";
264
            return $res;
265
        }
266
267
        // Extract files from initramfs.igz
268
        $initramfsPath = "{$mountPoint}/boot/initramfs.igz";
269
        self::extractFileFromInitramfs($initramfsPath, 'sbin/pbx_firmware', "{$desiredLocation}/pbx_firmware");
270
        self::extractFileFromInitramfs($initramfsPath, 'etc/version', "{$desiredLocation}/version");
271
272
        // Clean-up
273
        $umount = Util::which('umount');
274
        Processes::mwExec("$umount $mountPoint");
275
        self::destroyLoopDevice($loopDev);
276
        unlink($decompressedImg);
277
278
        $res->data['message'] = "Upgrade process completed successfully.";
279
        return $res;
280
    }
281
282
    /**
283
     * Sets up a loop device for a specified image file at a given offset.
284
     *
285
     * @param string $filePath The path to the file that needs a loop device.
286
     * @param int $offset The byte offset at which to start the loop device.
287
     * @return string|null The path to the loop device, or null if the setup failed.
288
     */
289
    private static function setupLoopDevice(string $filePath, int $offset): ?string
290
    {
291
        $losetup = Util::which('losetup');
292
        $cmd = "{$losetup} --show -f -o {$offset} {$filePath}";
293
294
        // Execute the command and capture the output
295
        Processes::mwExec($cmd, $output, $returnVar);
296
        if ($returnVar === 0 && !empty($output[0])) {
297
            return $output[0];  // Returns the path to the loop device, e.g., /dev/loop0
298
        }
299
300
        return null;  // Return null if the command failed
301
    }
302
303
    /**
304
     * Extracts a specific file from an initramfs image.
305
     *
306
     * @param string $initramfsPath The path to the initramfs file.
307
     * @param string $filePath The path of the file inside the initramfs.
308
     * @param string $outputPath Where to save the extracted file.
309
     *
310
     * @return void
311
     */
312
    private static function extractFileFromInitramfs(string $initramfsPath, string $filePath, string $outputPath): void
313
    {
314
        $gunzip = Util::which('gunzip');
315
        $cpio = Util::which('cpio');
316
        $cmd = "$gunzip -c '{$initramfsPath}' | $cpio -i --to-stdout '{$filePath}' > '{$outputPath}'";
317
        Processes::mwExec($cmd);
318
    }
319
320
    /**
321
     * Destroys a loop device, freeing it up for other uses.
322
     *
323
     * @param string $loopDevice The path to the loop device (e.g., /dev/loop0).
324
     * @return void
325
     */
326
    private static function destroyLoopDevice(string $loopDevice): void
327
    {
328
        $losetup = Util::which('losetup');
329
        $cmd = "{$losetup} -d {$loopDevice}";
330
        Processes::mwExec($cmd, $output, $returnVar);
331
    }
332
333
    /**
334
     * Prepares the .env file for the upgrade process.
335
     *
336
     * @param string $path The path to the directory containing the .env file.
337
     * @param array $parameters An array containing the parameters for the .env file.
338
     * @return void
339
     */
340
    private static function prepareEnvironmentFile(string $path, array $parameters): void
341
    {
342
        $envFilePath = "$path/.env";
343
        $config = [
344
            'STORAGE_UUID' => $parameters['storage_uuid'],
345
            'CF_UUID' => $parameters['cf_uuid'],
346
            'UPDATE_IMG_FILE' => $parameters['imageFileLocation'],
347
        ];
348
        $file = fopen($envFilePath, 'w');
349
350
        foreach ($config as $key => $value) {
351
            fwrite($file, "$key='$value'\n");
352
        }
353
        fclose($file);
354
    }
355
}