#!/usr/bin/python
# encoding: utf-8

# Copyright 2016-2017 ProfitBricks GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

'''
pb_addNewServer -- adds a new server to a data center

pb_addNewServer is a tool to add a new server/storage composite to an
existing data center. The server may be set up by a disk or cdrom image.


@author:     Jürgen Buchhammer

@copyright:  2016 ProfitBricks GmbH. All rights reserved.

@license:    Apache License 2.0

@contact:    juergen.buchhammer@profitbricks.com
@deffield    updated: Updated
'''

import sys
import os
import traceback

from time import sleep
from datetime import datetime

from argparse import ArgumentParser
from argparse import RawDescriptionHelpFormatter
from base64 import b64encode, b64decode

from profitbricks.client import ProfitBricksService, Server, Volume, NIC


__all__ = []
__version__ = 0.1
__date__ = '2016-02-16'
__updated__ = '2016-02-16'


class CLIError(Exception):
    '''Generic exception to raise and log different fatal errors.'''
    def __init__(self, msg):
        super(CLIError).__init__(type(self))
        self.msg = "E: %s" % msg

    def __str__(self):
        return self.msg

    def __unicode__(self):
        return self.msg
# end class CLIError


def getLogin(filename, user, passwd):
    '''
    write user/passwd to login file or get them from file.
    This method is not Py3 safe (byte vs. str)
    '''
    if filename is None:
        return (user, passwd)
    if os.path.exists(filename):
        print("Using file {} for Login".format(filename))
        with open(filename, "r") as loginfile:
            encoded_cred = loginfile.read()
            decoded_cred = b64decode(encoded_cred)
            login = decoded_cred.split(':', 1)
            return (login[0], login[1])
    else:
        if user is None or passwd is None:
            raise ValueError("user and password must not be None")
        print("Writing file {} for Login".format(filename))
        with open(filename, "w") as loginfile:
            encoded_cred = b64encode(user+":"+passwd)
            loginfile.write(encoded_cred)
        return (user, passwd)
# end getLogin()


def wait_for_request(pbclient, request_id,
                     timeout=0, initial_wait=5, scaleup=10):
    '''
    Waits for a request to finish until timeout.
    timeout==0 is interpreted as infinite wait time.
    Returns a tuple (return code, request status, message) where return code
    0  : request successful
    1  : request failed
    -1 : timeout exceeded
    The wait_period is increased every scaleup steps to adjust for long
    running requests.
    '''
    total_wait = 0
    wait_period = initial_wait
    next_scaleup = scaleup * wait_period
    wait = True
    while wait:
        request_status = pbclient.get_request(request_id, status=True)
        state = request_status['metadata']['status']
        if state == "DONE":
            return(0, state, request_status['metadata']['message'])
        if state == 'FAILED':
            return(1, state, request_status['metadata']['message'])
        if verbose > 0:
            print("Request '{}' is in state '{}'. Sleeping for {} seconds..."
                  .format(request_id, state, wait_period))
        sleep(wait_period)
        total_wait += wait_period
        if timeout != 0 and total_wait > timeout:
            wait = False
        next_scaleup -= wait_period
        if next_scaleup == 0:
            wait_period += initial_wait
            next_scaleup = scaleup * wait_period
            if verbose > 0:
                print("scaling up wait_period to {}, next change in {} seconds"
                      .format(wait_period, next_scaleup))
    # end while(wait)
    return(-1, state, "request not finished before timeout")
# end wait_for_request()


