#!/usr/bin/env python3

# Copyright (C) 2020  Braiins Systems s.r.o.
#
# This file is part of Braiins Open-Source Initiative (BOSI).
#
# BOSI is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# Please, keep in mind that we may also license BOSI or any part thereof
# under a proprietary license. For more information on the terms and conditions
# of such proprietary license or if you have any other questions, please
# contact us at opensource@braiins.com.

import argparse
import base58
import logging
import os
import subprocess
import sys
import tarfile
import tempfile

from subprocess import CalledProcessError
from urllib.request import Request, urlopen

import bos_toolbox.platform.backup as backup
import bos_utils.hwid as hwid

from bos_toolbox.batch import read_hosts
from bos_toolbox.cache import WebCacheContext, WebCache
from bos_toolbox.common import (
    WHATSMINER_EXTENSION_PATH,
    BOS_URl,
    BOS_PLUS_URl,
    BOS_PLUS_NIGHTLY_URl,
    BOS_REFERRAL_URL,
    STAGE3_BUILTIN_SRC_DIR,
)
from bos_toolbox.platform import (
    get_platform_from_ssh,
    OsType,
    Platform,
    PlatformStop,
    OsMode,
)
from bos_toolbox.transfer import upload_local_files, wait_for_port, Progress
from bos_toolbox.util import get_payload_path, get_refid, load_module_from_home_path
from bos_utils.packages import Packages
from bos_utils.ssh import SSHManager, SSHError
from bos_toolbox.platform.descriptor import MinerType, Wm1Variant
from bos_toolbox.platform.antminer import (
    STOCK_USERNAME_ANTMINER,
    STOCK_PASSWORD_ANTMINER_SSH,
    STOCK_PASSWORD_ANTMINER_WEB,
)

from .unlock import unlock, UnlockStop, miner_recognition

LOG = logging.getLogger(__name__)

# default credentials for connecting to device. will ask password if they wont do


# binaries necessary for upgrade will be copied onto host from here (sftp, fw_printenv)
SYSTEM_DIR = 'system'
# recovery partition files?
SOURCE_DIR = 'firmware'
# path to auxiliary scripts
PAYLOAD_DIR = get_payload_path()
# stage3 stagging dir, to be tarballed for transfer
TARGET_DIR = '/tmp/firmware'

STAGE3_DIR = 'upgrade'
STAGE3_BUILTIN_DIR = 'upgrade/builtin'
STAGE3_USER_DIR = 'upgrade/usr'
STAGE2_FILE = 'stage2.tgz'
STAGE3_FILE = 'stage3.tgz'
STAGE3_SCRIPT = 'stage3.sh'
STAGE3_REFID = 'bos_refid'
STAGE3_REFERRAL = 'referral'
SYSUPGRADE_FILE = 'sysupgrade.tar'
SYSUPGRADE_GZ_FILE = 'sysupgrade.tar.gz'

REBOOT_DELAY = (3, 5)


class UpgradeStop(Exception):
    pass


class Locked(Exception):
    def __init__(self, message='Miner is locked'):
        self.message = message
        super().__init__(self.message)


class FirmwareInfo:
    PLUS_SIGNATURE = '-plus'

    def __init__(self, web_cache: WebCache, url, version: str, name: str, ext: str):
        self._web_cache = web_cache
        self.url = url
        self.version = version
        self.name = name
        self.ext = ext

    @property
    def cache_name(self):
        return self.name + self.ext

    def is_plus(self):
        return self.PLUS_SIGNATURE in self.name

    def download(self):
        return self._web_cache.download(self.cache_name, self.url)

    def extract_to_tmp_dir(self):
        extracted_dir = self._web_cache.download_and_extract(self.cache_name, self.url)
        return os.path.join(extracted_dir, self.name)

    def extract(self, cache_name: str, file_path, ungzip_name=None):
        return self._web_cache.extract(cache_name, file_path, ungzip_name)


