Skip to content

Commit

Permalink
ag_setup_solo: use ocap discipline in main.py (WIP)
Browse files Browse the repository at this point in the history
  - replace print() with logging (ocap exception) (WIP)
  - don't import os, sys, etc. at module scope
    (everything globally available is immutable data)
    - style: move stdlib imports before others
  - AG_SOLO wasn't a constant. refactor lookup loop as Runner.locate
    and pass ag_solo around explicitly
  - BASEDIR in run_client() wasn't used
  - Options takes access to environ (WIP)
  - sys.exit(1) -> raise SystemExit(1)
  - factor out Path access
  - add type hints to facilitate refactoring
  - style: 80 char line width
  • Loading branch information
dckc committed Oct 20, 2019
1 parent 9908a7e commit 7a85ca8
Showing 1 changed file with 206 additions and 73 deletions.
279 changes: 206 additions & 73 deletions setup-solo/src/ag_setup_solo/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from twisted.internet.task import react
from twisted.internet import endpoints, defer
from twisted.python import usage
import wormhole
import treq
import json
import os.path
import shutil
import subprocess
import sys
import os
import urllib.request
import logging
from urllib.request import Request as Request_T
from subprocess import CompletedProcess

from typing import (
Any,
Callable, Iterable,
Dict, List, Optional as Opt,
IO,
cast,
)

from twisted.internet.task import react # type: ignore
from twisted.internet import defer # type: ignore
from twisted.python import usage # type: ignore

MAILBOX_URL = u"ws://relay.magic-wormhole.io:4000/v1"
# MAILBOX_URL = u"ws://10.0.2.24:4000/v1"
Expand All @@ -18,56 +23,147 @@
# We need this to connect to cloudflare's https.
USER_AGENT = "Mozilla/5.0"

# Locate the ag-solo binary.
# Look up until we find a different bin directory.
candidate = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..'))
AG_SOLO = os.path.join(candidate, 'bin', 'ag-solo')
while not os.path.exists(AG_SOLO):
next_candidate = os.path.dirname(candidate)
if next_candidate == candidate:
AG_SOLO = 'ag-solo'
break
candidate = next_candidate
AG_SOLO = os.path.join(candidate, 'bin', 'ag-solo')
log = logging.getLogger(__name__)


class Path:
# like pathlib.Path but with rmtree
def __init__(self, here: str,
io_open: Callable[..., IO[Any]],
path_join: Callable[[str, str], str],
basename: Callable[[str], str],
exists: Callable[[str], bool],
isdir: Callable[[str], bool],
islink: Callable[[str], bool],
realpath: Callable[[str], str],
samefile: Callable[[str, str], bool],
rmtree: Callable[[str], None]) -> None:
self.here = here
self.exists: Callable[[], bool] = lambda: exists(here)
self.is_dir: Callable[[], bool] = lambda: isdir(here)
self.is_symlink: Callable[[], bool] = lambda: islink(here)
self.samefile = lambda other: samefile(here, str(other))
self.open: Callable[[], IO[Any]] = lambda: io_open(here)
self.rmtree: Callable[[], None] = lambda: rmtree(here)

def make(there: str) -> Path:
return Path(there,
io_open, path_join, basename, exists,
isdir, islink, realpath, samefile,
rmtree)

self.resolve: Callable[[], Path] = lambda: make(realpath(here))
self.joinpath: Callable[[str], Path] = lambda other: make(
path_join(here, other))
self._parent: Callable[[], Path] = lambda: make(basename(here))

@property
def parent(self) -> 'Path':
return self._parent()

def __truediv__(self, other: str) -> 'Path':
return self.joinpath(other)


CP = Callable[..., CompletedProcess]
CN = Callable[..., None]


class Runner:
def __init__(self, prog: str,
run: CP,
execvp: CN):
self.__prog = prog
self.run: CP = lambda args, **kwargs: run([str(prog)] + args, **kwargs)
self.execvp: CN = lambda args: execvp(str(prog), [str(prog)] + args)

def __repr__(self) -> str:
return repr(self.__prog)

@classmethod
def locate(cls, start: Path,
run: CP, execvp: CN,
name: str = 'ag-solo') -> 'Runner':
# Locate the ag-solo binary.
# Look up until we find a different bin directory.
candidate = start.resolve().parent / '..' / '..'

