#!/usr/bin/env python # -*- coding: utf-8 -*- """ vyos_upgrade.py Compatible with: - Python 2.6.6 - Python 3.x Usage: ./vyos_upgrade.py [-u URL] [-s] [-n N] [-y] [-h] """ from __future__ import print_function import hashlib import hashlib import optparse import os import re import shutil import subprocess import sys try: from urllib.parse import urljoin, urlparse from urllib.request import urlopen except ImportError: from urlparse import urljoin, urlparse from urllib2 import urlopen try: from html.parser import HTMLParser except ImportError: from HTMLParser import HTMLParser DEFAULT_PAGE_URL = "https://vyos.net/get/nightly-builds/" MINISIGN_PUBLIC_KEY = ( "RWSIhkR/dkM2DSaBRniv/bbbAf8hmDqdbOEmgXkf1RxRoxzodgKcDyGq" ) def log(message): print("[*] " + message) def error(message): print("[!] " + message, file=sys.stderr) def text_value(value): """ Convert bytes/unicode to text consistently on Python 2 and 3. """ if isinstance(value, bytes): try: return value.decode("utf-8") except UnicodeDecodeError: return value.decode("latin-1") return value class BuildParser(HTMLParser): """ Extracts the first two links from each
  • inside:
    """ def __init__(self): HTMLParser.__init__(self) self.in_container = False self.container_depth = 0 self.current_li = None self.li_depth = 0 self.current_link = None self.builds = [] def handle_starttag(self, tag, attrs): attrs = dict(attrs) if tag == "div" and attrs.get("id") == "rolling-current": self.in_container = True self.container_depth = 1 return if not self.in_container: return if tag == "div": self.container_depth += 1 if tag == "li": self.current_li = { "links": [] } self.li_depth = 1 return if self.current_li is not None and tag == "a": href = attrs.get("href") if href: self.current_link = { "href": href, "text": [] } def handle_endtag(self, tag): if not self.in_container: return if tag == "a" and self.current_link is not None: text = "".join(self.current_link["text"]).strip() self.current_li["links"].append( (self.current_link["href"], text) ) self.current_link = None return if tag == "li" and self.current_li is not None: links = self.current_li["links"] if len(links) >= 2: iso_url, iso_text = links[0] sig_url, sig_text = links[1] iso_name = iso_text.strip() if not iso_name: iso_name = os.path.basename( urlparse(iso_url).path ) iso_name = iso_name.split("/")[-1] self.builds.append({ "name": iso_name, "iso_url": iso_url, "sig_url": sig_url }) self.current_li = None self.current_link = None return if tag == "div": self.container_depth -= 1 if self.container_depth <= 0: self.in_container = False def handle_data(self, data): if self.current_link is not None: self.current_link["text"].append(data) def read_url(url): response = urlopen(url) try: data = response.read() finally: response.close() return text_value(data) def download_file(url, filename): response = urlopen(url) try: output = open(filename, "wb") try: shutil.copyfileobj(response, output) finally: output.close() finally: response.close() def sha256_file(filename): digest = hashlib.sha256() handle = open(filename, "rb") try: while True: block = handle.read(1024 * 1024) if not block: break digest.update(block) finally: handle.close() return digest.hexdigest() def verify_sha256(iso_file, checksum_file): """ Supports common sha256sum formats, for example: abcdef... filename.iso abcdef... *filename.iso """ handle = open(checksum_file, "rb") try: content = handle.read() finally: handle.close() content = text_value(content) expected = None for line in content.splitlines(): line = line.strip() if not line or line.startswith("#"): continue match = re.match( r"^([0-9a-fA-F]{64})\s+[* ]?(.*)$", line ) if match: expected = match.group(1).lower() break if expected is None: raise RuntimeError( "Could not read a SHA-256 checksum from %s" % checksum_file ) actual = sha256_file(iso_file).lower() return actual == expected def run_minisign(iso_file, signature_file): """ Verify using minisign. The original shell script uses: minisign -Vm ISO -P PUBLIC_KEY This assumes the signature file is named in a way that minisign automatically discovers, or that the signature has been saved beside the ISO using the expected .minisig suffix. """ command = [ "minisign", "-Vm", iso_file, "-P", MINISIGN_PUBLIC_KEY ] process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, stderr = process.communicate() if process.returncode != 0: if stderr: error(text_value(stderr).strip()) return False if stdout: print(text_value(stdout).strip()) return True def verify_iso(iso_file, signature_file): checksum_file = iso_file + ".sha256" if os.path.isfile(checksum_file): log("Checking SHA-256 checksum ...") if not verify_sha256(iso_file, checksum_file): error("SHA-256 mismatch!") return False log("SHA-256 OK") return True if os.path.isfile(signature_file): log("Running minisign verification ...") if not run_minisign(iso_file, signature_file): error("minisign verification failed!") return False log("minisign verification OK") return True error("No checksum or signature file found to verify the ISO.") return False def ask_confirmation(): try: answer = raw_input( "Do you want to install this image now? (y/N) " ) except NameError: answer = input( "Do you want to install this image now? (y/N) " ) return re.match(r"^[Yy]$", answer.strip()) is not None def main(): parser = optparse.OptionParser( usage="%prog [-u URL] [-s] [-n N] [-y] [-h]" ) parser.add_option( "-u", "--url", dest="page_url", default=DEFAULT_PAGE_URL, help="URL containing the build list" ) parser.add_option( "-s", "--install", dest="do_install", action="store_true", default=False, help="Print commands for installing the selected image" ) parser.add_option( "-n", "--number", dest="select_num", type="int", default=1, help="Select the N-th build; default is 1" ) parser.add_option( "-y", "--yes", dest="auto_confirm", action="store_true", default=False, help="Skip the installation confirmation prompt" ) options, unused_args = parser.parse_args() if options.select_num < 1: error("Selection must be at least 1.") return 1 log("Downloading page %s ..." % options.page_url) try: html = read_url(options.page_url) except Exception as exc: error("Failed to download page: %s" % exc) return 1 parser_html = BuildParser() try: parser_html.feed(html) parser_html.close() except Exception as exc: error("Failed to parse page: %s" % exc) return 1 builds = parser_html.builds if not builds: error("No ISO entries were found on the page.") return 1 # Resolve relative URLs against the page URL. for build in builds: build["iso_url"] = urljoin( options.page_url, build["iso_url"] ) build["sig_url"] = urljoin( options.page_url, build["sig_url"] ) log("Found %d nightly builds:" % len(builds)) for index, build in enumerate(builds): print(" %d) %s" % (index + 1, build["name"])) if options.select_num > len(builds): error( "Selection %d is out of range (1-%d)" % (options.select_num, len(builds)) ) return 1 selected = builds[options.select_num - 1] iso_name = selected["name"] iso_url = selected["iso_url"] sig_url = selected["sig_url"] log("You selected: %s" % iso_name) log("ISO URL : %s" % iso_url) log("SIG URL : %s" % sig_url) iso_basename = os.path.basename(urlparse(iso_url).path) sig_basename = os.path.basename(urlparse(sig_url).path) iso_file = "vyos-" + iso_basename sig_file = "vyos-" + sig_basename log("Downloading ISO ...") try: download_file(iso_url, iso_file) except Exception as exc: error("ISO download failed: %s" % exc) return 1 log("Downloading signature ...") try: download_file(sig_url, sig_file) except Exception as exc: error("Signature download failed: %s" % exc) return 1 log("Verifying ISO ...") if not verify_iso(iso_file, sig_file): return 1 log("ISO verification succeeded.") if options.do_install: if not options.auto_confirm: if not ask_confirmation(): log("Installation aborted by user.") return 0 absolute_iso_path = os.path.abspath(iso_file) log("Commands to run inside the VyOS CLI:") print("add system image %s" % absolute_iso_path) print("set system image default-boot %s" % iso_name) print("commit") print("save") print("---------------------------------------------------") if options.auto_confirm: log( "Commands were printed only; run them inside the VyOS CLI." ) log( "All done. Keep the ISO and its signature file; " "they can be used again later." ) return 0 if __name__ == "__main__": sys.exit(main())