Esempio n. 1
0
def save_recent_session(session_key, argv):
    """
    Saves session arguments into recents file.

    :param session_key: key to save under (only one session per key is saved)
    :param argv:        argument list to save
    :return:            None
    """
    # add current line to history, if not already there
    cmdline = " ".join(
        [x if x and not ' ' in x else "'{}'".format(x) for x in argv])
    if not _last_input:
        if cmdline != readline.get_history_item(
                readline.get_current_history_length()):
            readline.add_history(cmdline)

    make_radiopadre_dir()
    try:
        readline.write_history_file(HISTORY_FILE)
    except IOError:
        traceback.print_exc()
        warning("Error writing history file (see above). Proceeding anyway.")

    readline.clear_history()

    # reform command-line without persisting options
    cmdline = " ".join([
        x if x and not ' ' in x else "'{}'".format(x) for x in argv
        if x not in config.NON_PERSISTING_OPTIONS
    ])

    recents = _load_recent_sessions(False) or OrderedDict()
    session_key = ":".join(map(str, session_key))
    if session_key in recents:
        del recents[session_key]
    if len(recents) >= 5:
        del recents[list(recents.keys())[0]]
    recents[session_key] = cmdline

    make_radiopadre_dir()
    with open(RECENTS_FILE, 'wt') as rf:
        for key, cmdline in recents.items():
            rf.write("{}:::{}\n".format(key, cmdline))

    global _recent_sessions
    _recent_sessions = recents
Esempio n. 2
0
def _get_config_value(section, key):
    globalval = globals().get(key.upper())
    value = section[key]
    if globalval is None or isinstance(globalval, six.string_types):
        return value
    elif type(globalval) is bool:
        if value.lower() in {'no', '0', 'false'}:
            return False
        elif value.lower() in {'yes', '1', 'true'}:
            return True
        else:
            warning("unrecognized setting {} = {} in config, assuming False".
                    format(key, value))
            return False
    elif type(globalval) is int:
        return int(value)
    else:
        raise TypeError("unsupported type {} for option {}".format(
            type(globalval), key))
Esempio n. 3
0
def update_installation(enable_pull=False):
    global docker_image
    enable_pull = enable_pull or config.AUTO_INIT or config.UPDATE
    if config.CONTAINER_DEV:
        update_server_from_repository()
    docker_image = config.DOCKER_IMAGE
    if check_output(ff("docker image inspect {docker_image}")) is None:
        if not enable_pull:
            bye(
                ff("  Radiopadre docker image {docker_image} not found. Re-run with --update or --auto-init perhaps?"
                   ))
        message(
            ff("  Radiopadre docker image {docker_image} not found locally"))
    else:
        message(ff("  Using radiopadre docker image {docker_image}"))
    if enable_pull:
        warning(ff("Calling docker pull {docker_image}"))
        warning(
            "  (This may take a few minutes if the image is not up to date...)"
        )
        try:
            subprocess.call([docker, "pull", docker_image])
        except subprocess.CalledProcessError as exc:
            if config.IGNORE_UPDATE_ERRORS:
                warning(
                    "docker pull failed, but --ignore-update-errors is set, proceeding anyway"
                )
                return
            raise exc
Esempio n. 4
0
def init():
    """Initializes radiopadre kernel"""
    iglesia.init()

    global FILE_URL_ROOT, NOTEBOOK_URL_ROOT, CACHE_URL_BASE, CACHE_URL_ROOT, \
        SHADOW_URL_PREFIX
    global \
        ABSROOTDIR, ROOTDIR, DISPLAY_ROOTDIR, SHADOW_HOME, SERVER_BASEDIR, SHADOW_BASEDIR, \
        SHADOW_ROOTDIR, SESSION_DIR, SESSION_URL, SESSION_ID, \
        VERBOSE, HOSTNAME, SNOOP_MODE

    from iglesia import \
        ABSROOTDIR, ROOTDIR, DISPLAY_ROOTDIR, SHADOW_HOME, SERVER_BASEDIR, SHADOW_BASEDIR, \
        SHADOW_ROOTDIR, SESSION_DIR, SESSION_URL, SESSION_ID, \
        VERBOSE, HOSTNAME, SNOOP_MODE

    # setup for snoop mode. Browsing /home/other/path/to,
    if SNOOP_MODE:
        # for a Jupyter basedir of ~/.radiopadre/home/other/path, this becomes /home/other/path
        unshadowed_server_base = SERVER_BASEDIR[len(SHADOW_HOME):]
        # Otherwise it'd better have been /home/other/path/to to begin with!
        if not _is_subdir(ABSROOTDIR, unshadowed_server_base):
            error(
                f"""The requested directory {ABSROOTDIR} is not under {unshadowed_server_base}.
                This is probably a bug! """)
        # Since Jupyter is running under ~/.radiopadre/home/other/path, we can serve other's files from
        # /home/other/path/to as /files/to/.content
        subdir = SHADOW_ROOTDIR[
            len(SERVER_BASEDIR
                ):]  # this becomes "/to" (or "" if paths are the same)
        # but do make sure that the .content symlink is in place!
        _make_symlink(ABSROOTDIR, SHADOW_ROOTDIR + "/.radiopadre.content")
    # else running in native mode
    else:
        if not _is_subdir(ABSROOTDIR, SERVER_BASEDIR):
            warning(
                f"""The requested directory {ABSROOTDIR} is not under {SERVER_BASEDIR}.
                This is probably a bug! """)
        # for a server dir of /home/user/path, and an ABSROOTDIR of /home/oms/path/to, get the subdir
        subdir = ABSROOTDIR[
            len(SERVER_BASEDIR
                ):]  # this becomes "/to" (or "" if paths are the same)

    os.chdir(ABSROOTDIR)
    ROOTDIR = '.'

    ## check casacore availability
    global casacore_tables
    try:
        import casacore.tables as casacore_tables
    except Exception as exc:
        casacore_tables = None
        warning(
            "casacore.tables failed to import. Table browsing functionality will not be available."
        )

    radiopadre_base = os.path.dirname(os.path.dirname(__file__))

    # # pre-init JS9 stuff and run JS9 helper
    # js9.preinit_js9(in_container, helper_port, userside_helper_port, http_rewrites)

    iglesia.init_helpers(radiopadre_base)

    # now a port is available (set up in init_helpers()), form up URLs

    SHADOW_URL_PREFIX = f"http://localhost:{iglesia.HTTPSERVER_PORT}/{SESSION_ID}"
    CACHE_URL_ROOT = SHADOW_URL_PREFIX + ABSROOTDIR
    CACHE_URL_BASE = CACHE_URL_ROOT[:-len(subdir
                                          )] if subdir else CACHE_URL_ROOT

    # when running nbconvert, it doesn't know about the magic "/files" URL, and just needs a local filename
    global NBCONVERT
    NBCONVERT = bool(os.environ.get("RADIOPADRE_NBCONVERT"))
    files_prefix = "." if NBCONVERT else "/files"

    if SNOOP_MODE:
        FILE_URL_ROOT = f"{files_prefix}{subdir}/.radiopadre.content/"
        NOTEBOOK_URL_ROOT = f"/notebooks{subdir}/.radiopadre.content/"
    else:
        FILE_URL_ROOT = f"{files_prefix}{subdir}/"
        NOTEBOOK_URL_ROOT = f"/notebooks{subdir}/"

    # init JS9 sources
    from . import js9
    js9.preinit_js9()
