Control: tags 881375 + patch Control: tags 881375 + pending
Dear maintainer, I've prepared an NMU for sen (versioned as 0.6.0-0.1) and uploaded it. Regards.
diff -Nru sen-0.5.1/debian/changelog sen-0.6.0/debian/changelog --- sen-0.5.1/debian/changelog 2017-11-18 04:11:21.000000000 +0100 +++ sen-0.6.0/debian/changelog 2018-04-03 12:28:44.000000000 +0200 @@ -1,3 +1,11 @@ +sen (0.6.0-0.1) unstable; urgency=medium + + * Non-maintainer upload + * New upstream release (Closes: #881375) + * Drop catchlog dependency, now merged into pytest + + -- Gianfranco Costamagna <locutusofb...@debian.org> Tue, 03 Apr 2018 12:28:44 +0200 + sen (0.5.1-1) unstable; urgency=medium * New upstream release. diff -Nru sen-0.5.1/debian/control sen-0.6.0/debian/control --- sen-0.5.1/debian/control 2017-11-18 04:09:18.000000000 +0100 +++ sen-0.6.0/debian/control 2018-04-03 12:28:44.000000000 +0200 @@ -10,8 +10,7 @@ python3-docker, python3-flexmock, python3-humanize, - python3-pytest, - python3-pytest-catchlog, + python3-pytest (>= 3.3.0), python3-setuptools, python3-urwid, python3-urwidtrees, diff -Nru sen-0.5.1/docs/releasing.md sen-0.6.0/docs/releasing.md --- sen-0.5.1/docs/releasing.md 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/docs/releasing.md 2018-03-22 08:34:32.000000000 +0100 @@ -5,5 +5,6 @@ 3. do **NOT** create feature branch with same name as tag 4. prepare changelog (`CHANGELOG.md` and release notes) 5. when tagged with release, verify that release was successful on travis -6. bump version in setup.py to `x.y.z-dev` +6. correct Docker Hub tags - let latest release point to `latest` +7. bump version in setup.py to `x.y.z-dev` diff -Nru sen-0.5.1/PKG-INFO sen-0.6.0/PKG-INFO --- sen-0.5.1/PKG-INFO 2017-03-18 22:50:27.000000000 +0100 +++ sen-0.6.0/PKG-INFO 2018-03-22 08:35:11.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: sen -Version: 0.5.1 +Version: 0.6.0 Summary: Terminal User Interface for Docker Engine Home-page: https://github.com/TomasTomecek/sen/ Author: Tomas Tomecek diff -Nru sen-0.5.1/README.md sen-0.6.0/README.md --- sen-0.5.1/README.md 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/README.md 2018-03-22 08:34:32.000000000 +0100 @@ -1,7 +1,6 @@ # sen -[](https://circleci.com/gh/TomasTomecek/sen) -[](https://microbadger.com/images/tomastomecek/sen "Get your own image badge on microbadger.com") +[](https://travis-ci.org/TomasTomecek/sen) `sen` is a terminal user interface for docker engine: @@ -16,6 +15,7 @@ * sen notifies you whenever something happens (and reports slow queries) * supports a lot of vim-like keybindings (`j`, `k`, `gg`, `/`, ...) * you can get interactive tree view of all images (equivalent of `docker images --tree`) + * see how much space containers, images and volumes occupy (just type `:df`) You can [see the features yourself](/docs/features.md). @@ -34,7 +34,7 @@ This is the recommended way of running `sen` in a container: ``` -$ docker run --privileged -v /var/run/docker.sock:/run/docker.sock -ti -e TERM tomastomecek/sen +$ docker run -v /var/run/docker.sock:/run/docker.sock -ti -e TERM tomastomecek/sen ``` Some distros have `/var/run` simlinked to `/run`, so you can do `/run/docker.sock:/run/docker.sock` instead. @@ -48,12 +48,21 @@ ``` $ docker build --tag=$USER/sen https://github.com/tomastomecek/sen -$ docker run --privileged -v /var/run/docker.sock:/run/docker.sock -ti -e TERM $USER/sen +$ docker run -v /var/run/docker.sock:/run/docker.sock -ti -e TERM $USER/sen ``` ## PyPI +`sen` is using [`urwidtrees`](https://github.com/pazz/urwidtrees) as a dependency. Unfortunately, the upstream +maintainer doesn't maintain it on PyPI so we need to install it directly from +git, before installing sen (the forked PyPI version has a [bug](https://github.com/TomasTomecek/sen/issues/128) in +installation process): + +``` +$ pip3 install git+https://github.com/pazz/urwidtrees.git@9142c59d3e41421ff6230708d08b6a134e0a8eed#egg=urwidtrees-1.0.3.dev +``` + `sen` releases are available on PyPI: ``` @@ -159,6 +168,7 @@ ``` i inspect image d remove image (irreversible!) +D remove image forcibly (irreversible!) enter display detailed info about image (when layer is focused) ``` @@ -169,6 +179,7 @@ l display logs of container f follow logs of container d remove container (irreversible!) +D remove container forcibly (irreversible!) t stop container s start container r restart container @@ -202,6 +213,11 @@ ``` +## Disk usage buffer + +You can enter it by typing command `df`. + + # Why I started sen? Since I started using docker, I always dreamed of having a docker TUI. Something like [tig](https://github.com/jonas/tig), [htop](http://hisham.hm/htop/) or [alot](https://github.com/pazz/alot). Some appeared over time. Such as [docker-mon](https://github.com/icecrime/docker-mon) or [ctop](https://github.com/yadutaf/ctop). Unfortunately, those are not proper docker TUIs. They are meant for monitoring and diagnostics. diff -Nru sen-0.5.1/requirements-test.txt sen-0.6.0/requirements-test.txt --- sen-0.5.1/requirements-test.txt 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/requirements-test.txt 2018-03-22 08:34:32.000000000 +0100 @@ -1,3 +1,2 @@ -pytest +pytest>=3.4.0 flexmock -pytest-capturelog diff -Nru sen-0.5.1/requirements.txt sen-0.6.0/requirements.txt --- sen-0.5.1/requirements.txt 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/requirements.txt 2018-03-22 08:34:32.000000000 +0100 @@ -1,4 +1,5 @@ urwid docker -# this is 1.0.2, pazz doesn't maintain urwidtrees on PyPI --e git://github.com/pazz/urwidtrees.git@b8159036521100c60ecaefdfe0bed4c50dc3ef23#egg=urwidtrees +# this is latest upstream commit (April 2017) +# upstream maintainer of urwidtrees doesn't maintain PyPI urwidtrees +-e git+https://github.com/pazz/urwidtrees.git@9142c59d3e41421ff6230708d08b6a134e0a8eed#egg=urwidtrees diff -Nru sen-0.5.1/sen/cli.py sen-0.6.0/sen/cli.py --- sen-0.5.1/sen/cli.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/cli.py 2018-03-22 08:34:32.000000000 +0100 @@ -21,6 +21,7 @@ import argparse import logging +import sen from sen import set_logging from sen.exceptions import TerminateApplication from sen.tui.init import Application @@ -33,8 +34,17 @@ parser = argparse.ArgumentParser( description="Terminal User Interface for Docker Engine" ) + parser.add_argument( + "--yolo", "--skip-prompt-for-irreversible-action", + action="store_true", + default=False, + help="Don't prompt when performing irreversible actions, a.k.a. YOLO!" + ) exclusive_group = parser.add_mutually_exclusive_group() - exclusive_group.add_argument("--debug", action="store_true", default=None) + exclusive_group.add_argument( + "--debug", action="store_true", default=None, + help="Set logging level to debug" + ) args = parser.parse_args() @@ -43,19 +53,20 @@ # don't want in a terminal app (thanks to Slavek Kabrda for explanation) if args.debug: set_logging(level=logging.DEBUG, path=get_log_file_path()) + logger.debug("sen loaded from %s", sen.__file__) else: set_logging(level=logging.INFO, path=get_log_file_path()) logger.info("application started") try: - ui = Application() + app = Application(yolo=args.yolo) except TerminateApplication as ex: print("Error: {0}".format(str(ex)), file=sys.stderr) return 1 try: - ui.run() + app.run() except KeyboardInterrupt: print("Quitting on user request.") return 1 diff -Nru sen-0.5.1/sen/docker_backend.py sen-0.6.0/sen/docker_backend.py --- sen-0.5.1/sen/docker_backend.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/docker_backend.py 2018-03-22 08:34:32.000000000 +0100 @@ -1,4 +1,3 @@ -import copy import functools import json import logging @@ -7,14 +6,20 @@ from operator import attrgetter from sen.constants import ISO_DATETIME_PARSE_STRING -from sen.exceptions import TerminateApplication, NotifyError, NotAvailableAnymore +from sen.exceptions import ( + TerminateApplication, NotifyError, NotAvailableAnymore +) import docker import docker.errors from sen.net import NetData -from sen.util import calculate_cpu_percent, calculate_blkio_bytes, calculate_network_bytes, repeater, \ - humanize_time +from sen.util import ( + calculate_cpu_percent, calculate_cpu_percent2, calculate_blkio_bytes, + calculate_network_bytes, repeater, + humanize_time, + graceful_chain_get +) logger = logging.getLogger(__name__) @@ -248,19 +253,14 @@ return hash(self._id) -def graceful_chain_get(d, *args): - if not d: - return None - t = copy.deepcopy(d) - for arg in args: - try: - t = t[arg] - except (AttributeError, KeyError, TypeError): - return None - return t - - class DockerImage(DockerObject): + def __init__(self, data, docker_backend, object_id=None): + super().__init__(data, docker_backend, object_id=object_id) + self._unique_size = None + self._total_size = None + self._virtual_size = None + self._shared_size = None + @property def image_id(self): return self.object_id @@ -372,22 +372,36 @@ return self.metadata_get(["Comment"]) @property - def size(self): + def total_size(self): """ Size of ALL layers in bytes - :return: int + :return: int or None """ - return self.data.get("VirtualSize", 0) + return self._total_size or self.data.get("Size", 0) @property - def layer_size(self): + def unique_size(self): """ Size of ONLY this particular layer - :return: int + :return: int or None """ - return self.data.get("Size", 0) + self._virtual_size = self._virtual_size or \ + graceful_chain_get(self.data, "VirtualSize", default=0) + try: + return self._virtual_size - self._shared_size + except TypeError: + return 0 + + @property + def shared_size(self): + """ + I guess this is size of layers which are shared with some other image + + :return: int or None + """ + return self._shared_size @property def names(self): @@ -443,8 +457,8 @@ return self._inspect @operation("{object_type} {object_short_name} removed!") - def remove(self): - return self.d.remove_image(self.image_id) + def remove(self, force=False): + return self.d.remove_image(self.image_id, force=force) @operation("Tag of {object_type} {object_short_name} removed!") def remove_tag(self, tag): @@ -501,6 +515,11 @@ Container related logic """ + def __init__(self, data, docker_backend, object_id=None): + super(DockerContainer, self).__init__(data, docker_backend, object_id) + self.size_root_fs = None + self.size_rw_fs = None + def __str__(self): return "{} ({})".format(self.container_id, self.short_name) @@ -653,13 +672,23 @@ @operation("Get resources statistics.") def stats(self): + cpu_total = 0.0 + cpu_system = 0.0 + cpu_percent = 0.0 for x in self.d.stats(self.container_id, decode=True, stream=True): blk_read, blk_write = calculate_blkio_bytes(x) net_r, net_w = calculate_network_bytes(x) mem_current = x["memory_stats"]["usage"] mem_total = x["memory_stats"]["limit"] + + try: + cpu_percent, cpu_system, cpu_total = calculate_cpu_percent2(x, cpu_total, cpu_system) + except KeyError as e: + logger.error("error while getting new CPU stats: %r, falling back") + cpu_percent = calculate_cpu_percent(x) + r = { - "cpu_percent": calculate_cpu_percent(x), + "cpu_percent": cpu_percent, "mem_current": mem_current, "mem_total": x["memory_stats"]["limit"], "mem_percent": (mem_current / mem_total) * 100.0, @@ -712,8 +741,8 @@ return logs_data @operation("{object_type} {object_short_name} removed!") - def remove(self): - self.d.remove_container(self.container_id) + def remove(self, force=False): + self.d.remove_container(self.container_id, force=force) @operation("{object_type} {object_short_name} started.") def start(self): @@ -749,6 +778,7 @@ self._containers = None self._images = None # displayed images self._all_images = None # docker images -a + self._df = None kwargs = {"version": "auto"} kwargs.update(docker.utils.kwargs_from_env(assert_hostname=False)) @@ -777,6 +807,7 @@ img = DockerImage(i, self) self._images[img.image_id] = img self._all_images = {} + # FIXME: performance: do just all=True all_images_response = repeater(self.client.images, kwargs={"all": True}) or [] for i in all_images_response: img = DockerImage(i, self) @@ -796,6 +827,30 @@ return [x for x in list(self._containers.values()) if x.running] return list(self._containers.values()) + @operation("Get disk usage.") + def df(self, cached=True): + if cached is False or self._df is None: + logger.debug("getting disk-usage") + # TODO: wrap in try/execpt + self._df = self.client.df() + # TODO: attach these to real containers and images + # # since DOCKER API-1.25 (v.1.13.0) + # df = self.client.df() + # if 'Containers' in df: + # df_containers = df['Containers'] + containers_data = graceful_chain_get(self._df, "Containers") + for c_data in containers_data: + c = graceful_chain_get(self._containers, graceful_chain_get(c_data, "Id")) + c.size_root_fs = graceful_chain_get(c_data, "SizeRootFs") + c.size_rw_fs = graceful_chain_get(c_data, "SizeRw") + images_data = graceful_chain_get(self._df, "Images") + for i_data in images_data: + i = graceful_chain_get(self._images, graceful_chain_get(i_data, "Id")) + i._total_size = graceful_chain_get(i_data, "Size") + i._shared_size = graceful_chain_get(i_data, "SharedSize") + i._virtual_size = graceful_chain_get(i_data, "VirtualSize") + return self._df + def realtime_updates(self): event = it = None while True: diff -Nru sen-0.5.1/sen/__init__.py sen-0.6.0/sen/__init__.py --- sen-0.5.1/sen/__init__.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/__init__.py 2018-03-22 08:34:32.000000000 +0100 @@ -2,7 +2,7 @@ from sen.constants import FALLBACK_LOG_PATH -__version__ = "0.5.1" +__version__ = "0.6.0" def set_logging(name="sen", level=logging.DEBUG, path=FALLBACK_LOG_PATH): diff -Nru sen-0.5.1/sen/tui/buffer.py sen-0.6.0/sen/tui/buffer.py --- sen-0.5.1/sen/tui/buffer.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/buffer.py 2018-03-22 08:34:32.000000000 +0100 @@ -3,6 +3,7 @@ from sen.docker_backend import DockerContainer, RootImage from sen.exceptions import NotifyError from sen.tui.commands.base import Command +from sen.tui.views.disk_usage import DfBufferView from sen.tui.views.help import HelpBufferView, HelpCommandView from sen.tui.views.main import MainListBox from sen.tui.views.image_info import ImageInfoWidget @@ -114,7 +115,8 @@ class ImageInfoBuffer(Buffer): - description = "Dashboard for information about selected image." + description = "Dashboard for information about selected image.\n" + \ + "You can run command `df` to get more detailed info about disk usage." keybinds = { "enter": "display-info", "d": "rm", @@ -184,7 +186,8 @@ display_name = "Listing" description = "List of all known docker images and containers display in a single list" keybinds = { - "d": "rm", # TODO: do also rmi + "d": "rm", + "D": "rm -f", "s": "start", "t": "stop", "r": "restart", @@ -293,3 +296,20 @@ self.widget = HelpCommandView(ui, inp) super().__init__() + + +class DfBuffer(Buffer): + description = "Show information about how much disk space container, images and volumes take." + display_name = "Disk Usage" + + def __init__(self, ui): + """ + :param ui: UI instance + """ + self.ui = ui + self.widget = DfBufferView(ui, self) + + super().__init__() + + def refresh(self, df=None, containers=None, images=None): + self.widget.refresh(df=df, containers=containers, images=images) diff -Nru sen-0.5.1/sen/tui/chunks/container.py sen-0.6.0/sen/tui/chunks/container.py --- sen-0.5.1/sen/tui/chunks/container.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/chunks/container.py 2018-03-22 08:34:32.000000000 +0100 @@ -28,7 +28,11 @@ container_id = SelectableText(docker_container.short_id) row.append(container_id) - command = SelectableText(docker_container.command, get_map(defult="main_list_ddg")) + commands = docker_container.command.split("\n") + command_str = commands[0] + if len(commands) > 0: + command_str += "..." + command = SelectableText(command_str, get_map(defult="main_list_ddg")) row.append(command) image = SelectableText(docker_container.image_name()) diff -Nru sen-0.5.1/sen/tui/chunks/image.py sen-0.6.0/sen/tui/chunks/image.py --- sen-0.5.1/sen/tui/chunks/image.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/chunks/image.py 2018-03-22 08:34:32.000000000 +0100 @@ -96,6 +96,6 @@ if with_size: text_markup.append(" ") - text_markup.append(("main_list_ddg", "(%s)" % humanize_bytes(docker_image.layer_size))) + text_markup.append(("main_list_ddg", "(%s)" % humanize_bytes(docker_image.total_size))) return text_markup diff -Nru sen-0.5.1/sen/tui/commands/backend.py sen-0.6.0/sen/tui/commands/backend.py --- sen-0.5.1/sen/tui/commands/backend.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/commands/backend.py 2018-03-22 08:34:32.000000000 +0100 @@ -5,7 +5,7 @@ import logging import webbrowser -from sen.tui.buffer import LogsBuffer, InspectBuffer +from sen.tui.buffer import LogsBuffer, InspectBuffer, DfBuffer from sen.tui.commands.base import BackendCommand, register_command, Option from sen.tui.widgets.list.util import get_operation_notify_widget from sen.docker_backend import DockerContainer @@ -15,12 +15,12 @@ class OperationCommand(BackendCommand): - def do(self, fn_name, pre_message=None, notif_level="info"): + def do(self, fn_name, pre_message=None, notif_level="info", **kwargs): pre_message = pre_message or self.pre_info_message.format( container_name=self.docker_object.short_name) self.ui.notify_message(pre_message) try: - operation = getattr(self.docker_object, fn_name)() + operation = getattr(self.docker_object, fn_name)(**kwargs) except AttributeError: log_txt = "you can't {} {}".format(fn_name, self.docker_object) logger.error(log_txt) @@ -49,14 +49,38 @@ class RemoveCommand(OperationCommand): name = "rm" description = "remove provided object, image or container" + options_definitions = [Option("force", + "Force removal of the selected object.", + default=False, aliases=["-f", "f"]), + Option("yes", + "Don't ask before removing.", + default=False, aliases=["-y"])] - # FIXME: split this into rm and rmi def run(self): + logger.debug("remove %s force=%s yes=%s", self.docker_object, self.arguments.force, + self.arguments.yes) + if not self.arguments.yes and not self.ui.yolo: + logger.debug("we need confirmation from user") + cmd = "rm -y" if not self.arguments.force else "rm -y -f" + self.ui.run_command( + "prompt prompt-text=\"You are about to remove %s %s, type enter to continue: \" " + "initial-text=\"%s\"" % ( + self.docker_object.pretty_object_type.lower(), + self.docker_object.short_name, + cmd + ), + docker_object=self.docker_object + ) + return + + if self.arguments.force: + self.ui.notify_message("Removing forcibly!", level="important") self.do("remove", pre_message="Removing {} {}...".format( self.docker_object.pretty_object_type.lower(), self.docker_object.short_name), - notif_level="important") + notif_level="important", + force=self.arguments.force) @register_command @@ -125,6 +149,20 @@ @register_command +class DfCommand(BackendCommand): + name = "df" + description = "show disk usage" + + def run(self): + b = DfBuffer(self.ui) + self.ui.add_and_display_buffer(b) + df = self.docker_backend.df() + b.refresh(df=df.response, + containers=self.docker_backend.get_containers(cached=True, stopped=True).response, + images=self.docker_backend.get_images(cached=True).response) + + +@register_command class OpenPortsInBrowser(BackendCommand): name = "open-browser" # TODO: user should be able to specify port and ip: by hitting keybind in container info view diff -Nru sen-0.5.1/sen/tui/commands/display.py sen-0.6.0/sen/tui/commands/display.py --- sen-0.5.1/sen/tui/commands/display.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/commands/display.py 2018-03-22 08:34:32.000000000 +0100 @@ -8,8 +8,11 @@ from sen.docker_backend import DockerImage, DockerContainer from sen.exceptions import NotifyError -from sen.tui.buffer import TreeBuffer, HelpBuffer, ImageInfoBuffer, ContainerInfoBuffer, \ +from sen.tui.buffer import ( + TreeBuffer, HelpBuffer, + ImageInfoBuffer, ContainerInfoBuffer, MainListBuffer +) from sen.tui.commands.base import FrontendCommand, register_command diff -Nru sen-0.5.1/sen/tui/commands/ui.py sen-0.6.0/sen/tui/commands/ui.py --- sen-0.5.1/sen/tui/commands/ui.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/commands/ui.py 2018-03-22 08:34:32.000000000 +0100 @@ -8,8 +8,11 @@ from sen.exceptions import NotifyError from sen.tui.buffer import HelpBuffer, TreeBuffer -from sen.tui.commands.base import register_command, SameThreadCommand, Option, Argument, \ +from sen.tui.commands.base import ( + register_command, SameThreadCommand, + Option, Argument, NoSuchCommand +) from sen.util import log_traceback, log_last_traceback @@ -132,7 +135,7 @@ @log_traceback -def run_command_callback(ui, edit_widget, text_input): +def run_command_callback(ui, docker_object, edit_widget, text_input): logger.debug("%r %r", edit_widget, text_input) if "\n" in text_input: inp = text_input.strip() @@ -141,7 +144,7 @@ ui.prompt_bar = None ui.set_focus("body") try: - ui.run_command(inp) + ui.run_command(inp, docker_object=docker_object) except NoSuchCommand as ex: logger.info("non-existent command initiated: %r", inp) ui.notify_message(str(ex), level="error") @@ -181,7 +184,8 @@ self.ui.reload_footer() self.ui.set_focus("footer") - urwid.connect_signal(editpart, "change", run_command_callback, user_args=[self.ui]) + urwid.connect_signal(editpart, "change", run_command_callback, + user_args=[self.ui, self.docker_object]) @register_command diff -Nru sen-0.5.1/sen/tui/init.py sen-0.6.0/sen/tui/init.py --- sen-0.5.1/sen/tui/init.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/init.py 2018-03-22 08:34:32.000000000 +0100 @@ -17,11 +17,13 @@ class Application: - def __init__(self): + def __init__(self, yolo=False): self.d = DockerBackend() self.loop, self.ui = get_app_in_loop(PALLETE) + self.ui.yolo = yolo + self.ui.commander = Commander(self.ui, self.d) self.rt_thread = threading.Thread(target=self.realtime_updates, daemon=True) diff -Nru sen-0.5.1/sen/tui/ui.py sen-0.6.0/sen/tui/ui.py --- sen-0.5.1/sen/tui/ui.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/ui.py 2018-03-22 08:34:32.000000000 +0100 @@ -10,8 +10,10 @@ import urwid from sen.exceptions import NotifyError -from sen.tui.commands.base import FrontendPriority, BackendPriority, SameThreadPriority, \ - KeyNotMapped +from sen.tui.commands.base import ( + FrontendPriority, BackendPriority, + SameThreadPriority, KeyNotMapped +) from sen.tui.constants import CLEAR_NOTIF_BAR_MESSAGE_IN from sen.tui.widgets.util import ThreadSafeFrame from sen.util import log_traceback, OrderedSet diff -Nru sen-0.5.1/sen/tui/views/container_info.py sen-0.6.0/sen/tui/views/container_info.py --- sen-0.5.1/sen/tui/views/container_info.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/views/container_info.py 2018-03-22 08:34:32.000000000 +0100 @@ -14,7 +14,10 @@ from sen.tui.widgets.list.base import WidgetBase from sen.tui.widgets.list.util import RowWidget, UnselectableRowWidget from sen.tui.widgets.table import assemble_rows -from sen.tui.widgets.util import SelectableText, get_map, ColorText, UnselectableListBox +from sen.tui.widgets.util import ( + SelectableText, get_map, + ColorText, UnselectableListBox +) from sen.util import log_traceback, humanize_bytes @@ -223,6 +226,16 @@ [SelectableText("Name", maps=get_map("main_list_green")), SelectableText("".join(self.docker_container.names))], ) + + if self.docker_container.size_root_fs: + data.append( + [SelectableText("Image Size", maps=get_map("main_list_green")), + SelectableText(humanize_bytes(self.docker_container.size_root_fs))]) + if self.docker_container.size_rw_fs: + data.append( + [SelectableText("Writable Layer Size", maps=get_map("main_list_green")), + SelectableText(humanize_bytes(self.docker_container.size_rw_fs))]) + self.view_widgets.extend(assemble_rows(data, ignore_columns=[1])) def _net(self): @@ -264,9 +277,10 @@ self.view_widgets.extend(assemble_rows(data, dividechars=3, ignore_columns=[1])) def _image(self): - self.view_widgets.append(RowWidget([SelectableText("")])) - self.view_widgets.append(RowWidget([SelectableText("Image", maps=get_map("main_list_white"))])) - self.view_widgets.append(RowWidget([LayerWidget(self.ui, self.docker_container.image)])) + if self.docker_container.image: + self.view_widgets.append(RowWidget([SelectableText("")])) + self.view_widgets.append(RowWidget([SelectableText("Image", maps=get_map("main_list_white"))])) + self.view_widgets.append(RowWidget([LayerWidget(self.ui, self.docker_container.image)])) def _resources(self): self.view_widgets.append(RowWidget([SelectableText("")])) @@ -322,6 +336,7 @@ continue logger.error("error while getting stats: %r", ex) self.ui.notify_message("Error while getting stats: %s" % ex, level="error") + # TODO: if debug raise break if self.stop.is_set(): diff -Nru sen-0.5.1/sen/tui/views/disk_usage.py sen-0.6.0/sen/tui/views/disk_usage.py --- sen-0.5.1/sen/tui/views/disk_usage.py 1970-01-01 01:00:00.000000000 +0100 +++ sen-0.6.0/sen/tui/views/disk_usage.py 2018-03-22 08:34:32.000000000 +0100 @@ -0,0 +1,114 @@ +""" +TODO: + * nicer list + * summary + * clickable items + * enable deleting volumes +""" +import urwid + +from sen.util import humanize_bytes, graceful_chain_get +from sen.tui.views.base import View +from sen.tui.widgets.list.util import SingleTextRow +from sen.tui.widgets.table import assemble_rows +from sen.tui.constants import MAIN_LIST_FOCUS +from sen.tui.widgets.util import SelectableText, get_map +from sen.tui.widgets.list.base import WidgetBase + + +class DfBufferView(WidgetBase, View): + def __init__(self, ui, buffer): + """ + + :param ui: + :param buffer: Buffer instance, display help about this buffer + """ + self.ui = ui + self.buffer = buffer + self.walker = urwid.SimpleFocusListWalker([]) + super().__init__(ui, self.walker) + + def refresh(self, df=None, containers=None, images=None): + content = [] + if df is None: + content += [ + SingleTextRow("Data is being loaded, it may even take a couple minutes.", + maps={"normal": "main_list_white", "focus": MAIN_LIST_FOCUS}), + ] + else: + if containers: + content += [ + SingleTextRow("Containers", + maps={"normal": "main_list_white", "focus": MAIN_LIST_FOCUS}), + SingleTextRow("") + ] + containers_content = [[ + SelectableText("Name", maps=get_map("main_list_lg")), + SelectableText("Image Size", maps=get_map("main_list_lg")), + SelectableText("Writable Layer Size", maps=get_map("main_list_lg")), + ]] + for c in containers: + containers_content.append( + [SelectableText(c.short_name), + SelectableText(humanize_bytes(c.size_root_fs or 0)), + SelectableText(humanize_bytes(c.size_rw_fs or 0)), + ]) + content.extend(assemble_rows(containers_content, dividechars=3)) + content += [ + SingleTextRow("") + ] + if images: + content += [ + SingleTextRow("Images", + maps={"normal": "main_list_white", "focus": MAIN_LIST_FOCUS}), + SingleTextRow("") + ] + images_content = [[ + SelectableText("Name", maps=get_map("main_list_lg")), + SelectableText("Size", maps=get_map("main_list_lg")), + SelectableText("Shared Size", maps=get_map("main_list_lg")), + SelectableText("Unique Size", maps=get_map("main_list_lg")) + ]] + for i in images: + images_content.append([ + SelectableText(i.short_name, maps=get_map("main_list_dg")), + SelectableText( + humanize_bytes(i.total_size or 0), + maps=get_map("main_list_dg")), + SelectableText( + humanize_bytes(i.shared_size or 0), + maps=get_map("main_list_dg")), + SelectableText( + humanize_bytes(i.unique_size or 0), + maps=get_map("main_list_dg")) + ]) + content.extend(assemble_rows(images_content, dividechars=3)) + content += [ + SingleTextRow("") + ] + volumes = graceful_chain_get(df, "Volumes") + if volumes: + content += [ + SingleTextRow("Volumes", + maps={"normal": "main_list_white", "focus": MAIN_LIST_FOCUS}), + SingleTextRow("") + ] + volumes_content = [[ + SelectableText("Name", maps=get_map("main_list_lg")), + SelectableText("Links", maps=get_map("main_list_lg")), + SelectableText("Size", maps=get_map("main_list_lg")), + ]] + for v in volumes: + v_name = graceful_chain_get(v, "Name", default="") + v_size = graceful_chain_get(v, "UsageData", "Size", default=0) + v_links = graceful_chain_get(v, "UsageData", "RefCount", default=0) + volumes_content.append([ + SelectableText(v_name, maps=get_map("main_list_dg")), + SelectableText("%s" % v_links, maps=get_map("main_list_dg")), + SelectableText( + humanize_bytes(v_size), + maps=get_map("main_list_dg")), + ]) + content.extend(assemble_rows(volumes_content, dividechars=3)) + self.set_body(content) + self.set_focus(0) diff -Nru sen-0.5.1/sen/tui/views/image_info.py sen-0.6.0/sen/tui/views/image_info.py --- sen-0.5.1/sen/tui/views/image_info.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/views/image_info.py 2018-03-22 08:34:32.000000000 +0100 @@ -65,10 +65,18 @@ SelectableText("{0}, {1}".format(self.docker_image.display_formal_time_created(), self.docker_image.display_time_created()))], [SelectableText("Size", maps=get_map("main_list_green")), - SelectableText(humanize_bytes(self.docker_image.size))], - [SelectableText("Command", maps=get_map("main_list_green")), - SelectableText(self.docker_image.container_command)], + SelectableText(humanize_bytes(self.docker_image.total_size))], ] + if self.docker_image.unique_size: + data.append( + [SelectableText("Unique Size", maps=get_map("main_list_green")), + SelectableText(humanize_bytes(self.docker_image.unique_size))]) + if self.docker_image.shared_size: + data.append( + [SelectableText("Shared Size", maps=get_map("main_list_green")), + SelectableText(humanize_bytes(self.docker_image.shared_size))]) + data.append([SelectableText("Command", maps=get_map("main_list_green")), + SelectableText(self.docker_image.container_command)]) self.walker.extend(assemble_rows(data, ignore_columns=[1])) def _image_names(self): diff -Nru sen-0.5.1/sen/tui/widgets/list/base.py sen-0.6.0/sen/tui/widgets/list/base.py --- sen-0.5.1/sen/tui/widgets/list/base.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/widgets/list/base.py 2018-03-22 08:34:32.000000000 +0100 @@ -10,7 +10,7 @@ class WidgetBase(urwid.ListBox): """ - common class fot widgets + common class for widgets """ def __init__(self, ui, *args, **kwargs): diff -Nru sen-0.5.1/sen/tui/widgets/table.py sen-0.6.0/sen/tui/widgets/table.py --- sen-0.5.1/sen/tui/widgets/table.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/tui/widgets/table.py 2018-03-22 08:34:32.000000000 +0100 @@ -32,7 +32,7 @@ return max_cols_lengths -def assemble_rows(data, headers=None, max_allowed_lengths=None, dividechars=1, +def assemble_rows(data, max_allowed_lengths=None, dividechars=1, ignore_columns=None): """ :param data: list of lists: @@ -40,7 +40,6 @@ ["row 2 column 1", "row 2 column 2"]] each item consists of instance of urwid.Text - :param headers: list of str, headers of table :param max_allowed_lengths: dict: {col_index: maximum_allowed_length} :param ignore_columns: list of ints, indexes which should not be calculated @@ -67,15 +66,6 @@ max_lengths[col_index] = max(l, max_lengths[col_index]) col_index += 1 - if headers: - header_widgets = [] - for header in headers: - try: - header_widgets.append(urwid.Text(*header[0], **header[1])) - except IndexError: - header_widgets.append(urwid.Text(*header[0])) - rows.append(urwid.Columns(header_widgets, dividechars=1)) - for row in data: row_widgets = [] for idx, item in enumerate(row): diff -Nru sen-0.5.1/sen/util.py sen-0.6.0/sen/util.py --- sen-0.5.1/sen/util.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/sen/util.py 2018-03-22 08:34:32.000000000 +0100 @@ -40,6 +40,10 @@ def setup_dirs(): + """Make required directories to hold logfile. + + :returns: str + """ try: top_dir = os.path.abspath(os.path.expanduser(os.environ["XDG_CACHE_HOME"])) except KeyError: @@ -166,6 +170,23 @@ cpu_percent = cpu_delta / system_delta * 100.0 * cpu_count return cpu_percent +# again taken directly from docker: +# https://github.com/docker/cli/blob/2bfac7fcdafeafbd2f450abb6d1bb3106e4f3ccb/cli/command/container/stats_helpers.go#L168 +# precpu_stats in 1.13+ is completely broken, doesn't contain any values +def calculate_cpu_percent2(d, previous_cpu, previous_system): + # import json + # du = json.dumps(d, indent=2) + # logger.debug("XXX: %s", du) + cpu_percent = 0.0 + cpu_total = float(d["cpu_stats"]["cpu_usage"]["total_usage"]) + cpu_delta = cpu_total - previous_cpu + cpu_system = float(d["cpu_stats"]["system_cpu_usage"]) + system_delta = cpu_system - previous_system + online_cpus = d["cpu_stats"].get("online_cpus", len(d["cpu_stats"]["cpu_usage"]["percpu_usage"])) + if system_delta > 0.0: + cpu_percent = (cpu_delta / system_delta) * online_cpus * 100.0 + return cpu_percent, cpu_system, cpu_total + def calculate_blkio_bytes(d): """ @@ -209,7 +230,7 @@ for a in args: try: t = t[a] - except (KeyError, ValueError, TypeError) as ex: + except (KeyError, ValueError, TypeError, AttributeError): logger.debug("can't get %r from %s", a, t) return default return t diff -Nru sen-0.5.1/sen.egg-info/dependency_links.txt sen-0.6.0/sen.egg-info/dependency_links.txt --- sen-0.5.1/sen.egg-info/dependency_links.txt 2017-03-18 22:50:27.000000000 +0100 +++ sen-0.6.0/sen.egg-info/dependency_links.txt 2018-03-22 08:35:11.000000000 +0100 @@ -1 +1 @@ - +git+https://github.com/pazz/urwidtrees.git@9142c59d3e41421ff6230708d08b6a134e0a8eed#egg=urwidtrees-1.0.3.dev diff -Nru sen-0.5.1/sen.egg-info/PKG-INFO sen-0.6.0/sen.egg-info/PKG-INFO --- sen-0.5.1/sen.egg-info/PKG-INFO 2017-03-18 22:50:27.000000000 +0100 +++ sen-0.6.0/sen.egg-info/PKG-INFO 2018-03-22 08:35:11.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: sen -Version: 0.5.1 +Version: 0.6.0 Summary: Terminal User Interface for Docker Engine Home-page: https://github.com/TomasTomecek/sen/ Author: Tomas Tomecek diff -Nru sen-0.5.1/sen.egg-info/requires.txt sen-0.6.0/sen.egg-info/requires.txt --- sen-0.5.1/sen.egg-info/requires.txt 2017-03-18 22:50:27.000000000 +0100 +++ sen-0.6.0/sen.egg-info/requires.txt 2018-03-22 08:35:11.000000000 +0100 @@ -1,3 +1,3 @@ urwid -urwidtrees docker +urwidtrees diff -Nru sen-0.5.1/sen.egg-info/SOURCES.txt sen-0.6.0/sen.egg-info/SOURCES.txt --- sen-0.5.1/sen.egg-info/SOURCES.txt 2017-03-18 22:50:27.000000000 +0100 +++ sen-0.6.0/sen.egg-info/SOURCES.txt 2018-03-22 08:35:11.000000000 +0100 @@ -39,6 +39,7 @@ sen/tui/views/__init__.py sen/tui/views/base.py sen/tui/views/container_info.py +sen/tui/views/disk_usage.py sen/tui/views/help.py sen/tui/views/image_info.py sen/tui/views/main.py @@ -62,15 +63,4 @@ tests/test_net.py tests/test_util.py tests/test_widgets.py -tests/utils.py -tests/__pycache__/__init__.cpython-33.pyc -tests/__pycache__/constants.cpython-33.pyc -tests/__pycache__/real.cpython-33.pyc -tests/__pycache__/test_commands.cpython-33-PYTEST.pyc -tests/__pycache__/test_concurrency.cpython-33-PYTEST.pyc -tests/__pycache__/test_container_info.cpython-33-PYTEST.pyc -tests/__pycache__/test_docker_backend.cpython-33-PYTEST.pyc -tests/__pycache__/test_net.cpython-33-PYTEST.pyc -tests/__pycache__/test_util.cpython-33-PYTEST.pyc -tests/__pycache__/test_widgets.cpython-33-PYTEST.pyc -tests/__pycache__/utils.cpython-33.pyc \ No newline at end of file +tests/utils.py \ No newline at end of file diff -Nru sen-0.5.1/setup.cfg sen-0.6.0/setup.cfg --- sen-0.5.1/setup.cfg 2017-03-18 22:50:27.000000000 +0100 +++ sen-0.6.0/setup.cfg 2018-03-22 08:35:11.000000000 +0100 @@ -2,7 +2,6 @@ description-file = README.md [egg_info] -tag_svn_revision = 0 -tag_date = 0 tag_build = +tag_date = 0 diff -Nru sen-0.5.1/setup.py sen-0.6.0/setup.py --- sen-0.5.1/setup.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/setup.py 2018-03-22 08:34:32.000000000 +0100 @@ -4,15 +4,20 @@ from setuptools import setup, find_packages +# vcs+proto://host/path@revision#egg=project-version +# this is latest upstream commit (April 2017) +# upstream maintainer of urwidtrees doesn't maintain PyPI urwidtrees +urwidtrees_source = "git+https://github.com/pazz/urwidtrees.git@9142c59d3e41421ff6230708d08b6a134e0a8eed#egg=urwidtrees-1.0.3.dev" + requirements = [ "urwid", - "urwidtrees", - "docker" + "docker", + "urwidtrees" ] setup( name='sen', - version='0.5.1', + version='0.6.0', description="Terminal User Interface for Docker Engine", author='Tomas Tomecek', author_email='to...@tomecek.net', @@ -23,6 +28,7 @@ }, packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), install_requires=requirements, + dependency_links=[urwidtrees_source], classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/constants.cpython-33.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/constants.cpython-33.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/__init__.cpython-33.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/__init__.cpython-33.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/real.cpython-33.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/real.cpython-33.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_commands.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_commands.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_concurrency.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_concurrency.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_container_info.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_container_info.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_docker_backend.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_docker_backend.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_net.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_net.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_util.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_util.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/test_widgets.cpython-33-PYTEST.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/test_widgets.cpython-33-PYTEST.pyc differ Binary files /tmp/IN7Zw8Skz3/sen-0.5.1/tests/__pycache__/utils.cpython-33.pyc and /tmp/o_7aodhgqh/sen-0.6.0/tests/__pycache__/utils.cpython-33.pyc differ diff -Nru sen-0.5.1/tests/real.py sen-0.6.0/tests/real.py --- sen-0.5.1/tests/real.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/tests/real.py 2018-03-22 08:34:32.000000000 +0100 @@ -383,6 +383,928 @@ } } +# stats 1.13 +stats_1_13 = [ +{ + "read": "2017-08-14T09:51:41.318110362Z", + "preread": "2017-08-14T09:51:40.318034402Z", + "pids_stats": { + "current": 1 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 7, + "minor": 0, + "op": "Read", + "value": 36864 + }, + { + "major": 7, + "minor": 0, + "op": "Write", + "value": 12288 + }, + { + "major": 7, + "minor": 0, + "op": "Sync", + "value": 49152 + }, + { + "major": 7, + "minor": 0, + "op": "Async", + "value": 0 + }, + { + "major": 7, + "minor": 0, + "op": "Total", + "value": 49152 + }, + { + "major": 253, + "minor": 1, + "op": "Read", + "value": 36864 + }, + { + "major": 253, + "minor": 1, + "op": "Write", + "value": 12288 + }, + { + "major": 253, + "minor": 1, + "op": "Sync", + "value": 49152 + }, + { + "major": 253, + "minor": 1, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 1, + "op": "Total", + "value": 49152 + }, + { + "major": 253, + "minor": 3, + "op": "Read", + "value": 46022656 + }, + { + "major": 253, + "minor": 3, + "op": "Write", + "value": 155648 + }, + { + "major": 253, + "minor": 3, + "op": "Sync", + "value": 46178304 + }, + { + "major": 253, + "minor": 3, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 3, + "op": "Total", + "value": 46178304 + } + ], + "io_serviced_recursive": [ + { + "major": 7, + "minor": 0, + "op": "Read", + "value": 6 + }, + { + "major": 7, + "minor": 0, + "op": "Write", + "value": 3 + }, + { + "major": 7, + "minor": 0, + "op": "Sync", + "value": 9 + }, + { + "major": 7, + "minor": 0, + "op": "Async", + "value": 0 + }, + { + "major": 7, + "minor": 0, + "op": "Total", + "value": 9 + }, + { + "major": 253, + "minor": 1, + "op": "Read", + "value": 6 + }, + { + "major": 253, + "minor": 1, + "op": "Write", + "value": 3 + }, + { + "major": 253, + "minor": 1, + "op": "Sync", + "value": 9 + }, + { + "major": 253, + "minor": 1, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 1, + "op": "Total", + "value": 9 + }, + { + "major": 253, + "minor": 3, + "op": "Read", + "value": 1074 + }, + { + "major": 253, + "minor": 3, + "op": "Write", + "value": 17 + }, + { + "major": 253, + "minor": 3, + "op": "Sync", + "value": 1091 + }, + { + "major": 253, + "minor": 3, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 3, + "op": "Total", + "value": 1091 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + }, + "num_procs": 0, + "storage_stats": {}, + "cpu_stats": { + "cpu_usage": { + "total_usage": 4325370082, + "percpu_usage": [ + 1583545267, + 1441574399, + 932669369, + 367581047, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 490000000, + "usage_in_usermode": 3800000000 + }, + "system_cpu_usage": 111726360000000, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "precpu_stats": { + "cpu_usage": { + "total_usage": 4172062391, + "percpu_usage": [ + 1545098206, + 1408480094, + 870204053, + 348280038, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 430000000, + "usage_in_usermode": 3710000000 + }, + "system_cpu_usage": 111722350000000, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 179355648, + "max_usage": 179404800, + "stats": { + "active_anon": 53280768, + "active_file": 11571200, + "cache": 119050240, + "dirty": 51224576, + "hierarchical_memory_limit": 9223372036854771712, + "hierarchical_memsw_limit": 9223372036854771712, + "inactive_anon": 0, + "inactive_file": 107479040, + "mapped_file": 21954560, + "pgfault": 37991, + "pgmajfault": 259, + "pgpgin": 64385, + "pgpgout": 22312, + "recent_rotated_anon": 35307, + "recent_rotated_file": 2829, + "recent_scanned_anon": 35307, + "recent_scanned_file": 31903, + "rss": 53280768, + "rss_huge": 0, + "swap": 0, + "total_active_anon": 53280768, + "total_active_file": 11571200, + "total_cache": 119050240, + "total_dirty": 51224576, + "total_inactive_anon": 0, + "total_inactive_file": 107479040, + "total_mapped_file": 21954560, + "total_pgfault": 37991, + "total_pgmajfault": 259, + "total_pgpgin": 64385, + "total_pgpgout": 22312, + "total_rss": 53280768, + "total_rss_huge": 0, + "total_swap": 0, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "limit": 12283822080 + }, + "name": "/hungry_brown", + "id": "98a9cd0009984e61a24499aec44bd244078a6a09486782015b3966916eee5c08", + "networks": { + "eth0": { + "rx_bytes": 63518221, + "rx_packets": 46327, + "rx_errors": 0, + "rx_dropped": 0, + "tx_bytes": 1601338, + "tx_packets": 23966, + "tx_errors": 0, + "tx_dropped": 0 + } + } +}, +{ + "read": "2017-08-14T09:51:42.318182309Z", + "preread": "2017-08-14T09:51:41.318110362Z", + "pids_stats": { + "current": 1 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 7, + "minor": 0, + "op": "Read", + "value": 36864 + }, + { + "major": 7, + "minor": 0, + "op": "Write", + "value": 12288 + }, + { + "major": 7, + "minor": 0, + "op": "Sync", + "value": 49152 + }, + { + "major": 7, + "minor": 0, + "op": "Async", + "value": 0 + }, + { + "major": 7, + "minor": 0, + "op": "Total", + "value": 49152 + }, + { + "major": 253, + "minor": 1, + "op": "Read", + "value": 36864 + }, + { + "major": 253, + "minor": 1, + "op": "Write", + "value": 12288 + }, + { + "major": 253, + "minor": 1, + "op": "Sync", + "value": 49152 + }, + { + "major": 253, + "minor": 1, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 1, + "op": "Total", + "value": 49152 + }, + { + "major": 253, + "minor": 3, + "op": "Read", + "value": 46039040 + }, + { + "major": 253, + "minor": 3, + "op": "Write", + "value": 155648 + }, + { + "major": 253, + "minor": 3, + "op": "Sync", + "value": 46194688 + }, + { + "major": 253, + "minor": 3, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 3, + "op": "Total", + "value": 46194688 + } + ], + "io_serviced_recursive": [ + { + "major": 7, + "minor": 0, + "op": "Read", + "value": 6 + }, + { + "major": 7, + "minor": 0, + "op": "Write", + "value": 3 + }, + { + "major": 7, + "minor": 0, + "op": "Sync", + "value": 9 + }, + { + "major": 7, + "minor": 0, + "op": "Async", + "value": 0 + }, + { + "major": 7, + "minor": 0, + "op": "Total", + "value": 9 + }, + { + "major": 253, + "minor": 1, + "op": "Read", + "value": 6 + }, + { + "major": 253, + "minor": 1, + "op": "Write", + "value": 3 + }, + { + "major": 253, + "minor": 1, + "op": "Sync", + "value": 9 + }, + { + "major": 253, + "minor": 1, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 1, + "op": "Total", + "value": 9 + }, + { + "major": 253, + "minor": 3, + "op": "Read", + "value": 1075 + }, + { + "major": 253, + "minor": 3, + "op": "Write", + "value": 17 + }, + { + "major": 253, + "minor": 3, + "op": "Sync", + "value": 1092 + }, + { + "major": 253, + "minor": 3, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 3, + "op": "Total", + "value": 1092 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + }, + "num_procs": 0, + "storage_stats": {}, + "cpu_stats": { + "cpu_usage": { + "total_usage": 4733027699, + "percpu_usage": [ + 1598666891, + 1746779810, + 999006836, + 388574162, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 550000000, + "usage_in_usermode": 4140000000 + }, + "system_cpu_usage": 111730350000000, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "precpu_stats": { + "cpu_usage": { + "total_usage": 4325370082, + "percpu_usage": [ + 1583545267, + 1441574399, + 932669369, + 367581047, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 490000000, + "usage_in_usermode": 3800000000 + }, + "system_cpu_usage": 111726360000000, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 186646528, + "max_usage": 186646528, + "stats": { + "active_anon": 53792768, + "active_file": 12742656, + "cache": 125739008, + "dirty": 57917440, + "hierarchical_memory_limit": 9223372036854771712, + "hierarchical_memsw_limit": 9223372036854771712, + "inactive_anon": 0, + "inactive_file": 112996352, + "mapped_file": 21954560, + "pgfault": 38262, + "pgmajfault": 259, + "pgpgin": 66151, + "pgpgout": 22318, + "recent_rotated_anon": 35432, + "recent_rotated_file": 3121, + "recent_scanned_anon": 35432, + "recent_scanned_file": 33834, + "rss": 53800960, + "rss_huge": 0, + "swap": 0, + "total_active_anon": 53792768, + "total_active_file": 12742656, + "total_cache": 125739008, + "total_dirty": 57917440, + "total_inactive_anon": 0, + "total_inactive_file": 112996352, + "total_mapped_file": 21954560, + "total_pgfault": 38262, + "total_pgmajfault": 259, + "total_pgpgin": 66151, + "total_pgpgout": 22318, + "total_rss": 53800960, + "total_rss_huge": 0, + "total_swap": 0, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "limit": 12283822080 + }, + "name": "/hungry_brown", + "id": "98a9cd0009984e61a24499aec44bd244078a6a09486782015b3966916eee5c08", + "networks": { + "eth0": { + "rx_bytes": 70507552, + "rx_packets": 51415, + "rx_errors": 0, + "rx_dropped": 0, + "tx_bytes": 1766536, + "tx_packets": 26469, + "tx_errors": 0, + "tx_dropped": 0 + } + } +}, +{ + "read": "2017-08-14T09:51:43.318289249Z", + "preread": "2017-08-14T09:51:42.318182309Z", + "pids_stats": { + "current": 1 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 7, + "minor": 0, + "op": "Read", + "value": 36864 + }, + { + "major": 7, + "minor": 0, + "op": "Write", + "value": 12288 + }, + { + "major": 7, + "minor": 0, + "op": "Sync", + "value": 49152 + }, + { + "major": 7, + "minor": 0, + "op": "Async", + "value": 0 + }, + { + "major": 7, + "minor": 0, + "op": "Total", + "value": 49152 + }, + { + "major": 253, + "minor": 1, + "op": "Read", + "value": 36864 + }, + { + "major": 253, + "minor": 1, + "op": "Write", + "value": 12288 + }, + { + "major": 253, + "minor": 1, + "op": "Sync", + "value": 49152 + }, + { + "major": 253, + "minor": 1, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 1, + "op": "Total", + "value": 49152 + }, + { + "major": 253, + "minor": 3, + "op": "Read", + "value": 46039040 + }, + { + "major": 253, + "minor": 3, + "op": "Write", + "value": 155648 + }, + { + "major": 253, + "minor": 3, + "op": "Sync", + "value": 46194688 + }, + { + "major": 253, + "minor": 3, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 3, + "op": "Total", + "value": 46194688 + } + ], + "io_serviced_recursive": [ + { + "major": 7, + "minor": 0, + "op": "Read", + "value": 6 + }, + { + "major": 7, + "minor": 0, + "op": "Write", + "value": 3 + }, + { + "major": 7, + "minor": 0, + "op": "Sync", + "value": 9 + }, + { + "major": 7, + "minor": 0, + "op": "Async", + "value": 0 + }, + { + "major": 7, + "minor": 0, + "op": "Total", + "value": 9 + }, + { + "major": 253, + "minor": 1, + "op": "Read", + "value": 6 + }, + { + "major": 253, + "minor": 1, + "op": "Write", + "value": 3 + }, + { + "major": 253, + "minor": 1, + "op": "Sync", + "value": 9 + }, + { + "major": 253, + "minor": 1, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 1, + "op": "Total", + "value": 9 + }, + { + "major": 253, + "minor": 3, + "op": "Read", + "value": 1075 + }, + { + "major": 253, + "minor": 3, + "op": "Write", + "value": 17 + }, + { + "major": 253, + "minor": 3, + "op": "Sync", + "value": 1092 + }, + { + "major": 253, + "minor": 3, + "op": "Async", + "value": 0 + }, + { + "major": 253, + "minor": 3, + "op": "Total", + "value": 1092 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + }, + "num_procs": 0, + "storage_stats": {}, + "cpu_stats": { + "cpu_usage": { + "total_usage": 5723669129, + "percpu_usage": [ + 1598666891, + 2737421240, + 999006836, + 388574162, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 570000000, + "usage_in_usermode": 5120000000 + }, + "system_cpu_usage": 111734320000000, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "precpu_stats": { + "cpu_usage": { + "total_usage": 4733027699, + "percpu_usage": [ + 1598666891, + 1746779810, + 999006836, + 388574162, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 550000000, + "usage_in_usermode": 4140000000 + }, + "system_cpu_usage": 111730350000000, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 218624000, + "max_usage": 218624000, + "stats": { + "active_anon": 85700608, + "active_file": 18706432, + "cache": 125739008, + "dirty": 57917440, + "hierarchical_memory_limit": 9223372036854771712, + "hierarchical_memsw_limit": 9223372036854771712, + "inactive_anon": 0, + "inactive_file": 107032576, + "mapped_file": 21954560, + "pgfault": 46060, + "pgmajfault": 259, + "pgpgin": 73949, + "pgpgout": 22318, + "recent_rotated_anon": 43222, + "recent_rotated_file": 4577, + "recent_scanned_anon": 43222, + "recent_scanned_file": 35290, + "rss": 85741568, + "rss_huge": 0, + "swap": 0, + "total_active_anon": 85700608, + "total_active_file": 18706432, + "total_cache": 125739008, + "total_dirty": 57917440, + "total_inactive_anon": 0, + "total_inactive_file": 107032576, + "total_mapped_file": 21954560, + "total_pgfault": 46060, + "total_pgmajfault": 259, + "total_pgpgin": 73949, + "total_pgpgout": 22318, + "total_rss": 85741568, + "total_rss_huge": 0, + "total_swap": 0, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "limit": 12283822080 + }, + "name": "/hungry_brown", + "id": "98a9cd0009984e61a24499aec44bd244078a6a09486782015b3966916eee5c08", + "networks": { + "eth0": { + "rx_bytes": 70507552, + "rx_packets": 51415, + "rx_errors": 0, + "rx_dropped": 0, + "tx_bytes": 1766606, + "tx_packets": 26470, + "tx_errors": 0, + "tx_dropped": 0 + } + } +} +] + # docker 1.11; docker inspect fedora inspect_image_data = [ { @@ -477,6 +1399,10 @@ global stats_data return iter([stats_data]) +def stats_response_1_13(*args, **kwargs): + global stats_1_13 + return iter(stats_1_13) + def mock(): try: @@ -488,5 +1414,5 @@ flexmock(client_class, containers=containers_response) flexmock(client_class, version=lambda *args, **kwargs: version_data) flexmock(client_class, top=lambda *args, **kwargs: top_data) - flexmock(client_class, stats=stats_response) + flexmock(client_class, stats=stats_response_1_13) flexmock(client_class, inspect_image=lambda *args, **kwargs: inspect_image_data) diff -Nru sen-0.5.1/tests/test_docker_backend.py sen-0.6.0/tests/test_docker_backend.py --- sen-0.5.1/tests/test_docker_backend.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/tests/test_docker_backend.py 2018-03-22 08:34:32.000000000 +0100 @@ -1,4 +1,5 @@ from sen.docker_backend import DockerBackend +from sen.util import calculate_cpu_percent2, calculate_cpu_percent from .real import image_data, mock, container_data @@ -51,3 +52,12 @@ operation = c0.stats() stats_stream = operation.response assert next(stats_stream) + + for x in c0.d.stats('x', decode=True, stream=True): + calculate_cpu_percent(x) + + t = 0.0 + s = 0.0 + for x in c0.d.stats('x', decode=True, stream=True): + calculate_cpu_percent(x) + _, s, t = calculate_cpu_percent2(x, t, s) diff -Nru sen-0.5.1/tests/test_util.py sen-0.6.0/tests/test_util.py --- sen-0.5.1/tests/test_util.py 2017-03-18 22:49:56.000000000 +0100 +++ sen-0.6.0/tests/test_util.py 2018-03-22 08:34:32.000000000 +0100 @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- +import logging import time from concurrent.futures.thread import ThreadPoolExecutor from datetime import datetime, timedelta -from flexmock import flexmock - from sen.tui.widgets.list.common import strip_from_ansi_esc_sequences from sen.util import _ensure_unicode, log_traceback, repeater, humanize_time, \ OrderedSet @@ -26,34 +25,37 @@ @log_traceback def f(): raise Exception() + caplog.set_level(logging.DEBUG) f() - assert caplog.records()[0].message.endswith(" is about to be started") - assert caplog.records()[1].message.startswith("Traceback") - assert caplog.records()[1].message.endswith("Exception\n") + assert caplog.records[0].message.endswith(" is about to be started") + assert caplog.records[1].message.startswith("Traceback") + assert caplog.records[1].message.endswith("Exception\n") def test_log_traceback_without_tb(caplog): @log_traceback def f(): pass + caplog.set_level(logging.DEBUG) f() - assert caplog.records()[0].message.endswith(" is about to be started") - assert caplog.records()[1].message.endswith(" finished") + assert caplog.records[0].message.endswith(" is about to be started") + assert caplog.records[1].message.endswith(" finished") def test_log_traceback_threaded(caplog): @log_traceback def f(): raise Exception() + caplog.set_level(logging.DEBUG) e = ThreadPoolExecutor(max_workers=1) f = e.submit(f) while f.running(): time.sleep(0.1) - assert caplog.records()[0].message.endswith(" is about to be started") - assert caplog.records()[1].message.startswith("Traceback") - assert caplog.records()[1].message.endswith("Exception\n") + assert caplog.records[0].message.endswith(" is about to be started") + assert caplog.records[1].message.startswith("Traceback") + assert caplog.records[1].message.endswith("Exception\n") # def test_log_vars_from_tback(caplog):
signature.asc
Description: OpenPGP digital signature