|
| 1 | +<?php |
| 2 | + |
| 3 | +/* This file is part of Jeedom. |
| 4 | +* |
| 5 | +* Jeedom is free software: you can redistribute it and/or modify |
| 6 | +* it under the terms of the GNU General Public License as published by |
| 7 | +* the Free Software Foundation, either version 3 of the License, or |
| 8 | +* (at your option) any later version. |
| 9 | +* |
| 10 | +* Jeedom is distributed in the hope that it will be useful, |
| 11 | +* but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | +* GNU General Public License for more details. |
| 14 | +* |
| 15 | +* You should have received a copy of the GNU General Public License |
| 16 | +* along with Jeedom. If not, see <http://www.gnu.org/licenses/>. |
| 17 | +*/ |
| 18 | + |
| 19 | +/* * ***************************Includes********************************* */ |
| 20 | +require_once __DIR__ . '/../../core/php/core.inc.php'; |
| 21 | + |
| 22 | +class recovery { |
| 23 | + /* * *************************Constants****************************** */ |
| 24 | + private const PROGRESS = 'jeedomRecovery'; |
| 25 | + private const CANCEL = 'jeedomRecoveryCancellation'; |
| 26 | + private const DEFAULT_IMGNAME = 'JeedomSystemUpdate.img.gz'; |
| 27 | + |
| 28 | + /* * ***********************Static Methods*************************** */ |
| 29 | + public static function isInstalled() { |
| 30 | + switch (system::getArch()) { |
| 31 | + case 'arm64': |
| 32 | + return file_exists('/etc/jeedom_board'); |
| 33 | + default: |
| 34 | + return false; |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + public static function install(bool $_force = false) { |
| 39 | + switch (system::getArch()) { |
| 40 | + case 'arm64': |
| 41 | + self::writeLog(__('Vérification du script de démarrage', __FILE__)); |
| 42 | + if (!$_force && self::isInstalled()) { |
| 43 | + self::writeLog(__('Le script de démarrage est à jour', __FILE__), 'debug'); |
| 44 | + return true; |
| 45 | + } |
| 46 | + |
| 47 | + $cmd = system::getCmdSudo() . ' /bin/bash ' . __DIR__ . '/../../resources/update_boot_script.sh'; |
| 48 | + $cmd .= ($_force) ? ' -f' : ''; |
| 49 | + $cmd .= ' >> ' . log::getPathToLog(__CLASS__) . ' 2>&1'; |
| 50 | + exec($cmd, $output, $returnCode); |
| 51 | + if ($returnCode == 0) { |
| 52 | + self::writeLog(__('Le script de démarrage a été mis à jour', __FILE__), 'debug'); |
| 53 | + if (!self::isInstalled()) { |
| 54 | + file_put_contents('/etc/jeedom_board', ucfirst(jeedom::getHardwareName())); |
| 55 | + } |
| 56 | + return true; |
| 57 | + } |
| 58 | + |
| 59 | + self::writeLog(__('Impossible de mettre à jour le script de démarrage', __FILE__), 'error'); |
| 60 | + return false; |
| 61 | + default: |
| 62 | + return false; |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + public static function start(string $_hardware, string $_mode = 'auto') { |
| 67 | + cache::delete(self::CANCEL); |
| 68 | + cache::set(self::PROGRESS, false, 60); |
| 69 | + self::writeLog('-----------------------------------------------------------------------------------------', __('Restauration', __FILE__) . ' ' . $_hardware); |
| 70 | + self::setProgress(['step' => __('Initialisation', __FILE__) . ' (' . strtoupper($_mode) . ')', 'details' => __('Initialisation de la procédure de restauration système', __FILE__), 'progress' => 0], 2); |
| 71 | + |
| 72 | + try { |
| 73 | + switch (system::getArch()) { |
| 74 | + case 'arm64': |
| 75 | + $imgInfos = self::getImgInfos($_hardware); |
| 76 | + |
| 77 | + if ($_mode == 'usb') { |
| 78 | + $downloadPath = '/mnt/usb'; |
| 79 | + self::prepareUsbDevice($_hardware, $downloadPath); |
| 80 | + } else { |
| 81 | + $downloadPath = realpath(__DIR__ . '/../../install/update'); |
| 82 | + } |
| 83 | + if (!file_exists($downloadPath . '/' . $imgInfos['name']) && !file_exists($downloadPath . '/' . self::DEFAULT_IMGNAME)) { |
| 84 | + self::checkFreeSpace($downloadPath, $imgInfos['size']); |
| 85 | + } |
| 86 | + self::downloadAndValidateImage($imgInfos['url'], $downloadPath . '/' . $imgInfos['name'], $imgInfos['SHA256']); |
| 87 | + |
| 88 | + self::setProgress(['step' => __('Finalisation', __FILE__) . ' (' . strtoupper($_mode) . ')', 'details' => __('Finalisation de la procédure de restauration système', __FILE__), 'progress' => 98], 2); |
| 89 | + if ($_mode == 'usb') { |
| 90 | + if (!file_exists($downloadPath . '/' . self::DEFAULT_IMGNAME)) { |
| 91 | + self::setProgress(['details' => __('Ecriture du fichier de configuration USB', __FILE__), 'progress' => 99], 1); |
| 92 | + if (!file_put_contents($downloadPath . '/JeedomSystemUpdate.ini', 'update_filename="' . $imgInfos['name'] . '"')) { |
| 93 | + throw new Exception(__('Une erreur est survenue lors de la finalisation de la procédure de restauration système', __FILE__)); |
| 94 | + } |
| 95 | + } |
| 96 | + shell_exec('sudo umount ' . $downloadPath); |
| 97 | + $message = __('Le nouveau système est prêt à être installé depuis la clé USB de restauration', __FILE__); |
| 98 | + $message .= "\n\n" . __('Veuillez redémarrer avec la clé USB branchée dans le port en haut à droite pour effectuer la restauration', __FILE__); |
| 99 | + } else { |
| 100 | + if (!file_exists($downloadPath . '/' . self::DEFAULT_IMGNAME)) { |
| 101 | + self::setProgress(['details' => __('Renommage du fichier de restauration système', __FILE__), 'progress' => 99], 1); |
| 102 | + if (!rename($downloadPath . '/' . $imgInfos['name'], $downloadPath . '/' . self::DEFAULT_IMGNAME)) { |
| 103 | + throw new Exception(__('Une erreur est survenue lors de la finalisation de la procédure de restauration système', __FILE__)); |
| 104 | + } |
| 105 | + } |
| 106 | + $message = __('Le nouveau système est prêt à être déployé automatiquement au prochain démarrage', __FILE__); |
| 107 | + $message .= "\n\n" . __('Veuillez redémarrer pour effectuer la restauration', __FILE__); |
| 108 | + } |
| 109 | + self::setProgress(['step' => __("Félicitations", __FILE__), 'details' => $message, 'progress' => 100], 2); |
| 110 | + return true; |
| 111 | + default: |
| 112 | + throw new Exception(__('Cette fonctionnalité est uniquement disponible sur les systèmes à base ARM64', __FILE__)); |
| 113 | + } |
| 114 | + } catch (Exception $e) { |
| 115 | + self::setProgress(['details' => $e->getMessage(), 'progress' => -1], 2); |
| 116 | + return false; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + public static function cancel() { |
| 121 | + cache::set(self::CANCEL, true, 60); |
| 122 | + } |
| 123 | + |
| 124 | + public static function usbConnected(string $_hardware) { |
| 125 | + foreach (['/dev/sda', '/dev/sdb', '/dev/sdc', '/dev/sdd'] as $device) { |
| 126 | + if (file_exists($device)) { |
| 127 | + $deviceInfos = shell_exec('udevadm info -q path -n ' . $device); |
| 128 | + switch ($_hardware) { |
| 129 | + case 'smart': |
| 130 | + if (strpos($deviceInfos, '/c9100000.usb/usb') !== false) { |
| 131 | + return $device; |
| 132 | + } |
| 133 | + break; |
| 134 | + case 'atlas': |
| 135 | + if (strpos($deviceInfos, '/fe3c0000.usb/usb') !== false) { |
| 136 | + return $device; |
| 137 | + } |
| 138 | + break; |
| 139 | + default: |
| 140 | + if (strpos($deviceInfos, '/usb1/1-1/') !== false) { |
| 141 | + return $device; |
| 142 | + } |
| 143 | + break; |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + return false; |
| 148 | + } |
| 149 | + |
| 150 | + public static function getProgress() { |
| 151 | + return cache::byKey(self::PROGRESS)->getValue(); |
| 152 | + } |
| 153 | + |
| 154 | + private static function setProgress(array $_progress, int $_pause = null) { |
| 155 | + cache::byKey(self::PROGRESS)->setValue(json_encode($_progress))->setLifetime(60)->save(); |
| 156 | + |
| 157 | + if ($_pause) { |
| 158 | + $level = 'info'; |
| 159 | + if (isset($_progress['progress'])) { |
| 160 | + if ($_progress['progress'] == 100) { |
| 161 | + $level = 'debug'; |
| 162 | + } else if ($_progress['progress'] > 0) { |
| 163 | + $level = $_progress['progress'] . '%'; |
| 164 | + } else if ($_progress['progress'] < 0) { |
| 165 | + $level = 'error'; |
| 166 | + if (cache::exist(self::CANCEL)) { |
| 167 | + $level = 'warning'; |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + $log = (isset($_progress['step']) ? $_progress['step'] . ' : ' : '') . (isset($_progress['details']) ? $_progress['details'] : ''); |
| 172 | + self::writeLog($log, $level); |
| 173 | + sleep($_pause); |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + private static function downloadAndValidateImage(string $_url, string $_filepath, string $_sha256) { |
| 178 | + self::setProgress(['step' => __("Téléchargement de l'image système", __FILE__), 'details' => __('Début du téléchargement', __FILE__), 'progress' => 5], 2); |
| 179 | + if (file_exists($imgPath = $_filepath) || file_exists($imgPath = dirname($_filepath) . '/' . self::DEFAULT_IMGNAME)) { |
| 180 | + try { |
| 181 | + self::validateImage($imgPath, $_sha256, false); |
| 182 | + return; |
| 183 | + } catch (Exception $e) { |
| 184 | + if (cache::exist(self::CANCEL)) { |
| 185 | + throw new Exception($e->getMessage()); |
| 186 | + } else { |
| 187 | + self::setProgress(['details' => __('Image système invalide, reprise du téléchargement', __FILE__), 'progress' => 5], 1); |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + // jeedom::cleanFileSystemRight(); |
| 193 | + $error = false; |
| 194 | + $ch = curl_init(); |
| 195 | + $fp = fopen($_filepath, 'wb'); |
| 196 | + |
| 197 | + curl_setopt_array($ch, [ |
| 198 | + CURLOPT_URL => $_url, |
| 199 | + CURLOPT_HEADER => false, |
| 200 | + CURLOPT_FOLLOWLOCATION => true, |
| 201 | + CURLOPT_FILE => $fp, |
| 202 | + CURLOPT_PROGRESSFUNCTION => ['self', 'downloadImageProgress'], |
| 203 | + CURLOPT_NOPROGRESS => false, |
| 204 | + CURLOPT_SSL_VERIFYPEER => false, |
| 205 | + CURLOPT_FAILONERROR => true |
| 206 | + ]); |
| 207 | + |
| 208 | + curl_exec($ch); |
| 209 | + |
| 210 | + if (curl_errno($ch)) { |
| 211 | + $error = __("Erreur lors du téléchargement", __FILE__) . ' : ' . curl_error($ch); |
| 212 | + if (cache::exist(self::CANCEL)) { |
| 213 | + $error = __("Téléchargement annulé à la demande de l'utilisateur", __FILE__); |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + curl_close($ch); |
| 218 | + fclose($fp); |
| 219 | + |
| 220 | + if ($error) { |
| 221 | + unlink($_filepath); |
| 222 | + throw new Exception($error); |
| 223 | + } |
| 224 | + |
| 225 | + self::validateImage($_filepath, $_sha256); |
| 226 | + } |
| 227 | + |
| 228 | + private static function downloadImageProgress($_resource, $_downloadSize, $_downloaded) { |
| 229 | + if (cache::exist(self::CANCEL)) { |
| 230 | + return 1; |
| 231 | + } |
| 232 | + |
| 233 | + if ($_downloaded > 0 && $_downloadSize > 0) { |
| 234 | + $percent = self::calculPercentProgress($_downloaded, $_downloadSize, 95, 5); |
| 235 | + $downloaded = cmd::autoValueArray($_downloaded, 2, 'o'); |
| 236 | + $downloadSize = cmd::autoValueArray($_downloadSize, 2, 'o'); |
| 237 | + $downloadSpeed = cmd::autoValueArray(curl_getinfo($_resource, CURLINFO_SPEED_DOWNLOAD), 2, 'o'); |
| 238 | + self::setProgress(['details' => $downloaded[0] . $downloaded[1] . '/' . $downloadSize[0] . $downloadSize[1] . ' (' . $downloadSpeed[0] . $downloadSpeed[1] . '/s)', 'progress' => $percent]); |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + private static function validateImage(string $_filepath, string $_sha256, bool $_downloaded = true) { |
| 243 | + if ($_downloaded) { |
| 244 | + $message = __("Vérification de l'intégrité de l'image système téléchargée", __FILE__); |
| 245 | + } else { |
| 246 | + $message = __("Vérification de l'intégrité de l'image système trouvée sur la cible", __FILE__); |
| 247 | + } |
| 248 | + self::setProgress(['details' => $message, 'progress' => 95], 1); |
| 249 | + |
| 250 | + $sha256 = hash_file('sha256', $_filepath); |
| 251 | + if (cache::exist(self::CANCEL)) { |
| 252 | + if ($_downloaded) { |
| 253 | + unlink($_filepath); |
| 254 | + } |
| 255 | + throw new Exception(__("Vérification annulée à la demande de l'utilisateur", __FILE__)); |
| 256 | + } |
| 257 | + if ($sha256 != $_sha256) { |
| 258 | + unlink($_filepath); |
| 259 | + throw new Exception(__("Erreur lors de la vérification de l'image système", __FILE__) . ' (' . $sha256 . ' != ' . $_sha256) . ')'; |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + private static function getImgInfos(string $_hardware) { |
| 264 | + self::setProgress(['details' => __("Collecte des informations concernant l'image système", __FILE__) . ' ' . ucfirst($_hardware), 'progress' => 2], 1); |
| 265 | + $url = 'https://images.jeedom.com/'; |
| 266 | + $jsonUrl = $url . $_hardware . '/info.json'; |
| 267 | + |
| 268 | + $jsonContent = @file_get_contents($jsonUrl); |
| 269 | + if ($jsonContent === false) { |
| 270 | + throw new Exception(__("Impossible de récupérer les informations relatives aux images systèmes", __FILE__) . ' (' . $jsonUrl . ')'); |
| 271 | + } |
| 272 | + |
| 273 | + $imgInfos = json_decode($jsonContent, true); |
| 274 | + $minOsVersion = config::byKey('os::min'); |
| 275 | + $currentOsVersion = trim(shell_exec('lsb_release -rs')); |
| 276 | + $osVersion = ((int) $currentOsVersion > (int) $minOsVersion) ? $currentOsVersion : $minOsVersion; |
| 277 | + if (isset($imgInfos[$osVersion]) && isset($imgInfos[$osVersion]['name']) && isset($imgInfos[$osVersion]['SHA256'])) { |
| 278 | + $imgInfos[$osVersion]['url'] = $url . $_hardware . '/' . $imgInfos[$osVersion]['name']; |
| 279 | + $imgInfos[$osVersion]['size'] = ceil((int) trim(shell_exec("curl -sI " . $imgInfos[$osVersion]['url'] . " | grep content-length | awk '{print $2}'")) / 1024); |
| 280 | + return $imgInfos[$osVersion]; |
| 281 | + } |
| 282 | + |
| 283 | + throw new Exception(__("Impossible de trouver les informations requises concernant l'image système", __FILE__) . ' ' . ucfirst($_hardware) . ' ' . __('en version', __FILE__) . ' ' . $osVersion); |
| 284 | + } |
| 285 | + |
| 286 | + private static function prepareUsbDevice(string $_hardware, string $_mountPath = '/mnt/usb') { |
| 287 | + self::setProgress(['details' => __('Vérification du périphérique USB', __FILE__), 'progress' => 3], 1); |
| 288 | + if (!$usbDevice = self::usbConnected($_hardware)) { |
| 289 | + throw new Exception(__('Périphérique USB non détecté', __FILE__)); |
| 290 | + } |
| 291 | + |
| 292 | + $partition = $usbDevice . '1'; |
| 293 | + $fsType = trim(shell_exec('sudo blkid -s TYPE -o value ' . $partition)); |
| 294 | + if ($fsType !== 'vfat') { |
| 295 | + throw new Exception(__("Le système de fichiers de la 1ère partition du périphérique USB n'est pas de type FAT", __FILE__) . ' (' . $fsType . ')'); |
| 296 | + } |
| 297 | + |
| 298 | + if (!file_exists($_mountPath)) { |
| 299 | + shell_exec('sudo mkdir ' . $_mountPath); |
| 300 | + } else if (!empty(shell_exec('mount | grep ' . $_mountPath))) { |
| 301 | + shell_exec('sudo umount ' . $_mountPath); |
| 302 | + } |
| 303 | + exec('sudo mount -o rw,uid=www-data,gid=www-data ' . $partition . ' ' . $_mountPath, $output, $returnCode); |
| 304 | + if ($returnCode !== 0 || empty(shell_exec('mount | grep ' . $_mountPath))) { |
| 305 | + throw new Exception(__("Impossible d'accéder au périphérique USB, vérifier les logs http.error", __FILE__) . ' (' . $partition . ')'); |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + private static function checkFreeSpace(string $_path, int $_imgSize = 1500000) { |
| 310 | + self::setProgress(['details' => __("Vérification de l'espace disque disponible", __FILE__), 'progress' => 4], 1); |
| 311 | + $available = (int) trim(shell_exec("sudo df --output=avail -k $_path | tail -1")); |
| 312 | + if ($available < $_imgSize) { |
| 313 | + throw new Exception(__('Espace disque disponible insuffisant', __FILE__) . ' : ' . $available . 'Ko < ' . $_imgSize . 'Ko (' . $_path . ')'); |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + private static function calculPercentProgress($_done, $_total, float $_max = 100, float $_base = 0) { |
| 318 | + $rawPercent = $_done / $_total; |
| 319 | + $mappedPercent = $_base + ($rawPercent * ($_max - $_base)); |
| 320 | + $percent = round($mappedPercent, 1); |
| 321 | + // return min(max($percent, $_base), $_max); |
| 322 | + return $percent; |
| 323 | + } |
| 324 | + |
| 325 | + private static function writeLog(string $_message, string $_level = 'info') { |
| 326 | + $logLine = "[" . date('Y-m-d H:i:s') . "][" . strtoupper($_level) . "] " . $_message . PHP_EOL; |
| 327 | + file_put_contents(log::getPathToLog(__CLASS__), $logLine, FILE_APPEND); |
| 328 | + } |
| 329 | +} |
0 commit comments