#!/usr/bin/python

# deploy-to - deploy a service to a specified machine.
# author - kapil thangavelu

# Copyright 2012 Canonical Ltd. All Rights Reserved
#
# 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/>.

import argparse

import os
import logging
import zookeeper

from twisted.internet.defer import inlineCallbacks
from twisted.internet import reactor

from juju.control.deploy import parse_config_options
from juju.control.utils import (
    expand_path, sync_environment_state)

from juju.charm.errors import ServiceConfigValueError
from juju.charm.publisher import CharmPublisher
from juju.charm.repository import resolve
from juju.environment.config import EnvironmentsConfig
from juju.errors import JujuError
from juju.state.endpoint import RelationEndpoint
from juju.state.machine import MachineStateManager
from juju.state.relation import RelationStateManager
from juju.state.service import ServiceStateManager


log = logging.getLogger("jitsu.deploy-to")


def get_parser():
    parser = argparse.ArgumentParser(
        description="deploy a service to a machine")

    parser.add_argument(
        "--environment", "-e",
        default=os.environ.get("JUJU_ENV"),
        help="Environment to deploy the charm in.")

    parser.add_argument(
        "--repository",
        help="Directory for charm lookup and retrieval",
        default=os.environ.get("JUJU_REPOSITORY"),
        type=expand_path)

    parser.add_argument(
        "--config",
        help="YAML file containing service options")

    parser.add_argument(
        "machine", nargs=None, type=int,
        help="Machine Id")

    parser.add_argument(
        "charm", nargs=None,
        help="Charm name")

    parser.add_argument(
        "service_name", nargs="?",
        help="Service name of deployed charm")

    return parser


def get_environment(env_option):
    env_config = EnvironmentsConfig()
    env_config.load_or_write_sample()

    if env_option is None:
        environment = env_config.get_default()
    else:
        environment = env_config.get(env_option)

    return env_config, environment


@inlineCallbacks
def deploy(env_config, environment, machine,
           repository_path, charm_name,
           service_name, log, config_file=None):
    """Deploy a charm within an environment.

    This will publish the charm to the environment, creating
    a service from the charm, and get it set to be launched
    on a new machine. If --repository is not specified, it
    will be taken from the environment variable JUJU_REPOSITORY.
    """
    # Sanity check machine value given
    repo, charm_url = resolve(
        charm_name, repository_path, environment.default_series)

    log.info("Searching for charm %s in %s" % (charm_url, repo))
    charm = yield repo.find(charm_url)
    charm_id = str(charm_url.with_revision(charm.get_revision()))

    # Can't use deploy to with subordinates
    if charm.metadata.is_subordinate:
        raise JujuError("Can only use deploy to with non-subordinate charms.")
    # Validate config options prior to deployment attempt
    service_options = {}
    service_name = service_name or charm_url.name
    if config_file:
        service_options = parse_config_options(
            config_file, service_name, charm)

    provider = environment.get_machine_provider()

    # XXX Empty constraints if we're deploying to an existing machine.
    # Perhaps use the constriants of the machine.
    constraint_set = yield provider.get_constraint_set()
    constraints = constraint_set.parse([])

    client = yield provider.connect()
    yield sync_environment_state(client, env_config, environment.name)

    # Publish the charm to juju
    storage = yield provider.get_file_storage()
    publisher = CharmPublisher(client, storage)
    yield publisher.add_charm(charm_id, charm)
    result = yield publisher.publish()

    # In future we might have multiple charms be published at
    # the same time.  For now, extract the charm_state from the
    # list.
    charm_state = result[0]

    # Create the service state
    service_manager = ServiceStateManager(client)
    service_state = yield service_manager.add_service_state(
        service_name, charm_state, constraints)

    # Use the charm's configuration options to validate service
    # options.. Invalid options passed will thrown an exception
    # and prevent the deploy.
    config = yield service_state.get_config()
    charm_config = yield charm_state.get_config()

    # Return the validated options with the defaults included.
    service_options = charm_config.validate(service_options)
    config.update(service_options)
    yield config.write()

    # Check if we have any peer relations to establish
    if charm.metadata.peers:
        relation_manager = RelationStateManager(client)
        for peer_name, peer_info in charm.metadata.peers.items():
            yield relation_manager.add_relation_state(
                RelationEndpoint(service_name,
                                 peer_info["interface"],
                                 peer_name,
                                 "peer"))

    # Create and deploy unit to requested machine.
    unit_state = yield service_state.add_unit_state()
    machines = MachineStateManager(client)
    machine = yield machines.get_machine_state(machine)
    yield unit_state.assign_to_machine(machine)

    log.info("Charm deployed as service: %r", service_name)

    yield client.close()


@inlineCallbacks
def run_one(func, *args, **kw):
    try:
        yield func(*args, **kw)
    except:
        import pdb, traceback, sys
        traceback.print_exc()
        pdb.post_mortem(sys.exc_info()[-1])
    finally:
        reactor.stop()

def main():
    """
    Deploy a charm to juju!
    """
    parser = get_parser()
    options = parser.parse_args()

    log_options = {
        "level": logging.DEBUG,
        "format": "%(asctime)s %(name)s:%(levelname)s %(message)s"}
    logging.basicConfig(**log_options)
    options.log = log

    zookeeper.set_debug_level(0)

    env_config, environment = get_environment(options.environment)

    reactor.callWhenRunning(
        run_one, deploy,
        env_config,
        environment,
        options.machine,
        options.repository,
        options.charm,
        options.service_name,
        options.log,
        options.config)
    reactor.run()


if __name__ == '__main__':
   main()

