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

994 lines
30 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
"""
Blockstack-client
~~~~~
copyright: (c) 2014 by Halfmoon Labs, Inc.
copyright: (c) 2015 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 os
import itertools
import logging
import traceback
import uuid
import urllib2
from binascii import hexlify
from ConfigParser import SafeConfigParser
import virtualchain
from .backend.utxo import *
from .constants import *
def get_logger(name="blockstack-client", debug=DEBUG):
logger = virtualchain.get_logger(name)
logger.setLevel(logging.DEBUG if debug else logging.INFO)
return logger
log = get_logger('blockstack-client')
# NOTE: duplicated from blockstack-core and streamlined.
def op_get_opcode_name(op_string):
"""
Get the name of an opcode, given the 'op' byte sequence of the operation.
"""
global OPCODE_NAMES
# special case...
if op_string == '{}:'.format(NAME_REGISTRATION):
return 'NAME_RENEWAL'
op = op_string[0]
if op not in OPCODE_NAMES:
raise Exception('No such operation "{}"'.format(op))
return OPCODE_NAMES[op]
def url_to_host_port(url, port=DEFAULT_BLOCKSTACKD_PORT):
"""
Given a URL, turn it into (host, port).
Return (None, None) on invalid URL
"""
if not url.startswith('http://') or not url.startswith('https://'):
url = 'http://' + url
urlinfo = urllib2.urlparse.urlparse(url)
hostport = urlinfo.netloc
parts = hostport.split('@')
if len(parts) > 2:
return None, None
if len(parts) == 2:
hostport = parts[1]
parts = hostport.split(':')
if len(parts) > 2:
return None, None
if len(parts) == 2:
try:
port = int(parts[1])
assert 0 < port < 65535, 'Invalid port'
except TypeError:
return None, None
return parts[0], port
def atlas_inventory_to_string( inv ):
"""
Inventory to string (bitwise big-endian)
"""
ret = ""
for i in xrange(0, len(inv)):
for j in xrange(0, 8):
bit_index = 1 << (7 - j)
val = (ord(inv[i]) & bit_index)
if val != 0:
ret += "1"
else:
ret += "0"
return ret
def interactive_prompt(message, parameters, default_opts):
"""
Prompt the user for a series of parameters
Return a dict mapping the parameter name to the
user-given value.
"""
# pretty-print the message
lines = message.split('\n')
max_line_len = max([len(l) for l in lines])
print('-' * max_line_len)
print(message)
print('-' * max_line_len)
ret = {}
for param in parameters:
formatted_param = param
prompt_str = '{}: '.format(formatted_param)
if param in default_opts:
prompt_str = '{} (default: "{}"): '.format(formatted_param, default_opts[param])
try:
value = raw_input(prompt_str)
except KeyboardInterrupt:
log.debug('Exiting on keyboard interrupt')
sys.exit(0)
if len(value) > 0:
ret[param] = value
elif param in default_opts:
ret[param] = default_opts[param]
else:
ret[param] = None
return ret
def find_missing(message, all_params, given_opts, default_opts, header=None, prompt_missing=True):
"""
Find and interactively prompt the user for missing parameters,
given the list of all valid parameters and a dict of known options.
Return the (updated dict of known options, missing, num_prompted), with the user's input.
"""
# are we missing anything?
missing_params = list(set(all_params) - set(given_opts))
num_prompted = 0
if not missing_params:
return given_opts, missing_params, num_prompted
if not prompt_missing:
# count the number missing, and go with defaults
missing_values = set(default_opts) - set(given_opts)
num_prompted = len(missing_values)
given_opts.update(default_opts)
else:
if header is not None:
print('-' * len(header))
print(header)
missing_values = interactive_prompt(message, missing_params, default_opts)
num_prompted = len(missing_values)
given_opts.update(missing_values)
return given_opts, missing_params, num_prompted
def opt_strip(prefix, opts):
"""
Given a dict of opts that start with prefix,
remove the prefix from each of them.
"""
ret = {}
for opt_name, opt_value in opts.items():
# remove prefix
if opt_name.startswith(prefix):
opt_name = opt_name[len(prefix):]
ret[opt_name] = opt_value
return ret
def opt_restore(prefix, opts):
"""
Given a dict of opts, add the given prefix to each key
"""
return {prefix + name: value for name, value in opts.items()}
def default_bitcoind_opts(config_file=None, prefix=False):
"""
Get our default bitcoind options, such as from a config file,
or from sane defaults
"""
default_bitcoin_opts = virtualchain.get_bitcoind_config(config_file=config_file)
# drop dict values that are None
default_bitcoin_opts = {k: v for k, v in default_bitcoin_opts.items() if v is not None}
# strip 'bitcoind_'
if not prefix:
default_bitcoin_opts = opt_strip('bitcoind_', default_bitcoin_opts)
return default_bitcoin_opts
def client_uuid_path(config_dir=CONFIG_DIR):
"""
where is the client UUID stored
"""
uuid_path = os.path.join(config_dir, 'client.uuid')
return uuid_path
def device_id_path(config_dir=CONFIG_DIR):
"""
get device ID path
"""
id_path = os.path.join(config_dir, 'client.device_id')
return id_path
def get_or_set_uuid(config_dir=CONFIG_DIR):
"""
Get or set the UUID for this installation.
Return the UUID either way
Return None on failure
"""
uuid_path = client_uuid_path(config_dir=config_dir)
u = None
if os.path.exists(uuid_path):
try:
with open(uuid_path, 'r') as f:
u = f.read()
u = u.strip()
except Exception as e:
log.exception(e)
return None
else:
try:
u = str(uuid.uuid4())
if not os.path.exists(config_dir):
os.makedirs(config_dir)
with open(uuid_path, 'w') as f:
f.write(u)
f.flush()
os.fsync(f.fileno())
except Exception as e:
log.exception(e)
return None
return u
def get_local_device_id(config_dir=CONFIG_DIR):
"""
Get the local device ID
"""
id_path = device_id_path(config_dir=config_dir)
did = None
if os.path.exists(id_path):
try:
with open(id_path, 'r') as f:
did = f.read()
return did
except Exception as e:
log.exception(e)
return get_or_set_uuid(config_dir=config_dir)
def get_all_device_ids(config_path=CONFIG_PATH):
"""
Get the list of all device IDs that use this wallet
The first device ID is guaranteed to be the local device ID
"""
local_device_id = get_local_device_id(config_dir=os.path.dirname(config_path))
device_ids = [local_device_id]
conf = get_config(config_path)
assert conf
if conf.has_key('default_devices'):
device_ids += filter(lambda x: len(x) > 0, conf['default_devices'].split(','))
return device_ids
def configure(config_file=CONFIG_PATH, force=False, interactive=True):
"""
Configure blockstack-client: find and store configuration parameters to the config file.
Optionally prompt for missing data interactively (with interactive=True). Or, raise an exception
if there are any fields missing.
Optionally force a re-prompting for all configuration details (with force=True)
Return {
'blockstack-client': { ... },
'bitcoind': { ... },
'blockchain-reader': { ... },
'blockchain-writer': { ... },
'uuid': ...
}
"""
global SUPPORTED_UTXO_PROVIDERS, SUPPORTED_UTXO_PARAMS, SUPPORTED_UTXO_PROMPT_MESSAGES
if not os.path.exists(config_file) and interactive:
# definitely ask for everything
force = True
config_dir = os.path.dirname(config_file)
# get blockstack client opts
blockstack_message = (
'Your client does not have enough information to connect\n'
'to a Blockstack server. Please supply the following\n'
'parameters, or press [ENTER] to select the default value.'
)
blockstack_opts = {}
blockstack_opts_defaults = read_config_file(path=config_file)['blockstack-client']
blockstack_params = blockstack_opts_defaults.keys()
if not force:
# defaults
blockstack_opts = read_config_file(path=config_file)['blockstack-client']
blockstack_opts, missing_blockstack_opts, num_blockstack_opts_prompted = find_missing(
blockstack_message,
blockstack_params,
blockstack_opts,
blockstack_opts_defaults,
prompt_missing=interactive
)
# get bitcoind options
bitcoind_message = (
'Blockstack does not have enough information to connect\n'
'to bitcoind. Please supply the following parameters, or\n'
'press [ENTER] to select the default value.'
)
bitcoind_opts = {}
bitcoind_opts_defaults = default_bitcoind_opts(config_file=config_file)
bitcoind_params = bitcoind_opts_defaults.keys()
if not force:
# get default set of bitcoind opts
bitcoind_opts = default_bitcoind_opts(config_file=config_file)
# get any missing bitcoind fields
bitcoind_opts, missing_bitcoin_opts, num_bitcoind_prompted = find_missing(
bitcoind_message,
bitcoind_params,
bitcoind_opts,
bitcoind_opts_defaults,
prompt_missing=interactive
)
# find the blockchain reader
blockchain_reader = blockstack_opts.get('blockchain_reader')
while blockchain_reader not in SUPPORTED_UTXO_PROVIDERS:
if not(interactive or force):
raise Exception('No blockchain reader given')
# prompt for it?
blockchain_message = (
'NOTE: Blockstack currently requires an external API\n'
'for querying the blockchain. The set of supported\n'
'service providers are:\n'
'\t\n'.join(SUPPORTED_UTXO_PROVIDERS) + '\n'
'Please enter the requisite information here.'
)
blockchain_reader_dict = interactive_prompt(blockchain_message, ['blockchain_reader'], {})
blockchain_reader = blockchain_reader_dict['blockchain_reader']
blockchain_reader_defaults = default_utxo_provider_opts(blockchain_reader, config_file=config_file)
blockchain_reader_params = SUPPORTED_UTXO_PARAMS[blockchain_reader]
# get current set of reader opts
blockchain_reader_opts = {} if force else blockchain_reader_defaults
blockchain_reader_opts, missing_reader_opts, num_reader_opts_prompted = find_missing(
SUPPORTED_UTXO_PROMPT_MESSAGES[blockchain_reader],
blockchain_reader_params,
blockchain_reader_opts,
blockchain_reader_defaults,
header='Blockchain reader configuration',
prompt_missing=interactive
)
blockchain_reader_opts['utxo_provider'] = blockchain_reader_defaults['utxo_provider']
# find the blockchain writer
blockchain_writer = blockstack_opts.get('blockchain_writer')
while blockchain_writer not in SUPPORTED_UTXO_PROVIDERS:
if not(interactive or force):
raise Exception('No blockchain reader given')
# prompt for it?
blockchain_message = (
'NOTE: Blockstack currently requires an external API\n'
'for sending transactions to the blockchain. The set\n'
'of supported service providers are:\n'
'\t\n'.join(SUPPORTED_UTXO_PROVIDERS) + '\n'
'Please enter the requisite information here.'
)
blockchain_writer_dict = interactive_prompt(blockchain_message, ['blockchain_writer'], {})
blockchain_writer = blockchain_writer_dict['blockchain_writer']
blockchain_writer_defaults = default_utxo_provider_opts(blockchain_writer, config_file=config_file)
blockchain_writer_params = SUPPORTED_UTXO_PARAMS[blockchain_writer]
# get current set of writer opts
blockchain_writer_opts = {} if force else blockchain_writer_defaults
blockchain_writer_opts, missing_writer_opts, num_writer_opts_prompted = find_missing(
SUPPORTED_UTXO_PROMPT_MESSAGES[blockchain_writer],
blockchain_writer_params,
blockchain_writer_opts,
blockchain_writer_defaults,
header='Blockchain writer configuration',
prompt_missing=interactive
)
blockchain_writer_opts['utxo_provider'] = blockchain_writer_defaults['utxo_provider']
missing_opts = [missing_bitcoin_opts, missing_writer_opts, missing_reader_opts, missing_blockstack_opts]
if not interactive and any(missing_opts):
# cannot continue
raise Exception(
'Missing configuration fields: {}'.format(
','.join(list(itertools.chain(*missing_opts)))
)
)
# ask for contact info, so we can send out notifications for bugfixes and
# upgrades
if blockstack_opts.get('email') is None:
email_msg = (
'Would you like to receive notifications\n'
'from the developers when there are critical\n'
'updates available to install?\n\n'
'If so, please enter your email address here.\n'
'If not, leave this field blank.\n\n'
'Your email address will be used solely\n'
'for this purpose.\n'
)
email_opts, _, email_prompted = find_missing(
email_msg, ['email'], {}, {'email': ''}, prompt_missing=interactive
)
# merge with blockstack section
num_blockstack_opts_prompted += 1
blockstack_opts['email'] = email_opts['email']
# get client UUID for analytics
u = get_or_set_uuid(config_dir=config_dir)
if u is None:
raise Exception('Failed to get/set UUID')
ret = {
'blockstack-client': blockstack_opts,
'bitcoind': bitcoind_opts,
'blockchain-reader': blockchain_reader_opts,
'blockchain-writer': blockchain_writer_opts
}
# if we prompted, then save
if any([num_bitcoind_prompted, num_reader_opts_prompted, num_writer_opts_prompted, num_blockstack_opts_prompted]):
print('Saving configuration to {}'.format(config_file), file=sys.stderr)
# rename appropriately, so other packages can find them
write_config_file(ret, config_file)
# preserve these extra helper fields
blockstack_opts['path'] = config_file
if config_file is not None:
blockstack_opts['dir'] = os.path.dirname(config_file)
else:
blockstack_opts['dir'] = None
# set this here, so we don't save it
ret['uuid'] = u
return ret
def write_config_file(opts, config_file):
"""
Write our config file with the given options dict.
Each key is a section name, and each value is the list of options.
Return True on success
Raise on error
"""
if 'blockstack-client' in opts:
assert 'path' not in opts['blockstack-client']
assert 'dir' not in opts['blockstack-client']
assert 'path' not in opts
assert 'dir' not in opts
parser = SafeConfigParser()
if os.path.exists(config_file):
parser.read(config_file)
for sec_name in opts:
sec_opts = opts[sec_name]
if parser.has_section(sec_name):
parser.remove_section(sec_name)
parser.add_section(sec_name)
for opt_name, opt_value in sec_opts.items():
if opt_value is None:
opt_value = ''
parser.set(sec_name, opt_name, '{}'.format(opt_value))
with open(config_file, 'w') as fout:
os.fchmod(fout.fileno(), 0600)
parser.write(fout)
return True
def write_config_field(config_path, section_name, field_name, field_value):
"""
Set a particular config file field
Return True on success
Return False on error
"""
if not os.path.exists(config_path):
return False
parser = SafeConfigParser()
parser.read(config_path)
parser.set(section_name, field_name, '{}'.format(field_value))
with open(config_path, 'w') as fout:
os.fchmod(fout.fileno(), 0600)
parser.write(fout)
return True
def set_advanced_mode(status, config_path=CONFIG_PATH):
"""
Enable or disable advanced mode
@status must be a bool
"""
return write_config_field(config_path, 'blockstack-client', 'advanced_mode', str(status))
def get_utxo_provider_client(config_path=CONFIG_PATH):
"""
Get or instantiate our blockchain UTXO provider's client.
Return None if we were unable to connect
"""
# acquire configuration (which we should already have)
opts = configure(interactive=False, config_file=config_path)
reader_opts = opts['blockchain-reader']
try:
utxo_provider = connect_utxo_provider(reader_opts)
return utxo_provider
except Exception as e:
log.exception(e)
return
return
def get_tx_broadcaster(config_path=CONFIG_PATH):
"""
Get or instantiate our blockchain UTXO provider's transaction broadcaster.
fall back to the utxo provider client, if one is not designated
"""
# acquire configuration (which we should already have)
opts = configure(interactive=False, config_file=config_path)
writer_opts = opts['blockchain-writer']
try:
blockchain_broadcaster = connect_utxo_provider(writer_opts)
return blockchain_broadcaster
except Exception as e:
log.exception(e)
return
return
def str_to_bool(s):
"""
Convert 'true' to True; 'false' to False
"""
if type(s) not in [str, unicode]:
raise ValueError('"{}" is not a string'.format(s))
if s.lower() == 'false':
return False
elif s.lower() == 'true':
return True
else:
raise ValueError('Indeterminate boolean "{}"'.format(s))
def read_config_file(path=CONFIG_PATH):
"""
Read or make a new empty config file with sane defaults.
Return the config dict on success
Raise on error
"""
global CONFIG_PATH, BLOCKSTACKD_SERVER, BLOCKSTACKD_PORT
# try to create
if path is not None:
dirname = os.path.dirname(path)
if not os.path.exists(dirname):
os.makedirs(dirname)
if not os.path.isdir(dirname):
raise Exception('Not a directory: {}'.format(path))
client_uuid = get_or_set_uuid(config_dir=os.path.dirname(path))
if client_uuid is None:
raise Exception("Failed to get client device ID")
config_dir = os.path.dirname(path)
if path is None or not os.path.exists(path):
parser = SafeConfigParser()
parser.add_section('blockstack-client')
parser.set('blockstack-client', 'server', str(BLOCKSTACKD_SERVER))
parser.set('blockstack-client', 'port', str(BLOCKSTACKD_PORT))
parser.set('blockstack-client', 'metadata', METADATA_DIRNAME)
parser.set('blockstack-client', 'storage_drivers', BLOCKSTACK_DEFAULT_STORAGE_DRIVERS)
parser.set('blockstack-client', 'storage_drivers_local', 'disk')
parser.set('blockstack-client', 'storage_drivers_required_write', BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE)
parser.set('blockstack-client', 'advanced_mode', 'false')
parser.set('blockstack-client', 'api_endpoint_port', str(DEFAULT_API_PORT))
parser.set('blockstack-client', 'queue_path', str(DEFAULT_QUEUE_PATH))
parser.set('blockstack-client', 'poll_interval', str(DEFAULT_POLL_INTERVAL))
parser.set('blockstack-client', 'rpc_detach', 'True')
parser.set('blockstack-client', 'blockchain_reader', DEFAULT_BLOCKCHAIN_READER)
parser.set('blockstack-client', 'blockchain_writer', DEFAULT_BLOCKCHAIN_WRITER)
parser.set('blockstack-client', 'anonymous_statistics', 'True')
parser.set('blockstack-client', 'client_version', VERSION)
parser.set('blockstack-client', 'default_devices', '')
api_pass = os.urandom(32)
parser.set('blockstack-client', 'api_password', hexlify(api_pass))
if path is not None:
try:
with open(path, 'w') as f:
parser.write(f)
f.flush()
os.fsync(f.fileno())
except:
traceback.print_exc()
log.error('Failed to write default configuration file to "{}".'.format(path))
return False
parser.add_section('blockchain-reader')
parser.set('blockchain-reader', 'utxo_provider', DEFAULT_BLOCKCHAIN_READER)
parser.add_section('blockchain-writer')
parser.set('blockchain-writer', 'utxo_provider', DEFAULT_BLOCKCHAIN_WRITER)
parser.add_section('bitcoind')
bitcoind_config = default_bitcoind_opts()
for k, v in bitcoind_config.items():
if v is not None:
parser.set('bitcoind', k, '{}'.format(v))
# save
if path is not None:
with open(path, 'w') as f:
parser.write(f)
f.flush()
os.fsync(f.fileno())
# now read it back
parser = SafeConfigParser()
parser.read(path)
# these are booleans--convert them
bool_values = {
'blockstack-client': [
'advanced_mode',
'rpc_detach',
'anonymous_statistics',
'authenticate_api',
]
}
ret = {}
for sec in parser.sections():
ret[sec] = {}
for opt in parser.options(sec):
if opt in bool_values.get(sec, {}):
# decode to bool
ret[sec][opt] = str_to_bool(parser.get(sec, opt))
else:
# literal
ret[sec][opt] = parser.get(sec, opt)
if 'advanced_mode' not in ret.get('blockstack-client', {}):
ret['blockstack-client']['advanced_mode'] = False
ret['path'] = path
ret['dir'] = os.path.dirname(path)
return ret
def get_config(path=CONFIG_PATH, interactive=False):
"""
Read our config file (legacy compat).
Flatten the resulting config:
* make all bitcoin-specific fields start with 'bitcoind_' (makes this config compatible with virtualchain)
* keep only the blockstack-client and bitcoin fields
Return our flattened configuration (as a dict) on success.
Return None on error
"""
try:
opts = configure(config_file=path, interactive=interactive)
except Exception as e:
log.exception(e)
return None
# flatten
blockstack_opts = opts['blockstack-client']
bitcoin_opts = opts['bitcoind']
bitcoin_opts = opt_restore('bitcoind_', bitcoin_opts)
blockstack_opts.update(bitcoin_opts)
# pass along the config path and dir, and statistics info
blockstack_opts['path'] = path
blockstack_opts['dir'] = os.path.dirname(path)
blockstack_opts['uuid'] = opts['uuid']
blockstack_opts['client_version'] = blockstack_opts.get('client_version', '')
if 'anonymous_statistics' not in blockstack_opts:
# not disabled
blockstack_opts['anonymous_statistics'] = True
return blockstack_opts
def get_version_parts(whole, func):
return [func(_.strip()) for _ in whole[0:3]]
def semver_match(v1, v2):
"""
Verify that two semantic version strings match:
the major and the minor versions must be equal.
Patch versions can be different
"""
v1_parts = v1.split('.')
v2_parts = v2.split('.')
if len(v1_parts) < 3 or len(v2_parts) < 3:
# one isn't a semantic version
return False
v1_major, v1_minor, v1_patch = get_version_parts(v1_parts, str)
v2_major, v2_minor, v2_patch = get_version_parts(v2_parts, str)
# NOTE: patch versions are not relevant here.
return [v1_major, v1_minor] == [v2_major, v2_minor]
def semver_newer(v1, v2):
"""
Verify (as semantic versions) if v1 < v2
Patch versions can be different
"""
v1_parts = v1.split('.')
v2_parts = v2.split('.')
if len(v1_parts) < 3 or len(v2_parts) < 3:
# one isn't a semantic version
return False
v1_major, v1_minor, v1_patch = get_version_parts(v1_parts, int)
v2_major, v2_minor, v2_patch = get_version_parts(v2_parts, int)
if v1_major > v2_major:
return False
if v1_major == v2_major and v1_minor >= v2_minor:
return False
return True
def backup_config_file(config_path=CONFIG_PATH):
"""
Back up the given config file
Return the backup file
"""
if not os.path.exists(config_path):
return None
legacy_path = config_path + ".legacy.{}".format(int(time.time()))
while os.path.exists(legacy_path):
time.sleep(1.0)
legacy_path = config_path + ".legacy.{}".format(int(time.time()))
log.warning('Back up old config file from {} to {}'.format(config_path, legacy_path))
shutil.move(config_path, legacy_path)
return legacy_path
def configure_zonefile(name, zonefile, data_pubkey=None):
"""
Given a name and zonefile, help the user configure the
zonefile information to store (just URLs for now).
@zonefile must be parsed and must be a dict.
Return the new zonefile on success
Return None if the zonefile did not change.
"""
from .zonefile import make_empty_zonefile
if zonefile is None:
print('WARNING: No zonefile could be found.')
print('WARNING: Creating an empty zonefile.')
zonefile = make_empty_zonefile(name, data_pubkey)
running = True
do_update = True
old_zonefile = {}
old_zonefile.update( zonefile )
while running:
public_key = None
try:
public_key = user_zonefile_data_pubkey(zonefile)
except ValueError:
# multiple keys
public_key = None
urls = user_zonefile_urls(zonefile)
if urls is None:
urls = []
url_drivers = {}
# which drivers?
for url in urls:
drivers = get_drivers_for_url(url)
url_drivers[url] = drivers
print('-' * 80)
if public_key is not None:
print('Data public key: {}'.format(public_key))
else:
print('Data public key: (not set)')
print('')
print('Profile replicas ({}):'.format(len(urls)))
if len(urls) > 0:
for i in xrange(0, len(urls)):
url = urls[i]
drivers = get_drivers_for_url(url)
print('({}) [{}] at {}'.format(i+1, ','.join([d.__name__ for d in drivers]), url))
else:
print('(none)')
print('')
print('What would you like to do?')
print('(a) Add URL')
print('(b) Remove URL')
print('(c) Save zonefile')
print('(d) Do not save zonefile')
print('')
selection = raw_input('Selection: ').lower()
if selection == 'd':
do_update = False
break
elif selection == 'a':
# add a url
while True:
try:
new_url = raw_input('Enter the new profile URL: ')
except KeyboardInterrupt:
print('Keyboard interrupt')
break
new_url = new_url.strip()
# do any drivers accept this URL?
drivers = get_drivers_for_url( new_url )
if len(drivers) == 0:
print('No drivers can handle "{}"'.format(new_url))
continue
else:
# add to the zonefile
new_zonefile = add_user_zonefile_url( zonefile, new_url )
if new_zonefile is None:
print('Duplicate URL')
continue
else:
zonefile = new_zonefile
break
elif selection == 'b':
# remove a URL
url_to_remove = None
while True:
try:
url_to_remove = raw_input('Which URL do you want to remove? ({}-{}): '.format(1, len(urls)+1))
try:
url_to_remove = int(url_to_remove)
assert 1 <= url_to_remove and url_to_remove <= len(urls)+1
except:
print('Bad selection')
continue
break
except KeyboardInterrupt:
running = False
print('Keyboard interrupt')
break
if url_to_remove is not None:
# remove this URL
url = urls[url_to_remove-1]
new_zonefile = remove_user_zonefile_url( zonefile, url )
if new_zonefile is None:
print('BUG: failed to remove url "{}" from zonefile\n{}\n'.format(url, json.dumps(zonefile, indent=4, sort_keys=True)))
os.abort()
else:
zonefile = new_zonefile
break
elif selection == 'c':
# save zonefile
break
# did the zonefile change?
if zonefile == old_zonefile:
# no changes
return None
else:
return zonefile