Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 72 additions & 35 deletions reproman/interface/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

__docformat__ = 'restructuredtext'

from collections import OrderedDict
from functools import partial

from .base import Interface
# import reproman.interface.base # Needed for test patching
Expand Down Expand Up @@ -55,49 +55,86 @@ class Ls(Interface):

@staticmethod
def __call__(resrefs=None, verbose=False, refresh=False):
id_length = 19 # todo: make it possible to output them long
template = '{:<20} {:<20} {:<%(id_length)s} {!s:<10}' % locals()
ui.message(template.format('RESOURCE NAME', 'TYPE', 'ID', 'STATUS'))
ui.message(template.format('-------------', '----', '--', '------'))
from pyout import Tabular

results = OrderedDict()
manager = get_manager()
if not resrefs:
resrefs = (manager.inventory[n]["id"] for n in sorted(manager)
if not n.startswith("_"))

for resref in resrefs:
try:
resource = manager.get_resource(resref)
name = resource.name
except ResourceError as e:
lgr.warning("Manager did not return a resource for %s: %s",
resref, exc_str(e))
continue

table = Tabular(
# Note: We're going with the name as the row key even though ID
# would be the more natural choice because (1) inventory already
# uses the name as the key, so we know it's unique and (2) sadly we
# can't rely on the ID saying set after a .connect() calls (e.g.,
# see docker_container.connect()).
["name", "type", "id", "status"],
style={
"default_": {"width": {"marker": "…", "truncate": "center"}},
"header_": {"underline": True,
"transform": str.upper},
"status": {"color":
{"re_lookup": [["^running$", "green"],
["^(stopped|exited)$", "red"],
["(ERROR|NOT FOUND)", "red"]]},
"bold":
{"re_lookup": [["(ERROR|NOT FOUND)", True]]}}})

def get_status(res):
if refresh:
def fn():
try:
res.connect()
except Exception as e:
status = 'CONNECTION ERROR'
else:
status = res.status if res.id else 'NOT FOUND'
return status
return "querying…", fn
else:
return res.status

# Store a list of actions to do after the table is finalized so that we
# don't interrupt the table's output.
do_after = []
# The refresh happens in an asynchronous call. Keep a list of resources
# that we should ask pyout about once the table is finalized.
resources_to_refresh = []
with table:
for resref in resrefs:
try:
resource.connect()
if not resource.id:
resource.status = 'NOT FOUND'
except Exception as e:
lgr.debug("%s resource query error: %s", name, exc_str(e))
resource.status = 'CONNECTION ERROR'

manager.inventory[name].update({'status': resource.status})

id_ = manager.inventory[name]['id']
msgargs = (
name,
resource.type,
id_[:id_length],
resource.status,
)
ui.message(template.format(*msgargs))
results[id_] = msgargs
resource = manager.get_resource(resref)
name = resource.name
except ResourceError as e:
do_after.append(
partial(lgr.warning,
"Manager did not return a resource for %s: %s",
resref,
exc_str(e)))
continue

id_ = manager.inventory[name]['id']
assert id_ == resource.id, "bug in resource logic"
table([name,
resource.type,
id_,
get_status(resource)])
resources_to_refresh.append(resource)

if do_after or not refresh:
# Distinguish between the table and added information.
ui.message("\n")

for fn in do_after:
fn()

if refresh:
manager.save_inventory()
if resources_to_refresh:
for res in resources_to_refresh:
name = res.name
status = table[(name,)]["status"]
manager.inventory[name].update({'status': status})
manager.save_inventory()
else:
ui.message('Use --refresh option to view updated status.')
return results
return table
49 changes: 34 additions & 15 deletions reproman/interface/tests/test_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##

import contextlib
from functools import partial
from io import StringIO
import logging
from unittest.mock import patch

import pytest

from pyout import Tabular

from ...api import ls
from ...utils import swallow_logs
from ...resource.base import ResourceManager
from ...tests.skip import skipif

Expand Down Expand Up @@ -54,36 +60,49 @@ def resource_manager():

@pytest.fixture(scope="function")
def ls_fn(resource_manager):
stream = StringIO()
TestTabular = partial(Tabular, stream=stream)

def fn(*args, **kwargs):
skipif.no_docker_dependencies()
with contextlib.ExitStack() as stack:
stack.enter_context(patch("docker.Client"))
stack.enter_context(patch("reproman.interface.ls.get_manager",
return_value=resource_manager))
return ls(*args, **kwargs)
stack.enter_context(patch("pyout.Tabular", TestTabular))
return ls(*args, **kwargs), stream.getvalue()
return fn


def test_ls_interface(ls_fn):
"""
Test listing the resources.
"""
results = ls_fn()
assert "running" in results["326b0fdfbf838"]
assert "docker-container" in results["326b0fdfbf838"]
assert "i-22221ddf096c22bb0" in results
assert "stopped" in results["i-3333f40de2b9b8967"]
assert "aws-ec2" in results["i-3333f40de2b9b8967"]
table, _ = ls_fn()
dr1 = table[("docker-resource-1",)]
assert dr1["status"] == "running"
assert dr1["type"] == "docker-container"
assert table[("ec2-resource-1",)]["id"] == "i-22221ddf096c22bb0"
er2 = table[("ec2-resource-2",)]
assert er2["status"] == "stopped"
assert er2["type"] == "aws-ec2"

# Test --refresh output
results = ls_fn(refresh=True)
assert "NOT FOUND" in results["326b0fdfbf838"]
assert "CONNECTION ERROR" in results["i-22221ddf096c22bb0"]
assert "CONNECTION ERROR" in results["i-3333f40de2b9b8967"]
table, _ = ls_fn(refresh=True)
assert table[("docker-resource-1",)]["status"] == "NOT FOUND"
assert table[("ec2-resource-1",)]["status"] == "CONNECTION ERROR"
assert table[("ec2-resource-2",)]["status"] == "CONNECTION ERROR"


def test_ls_interface_limited(ls_fn):
results = ls_fn(resrefs=["326", "i-33"])
assert "326b0fdfbf838" in results
assert "i-22221ddf096c22bb0" not in results
assert "i-3333f40de2b9b8967" in results
_, stdout = ls_fn(resrefs=["326", "i-33"])
assert "326b0fdfbf838" in stdout
assert "i-22221ddf096c22bb0" not in stdout
assert "i-3333f40de2b9b8967" in stdout


def test_ls_interface_missing(ls_fn):
with swallow_logs(new_level=logging.WARNING) as log:
_, stdout = ls_fn(resrefs=["idonotexist"])
assert "idonotexist" not in stdout
assert "idonotexist" in log.out
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def findsome(subdir, extensions):
'scp',
'pycrypto',
'pyOpenSSL==16.2.0',
'pyout>=0.4.0',
'requests',
'reprozip; sys_platform=="linux" or sys_platform=="linux2"',
'rpaths',
Expand Down