#!/bin/python
#
# Script to import/export Cisco APIC blueprints to vRealize Automation versions 7.x
# usage:
#             users must specifiy either to export (-e) or import (-i)
#             python cisco-vra-utils.py -d <domain> -a <admin-user> -p <password> -t <tenant-name> -f vra-properties.json (-e|-i) <file-path to export to | zipfile to import>
#
# exporting:
#             python cisco-vra-utils.py -d vra-setup@domain.com -a myadmin@vsphere.local -p admin123 -f vra-properties.json
#             \ -t myTenant -e /home/user/Documents/
#
# importing::
#             python cisco-vra-utils.py -d vra-setup@domain.com -a myadmin@vsphere.local -p admin123 -f vra-properties.json
#             \ -t myTenant -i /home/user/Documents/cisco-blueprints.zip
#

from os import path
import json
import requests
import argparse
import logging
import sys

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

PACKAGE_NAME='cisco-apic-blueprints'
PACKAGE_DESCRIPTION='Cisco APIC blueprints'
PACKAGE_PATH = '/content-management-service/api/packages'
XAAS_BLUEPRINTS_PATH = '/content-management-service/api/contents'
SERVICE_BLUEPRINTS_PATH = '/advanced-designer-service/api/tenants'
QUERY_NAME_FILTER='%24filter=name+eq+'
TOKEN_PATH = '/identity/api/tokens'
ZIP_FILE_NAME = PACKAGE_NAME + '.zip'