class PackageInfo:
    def __init__(
        self, web_cache: WebCache, root_url, version: str, name: str, filename
    ):
        self._web_cache = web_cache
        self.root_url = root_url
        self.version = version
        self.name = name
        self.filename = filename

    @property
    def url(self):
        return '/'.join([self.root_url, self.filename])

    def download(self):
        return self._web_cache.download(self.filename, self.url)


class FirmwareCache:
    INDEX_PREFIX = 'toolbox_bos'
    REFERRAL_INDEX = 'Packages'
    FILE_EXT_GZ = '.tar.gz'
    FILE_EXT = '.tar'

    def __init__(
        self, web_cache: WebCache, feeds_url, referral_url, fw_version: str, refid: str
    ):
        self._web_cache = web_cache
        self.feeds_url = feeds_url.rstrip('/')
        self.referral_url = referral_url.rstrip('/')
        self.fw_version = fw_version
        self.refid = refid
        self.target_list = {}
        self.referral_list = []

    def _get_index_url(self, platform: Platform):
        if platform.os_info.type == OsType.BOS:
            mode = platform.os_info.mode
            if mode not in {OsMode.nand, OsMode.sd}:
                LOG.error(f"Unexpected OS mode '{mode.name}'!")
                raise UpgradeStop
            mode = mode.name
        else:
            mode = 'install'

        target = platform.triple.target_full_name
        return f'{self.feeds_url}/{self.INDEX_PREFIX}_{mode}_{target}'

    def _load_fw_list(self, platform: Platform):
        index_url = self._get_index_url(platform)
        LOG.info(f"Downloading firmware list '{index_url}'...")
        fw_list = []
        with urlopen(
            Request(index_url, headers={'User-Agent': 'Mozilla/5.0'})
        ) as response:
            for line in response.readlines():
                fw_version, fw_file_path = line.decode().split()

                for fw_ext in [self.FILE_EXT_GZ, self.FILE_EXT]:
                    if fw_file_path.endswith(fw_ext):
                        fw_name = fw_file_path[: -len(fw_ext)]
                        version_prefix = f'{fw_version}/'
                        if fw_name.startswith(version_prefix):
                            fw_name = fw_name[len(version_prefix) :]
                        fw_name = fw_name.replace('/', '_')
                        break
                else:
                    LOG.error(
                        f"Unexpected file extension of firmware '{fw_file_path}'!"
                    )
                    raise UpgradeStop

                fw_info = FirmwareInfo(
                    web_cache=self._web_cache,
                    url=f'{self.feeds_url}/{fw_file_path}',
                    version=fw_version,
                    name=fw_name,
                    ext=fw_ext,
                )
                fw_list.append(fw_info)
        return fw_list

    def get_fw_list(self, platform: Platform):
        fw_list = self.target_list.get(platform)
        if not fw_list:
            fw_list = self._load_fw_list(platform)
            self.target_list[platform] = fw_list
        return fw_list

    def get_firmware_info(self, platform: Platform) -> FirmwareInfo:
        fw_version = self.fw_version
        fw_list = self.get_fw_list(platform)
        fw_info = next(
            (x for x in fw_list if not fw_version or x.version == fw_version), None
        )
        if not fw_info:
            msg = f"firmware '{fw_version}'" if fw_version else 'any firmware'
            LOG.error(
                f"Cannot get {msg} for platform '{platform.triple.target_full_name}'!"
            )
            raise UpgradeStop
        return fw_info

    def get_referral_url(self, filename=None):
        url_list = [self.referral_url, self.refid]
        if filename:
            url_list.append(filename)
        return '/'.join(url_list)

    def _load_referral_list(self):
        referral_index_url = self.get_referral_url(self.REFERRAL_INDEX)
        referral_list = {}
        with urlopen(
            Request(referral_index_url, headers={'User-Agent': 'Mozilla/5.0'})
        ) as response:
            LOG.info("Preparing referral program with id '{}'...".format(self.refid))
            lines = (line.decode() for line in response)
            referral_url = self.get_referral_url()
            for package in Packages(None, lines):
                prev_package = referral_list.get(package.name)
                if not prev_package or prev_package.version < package.version:
                    referral_list[package.name] = PackageInfo(
                        self._web_cache,
                        referral_url,
                        package.version,
                        package.name,
                        package.filename,
                    )
        return list(referral_list.values())

    def init_referral(self):
        if not self.refid:
            return

        self.referral_list = self._load_referral_list()


