From 2e6029dfd766cfba806a0ac34af2025d9092ace5 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 14 Nov 2019 11:57:09 -0500 Subject: [PATCH] Very simple initial implementation of Moe's new `dashboard` command --- nucypher/characters/chaotic.py | 2 +- nucypher/cli/characters/moe.py | 109 +++++++++++++++----- nucypher/network/status_app/crawler.py | 7 +- nucypher/network/status_app/db.py | 4 +- nucypher/network/status_app/moe.py | 131 ++++++++++++------------- 5 files changed, 157 insertions(+), 96 deletions(-) diff --git a/nucypher/characters/chaotic.py b/nucypher/characters/chaotic.py index a51f0208d..ac755016e 100644 --- a/nucypher/characters/chaotic.py +++ b/nucypher/characters/chaotic.py @@ -39,7 +39,7 @@ from nucypher.crypto.powers import SigningPower, TransactingPower from nucypher.keystore.keypairs import HostingKeypair from nucypher.keystore.threading import ThreadedSession from nucypher.network.server import TLSHostingPower -from nucypher.network.status_app.moe import MoeStatusApp + class Moe(Character): diff --git a/nucypher/cli/characters/moe.py b/nucypher/cli/characters/moe.py index 6a1c8ef60..fe53f9b7d 100644 --- a/nucypher/cli/characters/moe.py +++ b/nucypher/cli/characters/moe.py @@ -1,4 +1,6 @@ import click +from flask import Flask +from umbral.keys import UmbralPrivateKey from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry @@ -7,11 +9,15 @@ from nucypher.characters.chaotic import Moe from nucypher.cli import actions from nucypher.cli.config import nucypher_click_config from nucypher.cli.types import NETWORK_PORT, EXISTING_READABLE_FILE +from nucypher.keystore.keypairs import HostingKeypair from nucypher.network.middleware import RestMiddleware +from cryptography.hazmat.primitives.asymmetric import ec from twisted.internet import reactor +from nucypher.network.server import TLSHostingPower from nucypher.network.status_app.crawler import NetworkCrawler +from nucypher.network.status_app.moe import MoeDashboardApp @click.group() @@ -26,7 +32,7 @@ def moe(): @click.option('--teacher', 'teacher_uri', help="An Ursula URI to start learning from (seednode)", type=click.STRING) @click.option('--registry-filepath', help="Custom contract registry filepath", type=EXISTING_READABLE_FILE) @click.option('--min-stake', help="The minimum stake the teacher must have to be a teacher", type=click.INT, default=0) -@click.option('--network', help="Network Domain Name", type=click.STRING) +@click.option('--network', help="Network Domain Name", type=click.STRING, default='goerli') @click.option('--host', help="The host to run Moe services on", type=click.STRING, default='127.0.0.1') @click.option('--certificate-filepath', help="Pre-signed TLS certificate filepath") @click.option('--tls-key-filepath', help="TLS private key filepath") @@ -51,12 +57,9 @@ def network_crawler(click_config, emitter = click_config.emitter emitter.clear() emitter.banner(MOE_BANNER) + emitter.echo("> NETWORK CRAWLER") - BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri) - if registry_filepath: - registry = LocalContractRegistry.from_latest_publication() - else: - registry = InMemoryContractRegistry.from_latest_publication() + registry = __get_registry(provider_uri, registry_filepath) # Teacher Ursula teacher_uris = [teacher_uri] if teacher_uri else None @@ -90,26 +93,84 @@ def network_crawler(click_config, @moe.command() -def dashboard(): +@click.option('--host', help="The host to run Moe services on", type=click.STRING, default='127.0.0.1') +@click.option('--http-port', help="The network port to run Moe services on", type=NETWORK_PORT, default=12500) +@click.option('--registry-filepath', help="Custom contract registry filepath", type=EXISTING_READABLE_FILE) +@click.option('--certificate-filepath', help="Pre-signed TLS certificate filepath") +@click.option('--tls-key-filepath', help="TLS private key filepath") +@click.option('--provider', 'provider_uri', help="Blockchain provider's URI", type=click.STRING) +@click.option('--network', help="Network Domain Name", type=click.STRING, default='goerli') +@click.option('--dry-run', '-x', help="Execute normally without actually starting the node", is_flag=True) +@nucypher_click_config +def dashboard(click_config, + host, + http_port, + registry_filepath, + certificate_filepath, + tls_key_filepath, + provider_uri, + network, + dry_run, + ): """ Run UI dashboard of NuCypher network. """ - # # - # # WSGI Service - # # - # self.rest_app = Flask("fleet-monitor") - # rest_app = self.rest_app - # MoeStatusApp(moe=self, - # title='Moe Monitoring Application', - # flask_server=self.rest_app, - # route_url='/') + + # Banner + emitter = click_config.emitter + emitter.clear() + emitter.banner(MOE_BANNER) + emitter.echo("> UI DASHBOARD") + + registry = __get_registry(provider_uri, registry_filepath) + # - # # - # # Server - # # - # deployer = self._crypto_power.power_ups(TLSHostingPower).get_deployer(rest_app=rest_app, port=self.http_port) - # if not dry_run: - # deployer.run() + # WSGI Service # - # pass - pass + rest_app = Flask("moe-dashboard") + MoeDashboardApp(title='Moe Dashboard Application', + flask_server=rest_app, + route_url='/', + registry=registry, + network=network) + + # + # Server + # + tls_hosting_power = __get_tls_hosting_power(host=host, + tls_certificate_filepath=certificate_filepath, + tls_private_key_filepath=tls_key_filepath) + emitter.message(f"Running Moe Dashboard - https://{host}:{http_port}") + deployer = tls_hosting_power.get_deployer(rest_app=rest_app, port=http_port) + if not dry_run: + deployer.run() + + +def __get_registry(provider_uri, registry_filepath): + BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri) + if registry_filepath: + registry = LocalContractRegistry.from_latest_publication() + else: + registry = InMemoryContractRegistry.from_latest_publication() + + return registry + + +def __get_tls_hosting_power(host: str = None, + tls_certificate_filepath: str = None, + tls_private_key_filepath: str = None): + # Pre-Signed + if tls_certificate_filepath and tls_private_key_filepath: + with open(tls_private_key_filepath, 'rb') as file: + tls_private_key = UmbralPrivateKey.from_bytes(file.read()) + tls_hosting_keypair = HostingKeypair(curve=ec.SECP384R1, + host=host, + certificate_filepath=tls_certificate_filepath, + private_key=tls_private_key) + + # Self-Sign + else: + tls_hosting_keypair = HostingKeypair(curve=ec.SECP384R1, host=host) + + tls_hosting_power = TLSHostingPower(keypair=tls_hosting_keypair, host=host) + return tls_hosting_power diff --git a/nucypher/network/status_app/crawler.py b/nucypher/network/status_app/crawler.py index 7e7936f1c..e62c105f4 100644 --- a/nucypher/network/status_app/crawler.py +++ b/nucypher/network/status_app/crawler.py @@ -10,7 +10,7 @@ from nucypher.characters.chaotic import Moe from influxdb import InfluxDBClient from maya import MayaDT -from nucypher.network.status_app.db import InfluxCrawlerClient +from nucypher.network.status_app.db import BlockchainCrawlerClient import sqlite3 import os @@ -235,8 +235,9 @@ class NetworkCrawler: and self._locked_tokens_learning_task.running and self._moe_known_nodes_learning_task.running) - def get_blockchain_crawler_client(self): - return InfluxCrawlerClient(host='localhost', port=8086, database=self.BLOCKCHAIN_DB_NAME) + @staticmethod + def get_blockchain_crawler_client(): + return BlockchainCrawlerClient(host='localhost', port=8086, database=NetworkCrawler.BLOCKCHAIN_DB_NAME) def get_node_db_row_information(self, node_info, teacher_address, current_period): # Staker address diff --git a/nucypher/network/status_app/db.py b/nucypher/network/status_app/db.py index e016b0ff6..c2380c87b 100644 --- a/nucypher/network/status_app/db.py +++ b/nucypher/network/status_app/db.py @@ -5,7 +5,7 @@ from influxdb import InfluxDBClient from maya import MayaDT -class InfluxCrawlerClient: +class BlockchainCrawlerClient: """ Performs operations on data in the MoeBlockchainCrawler DB. @@ -70,4 +70,4 @@ class InfluxCrawlerClient: return num_stakers_dict def close(self): - self._client.close() \ No newline at end of file + self._client.close() diff --git a/nucypher/network/status_app/moe.py b/nucypher/network/status_app/moe.py index 8ad51ad6d..991878e2e 100644 --- a/nucypher/network/status_app/moe.py +++ b/nucypher/network/status_app/moe.py @@ -6,11 +6,13 @@ import plotly.graph_objs as go from dash.dependencies import Output, Input from maya import MayaDT +from nucypher.blockchain.eth.agents import StakingEscrowAgent, ContractAgency from nucypher.blockchain.eth.token import NU from nucypher.network.status_app.base import NetworkStatusPage +from nucypher.network.status_app.crawler import NetworkCrawler -class MoeStatusApp(NetworkStatusPage): +class MoeDashboardApp(NetworkStatusPage): """ Status application for 'Moe' monitoring node. """ @@ -24,15 +26,14 @@ class MoeStatusApp(NetworkStatusPage): 'fillFrame': False, 'displayModeBar': False} - def __init__(self, moe, *args, **kwargs): + def __init__(self, registry, network, *args, **kwargs): super().__init__(*args, **kwargs) - self.moe = moe - from nucypher.network.status_app.crawler import NetworkCrawler - self.moe_crawler = NetworkCrawler(moe=moe) - self.moe_crawler.start() + self.blockchain_db_client = NetworkCrawler.get_blockchain_crawler_client() + self.registry = registry + self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry) - self.moe_db_client = self.moe_crawler.get_blockchain_crawler_client() + self.network = network self.dash_app.layout = html.Div([ dcc.Location(id='url', refresh=False), @@ -57,7 +58,7 @@ class MoeStatusApp(NetworkStatusPage): html.Div(id='time-remaining'), html.Div(id='domains'), html.Div(id='active-stakers'), - html.Div(id='stakers-breakdown'), + html.Div(id='staker-breakdown'), html.Div(id='staked-tokens'), ], id='stats'), @@ -65,15 +66,15 @@ class MoeStatusApp(NetworkStatusPage): html.Div([ html.Div(id='prev-num-stakers-graph'), html.Div(id='prev-locked-stake-graph'), - html.Div(id='locked-stake-graph'), + #html.Div(id='locked-stake-graph'), ], id='widgets'), # States and Known Nodes Table - html.Div([ - html.Div(id='prev-states'), - html.Br(), - html.Div(id='known-nodes'), - ]) + # html.Div([ + # html.Div(id='prev-states'), + # html.Br(), + # html.Div(id='known-nodes'), + # ]) ]), ], id='main'), @@ -96,42 +97,41 @@ class MoeStatusApp(NetworkStatusPage): def header(pathname): return self.header() - @self.dash_app.callback(Output('prev-states', 'children'), - [Input('state-update-button', 'n_clicks'), - Input('minute-interval', 'n_intervals')]) - def state(n_clicks, n_intervals): - return self.previous_states(moe) + # @self.dash_app.callback(Output('prev-states', 'children'), + # [Input('state-update-button', 'n_clicks'), + # Input('minute-interval', 'n_intervals')]) + # def state(n_clicks, n_intervals): + # return self.previous_states(moe) - @self.dash_app.callback(Output('known-nodes', 'children'), - [Input('node-update-button', 'n_clicks'), - Input('minute-interval', 'n_intervals')]) - def known_nodes(n_clicks, n_intervals): - return self.known_nodes(moe) + # @self.dash_app.callback(Output('known-nodes', 'children'), + # [Input('node-update-button', 'n_clicks'), + # Input('minute-interval', 'n_intervals')]) + # def known_nodes(n_clicks, n_intervals): + # return self.known_nodes(moe) @self.dash_app.callback(Output('active-stakers', 'children'), [Input('minute-interval', 'n_intervals')]) def active_stakers(n): - confirmed, pending, inactive = moe.staking_agent.partition_stakers_by_activity() + confirmed, pending, inactive = self.staking_agent.partition_stakers_by_activity() total_stakers = len(confirmed) + len(pending) + len(inactive) return html.Div([html.H4("Active Ursulas"), html.H5(f"{len(confirmed)}/{total_stakers}")]) - @self.dash_app.callback(Output('stakers-breakdown', 'children'), + @self.dash_app.callback(Output('staker-breakdown', 'children'), [Input('minute-interval', 'n_intervals')]) def stakers_breakdown(n): - confirmed, pending, inactive = moe.staking_agent.partition_stakers_by_activity() + confirmed, pending, inactive = self.staking_agent.partition_stakers_by_activity() stakers = dict() stakers['Active'] = len(confirmed) stakers['Pending'] = len(pending) stakers['Inactive'] = len(inactive) - staker_type_numbers = list(stakers.values()) + staker_breakdown = list(stakers.values()) fig = go.Figure( data=[ go.Pie( labels=list(stakers.keys()), - values=staker_type_numbers, - name='Stakers', - marker=go.bar.Marker(color=staker_type_numbers, colorscale='Viridis') + values=staker_breakdown, + name='Stakers' ) ], layout=go.Layout( @@ -146,7 +146,7 @@ class MoeStatusApp(NetworkStatusPage): [Input('minute-interval', 'n_intervals')]) def current_period(pathname): return html.Div([html.H4("Current Period"), - html.H5(moe.staking_agent.get_current_period())]) + html.H5(self.staking_agent.get_current_period())]) @self.dash_app.callback(Output('time-remaining', 'children'), [Input('minute-interval', 'n_intervals')]) @@ -161,16 +161,15 @@ class MoeStatusApp(NetworkStatusPage): @self.dash_app.callback(Output('domains', 'children'), [Input('url', 'pathname')]) # on page-load def domains(pathname): - domains = ' | '.join(moe.learning_domains) return html.Div([ - html.H4('Learning Domains'), - html.H5(domains), + html.H4('Domain'), + html.H5(self.network), ]) @self.dash_app.callback(Output('staked-tokens', 'children'), [Input('minute-interval', 'n_intervals')]) def staked_tokens(n): - nu = NU.from_nunits(moe.staking_agent.get_global_locked_tokens()) + nu = NU.from_nunits(self.staking_agent.get_global_locked_tokens()) return html.Div([ html.H4('Staked Tokens'), html.H5(f"{nu}"), @@ -180,7 +179,7 @@ class MoeStatusApp(NetworkStatusPage): [Input('daily-interval', 'n_intervals')]) def prev_locked_tokens(n): prior_periods = 30 - locked_tokens_dict = self.moe_db_client.get_historical_locked_tokens_over_range(prior_periods) + locked_tokens_dict = self.blockchain_db_client.get_historical_locked_tokens_over_range(prior_periods) token_values = list(locked_tokens_dict.values()) fig = go.Figure(data=[ go.Bar( @@ -207,7 +206,7 @@ class MoeStatusApp(NetworkStatusPage): [Input('daily-interval', 'n_intervals')]) def historical_known_nodes(n): prior_periods = 30 - num_stakers_dict = self.moe_db_client.get_historical_num_stakers_over_range(prior_periods) + num_stakers_dict = self.blockchain_db_client.get_historical_num_stakers_over_range(prior_periods) marker_color = 'rgb(0, 163, 239)' fig = go.Figure(data=[ go.Scatter( @@ -230,31 +229,31 @@ class MoeStatusApp(NetworkStatusPage): fig['layout'].update(autosize=True, width=None, height=None) return dcc.Graph(figure=fig, id='prev-stakers-graph', config=self.GRAPH_CONFIG) - @self.dash_app.callback(Output('locked-stake-graph', 'children'), - [Input('daily-interval', 'n_intervals')]) - def future_locked_tokens(n): - token_counter = self.moe_crawler.snapshot['future_locked_tokens'] - periods = len(token_counter) - period_range = list(range(1, periods + 1)) - token_counter_values = list(token_counter.values()) - fig = go.Figure(data=[ - go.Bar( - textposition='auto', - x=period_range, - y=token_counter_values, - name='Stake', - marker=go.bar.Marker(color=token_counter_values, colorscale='Viridis') - ) - ], - layout=go.Layout( - title=f'Staked NU over the next {periods} days.', - xaxis={'title': 'Days'}, - yaxis={'title': 'NU Tokens', 'rangemode': 'tozero'}, - showlegend=False, - legend=go.layout.Legend(x=0, y=1.0), - paper_bgcolor='rgba(0,0,0,0)', - plot_bgcolor='rgba(0,0,0,0)' - )) - - fig['layout'].update(autosize=True, width=None, height=None) - return dcc.Graph(figure=fig, id='locked-graph', config=self.GRAPH_CONFIG) + # @self.dash_app.callback(Output('locked-stake-graph', 'children'), + # [Input('daily-interval', 'n_intervals')]) + # def future_locked_tokens(n): + # token_counter = self.moe_crawler.snapshot['future_locked_tokens'] + # periods = len(token_counter) + # period_range = list(range(1, periods + 1)) + # token_counter_values = list(token_counter.values()) + # fig = go.Figure(data=[ + # go.Bar( + # textposition='auto', + # x=period_range, + # y=token_counter_values, + # name='Stake', + # marker=go.bar.Marker(color=token_counter_values, colorscale='Viridis') + # ) + # ], + # layout=go.Layout( + # title=f'Staked NU over the next {periods} days.', + # xaxis={'title': 'Days'}, + # yaxis={'title': 'NU Tokens', 'rangemode': 'tozero'}, + # showlegend=False, + # legend=go.layout.Legend(x=0, y=1.0), + # paper_bgcolor='rgba(0,0,0,0)', + # plot_bgcolor='rgba(0,0,0,0)' + # )) + # + # fig['layout'].update(autosize=True, width=None, height=None) + # return dcc.Graph(figure=fig, id='locked-graph', config=self.GRAPH_CONFIG)