xref: /openbmc/skeleton/pyflashbmc/bmc_update.py (revision bba48eaf)
1#!/usr/bin/env python
2
3# TODO: openbmc/openbmc#2994 remove python 2 support
4try:  # python 2
5    import gobject
6except ImportError:  # python 3
7    from gi.repository import GObject as gobject
8import dbus
9import dbus.service
10import dbus.mainloop.glib
11import subprocess
12import tempfile
13import shutil
14import tarfile
15import os
16from obmc.dbuslib.bindings import get_dbus, DbusProperties, DbusObjectManager
17
18DBUS_NAME = "org.openbmc.control.BmcFlash"
19OBJ_NAME = "/org/openbmc/control/flash/bmc"
20DOWNLOAD_INTF = "org.openbmc.managers.Download"
21
22BMC_DBUS_NAME = "xyz.openbmc_project.State.BMC"
23BMC_OBJ_NAME = "/xyz/openbmc_project/state/bmc0"
24
25UPDATE_PATH = "/run/initramfs"
26
27
28def doExtract(members, files):
29    for tarinfo in members:
30        if tarinfo.name in files:
31            yield tarinfo
32
33
34def save_fw_env():
35    fw_env = "/etc/fw_env.config"
36    lines = 0
37    files = []
38    envcfg = open(fw_env, "r")
39    try:
40        for line in envcfg.readlines():
41            # ignore lines that are blank or start with #
42            if line.startswith("#"):
43                continue
44            if not len(line.strip()):
45                continue
46            fn = line.partition("\t")[0]
47            files.append(fn)
48            lines += 1
49    finally:
50        envcfg.close()
51    if lines < 1 or lines > 2 or (lines == 2 and files[0] != files[1]):
52        raise Exception("Error parsing %s\n" % fw_env)
53    shutil.copyfile(files[0], os.path.join(UPDATE_PATH, "image-u-boot-env"))
54
55
56class BmcFlashControl(DbusProperties, DbusObjectManager):
57    def __init__(self, bus, name):
58        super(BmcFlashControl, self).__init__(conn=bus, object_path=name)
59
60        self.Set(DBUS_NAME, "status", "Idle")
61        self.Set(DBUS_NAME, "filename", "")
62        self.Set(DBUS_NAME, "preserve_network_settings", True)
63        self.Set(DBUS_NAME, "restore_application_defaults", False)
64        self.Set(DBUS_NAME, "update_kernel_and_apps", False)
65        self.Set(DBUS_NAME, "clear_persistent_files", False)
66        self.Set(DBUS_NAME, "auto_apply", False)
67
68        bus.add_signal_receiver(
69            self.download_error_handler, signal_name="DownloadError"
70        )
71        bus.add_signal_receiver(
72            self.download_complete_handler, signal_name="DownloadComplete"
73        )
74
75        self.update_process = None
76        self.progress_name = None
77
78    @dbus.service.method(DBUS_NAME, in_signature="ss", out_signature="")
79    def updateViaTftp(self, ip, filename):
80        self.Set(DBUS_NAME, "status", "Downloading")
81        self.TftpDownload(ip, filename)
82
83    @dbus.service.method(DBUS_NAME, in_signature="s", out_signature="")
84    def update(self, filename):
85        self.Set(DBUS_NAME, "filename", filename)
86        self.download_complete_handler(filename, filename)
87
88    @dbus.service.signal(DOWNLOAD_INTF, signature="ss")
89    def TftpDownload(self, ip, filename):
90        self.Set(DBUS_NAME, "filename", filename)
91        pass
92
93    # Signal handler
94    def download_error_handler(self, filename):
95        if filename == self.Get(DBUS_NAME, "filename"):
96            self.Set(DBUS_NAME, "status", "Download Error")
97
98    def download_complete_handler(self, outfile, filename):
99        # do update
100        if filename != self.Get(DBUS_NAME, "filename"):
101            return
102
103        print("Download complete. Updating...")
104
105        self.Set(DBUS_NAME, "status", "Download Complete")
106        copy_files = {}
107
108        # determine needed files
109        if not self.Get(DBUS_NAME, "update_kernel_and_apps"):
110            copy_files["image-bmc"] = True
111        else:
112            copy_files["image-kernel"] = True
113            copy_files["image-rofs"] = True
114
115        if self.Get(DBUS_NAME, "restore_application_defaults"):
116            copy_files["image-rwfs"] = True
117
118        # make sure files exist in archive
119        try:
120            tar = tarfile.open(outfile, "r")
121            files = {}
122            for f in tar.getnames():
123                files[f] = True
124            tar.close()
125            for f in list(copy_files.keys()):
126                if f not in files:
127                    raise Exception(
128                        "ERROR: File not found in update archive: " + f
129                    )
130
131        except Exception as e:
132            print(str(e))
133            self.Set(DBUS_NAME, "status", "Unpack Error")
134            return
135
136        try:
137            tar = tarfile.open(outfile, "r")
138            tar.extractall(UPDATE_PATH, members=doExtract(tar, copy_files))
139            tar.close()
140
141            if self.Get(DBUS_NAME, "clear_persistent_files"):
142                print("Removing persistent files")
143                try:
144                    os.unlink(UPDATE_PATH + "/whitelist")
145                except OSError as e:
146                    if e.errno == errno.EISDIR:
147                        pass
148                    elif e.errno == errno.ENOENT:
149                        pass
150                    else:
151                        raise
152
153                try:
154                    wldir = UPDATE_PATH + "/whitelist.d"
155
156                    for file in os.listdir(wldir):
157                        os.unlink(os.path.join(wldir, file))
158                except OSError as e:
159                    if e.errno == errno.EISDIR:
160                        pass
161                    else:
162                        raise
163
164            if self.Get(DBUS_NAME, "preserve_network_settings"):
165                print("Preserving network settings")
166                save_fw_env()
167
168        except Exception as e:
169            print(str(e))
170            self.Set(DBUS_NAME, "status", "Unpack Error")
171
172        self.Verify()
173
174    def Verify(self):
175        self.Set(DBUS_NAME, "status", "Checking Image")
176        try:
177            subprocess.check_call(
178                [
179                    "/run/initramfs/update",
180                    "--no-flash",
181                    "--no-save-files",
182                    "--no-restore-files",
183                    "--no-clean-saved-files",
184                ]
185            )
186
187            self.Set(DBUS_NAME, "status", "Image ready to apply.")
188            if self.Get(DBUS_NAME, "auto_apply"):
189                self.Apply()
190        except Exception:
191            self.Set(DBUS_NAME, "auto_apply", False)
192            try:
193                subprocess.check_output(
194                    [
195                        "/run/initramfs/update",
196                        "--no-flash",
197                        "--ignore-mount",
198                        "--no-save-files",
199                        "--no-restore-files",
200                        "--no-clean-saved-files",
201                    ],
202                    stderr=subprocess.STDOUT,
203                )
204                self.Set(
205                    DBUS_NAME,
206                    "status",
207                    "Deferred for mounted filesystem. reboot BMC to apply.",
208                )
209            except subprocess.CalledProcessError as e:
210                self.Set(DBUS_NAME, "status", "Verify error: %s" % e.output)
211            except OSError as e:
212                self.Set(
213                    DBUS_NAME,
214                    "status",
215                    "Verify error: problem calling update: %s" % e.strerror,
216                )
217
218    def Cleanup(self):
219        if self.progress_name:
220            try:
221                os.unlink(self.progress_name)
222                self.progress_name = None
223            except oserror as e:
224                if e.errno == EEXIST:
225                    pass
226                raise
227        self.update_process = None
228        self.Set(DBUS_NAME, "status", "Idle")
229
230    @dbus.service.method(DBUS_NAME, in_signature="", out_signature="")
231    def Abort(self):
232        if self.update_process:
233            try:
234                self.update_process.kill()
235            except Exception:
236                pass
237        for file in os.listdir(UPDATE_PATH):
238            if file.startswith("image-"):
239                os.unlink(os.path.join(UPDATE_PATH, file))
240
241        self.Cleanup()
242
243    @dbus.service.method(DBUS_NAME, in_signature="", out_signature="s")
244    def GetUpdateProgress(self):
245        msg = ""
246
247        if self.update_process and self.update_process.returncode is None:
248            self.update_process.poll()
249
250        if self.update_process is None:
251            pass
252        elif self.update_process.returncode > 0:
253            self.Set(DBUS_NAME, "status", "Apply failed")
254        elif self.update_process.returncode is None:
255            pass
256        else:  # (self.update_process.returncode == 0)
257            files = ""
258            for file in os.listdir(UPDATE_PATH):
259                if file.startswith("image-"):
260                    files = files + file
261            if files == "":
262                msg = "Apply Complete.  Reboot to take effect."
263            else:
264                msg = "Apply Incomplete, Remaining:" + files
265            self.Set(DBUS_NAME, "status", msg)
266
267        msg = self.Get(DBUS_NAME, "status") + "\n"
268        if self.progress_name:
269            try:
270                prog = open(self.progress_name, "r")
271                for line in prog:
272                    # strip off initial sets of xxx\r here
273                    # ignore crlf at the end
274                    # cr will be -1 if no '\r' is found
275                    cr = line.rfind("\r", 0, -2)
276                    msg = msg + line[(cr + 1):]
277            except OSError as e:
278                if e.error == EEXIST:
279                    pass
280                raise
281        return msg
282
283    @dbus.service.method(DBUS_NAME, in_signature="", out_signature="")
284    def Apply(self):
285        progress = None
286        self.Set(DBUS_NAME, "status", "Writing images to flash")
287        try:
288            progress = tempfile.NamedTemporaryFile(
289                delete=False, prefix="progress."
290            )
291            self.progress_name = progress.name
292            self.update_process = subprocess.Popen(
293                ["/run/initramfs/update"],
294                stdout=progress.file,
295                stderr=subprocess.STDOUT,
296            )
297        except Exception as e:
298            try:
299                progress.close()
300                os.unlink(progress.name)
301                self.progress_name = None
302            except Exception:
303                pass
304            raise
305
306        try:
307            progress.close()
308        except Exception:
309            pass
310
311    @dbus.service.method(DBUS_NAME, in_signature="", out_signature="")
312    def PrepareForUpdate(self):
313        subprocess.call(
314            [
315                "fw_setenv",
316                "openbmconce",
317                "copy-files-to-ram copy-base-filesystem-to-ram",
318            ]
319        )
320        # Set the variable twice so that it is written to both environments of
321        # the u-boot redundant environment variables since initramfs can only
322        # read one of the environments.
323        subprocess.call(
324            [
325                "fw_setenv",
326                "openbmconce",
327                "copy-files-to-ram copy-base-filesystem-to-ram",
328            ]
329        )
330        self.Set(DBUS_NAME, "status", "Switch to update mode in progress")
331        o = bus.get_object(BMC_DBUS_NAME, BMC_OBJ_NAME)
332        intf = dbus.Interface(o, "org.freedesktop.DBus.Properties")
333        intf.Set(
334            BMC_DBUS_NAME,
335            "RequestedBMCTransition",
336            "xyz.openbmc_project.State.BMC.Transition.Reboot",
337        )
338
339
340if __name__ == "__main__":
341    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
342
343    bus = get_dbus()
344    obj = BmcFlashControl(bus, OBJ_NAME)
345    mainloop = gobject.MainLoop()
346
347    obj.unmask_signals()
348    name = dbus.service.BusName(DBUS_NAME, bus)
349
350    print("Running Bmc Flash Control")
351    mainloop.run()
352
353# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
354