def cleanup_system(ssh, platform):
    LOG.info('Cleaning remote system...')
    ssh.run('umount', TARGET_DIR)
    platform.cleanup_system(ssh)


def check_stage3_path(path):
    if not os.path.isdir(path):
        LOG.error(
            "Post-upgrade path '{}' is missing or is not a directory!".format(path)
        )
        raise UpgradeStop
    if not os.path.isfile(os.path.join(path, STAGE3_SCRIPT)):
        LOG.error("Script '{}' is missing in '{}'!".format(STAGE3_SCRIPT, path))
        raise UpgradeStop


def try_unlock(host: str, username, web_password) -> bool:
    try:
        params = {}
        if web_password:
            params['password'] = web_password
        if web_password:
            params['username'] = username
        LOG.info('Host {} : try unlock'.format(host))
        unlock(host, **params)
    except UnlockStop:
        return False
    else:
        return True


def get_firmware(fw_cache: FirmwareCache, platform: Platform) -> FirmwareInfo:
    firmware = fw_cache.get_firmware_info(platform)
    firmware_plus = '+' if firmware.is_plus() else ''

    LOG.info(
        'Preparing Braiins OS{} {} ({})...'.format(
            firmware_plus, firmware.version, firmware.name
        )
    )

    return firmware


def main(parser, args):
    stage3_user_path = args.post_upgrade
    stage3_builtin_path = None

    if stage3_user_path:
        check_stage3_path(stage3_user_path)
    if os.path.isdir(STAGE3_BUILTIN_SRC_DIR):
        check_stage3_path(STAGE3_BUILTIN_SRC_DIR)
        stage3_builtin_path = STAGE3_BUILTIN_SRC_DIR

    if args.feeds_url:
        feeds_url = args.feeds_url
    elif args.open_source:
        feeds_url = BOS_URl
    elif args.nightly:
        feeds_url = BOS_PLUS_NIGHTLY_URl
    elif args.default:
        feeds_url = BOS_PLUS_URl
    else:
        feeds_url = BOS_PLUS_URl

    referral_url = BOS_REFERRAL_URL

    hosts = read_hosts(args.hosts)
    batch = len(hosts) > 1

    error = None
    with WebCacheContext() as web_cache:
        fw_cache = FirmwareCache(
            web_cache, feeds_url, referral_url, args.fw_version, get_refid()
        )
        fw_cache.init_referral()
        for host in hosts:
            # user is not handled at all since we need root
            # ssh wrapper may ask for password based on it's own logic, we just provide default
            miner_type, _ = miner_recognition(host)

            if miner_type == MinerType.AntMiner:
                user = STOCK_USERNAME_ANTMINER
                password_ssh = args.password or STOCK_PASSWORD_ANTMINER_SSH
                password_web = args.password or STOCK_PASSWORD_ANTMINER_WEB
            elif miner_type == MinerType.WhatsMiner:
                module = load_module_from_home_path(
                    'whatsminer', WHATSMINER_EXTENSION_PATH
                )
                user = module.STOCK_USERNAME_WHATSMINER
                password_ssh = args.password or module.STOCK_PASSWORD_WHATSMINER
                password_web = password_ssh
            else:
                LOG.warning('Unsupported miner')
                continue

            curr_error = None
            tried_unlock = False
            while True:
                try:
                    install(
                        args,
                        fw_cache,
                        host,
                        batch,
                        user,
                        password_ssh,
                        stage3_user_path,
                        stage3_builtin_path,
                        tried_unlock,
                    )
                except (SSHError, Locked) as ex:
                    LOG.error('Host {} : {}'.format(host, ex))
                    if tried_unlock:
                        curr_error = UpgradeStop
                    elif try_unlock(host, user, password_web):
                        # miner has been successfully unlocked and set default Antminer password
                        # try again ssh connection with default Antminer password...
                        if miner_type == MinerType.AntMiner:
                            password_ssh = STOCK_PASSWORD_ANTMINER_SSH
                        tried_unlock = True
                        continue
                except (UpgradeStop, PlatformStop) as ex:
                    LOG.error('Host {} : {}'.format(host, ex))
                    curr_error = ex
                break
            if curr_error and not error:
                # store first error
                error = curr_error
    if error:
        raise error


