# Copyright 2009-2015 Eucalyptus Systems, Inc.
#
# Redistribution and use of this software in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
#   Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
#
#   Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import base64
import os.path

from requestbuilder import Arg, MutuallyExclusiveArgList
from requestbuilder.exceptions import ArgumentError

from euca2ools.commands.argtypes import (ec2_block_device_mapping,
                                         flexible_bool, vpc_interface)
from euca2ools.commands.ec2 import EC2Request


class RunInstances(EC2Request):
    DESCRIPTION = 'Launch instances of a machine image'
    ARGS = [Arg('ImageId', metavar='IMAGE',
                help='ID of the image to instantiate (required)'),
            Arg('-n', '--instance-count', dest='count', metavar='MIN[-MAX]',
                default='1', route_to=None,
                help='''number of instances to launch. If this number of
                        instances cannot be launched, no instances will launch.
                        If specified as a range (min-max), the server will
                        attempt to launch the maximum number, but no fewer
                        than the minimum number.'''),
            Arg('-g', '--group', action='append', default=[], route_to=None,
                help='security group(s) in which to launch the instances'),
            Arg('-k', '--key', dest='KeyName', metavar='KEYPAIR',
                help='name of the key pair to use'),
            MutuallyExclusiveArgList(
                Arg('-d', '--user-data', metavar='DATA', route_to=None,
                    help='''user data to make available to instances in this
                            reservation'''),
                Arg('--user-data-force', metavar='DATA', route_to=None,
                    help='''same as -d/--user-data, but without checking if a
                    file by that name exists first'''),
                Arg('-f', '--user-data-file', metavar='FILE', route_to=None,
                    help='''file containing user data to make available to the
                    instances in this reservation''')),
            Arg('--addressing', dest='AddressingType',
                choices=('public', 'private'),
                help='''[Eucalyptus only, non-VPC only]
                addressing scheme to launch the instance with.  Use "private"
                to run an instance with no public address.'''),
            Arg('-t', '--instance-type', dest='InstanceType',
                help='type of instance to launch'),
            Arg('-z', '--availability-zone', metavar='ZONE',
                dest='Placement.AvailabilityZone'),
            Arg('--kernel', dest='KernelId', metavar='KERNEL',
                help='ID of the kernel to launch the instance(s) with'),
            Arg('--ramdisk', dest='RamdiskId', metavar='RAMDISK',
                help='ID of the ramdisk to launch the instance(s) with'),
            Arg('-b', '--block-device-mapping', metavar='DEVICE=MAPPED',
                dest='BlockDeviceMapping', action='append',
                type=ec2_block_device_mapping, default=[],
                help='''define a block device mapping for the instances, in the
                form DEVICE=MAPPED, where "MAPPED" is "none", "ephemeral(0-3)",
                or
                "[SNAP-ID]:[GiB]:[true|false]:[standard|VOLTYPE[:IOPS]]"'''),
            Arg('-m', '--monitor', dest='Monitoring.Enabled',
                action='store_const', const='true',
                help='enable detailed monitoring for the instance(s)'),
            Arg('--disable-api-termination', dest='DisableApiTermination',
                action='store_const', const='true',
                help='prevent API users from terminating the instance(s)'),
            Arg('--instance-initiated-shutdown-behavior',
                dest='InstanceInitiatedShutdownBehavior',
                choices=('stop', 'terminate'),
                help=('whether to "stop" (default) or terminate EBS instances '
                      'when they shut down')),
            Arg('--placement-group', dest='Placement.GroupName',
                metavar='PLGROUP', help='''name of a placement group to launch
                into'''),
            Arg('--tenancy', dest='Placement.Tenancy',
                choices=('default', 'dedicated'), help='''[VPC only]
                "dedicated" to run on single-tenant hardware'''),
            Arg('--client-token', dest='ClientToken', metavar='TOKEN',
                help='unique identifier to ensure request idempotency'),
            Arg('-s', '--subnet', metavar='SUBNET', route_to=None,
                help='''[VPC only] subnet to create the instance's network
                interface in'''),
            Arg('--associate-public-ip-address', type=flexible_bool,
                route_to=None, help='''[VPC only] assign a public address
                to the instance's network interface'''),
            Arg('--private-ip-address', metavar='ADDRESS', route_to=None,
                help='''[VPC only] assign a specific primary private IP address
                to an instance's interface'''),
            MutuallyExclusiveArgList(
                Arg('--secondary-address', '--secondary-private-ip-address',
                    metavar='ADDRESS', action='append', route_to=None,
                    help='''[VPC only] assign a specific secondary private IP
                    address to an instance's network interface.  Use this
                    option multiple times to add additional addresses.'''),
                Arg('--secondary-count', '--secondary-private-ip-address-count',
                    metavar='COUNT', type=int, route_to=None, help='''[VPC only]
                    automatically assign a specific number of secondary private
                    IP addresses to an instance's network interface''')),
            Arg('-a', '--network-interface', dest='NetworkInterface',
                metavar='INTERFACE', action='append', type=vpc_interface,
                help=('[VPC only] add a network interface to the new '
                      'instance.  If the interface already exists, supply its '
                      'ID and a numeric index for it, separated by ":", in '
                      'the form "eni-NNNNNNNN:INDEX".  To create a new '
                      'interface, supply a numeric index and subnet ID for '
                      'it, along with (in order) an optional description, a '
                      'primary private IP address, a list of security group '
                      'IDs to associate with the interface, whether to delete '
                      'the interface upon instance termination ("true" or '
                      '"false"), a number of secondary private IP addresses '
                      'to create automatically, and a list of secondary '
                      'private IP addresses to assign to the interface, '
                      'separated by ":", in the form ":INDEX:SUBNET:'
                      '[DESCRIPTION]:[PRIV_IP]:[GROUP1,GROUP2,...]:[true|'
                      'false]:[SEC_IP_COUNT|:SEC_IP1,SEC_IP2,...]".  You '
                      'cannot specify both of the latter two.  This option '
                      'may be used multiple times.  Each adds another network '
                      'interface.')),
            Arg('-p', '--iam-profile', metavar='IPROFILE', route_to=None,
                help='''name or ARN of the IAM instance profile to associate
                with the new instance(s)'''),
            Arg('--ebs-optimized', dest='EbsOptimized', action='store_const',
                const='true', help='optimize the new instance(s) for EBS I/O')]

    LIST_TAGS = ['reservationSet', 'instancesSet', 'groupSet', 'tagSet',
                 'blockDeviceMapping', 'productCodes', 'networkInterfaceSet',
                 'privateIpAddressesSet']

    # noinspection PyExceptionInherit
    def configure(self):
        EC2Request.configure(self)
        if self.args.get('user_data'):
            if os.path.isfile(self.args['user_data']):
                raise ArgumentError(
                    'argument -d/--user-data: to pass the contents of a file '
                    'as user data, use -f/--user-data-file.  To pass the '
                    "literal value '{0}' as user data even though it matches "
                    'the name of a file, use --user-data-force.')
            else:
                self.params['UserData'] = base64.b64encode(
                    self.args['user_data'])
        elif self.args.get('user_data_force'):
            self.params['UserData'] = base64.b64encode(
                self.args['user_data_force'])
        elif self.args.get('user_data_file'):
            with open(self.args['user_data_file']) as user_data_file:
                self.params['UserData'] = base64.b64encode(
                    user_data_file.read())

        if self.args.get('KeyName') is None:
            default_key_name = self.config.get_region_option(
                'ec2-default-keypair')
            if default_key_name:
                self.log.info("using default key pair '%s'", default_key_name)
                self.params['KeyName'] = default_key_name

    # noinspection PyExceptionInherit
    def preprocess(self):
        counts = self.args['count'].split('-')
        if len(counts) == 1:
            try:
                self.params['MinCount'] = int(counts[0])
                self.params['MaxCount'] = int(counts[0])
            except ValueError:
                raise ArgumentError('argument -n/--instance-count: instance '
                                    'count must be an integer')
        elif len(counts) == 2:
            try:
                self.params['MinCount'] = int(counts[0])
                self.params['MaxCount'] = int(counts[1])
            except ValueError:
                raise ArgumentError('argument -n/--instance-count: instance '
                                    'count range must be must be comprised of '
                                    'integers')
        else:
            raise ArgumentError('argument -n/--instance-count: value must '
                                'have format "1" or "1-2"')
        if self.params['MinCount'] < 1 or self.params['MaxCount'] < 1:
            raise ArgumentError('argument -n/--instance-count: instance count '
                                'must be positive')
        if self.params['MinCount'] > self.params['MaxCount']:
            self.log.debug('MinCount > MaxCount; swapping')
            self.params.update({'MinCount': self.params['MaxCount'],
                                'MaxCount': self.params['MinCount']})

        iprofile = self.args.get('iam_profile')
        if iprofile:
            if iprofile.startswith('arn:'):
                self.params['IamInstanceProfile.Arn'] = iprofile
            else:
                self.params['IamInstanceProfile.Name'] = iprofile

        if self.args.get('subnet') or self.args.get('NetworkInterface'):
            # This is going into a VPC.
            # We can't mix top-level and interface-level parameters, so
            # build an interface out of all the network-related options
            # to make the split-up, "friendlier" options work.
            cli_iface = {}
            for group in self.args['group']:
                if not group.startswith('sg-'):
                    raise ArgumentError('argument -g/--group: groups must be '
                                        'specified by ID when using VPC')
                cli_iface.setdefault('SecurityGroupId', [])
                cli_iface['SecurityGroupId'].append(group)
            if self.args.get('associate_public_ip_address') is not None:
                cli_iface['AssociatePublicIpAddress'] = \
                    self.args['associate_public_ip_address']
            if self.args.get('private_ip_address'):
                cli_iface['PrivateIpAddresses'] = [
                    {'PrivateIpAddress': self.args['private_ip_address'],
                     'Primary': 'true'}]
            if self.args.get('secondary_address'):
                sec_ips = [{'PrivateIpAddress': addr} for addr in
                           self.args['secondary_address']]
                if not cli_iface.get('PrivateIpAddresses'):
                    cli_iface['PrivateIpAddresses'] = []
                cli_iface['PrivateIpAddresses'].extend(sec_ips)
            if self.args.get('secondary_count'):
                sec_ip_count = self.args['secondary_count']
                cli_iface['SecondaryPrivateIpAddressCount'] = sec_ip_count
            if self.args.get('subnet'):
                cli_iface['SubnetId'] = self.args['subnet']
            if cli_iface:
                cli_iface['DeviceIndex'] = 0
                if not self.params.get('NetworkInterface'):
                    self.params['NetworkInterface'] = []
                self.params['NetworkInterface'].append(cli_iface)
            self.log.debug('built network interface from CLI options: {0}'
                           .format(cli_iface))
        else:
            # Non-VPC
            for group in self.args['group']:
                if group.startswith('sg-'):
                    if not self.params.get('SecurityGroupId'):
                        self.params['SecurityGroupId'] = []
                    self.params['SecurityGroupId'].append(group)
                else:
                    if not self.params.get('SecurityGroup'):
                        self.params['SecurityGroup'] = []
                    self.params['SecurityGroup'].append(group)


    def print_result(self, result):
        self.print_reservation(result)
