From 7fa5752ab49b2366df26f75fd14d9e638376768a Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Mon, 22 Jul 2019 16:26:30 -0700 Subject: [PATCH 01/10] add separate start, provision, and help commands; implement basic wallet provisioning Signed-off-by: Andrew Whitehead --- aries_cloudagent/__init__.py | 102 +-- aries_cloudagent/commands/__init__.py | 30 + aries_cloudagent/commands/help.py | 20 + aries_cloudagent/commands/provision.py | 75 +++ aries_cloudagent/commands/start.py | 86 +++ aries_cloudagent/config/argparse.py | 860 ++++++++++++++----------- aries_cloudagent/config/util.py | 43 ++ aries_cloudagent/wallet/base.py | 24 + aries_cloudagent/wallet/basic.py | 9 + aries_cloudagent/wallet/indy.py | 1 + bin/aca-py | 13 +- docker/manage | 11 +- 12 files changed, 781 insertions(+), 493 deletions(-) create mode 100644 aries_cloudagent/commands/__init__.py create mode 100644 aries_cloudagent/commands/help.py create mode 100644 aries_cloudagent/commands/provision.py create mode 100644 aries_cloudagent/commands/start.py create mode 100644 aries_cloudagent/config/util.py diff --git a/aries_cloudagent/__init__.py b/aries_cloudagent/__init__.py index c5389c6d5b..9b62c27cb0 100644 --- a/aries_cloudagent/__init__.py +++ b/aries_cloudagent/__init__.py @@ -1,101 +1 @@ -"""Entrypoint.""" - -import asyncio -import functools -import os -import signal - -from aiohttp import ClientSession - -from .conductor import Conductor -from .config.argparse import get_settings, parse_args -from .config.default_context import DefaultContextBuilder -from .config.logging import LoggingConfigurator -from .postgres import load_postgres_plugin - - -async def get_genesis_transactions(genesis_url: str): - """Get genesis transactions.""" - headers = {} - headers["Content-Type"] = "application/json" - async with ClientSession() as client_session: - response = await client_session.get(genesis_url, headers=headers) - genesis_txns = await response.text() - return genesis_txns - - -async def start_app(conductor: Conductor): - """Start up.""" - await conductor.setup() - await conductor.start() - - -async def shutdown_app(conductor: Conductor): - """Shut down.""" - print("\nShutting down") - await conductor.stop() - tasks = [ - task - for task in asyncio.Task.all_tasks() - if task is not asyncio.tasks.Task.current_task() - ] - for task in tasks: - task.cancel() - await asyncio.gather(*tasks, return_exceptions=True) - asyncio.get_event_loop().stop() - - -def main(): - """Entrypoint.""" - args = parse_args() - settings = get_settings(args) - - # Set up logging - log_config = settings.get("log.config") - log_level = settings.get("log.level") or os.getenv("LOG_LEVEL") - LoggingConfigurator.configure(log_config, log_level) - - # Fetch genesis transactions if necessary - if not settings.get("ledger.genesis_transactions") and settings.get( - "ledger.genesis_url" - ): - loop = asyncio.get_event_loop() - settings["ledger.genesis_transactions"] = loop.run_until_complete( - get_genesis_transactions(settings["ledger.genesis_url"]) - ) - - # Load postgres plug-in if necessary - if ( - settings.get("wallet.type") - and settings.get("wallet.storage_type") == "postgres_storage" - ): - if args.wallet_storage_type == "postgres_storage": - load_postgres_plugin() - - # Support WEBHOOK_URL environment variable - webhook_url = os.environ.get("WEBHOOK_URL") - if webhook_url: - webhook_urls = list(settings.get("admin.webhook_urls") or []) - webhook_urls.append(webhook_url) - settings["admin.webhook_urls"] = webhook_urls - - # Create the Conductor instance - context_builder = DefaultContextBuilder(settings) - conductor = Conductor(context_builder) - - # Run the application - loop = asyncio.get_event_loop() - loop.add_signal_handler( - signal.SIGTERM, - functools.partial(asyncio.ensure_future, shutdown_app(conductor), loop=loop), - ) - asyncio.ensure_future(start_app(conductor), loop=loop) - - try: - loop.run_forever() - except KeyboardInterrupt: - loop.run_until_complete(shutdown_app(conductor)) - - -if __name__ == "__main__": - main() # pragma: no cover +"""Aries Cloud Agent""" diff --git a/aries_cloudagent/commands/__init__.py b/aries_cloudagent/commands/__init__.py new file mode 100644 index 0000000000..dc010ff17e --- /dev/null +++ b/aries_cloudagent/commands/__init__.py @@ -0,0 +1,30 @@ +from importlib import import_module +from typing import Sequence + + +def available_commands(): + return [ + {"name": "help", "summary": "Print available commands"}, + {"name": "provision", "summary": "Provision an agent"}, + {"name": "start", "summary": "Start a new agent process"}, + ] + + +def load_command(command: str): + module = None + module_path = None + for cmd in available_commands(): + if cmd["name"] == command: + module = cmd["name"] + if "module" in cmd: + module_path = cmd["module"] + break + if module and not module_path: + module_path = f"{__package__}.{module}" + if module_path: + return import_module(module_path) + + +def run_command(command: str, argv: Sequence[str] = None): + module = load_command(command) or load_command("help") + module.execute(argv) diff --git a/aries_cloudagent/commands/help.py b/aries_cloudagent/commands/help.py new file mode 100644 index 0000000000..61b11252cf --- /dev/null +++ b/aries_cloudagent/commands/help.py @@ -0,0 +1,20 @@ +from argparse import ArgumentParser +from typing import Sequence + + +def execute(argv: Sequence[str] = None): + from . import available_commands, load_command + + parser = ArgumentParser() + subparsers = parser.add_subparsers() + for cmd in available_commands(): + if cmd["name"] == "help": + continue + module = load_command(cmd["name"]) + subparser = subparsers.add_parser(cmd["name"], help=cmd["summary"]) + module.init_argument_parser(subparser) + parser.print_help() + + +if __name__ == "__main__": + execute() diff --git a/aries_cloudagent/commands/provision.py b/aries_cloudagent/commands/provision.py new file mode 100644 index 0000000000..3c4aff434d --- /dev/null +++ b/aries_cloudagent/commands/provision.py @@ -0,0 +1,75 @@ +import asyncio +from argparse import ArgumentParser +from typing import Sequence + +from ..config import argparse as arg +from ..config.default_context import DefaultContextBuilder +from ..config.util import common_config +from ..error import BaseError +from ..wallet.base import BaseWallet +from ..wallet.crypto import seed_to_did + + +class ProvisionError(BaseError): + """Base exception for provisioning errors.""" + + +def init_argument_parser(parser: ArgumentParser): + return arg.load_argument_groups( + parser, arg.GeneralGroup(), arg.LoggingGroup(), arg.WalletGroup() + ) + + +async def provision(category: str, settings: dict): + """Perform provisioning.""" + context_builder = DefaultContextBuilder(settings) + context = await context_builder.build() + + if category == "wallet": + # Initialize wallet + wallet: BaseWallet = await context.inject(BaseWallet) + print("Wallet type:", wallet.type) + print("Wallet name:", wallet.name) + wallet_seed = context.settings.get("wallet.seed") + public_did_info = await wallet.get_public_did() + if public_did_info: + # If we already have a registered public did and it doesn't match + # the one derived from `wallet_seed` then we error out. + # TODO: Add a command to change public did explicitly + if wallet_seed and seed_to_did(wallet_seed) != public_did_info.did: + raise ProvisionError( + "New seed provided which doesn't match the registered" + + f" public did {public_did_info.did}" + ) + elif wallet_seed: + public_did_info = await wallet.create_public_did(seed=wallet_seed) + print("Created new public DID") + if public_did_info: + print("Public DID:", public_did_info.did) + print("Verkey:", public_did_info.verkey) + else: + print("No public DID") + + +def execute(argv: Sequence[str] = None): + """Entrypoint.""" + parser = ArgumentParser() + parser.prog += " provision" + parser.add_argument( + dest="provision_category", + type=str, + metavar=(""), + choices=["wallet"], + help="The provision command to invoke", + ) + get_settings = init_argument_parser(parser) + args = parser.parse_args(argv) + settings = get_settings(args) + common_config(settings) + + loop = asyncio.get_event_loop() + loop.run_until_complete(provision(args.provision_category, settings)) + + +if __name__ == "__main__": + execute() diff --git a/aries_cloudagent/commands/start.py b/aries_cloudagent/commands/start.py new file mode 100644 index 0000000000..a544afac46 --- /dev/null +++ b/aries_cloudagent/commands/start.py @@ -0,0 +1,86 @@ +"""Entrypoint.""" + +import asyncio +import functools +import os +import signal +from argparse import ArgumentParser +from typing import Sequence + +from ..conductor import Conductor +from ..config import argparse as arg +from ..config.default_context import DefaultContextBuilder +from ..config.util import common_config + + +async def start_app(conductor: Conductor): + """Start up.""" + await conductor.setup() + await conductor.start() + + +async def shutdown_app(conductor: Conductor): + """Shut down.""" + print("\nShutting down") + await conductor.stop() + tasks = [ + task + for task in asyncio.Task.all_tasks() + if task is not asyncio.tasks.Task.current_task() + ] + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + asyncio.get_event_loop().stop() + + +def init_argument_parser(parser: ArgumentParser): + return arg.load_argument_groups( + parser, + arg.AdminGroup(), + arg.DebugGroup(), + arg.GeneralGroup(), + arg.LedgerGroup(), + arg.LoggingGroup(), + arg.ProtocolGroup(), + arg.TransportGroup(), + arg.WalletGroup(), + ) + + +def execute(argv: Sequence[str] = None): + """Entrypoint.""" + parser = ArgumentParser() + parser.prog += " start" + get_settings = init_argument_parser(parser) + args = parser.parse_args(argv) + settings = get_settings(args) + common_config(settings) + + # Support WEBHOOK_URL environment variable + webhook_url = os.environ.get("WEBHOOK_URL") + if webhook_url: + webhook_urls = list(settings.get("admin.webhook_urls") or []) + webhook_urls.append(webhook_url) + settings["admin.webhook_urls"] = webhook_urls + + # Create the Conductor instance + context_builder = DefaultContextBuilder(settings) + conductor = Conductor(context_builder) + + # Run the application + loop = asyncio.get_event_loop() + loop.add_signal_handler( + signal.SIGTERM, + functools.partial(asyncio.ensure_future, shutdown_app(conductor), loop=loop), + ) + asyncio.ensure_future(start_app(conductor), loop=loop) + + try: + loop.run_forever() + except KeyboardInterrupt: + loop.run_until_complete(shutdown_app(conductor)) + + +if __name__ == "__main__": + execute() diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 0993473b94..dbac9dd4cc 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -1,393 +1,479 @@ """Command line option parsing.""" +import abc import os -import argparse -from typing import Sequence + +from argparse import ArgumentParser, Namespace from .error import ArgsParseError -PARSER = argparse.ArgumentParser(description="Runs an Aries Cloud Agent.") - - -PARSER.add_argument( - "-it", - "--inbound-transport", - dest="inbound_transports", - type=str, - action="append", - nargs=3, - required=True, - metavar=("", "", ""), - help="Choose which interface(s) to listen on", -) - -PARSER.add_argument( - "-ot", - "--outbound-transport", - dest="outbound_transports", - type=str, - action="append", - required=True, - metavar="", - help="Choose which outbound transport handlers to register", -) - -PARSER.add_argument( - "--log-config", - dest="log_config", - type=str, - metavar="", - default=None, - help="Specifies a custom logging configuration file", -) - -PARSER.add_argument( - "--log-level", - dest="log_level", - type=str, - metavar="", - default=None, - help="Specifies a custom logging level " - + "(debug, info, warning, error, critical)", -) - -PARSER.add_argument( - "-e", - "--endpoint", - type=str, - metavar="", - help="Specify the default endpoint to use when " - + "creating connection invitations and requests", -) - -PARSER.add_argument( - "-l", - "--label", - type=str, - metavar="