def install(
    args,
    fw_cache: FirmwareCache,
    host,
    batch,
    username,
    password,
    stage3_user_path,
    stage3_builtin_path,
    tried_unlock,
):
    with SSHManager(
        host, username, password, load_host_keys=False, get_pass=False
    ) as ssh:
        platform = get_platform_from_ssh(ssh)
        if not platform:
            if not tried_unlock:
                raise Locked
            LOG.error('Braiins OS is being installed on unsupported platform!')
            raise UpgradeStop

        if platform.type == MinerType.WhatsMiner:
            install_whatsminer(args, fw_cache, host, platform, ssh)
        else:
            install_antminer(
                args,
                fw_cache,
                host,
                platform,
                ssh,
                batch,
                stage3_user_path,
                stage3_builtin_path,
            )


def install_whatsminer(
    args, fw_cache: FirmwareCache, host, platform: Platform, ssh: SSHManager
):
    # check compatibility of remote system
    if platform.os_info.type == OsType.BOS:
        # the system already runs BOS so call update instead
        LOG.warning(
            "Remote target is already running Braiins OS '{}'".format(
                platform.os_info.version
            )
        )
        return

    # check compatibility of remote system
    variant = platform.triple.variant
    if variant not in [Wm1Variant.M20S_V10, Wm1Variant.M21S_V20]:
        # the system already runs BOS so call update instead
        LOG.error(
            f"'Braiins OS is being installed on unsupported WhatsMiner type '{variant}'!"
        )
        return

    # check if miner is unlocked
    try:
        ssh.run('test', '$(id -u)', '==', '0')
    except CalledProcessError:
        raise Locked

    firmware = get_firmware(fw_cache, platform)
    firmware_path = firmware.download()

    # prepare target directory
    ssh.run('rm', '-fr', TARGET_DIR)
    ssh.run('mkdir', '-p', TARGET_DIR)

    try:
        LOG.info('Uploading firmware...')
        target_firmware_path = f'{TARGET_DIR}/{firmware.cache_name}'
        ssh.put(firmware_path, target_firmware_path)
        # generate hw id
        hw_id = hwid.generate()

        # get other stage1 parameters
        if args.psu_power_limit == 0:
            # 0 is special parameter for disabling autotuning
            psu_power_limit = ''
        else:
            psu_power_limit = args.psu_power_limit or 'default'

        pool_user = args.pool_user or ''
        keep_pools = 'no' if args.no_keep_pools else 'yes'

        auto_upgrade = 'no' if args.no_auto_upgrade else 'yes'

        LOG.info(f'Extracting... {firmware.cache_name}')
        ssh.run('tar', '-xzf', target_firmware_path, '-C', TARGET_DIR)
        ssh.run('rm', target_firmware_path)

        extracted_dir = f'{TARGET_DIR}/{firmware.name}'

        LOG.info('Run install...')
        start_script_path = extracted_dir + '/install.sh'
        (stdout, stderr) = ssh.run(
            '/bin/sh',
            start_script_path,
            f"'{hw_id}' '{pool_user}' '{psu_power_limit}' '{keep_pools}' '{auto_upgrade}'",
        )
        for line in stdout.readlines():
            LOG.info(line)
        ssh.run('rm', '-fr', TARGET_DIR)

        LOG.info('Upgrade was successful!')
        print('Rebooting...', end='')
        try:
            ssh.run('/sbin/reboot')
        except subprocess.CalledProcessError:
            # reboot may return exit status -1
            pass
    except CalledProcessError as e:
        for line in e.stderr.readlines():
            LOG.error(line)
    finally:
        pass


