#!/usr/bin/env python3 # # Copyright 2018 Ettus Research, a National Instruments Company # # SPDX-License-Identifier: GPL-3.0-or-later # """ Embedded USRP filesystem update utility """ import sys import os import time import subprocess import argparse import tempfile import requests MPM_DEVICE = None # This is the default value try: from usrp_mpm import __mpm_device__ as MPM_DEVICE except ImportError: print("ERROR: Could not import MPM.") DEFAULT_IMAGES_INSTALL_LOCATION = '/usr/share/uhd/images/' DEFAULT_REMOTE_MANIFEST_URL =\ 'https://raw.githubusercontent.com/EttusResearch/uhd/{tag}/images/manifest.txt' DEFAULT_FS_IMAGE = { 'n3xx': 'usrp_n3xx_fs.mender', 'e320': 'usrp_e320_fs.mender', 'x4xx': 'usrp_x4xx_fs.mender', } DEFAULT_MENDER_TARGET = { 'n3xx': 'n3xx_...._mender_default', 'e320': 'e320_...._mender_default', 'x4xx': 'x4xx_common_mender_default', } def parse_args(): """ Parse args """ parser = argparse.ArgumentParser( description="""USRP filesystem update If run without any options, it will reset the filesystem to the state it was originally in, but with the same version ("factory reset"). By using -m or -t, the precise version of the new filesystem can be selected. -t master will update to the very latest filesystem image. Most options require access to the internet to download the latest manifest and/or filesystem image. """, ) parser.add_argument( '--image', help="Binary image of the filesystem update. If this is given, all " "other options are ignored. Can be a file or a URL", ) parser.add_argument( '-y', '--yes', action="store_true", help="Answer questions with yes", ) parser.add_argument( '-m', '--manifest', help="Manifest source. Can be a file or a URL. Overwrites -t", ) parser.add_argument( '-d', '--device-type', default=MPM_DEVICE, required=MPM_DEVICE is None, help="Specify/overwrite device type " \ "(DANGER! May brick device if used incorrectly!)", ) parser.add_argument( '-t', '--git-tag', help="Will try and locate the given git tag or branch and get the " "corresponding manifest. Ignored when -m is given. Using this " "requires internet access.", ) return parser.parse_args() def download_image(device_type, manifest_path=None): """ Run uhd_images_downloader to fetch the mender image """ downloader_command = [ 'python3', '/usr/bin/uhd_images_downloader', '-t', DEFAULT_MENDER_TARGET[device_type], '-i', DEFAULT_IMAGES_INSTALL_LOCATION, ] if manifest_path is not None: downloader_command = downloader_command + ['-m', manifest_path] subprocess.check_call(downloader_command) def prepare_manifest(manifest): """Create a valid local path for a manifest""" manifest = manifest.strip() if os.path.isfile(manifest): return manifest if manifest.strip().startswith('http'): manifest = manifest.strip() temp_dir = tempfile.mkdtemp() print("Downloading manifest file from {}...".format( manifest.strip())) req_obj = requests.get(manifest) manifest_path = os.path.join(temp_dir, 'manifest.txt') open(manifest_path, 'wb').write(req_obj.content) # This will leave a stray file in /tmp but that's OK... we're blowing # away the FS anyway return manifest_path raise RuntimeError("Unknown manifest location type: " + manifest) def get_manifest_from_tag(git_tag): """Turn a git tag or branch into a URL to a valid manifest""" manifest_url = DEFAULT_REMOTE_MANIFEST_URL.format(tag=git_tag) return prepare_manifest(manifest_url) def prepare_image(device_type, args): """Figure out which mender image to use, and make sure it's available. Returns the path to a valid Mender image. """ if args.image is not None: print("Using image for upgrade: {}".format(args.image)) # This is the only case where we don't invoke the images downloader return args.image manifest_path = None if args.manifest is not None: print("Using manifest to download image: {}".format(args.manifest)) manifest_path = prepare_manifest(args.manifest) elif args.git_tag: manifest_path = get_manifest_from_tag(args.git_tag) else: print("Using default manifest.") # Note: The image downloader may choose to do nothing if the requested # image is already downloaded. It's safe to call anyway. download_image(device_type, manifest_path) return os.path.join( DEFAULT_IMAGES_INSTALL_LOCATION, DEFAULT_FS_IMAGE[device_type] ) def apply_image(mender_image): """Actually run mender to inject the mender_image""" subprocess.check_call(['mender', 'install', mender_image]) def reboot(): """ Reboot """ print("Will reboot now. Hit Ctrl-C before the countdown expires to cancel.") print("Rebooting in 3...") time.sleep(1) print("2...") time.sleep(1) print("1...") time.sleep(1) os.system('reboot') def run(): """Main execution""" args = parse_args() mender_image = prepare_image(args.device_type, args) if mender_image is None: print("Error: Unable to identify filesystem image!") return False apply_image(mender_image) print("Applied image. After reboot, check if everything works, and then " "run the command '$ mender -commit' to confirm (otherwise, this " "update will be undone).") print("Note: Any data stored in this partition will be not accessible " "after reboot.") if args.yes: reboot_now = 'y' else: reboot_now = input("Reboot now? [Yn] ") if reboot_now == "" or reboot_now.lower() == 'y': try: reboot() # This will implicitly terminate the program, but it looks # neater if we have the return statement here return True except KeyboardInterrupt: pass print("Skipping reboot. Your device will load the updated file system " "on the next boot.") return True def main(): """ Go, go, go! """ try: return run() except SystemExit: # for argparse's --help return True except BaseException as ex: print("Error: Unexpected exception caught!") print(str(ex)) return False if __name__ == '__main__': sys.exit(not main())