class Cisco_VRA_Utils(object):
    def __init__(self, ip_address, admin_user, pwd, tenant, vra_json):
        self.url = 'https://' + ip_address
        self.token = self.get_token(admin_user, pwd, tenant)
        self.tenant = tenant
        self.vra_json = vra_json
        self.cisco_blueprint_names = self.get_cisco_blueprint_names()
        self.old_blueprint_names = self.get_old_blueprint_names()
        self.headers = {'Authorization': 'Bearer ' + self.token}

    def get_cisco_blueprint_names(self):
        cisco_catalog_item_and_services = json.load(open(self.vra_json))['catalogItemAndServices']
        cisco_blueprint_names = [ dict['catalogItem'] for dict in cisco_catalog_item_and_services ]
        return cisco_blueprint_names

    def get_old_blueprint_names(self):
        old_blueprints = json.load(open(self.vra_json))['oldBlueprints']
        return old_blueprints

    def _check_response_code(self, response, expected_code):
        """
        Checks response code of HTTP request comparing it to the expected response code

        """
        if response.status_code != expected_code:
            http_status_code_msg = '\nHTTP Status Code: ' + str(response.status_code)
            res_text = 'REST API call failed with message: ' + str(response.text) + http_status_code_msg
            error_message = '%s\n%s' % (http_status_code_msg, res_text)
            logging.error(error_message)
            raise ValueError(error_message)

    def get_token(self, username, pwd, tenant):
        """
        Request an HTTP Bearer token to authenticate for REST

        """
        request_body = {'username': username, 'password': pwd, 'tenant': tenant}
        url = self.url + TOKEN_PATH
        logging.info("Requesting vRealize Automation Bearer token")
        res = requests.post(url, data=json.dumps(request_body), verify=False )
        self._check_response_code(res, 200)
        content = json.loads(res.content)['id']
        logging.info("vRealize Automation Bearer token received")
        return str(content)

    def get_cisco_blueprint_id(self, url, cisco_blueprint_name, id_lambda):
        cisco_query_name_filter = "?%s'%s'" % (QUERY_NAME_FILTER, cisco_blueprint_name)
        url_with_filter = url + cisco_query_name_filter
        logging.info("Requesting Cisco blueprint id for: " + cisco_blueprint_name)
        res = requests.get(url_with_filter, headers=self.headers, verify=False)
        self._check_response_code(res, 200)
        cisco_blueprint_id = id_lambda(json.loads(res.content)['content'][0])
        logging.info("Cisco blueprint id: " + cisco_blueprint_id)
        return cisco_blueprint_id

    def get_package_id(self, package_name, package_description):
        """
        Get a vRealize Automation package ID from content-management-service

        """
        url =  self.url + PACKAGE_PATH
        res = requests.get(url, headers=self.headers, verify=False)
        self._check_response_code(res, 200)
        try:
            packages = json.loads(res.content)['content']
        except:
            return None
        for package in packages:
            name_matches = str(package['name']) == package_name
            description_matches = str(package['description']) == package_description
            if name_matches and description_matches:
                return str(package['id'])
        return None

    def create_cisco_package(self, cisco_blueprints):
        """
        Create Cisco package with Cisco APIC blueprints

        """
        blueprint_ids = [ blueprint['id'] for blueprint in cisco_blueprints ]
        import_data = {'name': PACKAGE_NAME, 'description': PACKAGE_DESCRIPTION, 'contents': blueprint_ids}
        url = self.url + PACKAGE_PATH
        logging.info('Creating ' + PACKAGE_NAME)
        res = requests.post(url, headers=self.headers, data=json.dumps(import_data), verify=False)
        self._check_response_code(res, 201)
        logging.info('Cisco APIC Blueprints Package created')

    def get_and_delete_cisco_package(self):
        """
        Delete Cisco package with Cisco APIC blueprints

        """
        cisco_package_id = self.get_package_id(PACKAGE_NAME, PACKAGE_DESCRIPTION)
        if cisco_package_id:
            url = self.url + PACKAGE_PATH + '/' + cisco_package_id
            res = requests.delete(url, headers=self.headers, verify=False)
            self._check_response_code(res, 204)
            logging.info('Cisco blueprints package deleted')
        else:
           logging.info('A Cisco package ID not found on server, deletion of Cisco blueprints package will not occur')

    def get_cisco_blueprints(self, filter_for_service_blueprints, cisco_blueprint_names):
        """

        Get Cisco blueprints

        Importing uses service bluerpints
        Exporting uses XaaS blueprints

        """
        cisco_blueprints = []
        for cisco_blueprint_name in cisco_blueprint_names:
            try:
                if filter_for_service_blueprints:
                    url = '%s%s/%s/%s' % (self.url, SERVICE_BLUEPRINTS_PATH, self.tenant, 'blueprints')
                    id_lambda = lambda dict : dict['id']['id']
                else:
                    url = self.url + XAAS_BLUEPRINTS_PATH
                    id_lambda = lambda dict : dict['id']
                cisco_blueprint_id = self.get_cisco_blueprint_id(url, cisco_blueprint_name, id_lambda)
                logging.info('Adding Cisco blueprint to list: ' + cisco_blueprint_name)
                cisco_blueprints.append({'name':cisco_blueprint_name, 'id':str(cisco_blueprint_id)})
            except IndexError as err:
                message =  '\nCisco blueprint: %s could not be found on the server\n' % cisco_blueprint_name
                message += 'Ignore above message if it is the first time importing Cisco APIC blueprints or if ' 
                message += 'deletion is occurring for old Cisco blueprints'
                logging.warning(message)
        return cisco_blueprints

    def export_cisco_package(self, package_id, path):
        url = self.url + PACKAGE_PATH + '/' + package_id
        res = requests.get(url + '/validate', headers=self.headers, verify=False)
        self._check_response_code(res, 200)
        content = json.loads(res.content)
        if content['operationStatus'] != 'SUCCESS':
            raise ValueError('Exporting failed with message: ' + res.text)
        else:
            export_headers = {'Authorization': 'Bearer ' + self.token, 'Accept': 'application/zip'}
            res = requests.get(url, headers=export_headers, verify=False)
            self._check_response_code(res, 200)
            with open(path + ZIP_FILE_NAME, 'wb') as f:
                f.write(res.content)
            logging.info('Export successful, ' + ZIP_FILE_NAME + ' exported to: ' + path)

    def delete_cisco_blueprints(self, cisco_blueprints):
        """
        Delete Cisco blueprints

        """
        
        for cisco_blueprint in cisco_blueprints:
            logging.info('Deleting Cisco blueprint: ' + cisco_blueprint['name'] +  \
                         ' with id: ' + cisco_blueprint['id'])
            url = '%s%s/%s/%s/%s' % (self.url, SERVICE_BLUEPRINTS_PATH, self.tenant, 'blueprints', cisco_blueprint['id'])
            res = requests.delete(url, headers=self.headers, verify=False)
            self._check_response_code(res, 204)
            logging.info('Deleted Cisco blueprint : ' + cisco_blueprint['name'])

    def validate_and_import_zip_file(self, file_path):
        zip_file = {'file': open(file_path, 'rb')}
        url = self.url + PACKAGE_PATH
        logging.info('Validating zip file: ' + file_path)
        res = requests.post(url + '/validate', files=zip_file, headers=self.headers, verify=False)
        self._check_response_code(res, 200)
        content = json.loads(res.content)
        operation_status = content['operationStatus']
        if operation_status not in ('SUCCESS', 'WARNING'):
            raise ValueError('Importing failed with message ' + res.text)
        else:
            if operation_status == 'WARNING':
                logging.info('Import will overwrite old blueprints')
            logging.info('Validation successful')
            # without the line below, the code fails due to 'invalid zip file, zip file is either in the wrong format contains invalid content'
            zip_file = {'file': open(file_path, 'rb')}
            logging.info('Importing zip file: ' + file_path)
            res = requests.post(url, headers=self.headers, files=zip_file, verify=False)
            self._check_response_code(res, 200)
            logging.info('Import successful')