Esempio n. 5
0
def run_radiopadre_server(command, arguments, notebook_path, workdir=None):
    global backend

    # message("Welcome to Radiopadre!")
    USE_VENV = USE_DOCKER = USE_SINGULARITY = False

    for backend in config.BACKEND:
        if backend == "venv" and find_which("virtualenv"):
            USE_VENV = True
            import radiopadre_client.backends.venv
            backend = radiopadre_client.backends.venv
            backend.init()
            break
        elif backend == "docker":
            has_docker = find_which("docker")
            if has_docker:
                USE_DOCKER = True
                message(ff("Using {has_docker} for container mode"))
                import radiopadre_client.backends.docker
                backend = radiopadre_client.backends.docker
                backend.init(binary=has_docker)
                break
        elif backend == "singularity":
            has_docker = find_which("docker")
            has_singularity = find_which("singularity")
            if has_singularity:
                USE_SINGULARITY = True
                message(ff("Using {has_singularity} for container mode"))
                import radiopadre_client.backends.singularity
                backend = radiopadre_client.backends.singularity
                backend.init(binary=has_singularity, docker_binary=has_docker)
                break
        message(ff("The '{backend}' back-end is not available."))
    else:
        bye(ff("None of the specified back-ends are available."))

    # if not None, gives the six port assignments
    attaching_to_ports = container_name = None

    # ### ps/ls command
    if command == 'ps' or command == 'ls':
        session_dict = backend.list_sessions()
        num = len(session_dict)
        message("{} session{} running".format(num, "s" if num != 1 else ""))
        for i, (id, (name, path, uptime, session_id,
                     ports)) in enumerate(session_dict.items()):
            print("{i}: id {id}, name {name}, in {path}, up since {uptime}".
                  format(**locals()))
        sys.exit(0)

    # ### kill command
    if command == 'kill':
        session_dict = backend.list_sessions()
        if not session_dict:
            bye("no sessions running, nothing to kill")
        if arguments[0] == "all":
            kill_sessions = session_dict.keys()
        else:
            kill_sessions = [
                backend.identify_session(session_dict, arg)
                for arg in arguments
            ]
        backend.kill_sessions(session_dict, kill_sessions)
        sys.exit(0)

    ## attach command
    if command == "resume":
        session_dict = backend.list_sessions()
        if arguments:
            id_ = backend.identify_session(session_dict, arguments[0])
        else:
            if not session_dict:
                bye("no sessions running, nothing to attach to")
            config.SESSION_ID = session_dict.keys()[0]
        container_name, path, _, _, attaching_to_ports = session_dict[id_]
        message(
            ff("  Attaching to existing session {config.SESSION_ID} running in {path}"
               ))

    # load command
    elif command == 'load':
        attaching_to_ports = None

    # else unknown command
    else:
        bye("unknown command {}".format(command))

    running_session_dict = None

    # ### SETUP LOCAL SESSION PROPERTIES: container_name, session_id, port assignments

    # REATTACH MODE: everything is read from the session file
    if attaching_to_ports:
        # session_id and container_name already set above. Ports read from session file and printed to the console
        # for the benefit of the remote end (if any)
        jupyter_port, helper_port, http_port, carta_port, carta_ws_port = selected_ports = attaching_to_ports[:
                                                                                                              5]
        userside_ports = attaching_to_ports[5:]
    # INSIDE CONTAINER: internal ports are fixed, userside ports are passed in, name is passed in, session ID is read from file
    elif config.INSIDE_CONTAINER_PORTS:
        message("started the radiopadre container")
        container_name = os.environ['RADIOPADRE_CONTAINER_NAME']
        config.SESSION_ID = os.environ['RADIOPADRE_SESSION_ID']
        selected_ports = config.INSIDE_CONTAINER_PORTS[:5]
        userside_ports = config.INSIDE_CONTAINER_PORTS[5:]
        message("  Inside container, using ports {}".format(" ".join(
            map(str, config.INSIDE_CONTAINER_PORTS))))
    # NORMAL MODE: find unused internal ports. Userside ports are passed from remote if in remote mode, or same in local mode
    else:
        if not USE_VENV:
            container_name = "radiopadre-{}-{}".format(config.USER,
                                                       uuid.uuid4().hex)
            message(ff("Starting new session in container {container_name}"))
            # get dict of running sessions (for GRIM_REAPER later)
            running_session_dict = backend.list_sessions()
        else:
            container_name = None
            message("Starting new session in virtual environment")
        selected_ports = [find_unused_port(1024)]
        for i in range(4):
            selected_ports.append(find_unused_port(selected_ports[-1] + 1))

        if config.REMOTE_MODE_PORTS:
            userside_ports = config.REMOTE_MODE_PORTS
        else:
            userside_ports = selected_ports

        os.environ['RADIOPADRE_SESSION_ID'] = config.SESSION_ID = uuid.uuid4(
        ).hex

        # write out session file
        if container_name:
            backend.save_session_info(container_name, selected_ports,
                                      userside_ports)

    global userside_jupyter_port  # needed for it to be visible to ff() from a list comprehension
    jupyter_port, helper_port, http_port, carta_port, carta_ws_port = selected_ports
    userside_jupyter_port, userside_helper_port, userside_http_port, userside_carta_port, userside_carta_ws_port = userside_ports

    # print port assignments to console -- in remote mode, remote script will parse this out
    if not config.INSIDE_CONTAINER_PORTS:
        message("  Selected ports: {}".format(":".join(
            map(str, selected_ports + userside_ports))))
        message(ff("  Session ID/notebook token is '{config.SESSION_ID}'"))
        if container_name is not None:
            message(ff("  Container name: {container_name}"))

    # ### will we be starting a browser?

    browser = False
    if config.INSIDE_CONTAINER_PORTS:
        if config.VERBOSE:
            message(
                "  Running inside container -- not opening a browser in here.")
    elif config.REMOTE_MODE_PORTS:
        if config.VERBOSE:
            message("  Remote mode -- not opening a browser locally.")
    elif os.environ.get("SSH_CLIENT"):
        message("You appear to have logged in via ssh.")
        message(
            "You're logged in via ssh, so I'm not opening a web browser for you."
        )
        message(
            "Please manually browse to the URL printed by Jupyter below. You will probably want to employ ssh"
        )
        message(
            "port forwarding if you want to browse this notebook from your own machine."
        )
        browser = False
    else:
        message("You appear to have a local session.")
        if not config.BROWSER:
            message("--no-browser is set, we will not invoke a browser.")
            message("Please manually browse to the URL printed below.")
            browser = False
        else:
            message(
                ff("We'll attempt to open a web browser (using '{config.BROWSER}') as needed. Use --no-browser to disable this."
                   ))
            browser = True

    # ### ATTACHING TO EXISTING SESSION: complete the attachment and exit

    if attaching_to_ports:
        url = ff(
            "http://localhost:{userside_jupyter_port}/tree#running?token={session_id}"
        )
        # in local mode, see if we need to open a browser. Else just print the URL -- remote script will pick it up
        if not config.REMOTE_MODE_PORTS and browser:
            message(ff("driving browser: {config.BROWSER} {url}"))
            subprocess.call([config.BROWSER, url], stdout=DEVNULL)
            time.sleep(1)
        else:
            message(ff("Browse to URL: {url}"), color="GREEN")
        # emit message so remote initiates browsing
        if config.REMOTE_MODE_PORTS:
            message(
                "The Jupyter Notebook is running inside the reattached session, presumably"
            )
            if config.VERBOSE:
                message("sleeping")
            while True:
                time.sleep(1000000)
        sys.exit(0)

    # ### NEW SESSION: from this point on, we're opening a new session

    # ### setup working directory and notebook paths
    global LOAD_DIR
    global LOAD_NOTEBOOK

    # if explicit notebook directory is given, change into it before doing anything else
    if notebook_path:
        if os.path.isdir(notebook_path):
            os.chdir(notebook_path)
            notebook_path = '.'
            LOAD_DIR = True
            LOAD_NOTEBOOK = None
        else:
            nbdir = os.path.dirname(notebook_path)
            if nbdir:
                if not os.path.isdir(nbdir):
                    bye("{} doesn't exist".format(nbdir))
                os.chdir(nbdir)
            notebook_path = os.path.basename(notebook_path)
            LOAD_DIR = False
            LOAD_NOTEBOOK = notebook_path
    else:
        LOAD_DIR = '.'
        LOAD_NOTEBOOK = None

    # message(ff("{LOAD_DIR} {LOAD_NOTEBOOK} {notebook_path}"))

    #
    if config.NBCONVERT and not LOAD_NOTEBOOK:
        bye("a notebook must be specified in order to use --nbconvert")

    # if using containers (and not inside a container), see if older sessions need to be reaped
    if running_session_dict and config.GRIM_REAPER:
        kill_sessions = []
        for cont, (_, path, _, sid, _) in running_session_dict.items():
            if sid != config.SESSION_ID and os.path.samefile(
                    path, os.getcwd()):
                message(ff("reaping older session {sid}"))
                kill_sessions.append(cont)

        if kill_sessions:
            backend.kill_sessions(running_session_dict,
                                  kill_sessions,
                                  ignore_fail=True)

    # virtual environment
    os.environ["RADIOPADRE_VENV"] = config.RADIOPADRE_VENV

    # init paths & environment
    iglesia.init()
    iglesia.set_userside_ports(userside_ports)

    global JUPYTER_OPTS
    if config.NBCONVERT:
        JUPYTER_OPTS = [
            "nbconvert", "--ExecutePreprocessor.timeout=600", "--no-input",
            "--to", "html_embed", "--execute"
        ]
        os.environ["RADIOPADRE_NBCONVERT"] = "True"
    else:
        JUPYTER_OPTS = [
            "notebook",
            "--ContentsManager.pre_save_hook=radiopadre_utils.notebook_utils._notebook_save_hook",
            "--ContentsManager.allow_hidden=True"
        ]
        os.environ.pop("RADIOPADRE_NBCONVERT", None)

    # update installation etc.
    backend.update_installation()

    # (when running natively (i.e. in a virtual environment), the notebook app doesn't pass the token to the browser
    # command properly... so let it pick its own token then)
    # if options.remote or options.config.INSIDE_CONTAINER_PORTS or not options.virtual_env:
    JUPYTER_OPTS += [
        ff("--NotebookApp.token='{config.SESSION_ID}'"),
        ff("--NotebookApp.custom_display_url='http://localhost:{userside_jupyter_port}'"
           )
    ]

    #=== figure out whether we initialize or load a notebook
    os.chdir(iglesia.SERVER_BASEDIR)
    if iglesia.SNOOP_MODE:
        warning(
            ff("{iglesia.ABSROOTDIR} is not writable for you, so radiopadre is operating in snoop mode."
               ))

    ALL_NOTEBOOKS = glob.glob("*.ipynb")

    if iglesia.SNOOP_MODE and not ALL_NOTEBOOKS:
        orig_notebooks = glob.glob(os.path.join(iglesia.ABSROOTDIR, "*.ipynb"))
        if orig_notebooks:
            message(
                "  No notebooks in shadow directory: will copy notebooks from target."
            )
            message("  Copying {} notebooks from {}".format(
                len(orig_notebooks), iglesia.ABSROOTDIR))
            for nb in orig_notebooks:
                shutil.copyfile(nb, './' + os.path.basename(nb))
            ALL_NOTEBOOKS = glob.glob("*.ipynb")

    message("  Available notebooks: " + " ".join(ALL_NOTEBOOKS))

    if not config.INSIDE_CONTAINER_PORTS:

        # if no notebooks in place, see if we need to create a default
        if not ALL_NOTEBOOKS:
            if config.DEFAULT_NOTEBOOK:
                message(
                    ff("  No notebooks yet: will create {config.DEFAULT_NOTEBOOK}"
                       ))
                LOAD_DIR = True
                open(config.DEFAULT_NOTEBOOK,
                     'wt').write(default_notebook_code)
                ALL_NOTEBOOKS = [config.DEFAULT_NOTEBOOK]
            else:
                message(
                    "  No notebooks and no default. Displaying directory only."
                )
                LOAD_DIR = True
                LOAD_NOTEBOOK = None

        # expand globs and apply auto-load as needed
        if LOAD_NOTEBOOK:
            LOAD_NOTEBOOK = [
                nb for nb in ALL_NOTEBOOKS
                if fnmatch.fnmatch(os.path.basename(nb), LOAD_NOTEBOOK)
            ]
        elif config.AUTO_LOAD == "1":
            LOAD_NOTEBOOK = ALL_NOTEBOOKS[0] if ALL_NOTEBOOKS else None
            message(ff("  Auto-loading {LOAD_NOTEBOOK[0]}."))
        elif config.AUTO_LOAD:
            LOAD_NOTEBOOK = [
                nb for nb in ALL_NOTEBOOKS
                if fnmatch.fnmatch(os.path.basename(nb), config.AUTO_LOAD)
            ]
            if LOAD_NOTEBOOK:
                message("  Auto-loading {}".format(" ".join(LOAD_NOTEBOOK)))
            else:
                message(
                    ff("  No notebooks matching --auto-load {config.AUTO_LOAD}"
                       ))

    urls = []
    if LOAD_DIR:
        urls.append(
            ff("http://localhost:{userside_jupyter_port}/?token={config.SESSION_ID}"
               ))
    if LOAD_NOTEBOOK:
        urls += [
            ff("http://localhost:{userside_jupyter_port}/notebooks/{nb}?token={config.SESSION_ID}"
               ) for nb in LOAD_NOTEBOOK
        ]

    if not config.NBCONVERT:
        for url in urls[::-1]:
            message(ff("Browse to URL: {url}"), color="GREEN")
        if config.CARTA_BROWSER:
            url = ff(
                "http://localhost:{iglesia.CARTA_PORT}/?socketUrl=ws://localhost:{iglesia.CARTA_WS_PORT}"
            )
            message(ff("Browse to URL: {url} (CARTA file browser)"),
                    color="GREEN")
            urls.append(url)

    # now we're ready to start the session

    backend.start_session(container_name, selected_ports, userside_ports,
                          notebook_path, browser and urls)
