PK!dVPtmn/__init__.pyimport logging __version__ = '0.4.3' handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('[%(levelname)s] %(message)s')) logger = logging.getLogger('tmn') logger.addHandler(handler) logger.setLevel('CRITICAL') PK!Śtmn/configuration.pyimport logging import sys from clint import resources from eth_keys import keys from slugify import slugify import docker from tmn import display from tmn.elements.network import Network from tmn.elements.service import Service from tmn.elements.volume import Volume from tmn.environments import environments logger = logging.getLogger('tmn') resources.init('tomochain', 'tmn') class Configuration: """docstring for Configuration.""" def __init__(self, name: str = None, net: str = None, pkey: str = None, start: bool = False, docker_url: str = None, api: bool = False) -> None: self.networks = {} self.services = {} self.volumes = {} self.name = name self.net = net self.pkey = pkey or '' self.id = None self.force_recreate = False if not docker_url: self.client = docker.from_env() else: self.client = docker.DockerClient(base_url=docker_url) self.api = 'True' if api else 'False' try: self.client.ping() except Exception as e: logger.error(e) display.error_docker() sys.exit('\n') if resources.user.read('name'): self._load() elif start: self._write() else: display.error_start_not_initialized() sys.exit('\n') self._compose() def _get_address(self) -> str: pk = keys.PrivateKey(bytes.fromhex(self.pkey)) return pk.public_key.to_address()[2:] def _load(self) -> None: if self.name or self.net or self.pkey: display.warning_ignoring_start_options(resources.user.read('name')) self.id = resources.user.read('id') self.name = resources.user.read('name') self.net = resources.user.read('net') self.api = resources.user.read('api') ####################################################################### # this is a dirty fix for retro compatiblity # # can be removed in some future version # # old `tmn` don't write the `id` or `net` option to disk # # screw with `tmn update` # # will ask to recreate as this is a breaking change # ####################################################################### if not self.id or not self.net: self.force_recreate = True self.net = 'devnet' ####################################################################### def _write(self) -> None: if not self.name: display.error_start_option_required('--name') sys.exit('\n') elif not self.net: display.error_start_option_required('--net') sys.exit('\n') elif not self.pkey: display.error_start_option_required('--pkey') sys.exit('\n') self._validate() self.id = self._get_address() resources.user.write('id', self.id) resources.user.write('name', self.name) resources.user.write('net', self.net) resources.user.write('api', self.api) def _compose(self) -> None: self.networks['tmn'] = Network( name='{}_tmn'.format(self.name), client=self.client ) self.volumes['chaindata'] = Volume( name='{}_chaindata'.format(self.name), client=self.client ) if self.net == 'mainnet': tag = 'stable' elif self.net == 'testnet': tag = 'testnet' else: tag = 'latest' if self.api == 'True': # this is dirty, should be refactored tomochain_ports = {'30303/udp': 30303, '30303/tcp': 30303, '8545/tcp': 8545, '8546/tcp': 8546} else: tomochain_ports = {'30303/udp': 30303, '30303/tcp': 30303} self.services['metrics'] = Service( name='{}_metrics'.format(self.name), hostname='{}'.format(self.id), image='tomochain/telegraf:{}'.format(tag), network=self.networks['tmn'].name, volumes={ '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro' }, '/sys': {'bind': '/rootfs/sys', 'mode': 'ro'}, '/proc': {'bind': '/rootfs/proc', 'mode': 'ro'}, '/etc': {'bind': '/rootfs/etc', 'mode': 'ro'} }, client=self.client ) self.services['tomochain'] = Service( name='{}_tomochain'.format(self.name), image='tomochain/node:{}'.format( 'devnet' if tag == 'latest' else tag ), network=self.networks['tmn'].name, environment={ 'IDENTITY': '{}'.format(self.name), 'PRIVATE_KEY': '{}'.format(self.pkey) }, volumes={ self.volumes['chaindata'].name: { 'bind': '/tomochain', 'mode': 'rw' } }, ports=tomochain_ports, client=self.client ) for container, variables in environments[self.net].items(): for variable, value in variables.items(): self.services[container].add_environment( name=variable, value=value ) def _validate(self) -> None: self.name = slugify(self.name) if len(self.name) < 5 or len(self.name) > 30: display.error_validation_option('--name', '5 to 30 characters ' 'slug') sys.exit('\n') if len(self.pkey) != 64: display.error_validation_option('--pkey', '64 characters hex ' 'string') sys.exit('\n') try: bytes.fromhex(self.pkey) except ValueError: display.error_validation_option('--pkey', '64 characters hex ' 'string') sys.exit('\n') def remove(self) -> None: resources.user.delete('id') resources.user.delete('name') resources.user.delete('net') PK!3""tmn/display.pyimport sys import pastel pastel.add_style('hg', 'green') pastel.add_style('hgb', 'green', options=['bold']) pastel.add_style('hy', 'yellow') pastel.add_style('hyb', 'yellow', options=['bold']) pastel.add_style('link', 'yellow', options=['underscore']) pastel.add_style('und', options=['underscore']) pastel.add_style('warning', 'yellow') pastel.add_style('error', 'red') help_url = 'https://docs.tomochain.com/masternode/tmn/' def newline(number: int = 1) -> None: "Print newlines" print('\n'*number, end='') def style(function): "Print and colorize strings with `pastel`" def wrapper(*args, **kwargs) -> None: print(pastel.colorize(function(*args, **kwargs))) return wrapper def style_no_new_line(function): "Print and colorize strings with `pastel`. No newline." def wrapper(*args) -> None: print(pastel.colorize(function(*args)), end='', flush=True) return wrapper @style def link(msg: str, url: str) -> str: "Return a pastel formated string for browser links" return '{msg} {url}'.format( msg=msg, url=url ) def link_docs() -> None: "Custom link message for documentation" link('Documentation on running a masternode:', help_url) @style def title(msg: str) -> str: "Return a pastel formated title string" return '{msg}\n'.format( msg=msg ) def title_start_masternode(name: str) -> None: "Title when starting a masternode" title('Starting masternode {}:'.format(name)) def title_stop_masternode(name: str) -> None: "Title when stopping a masternode" title('Stopping masternode {}:'.format(name)) def title_status_masternode(name: str) -> None: "Title when viewing a masternode status" title('Masternode {} status:'.format(name)) def title_inspect_masternode(name: str) -> None: "Title when inspecting a masternode" title('Masternode {} details:'.format(name)) def title_update_masternode(name: str) -> None: "Title when updating a masternode" title('Updating masternode {}:'.format(name)) def title_remove_masternode(name: str) -> None: "Title when removing a masternode" title('Removing masternode {}:'.format(name)) @style def subtitle(msg: str) -> str: "Return a pastel formated subtitle string" return '{msg}\n'.format( msg=msg ) def subtitle_create_volumes() -> None: "Subtitle when creating volumes" subtitle('Volumes') def subtitle_remove_volumes() -> None: "Subtitle when removing volumes" subtitle('Volumes') def subtitle_create_networks() -> None: "Subtitle when creating networks" subtitle('Networks') def subtitle_remove_networks() -> None: "Subtitle when removing networks" subtitle('Networks') def subtitle_create_containers() -> None: "Subtitle when creating containers" subtitle('Containers') def subtitle_remove_containers() -> None: "Subtitle when removing containers" subtitle('Containers') @style def detail(msg, content: str, indent: int = 1) -> str: "Return a pastel formated detail" return (' '*indent + '{msg}:\n'.format(msg=msg) + ' '*indent + '{content}'.format(content=content)) def detail_identity(content: str) -> None: "Custom detail message for the masternode identity" detail('Unique identity', content) def detail_coinbase(content: str) -> None: "Custom detail message for the masternode coinbase address" detail('Coinbase address (account public key)', content) @style_no_new_line def step(msg: str, indent: int = 1) -> str: "Return a pastel formated step with indentation." step = ' '*indent + '- {msg}... '.format( msg=msg ) return step def step_create_volume(name: str) -> None: "Custom step message for docker volumes creation" step('Creating {name}'.format( name=name )) def step_remove_volume(name: str) -> None: "Custom step message for docker volumes removal" step('Removing {name}'.format( name=name )) def step_create_network(name: str) -> None: "Custom step message for docker networks creation" step('Creating {name}'.format( name=name )) def step_remove_network(name: str) -> None: "Custom step message for docker networks creation" step('Removing {name}'.format( name=name )) def step_create_container(name: str) -> None: "Custom step message for docker container creation" step('Creating {name}'.format( name=name )) def step_start_container(name: str) -> None: "Custom step message for docker container starting" step('Starting {name}'.format( name=name )) def step_remove_container(name: str) -> None: "Custom step message for docker container starting" step('Removing {name}'.format( name=name )) def step_stop_container(name: str) -> None: "Custom step message for docker container stopping" step('Stopping {name}'.format( name=name )) @style def step_close(msg: str, color: str = 'green') -> str: "Return a pastel formated end of step" return '{msg}'.format( msg=msg, color=color ) def step_close_ok() -> None: "Custom close message when all ok" msg = 'ok' if sys.stdout.encoding == 'UTF-8': msg = '✔' step_close(msg) def step_close_nok() -> None: "Custom close message when all ok" msg = 'error' if sys.stdout.encoding == 'UTF-8': msg = '✗' step_close(msg, 'red') @style def status(name: str = '', status: str = 'absent', id: str = '', status_color: str = 'red') -> str: "Return a pastel formated end of step" if id: return ' {name}\t{status}({id})'.format( name=name, status=status, color=status_color, id=id ) else: return ' {name}\t{status}'.format( name=name, status=status, color=status_color, ) @style def warning(msg: str, newline: bool = True) -> str: "Return a pastel formated string for warnings" before = '' if newline: before = '\n' return before + '! warning: {msg}\n'.format( msg=msg ) def warning_ignoring_start_options(name: str) -> None: "Custom warning when tmn is ignoring the start options" warning( 'masternode {} is already configured\n'.format(name) + ' ' + 'ignoring start options\n' ) def warning_remove_masternode(name: str) -> None: "Custom warning when tmn is removing masternode but no confirmation" warning( 'you are about to remove masternode {}\n'.format(name) + ' ' + 'this will permanently delete its data\n' + ' ' + 'to confirm use the --confirm flag' ) @style def error(msg: str) -> str: "Return a pastel formated string for errors" return ( '\n! error: {msg}\n'.format(msg=msg) + ' ' + 'need help? {}'.format(help_url) ) def error_docker() -> None: "Custom error when docker is not accessible" error('could not access the docker daemon') def error_docker_api() -> None: "Custom error when docker is not accessible" error('something went wrong while doing stuff with docker') def error_start_not_initialized() -> None: "Custom error when `tmn start` has never been used with `--name` option" error( 'tmn doesn\'t manage any masternode yet\n' ' please use ' 'tmn start --name to get started' ) def error_start_option_required(option: str) -> None: "Custom error when `tmn start` is used with name but not the other options" error( '{} is required when starting a new masternode' .format(option) ) def error_validation_option(option: str, format: str) -> None: "Custom error when an option format is not valide" error( '{} is not valid\n'.format(option) + ' it should be a {}'.format(format) ) def error_breaking_change() -> None: "Custom error when breaking changes need to recreate the node" error( 'latest update introduced some non-retrocompatible changes\n' ' ' 'please recreate your node by deleting it\n' ' ' 'tmn remove --confirm\n' ' ' 'and creating it back with the same options as the old one\n' ' ' 'tmn start --name ... --net ... --pkey ...' ) PK!kKtmn/elements/__init__.pyfrom tmn.elements.network import Network from tmn.elements.service import Service from tmn.elements.volume import Volume __all__ = ['Network', 'Service', 'Volume'] PK!`Htmn/elements/network.pyimport logging import docker logger = logging.getLogger('tmn') class Network: """docstring for Network.""" def __init__(self, client: docker.DockerClient, name: str) -> None: self.name = name self.network = None self.client = client try: self.network = self.client.networks.get(self.name) except docker.errors.NotFound as e: logger.debug('network {} not yet created ({})' .format(self.name, e)) except docker.errors.APIError as e: logger.error(e) def create(self) -> bool: "create docker network" try: if self.network: return True else: self.client.networks.create(self.name) return True except docker.errors.APIError as e: logger.error(e) except docker.errors.ConnectionRefusedError as e: logger.error(e) def remove(self) -> bool: "delete docker network" try: if self.network: self.network.remove() self.network = None return True else: return True except docker.errors.APIError as e: logger.error(e) return False PK!ϙtmn/elements/service.pyimport logging from typing import Dict, Union import docker logger = logging.getLogger('tmn') class Service: """docstring for Service.""" def __init__( self, client: docker.DockerClient, name: str, image: str = None, hostname: str = None, network: str = None, environment: Dict[str, str] = {}, volumes: Dict[str, Dict[str, str]] = {}, ports: Dict[str, Dict[str, str]] = {}, log_driver: str = 'json-file', log_opts: Dict[str, str] = {'max-size': '3g'} ) -> None: self.container = False self.image = image self.name = name self.environment = environment self.network = network self.hostname = hostname self.volumes = volumes self.ports = ports self.log_driver = log_driver self.log_opts = log_opts self.client = client try: self.container = self.client.containers.get(self.name) except docker.errors.NotFound as e: logger.debug('container {} not yet created ({})' .format(self.name, e)) except docker.errors.APIError as e: logger.error(e) def add_environment(self, name: str, value: str) -> None: "add a new environment to the service" self.environment[name] = value def add_volume(self, source: str, target: str, mode: str = 'rw') -> None: "add a new volume to the service" self.volumes[source] = {'bind': target, 'mode': mode} def add_port(self, source: str, target: str) -> None: "add a new port mapping to the service" self.ports[source] = target def create(self) -> bool: "create the service container" try: if self.container: return True else: self.client.images.pull(self.image) self.container = self.client.containers.create( image=self.image, name=self.name, hostname=self.hostname, network=self.network, environment=self.environment, volumes=self.volumes, ports=self.ports, log_config={'type': self.log_driver, 'config': self.log_opts}, detach=True ) return True except docker.errors.APIError as e: logger.error(e) return False def start(self) -> bool: "start the service container" try: if self.container: self.container.reload() if self.container.status in ['running', 'restarting']: return True elif self.container.status in ['paused']: self.container.unpause() return True else: self.container.start() return True else: return False except docker.errors.APIError as e: logger.error(e) return False def status(self) -> Union[str, bool]: "return the status of the service container" try: if self.container: self.container.reload() return self.container.status else: return 'absent' except docker.errors.APIError as e: logger.error(e) return False def execute(self, command: str) -> Union[str, bool]: "return the result of a command on the service container" try: if self.container: self.container.reload() return self.container.exec_run( '/bin/sh -c "{}"'.format(command) ).output.decode("utf-8") else: return False except docker.errors.APIError as e: logger.error(e) return False def stop(self) -> bool: "stop the service container" try: if self.container: self.container.reload() if self.container.status in ['created', 'exited', 'dead']: return True else: self.container.stop() return True else: return True except docker.errors.APIError as e: logger.error(e) return False def remove(self) -> bool: "stop the service container" try: if self.container: self.container.remove(force=True) self.container = None return True else: return True except docker.errors.APIError as e: logger.error(e) return False def update(self) -> bool: "update the service container" try: if self.container: self.container.remove(force=True) return self.create() and self.start() else: return False except docker.errors.APIError as e: logger.error(e) return False PK!Ctmn/elements/volume.pyimport logging import docker logger = logging.getLogger('tmn') class Volume: """docstring for Volume.""" def __init__(self, client: docker.DockerClient, name: str) -> None: self.name = name self.volume = None self.client = client try: self.volume = self.client.volumes.get(self.name) except docker.errors.NotFound as e: logger.debug('volume {} not yet created ({})' .format(self.name, e)) except docker.errors.APIError as e: logger.error(e) def create(self) -> bool: "create docker volumes" try: if self.volume: return True else: self.client.volumes.create(self.name) return True except docker.errors.APIError as e: logger.error(e) return False def remove(self) -> bool: "delete docker volume" try: if self.volume: self.volume.remove(force=True) self.volume = None return True else: return True except docker.errors.APIError as e: logger.error(e) return False PK!)/eg tmn/environments.pyenvironments = { 'mainnet': { 'tomochain': { 'BOOTNODES': ( 'enode://97f0ca95a653e3c44d5df2674e19e9324ea4bf4d47a46b1d8560f' '3ed4ea328f725acec3fcfcb37eb11706cf07da669e9688b091f1543f89b24' '25700a68bc8876@104.248.98.78:30301,enode://b72927f349f3a27b78' '9d0ca615ffe3526f361665b496c80e7cc19dace78bd94785fdadc270054ab' '727dbb172d9e3113694600dd31b2558dd77ad85a869032dea@188.166.207' '.189:30301,enode://c8f2f0643527d4efffb8cb10ef9b6da4310c5ac9f2' 'e988a7f85363e81d42f1793f64a9aa127dbaff56b1e8011f90fe9ff57fa02' 'a36f73220da5ff81d8b8df351@104.248.98.60:30301' ), 'NETSTATS_HOST': 'stats.tomochain.com', 'NETSTATS_PORT': '443', 'NETWORK_ID': '88', 'WS_SECRET': ( 'getty-site-pablo-auger-room-sos-blair-shin-whiz-delhi' ) }, 'metrics': { 'METRICS_ENDPOINT': 'https://metrics.tomochain.com' } }, 'testnet': { 'tomochain': { 'BOOTNODES': ( 'enode://4d3c2cc0ce7135c1778c6f1cfda623ab44b4b6db55289543d48ec' 'fde7d7111fd420c42174a9f2fea511a04cf6eac4ec69b4456bfaaae0e5bd2' '36107d3172b013@52.221.28.223:30301,enode://298780104303fcdb37' 'a84c5702ebd9ec660971629f68a933fd91f7350c54eea0e294b0857f1fd2e' '8dba2869fcc36b83e6de553c386cf4ff26f19672955d9f312@13.251.101.' '216:30301,enode://46dba3a8721c589bede3c134d755eb1a38ae7c5a4c6' '9249b8317c55adc8d46a369f98b06514ecec4b4ff150712085176818d18f5' '9a9e6311a52dbe68cff5b2ae@13.250.94.232:30301' ), 'NETSTATS_HOST': 'stats.testnet.tomochain.com', 'NETSTATS_PORT': '443', 'NETWORK_ID': '89', 'WS_SECRET': ( 'anna-coal-flee-carrie-zip-hhhh-tarry-laue-felon-rhine' ) }, 'metrics': { 'METRICS_ENDPOINT': 'https://metrics.testnet.tomochain.com' } }, 'devnet': { 'tomochain': { 'BOOTNODES': ( 'enode://5bec42d41c9eb291c1d20c9ac92bd9c86a4954189b6592b0833e5' 'c28e389b59e3992efed119a2782d9b95ba7aa78e7f71813067cd6734fadff' '322f7dd6fc3b3c@104.248.99.234:30301,enode://89028bc15e9dda643' 'bc4b9a1a6352896dd3bce7411543b0b160a9eb95093ddbe1f5eda5999e38a' '4874bfa6a00fb3526cc2fb9b4feb2a3f7cc80ef8016e05c493@104.248.99' '.235:30301,enode://ea8f1eb1a2a695960bfa6df52094c635e173c65e5f' 'c120501672c0d21900d826d6c1c5a07d64ad36509ec5e7306d7a2c3398398' 'f34f3e279b91c487c2b3a9537@104.248.99.233:30301' ), 'NETSTATS_HOST': 'stats.devnet.tomochain.com', 'NETSTATS_PORT': '443', 'NETWORK_ID': '90', 'WS_SECRET': ( 'torn-fcc-caper-drool-jelly-zip-din-fraud-rater-darn' ) }, 'metrics': { 'METRICS_ENDPOINT': 'https://metrics.devnet.tomochain.com' } } } PK!]!! tmn/tmn.pyimport logging import sys import click from tmn import display from tmn import __version__ from tmn.configuration import Configuration logger = logging.getLogger('tmn') docker_url = None @click.group(help=('Tomo MasterNode (tmn) is a cli tool to help you run a Tomo' 'chain masternode')) @click.option('--debug', is_flag=True, help='Enable debug mode') @click.option('--docker', metavar='URL', help='Url to the docker server') @click.version_option(version=__version__) def main(debug: bool, docker: str) -> None: "Cli entrypoint" global docker_url if debug: logger.setLevel('DEBUG') logger.debug('Debugging enabled') docker_url = docker @click.command(help='Display TomoChain documentation link') def docs() -> None: "Link to the documentation" display.link_docs() @click.command(help='Start your TomoChain masternode') @click.option('--name', metavar='NAME', help='Your masternode\'s name') @click.option('--net', type=click.Choice(['mainnet', 'testnet', 'devnet']), help='The environment your masternode will connect to') @click.option('--pkey', metavar='KEY', help=('Private key of the account your ' 'masternode will collect rewards ' 'on')) @click.option('--api', is_flag=True) def start(name: str, net: str, pkey: str, api: bool) -> None: "Start the containers needed to run a masternode" configuration = Configuration(name=name, net=net, pkey=pkey, start=True, docker_url=docker_url, api=api) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_start_masternode(configuration.name) # volumes display.subtitle_create_volumes() for _, value in configuration.volumes.items(): display.step_create_volume(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # networks display.subtitle_create_networks() for _, value in configuration.networks.items(): display.step_create_network(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # container # create display.subtitle_create_containers() for _, value in configuration.services.items(): display.step_create_container(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # start for _, value in configuration.services.items(): display.step_start_container(value.name) if value.start(): display.step_close_ok() else: display.step_close_nok() display.newline() @click.command(help='Stop your TomoChain masternode') def stop() -> None: "Stop the masternode containers" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_stop_masternode(configuration.name) for _, service in configuration.services.items(): display.step_stop_container(service.name) if service.stop(): display.step_close_ok() else: display.step_close_nok() display.newline() @click.command(help='Show the status of your TomoChain masternode') def status() -> None: "Show the status of the masternode containers" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_status_masternode(configuration.name) for _, service in configuration.services.items(): status = service.status() if status and status == 'absent': display.status( name=service.name ) if status and status in ['running']: display.status( name=service.name, status=status, id=service.container.short_id, status_color='green' ) elif status: display.status( name=service.name, status=status, id=service.container.short_id, ) else: display.status( name=service.name, status='error' ) display.newline() @click.command(help='Show details about your TomoChain masternode') def inspect() -> None: "Show details about the tomochain masternode" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_inspect_masternode(configuration.name) identity = configuration.services['tomochain'].execute( 'echo $IDENTITY' ) or 'container not running' display.detail_identity(identity) display.newline() coinbase = configuration.services['tomochain'].execute( 'tomo account list --keystore keystore 2> /dev/null | head -n 1 | cut ' '-d"{" -f 2 | cut -d"}" -f 1' ) if coinbase: coinbase = '0x{}'.format(coinbase) else: coinbase = 'container not running' display.detail_coinbase(coinbase) display.newline() @click.command(help='Update your masternode') def update() -> None: "Update the tomochain masternode with the lastest images" configuration = Configuration(docker_url=docker_url) if configuration.force_recreate: display.error_breaking_change() sys.exit('\n') display.title_update_masternode(configuration.name) display.subtitle_remove_containers() # containers # stop for _, service in configuration.services.items(): display.step_stop_container(service.name) if service.stop(): display.step_close_ok() else: display.step_close_nok() display.newline() # remove for _, service in configuration.services.items(): display.step_remove_container(service.name) if service.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() # create for _, value in configuration.services.items(): display.step_create_container(value.name) if value.create(): display.step_close_ok() else: display.step_close_nok() display.newline() # start for _, value in configuration.services.items(): display.step_start_container(value.name) if value.start(): display.step_close_ok() else: display.step_close_nok() display.newline() @click.command(help='Remove your TomoChain masternode') @click.option('--confirm', is_flag=True) def remove(confirm: bool) -> None: "Remove the masternode (containers, networks volumes)" configuration = Configuration(docker_url=docker_url) if not confirm: display.warning_remove_masternode(configuration.name) sys.exit('\n') display.title_remove_masternode(configuration.name) display.subtitle_remove_containers() # containers # stop for _, service in configuration.services.items(): display.step_stop_container(service.name) if service.stop(): display.step_close_ok() else: display.step_close_nok() display.newline() # remove for _, service in configuration.services.items(): display.step_remove_container(service.name) if service.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() # networks display.subtitle_remove_networks() for _, network in configuration.networks.items(): display.step_remove_network(network.name) if network.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() # volumes display.subtitle_remove_volumes() for _, volume in configuration.volumes.items(): display.step_remove_volume(volume.name) if volume.remove(): display.step_close_ok() else: display.step_close_nok() display.newline() configuration.remove() main.add_command(docs) main.add_command(start) main.add_command(stop) main.add_command(status) main.add_command(inspect) main.add_command(update) main.add_command(remove) PK!H #$$tmn-0.4.3.dist-info/entry_points.txtN+I/N.,()*ͳb= MPK!HnHTUtmn-0.4.3.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H "tmn-0.4.3.dist-info/METADATAXmoH_K\ӵ4$wiMCPcylEΌ߇3[f^H|HBz1^){I.E){Kd?f{m]6=VzjLmKP&L)Jaa}zݮ7'B,7err^^{ɿed̙dgP1^.w Gۣ^I%]ٛTB=VJ^7Fh{[w\HIn}^n訪!ߟ^өҊAtB[z{?lflEY*=s5`bOZ'^tlО%v՛N!RơGDalgt"jrT Q2]V(7}K)hqwX%uE=Vd 'sߜu)Й5uJuuEtW8m6OAН[bDܪOL8>k*nLq}'$ܩ/hb`c0<v7U9&Q>ذj<lPXl>:3Y1ta|wV|BnOpqV\w>MSZų<]a՜1**3v@©ێ͝<( )ӵ67f"`ؼ?3UE3 hn&Y(+pI} 8)3/ld ›Qk!-(ܮPsѳMk p~.Z}6T#cpxPDXEy}ct@ 1p6Hz;DWG'ptsz)ˠEWUi:zLFJJ q[d&ŬǨRg<䄙D>+g׊"R:dI69Fp JB4K93!⩐!VxU6yCsnKq(KEy/a>1?3[xImj{O>(_u>rQxuK*tR*&LYD,9Q`4Ԙ-8Y>8D/PxG#a-R1/1"I`j5GΕP1}xe>]  PSO *8@H=UzP%҃jF˰*+<7EZjbcG#Z@ԥXDzNliد[Ăj'RKZFkc .k |D,t3bbM%MFҾfeH~ױHAnȸxwPQy[|BƵAVKBҵGzbg^N=w)I>tWU?5HVc1db-nl(!mG֔aC/0AwPK!H$otmn-0.4.3.dist-info/RECORDmK@}p*^Yҭ(BÆ@P<~l'hҋrn!e'pI,3ե! 嘞Lʋ7gh&ќ0#guTWmHp]=YWO !g=y  ĝ1.)qw-a݃1}]/6`8AιhoS1&.2Hۍ5kȅON=4j$u!-$!nzbիb2m&12߅|9 ݰݺHgp/De_.n'Ot<E`&\.Kcy ޹woX@ω//黩e,z>1*zxLO]ǘɺ+9rPUnA>Y_a@Xf\Jl#kΕtS)1úXE": Z9ڝ/6%ف.|w L(I7A3 4K}G9NokoahebUn*knZA$ FU?1gњVmkOnq:w%zj+kV~ȮFj#QΞ:aGҒoiX{z %Y5Օa)PK!dVPtmn/__init__.pyPK!Śtmn/configuration.pyPK!3""Vtmn/display.pyPK!kKZ=tmn/elements/__init__.pyPK!`H5>tmn/elements/network.pyPK!ϙCtmn/elements/service.pyPK!CWXtmn/elements/volume.pyPK!)/eg k]tmn/environments.pyPK!]!! jtmn/tmn.pyPK!H #$$tmn-0.4.3.dist-info/entry_points.txtPK!HnHTUttmn-0.4.3.dist-info/WHEELPK!H "tmn-0.4.3.dist-info/METADATAPK!H$otmn-0.4.3.dist-info/RECORDPK s