def main(argv=None):
    '''Parse command line options and create a server/volume composite.'''

    if argv is None:
        argv = sys.argv
    else:
        sys.argv.extend(argv)

    program_name = os.path.basename(sys.argv[0])
    program_version = "v%s" % __version__
    program_build_date = str(__updated__)
    program_version_message = '%%(prog)s %s (%s)' % (program_version,
                                                     program_build_date)
    program_shortdesc = __import__('__main__').__doc__.split("\n")[1]
    program_license = '''%s

  Created by J. Buchhammer on %s.
  Copyright 2016 ProfitBricks GmbH. All rights reserved.

  Licensed under the Apache License 2.0
  http://www.apache.org/licenses/LICENSE-2.0

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied.

USAGE
''' % (program_shortdesc, str(__date__))

    try:
        # Setup argument parser
        parser = ArgumentParser(description=program_license,
                                formatter_class=RawDescriptionHelpFormatter)
        parser.add_argument('-u', '--user', dest='user', help='the login name')
        parser.add_argument('-p', '--password', dest='password',
                            help='the login password')
        parser.add_argument('-L', '--Login', dest='loginfile', default=None,
                            help='the login file to use')
        parser.add_argument('-d', '--datacenterid', dest='datacenterid',
                            required=True, default=None,
                            help='datacenter of the new server')
        parser.add_argument('-l', '--lanid', dest='lanid', required=True,
                            default=None, help='LAN of the new server')
        parser.add_argument('-n', '--name', dest='servername',
                            default="SRV_"+datetime.now().isoformat(),
                            help='name of the new server')
        parser.add_argument('-c', '--cores', dest='cores', type=int,
                            default=2, help='CPU cores')
        parser.add_argument('-r', '--ram', dest='ram', type=int, default=4,
                            help='RAM in GB')
        parser.add_argument('-s', '--storage', dest='storage', type=int,
                            default=4, help='storage in GB')
        parser.add_argument('-b', '--boot', dest='bootdevice', default="HDD",
                            help='boot device')
        parser.add_argument('-i', '--imageid', dest='imageid', default=None,
                            help='installation image')
        parser.add_argument('-P', '--imagepassword', dest='imgpassword',
                            default=None, help='the image password')
        parser.add_argument('-v', '--verbose', dest="verbose", action="count",
                            help="set verbosity level [default: %(default)s]")
        parser.add_argument('-V', '--version', action='version',
                            version=program_version_message)

        # Process arguments
        args = parser.parse_args()
        global verbose
        verbose = args.verbose
        dc_id = args.datacenterid
        lan_id = args.lanid
        servername = args.servername

        if verbose > 0:
            print("Verbose mode on")
            print("start {} with args {}".format(program_name, str(args)))

        # Test images (location de/fra)
        # CDROM: 7fc885b3-c9a6-11e5-aa10-52540005ab80   # debian-8.3.0-amd64-netinst.iso
        # HDD:   28007a6d-c88a-11e5-aa10-52540005ab80   # CentOS-7-server-2016-02-01
        hdimage = args.imageid
        cdimage = None
        if args.bootdevice == "CDROM":
            hdimage = None
            cdimage = args.imageid
        print("using boot device {} with image {}"
              .format(args.bootdevice, args.imageid))

        (user, password) = getLogin(args.loginfile, args.user, args.password)
        if user is None or password is None:
            raise ValueError("user or password resolved to None")
        pbclient = ProfitBricksService(user, password)

        first_nic = NIC(name="local", ips=[], dhcp=True, lan=lan_id)
        volume = Volume(name=servername+"-Disk", size=args.storage,
                        image=hdimage, image_password=args.imgpassword)
        server = Server(name=servername, cores=args.cores, ram=args.ram*1024,
                        create_volumes=[volume], nics=[first_nic],
                        boot_cdrom=cdimage)
        print("creating server..")
        if verbose > 0:
            print("SERVER: {}".format(str(server)))
        response = pbclient.create_server(dc_id, server)
        print("wait for provisioning..")
        wait_for_request(pbclient, response["requestId"])
        server_id = response['id']
        print("Server provisioned with ID {}".format(server_id))
        nics = pbclient.list_nics(dc_id, server_id, 1)
        # server should have exactly one nic, but we only test empty nic list
        if len(nics['items']) == 0:
            raise CLIError("No NICs found for newly created server {}"
                           .format(server_id))
        nic0 = nics['items'][0]
        if verbose > 0:
            print("NIC0: {}".format(str(nic0)))
        (nic_id, nic_mac) = (nic0['id'], nic0['properties']['mac'])
        print("NIC of new Server has ID {} and MAC {}".format(nic_id, nic_mac))
        print("{} finished w/o errors".format(program_name))
        return 0

    except KeyboardInterrupt:
        # handle keyboard interrupt #
        return 0
    except Exception:
        traceback.print_exc()
        sys.stderr.write("\n" + program_name + ":  for help use --help\n")
        return 2
# end main()


if __name__ == "__main__":
    sys.exit(main())