def configure_logging():
    logging.getLogger("requests").setLevel(logging.ERROR)
    log_filename = path.basename(__file__).rsplit('.')[0] + '.log'
    logging.basicConfig(filename=log_filename, filemode='w', level=logging.DEBUG, 
                        format='%(asctime)s %(message)s', datefmt='[%d/%m/%Y %I:%M:%S %p]')
    root = logging.getLogger()
    # adding logging to stdout as well as the log file
    streamHandler = logging.StreamHandler(sys.stdout)
    streamHandler.setLevel(logging.INFO)
    root.addHandler(streamHandler)

def parse_args():
    """
    Parse command-line arguments
    """
    help_text = 'Script to import/export Cisco APIC blueprints to/from vRealize Automation'
    parser = argparse.ArgumentParser(description=help_text)
    required_login = parser.add_argument_group('Required login arguments')
    help_text = 'The IP-Address'
    required_login.add_argument('--ip-address', help=help_text, required=True)
    help_text = 'The admin user with the domain name, ie: administrator@vsphere.local'
    required_login.add_argument('-a', '--admin',  help=help_text, required=True)
    required_login.add_argument('-p', '--password', required=True)
    help_text = 'Path to vra-properties.json file'
    required_login.add_argument('-f', '--filepath', help=help_text, required=True)
    help_text = 'The vRA tenant, Do NOT provide the default tenant'
    required_login.add_argument('-t', '--tenant', help=help_text, required=True)
    group = parser.add_mutually_exclusive_group(required=True)
    help_text = 'Imports a ZIP file to a vRA, provide filepath of zipfile, ie: /home/user/Documents/cisco-blueprints.zip'
    group.add_argument('-i', '--import-file', help=help_text)
    help_text = 'Exports Cisco blueprints as a zip file; provide directory to export zipfile, ie: /home/user/Documents'
    group.add_argument('-e', '--export', help=help_text)
    return parser.parse_args()

def import_cisco_apic_blueprints(cisco_vra_utils, file_path):
    """
    Import Cisco APIC blueprints in a zip file

    """
    if not path.isfile(file_path):
        raise ValueError('Cisco blueprint zip-file: \'%s\' is not a zip file' % file_path)
    else:
        # delete the old blueprints
        old_blueprint_names = cisco_vra_utils.old_blueprint_names
        old_cisco_blueprints = cisco_vra_utils.get_cisco_blueprints(True, old_blueprint_names)
        logging.info('Deleting old Cisco blueprints')
        cisco_vra_utils.delete_cisco_blueprints(old_cisco_blueprints)
        logging.info('Finished deleting old Cisco blueprints')

        # delete the current blueprints
        current_blueprint_names = cisco_vra_utils.cisco_blueprint_names
        cisco_blueprints = cisco_vra_utils.get_cisco_blueprints(True, current_blueprint_names)
        logging.info('Deleting Cisco APIC blueprints')
        cisco_vra_utils.delete_cisco_blueprints(cisco_blueprints)
        logging.info('Finished deleting Cisco APIC blueprints')

        cisco_vra_utils.validate_and_import_zip_file(file_path)

def export_cisco_apic_blueprints(cisco_vra_utils, file_path):
    """
    Export Cisco APIC blueprints as a zip file

    """
    if not path.isdir(file_path):
        raise ValueError('Export path: \'%s\' is not a valid path' % args.export)
    else:
        cisco_vra_utils.get_and_delete_cisco_package()
        current_blueprint_names = cisco_vra_utils.cisco_blueprint_names
        cisco_blueprints = cisco_vra_utils.get_cisco_blueprints(False, current_blueprint_names)
        cisco_vra_utils.create_cisco_package(cisco_blueprints)
        cisco_package_id = cisco_vra_utils.get_package_id(PACKAGE_NAME, PACKAGE_DESCRIPTION)
        if not file_path.endswith('/'):
            file_path += '/'
        cisco_vra_utils.export_cisco_package(cisco_package_id, file_path)

def main():
    args = parse_args()
    configure_logging()
    cisco_vra_utils = Cisco_VRA_Utils(args.ip_address, args.admin, args.password,
                                      args.tenant, args.filepath)
    if args.export:
        logging.info("Calling export_cisco_apic_blueprints")
        export_cisco_apic_blueprints(cisco_vra_utils, args.export)
    else:
        logging.info("Calling import_cisco_apic_blueprints")
        import_cisco_apic_blueprints(cisco_vra_utils, args.import_file)

if __name__ == '__main__':
    main()
