Skip to content

Commit 2060ba6

Browse files
authored
Merge pull request #3112 from jeedom/arm64-recovery-master
New recovery (Smart/Atlas)
2 parents 8b3126d + adaa47b commit 2060ba6

12 files changed

Lines changed: 980 additions & 6 deletions

File tree

core/ajax/recovery.ajax.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
try {
20+
21+
require_once __DIR__ . '/../php/core.inc.php';
22+
include_file('core', 'authentification', 'php');
23+
24+
if (!isConnect('admin')) {
25+
throw new Exception(__('401 - Accès non autorisé', __FILE__), -1234);
26+
}
27+
28+
ajax::init();
29+
30+
if (init('action') == 'start') {
31+
ajax::success(recovery::start(init('hardware'), init('mode')));
32+
}
33+
34+
if (init('action') == 'cancel') {
35+
ajax::success(recovery::cancel());
36+
}
37+
38+
if (init('action') == 'getProgress') {
39+
ajax::success(recovery::getProgress());
40+
}
41+
42+
if (init('action') == 'usbConnected') {
43+
ajax::success(recovery::usbConnected(init('hardware')));
44+
}
45+
46+
throw new Exception(__('Aucune méthode correspondante à :', __FILE__) . ' ' . init('action'));
47+
/* * *********Catch exeption*************** */
48+
} catch (Exception $e) {
49+
ajax::error(displayException($e), $e->getCode());
50+
}

core/class/recovery.class.php

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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+
}

core/config/default.config.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ logo_light = core/img/logo-jeedom_Light.png
1818
logo_dark = core/img/logo-jeedom_Dark.png
1919
logo_mobile_light = core/img/jeedom_home_Light.png
2020
logo_mobile_dark = core/img/jeedom_home_Dark.png
21+
os::min = 11
2122

2223
;interface
2324
jeedom_theme_main = core2019_Light

core/config/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.4.19
1+
4.4.20

0 commit comments

Comments
 (0)