Permalink
0befae4 Dec 21, 2017
@battlemidget @johnsca @mikemccracken
461 lines (399 sloc) 17.8 KB
""" Application entrypoint
"""
import argparse
import asyncio
import configparser
import os
import os.path as path
import pathlib
import platform
import signal
import subprocess
import sys
import textwrap
import uuid
import raven
import yaml
from kv import KV
from prettytable import PrettyTable
from raven.transport.requests import RequestsHTTPTransport
from termcolor import colored
from ubuntui.ev import EventLoop
from ubuntui.palette import STYLES
from conjureup import __version__ as VERSION
from conjureup import charm, consts, controllers, events, juju, utils
from conjureup.app_config import app
from conjureup.download import (
EndpointType,
detect_endpoint,
download,
download_local,
download_or_sync_registry,
get_remote_url
)
from conjureup.log import setup_logging
from conjureup.models.addon import AddonModel
from conjureup.models.provider import SchemaErrorUnknownCloud, load_schema
from conjureup.models.step import StepModel
from conjureup.telemetry import SENTRY_DSN, track_event, track_screen
from conjureup.ui import ConjureUI
def parse_options(argv):
parser = argparse.ArgumentParser(prog="conjure-up")
parser.add_argument('spell', nargs='?',
default=consts.UNSPECIFIED_SPELL,
help="""The name ('openstack-nclxd') or location
('githubusername/spellrepo') of a conjure-up
spell, or a keyword matching multiple spells
('openstack')""")
parser.add_argument('-d', '--debug', action='store_true',
dest='debug', default=False,
help='Enable debug logging.')
parser.add_argument('--show-env', action='store_true',
dest='show_env',
help='Shows what environment variables are used '
'during post deployment actions. This is useful '
'for headless installs allowing you to set those '
'variables to further customize your deployment.')
parser.add_argument('--registry', dest='registry',
help='Spells Registry',
default='https://github.com/conjure-up/spells.git')
parser.add_argument('--cache-dir', dest='cache_dir',
help='Download directory for spells',
default=os.path.expanduser("~/.cache/conjure-up"))
parser.add_argument('--spells-dir', dest='spells_dir',
help='Location of conjure-up managed spells directory',
default=os.path.expanduser(
"~/.cache/conjure-up-spells"))
parser.add_argument('--conf-file', dest='conf_file',
help='Path to configuration file', type=pathlib.Path,
default=pathlib.Path("~/.config/conjure-up.conf"))
parser.add_argument('--apt-proxy', dest='apt_http_proxy',
help='Specify APT proxy')
parser.add_argument('--apt-https-proxy', dest='apt_https_proxy',
help='Specify APT HTTPS proxy')
parser.add_argument('--http-proxy', dest='http_proxy',
help='Specify HTTP proxy')
parser.add_argument('--https-proxy', dest='https_proxy',
help='Specify HTTPS proxy')
parser.add_argument('--no-proxy', dest='no_proxy',
help='Comma separated list of IPs to not '
'filter through a proxy')
parser.add_argument('--bootstrap-timeout', dest='bootstrap_timeout',
help='Amount of time to wait for initial controller '
'creation. Useful for slower network connections.')
parser.add_argument('--bootstrap-to', dest='bootstrap_to',
help='The MAAS node hostname to deploy to. Useful '
'for using lower end hardware as the Juju admin '
'controller.', metavar='<host>.maas')
parser.add_argument(
'--version', action='version', version='%(prog)s {}'.format(VERSION))
parser.add_argument('--notrack', action='store_true',
dest='notrack',
help='Opt out of sending anonymous usage '
'information to Canonical.')
parser.add_argument('--noreport', action='store_true',
dest='noreport',
help='Opt out of sending anonymous error reports '
'to Canonical.')
parser.add_argument('--nosync', action='store_true',
dest='nosync',
help='Opt out of syncing with spells '
'registry.')
parser.add_argument('--color', type=str, default='auto',
choices=['auto', 'never', 'always'],
help='Whether to use colorized output '
'in headless mode.')
parser.add_argument('--bundle-add', type=pathlib.Path,
help="Path to a bundle fragment file which will be "
"merged with the spell's bundle")
parser.add_argument('--bundle-remove', type=pathlib.Path,
help="Path to a bundle fragment file which will be "
"subtracted from the spell's bundle")
# Channels
parser.add_argument('--channel', type=str,
choices=charm.CHANNELS,
dest='channel',
default='stable',
help='conjure-up spell from a release channel')
parser.add_argument('cloud', nargs='?',
help="Name of a Juju cloud to "
"target, such as ['aws', 'localhost' ...], optionally "
"with a region, in the form <cloud>/<region>. "
"If no controller exists there, one may be created")
parser.add_argument('controller', nargs='?',
help="Name of a juju controller to target. "
"If not provided, a new one is created.")
parser.add_argument('model', nargs='?',
help="Name of a juju model to target. "
"If not provided, a new one is created.")
return parser.parse_args(argv)
async def _start(*args, **kwargs):
# NB: we have to set the exception handler here because we need to
# override the one set by urwid, which happens in MainLoop.run()
app.loop.set_exception_handler(events.handle_exception)
if app.endpoint_type in [None, EndpointType.LOCAL_SEARCH]:
controllers.use('spellpicker').render()
return
utils.setup_metadata_controller()
if app.headless:
controllers.use('clouds').render()
elif app.selected_addons:
controllers.use('clouds').render()
else:
controllers.use('addons').render()
def apply_proxy():
""" Sets up proxy information.
"""
# Apply proxy information
if app.argv.http_proxy:
os.environ['HTTP_PROXY'] = app.argv.http_proxy
os.environ['http_proxy'] = app.argv.http_proxy
if app.argv.https_proxy:
os.environ['HTTPS_PROXY'] = app.argv.https_proxy
os.environ['https_proxy'] = app.argv.https_proxy
def show_env():
""" Shows environment variables from post deploy actions
"""
print("Available environment variables: \n")
table = PrettyTable()
table.field_names = ["ENV", "DEFAULT", ""]
table.align = 'l'
for step in app.steps:
for x in step.additional_input:
default = colored(x.get('default', ''), 'green', attrs=['bold'])
key = colored(x['key'], 'blue', attrs=['bold'])
table.add_row([key, default,
textwrap.fill(step.description, width=55)])
print(table)
print("")
url = ("https://docs.ubuntu.com/conjure-up/"
"en/usage#customising-headless-mode")
print(
textwrap.fill(
"See {} for more information on using these variables to further "
"customize your deployment.".format(url), width=79))
sys.exit(0)
def main():
if os.geteuid() == 0:
print("")
print(" !! This should _not_ be run as root or with sudo. !!")
print("")
sys.exit(1)
# Verify we can access ~/.local/share/juju if it exists
juju_dir = pathlib.Path('~/.local/share/juju').expanduser()
if juju_dir.exists():
try:
for f in juju_dir.iterdir():
if f.is_file():
f.read_text()
except PermissionError:
print("")
print(" !! Unable to read from ~/.local/share/juju, please "
"double check your permissions on that directory "
"and its files. !!")
print("")
sys.exit(1)
utils.set_terminal_title("conjure-up")
opts = parse_options(sys.argv[1:])
spell = os.path.basename(os.path.abspath(opts.spell))
if not os.path.isdir(opts.cache_dir):
os.makedirs(opts.cache_dir)
# Application Config
kv_db = os.path.join(opts.cache_dir, '.state.db')
app.state = KV(kv_db)
app.env = os.environ.copy()
app.env['KV_DB'] = kv_db
app.config = {'metadata': None}
app.argv = opts
app.log = setup_logging(app,
os.path.join(opts.cache_dir, 'conjure-up.log'),
opts.debug)
# Make sure juju paths are setup
juju.set_bin_path()
juju.set_wait_path()
if app.argv.conf_file.expanduser().exists():
conf = configparser.ConfigParser()
conf.read_string(app.argv.conf_file.expanduser().read_text())
app.notrack = conf.getboolean('REPORTING', 'notrack', fallback=False)
app.noreport = conf.getboolean('REPORTING', 'noreport', fallback=False)
if app.argv.notrack:
app.notrack = True
if app.argv.noreport:
app.noreport = True
# Grab current LXD and Juju versions
app.log.debug("Juju version: {}, "
"conjure-up version: {}".format(
utils.juju_version(),
VERSION))
# Setup proxy
apply_proxy()
app.session_id = os.getenv('CONJURE_TEST_SESSION_ID',
str(uuid.uuid4()))
spells_dir = app.argv.spells_dir
app.config['spells-dir'] = spells_dir
spells_index_path = os.path.join(app.config['spells-dir'],
'spells-index.yaml')
spells_registry_branch = os.getenv('CONJUREUP_REGISTRY_BRANCH', 'master')
if not app.argv.nosync:
if not os.path.exists(spells_dir):
utils.info("No spells found, syncing from registry, please wait.")
try:
download_or_sync_registry(
app.argv.registry,
spells_dir, branch=spells_registry_branch)
except subprocess.CalledProcessError as e:
if not os.path.exists(spells_dir):
utils.error("Could not load from registry")
sys.exit(1)
app.log.debug(
'Could not sync spells from github: {}'.format(e))
else:
if not os.path.exists(spells_index_path):
utils.error(
"You opted to not sync from the spells registry, however, "
"we could not find any suitable spells in: "
"{}".format(spells_dir))
sys.exit(1)
with open(spells_index_path) as fp:
app.spells_index = yaml.safe_load(fp.read())
addons_aliases_index_path = os.path.join(app.config['spells-dir'],
'addons-aliases.yaml')
if os.path.exists(addons_aliases_index_path):
with open(addons_aliases_index_path) as fp:
app.addons_aliases = yaml.safe_load(fp.read())
spell_name = spell
app.endpoint_type = detect_endpoint(opts.spell)
# Check if spell is actually an addon
addon = utils.find_addons_matching(opts.spell)
if addon:
app.log.debug("addon found, setting required spell")
utils.set_chosen_spell(addon['spell'],
os.path.join(opts.cache_dir,
addon['spell']))
download_local(os.path.join(app.config['spells-dir'],
addon['spell']),
app.config['spell-dir'])
utils.set_spell_metadata()
StepModel.load_spell_steps()
AddonModel.load_spell_addons()
app.selected_addons = addon['addons']
utils.setup_metadata_controller()
app.endpoint_type = EndpointType.LOCAL_DIR
elif app.endpoint_type == EndpointType.LOCAL_SEARCH:
spells = utils.find_spells_matching(opts.spell)
if len(spells) == 0:
utils.error("Can't find a spell matching '{}'".format(opts.spell))
sys.exit(1)
# One result means it was a direct match and we can copy it
# now. Changing the endpoint type then stops us from showing
# the picker UI. More than one result means we need to show
# the picker UI and will defer the copy to
# SpellPickerController.finish(), so nothing to do here.
if len(spells) == 1:
app.log.debug("found spell {}".format(spells[0][1]))
spell = spells[0][1]
utils.set_chosen_spell(spell_name,
os.path.join(opts.cache_dir,
spell['key']))
download_local(os.path.join(app.config['spells-dir'],
spell['key']),
app.config['spell-dir'])
utils.set_spell_metadata()
StepModel.load_spell_steps()
AddonModel.load_spell_addons()
app.endpoint_type = EndpointType.LOCAL_DIR
# download spell if necessary
elif app.endpoint_type == EndpointType.LOCAL_DIR:
if not os.path.isdir(opts.spell):
utils.warning("Could not find spell {}".format(opts.spell))
sys.exit(1)
if not os.path.exists(os.path.join(opts.spell,
"metadata.yaml")):
utils.warning("'{}' does not appear to be a spell. "
"{}/metadata.yaml was not found.".format(
opts.spell, opts.spell))
sys.exit(1)
spell_name = os.path.basename(os.path.abspath(spell))
utils.set_chosen_spell(spell_name,
path.join(opts.cache_dir, spell_name))
download_local(opts.spell, app.config['spell-dir'])
utils.set_spell_metadata()
StepModel.load_spell_steps()
AddonModel.load_spell_addons()
elif app.endpoint_type in [EndpointType.VCS, EndpointType.HTTP]:
utils.set_chosen_spell(spell, path.join(opts.cache_dir, spell))
remote = get_remote_url(opts.spell)
if remote is None:
utils.warning("Can't guess URL matching '{}'".format(opts.spell))
sys.exit(1)
download(remote, app.config['spell-dir'], True)
utils.set_spell_metadata()
StepModel.load_spell_steps()
AddonModel.load_spell_addons()
app.env['CONJURE_UP_CACHEDIR'] = app.argv.cache_dir
app.env['PATH'] = "/snap/bin:{}".format(app.env['PATH'])
if app.argv.show_env:
if app.endpoint_type in [None, EndpointType.LOCAL_SEARCH]:
utils.error("Please specify a spell for headless mode.")
sys.exit(1)
show_env()
app.sentry = raven.Client(
dsn=SENTRY_DSN,
release=VERSION,
transport=RequestsHTTPTransport,
processors=(
'conjureup.utils.SanitizeDataProcessor',
)
)
track_screen("Application Start")
track_event("OS", platform.platform(), "")
app.loop = asyncio.get_event_loop()
app.loop.add_signal_handler(signal.SIGINT, events.Shutdown.set)
try:
if app.argv.cloud:
cloud = None
region = None
if '/' in app.argv.cloud:
parse_cli_cloud = app.argv.cloud.split('/')
cloud, region = parse_cli_cloud
app.log.debug(
"Region found {} for cloud {}".format(cloud,
region))
else:
cloud = app.argv.cloud
cloud_types = juju.get_cloud_types_by_name()
if cloud not in cloud_types:
utils.error('Unknown cloud: {}'.format(cloud))
sys.exit(1)
if app.endpoint_type in [None, EndpointType.LOCAL_SEARCH]:
utils.error("Please specify a spell for headless mode.")
sys.exit(1)
app.provider = load_schema(cloud_types[cloud])
try:
app.provider.load(cloud)
except SchemaErrorUnknownCloud as e:
utils.error(e)
sys.exit(1)
if region:
app.provider.region = region
app.headless = True
app.ui = None
app.env['CONJURE_UP_HEADLESS'] = "1"
app.loop.create_task(events.shutdown_watcher())
app.loop.create_task(_start())
app.loop.run_forever()
else:
app.ui = ConjureUI()
EventLoop.build_loop(app.ui, STYLES,
unhandled_input=events.unhandled_input,
handle_mouse=False)
app.loop.create_task(events.shutdown_watcher())
app.loop.create_task(_start())
EventLoop.run()
finally:
# explicitly close asyncio event loop to avoid hitting the
# following issue due to signal handlers added by
# asyncio.create_subprocess_exec being cleaned up during final
# garbage collection: https://github.com/python/asyncio/issues/396
app.loop.close()
sys.exit(app.exit_code)