PK!`tmn/__init__.py__version__ = '0.1.1' PK!/tmn/compose.pyimport uuid from tmn import configuration volumes = ['chaindata'] networks = ['tmn_default'] environment = { } containers = { 'metrics': { 'image': 'tomochain/telegraf:latest', 'hostname': None, 'name': 'metrics', 'environment': { 'METRICS_ENDPOINT': None }, 'network': 'tmn_default', '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'} }, 'detach': True }, 'tomochain': { 'image': 'tomochain/node:latest', 'name': 'tomochain', 'environment': { 'IDENTITY': None, 'PRIVATE_KEY': None, 'BOOTNODES': None, 'NETWORK_ID': None, 'NETSTATS_HOST': None, 'NETSTATS_PORT': None, 'WS_SECRET': None }, 'network': 'tmn_default', 'ports': {'30303/udp': 30303, '30303/tcp': 30303}, 'volumes': { 'chaindata': {'bind': '/tomochain/data', 'mode': 'rw'} }, 'detach': True } } def process(name): """ Compose the containers with their variables :param name: masternode name :type name: str """ # custom if configuration.read_conf('identity'): identity = configuration.read_conf('identity') else: identity = '{}_{}'.format(name, uuid.uuid4().hex[:6]) configuration.write_conf('identity', identity) containers['metrics']['hostname'] = identity containers['tomochain']['environment']['IDENTITY'] = identity for container in list(containers): # add environment variables for variable in list(containers[container]['environment']): try: containers[container]['environment'][variable] = ( environment[variable]) except KeyError: # TODO add logging pass # rename containers containers[container]['name'] = '{}_{}'.format(name, container) PK!.m. . tmn/configuration.pyimport sys from clint import resources import validators from tmn import display from tmn import networks from tmn import compose resources.init('tomochain', 'tmn') name = None def init(new_name=None, net=None, pkey=None): """ Init a configuration for a new masternode :param new_name: new name of the masternode :type new_name: str :param net: network to use :type net: str :param pkey: private key to use as account for the masternode :type pkey: str """ global name create = False conf_name = resources.user.read('name') if conf_name: if new_name: display.warning_ignoring_start_options(conf_name) name = conf_name else: if _validate_name(new_name): name = new_name create = True elif not new_name: display.error_start_not_initialized() sys.exit() else: display.error_validation_option('--name', '4 to 10 characters slug') sys.exit() if create: if not net: display.error_start_option_required('--net') sys.exit() elif not pkey: display.error_start_option_required('--pkey') sys.exit() if not _validate_pkey(pkey): display.error_validation_option('--pkey', '64 characters hex string') sys.exit() else: if net == 'devnet': compose.environment = networks.devnet if net == 'testnet': compose.environment = networks.testnet compose.environment['PRIVATE_KEY'] = pkey resources.user.write('name', name) def write_conf(conf, content): """ Write a configuration to a file :param conf: name of the configuration :type conf: str :param content: content :type content: str """ resources.user.write(conf, content) def read_conf(conf): """ Read a configuration from a file :param conf: name of the configuration :type conf: str :returns: the content of the configuration :rtype: str """ return resources.user.read(conf) def remove_conf(conf): """ remove a configuration file :param conf: name of the configuration :type conf: str """ resources.user.delete(conf) def _validate_name(name): """ Validate a name string :param name: name string :type conf: str :returns: is valid :rtype: bool """ if ( name and validators.slug(name) and validators.length(name, min=4, max=10) ): return True else: return False def _validate_pkey(pkey): """ Validate a pkey string :param name: pkey string :type conf: str :returns: is valid :rtype: bool """ if ( pkey and validators.length(pkey, min=64, max=64) ): try: int(pkey, 16) return True except ValueError: return False else: return False PK!wD''tmn/display.pyimport 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/' def newline(number=1): """ Print newlines :param number: the number of newlines to print :type number: int """ print('\n'*number, end='') def style(function): """ Print and colorize strings with `pastel` :param function: function to decorate :type function: function :returns: decorated function :rtype: function """ def wrapper(*args, **kwargs): print(pastel.colorize(function(*args, **kwargs))) return wrapper def style_no_new_line(function): """ Print and colorize strings with `pastel`. Don't add a new line at the end. Decorator to print and colorize strings with `pastel`. Don't add a new line at the end. :param function: function to decorate :type function: function :returns: decorated function :rtype: function """ def wrapper(*args): print(pastel.colorize(function(*args)), end='', flush=True) return wrapper @style def link(msg, url): """ Return a pastel formated string for browser links :param msg: message :type msg: str :param url: website url :type url: str """ return '{msg} {url}'.format( msg=msg, url=url ) def link_docs(): """ Custom link message for documentation """ link('Documentation on running a masternode:', help_url) @style def title(msg): """ Return a pastel formated title string :param msg: title message :type msg: str :returns: subtitle formated :rtype: str """ return '{msg}\n'.format( msg=msg ) def title_start_masternode(name): """ Title when starting a masternode """ title('Starting masternode {}:'.format(name)) def title_stop_masternode(name): """ Title when stopping a masternode """ title('Stopping masternode {}:'.format(name)) def title_status_masternode(name): """ Title when stopping a masternode """ title('Masternode {} status:'.format(name)) def title_inspect_masternode(name): """ Title when stopping a masternode """ title('Masternode {} details:'.format(name)) def title_remove_masternode(name): """ Title when stopping a masternode """ title('Removing masternode {}:'.format(name)) @style def subtitle(msg): """ Return a pastel formated subtitle string :param msg: subtitle message :type msg: str :returns: subtitle formated :rtype: str """ return '{msg}\n'.format( msg=msg ) def subtitle_create_volumes(): """ Subtitle when creating volumes """ subtitle('Volumes') def subtitle_remove_volumes(): """ Subtitle when removing volumes """ subtitle('Volumes') def subtitle_create_networks(): """ Subtitle when creating networks """ subtitle('Networks') def subtitle_remove_networks(): """ Subtitle when removing networks """ subtitle('Networks') def subtitle_create_containers(): """ Subtitle when creating containers """ subtitle('Containers') def subtitle_remove_containers(): """ Subtitle when removing containers """ subtitle('Containers') @style def detail(msg, content, indent=1): """ Return a pastel formated detail :param msg: detail message :type msg: str :param content: detail content :type content: str :returns: `msg` formated :rtype: str """ return (' '*indent + '{msg}:\n'.format(msg=msg) + ' '*indent + '{content}'.format(content=content)) def detail_identity(content): """ Custom detail message for the masternode identity """ detail('Unique identity', content) def detail_coinbase(content): """ Custom detail message for the masternode coinbase address """ detail('Coinbase address (account public key)', content) @style_no_new_line def step(msg, indent=1): """ Return a pastel formated step with indentation. One indent is two spaces. :param msg: step message :type msg: str :param indent: number of idents :type indent: int :returns: `msg` formated :rtype: str """ step = ' '*indent + '- {msg}... '.format( msg=msg ) return step def step_create_masternode_volume(volume): """ Custom step message for docker volumes creation """ step('Creating {volume}'.format( volume=volume )) def step_remove_masternode_volume(volume): """ Custom step message for docker volumes removal """ step('Removing {volume}'.format( volume=volume )) def step_create_masternode_network(network): """ Custom step message for docker networks creatin """ step('Creating {network}'.format( network=network )) def step_remove_masternode_network(network): """ Custom step message for docker networks creatin """ step('Removing {network}'.format( network=network )) def step_create_masternode_container(container): """ Custom step message for docker container creation """ step('Creating {container}'.format( container=container )) def step_start_masternode_container(container): """ Custom step message for docker container starting """ step('Starting {container}'.format( container=container )) def step_remove_masternode_container(container): """ Custom step message for docker container starting """ step('Removing {container}'.format( container=container )) def step_stop_masternode_container(container): """ Custom step message for docker container stopping """ step('Stopping {container}'.format( container=container )) @style def step_close(msg, color='green'): """ Return a pastel formated end of step :param msg: task status of the step :type msg: str :returns: `msg` formated :rtype: str """ return '{msg}'.format( msg=msg, color=color ) # TODO only use step_close_status def step_close_created(): """ Custom 'created' closing step message """ step_close('created') def step_close_exists(): """ Custom 'exists' closing step message """ step_close('exists') def step_close_status(status): """ Custom 'status' closing step message """ step_close(status) @style def status(name='', status='absent', id='', status_color='red'): """ Return a pastel formated end of step :param msg: task status of the step :type msg: str :returns: `msg` formated :rtype: str """ if id: return ' {name}\t{status}({id})'.format( name=name, status=status, color=status_color, id=id ) else: return ' {name}\t{status}{id}'.format( name=name, status=status, color=status_color, id=id ) @style def warning(msg, newline=True): """ Return a pastel formated string for warnings :param msg: error message :type msg: str :returns: `msg` formated :rtype: str """ before = '' if newline: before = '\n' return before + '! warning: {msg}\n'.format( msg=msg ) def warning_ignoring_start_options(name): """ Custom warning when tmn is ignoring the start options """ warning( 'masternode {} is already configured\n'.format(name) + ' ' + 'ignoring start options\n' ) def warning_nothing_to_remove(): """ Custom warning when tmn is removing docker objects but it's empty """ warning( 'nothing to remove', newline=False ) def warning_remove_masternode(name): """ 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): """ Return a pastel formated string for errors :param msg: error message :type msg: str :returns: `msg` formated :rtype: str """ return ( '\n! error: {msg}\n'.format(msg=msg) + ' ' + 'need help? {}'.format(help_url) ) def error_docker(): """ Custom error when docker is not accessible """ error('could not access the docker daemon') def error_docker_api(): """ Custom error when docker is not accessible """ error('something went wrong while doing stuff with docker') def error_start_not_initialized(): """ Custom error when `tmn start` has never been used with the `--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): """ 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, format): """ Custom error when an option format is not valide """ error( '{} is not valid\n'.format(option) + ' it should be a {}'.format(format) ) PK!$E^'^'tmn/masternode.pyimport sys import docker as dockerpy from tmn import compose from tmn import display from tmn import configuration _client = None def apierror(function): """ Decorator to catch `docker.errors.APIerror` Exception :param function: function to decorate :type function: function :returns: decorated function :rtype: function """ def wrapper(*args, **kwargs): try: function(*args, **kwargs) except dockerpy.errors.APIError: display.error_docker_api() sys.exit() return wrapper def connect(url=None): """ Create the docker client. Try to ping the docker server to establish if the connexion in successful :param url: url to the docker deamon :type url: str :returns: is connected to Docker api :rtype: bool """ global _client if not url: _client = dockerpy.from_env() else: _client = dockerpy.DockerClient(base_url=url) return _ping() def _ping(): """ Try to ping the Docker daemon. Check if accessible. :returns: is Docker api reachable :rtype: bool """ try: return _client.ping() except Exception: return False def _create_volumes(): """ Try to get the volumes defined in `compose.volumes`. If it fails, create them. """ for volume in compose.volumes: display.step_create_masternode_volume(volume) try: _client.volumes.get(volume) display.step_close_exists() except dockerpy.errors.NotFound: _client.volumes.create(volume) display.step_close_created() display.newline() def _remove_volumes(): """ Remove Volumes """ for volume in compose.volumes: display.step_remove_masternode_volume(volume) try: v = _client.volumes.get(volume) v.remove(force=True) display.step_close_status('removed') except dockerpy.errors.NotFound: display.step_close_status('absent') display.newline() def _create_networks(): """ Try to get the networks defined in `compose.networks`. If it fails, create them. """ for network in compose.networks: display.step_create_masternode_network(network) try: _client.networks.get(network) display.step_close_exists() except dockerpy.errors.NotFound: _client.networks.create(network) display.step_close_created() display.newline() def _remove_networks(): """ Remove networks """ for network in compose.networks: display.step_remove_masternode_network(network) try: n = _client.networks.get(network) n.remove() display.step_close_status('removed') except dockerpy.errors.NotFound: display.step_close_status('absent') display.newline() def _get_existing_containers(): """ Get from docker the containers defined in `compose.containers`. :returns: The existing `docker.Container` :rtype: list """ containers = {} for key, value in compose.containers.items(): try: container = _client.containers.get(value['name']) containers[container.name] = container except dockerpy.errors.NotFound: pass return containers def _create_containers(): """ Try to get the containers defined in `compose.containers`. If it fails, create them. :returns: The created or existing `docker.Container` :rtype: list """ containers = {} for key, value in compose.containers.items(): display.step_create_masternode_container(value['name']) try: container = _client.containers.get(value['name']) display.step_close_exists() except dockerpy.errors.NotFound: # temporary, see https://github.com/docker/docker-py/issues/2101 _client.images.pull(value['image']) container = _client.containers.create(**value) display.step_close_created() containers[container.name] = container return containers def _start_containers(containers): """ Verify the container status. If it's not restarting or running, start them. :param containers: dict of name:`docker.Container` :type containers: dict """ for name, container in containers.items(): display.step_start_masternode_container(container.name) container.reload() # filtered status are: # created|restarting|running|removing|paused|exited|dead # might have to add all the status if container.status in ['restarting', 'running']: pass elif container.status in ['paused']: container.unpause() elif container.status in ['created', 'exited', 'dead']: container.start() container.reload() display.step_close_status(container.status) def _remove_containers(containers): """ Remove given containers :param containers: dict of name:`docker.Container` :type containers: dict """ if not containers: display.warning_nothing_to_remove() else: display.newline() for name, container in containers.items(): display.step_remove_masternode_container(container.name) container.remove(force=True) display.step_close_status('removed') if containers: display.newline() def _stop_containers(containers): """ Stop the given dict of `docker.Container` :param containers: dict of name:`docker.Container` :type containers: dict """ for name, container in containers.items(): display.step_stop_masternode_container(container.name) container.reload() # filtered status are: # created|restarting|running|removing|paused|exited|dead # might have to add all the status if container.status in ['restarting', 'running', 'paused']: container.stop() elif container.status in ['created', 'exited', 'dead']: pass container.reload() display.step_close_status(container.status) def _status_containers(containers): """ Display the status of `CONTAINERS` w/ the passed list of `docker.Container` :param containers: dict of `docker.Container` :type containers: dict """ names = [ compose.containers[container]['name'] for container in list(compose.containers) ] for name in names: display_kwargs = {} display_kwargs.update({'name': name}) try: containers[name].reload() if containers[name].status in ['running']: display_kwargs.update({'status_color': 'green'}) display_kwargs.update({'status': containers[name].status}) display_kwargs.update({'id': containers[name].short_id}) except KeyError: display_kwargs.update({'name': name}) display_kwargs.update({'name': name}) display.status(**display_kwargs) def _get_coinbase(): """ Retrieve the coinbase address from the account used by the masternode :returns: coinbase address :rtype: str """ container = _client.containers.get(compose.containers['tomochain']['name']) return '0x' + container.exec_run( 'tomo account list --keystore keystore' ).output.decode("utf-8").replace('}', '{').split("{")[1] def _get_identity(): """ Retrieve the masternode identity :returns: identity :rtype: str """ container = _client.containers.get(compose.containers['tomochain']['name']) return container.exec_run( '/bin/sh -c "echo $IDENTITY"' ).output.decode("utf-8") @apierror def start(name): """ Start a masternode. Includes: - process components - creating volumes - creating networks - creating containers - starting containers :param name: name of the masternode :type name: str """ compose.process(name) display.subtitle_create_volumes() _create_volumes() display.subtitle_create_networks() _create_networks() display.subtitle_create_containers() containers = _create_containers() display.newline() _start_containers(containers) @apierror def stop(name): """ Stop a masternode. Includes: - process components - getting the list of containers - stoping them :param name: name of the masternode :type name: str """ compose.process(name) containers = _get_existing_containers() _stop_containers(containers) @apierror def status(name): """ Retrieve masternode status. Includes: - process components - getting the list of containers - displaying their status :param name: name of the masternode :type name: str """ compose.process(name) containers = _get_existing_containers() _status_containers(containers) @apierror def remove(name): """ Remove masternode. Includes: - process components - stop containers - remove containers, networks and volumes - remove tmn persistent configuration :param name: name of the masternode :type name: str """ compose.process(name) containers = _get_existing_containers() display.subtitle_remove_containers() _stop_containers(containers) _remove_containers(containers) display.subtitle_remove_networks() _remove_networks() display.subtitle_remove_volumes() _remove_volumes() configuration.remove_conf('name') configuration.remove_conf('identity') @apierror def details(name): """ Remove masternode. Includes: - process components - stop containers - remove containers, networks and volumes - remove tmn persistent configuration :param name: name of the masternode :type name: str """ compose.process(name) display.detail_identity(_get_identity()) display.detail_coinbase(_get_coinbase()) PK!5ztmn/networks.pytestnet = { 'METRICS_ENDPOINT': 'https://metrics.testnet.tomochain.com', 'BOOTNODES': ( 'enode://4d3c2cc0ce7135c1778c6f1cfda623ab44b4b6db55289543d48ecf' 'de7d7111fd420c42174a9f2fea511a04cf6eac4ec69b4456bfaaae0e5bd236' '107d3172b013@52.221.28.223:30301,enode://298780104303fcdb37a84' 'c5702ebd9ec660971629f68a933fd91f7350c54eea0e294b0857f1fd2e8dba' '2869fcc36b83e6de553c386cf4ff26f19672955d9f312@13.251.101.216:3' '0301,enode://46dba3a8721c589bede3c134d755eb1a38ae7c5a4c69249b8' '317c55adc8d46a369f98b06514ecec4b4ff150712085176818d18f59a9e631' '1a52dbe68cff5b2ae@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' ) } devnet = { 'METRICS_ENDPOINT': 'https://metrics.devnet.tomochain.com', 'BOOTNODES': ( 'enode://f3d3d5d6cd0fdde8996722ff5b5a92f331029b2dcbdb9748f50db1' '421851a939eb660bf81a7ec7f359454aa0fd65fe4c03ae5c6bb2382b34dfaa' 'ca7eb6ecaf4e@52.77.194.164:30301,enode://34b923ddfcba1bfafdd1a' 'c7a030436f9fbdc565919189f5e62c8cadd798c239b5807a26ab7f6b96a442' '00eb0399d1ebc2d9c1be94d2a774c8cc7660ff4c10367@13.228.93.232:30' '301,enode://e2604862d18049e025f294d63d537f9f54309ff09e45ed69ff' '4f18c984831f5ef45370053355301e3a4da95aba2698c6116f4d2a347e5a5e' '0a3152ac0ae0f574@18.136.42.72: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' ) } PK!8>9Ts s tmn/tmn.pyimport sys import click from tmn import __version__ from tmn import display from tmn import masternode from tmn import configuration conf = None @click.group(help='Tomo MasterNode (tmn) is a cli tool to help you run a ' + 'Tomochain masternode') @click.option('--dockerurl', metavar='URL', help='Url to the docker server') @click.version_option(version=__version__) def main(dockerurl): """ Cli entrypoint. :param config: path to the configuration file :type config: str """ if not masternode.connect(url=dockerurl): display.error_docker() sys.exit() @click.command(help='Display Tomochain documentation link') def docs(): """ Link to the documentation :param open: open the link in your navigator :type open: bool """ 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(['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')) def start(name, net, pkey): """ Start the containers needed to run a masternode """ configuration.init(name, net, pkey) display.title_start_masternode(configuration.name) masternode.start(configuration.name) @click.command(help='Stop your Tomochain masternode') def stop(): """ Stop the containers needed to run a masternode """ configuration.init() display.title_stop_masternode(configuration.name) masternode.stop(configuration.name) @click.command(help='Show the status of your Tomochain masternode') def status(): """ Show the status of the masternode containers """ configuration.init() display.title_status_masternode(configuration.name) masternode.status(configuration.name) @click.command(help='Show details about your Tomochain masternode') def inspect(): """ Show details about the tomochain masternode """ configuration.init() display.title_inspect_masternode(configuration.name) masternode.details(configuration.name) @click.command(help='Remove your Tomochain masternode') @click.option('--confirm', is_flag=True) def remove(confirm): """ Remove the masternode completly (containers, networks volumes) """ configuration.init() if not confirm: display.warning_remove_masternode(configuration.name) sys.exit() display.title_remove_masternode(configuration.name) masternode.remove(configuration.name) main.add_command(docs) main.add_command(start) main.add_command(stop) main.add_command(status) main.add_command(inspect) main.add_command(remove) PK!H #$$tmn-0.1.1.dist-info/entry_points.txtN+I/N.,()*ͳb= MPK!HW"TTtmn-0.1.1.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!H n?tmn-0.1.1.dist-info/METADATAWko6_qXXrm M4Yעmb hD$@߾sI9/]{xxAzQ/?uf;ɉ|l.v>䍩dZ n{S|*rS%U.竓wOwr~kl`w!&J8/6#9lZ҉M)^|Ie%T9 \~n.=C<{/fQ)Sc%a̚U =i xXlϻxKГދ Yb;{^lgk WZ˞\2Zɶ(pbkEw]ccrjGF{}z9C27I@|WV|J.̤|4Mi剽"ظUʢV̔Ks;Qp~wj.Çt2}+khH\1{azr&KSw+U\!>]Ir '')G#h͔xT5MCtNu :ofV Q%T#i?GJ2D~ܔC"ap$t#Q`N=Wf1om>eAr] ábaF>=|{zrN?|8<9/>e4\)ni/Ɩtuʖ E>T~Q> !J#J:{I<92tbb4 ܑu)KYpn 22*Qi|*VڂxH6EX@oޥ;f=ulaAu>FjӍ|6+ÈI&B`^%o}T;`ɱ aE ht͞ tH[8`Ubăp9 uX\P_s_IԶ, {3z\XmC,;|GdIפk2ʕ * y@~YtuB̏H+h9FäOp0ngnI2&?&8IGA79cn@Bx 0;tL@!QKZHQoPK!`tmn/__init__.pyPK!/Ctmn/compose.pyPK!.m. . " tmn/configuration.pyPK!wD''tmn/display.pyPK!$E^'^'y=tmn/masternode.pyPK!5zetmn/networks.pyPK!8>9Ts s ktmn/tmn.pyPK!H #$$zwtmn-0.1.1.dist-info/entry_points.txtPK!HW"TTwtmn-0.1.1.dist-info/WHEELPK!H n?jxtmn-0.1.1.dist-info/METADATAPK!HP~tmn-0.1.1.dist-info/RECORDPK