prog = candidate / 'bin' / name
while not prog.exists():
next_candidate = candidate.parent
if next_candidate == candidate:
return cls(name, run, execvp)
candidate = next_candidate
prog = candidate / 'bin' / name
return cls(str(prog), run, execvp)


class Options(usage.Options):
class Options(usage.Options): # type: ignore
def __init__(self, argv: List[str], environ: Dict[str, str]) -> None:
self.__environ = environ

optParameters = [
["webhost", "h", "127.0.0.1", "client-visible HTTP listening address"],
["webport", "p", "8000", "client-visible HTTP listening port"],
["netconfig", None, NETWORK_CONFIG, "website for network config"]
]

def parseArgs(self, basedir=os.environ.get('AG_SOLO_BASEDIR', 'agoric')):
self['basedir'] = os.environ['AG_SOLO_BASEDIR'] = basedir
def parseArgs(self, basedir: Opt[str] = None) -> None:
environ = self.__environ
if basedir is None:
basedir = environ.get('AG_SOLO_BASEDIR', 'agoric')
assert(basedir) # mypy isn't too smart
self['basedir'] = environ['AG_SOLO_BASEDIR'] = basedir


def setIngress(sm: Dict[str, object], ag_solo: Runner) -> None:
log.info('Setting chain parameters with %s', ag_solo)
ag_solo.run(['set-gci-ingress', '--chainID=%s' % sm['chainName'],
sm['gci'], *cast(List[str], sm['rpcAddrs'])], check=True)


def restart(ag_solo: Runner) -> None:
log.info('Restarting %s', ag_solo)
ag_solo.execvp(['start', '--role=client'])


class WormHoleI:
def input_code(self) -> str: ...

def send_message(self, msg: bytes) -> None: ...

def setIngress(sm):
print('Setting chain parameters with ' + AG_SOLO)
subprocess.run([AG_SOLO, 'set-gci-ingress', '--chainID=%s' % sm['chainName'], sm['gci'], *sm['rpcAddrs']], check=True)
def get_message(self) -> Iterable[bytes]: ...

def close(self) -> Iterable[None]: ...

def restart():
print('Restarting ' + AG_SOLO)
os.execvp(AG_SOLO, [AG_SOLO, 'start', '--role=client'])

class WormHoleModI:
@staticmethod
def create(APPID: str, MAILBOX_URL: str, reactor: Any) -> WormHoleI: ...

@defer.inlineCallbacks
def run_client(reactor, o, pkeyFile):
def cleanup():
@staticmethod
def input_with_completion(prompt: str, code: str,
reactor: Any) -> None: ...


@defer.inlineCallbacks # type: ignore
def run_client(reactor: Any, o: Options, pkeyFile: Path,
ag_solo: Runner,
cwd: Path, wormhole: WormHoleModI) -> None:
def cleanup() -> None:
try:
# Delete the basedir if we failed
shutil.rmtree(o['basedir'])
(cwd / o['basedir']).rmtree()
except FileNotFoundError:
pass

try:
# Try to initialize the client
print("initializing ag-solo", o['basedir'])
doInit(o)
log.info("initializing ag-solo %s", o['basedir'])
doInit(o, ag_solo)

# read the pubkey out of BASEDIR/ag-cosmos-helper-address
f = open(pkeyFile)
f = pkeyFile.open()
pubkey = f.read()
f.close()
pubkey = pubkey.strip()
Expand All @@ -77,7 +173,8 @@ def cleanup():

# Ensure cleanup gets called before aborting
t = reactor.addSystemEventTrigger("before", "shutdown", cleanup)
yield wormhole.input_with_completion("Provisioning code: ", w.input_code(), reactor)
yield wormhole.input_with_completion("Provisioning code: ",
w.input_code(), reactor)
reactor.removeSystemEventTrigger(t)

