Files
stacks-puppet-node/blockstack_client/client.py

387 lines
12 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Blockstack-client
~~~~~
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
copyright: (c) 2016 by Blockstack.org
This file is part of Blockstack-client.
Blockstack-client is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Blockstack-client is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Blockstack-client. If not, see <http://www.gnu.org/licenses/>.
"""
import sys
import os
import importlib
from proxy import *
from virtualchain import SPVClient
import storage
from .constants import CONFIG_PATH, VERSION
from .config import get_logger, get_config, semver_match
log = get_logger()
# ancillary storage providers
STORAGE_IMPL = None
ANALYTICS_KEY = None
def session(conf=None, config_path=CONFIG_PATH, server_host=None, server_port=None, wallet_password=None,
storage_drivers=None, metadata_dir=None, spv_headers_path=None, set_global=False):
"""
Create a blockstack session:
* validate the configuration
* load all storage drivers
* initialize all storage drivers
* load an API proxy to blockstack
conf's fields override specific keyword arguments.
Returns the API proxy object.
"""
if conf is None:
conf = get_config(config_path)
if conf is None:
log.error("Failed to read configuration file {}".format(config_path))
return None
conf_version = conf.get('client_version', '')
if not semver_match(conf_version, VERSION):
log.error("Failed to use legacy configuration file {}".format(config_path))
return None
if conf is not None:
if server_host is None:
server_host = conf['server']
if server_port is None:
server_port = conf['port']
if storage_drivers is None:
storage_drivers = conf['storage_drivers']
if metadata_dir is None:
metadata_dir = conf['metadata']
if spv_headers_path is None:
spv_headers_path = conf['blockchain_headers']
if storage_drivers is None:
msg = ('No storage driver(s) defined in the config file. '
'Please set "storage=" to a comma-separated list of drivers')
log.error(msg)
sys.exit(1)
# create proxy
log.debug('Connect to {}:{}'.format(server_host, server_port))
proxy = BlockstackRPCClient(server_host, server_port)
# load all storage drivers
for storage_driver in storage_drivers.split(','):
storage_impl = load_storage(storage_driver)
if storage_impl is None:
log.error('Failed to load storage driver "{}"'.format(storage_driver))
sys.exit(1)
rc = register_storage(storage_impl, conf)
if not rc:
log.error('Failed to initialize storage driver "{}" ({})'.format(storage_driver, rc))
sys.exit(1)
# initialize SPV
SPVClient.init(spv_headers_path)
proxy.spv_headers_path = spv_headers_path
proxy.conf = conf
if set_global:
set_default_proxy(proxy)
return proxy
def load_storage(module_name):
"""
Load a storage implementation, given its module name.
"""
try:
prefix = 'blockstack_client.backend.drivers.{}'
storage_impl = importlib.import_module(prefix.format(module_name))
storage_impl.__name__ = module_name
log.debug('Loaded storage driver "{}"'.format(module_name))
except ImportError as e:
msg = ('Failed to import blockstack_client.backend.drivers.{}. '
'Please verify that it is installed and is accessible via your PYTHONPATH')
raise Exception(msg.format(module_name))
return storage_impl
def register_storage(storage_impl, conf):
"""
Register a storage implementation.
"""
rc = storage.register_storage(storage_impl)
if rc:
rc = storage_impl.storage_init(conf)
return rc
def get_analytics_key(uuid, proxy=None):
"""
Get the analytics key from the blockstack server
"""
key = os.environ.get('BLOCKSTACK_TEST_ANALYTICS_KEY', None)
if key is not None:
return key
try:
proxy = get_default_proxy() if proxy is None else proxy
key = proxy.get_analytics_key(uuid)
except Exception as e:
log.debug('Failed to get analytics key')
return
key = {} if key is None else key
if 'error' in key:
log.debug('Failed to fetch analytics key: {}'.format(key['error']))
return
key = key.get('analytics_key', None)
if key is not None:
return key
log.debug('No analytics key returned')
return
def analytics_event(event_type, event_payload, config_path=CONFIG_PATH,
proxy=None, analytics_key=None, action_tag='Perform action'):
"""
Log an analytics event
Return True if logged
Return False if not
The client uses 'Perform action' as its action tag, so we can distinguish
client events from server events. The server uses separate action tags.
"""
global ANALYTICS_KEY
try:
import mixpanel
except:
log.debug('mixpanel is not installed; no analytics will be reported')
return False
conf = get_config(path=config_path)
if conf is None:
log.debug('Failed to load config')
return False
if not conf['anonymous_statistics']:
return False
u = conf['uuid']
# use the given analytics key, if possible. or fallback.
analytics_key = ANALYTICS_KEY if analytics_key is None else analytics_key
# no fallback. so fetch from server.
if analytics_key is None:
ANALYTICS_KEY = get_analytics_key(u, proxy=proxy) if ANALYTICS_KEY is None else ANALYTICS_KEY
analytics_key = ANALYTICS_KEY
# all attempts failed. nothing more to do.
if analytics_key is None:
return False
# log the event
log.debug('Track event "{}": {}'.format(event_type, event_payload))
mp = mixpanel.Mixpanel(analytics_key)
mp.track(u, event_type, event_payload)
mp.track(u, action_tag, {})
return True
def analytics_user_register(u, email, config_path=CONFIG_PATH, proxy=None):
"""
Register a user with the analytics service
"""
global ANALYTICS_KEY
try:
import mixpanel
except:
log.debug('mixpanel is not installed; no analytics will be reported')
return False
conf = get_config(path=config_path)
if conf is None:
log.debug('Failed to load config')
return False
if not conf['anonymous_statistics']:
return False
ANALYTICS_KEY = get_analytics_key(u) if ANALYTICS_KEY is None else ANALYTICS_KEY
if ANALYTICS_KEY is None:
return False
# register the user
log.debug('Register user "{}"'.format(u))
mp = mixpanel.Mixpanel(ANALYTICS_KEY)
mp.people_set_once(u, {})
return True
def analytics_user_update(payload, proxy=None):
"""
Update a user's info on the analytics service
"""
global ANALYTICS_KEY
try:
import mixpanel
except:
log.debug('mixpanel is not installed; no analytics will be reported')
return False
conf = get_config(config_path)
if conf is None:
log.debug('Failed to load config')
return False
if not conf['anonymous_statistics']:
return False
u = conf['uuid']
ANALYTICS_KEY = get_analytics_key(u) if ANALYTICS_KEY is None else ANALYTICS_KEY
if ANALYTICS_KEY is None:
return False
# update the user
log.debug('Update user "{}"'.format(u))
mp = mixpanel.Mixpanel(ANALYTICS_KEY)
mp.people_append(u, payload)
return True
def check_storage_setup(config_path=CONFIG_PATH):
"""
Verify whether or not we have successfully upgraded
to the latest storage format
Return {'status': True} on success
Return {'error': ...} on error
"""
from .wallet import get_addresses_from_file
config_dir = os.path.dirname(config_path)
_, _, data_pubkey = get_addresses_from_file(config_dir=config_dir)
if data_pubkey is None:
return {'error': 'Wallet is not set up. Please run `upgrade_wallet`'}
# use a file to indicate that setup is in progress (in case we get interrupted)
setup_complete_path = os.path.join(config_dir, '.storage-setup-complete-{}-{}'.format(data_pubkey, VERSION))
if os.path.exists(setup_complete_path):
# already did this
return {'status': True}
return {'error': 'Storage is not set up'}
def set_storage_setup(config_path=CONFIG_PATH):
"""
Mark that we have successfully setup storage
"""
from .wallet import get_addresses_from_file
# record that we've succeeded
config_dir = os.path.dirname(config_path)
_, _, data_pubkey = get_addresses_from_file(config_dir=config_dir)
if data_pubkey is None:
return {'error': 'Wallet is not set up. Please run `upgrade_wallet`'}
setup_complete_path = os.path.join(config_dir, '.storage-setup-complete-{}-{}'.format(data_pubkey, VERSION))
try:
with open(setup_complete_path, 'w') as f:
pass
except:
log.error("Failed to record successful startup")
return {'error': 'Failed to set up'}
return {'status': True}
def storage_setup(blockchain_id, config_path=CONFIG_PATH, wallet_data=None, password=None, interactive=True):
"""
Set up storage for this blockchain ID
* make sure the wallet has been migrated to the latest format
* bootstrap mutable storage for the identity pointed to by the given name
(making sure our wallet info is consistent with the blockchain ID)
Return {'status': True} if all is well
Return {'error': ..., 'need_migrate': True} if the wallet needs to be updated
Return {'error': ..., 'need_migrate': False} if this is some other error
"""
from .data import data_setup
from .wallet import wallet_setup
config_dir = os.path.dirname(config_path)
# make sure the wallet is migrated
res = wallet_setup(config_path=config_path, wallet_data=wallet_data, password=password, dry_run=True)
if 'error' in res:
log.error("wallet_setup failed")
return res
if res['migrated']:
# wallet must be migrated
log.error("Wallet must be migrated to the latest format first")
return {'error': 'Wallet must be migrated. Please use the `upgrade_wallet` command.', 'need_migrate': True}
wallet = res['wallet']
# are we good to do already?
res = check_storage_setup(config_path=config_path)
if 'error' not in res:
return res
if not interactive and password is None:
log.error("No password given, and not in interactive mode")
return {'error': 'Password required'}
log.debug("Doing one-time setup for Blockstack version {}".format(VERSION))
# make sure we have private key indexes and user listings set up
res = data_setup(blockchain_id, password, wallet_keys=wallet, config_path=config_path)
if 'error' in res:
log.error("data_setup failed")
res['need_migrate'] = False
return res
res = set_storage_setup(config_path=config_path)
if 'error' in res:
return res
return {'status': True}