├── .gitignore ├── .pystartup ├── Dockerfile ├── README.md ├── bin └── capsule ├── capsule ├── __init__.py ├── capsule.py ├── container.py └── image.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.ipynb 3 | *.pyc 4 | -------------------------------------------------------------------------------- /.pystartup: -------------------------------------------------------------------------------- 1 | import atexit 2 | import os 3 | import readline 4 | import rlcompleter 5 | 6 | historyPath = os.path.expanduser("~/.pyhistory") 7 | 8 | def save_history(historyPath=historyPath): 9 | import readline 10 | readline.write_history_file(historyPath) 11 | 12 | if os.path.exists(historyPath): 13 | readline.read_history_file(historyPath) 14 | 15 | atexit.register(save_history) 16 | del os, atexit, readline, rlcompleter, save_history, historyPath 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:15.10 2 | 3 | RUN apt-get update && apt-get dist-upgrade -y && apt-get upgrade -y 4 | 5 | RUN apt-get install python python-pip nano -y 6 | 7 | ENV PYTHONSTARTUP=/root/.pystartup 8 | 9 | ADD [".pystartup", "/root/.pystartup"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This package is meant to help manage development environments using docker, while providing an interface similar to that of python's `virtualenvwrapper` module. 2 | 3 | **Note:** This code is in alpha stage. You're free to try it out and provide feedback, but keep in mind it will undergo heavy changes before it's production ready. 4 | 5 | [![Stories in Ready](https://badge.waffle.io/tryexceptpass/capsule.png?label=ready&title=Ready)](https://waffle.io/tryexceptpass/capsule) 6 | 7 | ## Features 8 | * Simple cli to create, switch to and delete environments that allow you to fiddle to your heart's content without breaking anything else. 9 | * Currently this is based on a clean Ubuntu install (`ubuntu:latest` docker [image](https://hub.docker.com/_/ubuntu/)) with basic python 2.7 configured. 10 | * Save your environment to a tarfile in case you want to take it with you anywhere. 11 | * Export the python history of anything you tried while using the python interpreter within your environment into an iPythonNotebook file that can be used with any Jupyter server (like the `jupyter/notebook` docker [image](https://hub.docker.com/r/jupyter/notebook/)). 12 | 13 | ## Install 14 | 1. Setup and install [docker](http://docs.docker.com/linux/started/) or [docker-machine](https://www.docker.com/docker-toolbox) on your computer. 15 | 2. `git clone` this repo. 16 | 3. `python setup.py install` 17 | 18 | ## Usage 19 | The setup script installs a `capsule` shell command that provides the interface described below. Any environment you create is essentially a new docker container and will therefore maintain state next time you work on it. The `save` and `load` commands essentially export / import the environment to a tarfile. 20 | 21 | The environment is an Ubuntu install with python 2.7 by default (I'll provide a python 3 option soon) and is configured to keep track of your python history when using the interpreter. This allows us to automatically export any experiments you run within the python interpreter to an iPythonNotebook through the `pyhistory` command. 22 | 23 | ``` 24 | Manage capsule environments. 25 | 26 | Usage: 27 | capsule make [options] 28 | capsule workon [options] 29 | capsule remove [options] 30 | capsule list [options] 31 | capsule save [options] 32 | capsule load 33 | capsule pyhistory 34 | 35 | Options: 36 | --baseimage 37 | 38 | --debug Print debug messages. 39 | -h --help Show this screen. 40 | --version Show version. 41 | ``` 42 | 43 | Note: please be patient the first time you run it, as it will take a little bit while we run a docker build to download and create the first container. 44 | -------------------------------------------------------------------------------- /bin/capsule: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from capsule import Capsule 4 | from docopt import docopt 5 | 6 | import logging 7 | 8 | DOCS = """Manage capsule environments. 9 | 10 | Usage: 11 | capsule make [options] 12 | capsule workon [options] 13 | capsule remove [options] 14 | capsule list [options] 15 | capsule pyhistory 16 | 17 | Options: 18 | --baseimage 19 | 20 | --debug Print debug messages. 21 | -h --help Show this screen. 22 | --version Show version. 23 | """ 24 | 25 | VERSION = "0.2" 26 | 27 | if __name__ == '__main__': 28 | logging.basicConfig(level=logging.WARNING, format='[%(levelname)s] %(asctime)s %(message)s') 29 | logging.getLogger('requests').setLevel(logging.CRITICAL) 30 | 31 | arguments = docopt(DOCS, version=VERSION) 32 | 33 | if arguments['--debug']: 34 | logging.getLogger().setLevel(logging.DEBUG) 35 | logging.getLogger('docker').setLevel(logging.DEBUG) 36 | else: 37 | logging.getLogger('docker').setLevel(logging.WARNING) 38 | 39 | if arguments['list']: 40 | cap = Capsule(None) 41 | for c in cap.list(): 42 | print(c) 43 | elif arguments['make']: 44 | cap = Capsule(arguments['']) 45 | cap.start() 46 | cap.stop() 47 | elif arguments['workon']: 48 | cap = Capsule(arguments['']) 49 | cap.start() 50 | print("Press Enter to activate this capsule...") 51 | cap.activate() 52 | elif arguments['remove']: 53 | cap = Capsule(arguments['']) 54 | cap.remove() 55 | elif arguments['pyhistory']: 56 | cap = Capsule(arguments['']) 57 | cap.pyhistory() 58 | -------------------------------------------------------------------------------- /capsule/__init__.py: -------------------------------------------------------------------------------- 1 | from .capsule import Capsule 2 | from .image import DockerImage 3 | from .container import DockerContainer 4 | -------------------------------------------------------------------------------- /capsule/capsule.py: -------------------------------------------------------------------------------- 1 | """Capsule helps create isloated development environments using Docker containers""" 2 | 3 | from docker import Client 4 | from docker.utils import kwargs_from_env 5 | 6 | from .image import DockerImage 7 | from .container import DockerContainer 8 | 9 | import logging 10 | import subprocess 11 | 12 | import tarfile 13 | import json 14 | 15 | class Capsule(object): 16 | baseimage = None 17 | environment = None 18 | 19 | client = None 20 | 21 | def __init__(self, name, baseimage="tryexceptpass/capsule", basetag="base"): 22 | """Make a new capsule environment""" 23 | 24 | logging.debug("Initializing") 25 | 26 | self.client = Client(**kwargs_from_env()) 27 | logging.debug("Connected client to docker socket") 28 | 29 | self.baseimage = DockerImage(baseimage, basetag, self.client) 30 | 31 | if name is None: 32 | return 33 | 34 | if self.baseimage.exists(): 35 | logging.info("Latest image is already stored locally") 36 | else: 37 | baseos = DockerImage(self.client, 'ubuntu', 'latest') 38 | if baseos.exists(): 39 | #self.baseimage = baseos.addtag('capsule', 'base') 40 | self.baseimage.build(".") 41 | else: 42 | logging.info("Latest image does not exist locally, will have to download") 43 | baseos.download() 44 | logging.debug("Image downloaded") 45 | self.baseimage.build(".") 46 | #self.baseimage = baseos.addtag('capsule', 'base') 47 | 48 | self.environment = DockerContainer(self.baseimage, name, client=self.client) 49 | 50 | def start(self): 51 | """Start this capsule environment""" 52 | 53 | self.environment.run() 54 | 55 | def stop(self): 56 | """Stop this capsule environment""" 57 | 58 | self.environment.stop() 59 | 60 | def remove(self): 61 | """Remove this capsule environment""" 62 | 63 | self.environment.remove() 64 | 65 | def activate(self): 66 | """Opens the environment for work""" 67 | 68 | self.environment.attach() 69 | 70 | def list(self): 71 | """List available capsule environments""" 72 | 73 | capsules = [] 74 | containers = self.client.containers(all=True) 75 | for container in containers: 76 | if container['Image'] == str(self.baseimage): 77 | capsules.append(container['Names'][0][1:]) 78 | 79 | return capsules 80 | 81 | def pyhistory(self): 82 | """Get python history for this capsule""" 83 | 84 | tar = tarfile.open(fileobj=self.environment.copy("/root/.pyhistory")) 85 | cells = [] 86 | for line in str(tar.extractfile(".pyhistory").read(), 'utf-8').split('\n'): 87 | cells.append({ "cell_type": "code", 88 | "execution_count": None, 89 | "metadata": { "collapsed": True }, 90 | "outputs": [], 91 | "source": [ line ] 92 | }) 93 | 94 | ipynb = { "cells": cells, 95 | "metadata": { 96 | "kernelspec": { 97 | "display_name": "Python 2", 98 | "language": "python", 99 | "name": "python2" 100 | }, 101 | "language_info": { 102 | "codemirror_mode": { 103 | "name": "ipython", 104 | "version": 2 105 | }, 106 | "file_extension": ".py", 107 | "mimetype": "text/x-python", 108 | "name": "python", 109 | "nbconvert_exporter": "python", 110 | "pygments_lexer": "ipython2", 111 | "version": "2.7.6" 112 | } 113 | }, 114 | "nbformat": 4, 115 | "nbformat_minor": 0 116 | } 117 | 118 | with open(self.environment.name + ".ipynb", 'wb') as f: 119 | f.write(bytes(json.dumps(ipynb), 'utf-8')) 120 | -------------------------------------------------------------------------------- /capsule/container.py: -------------------------------------------------------------------------------- 1 | from docker.utils import create_host_config 2 | 3 | import logging 4 | import subprocess 5 | 6 | import io 7 | 8 | class DockerContainer(object): 9 | """Simple representation of a docker container as defined by a name and parent image""" 10 | 11 | def __init__(self, image, name, config=None, client=None): 12 | self.image = image 13 | self.name = name 14 | self.client = client 15 | 16 | self.container = None 17 | self.config = None 18 | 19 | def __repr__(self): 20 | return "" 21 | 22 | def __str__(self): 23 | return self.name + "<" + str(self.image) + ">" 24 | 25 | def sethostconfig(self, **kwargs): 26 | """Set the host configuration when creating this container. Keyword arguments include: 27 | * binds: bind host directory to container. 28 | * links: link to another container. 29 | * port_bindings: expose container ports to the host. 30 | * lxc_conf 31 | * priviledged 32 | * dns 33 | * volumes_from 34 | * network_mode: bridge, none, container[id|name] or host. 35 | * restart_policy 36 | ... 37 | There are several more at https://docker-py.readthedocs.io/en/stable/hostconfig/ 38 | """ 39 | 40 | self.config = create_host_config(kwargs) 41 | 42 | def run(self, command='/bin/bash', client=None): 43 | """Run the container and, if provided, execute the command""" 44 | 45 | if client is None: 46 | client = self.client 47 | 48 | status = None 49 | container = None 50 | 51 | if self.container: 52 | container = self._findcontainerbyid(self.container['Id']) 53 | else: 54 | container = self._findcontainerbyname(self.name) 55 | 56 | if container is not None: 57 | self.container = container 58 | status = container['Status'] 59 | logging.debug("Container %s already exists", self.name) 60 | else: 61 | if self.config: 62 | self.container = client.create_container(name=self.name, command=command, image=str(self.image), tty=True, stdin_open=True, host_config=self.config) 63 | else: 64 | self.container = client.create_container(name=self.name, command=command, image=str(self.image), tty=True, stdin_open=True) 65 | logging.debug("Container %s built from %s image", self.name, str(self.image)) 66 | 67 | if status is None: 68 | self.start() 69 | elif 'Exited' in status: 70 | self.restart() 71 | 72 | logging.debug("Started %s", self.name) 73 | 74 | def _findcontainerbyname(self, name, client=None): 75 | if client is None: 76 | client = self.client 77 | 78 | for container in client.containers(all=True): 79 | if '/' + name in container['Names']: 80 | return container 81 | 82 | return None 83 | 84 | def _findcontainerbyid(self, ident, client=None): 85 | if client is None: 86 | client = self.client 87 | 88 | for container in client.containers(all=True): 89 | if ident == container['Id']: 90 | return container 91 | 92 | return None 93 | 94 | def inspect(self, client=None): 95 | """Retrieve a dictionary with container details""" 96 | 97 | if client is None: 98 | client = self.client 99 | 100 | return client.inspect_container(container=self.name) 101 | 102 | def stop(self, timeout=10, client=None): 103 | """Stop a running container""" 104 | 105 | if client is None: 106 | client = self.client 107 | 108 | client.stop(container=self.container['Id'], timeout=timeout) 109 | 110 | def remove(self, volumes=True, force=True, client=None): 111 | """Remove the container""" 112 | 113 | if client is None: 114 | client = self.client 115 | 116 | if self.container is None: 117 | container = self._findcontainerbyname(self.name) 118 | 119 | if container: 120 | self.container = container 121 | else: 122 | return 123 | 124 | client.remove_container(container=self.container['Id'], force=force, v=volumes) 125 | 126 | def restart(self, client=None): 127 | """Restart the running container""" 128 | 129 | if client is None: 130 | client = self.client 131 | 132 | client.restart(container=self.container['Id']) 133 | 134 | def start(self, client=None): 135 | """Start the container""" 136 | 137 | if client is None: 138 | client = self.client 139 | 140 | client.start(container=self.container['Id']) 141 | 142 | def attach(self): 143 | """Attach to the running container""" 144 | 145 | subprocess.Popen(['docker', 'attach', self.container['Id']]).communicate() 146 | 147 | def copy(self, resource, client=None): 148 | """Copy the container""" 149 | 150 | if client is None: 151 | client = self.client 152 | 153 | if (self.container is None): 154 | self.run() 155 | 156 | return io.BytesIO(client.copy(container=self.container['Id'], resource=resource).read(cache_content=False)) 157 | -------------------------------------------------------------------------------- /capsule/image.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class DockerImage(object): 4 | """Simple representation of a docker image as defined by the repo and tag fields""" 5 | 6 | def __init__(self, repo, tag, client=None): 7 | self.repo = repo 8 | self.tag = tag 9 | 10 | self.client = client 11 | 12 | def __repr__(self): 13 | return "" 14 | 15 | def __str__(self): 16 | return self.repo + ":" + self.tag 17 | 18 | def exists(self, client=None): 19 | """Test whether this image exists in the docker client""" 20 | 21 | if client is None: 22 | client = self.client 23 | 24 | return str(self) in [img for sublist in client.images() for img in sublist['RepoTags']] 25 | 26 | def download(self, client=None): 27 | """Download this image using the docker client""" 28 | 29 | if client is None: 30 | client = self.client 31 | 32 | logging.debug("Downloading %s", self) 33 | [logging.debug(line) for line in client.pull(repository=self.repo, tag=self.tag)] 34 | 35 | if self.exists(): 36 | logging.debug("Download complete") 37 | else: 38 | logging.error("Something went wrong while downloading %s", self) 39 | 40 | def build(self, dockerfile, client=None): 41 | """Build this image given a dockerfile string""" 42 | 43 | if client is None: 44 | client = self.client 45 | 46 | [logging.debug(line) for line in client.build(path=dockerfile, rm=True, tag=str(self))] 47 | 48 | def addtag(self, repo, tag, client=None): 49 | """Give this image another tag""" 50 | 51 | if client is None: 52 | client = self.client 53 | 54 | client.tag(image=str(self), repository=repo, tag=tag) 55 | 56 | return DockerImage(client, repo, tag) 57 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | author = "tryexceptpass", 5 | author_email = "tryexceptpass@users.noreply.github.com", 6 | 7 | name = "capsule", 8 | version = "0.2.1", 9 | description = "Manage development environments like virtualenv but with docker containers and export your python history to iPythonNotebook", 10 | long_description="This package is meant to help manage development environments using docker, while providing an interface similar to that of python's virtualenvwrapper module.\n\nFeatures:\n\n* Simple cli to create, switch to and delete environments that allow you to fiddle to your heart's content without breaking anything else.\n\n* Currently this is based on a clean Ubuntu install (ubuntu:latest docker image) with basic python 2.7 configured.\n\n* Save your environment to a tarfile in case you want to take it with you anywhere.\n\n* Export the python history of anything you tried while using the python interpreter within your environment into a JupyterNotebook file that can be used with any Jupyter server (like the jupyter/notebook docker image).", 11 | 12 | url = "https://github.com/tryexceptpass/capsule", 13 | 14 | packages = find_packages(), 15 | 16 | install_requires = [ 'docopt', 'docker-py' ], 17 | 18 | license = "MIT", 19 | classifiers = [ 'License :: OSI Approved :: MIT License', 20 | 21 | 'Topic :: Software Development', 22 | 'Topic :: Scientific/Engineering', 23 | 'Topic :: System', 24 | 'Topic :: Software Development :: Testing', 25 | 'Topic :: Utilities', 26 | 27 | 'Development Status :: 4 - Beta', 28 | 29 | 'Framework :: IPython', 30 | ], 31 | keywords = [ 'docker', 'virtualenv', 'environments', 'ipynb', 'ipythonnotebook', 'jupyternotebook' ], 32 | 33 | scripts = ["bin/capsule"] 34 | ) 35 | --------------------------------------------------------------------------------