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