Esempio n. 6
0
def update_installation():
    # are we already running inside a virtualenv? Proceed directly if so
    #       (see https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv)

    # pip install command with -v repeated for each VERBOSE increment
    pip_install = "pip install " + "-v " * min(max(config.VERBOSE - 1, 0), 3)

    if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix')
                                       and sys.base_prefix != sys.prefix):
        if sys.prefix == config.RADIOPADRE_VENV:
            message(
                ff("Running inside radiopadre virtual environment {sys.prefix}"
                   ))
        else:
            message(
                ff("Running inside non-default virtual environment {sys.prefix}"
                   ))
            message(ff("Will assume radiopadre has been installed here."))
            config.RADIOPADRE_VENV = sys.prefix

        if config.VENV_REINSTALL:
            bye("Can't --venv-reinstall from inside the virtualenv itself.")

    # Otherwise check for virtualenv, nuke/remake one if needed, then activate it
    else:
        config.RADIOPADRE_VENV = os.path.expanduser(config.RADIOPADRE_VENV)
        activation_script = os.path.join(config.RADIOPADRE_VENV,
                                         "bin/activate_this.py")

        # see if a reinstall is needed
        if config.AUTO_INIT and config.VENV_REINSTALL and os.path.exists(
                config.RADIOPADRE_VENV):
            if not os.path.exists(activation_script):
                error(ff("{activation_script} does not exist. Bat country!"))
                bye(
                    ff("Refusing to touch this virtualenv. Please remove it by hand if you must."
                       ))
            cmd = ff("rm -fr {config.RADIOPADRE_VENV}")
            warning(ff("Found a virtualenv in {config.RADIOPADRE_VENV}."))
            warning("However, --venv-reinstall was specified. About to run:")
            warning("    " + cmd)
            if config.FULL_CONSENT:
                warning(
                    "--full-consent given, so not asking for confirmation.")
            else:
                warning(ff("Your informed consent is required!"))
                inp = INPUT(
                    ff("Please enter 'yes' to rm -fr {config.RADIOPADRE_VENV}: "
                       )).strip()
                if inp != "yes":
                    bye(ff("'{inp}' is not a 'yes'. Phew!"))
                message("OK, nuking it!")
            shell(cmd)

        new_venv = False
        if not os.path.exists(config.RADIOPADRE_VENV):
            if config.AUTO_INIT:
                message(ff("Creating virtualenv {config.RADIOPADRE_VENV}"))
                shell(ff("virtualenv -p python3 {config.RADIOPADRE_VENV}"))
                new_venv = True
            else:
                error(
                    ff("Radiopadre virtualenv {config.RADIOPADRE_VENV} doesn't exist."
                       ))
                bye(ff("Try re-running with --auto-init to reinstall it."))

        message(
            ff("  Activating the radiopadre virtualenv via {activation_script}"
               ))
        with open(activation_script) as f:
            code = compile(f.read(), activation_script, 'exec')
            exec(code, dict(__file__=activation_script), {})

        if new_venv:
            extras = config.VENV_EXTRAS.split(
                ",") if config.VENV_EXTRAS else []
            # add numpy explicitly to quicken up pyregion install
            extras.append("numpy")
            if extras:
                extras = " ".join(extras)
                message(ff("Installing specified extras: {extras}"))
                shell(ff("{pip_install} {extras}"))

    # now check for a radiopadre install inside the venv
    have_install = check_output("pip show radiopadre")

    if have_install:
        install_info = dict(
            [x.split(": ", 1) for x in have_install.split("\n") if ': ' in x])
        version = install_info.get("Version", "unknown")
        if config.UPDATE:
            warning(
                ff("radiopadre (version {version}) is installed, but --update specified."
                   ))
        else:
            message(ff("radiopadre (version {version}) is installed."))

    if not have_install or config.UPDATE:
        if config.SERVER_INSTALL_PATH and os.path.exists(
                config.SERVER_INSTALL_PATH):
            message(
                ff("--server-install-path {config.SERVER_INSTALL_PATH} is configured and exists."
                   ))
            update_server_from_repository()
            install = ff("-e {config.SERVER_INSTALL_PATH}")

        elif config.SERVER_INSTALL_REPO:
            if config.SERVER_INSTALL_REPO == "default":
                config.SERVER_INSTALL_REPO = config.DEFAULT_SERVER_INSTALL_REPO
            branch = config.SERVER_INSTALL_BRANCH or "master"
            if config.SERVER_INSTALL_PATH:
                message(
                    ff("--server-install-path and --server-install-repo configured, will clone and install"
                       ))
                cmd = ff(
                    "git clone -b {branch} {config.SERVER_INSTALL_REPO} {config.SERVER_INSTALL_PATH}"
                )
                message(ff("Running {cmd}"))
                shell(cmd)
                install = ff("-e {config.SERVER_INSTALL_PATH}")
            else:
                message(
                    ff("only --server-install-repo specified, will install directly from git"
                       ))
                install = ff("git+{config.SERVER_INSTALL_REPO}@{branch}")
        elif config.SERVER_INSTALL_PIP:
            message(
                ff("--server-install-pip {config.SERVER_INSTALL_PIP} is configured."
                   ))
            install = config.SERVER_INSTALL_PIP
        else:
            bye("no radiopadre installation method specified (see --server-install options)"
                )

        cmd = ff("{pip_install} -U {install}")
        message(ff("Running {cmd}"))
        shell(cmd)
