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