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