Esempio n. 7
0
def run_remote_session(command, copy_initial_notebook, notebook_path,
                       extra_arguments):

    SSH_MUX_OPTS = "-o ControlPath=/tmp/ssh_mux_radiopadre_%C -o ControlMaster=auto -o ControlPersist=1h".split(
    )

    SCP_OPTS = ["scp"] + SSH_MUX_OPTS
    #    SSH_OPTS = ["ssh", "-t"] + SSH_MUX_OPTS + [config.REMOTE_HOST]
    SSH_OPTS = ["ssh"] + SSH_MUX_OPTS + [config.REMOTE_HOST]

    # See, possibly: https://stackoverflow.com/questions/44348083/how-to-send-sigint-ctrl-c-to-current-remote-process-over-ssh-without-t-optio

    # master ssh connection, to be closed when we exit
    message(
        ff("Opening ssh connection to {config.REMOTE_HOST}. You may be prompted for your password."
           ))
    debug("  {}".format(" ".join(SSH_OPTS)))
    ssh_master = subprocess.check_call(SSH_OPTS + ["exit"], stderr=DEVNULL)

    # raw_input("Continue?")

    def help_yourself(problem, suggestion=None):
        """
        Prints a "help yourself" message and exits
        """
        message("{}".format(problem))
        message(
            ff("Please ssh {config.REMOTE_HOST} and sort it out yourself, then rerun this script"
               ))
        if suggestion:
            message(ff("({suggestion})"))
        sys.exit(1)

    def ssh_remote(command, fail_retcode=None, stderr=DEVNULL):
        """Runs command on remote host. Returns its output if the exit status is 0, or None if the exit status matches fail_retcode.

        Any other non-zero exit status (or any other error) will result in an exception.
        """
        try:
            return subprocess.check_output(SSH_OPTS + [command],
                                           stderr=stderr).decode('utf-8')
        except subprocess.CalledProcessError as exc:
            if exc.returncode == fail_retcode:
                return None
            message(ff("ssh {command} failed with exit code {exc.returncode}"))
            raise

    def ssh_remote_v(command, fail_retcode=None):
        return ssh_remote(command, fail_retcode, stderr=sys.stderr)

    def ssh_remote_interactive(command, fail_retcode=None):
        """Runs command on remote host. Returns the exit status if 0, or None if the exit status matches fail_retcode.

        Any other non-zero exit status (or any other error) will result in an exception.
        """
        try:
            return subprocess.check_call(SSH_OPTS + [command])
        except subprocess.CalledProcessError as exc:
            if exc.returncode == fail_retcode:
                return None
            message(ff("ssh {command} failed with exit code {exc.returncode}"))
            raise

    def scp_to_remote(path, remote_path):
        return subprocess.check_output(
            SCP_OPTS + [path, "{}:{}".format(config.REMOTE_HOST, remote_path)])

    def check_remote_file(remote_file, test="-x"):
        """
        Checks that a remote file exists. 'test' is specified bash-style, e.g. "-x" for executable.
        Can also use -f and -f, for example.
        Returns True or False, or raises an exception on other errors.
        """
        return ssh_remote(
            "if [ {} {} ]; then exit 0; else exit 199; fi".format(
                test, remote_file),
            fail_retcode=199,
            stderr=DEVNULL) is not None

    def check_remote_command(command):
        """
        Checks that remote host has a particular command available (by running 'which' on the remote).
        Returns True or False, or raises an exception on other errors.
        """
        if config.SKIP_CHECKS:
            return command
        return (ssh_remote("which " + command, fail_retcode=1, stderr=DEVNULL)
                or "").strip()

    # --update or --auto-init disables --skip-checks
    if config.SKIP_CHECKS:
        if config.UPDATE:
            message("Note that --update implies --no-skip-checks")
            config.SKIP_CHECKS = False
        elif config.AUTO_INIT:
            message("Note that --auto-init implies --no-skip-checks")
            config.SKIP_CHECKS = False

    # propagate our config to command-line arguments
    remote_config = config.get_config_dict()
    remote_config['BROWSER'] = 'None'
    remote_config['SKIP_CHECKS'] = False
    remote_config['VENV_REINSTALL'] = False

    # Check for various remote bits
    if config.VERBOSE and not config.SKIP_CHECKS:
        message(ff("Checking installation on {config.REMOTE_HOST}."))

    has_git = check_remote_command("git")

    USE_VENV = has_singularity = has_docker = None

    for backend in config.BACKEND:
        remote_config["BACKEND"] = backend
        if backend == "venv" and check_remote_command(
                "virtualenv") and check_remote_command("pip"):
            USE_VENV = True
            break
        elif backend == "docker":
            has_docker = check_remote_command("docker")
            if has_docker:
                break
        elif backend == "singularity":
            has_singularity = check_remote_command("singularity")
            if has_singularity:
                break
        message(
            ff("The '{backend}' back-end is not available on {config.REMOTE_HOST}, skipping."
               ))
    else:
        bye(
            ff("None of the specified back-ends are available on {config.REMOTE_HOST}."
               ))

    if remote_config["BACKEND"] != "docker":
        config.CONTAINER_PERSIST = config.CONTAINER_DEBUG = False

    # which runscript to look for
    runscript0 = "run-radiopadre"

    # form up remote venv path, but do not expand ~ at this point (it may be a different username on the remote)
    env = os.environ.copy()
    env.setdefault("RADIOPADRE_DIR", config.REMOTE_RADIOPADRE_DIR
                   or "~/.radiopadre")
    config.RADIOPADRE_VENV = (config.RADIOPADRE_VENV or "").format(**env)
    # this variable used in error and info messages
    remote_venv = ff("{config.REMOTE_HOST}:{config.RADIOPADRE_VENV}")

    # pip install command with -v repeated for each VERBOSE increment
    pip_install = "pip install " + "-v " * min(max(config.VERBOSE - 1, 0), 3)

    # do we want to do an install/update -- will be forced to True (if we must install),
    # or False if we can't update
    do_update = config.UPDATE

    if config.SKIP_CHECKS:
        runscript = ff(
            "if [ -f {config.RADIOPADRE_VENV}/bin/activate ]; then " +
            "source {config.RADIOPADRE_VENV}/bin/activate; fi; run-radiopadre "
        )
        do_update = False
    else:
        runscript = None

        # (a) if --auto-init and --venv-reinstall specified, zap remote virtualenv if present
        if config.AUTO_INIT and config.VENV_REINSTALL:
            if not config.RADIOPADRE_VENV:
                bye(
                    ff("Can't do --auto-init --venv-reinstall because --radiopadre-venv is not set"
                       ))
            if "~" in config.RADIOPADRE_VENV:
                config.RADIOPADRE_VENV = ssh_remote(
                    ff("echo {config.RADIOPADRE_VENV}")).strip(
                    )  # expand "~" on remote
            if check_remote_file(ff("{config.RADIOPADRE_VENV}"), "-d"):
                if not check_remote_file(
                        ff("{config.RADIOPADRE_VENV}/bin/activate"), "-f"):
                    error(
                        ff("{remote_venv}/bin/activate} does not exist. Bat country!"
                           ))
                    bye(
                        ff("Refusing to touch this virtualenv. Please remove it by hand if you must."
                           ))
                cmd = ff("rm -fr {config.RADIOPADRE_VENV}")
                warning(ff("Found a virtualenv in {remote_venv}."))
                warning(
                    "However, --venv-reinstall was specified. About to run:")
                warning(ff("    ssh {config.REMOTE_HOST} " + cmd))
                if config.FULL_CONSENT:
                    warning(
                        "--full-consent given, so not asking for confirmation."
                    )
                else:
                    warning(ff("Your informed consent is required!"))
                    inp = INPUT(
                        ff("Please enter 'yes' to rm -fr {remote_venv}: ")
                    ).strip()
                    if inp != "yes":
                        bye(ff("'{inp}' is not a 'yes'. Phew!"))
                    message("OK, nuking it!")
                ssh_remote(cmd)
            # force update
            do_update = True

        # (b) look inside venv
        if runscript is None and config.RADIOPADRE_VENV:
            if "~" in config.RADIOPADRE_VENV:
                config.RADIOPADRE_VENV = ssh_remote(
                    ff("echo {config.RADIOPADRE_VENV}")).strip(
                    )  # expand "~" on remote
            if check_remote_file(ff("{config.RADIOPADRE_VENV}/bin/activate"),
                                 "-f"):
                if ssh_remote(ff(
                        "source {config.RADIOPADRE_VENV}/bin/activate && which {runscript0}"
                ),
                              fail_retcode=1):
                    runscript = ff(
                        "source {config.RADIOPADRE_VENV}/bin/activate && {runscript0}"
                    )
                    message(
                        ff("Using remote client script within {config.RADIOPADRE_VENV}"
                           ))
                else:
                    message(
                        ff("Remote virtualenv {config.RADIOPADRE_VENV} exists, but does not contain a radiopadre-client installation."
                           ))
            else:
                message(ff("No remote virtualenv found at {remote_venv}"))

        # (c) just try `which` directly
        if runscript is None:
            runscript = check_remote_command(runscript0)
            if runscript:
                message(ff("Using remote client script at {runscript}"))
                runscript = ff()
                do_update = False
                if config.UPDATE:
                    warning(
                        ff("ignoring --update for client since it isn't in a virtualenv"
                           ))
            else:
                message(ff("No remote client script {runscript0} found"))
                runscript = None

    ## No runscript found on remote?
    ## First, figure out whether to reinstall a virtualenv for it
    if not runscript:
        message(ff("No {runscript0} script found on {config.REMOTE_HOST}"))
        if not config.AUTO_INIT:
            bye(
                ff("Try re-running with --auto-init to install radiopadre-client on {config.REMOTE_HOST}."
                   ))
        if not config.RADIOPADRE_VENV:
            bye(ff("Can't do --auto-init because --virtual-env is not set."))

        message("Trying to --auto-init an installation for you...")

        # try to auto-init a virtual environment
        if not check_remote_file(ff("{config.RADIOPADRE_VENV}/bin/activate"),
                                 "-f"):
            message(ff("Creating virtualenv {remote_venv}"))
            ssh_remote_v(ff("virtualenv -p python3 {config.RADIOPADRE_VENV}"))
            extras = config.VENV_EXTRAS.split(
                ",") if config.VENV_EXTRAS else []
            # add numpy explicitly to quicken up pyregion install
            extras.append("numpy")
            if extras:
                extras = " ".join(extras)
                message(ff("Installing specified extras: {extras}"))
                ssh_remote_v(
                    ff("source {config.RADIOPADRE_VENV}/bin/activate && {pip_install} {extras}"
                       ))
        else:
            message(ff("Installing into existing virtualenv {remote_venv}"))

    # Now, figure out how to install or update the client package
    if not runscript or do_update:
        # installing from a specified existing path
        if config.CLIENT_INSTALL_PATH and check_remote_file(
                config.CLIENT_INSTALL_PATH, "-d"):
            install_path = config.CLIENT_INSTALL_PATH
            message(
                ff("--client-install-path {install_path} is configured and exists on {config.REMOTE_HOST}."
                   ))
            # update if managed by git
            if check_remote_file(ff("{install_path}/.git"),
                                 "-d") and config.UPDATE:
                if has_git:
                    if config.CLIENT_INSTALL_BRANCH:
                        cmd = ff(
                            "cd {install_path} && git fetch origin && git checkout {config.CLIENT_INSTALL_BRANCH} && git pull"
                        )
                    else:
                        cmd = ff("cd {install_path} && git pull")
                    warning(
                        ff("--update specified and git detected, will attempt to update via"
                           ))
                    message(ff("    {cmd}"))
                    ssh_remote_v(cmd)
                else:
                    warning(
                        ff("--update specified, but no git command found on {config.REMOTE_HOST}"
                           ))
            install_path = "-e " + install_path
        # else, installing from git
        elif config.CLIENT_INSTALL_REPO:
            if config.CLIENT_INSTALL_REPO == "default":
                config.CLIENT_INSTALL_REPO = config.DEFAULT_CLIENT_INSTALL_REPO
            branch = config.CLIENT_INSTALL_BRANCH or "master"
            if config.CLIENT_INSTALL_PATH:
                message(
                    ff("--client-install-path and --client-install-repo configured, will attempt"
                       ))
                cmd = ff(
                    "git clone -b {branch} {config.CLIENT_INSTALL_REPO} {config.CLIENT_INSTALL_PATH}"
                )
                message(ff("    ssh {config.REMOTE_HOST} {cmd}"))
                ssh_remote_v(cmd)
                install_path = ff("-e {config.CLIENT_INSTALL_PATH}")
            else:
                message(
                    ff("--client-install-repo is configured, will try to install directly from git"
                       ))
                install_path = ff("git+{config.CLIENT_INSTALL_REPO}@{branch}")

            # now pip install
            message(
                ff("Doing pip install -e {install_path} in {config.RADIOPADRE_VENV}"
                   ))
            ssh_remote_v(
                ff("source {config.RADIOPADRE_VENV}/bin/activate && {pip_install} -e {install_path}"
                   ))
        # else, installing directly from pip
        elif config.CLIENT_INSTALL_PIP:
            message(
                ff("--client-install-pip {config.CLIENT_INSTALL_PIP} is configured."
                   ))
            install_path = config.CLIENT_INSTALL_PIP
        else:
            bye("no radiopadre-client installation method specified (see --client-install options)"
                )

        # now install
        message(
            ff("Will attempt to pip install -U {install_path} in {remote_venv}"
               ))
        ssh_remote_v(
            ff("source {config.RADIOPADRE_VENV}/bin/activate && {pip_install} -U {install_path}"
               ))

        # sanity check
        if ssh_remote(ff(
                "source {config.RADIOPADRE_VENV}/bin/activate && which {runscript0}"
        ),
                      fail_retcode=1):
            runscript = ff(
                "source {config.RADIOPADRE_VENV}/bin/activate && {runscript0}")
        else:
            bye(
                ff("Something went wrong during installation on {config.REMOTE_HOST}, since I still don't see the {runscript0} script"
                   ))

        message("Success!")

    runscript = ff(
        "export RADIOPADRE_DIR={config.REMOTE_RADIOPADRE_DIR}; {runscript}")
    # copy notebook to remote
    if copy_initial_notebook:
        if not os.path.exists(copy_initial_notebook):
            bye("{} doesn't exist".format(copy_initial_notebook))
        if check_remote_file(notebook_path or ".", "-d"):
            nbpath = "{}/{}".format(notebook_path or ".",
                                    copy_initial_notebook)
            if check_remote_file(nbpath, "-ff("):
                message(
                    ff("remote notebook {nbpath} exists, will not copy over"))
            else:
                message(
                    ff("remote notebook {nbpath} doesn't exist, will copy over"
                       ))
                scp_to_remote(copy_initial_notebook, notebook_path)
            notebook_path = nbpath

    # allocate 5 suggested ports (in resume mode, this will be overridden by the session settings)
    starting_port = 10000 + os.getuid() * 3
    ports = []
    for _ in range(5):
        starting_port = find_unused_port(starting_port + 1, 10000)
        ports.append(starting_port)

    remote_config["remote"] = ":".join(map(str, ports))

    # turn the remote_config dict into a command line
    runscript += " " + " ".join(
        config.get_options_list(remote_config, quote=True))

    runscript += " '{}' {}".format(
        command if command is not "load" else notebook_path,
        " ".join(extra_arguments))

    # start ssh subprocess to launch notebook
    args = list(SSH_OPTS) + ["shopt -s huponexit && " + runscript]

    if config.VERBOSE:
        message("running {}".format(" ".join(args)))
    else:
        message(ff("running radiopadre client on {config.REMOTE_HOST}"))
    ssh = subprocess.Popen(args,
                           stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE,
                           bufsize=1,
                           universal_newlines=True)

    poller = Poller()
    poller.register_process(ssh, config.REMOTE_HOST,
                            config.REMOTE_HOST + " stderr")
    if not USE_VENV:
        poller.register_file(sys.stdin, "stdin")

    container_name = None
    urls = []
    remote_running = False
    status = 0

    try:
        while remote_running is not None and poller.fdlabels:
            fdlist = poller.poll(verbose=config.VERBOSE > 1)
            for fname, fobj in fdlist:
                try:
                    line = fobj.readline()
                except EOFError:
                    line = b''
                empty_line = not line
                line = (line.decode('utf-8')
                        if type(line) is bytes else line).rstrip()
                if fobj is sys.stdin and line == 'D' and config.CONTAINER_PERSIST:
                    sys.exit(0)
                # break out if ssh closes
                if empty_line:
                    poller.unregister_file(fobj)
                    if ssh.stdout not in poller and ssh.stderr not in poller:
                        message(
                            ff("The ssh process to {config.REMOTE_HOST} has exited"
                               ))
                        remote_running = None
                        break
                    continue
                # print remote output
                print_output = False
                if fobj is ssh.stderr:
                    print_output = not line.startswith("Shared connection to")
                else:
                    print_output = not line.startswith(
                        "radiopadre:") or command != 'load'
                if not empty_line and (config.VERBOSE or print_output):
                    for key, dispatch in _dispatch_message.items():
                        if key in line:
                            dispatch(u"{}: {}".format(fname, line))
                            break
                    else:
                        message(u"{}: {}".format(fname, line))
                if not line:
                    continue
                # if remote is not yet started, check output
                if not remote_running:
                    # check for session ID
                    match = re.match(
                        ".*Session ID/notebook token is '([0-9a-f]+)'", line)
                    if match:
                        config.SESSION_ID = match.group(1)
                        continue
                    # check for notebook port, and launch second ssh when we have it
                    re_ports = ":".join(["([\\d]+)"] *
                                        10)  # form up regex for ddd:ddd:...
                    match = re.match(ff(".*Selected ports: {re_ports}[\s]*$"),
                                     line)
                    if match:
                        ports = list(map(int, match.groups()))
                        remote_ports = ports[:5]
                        local_ports = ports[5:]
                        if config.VERBOSE:
                            message(
                                "Detected ports {}:{}:{}:{}:{} -> {}:{}:{}:{}:{}"
                                .format(*ports))
                        ssh2_args = ["ssh"] + SSH_MUX_OPTS + [
                            "-O", "forward", config.REMOTE_HOST
                        ]
                        for loc, rem in zip(local_ports, remote_ports):
                            ssh2_args += [
                                "-L",
                                "localhost:{}:localhost:{}".format(loc, rem)
                            ]
                        # tell mux process to forward the ports
                        if config.VERBOSE:
                            message(
                                "sending forward request to ssh mux process".
                                format(ssh2_args))
                        subprocess.call(ssh2_args)
                        continue

                    # check for launch URL
                    match = re.match(".*Browse to URL: ([^\s\033]+)", line)
                    if match:
                        urls.append(match.group(1))
                        continue

                    # check for container name
                    match = re.match(".*Container name: ([^\s\033]+)", line)
                    if match:
                        container_name = match.group(1)
                        continue

                    if "jupyter notebook server is running" in line:
                        remote_running = True
                        time.sleep(1)
                        iglesia.register_helpers(*run_browser(*urls))
                        message(
                            "The remote radiopadre session is now fully up")
                        if USE_VENV or not config.CONTAINER_PERSIST:
                            message("Press Ctrl+C to kill the remote session")
                        else:
                            message(
                                "Press D<Enter> to detach from remote session, or Ctrl+C to kill it"
                            )

    except SystemExit as exc:
        message(ff("SystemExit: {exc.code}"))
        status = exc.code

    except KeyboardInterrupt:
        message("Ctrl+C caught")
        status = 1

    except Exception as exc:
        traceback.print_exc()
        message("Exception caught: {}".format(str(exc)))

    if remote_running and ssh.poll() is None:
        message("Asking remote session to exit, nicely")
        try:
            try:
                ssh.stdin.write("exit\n")
            except TypeError:
                ssh.stdin.write(b"exit\n")  # because f**k you python
        except IOError:
            debug("  looks like it's already exited")

    # if status and not USE_VENV and container_name:
    #     message(ff("killing remote container {container_name}"))
    #     try:
    #         if has_docker:
    #             ssh_remote(ff("{has_docker} kill {container_name}"))
    #         elif has_singularity:
    #             from .backends.singularity import get_singularity_image
    #             singularity_image = get_singularity_image(config.DOCKER_IMAGE)
    #             ssh_remote(ff("{has_singularity} instance.stop {singularity_image} {container_name}"))
    #     except subprocess.CalledProcessError as exc:
    #         message(exc.output.decode())

    for i in range(10, 0, -1):
        if ssh.poll() is not None:
            debug("Remote session has exited")
            ssh.wait()
            break
        message(ff("Waiting for remote session to exit ({i})"))
        time.sleep(1)
    else:
        message(ff("Remote session hasn't exited, killing the ssh process"))
        ssh.kill()

    return status
