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