cm = json.dumps({
Expand All @@ -86,77 +183,113 @@ def cleanup():
w.send_message(cm.encode("utf-8"))
server_message = yield w.get_message()
sm = json.loads(server_message.decode("utf-8"))
print("server message is", sm)
log.info("server message is%s", sm)
yield w.close()

if not sm['ok']:
raise Exception("error from server: " + sm['error'])

BASEDIR = o['basedir']
setIngress(sm)
except:
setIngress(sm, ag_solo)
except: # noqa
cleanup()
raise
restart()
restart(ag_solo)


def doInit(o):
def doInit(o: Options, ag_solo: Runner) -> None:
BASEDIR = o['basedir']
# run 'ag-solo init BASEDIR'
subprocess.run([AG_SOLO, 'init', BASEDIR, '--webhost=' + o['webhost'], '--webport=' + o['webport']], check=True)
ag_solo.run(['init', BASEDIR,
'--webhost=' + o['webhost'], '--webport=' + o['webport']],
check=True)


def main():
o = Options()
def main(argv: List[str],
environ: Dict[str, str], env_update: Callable[[Dict[str, str]], None],
cwd: Path,
run: CP, execvp: CN,
source_file: Path, input: Callable[[str], str],
makeRequest: Callable[..., Request_T],
wormhole: WormHoleModI) -> None:
o = Options(argv, environ)
o.parseOptions()
pkeyFile = os.path.join(o['basedir'], 'ag-cosmos-helper-address')
ag_solo = Runner.locate(source_file, run, execvp, name='ag-solo')
pkeyFile = cwd / o['basedir'] / 'ag-cosmos-helper-address'
# If the public key file does not exist, just init and run.
if not os.path.exists(pkeyFile):
react(run_client, (o, pkeyFile))
sys.exit(1)
if not pkeyFile.exists():
react(run_client, (o, pkeyFile, ag_solo, cwd, wormhole))
raise SystemExit(1)

yesno = input('Type "yes" to reset state from ' + o['netconfig'] + ', anything else cancels: ')
yesno = input('Type "yes" to reset state from ' + o['netconfig'] +
', anything else cancels: ')
if yesno.strip() != 'yes':
print('Cancelling!')
sys.exit(1)
log.warning('Cancelling!')
raise SystemExit(1)

# Download the netconfig.
print('downloading netconfig from', o['netconfig'])
req = urllib.request.Request(o['netconfig'], data=None, headers={'User-Agent': USER_AGENT})
log.info('downloading netconfig from', o['netconfig'])
req = urllib.request.Request(o['netconfig'], data=None,
headers={'User-Agent': USER_AGENT})
resp = urllib.request.urlopen(req)
encoding = resp.headers.get_content_charset('utf-8')
decoded = resp.read().decode(encoding)
netconfig = json.loads(decoded)

connections_json = os.path.join(o['basedir'], 'connections.json')
conns = []
connections_json = cwd / o['basedir'] / 'connections.json'
conns = [] # type: List[Dict[str, str]]
try:
f = open(connections_json)
f = connections_json.open()
conns = json.loads(f.read())
except FileNotFoundError:
pass

# Maybe just run the ag-solo command if our params already match.
for conn in conns:
if 'GCI' in conn and conn['GCI'] == netconfig['gci']:
print('Already have an entry for ' + conn['GCI'] + '; not replacing')
restart()
sys.exit(1)
log.warning('Already have an entry for %s; not replacing',
conn['GCI'])
restart(ag_solo)
raise SystemExit(1)

# Blow away everything except the key file and state dir.
helperStateDir = os.path.join(o['basedir'], 'ag-cosmos-helper-statedir')
for name in os.listdir(o['basedir']):
p = os.path.join(o['basedir'], name)
if p == pkeyFile or p == helperStateDir:
helperStateDir = cwd / o['basedir'] / 'ag-cosmos-helper-statedir'
for p in (cwd / o['basedir']).listdir():
if p.samefile(pkeyFile) or p.samefile(helperStateDir):
continue
if os.path.isdir(p) and not os.path.islink(p):
shutil.rmtree(p)
if p.isdir() and not p.islink():
p.rmtree()
else:
os.remove(p)
p.remove()

# Upgrade the ag-solo files.
doInit(o)
doInit(o, ag_solo)

setIngress(netconfig, ag_solo)
restart(ag_solo)
raise SystemExit(1)


if __name__ == '__main__':
def _script_io() -> None:
from io import open as io_open
from os import environ, execvp
from shutil import rmtree
from subprocess import run
from sys import argv
from urllib.request import Request
import os.path

import wormhole # type: ignore

cwd = Path('.', io_open, os.path.join, os.path.basename,
os.path.exists, os.path.isdir, os.path.islink,
os.path.realpath, os.path.samefile, rmtree)
main(argv[:], environ.copy(), environ.update,
cwd=cwd,
run=run, execvp=execvp,
source_file=cwd / __file__,
input=input,
makeRequest=Request,
wormhole=wormhole)

setIngress(netconfig)
restart()
sys.exit(1)
_script_io()

0 comments on commit 7a85ca8

Please sign in to comment.