Esempio n. 8
0
def check_recent_sessions(options, argv, parser=None):
    """
    Loads a recent session if requested

    :param options: Options object from ArgumentParser
    :param argv:    Argument list (from sys.argv[1:] initially)
    :param parser:  ArgumentParser object used to (re)parse the options

    :return: options, argv
                    Where options and argv may have been loaded from the recent
                    options file
    """
    make_radiopadre_dir()
    # load history
    try:
        readline.read_history_file(HISTORY_FILE)
    except IOError:
        pass

    resume_session = None
    # a single-digit argument resumes session #N
    if len(options.arguments) == 1 and re.match("^\d$", options.arguments[0]):
        resume_session = int(options.arguments[0])
    # no arguments is resume session #0
    elif not options.arguments:
        resume_session = 0

    if resume_session is not None:

        last = _load_recent_sessions()
        num_recent = len(last)
        if resume_session >= num_recent:
            bye(ff("no recent session #{resume_session}"))

        message("Your most recent radiopadre sessions are:")
        message("")
        for i, (_, opts) in enumerate(list(last.items())[::-1]):
            message("    [#{0}] {1}".format(i, opts), color="GREEN")
        message("")
        print(
            "\nInteractive startup mode. Edit arguments and press Enter to run, or Ctrl+C to bail out. "
        )
        print(
            "    (Ctrl+U + <NUM> + Enter will paste other recent session arguments from the list above)\n"
        )

        inp = None
        cmdline = ''
        readline.set_startup_hook(lambda: readline.insert_text(cmdline))

        while inp is None:
            # form up list of fake args to be re-parsed for the last session
            cmdline = list(last.items())[-(resume_session + 1)][1]
            # non-persisting options raised in command line shall be appended to the fake args
            for opt in config.NON_PERSISTING_OPTIONS:
                if opt.startswith("--") and getattr(options, opt[2:].replace(
                        "-", "_"), None):
                    cmdline += " " + opt
            cmdline += " "

            ## colors confuse Ctrl+U and such
            # prompt = ff("{logger.Colors.GREEN}[#{resume_session}]:{logger.Colors.ENDC} ")
            prompt = ff("[#{resume_session}] ")
            inp = INPUT(prompt)
            inp = inp.strip()
            if not inp:
                resume_session = 0
                inp = None
            elif re.match("^\d+$", inp):
                res = int(inp)
                if res >= num_recent:
                    warning(ff("no recent session #{res}"))
                else:
                    resume_session = res
                readline.remove_history_item(1)
                inp = None

        readline.set_startup_hook(None)

        global _last_input
        _last_input = inp

        argv = shlex.split(inp, posix=False)

        options = parser.parse_args(argv)

    return options, argv