def install_antminer(
    args,
    fw_cache: FirmwareCache,
    host,
    platform: Platform,
    ssh: SSHManager,
    batch,
    stage3_user_path,
    stage3_builtin_path,
):
    firmware = get_firmware(fw_cache, platform)

    firmware_dir = firmware.extract_to_tmp_dir()

    downloaded_firmware = firmware.download()

    # check compatibility of remote system
    is_bos = platform.os_info.type == OsType.BOS
    if is_bos:
        # the system already runs BOS so call update instead
        if platform.os_info.version in firmware.name:
            LOG.info(
                f"Remote target is already running latest Braiins OS '{platform.os_info.version}'!"
            )
            LOG.info('Skipping installation')
            return

        LOG.info(
            f"Remote target is already running Braiins OS '{platform.os_info.version}'"
        )

    LOG.info(f'Installing system to {host}...')

    if args.backup and not is_bos:
        mac = platform.probe.get_mac()
        backup_dir = backup.get_output_dir(mac)
        if not platform.backup_firmware(args, ssh, backup_dir, mac):
            raise UpgradeStop

        if not is_bos:
            # upgrade remote system with missing utilities
            LOG.info('Preparing remote system...')
            ssh.run('mount', '-t', 'tmpfs', 'tmpfs', TARGET_DIR)
            platform.prepare_system(ssh, os.path.join(firmware_dir, SYSTEM_DIR))

    # prepare target directory
    ssh.run('rm', '-fr', TARGET_DIR)
    ssh.run('mkdir', '-p', TARGET_DIR)

    if not is_bos:
        # upgrade remote system with missing utilities
        LOG.info('Preparing remote system...')
        platform.prepare_system(ssh, os.path.join(firmware_dir, SYSTEM_DIR))

    LOG.info('Uploading firmware...')

    if is_bos:
        target_firmware_path = f'{TARGET_DIR}/{SYSUPGRADE_FILE}'
        ssh.put(downloaded_firmware, target_firmware_path)

        LOG.info('Running standard system update...')
        try:
            LOG.info('Upgrading firmware...')
            stdout, _ = ssh.run('sysupgrade', f'{TARGET_DIR}/{SYSUPGRADE_FILE}')
        except subprocess.CalledProcessError as error:
            if error.returncode is not None:
                checking_compatibility = False
                saving_config_files = False
                for line in error.stdout.readlines():
                    if 'Checking compatibility...' in line:
                        checking_compatibility = True
                    if 'Saving config files...' in line:
                        saving_config_files = True
                if (
                    not checking_compatibility
                    or not saving_config_files
                    or error.returncode != -1
                ):
                    LOG.error('Error log:')
                    for line in error.stderr.readlines():
                        print(line, end='')
                    raise UpgradeStop
        LOG.info('Update finished successfully!')
        return

    # copy firmware files to the server over SFTP
    sftp = ssh.open_sftp()
    sftp.chdir(TARGET_DIR)

    upload_local_files(sftp, os.path.join(firmware_dir, SOURCE_DIR), '')
    if (
        stage3_user_path
        or stage3_builtin_path
        or fw_cache.refid
        or fw_cache.referral_list
    ):
        LOG.info('Uploading post-upgrade (stage3)...')
        with tempfile.TemporaryDirectory() as stage3_dir:
            stage3_file = os.path.join(stage3_dir, STAGE3_FILE)
            with tarfile.open(stage3_file, 'w:gz') as stage3:
                if stage3_user_path:
                    stage3.add(stage3_user_path, STAGE3_USER_DIR)
                if stage3_builtin_path:
                    stage3.add(stage3_builtin_path, STAGE3_BUILTIN_DIR)
                stage3.add(
                    os.path.join(PAYLOAD_DIR, STAGE3_SCRIPT),
                    arcname='/'.join([STAGE3_DIR, STAGE3_SCRIPT]),
                )
                if fw_cache.refid:
                    bos_refid_path = os.path.join(stage3_dir, STAGE3_REFID)
                    with open(bos_refid_path, 'w') as file:
                        file.write('{}\n'.format(fw_cache.refid))
                    stage3.add(
                        bos_refid_path, arcname='/'.join([STAGE3_DIR, STAGE3_REFID])
                    )
                for referral in fw_cache.referral_list:
                    referral_ipk_path = referral.download()
                    stage3.add(
                        referral_ipk_path,
                        arcname='/'.join(
                            [STAGE3_DIR, STAGE3_REFERRAL, referral.filename]
                        ),
                    )
                stage3.close()
                with Progress(STAGE3_FILE, os.path.getsize(stage3_file)) as progress:
                    sftp.put(stage3_file, STAGE3_FILE, callback=progress)
    sftp.close()

    # generate HW identifier for miner
    hw_id = hwid.generate()

    # get other stage1 parameters
    if args.psu_power_limit == 0:
        # 0 is special parameter for disabling autotuning
        psu_power_limit = ''
    else:
        psu_power_limit = args.psu_power_limit or 'default'

    if args.keep_hostname:
        keep_hostname = 'yes'
    elif args.no_keep_hostname:
        keep_hostname = 'no'
    else:
        # keep only user defined hostname and skip factory one (default behaviour)
        keep_hostname = 'cond'

    pool_user = args.pool_user or ''
    keep_network = 'no' if args.no_keep_network else 'yes'
    keep_pools = 'no' if args.no_keep_pools else 'yes'
    auto_upgrade = 'no' if args.no_auto_upgrade else 'yes'
    dry_run = 'yes' if args.dry_run else 'no'

    # run stage1 upgrade process
    try:
        LOG.info('Upgrading firmware...')
        stdout, _ = ssh.run(
            'cd',
            TARGET_DIR,
            '&&',
            'ls',
            '-l',
            '&&',
            "/bin/sh stage1.sh '{}' '{}' '{}' '{}' '{}' '{}' '{}' '{}'".format(
                hw_id,
                pool_user,
                psu_power_limit,
                keep_network,
                keep_hostname,
                keep_pools,
                auto_upgrade,
                dry_run,
            ),
        )
    except subprocess.CalledProcessError as error:
        cleanup_system(ssh, platform)
        LOG.error('Error log:')
        for line in error.stderr.readlines():
            print(line, end='')
        raise UpgradeStop
    else:
        if args.dry_run:
            cleanup_system(ssh, platform)
            LOG.info('Dry run of upgrade was successful!')
            return

        for line in stdout.readlines():
            print(line, end='')
        LOG.info('Upgrade was successful!')
        print('Rebooting...', end='')
        try:
            ssh.run('/sbin/reboot')
        except subprocess.CalledProcessError:
            # reboot returns exit status -1
            pass

    # do not wait when install is called in batch mode
    if not batch:
        if args.no_wait:
            print()
            LOG.info(
                'Wait for 120 seconds before the system becomes fully operational!'
            )
        else:
            wait_for_port(host, 80, REBOOT_DELAY)


