#!/usr/bin/env python3
# -*- mode: python; -*-
#
# Copyright 2014, 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

""" CLI for installing Ubuntu OpenStack """

import argparse
import sys
import os
import logging
from functools import partial
from cloudinstall.log import setup_logger
import cloudinstall.utils as utils
from cloudinstall.gui import PegasusGUI, InstallHeader
from cloudinstall.consoleui import ConsoleUI
from cloudinstall.controllers.installbase import InstallController
from cloudinstall.config import Config
from cloudinstall.ev import EventLoop
from cloudinstall.alarms import AlarmMonitor
from cloudinstall import __version__ as version

CFG_FILE = os.path.join(utils.install_home(),
                        '.cloud-install/config.yaml')


def parse_options(argv):
    parser = argparse.ArgumentParser(description='Ubuntu Openstack Installer',
                                     prog='openstack-install',
                                     argument_default=argparse.SUPPRESS)
    parser.add_argument('-i', '--install-only', action='store_true',
                        dest='install_only',
                        help='install and bootstrap MAAS/Juju/Landscape '
                        '(as applicable) only. '
                        'Will not deploy any OpenStack services '
                        'in single or multi mode. '
                        'You can invoke openstack-status manually to '
                        'continue.')
    parser.add_argument('-u', '--uninstall', action='store_true',
                        dest='uninstall',
                        help='Uninstalls the current cloud including removing '
                        'of packages.')
    parser.add_argument('-c', '--config', type=str, dest='config_file',
                        help="Custom configuration for OpenStack installer.")
    parser.add_argument('--charm-config', type=str, dest='charm_config_file',
                        help="Additional charm settings")
    parser.add_argument('--charm-plugin-dir', type=str,
                        dest='charm_plugin_dir',
                        help="Location of additional charm plugins to extend "
                        "the installer.")
    parser.add_argument('-g', '--get-config', type=str, dest='get_config',
                        default=None,
                        help="with arg <key>, prints config value for 'key' "
                        "to stdout and exits.")
    parser.add_argument('--openstack-release',
                        dest='openstack_release', default="kilo",
                        choices=['icehouse', 'juno', 'kilo', 'liberty'],
                        help="Specify a specific OpenStack release.")
    parser.add_argument('--openstack-git-branch',
                        choices=["master", "stable"],
                        dest='openstack_git_branch', default=None,
                        help="Specifies upstream git branch to clone for "
                        "OpenStack services. Note that 'master' is just "
                        " the branch 'master', while 'stable' is combined "
                        " with the --openstack-release value, e.g."
                        " 'stable/kilo'")
    parser.add_argument('-a', type=str, default=None,
                        help='<arch, ..> comma-separated list of '
                        'architectures to filter available cloud '
                        'images with which to populate Glance, '
                        'e.g. amd64,arm64', dest='arch')
    parser.add_argument('-r', type=str, default=None, dest='release',
                        help='<rel, ..> comma-separated list of Ubuntu '
                        'releases to filter available cloud images with '
                        'which to populate Glance, e.g. precise,trusty')
    parser.add_argument('--edit-placement', action='store_true',
                        dest='edit_placement',
                        help='Show machine placement UI before deploying')
    parser.add_argument('--upstream-ppa', action="store_true",
                        dest='upstream_ppa',
                        help='Use upstream experimental ppa.')
    parser.add_argument('--upstream-deb', dest='upstream_deb',
                        help='Upload a local copy of openstack debian package '
                        'to be used in a single install. (DEVELOPERS)')
    parser.add_argument('--apt-proxy', dest='apt_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('--headless', action='store_true',
                        help="Run deployment without prompts/gui",
                        dest='headless')
    parser.add_argument('--debug', action='store_true',
                        dest='debug',
                        help='Debug mode')
    parser.add_argument('--next-charms', dest='next_charms',
                        action='store_true',
                        help="Use /next charms to test upcoming features.")
    parser.add_argument('--use-nclxd', dest='use_nclxd', action="store_true",
                        default=False,
                        help="Enable LXD support for Nova Compute. Requires"
                        "--series vivid to be set.")
    # TODO: currently only works for single installs. Use
    # SHOW_JUJU_LOGS=1 in the env to enable. See github issue #421
    # parser.add_argument('--show-logs', action='store_true',
    #                     help="Show Juju log tails for deploying services.",
    #                     dest='show_logs')
    parser.add_argument('--series', dest='ubuntu_series',
                        default='trusty',
                        help="Ubuntu series codename (eg. 'trusty', 'utopic') "
                        "for juju default")
    parser.add_argument('--constraints', dest='constraints',
                        help="Constraints to pass to Juju to control what "
                        "machines are used. A single string with key=value "
                        "pairs separated by spaces. "
                        "e.g. \"arch=amd64 tags=foo\". Also limits MAAS "
                        "machines used for service placement.")
    parser.add_argument('--force', dest='force', action="store_true",
                        default=False,
                        help="Ignores any prompting, e.g. with --uninstall. "
                        "Use with caution.")
    parser.add_argument(
        '--version', action='version', version='%(prog)s {}'.format(version))
    return parser.parse_args(argv)