Esempio n. 9
0
def update_installation(rebuild=False, docker_pull=True):
    global docker_image
    global singularity_image
    docker_image = config.DOCKER_IMAGE
    singularity_image = os.path.expanduser(get_singularity_image(docker_image))
    # this will be True if we need to build the image
    build_image = False

    # clearly true if no image
    if not os.path.exists(singularity_image):
        if config.SINGULARITY_AUTO_BUILD:
            message(ff("Singularity image {singularity_image} does not exist"))
            build_image = True
        else:
            error(
                ff("Singularity image {singularity_image} does not exist, and auto-build is disabled"
                   ))
            bye(ff("Re-run with --singularity-auto-build to proceed"))
    # also true if rebuild forced by flags or config or command line
    elif rebuild or config.SINGULARITY_REBUILD:
        config.SINGULARITY_AUTO_BUILD = build_image = True
        message(
            ff("--singularity-rebuild specified, removing singularity image {singularity_image}"
               ))

    # pull down docker image first
    if has_docker and docker_pull:
        message(
            "Checking docker image (from which our singularity image is built)"
        )
        docker.update_installation(enable_pull=True)
    # if we're not forced to build yet, check for an update
    if config.UPDATE and not build_image:
        if has_docker:
            # check timestamp of docker image
            docker_image_time = None
            output = check_output(
                ff("{has_docker} image inspect {docker_image} -f '{{{{ .Created }}}}'"
                   )).strip()
            message(ff("  docker image timestamp is {output}"))
            # in Python 3.7 we have datetime.fromisoformat(date_string), but for now we muddle:
            match = output and re.match("(^.*)[.](\d+)Z", output)
            if match:
                try:
                    docker_image_time = calendar.timegm(
                        time.strptime(match.group(1), "%Y-%m-%dT%H:%M:%S"))
                    docker_image_time = datetime.datetime.utcfromtimestamp(
                        docker_image_time)
                except ValueError:
                    pass
            sing_image_time = datetime.datetime.utcfromtimestamp(
                os.path.getmtime(singularity_image))
            message("  singularity image timestamp is {}".format(
                sing_image_time.isoformat()))
            if docker_image_time is None:
                warning(
                    ff("can't parse docker image timestamp '{output}', rebuilding {singularity_image} just in case"
                       ))
                build_image = True
            elif docker_image_time > sing_image_time:
                warning(
                    ff("rebuilding outdated singularity image {singularity_image}"
                       ))
                build_image = True
            else:
                message(
                    ff("singularity image {singularity_image} is up-to-date"))
        else:
            message(
                ff("--update specified but no docker access, assuming {singularity_image} is up-to-date"
                   ))
    # now build if needed
    if build_image:
        warning(
            ff("Rebuilding singularity image from docker://{docker_image}"))
        warning(ff("  (This may take a few minutes....)"))
        singularity_image_new = singularity_image + ".new.img"
        if os.path.exists(singularity_image_new):
            os.unlink(singularity_image_new)
        cmd = [
            singularity, "build", singularity_image_new,
            ff("docker://{docker_image}")
        ]
        message("running " + " ".join(cmd))
        try:
            subprocess.check_call(cmd)
        except subprocess.CalledProcessError as exc:
            if config.IGNORE_UPDATE_ERRORS:
                if os.path.exists(singularity_image):
                    warning(
                        "singularity build failed but --ignore-update-errors is set, proceeding with old image"
                    )
                else:
                    error(
                        "singularity build failed, --ignore-update-errors is set, but we have no older image"
                    )
                    raise
            else:
                raise
        # move old image
        message(ff("Build successful, renaming to {singularity_image}"))
        os.rename(singularity_image_new, singularity_image)
    else:
        message(
            ff("Using existing radiopadre singularity image {singularity_image}"
               ))

    # not supported with Singularity
    config.CONTAINER_PERSIST = False