def build_arg_parser(parser, desc):
    label = desc['label']
    hlp = desc['help']

    parser.description = desc['description']
    parser.add_argument(
        'hosts',
        nargs='?',
        metavar=label['hosts'],
        help=hlp['hosts'],
        widget='FileChooser',
    )
    parser.add_argument(
        '-p',
        '--password',
        metavar=label['password'],
        help=hlp['password'],
        widget='PasswordField',
    )
    parser.add_argument(
        '--psu-power-limit',
        nargs='?',
        type=int,
        metavar=label['psu-power-limit'],
        help=hlp['psu-power-limit'],
    )
    parser.add_argument(
        '--pool-user', nargs='?', metavar=label['pool-user'], help=hlp['pool-user']
    )
    parser_feeds = parser.add_mutually_exclusive_group(
        gooey_options={'initial_selection': 0}
    )
    parser_feeds.add_argument(
        '--default', action='store_true', metavar=label['default'], help=hlp['default']
    )
    parser_feeds.add_argument(
        '--open-source',
        action='store_true',
        metavar=label['open-source'],
        help=hlp['open-source'],
    )
    parser_feeds.add_argument(
        '--nightly', action='store_true', metavar=label['nightly'], help=hlp['nightly']
    )
    parser_feeds.add_argument(
        '--feeds-url', nargs='?', metavar=label['feeds-url'], help=hlp['feeds-url']
    )
    parser.add_argument(
        '--fw-version', nargs='?', metavar=label['fw-version'], help=hlp['fw-version']
    )
    parser.add_argument(
        '--no-auto-upgrade',
        action='store_true',
        metavar=label['no-auto-upgrade'],
        help=hlp['no-auto-upgrade'],
    )
    parser.add_argument(
        '--no-keep-pools',
        action='store_true',
        metavar=label['no-keep-pools'],
        help=hlp['no-keep-pools'],
    )
    parser_network = parser.add_mutually_exclusive_group(
        gooey_options={'initial_selection': 0}
    )
    parser_network.add_argument(
        '--keep-network',
        action='store_true',
        metavar=label['keep-network'],
        help=hlp['keep-network'],
    )
    parser_network.add_argument(
        '--no-keep-network',
        action='store_true',
        metavar=label['no-keep-network'],
        help=hlp['no-keep-network'],
    )
    parser_hostname = parser.add_mutually_exclusive_group(
        gooey_options={'initial_selection': 0}
    )
    parser_hostname.add_argument(
        '--no-keep-hostname',
        action='store_true',
        metavar=label['no-keep-hostname'],
        help=hlp['no-keep-hostname'],
    )
    parser_hostname.add_argument(
        '--keep-hostname',
        action='store_true',
        metavar=label['keep-hostname'],
        help=hlp['keep-hostname'],
    )
    parser.add_argument(
        '--post-upgrade',
        nargs='?',
        metavar=label['post-upgrade'],
        help=hlp['post-upgrade'],
        widget='FileChooser',
    )

    parser.add_argument(
        '--no-wait',
        action='store_true',
        metavar=label['no-wait'],
        help=hlp['no-wait'],
        gooey_options={'visible': False},
    )
    parser.add_argument(
        '--dry-run',
        action='store_true',
        metavar=label['dry-run'],
        help=hlp['dry-run'],
        gooey_options={'visible': False},
    )
    parser.add_argument(
        '--backup',
        action='store_true',
        metavar=label['backup'],
        help=hlp['backup'],
        gooey_options={'visible': False},
    )
    parser.add_argument(
        '--no-nand-backup',
        action='store_true',
        metavar=label['no-nand-backup'],
        help=hlp['no-nand-backup'],
        gooey_options={'visible': False},
    )


if __name__ == '__main__':
    # execute only if run as a script
    parser = argparse.ArgumentParser()
    build_arg_parser(parser)
    # parse command line arguments
    args = parser.parse_args(sys.argv[1:])

    try:
        main(parser, args)
    except KeyboardInterrupt:
        print()
        sys.exit(1)
    except SSHError as e:
        LOG.error(str(e))
        sys.exit(1)
    except UpgradeStop:
        sys.exit(2)
    except PlatformStop:
        sys.exit(3)