if __name__ == '__main__':
    opts = parse_options(sys.argv[1:])
    cfg = Config(utils.populate_config(opts))
    if 'uninstall' in opts:
        def clean():
            print("Restoring system to last known state.")
            os.execl('/usr/bin/openstack-uninstall', '')

        if opts.force:
            clean()
        else:
            msg = ("Warning:\n\nThis will uninstall OpenStack and "
                   "make a best effort to return the system back "
                   "to its original state.")
            print(msg)
            yn = input("Proceed? [y/N] ")

        if "y" in yn or "Y" in yn:
            clean()

        if "n" in yn or "N" in yn:
            print("Uninstall cancelled.")
            sys.exit(1)

    if sys.getdefaultencoding() != 'utf-8':
        print("Ubuntu OpenStack Installer requires unicode support. "
              "Please enable this on the system running the installer.\n\n")
        print("Example:\n")
        print("  export LC_ALL=en_US.UTF-8")
        print("  export LANG=en_US.UTF-8")
        print("  export LANGUAGE=en_US.UTF-8")
        sys.exit(1)

    if opts.get_config:
        conf_bin = "/usr/bin/openstack-config"
        os.execl(conf_bin, conf_bin, opts.get_config)

    # see github issue #421:
    if os.environ.get("SHOW_JUJU_LOGS", False):
        cfg.setopt('show_logs', True)

    # validate series exists for nclxd
    if opts.use_nclxd and opts.ubuntu_series[0] < 'v':
        print("You must pass --series vivid or higher in order to use nclxd.")
        sys.exit(1)

    try:
        setup_logger(headless=cfg.getopt('headless'))
    except PermissionError:  # NOQA
        print("Permission error accessing log file.\n"
              "This probably indicates a broken partial install.\n"
              "Please use 'openstack-install -u' to uninstall, "
              "and try again.\n"
              "(You may want to save a copy of ~/.cloud-install/commands.log"
              " for reporting a bug.)")
        sys.exit(1)

    logger = logging.getLogger('cloudinstall')
    logger.info('Starting OpenStack Installer v{}'.format(version))
    logger.info('Start command: {}'.format(sys.argv))

    if os.geteuid() != 0:
        sys.exit(
            "Installing a cloud requires root privileges. Rerun with sudo")

    if not os.path.exists(cfg.cfg_path):
        os.makedirs(cfg.cfg_path)
        utils.chown(cfg.cfg_path, utils.install_user())

    if not os.path.exists(cfg.juju_path()):
        logger.info("Creating juju directories: {}".format(cfg.juju_path()))
        os.makedirs(cfg.juju_path())
        utils.chown(
            cfg.juju_path(), utils.install_user(), utils.install_user())

    if not cfg.getopt('container_name'):
        cfg.setopt('container_name',
                   'openstack-single-{}'.format(utils.install_user()))

    if os.path.isfile(os.path.join(cfg.cfg_path, 'installed')):
        msg = ("\n\nError:\n\n"
               "Previous installation detected. Did you mean to run "
               "openstack-status instead? \n"
               "If attempting to re-install please run "
               "    $ sudo openstack-install -u\n\n")
        print(msg)
        sys.exit(1)

    out = utils.get_command_output(
        '{} juju api-endpoints'.format(cfg.juju_home()), user_sudo=True)
    if out['status'] == 0:
        msg = ("\n\nExisting OpenStack environment detected. Please destroy "
               "that environment before proceeding with a new install.\n\n")
        print(msg)
        sys.exit(1)

    if cfg.getopt('headless'):
        ui = ConsoleUI()
    else:
        ui = PegasusGUI(header=InstallHeader())

    # Set proxy
    if cfg.getopt('http_proxy'):
        os.environ['HTTP_PROXY'] = cfg.getopt('http_proxy')
        os.environ['http_proxy'] = cfg.getopt('http_proxy')
    if cfg.getopt('https_proxy'):
        os.environ['HTTPS_PROXY'] = cfg.getopt('https_proxy')
        os.environ['https_proxy'] = cfg.getopt('https_proxy')

    # Choose event loop
    ev = EventLoop(ui, cfg, logger)

    # Bind event loops alarm tracking
    AlarmMonitor.loop = ev

    try:
        install = InstallController(
            ui=ui, config=cfg, loop=ev)
    except:
        print("Unable to start install controller")
        sys.exit(1)

    logger.info('Running {} release'.format(
                cfg.getopt('openstack_release').capitalize()))

    utils.spew(os.path.join(cfg.cfg_path, 'openstack_release'),
               cfg.getopt('openstack_release'), owner=utils.install_user())

    try:
        import atexit
        atexit.register(partial(utils.cleanup, cfg))
        install.start()
    except:
        if opts.debug and not cfg.getopt('headless'):
            import pdb
            pdb.post_mortem()
    finally:
        sys.exit(ev.error_code)
