4ea67ce70a9d4890d3c175ea682e638d435d561f
[project/luci.git] / applications / luci-app-attendedsysupgrade / luasrc / view / attendedsysupgrade.htm
1 <%
2 -- all lua code provided by https://github.com/jow-/
3 -- thank you very much!
4
5     function apply_acls(filename, session)
6         local json = require "luci.jsonc"
7         local util = require "luci.util"
8         local fs   = require "nixio.fs"
9
10         local grants = { }
11
12         local acl = json.parse(fs.readfile(filename))
13         if type(acl) ~= "table" then
14             return
15         end
16
17         local group, perms
18         for group, perms in pairs(acl) do
19             local perm, scopes
20             for perm, scopes in pairs(perms) do
21                 if type(scopes) == "table" then
22                     local scope, objects
23                     for scope, objects in pairs(scopes) do
24                         if type(objects) == "table" then
25                             if not grants[scope] then
26                                 grants[scope] = { }
27                             end
28
29                             if next(objects) == 1 then
30                                 local _, object
31                                 for _, object in ipairs(objects) do
32                                     if not grants[scope][object] then
33                                         grants[scope][object] = { }
34                                     end
35                                     table.insert(grants[scope][object], perm)
36                                 end
37                             else
38                                 local object, funcs
39                                 for object, funcs in pairs(objects) do
40                                     if type(funcs) == "table" then
41                                         local _, func
42                                         for _, func in ipairs(funcs) do
43                                             if not grants[scope][object] then
44                                                 grants[scope][object] = { }
45                                             end
46                                             table.insert(grants[scope][object], func)
47                                         end
48                                     end
49                                 end
50                             end
51                         end
52                     end
53                 end
54             end
55         end
56
57         local _, scope, object, func
58         for scope, _ in pairs(grants) do
59             local objects = { }
60             for object, _ in pairs(_) do
61                 for _, func in ipairs(_) do
62                     table.insert(objects, { object, func })
63                 end
64             end
65
66             util.ubus("session", "grant", {
67                 ubus_rpc_session = session,
68                 scope = scope, objects = objects
69             })
70         end
71     end
72
73     apply_acls("/usr/share/rpcd/acl.d/attendedsysupgrade.json", luci.dispatcher.context.authsession)
74 %>
75 <%+header%>
76 <h2 name="content"><%:Attended Sysupgrade%></h2>
77 <div class="cbi-map-descr">
78         Easily search and install new releases and package upgrades. Sysupgrade firmware are created on demand based on locally installed packages.
79 </div>
80 <div style="display: none" id="info_box" class="alert-message info"></div>
81 <div style="display: none" id="error_box" class="alert-message danger"></div>
82 <div style="display: none" id="packages" class="alert-message success"></div>
83 <p>
84 <textarea style="display: none; width: 100%;" id="edit_packages" rows="15"></textarea>
85 </p>
86 <fieldset class="cbi-section">
87         <form method="post" action="">
88                 <div class="cbi-selection-node">
89                         <div class="cbi-value" id="keep_container" style="display: none">
90                                 <div class="cbi-section-descr">
91                                         Check "Keep settings" to retain the current configuration (requires a compatible firmware).
92                                 </div>
93                                 <label class="cbi-value-title" for="keep">Keep settings:</label>
94                                 <div class="cbi-value-field">
95                                         <input name="keep" id="keep" checked="checked" type="checkbox">
96                                 </div>
97                         </div>
98                         <div class="cbi-value" id="edit_button" style="display: none">
99                                 <div class="cbi-value-field">
100                                         <input class="cbi-button" value="Edit installed packages" onclick="edit_packages()" type="button">
101                                 </div>
102                         </div>
103                         <div class="cbi-value cbi-value" id="server_div" style="display:none">
104                                 <label class="cbi-value-title" for="server">Server:</label>
105                                 <div class="cbi-value-field">
106                                         <input onclick="edit_server()" class="cbi-button cbi-button-edit" value="" type="button" id="server" name="server">
107                                 </div>
108                         </div>
109                         <div class="cbi-value cbi-value-last">
110                                 <div class="cbi-value-field">
111                                         <input class="cbi-button cbi-button-apply" value="Search for upgrades" style="display: none" onclick="upgrade_check()" type="button" id="upgrade_button">
112                                 </div>
113                         </div>
114                 </div>
115         </form>
116 </fieldset>
117 <script type="text/javascript">
118 data = {};
119 origin = document.location.href.replace(location.pathname, "")
120 ubus_url = origin + "/ubus/"
121
122 function set_server() {
123         document.getElementById("error_box").style.display = "none";
124         data.url = document.getElementById("server").value;
125         ubus_call("uci", "set", { "config": "attendedsysupgrade", "section": "server", values: { "url": data.url } })
126         ubus_call("uci", "commit", { "config": "attendedsysupgrade" })
127         var server_button = document.getElementById("server")
128         server_button.type = 'button';
129         server_button.className = 'cbi-button cbi-button-edit';
130         server_button.parentElement.removeChild(document.getElementById("button_set"));
131         server_button.onclick = edit_server;
132 }
133
134 function edit_server() {
135         document.getElementById("server").type = 'text';
136         document.getElementById("server").onkeydown = function(event) {
137                 if(event.key === 'Enter') {
138                         set_server();
139                         return false;
140                 }
141         }
142         document.getElementById("server").className = '';
143         document.getElementById("server").onclick = null;
144
145         button_set = document.createElement("input");
146         button_set.type = "button";
147         button_set.value = "Save";
148         button_set.name = "button_set";
149         button_set.id = "button_set";
150         button_set.className = 'cbi-button cbi-button-edit';
151         button_set.style = 'background-image: url("/luci-static/resources/cbi/save.gif");'
152         button_set.onclick = set_server
153         document.getElementById("server").parentElement.appendChild(button_set);
154 }
155
156 function edit_packages() {
157         data.edit_packages = true
158         document.getElementById("edit_button").style.display = "none";
159         document.getElementById("edit_packages").value = data.packages.join("\n");
160         document.getElementById("edit_packages").style.display = "block";
161 }
162
163 // requests to the upgrade server
164 function server_request(request_dict, path, callback) {
165         request_dict.distro = data.release.distribution;
166         request_dict.target = data.release.target.split("\/")[0];
167         request_dict.subtarget = data.release.target.split("\/")[1];
168         var request = new XMLHttpRequest();
169         request.open("POST", data.url + "/" + path, true);
170         request.setRequestHeader("Content-type", "application/json");
171         request.send(JSON.stringify(request_dict));
172         request.onerror = function(e) {
173                 error_box("upgrade server down")
174                 document.getElementById("server_div").style.display = "block";
175         }
176         request.addEventListener('load', function(event) {
177                 callback(request)
178         });
179 }
180
181 // initial setup, get system information
182 function setup() {
183         data["ubus_rpc_session"] = "<%=luci.dispatcher.context.authsession%>"
184         ubus_call("rpc-sys", "packagelist", {}, "packages");
185         ubus_call("system", "board", {}, "release");
186         ubus_call("system", "board", {}, "board_name");
187         ubus_call("system", "board", {}, "model");
188         ubus_call("system", "info", {}, "memory");
189         uci_get({ "config": "attendedsysupgrade", "section": "server", "option": "url" })
190         uci_get({ "config": "attendedsysupgrade", "section": "client", "option": "upgrade_packages" })
191         uci_get({ "config": "attendedsysupgrade", "section": "client", "option": "advanced_mode" })
192         uci_get({ "config": "attendedsysupgrade", "section": "client", "option": "auto_search" })
193         setup_ready();
194 }
195
196 function setup_ready() {
197         // checks if a async ubus calls have finished
198         if(ubus_counter != ubus_closed) {
199                 setTimeout(setup_ready, 300)
200         } else {
201                 if(data.auto_search == 1) {
202                         upgrade_check();
203                 } else {
204                         document.getElementById("upgrade_button").style.display = "block";
205                         document.getElementById("server_div").style.display = "block";
206                         document.getElementById("server").value = data.url;
207                 }
208         }
209 }
210
211 function uci_get(option) {
212         // simple wrapper to get a uci value store in data.<option>
213         ubus_call("uci", "get", option, option["option"])
214 }
215
216 ubus_counter = 0;
217 ubus_closed = 0;
218 function ubus_call(command, argument, params, variable) {
219         var request_data = {};
220         request_data.jsonrpc = "2.0";
221         request_data.id = ubus_counter;
222         request_data.method = "call";
223         request_data.params = [ data.ubus_rpc_session, command, argument, params ]
224         request_json = JSON.stringify(request_data)
225         ubus_counter++;
226         var request = new XMLHttpRequest();
227         request.open("POST", ubus_url, true);
228         request.setRequestHeader("Content-type", "application/json");
229         request.onload = function(event) {
230                 if(request.status === 200) {
231                         var response = JSON.parse(request.responseText)
232                         if(!("error" in response) && "result" in response) {
233                                 if(response.result.length === 2) {
234                                         if(command === "uci") {
235                                                 data[variable] = response.result[1].value
236                                         } else {
237                                                 data[variable] = response.result[1][variable]
238                                         }
239                                 }
240                         } else {
241                                 error_box("<b>Ubus call faild:</b></br>Request: " + request_json + "</br>Response: " + JSON.stringify(response))
242                         }
243                         ubus_closed++;
244                 }
245         }
246         request.send(request_json);
247 }
248
249 function info_box(info_output, loading) {
250         // Shows notification if upgrade is available
251         // If loading is true then an "processing" animation is added
252         document.getElementById("info_box").style.display = "block";
253         var loading_image = '';
254         if(loading) {
255                 loading_image = '<img src="/luci-static/resources/icons/loading.gif" alt="Loading" style="vertical-align:middle">';
256         }
257         document.getElementById("info_box").innerHTML = loading_image + info_output;
258 }
259
260 function error_box(error_output) {
261         // Shows erros in red box
262         document.getElementById("error_box").style.display = "block";
263         document.getElementById("error_box").innerHTML = error_output;
264         document.getElementById("info_box").style.display = "none";
265 }
266
267 function upgrade_check() {
268         // Asks server for new firmware
269         // If data.upgrade_packages is set to true search for new package versions as well
270         document.getElementById("error_box").style.display = "none";
271         document.getElementById("server_div").style.display = "none";
272         info_box("Searching for upgrades", true);
273         var request_dict = {}
274         request_dict.version = data.release.version;
275         request_dict.packages = data.packages;
276         request_dict.upgrade_packages = data.upgrade_packages
277         server_request(request_dict, "api/upgrade-check", upgrade_check_callback)
278 }
279
280 function upgrade_check_callback(request_text) {
281         var request_json = JSON.parse(request_text)
282
283         // create simple output to tell user whats going to be upgrade (release/packages)
284         var info_output = ""
285         if(request_json.version != undefined) {
286                 info_output += "<h3>New firmware release available</h3>"
287                 info_output += data.release.version + " to " + request_json.version
288                 data.latest_version = request_json.version;
289         }
290         if(request_json.upgrades != undefined) {
291                 info_output += "<h3>Package upgrades available</h3>"
292                 for (upgrade in request_json.upgrades) {
293                         info_output += "<b>" + upgrade + "</b>: " + request_json.upgrades[upgrade][1] + " to " + request_json.upgrades[upgrade][0] + "</br>"
294                 }
295         }
296         data.packages = request_json.packages
297         info_box(info_output)
298
299         if(data.advanced_mode == 1) {
300                 document.getElementById("edit_button").style.display = "block";
301         }
302         var upgrade_button = document.getElementById("upgrade_button")
303         upgrade_button.value = "Request firmware";
304         upgrade_button.style.display = "block";
305         upgrade_button.disabled = false;
306         upgrade_button.onclick = upgrade_request;
307
308 }
309
310 function upgrade_request() {
311         // Request the image
312         // Needed values
313         // version/release
314         // board_name or model (server tries to find the corrent profile)
315         // packages
316         // The rest is added by server_request()
317         document.getElementById("upgrade_button").disabled = true;
318         document.getElementById("edit_packages").style.display = "none";
319         document.getElementById("edit_button").style.display = "none";
320         document.getElementById("keep_container").style.display = "none";
321
322         var request_dict = {}
323         request_dict.version = data.latest_version;
324         request_dict.board = data.board_name
325         request_dict.model = data.model
326
327         if(data.edit_packages == true) {
328                 request_dict.packages = document.getElementById("edit_packages").value.split("\n")
329         } else {
330                 request_dict.packages = data.packages;
331         }
332
333         server_request(request_dict, "api/upgrade-request", upgrade_request_callback)
334 }
335
336 function upgrade_request_callback(request) {
337         // ready to download
338         var request_json = JSON.parse(request);
339         data.sysupgrade_url = request_json.sysupgrade;
340         data.checksum = request_json.checksum;
341         data.filesize = request_json.filesize;
342
343         info_output = "Firmware created"
344         if(data.advanced_mode == 1) {
345                 info_output += '</br><a target="_blank" href="' + data.sysupgrade_url + '.log">Build log</a>'
346         }
347         info_box(info_output);
348
349         document.getElementById("keep_container").style.display = "block";
350         var upgrade_button = document.getElementById("upgrade_button")
351         upgrade_button.disabled = false;
352         upgrade_button.style.display = "block";
353         upgrade_button.value = "Flash firmware";
354         upgrade_button.onclick = download_image;
355 }
356
357 function flash_image() {
358         // Flash image via rpc-sys upgrade_start
359         info_box("Flashing firmware. Don't unpower device", true)
360         ubus_call("rpc-sys", "upgrade_start", { "keep": document.getElementById("keep").checked }, 'message');
361         ping_max = 3600; // in seconds
362         setTimeout(ping_ubus, 10000)
363 }
364
365 function ping_ubus() {
366         // Tries to connect to ubus. If the connection fails the device is likely still rebooting.
367         // If more time than ping_max passes update may failed
368         if(ping_max > 0) {
369                 ping_max--;
370                 var request = new XMLHttpRequest();
371                 request.open("GET", ubus_url, true);
372                 request.addEventListener('error', function(event) {
373                         info_box("Rebooting device", true);
374                         setTimeout(ping_ubus, 1000)
375                 });
376                 request.addEventListener('load', function(event) {
377                         info_box("Success! Please reload web interface");
378                         document.getElementById("upgrade_button").value = "reload page";
379                         document.getElementById("upgrade_button").style.display = "block";
380                         document.getElementById("upgrade_button").disabled = false;
381                         document.getElementById("upgrade_button").onclick = function() { location.reload(); }
382                 });
383                 request.send();
384         } else {
385                 error_box("Web interface could not reconnect to your device. Please reload web interface or check device manually")
386         }
387 }
388
389 function upload_image(blob) {
390         // Uploads received blob data to the server using cgi-io
391         var request = new XMLHttpRequest();
392         var form_data  = new FormData();
393
394         form_data.append("sessionid", data.ubus_rpc_session)
395         form_data.append("filename", "/tmp/firmware.bin")
396         form_data.append("filemode", 755) // insecure?
397         form_data.append("filedata", blob)
398
399         request.addEventListener('load', function(event) {
400                 request_json = JSON.parse(request.responseText)
401                 if(data.checksum != request_json.checksum) {
402                         error_box("Checksum missmatch! Please retry")
403                 } else {
404                         flash_image();
405                 }
406         });
407
408         request.addEventListener('error', function(event) {
409                 info_box("Upload of firmware failed, please retry by reloading web interface")
410         });
411
412         request.open('POST', origin + '/cgi-bin/cgi-upload');
413         request.send(form_data);
414 }
415
416
417 function download_image() {
418         // Download image from server once the url was received by upgrade_request
419         if(data.filesize > data.memory.free) {
420                 error_box("Not enough free memory to download firmware. Please stop unneeded services on router and retry")
421         } else {
422                 document.getElementById("keep_container").style.display = "none";
423                 document.getElementById("upgrade_button").style.display = "none";
424                 var download_request = new XMLHttpRequest();
425                 download_request.open("GET", data.sysupgrade_url);
426                 download_request.responseType = "arraybuffer";
427
428                 download_request.onload = function () {
429                         if (this.status === 200) {
430                                 var blob = new Blob([download_request.response], {type: "application/octet-stream"});
431                                 upload_image(blob)
432                         }
433                 };
434                 info_box("Downloading firmware", true);
435                 download_request.send();
436         }
437 }
438
439 function server_request(request_dict, path, callback) {
440         request_dict.distro = data.release.distribution;
441         request_dict.target = data.release.target.split("\/")[0];
442         request_dict.subtarget = data.release.target.split("\/")[1];
443         var request = new XMLHttpRequest();
444         request.open("POST", data.url + "/" + path, true);
445         request.setRequestHeader("Content-type", "application/json");
446         request.send(JSON.stringify(request_dict));
447         request.onerror = function(e) {
448                 error_box("Upgrade server down or could not connect")
449                 document.getElementById("server_div").style.display = "block";
450         }
451         request.addEventListener('load', function(event) {
452                 request_text = request.responseText;
453                 if (request.status === 200) {
454                         callback(request_text)
455
456                 } else if (request.status === 202) {
457                         var imagebuilder = request.getResponseHeader("X-Imagebuilder-Status");
458                         if(imagebuilder === "queue") {
459                                 // in queue
460                                 var queue = request.getResponseHeader("X-Build-Queue-Position");
461                                 info_box("In build queue position " + queue, true)
462                                 console.log("queued");
463                         } else if(imagebuilder === "initialize") {
464                                 info_box("Setting up ImageBuilder", true)
465                                 console.log("Setting up imagebuilder");
466                         } else if(imagebuilder === "building") {
467                                 info_box("Building image");
468                                 console.log("building");
469                         } else {
470                                 info_box("Processing request");
471                                 console.log(imagebuilder)
472                         }
473                         setTimeout(function() { server_request(request_dict, path, callback) }, 5000)
474
475                 } else if (request.status === 204) {
476                         // no upgrades available
477                         info_box("No upgrades available")
478
479                 } else if (request.status === 400) {
480                         // bad request
481                         request_json = JSON.parse(request_text)
482                         error_box(request_json.error)
483
484                 } else if (request.status === 412) {
485                         // this is a bit generic
486                         error_box("Unsupported device, release, target, subtraget or board")
487
488                 } else if (request.status === 413) {
489                         error_box("No firmware created due to image size. Try again with less packages selected.")
490
491                 } else if (request.status === 422) {
492                         error_box("Unknown package in request")
493
494                 } else if (request.status === 500) {
495                         request_json = JSON.parse(request_text)
496
497                         error_box_content = "<b>Internal server error</b></br>"
498                         error_box_content += request_json.error
499                         if(request_json.log != undefined) {
500                                 data.log_url = request_json.log
501                         }
502                         error_box(error_box_content)
503
504                 } else if (request.status === 501) {
505                         error_box("No sysupgrade file produced, may not supported by modell.")
506
507                 } else if (request.status === 502) {
508                         // python part offline
509                         error_box("Server down for maintenance")
510                         setTimeout(function() { server_request(request_dict, path, callback) }, 30000)
511                 } else if (request.status === 503) {
512                         error_box("Server overloaded")
513                         setTimeout(function() { server_request(request_dict, path, callback) }, 30000)
514                 }
515         });
516 }
517 document.onload = setup()
518 </script>
519
520 <%+footer%>