#!/usr/bin/python
#
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = '''
---
module: azure_rm_deployment

version_added: "0.1.0"

short_description: Create or destroy Azure Resource Manager template deployments

description:
    - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python.
    - You can find some quick start templates in GitHub here U(https://github.com/azure/azure-quickstart-templates).
    - For more information on Azure Resource Manager templates see U(https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/).

options:
  resource_group:
    description:
        - The resource group name to use or create to host the deployed template.
    required: true
    aliases:
        - resource_group_name
  name:
    description:
        - The name of the deployment to be tracked in the resource group deployment history.
        - Re-using a deployment name will overwrite the previous value in the resource group's deployment history.
    default: ansible-arm
    aliases:
        - deployment_name
  location:
    description:
        - The geo-locations in which the resource group will be located.
    default: westus
  deployment_mode:
    description:
        - In incremental mode, resources are deployed without deleting existing resources that are not included in the template.
        - In complete mode resources are deployed and existing resources in the resource group not included in the template are deleted.
    default: incremental
    choices:
        - complete
        - incremental
  template:
    description:
        - A hash containing the templates inline. This parameter is mutually exclusive with I(template_link).
        - Either I(template) or I(template_link) is required if I(state=present).
    type: dict
  template_link:
    description:
        - Uri of file containing the template body. This parameter is mutually exclusive with I(template).
        - Either I(template) or I(template_link) is required if I(state=present).
  parameters:
    description:
        - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with I(parameters_link).
        - Either I(parameters_link) or I(parameters) is required if I(state=present).
    type: dict
  parameters_link:
    description:
        - Uri of file containing the parameters body. This parameter is mutually exclusive with I(parameters).
        - Either I(parameters_link) or I(parameters) is required if I(state=present).
  wait_for_deployment_completion:
    description:
        - Whether or not to block until the deployment has completed.
    type: bool
    default: 'yes'
  wait_for_deployment_polling_period:
    description:
        - Time (in seconds) to wait between polls when waiting for deployment completion.
    default: 10
  state:
    description:
        - If I(state=present), template will be created.
        - If I(state=present) and deployment exists, it will be updated.
        - If I(state=absent), the resource group will be removed.
    default: present
    choices:
        - present
        - absent

extends_documentation_fragment:
    - azure.azcollection.azure
    - azure.azcollection.azure_tags

author:
    - David Justice (@devigned)
    - Laurent Mazuel (@lmazuel)
    - Andre Price (@obsoleted)

'''

EXAMPLES = '''
# Destroy a template deployment
- name: Destroy Azure Deploy
  azure_rm_deployment:
    resource_group: myResourceGroup
    name: myDeployment
    state: absent

# Create or update a template deployment based on uris using parameter and template links
- name: Create Azure Deploy
  azure_rm_deployment:
    resource_group: myResourceGroup
    name: myDeployment
    template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.json'
    parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.parameters.json'

# Create or update a template deployment based on a uri to the template and parameters specified inline.
# This deploys a VM with SSH support for a given public key, then stores the result in 'azure_vms'. The result is then
# used to create a new host group. This host group is then used to wait for each instance to respond to the public IP SSH.

- name: Create Azure Deploy
  azure_rm_deployment:
    resource_group: myResourceGroup
    name: myDeployment
    parameters:
      newStorageAccountName:
        value: devopsclestorage1
      adminUsername:
        value: devopscle
      dnsNameForPublicIP:
        value: devopscleazure
      location:
        value: West US
      vmSize:
        value: Standard_A2
      vmName:
        value: ansibleSshVm
      sshKeyData:
        value: YOUR_SSH_PUBLIC_KEY
    template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-sshkey/azuredeploy.json'
  register: azure
- name: Add new instance to host group
  add_host:
    hostname: "{{ item['ips'][0].public_ip }}"
    groupname: azure_vms
  loop: "{{ azure.deployment.instances }}"

# Deploy an Azure WebApp running a hello world'ish node app
- name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js
  azure_rm_deployment:
    resource_group: myResourceGroup
    name: myDeployment
    parameters:
      repoURL:
        value: 'https://github.com/devigned/az-roadshow-oss.git'
      siteName:
        value: devopscleweb
      hostingPlanName:
        value: someplan
      siteLocation:
        value: westus
      sku:
        value: Standard
    template_link: 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json'

# Create or update a template deployment based on an inline template and parameters
- name: Create Azure Deploy
  azure_rm_deployment:
    resource_group: myResourceGroup
    name: myDeployment
    template:
      $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#"
      contentVersion: "1.0.0.0"
      parameters:
        newStorageAccountName:
          type: "string"
          metadata:
            description: "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed."
        adminUsername:
          type: "string"
          metadata:
            description: "User name for the Virtual Machine."
        adminPassword:
          type: "securestring"
          metadata:
            description: "Password for the Virtual Machine."
        dnsNameForPublicIP:
          type: "string"
          metadata:
            description: "Unique DNS Name for the Public IP used to access the Virtual Machine."
        ubuntuOSVersion:
          type: "string"
          defaultValue: "14.04.2-LTS"
          allowedValues:
            - "12.04.5-LTS"
            - "14.04.2-LTS"
            - "15.04"
          metadata:
            description: >
                         The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version.
                         Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04."
      variables:
        location: "West US"
        imagePublisher: "Canonical"
        imageOffer: "UbuntuServer"
        OSDiskName: "osdiskforlinuxsimple"
        nicName: "myVMNic"
        addressPrefix: "192.0.2.0/24"
        subnetName: "Subnet"
        subnetPrefix: "10.0.0.0/24"
        storageAccountType: "Standard_LRS"
        publicIPAddressName: "myPublicIP"
        publicIPAddressType: "Dynamic"
        vmStorageAccountContainerName: "vhds"
        vmName: "MyUbuntuVM"
        vmSize: "Standard_D1"
        virtualNetworkName: "MyVNET"
        vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]"
        subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]"
      resources:
        - type: "Microsoft.Storage/storageAccounts"
          name: "[parameters('newStorageAccountName')]"
          apiVersion: "2015-05-01-preview"
          location: "[variables('location')]"
          properties:
            accountType: "[variables('storageAccountType')]"
        - apiVersion: "2015-05-01-preview"
          type: "Microsoft.Network/publicIPAddresses"
          name: "[variables('publicIPAddressName')]"
          location: "[variables('location')]"
          properties:
            publicIPAllocationMethod: "[variables('publicIPAddressType')]"
            dnsSettings:
              domainNameLabel: "[parameters('dnsNameForPublicIP')]"
        - type: "Microsoft.Network/virtualNetworks"
          apiVersion: "2015-05-01-preview"
          name: "[variables('virtualNetworkName')]"
          location: "[variables('location')]"
          properties:
            addressSpace:
              addressPrefixes:
                - "[variables('addressPrefix')]"
            subnets:
              -
                name: "[variables('subnetName')]"
                properties:
                  addressPrefix: "[variables('subnetPrefix')]"
        - type: "Microsoft.Network/networkInterfaces"
          apiVersion: "2015-05-01-preview"
          name: "[variables('nicName')]"
          location: "[variables('location')]"
          dependsOn:
            - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
            - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
          properties:
            ipConfigurations:
              -
                name: "ipconfig1"
                properties:
                  privateIPAllocationMethod: "Dynamic"
                  publicIPAddress:
                    id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
                  subnet:
                    id: "[variables('subnetRef')]"
        - type: "Microsoft.Compute/virtualMachines"
          apiVersion: "2015-06-15"
          name: "[variables('vmName')]"
          location: "[variables('location')]"
          dependsOn:
            - "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]"
            - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
          properties:
            hardwareProfile:
              vmSize: "[variables('vmSize')]"
            osProfile:
              computername: "[variables('vmName')]"
              adminUsername: "[parameters('adminUsername')]"
              adminPassword: "[parameters('adminPassword')]"
            storageProfile:
              imageReference:
                publisher: "[variables('imagePublisher')]"
                offer: "[variables('imageOffer')]"
                sku: "[parameters('ubuntuOSVersion')]"
                version: "latest"
              osDisk:
                name: "osdisk"
                vhd:
                  uri: >
                       [concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',
                       variables('OSDiskName'),'.vhd')]
                caching: "ReadWrite"
                createOption: "FromImage"
            networkProfile:
              networkInterfaces:
                -
                  id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
            diagnosticsProfile:
              bootDiagnostics:
                enabled: "true"
                storageUri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]"
    parameters:
      newStorageAccountName:
        value: devopsclestorage
      adminUsername:
        value: devopscle
      adminPassword:
        value: Password1!
      dnsNameForPublicIP:
        value: devopscleazure
'''

RETURN = '''
deployment:
    description: Deployment details.
    type: complex
    returned: always
    contains:
        group_name:
            description:
                - Name of the resource group.
            type: str
            returned: always
            sample: myResourceGroup
        id:
            description:
                - The Azure ID of the deployment.
            type: str
            returned: always
            sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Resources/deployments/myD
                     eployment"
        instances:
            description:
                - Provides the public IP addresses for each VM instance.
            type: list
            returned: always
            contains:
                ips:
                    description:
                        - List of Public IP addresses.
                    type: list
                    returned: always
                    contains:
                        dns_settings:
                            description:
                                - DNS Settings.
                            type: complex
                            returned: always
                            contains:
                                domain_name_label:
                                    description:
                                        - Domain Name Label.
                                    type: str
                                    returned: always
                                    sample: myvirtualmachine
                                fqdn:
                                    description:
                                        - Fully Qualified Domain Name.
                                    type: str
                                    returned: always
                                    sample: myvirtualmachine.eastus2.cloudapp.azure.com
                        id:
                            description:
                                - Public IP resource id.
                            returned: always
                            type: str
                            sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Network/p
                                     ublicIPAddresses/myPublicIP"
                        name:
                            description:
                                -  Public IP resource name.
                            returned: always
                            type: str
                            sample: myPublicIP
                        public_ip:
                            description:
                                - Public IP address value.
                            returned: always
                            type: str
                            sample: 104.209.244.123
                        public_ip_allocation_method:
                            description:
                                - Public IP allocation method.
                            returned: always
                            type: str
                            sample: Dynamic
                vm_name:
                    description:
                        - Virtual machine name.
                    returned: always
                    type: str
                    sample: myvirtualmachine
        name:
          description:
              - Name of the deployment.
          type: str
          returned: always
          sample: myDeployment
        outputs:
          description:
              - Dictionary of outputs received from the deployment.
          type: complex
          returned: always
          sample: { "hostname": { "type": "String", "value": "myvirtualmachine.eastus2.cloudapp.azure.com" } }
'''

import time

try:
    from azure.common.credentials import ServicePrincipalCredentials
    import time
    import yaml
except ImportError as exc:
    IMPORT_ERROR = "Error importing module prerequisites: %s" % exc

try:
    from itertools import chain
    from azure.core.exceptions import ResourceNotFoundError
    from azure.mgmt.resource.resources import ResourceManagementClient
    from azure.mgmt.network import NetworkManagementClient

except ImportError:
    # This is handled in azure_rm_common
    pass

from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase


class AzureRMDeploymentManager(AzureRMModuleBase):

    def __init__(self):

        self.module_arg_spec = dict(
            resource_group=dict(type='str', required=True, aliases=['resource_group_name']),
            name=dict(type='str', default="ansible-arm", aliases=['deployment_name']),
            state=dict(type='str', default='present', choices=['present', 'absent']),
            template=dict(type='dict', default=None),
            parameters=dict(type='dict', default=None),
            template_link=dict(type='str', default=None),
            parameters_link=dict(type='str', default=None),
            location=dict(type='str', default="westus"),
            deployment_mode=dict(type='str', default='incremental', choices=['complete', 'incremental']),
            wait_for_deployment_completion=dict(type='bool', default=True),
            wait_for_deployment_polling_period=dict(type='int', default=10)
        )

        mutually_exclusive = [('template', 'template_link'),
                              ('parameters', 'parameters_link')]

        self.resource_group = None
        self.state = None
        self.template = None
        self.parameters = None
        self.template_link = None
        self.parameters_link = None
        self.location = None
        self.deployment_mode = None
        self.name = None
        self.wait_for_deployment_completion = None
        self.wait_for_deployment_polling_period = None
        self.tags = None
        self.append_tags = None

        self.results = dict(
            deployment=dict(),
            changed=False,
            msg=""
        )

        super(AzureRMDeploymentManager, self).__init__(derived_arg_spec=self.module_arg_spec,
                                                       mutually_exclusive=mutually_exclusive,
                                                       supports_check_mode=False)

    def exec_module(self, **kwargs):

        for key in list(self.module_arg_spec.keys()) + ['append_tags', 'tags']:
            setattr(self, key, kwargs[key])

        if self.state == 'present':
            deployment = self.deploy_template()
            if deployment is None:
                self.results['deployment'] = dict(
                    name=self.name,
                    group_name=self.resource_group,
                    id=None,
                    outputs=None,
                    instances=None
                )
            else:
                self.results['deployment'] = dict(
                    name=deployment.name,
                    group_name=self.resource_group,
                    id=deployment.id,
                    outputs=deployment.properties.outputs,
                    instances=self._get_instances(deployment)
                )

            self.results['changed'] = True
            self.results['msg'] = 'deployment succeeded'
        else:
            try:
                if self.get_resource_group(self.resource_group):
                    self.destroy_resource_group()
                    self.results['changed'] = True
                    self.results['msg'] = "deployment deleted"
            except Exception:
                # resource group does not exist
                pass

        return self.results

    def deploy_template(self):
        """
        Deploy the targeted template and parameters
        :param module: Ansible module containing the validated configuration for the deployment template
        :param client: resource management client for azure
        :param conn_info: connection info needed
        :return:
        """

        deploy_parameter = self.rm_models.DeploymentProperties(mode=self.deployment_mode)
        if not self.parameters_link:
            deploy_parameter.parameters = self.parameters
        else:
            deploy_parameter.parameters_link = self.rm_models.ParametersLink(
                uri=self.parameters_link
            )
        if not self.template_link:
            deploy_parameter.template = self.template
        else:
            deploy_parameter.template_link = self.rm_models.TemplateLink(
                uri=self.template_link
            )

        try:
            # fetch the RG directly (instead of using the base helper) since we don't want to exit if it's missing
            rg = self.rm_client.resource_groups.get(self.resource_group)
            if rg.tags:
                update_tags, self.tags = self.update_tags(rg.tags)
        except ResourceNotFoundError:
            # resource group does not exist
            pass

        params = self.rm_models.ResourceGroup(location=self.location, tags=self.tags)

        try:
            self.rm_client.resource_groups.create_or_update(self.resource_group, params)
        except Exception as exc:
            self.fail("Resource group create_or_update failed with status code: %s and message: %s" %
                      (exc.status_code, exc.message))
        try:
            result = self.rm_client.deployments.begin_create_or_update(self.resource_group,
                                                                       self.name,
                                                                       {'properties': deploy_parameter})

            deployment_result = None
            if self.wait_for_deployment_completion:
                deployment_result = self.get_poller_result(result)
                while deployment_result.properties is None or deployment_result.properties.provisioning_state not in ['Canceled', 'Failed', 'Deleted',
                                                                                                                      'Succeeded']:
                    time.sleep(self.wait_for_deployment_polling_period)
                    deployment_result = self.rm_client.deployments.get(self.resource_group, self.name)
        except Exception as exc:
            failed_deployment_operations = self._get_failed_deployment_operations(self.name)
            self.log("Deployment failed %s: %s" % (exc.status_code, exc.message))
            error_msg = self._error_msg_from_cloud_error(exc)
            self.fail(error_msg, failed_deployment_operations=failed_deployment_operations)

        if self.wait_for_deployment_completion and deployment_result.properties.provisioning_state != 'Succeeded':
            self.log("provisioning state: %s" % deployment_result.properties.provisioning_state)
            failed_deployment_operations = self._get_failed_deployment_operations(self.name)
            self.fail('Deployment failed. Deployment id: %s' % deployment_result.id,
                      failed_deployment_operations=failed_deployment_operations)

        return deployment_result

    def destroy_resource_group(self):
        """
        Destroy the targeted resource group
        """
        try:
            result = self.rm_client.resource_groups.begin_delete(self.resource_group)
            result.wait()  # Blocking wait till the delete is finished
        except Exception as e:
            if e.status_code == 404 or e.status_code == 204:
                return
            else:
                self.fail("Delete resource group and deploy failed with status code: %s and message: %s" %
                          (e.status_code, e.message))

    def _get_failed_nested_operations(self, current_operations):
        new_operations = []
        for operation in current_operations:
            if operation.properties.provisioning_state == 'Failed':
                new_operations.append(operation)
                if operation.properties.target_resource and \
                   'Microsoft.Resources/deployments' in operation.properties.target_resource.id:
                    nested_deployment = operation.properties.target_resource.resource_name
                    try:
                        nested_operations = self.rm_client.deployment_operations.list(self.resource_group,
                                                                                      nested_deployment)
                    except Exception as exc:
                        self.fail("List nested deployment operations failed with status code: %s and message: %s" %
                                  (exc.status_code, exc.message))
                    new_nested_operations = self._get_failed_nested_operations(nested_operations)
                    new_operations += new_nested_operations
        return new_operations

    def _get_failed_deployment_operations(self, name):
        results = []
        # time.sleep(15) # there is a race condition between when we ask for deployment status and when the
        #               # status is available.

        try:
            operations = self.rm_client.deployment_operations.list(self.resource_group, name)
        except Exception as exc:
            self.fail("Get deployment failed with status code: %s and message: %s" %
                      (exc.status_code, exc.message))
        try:
            results = [
                dict(
                    id=op.id,
                    operation_id=op.operation_id,
                    status_code=op.properties.status_code,
                    status_message=op.properties.status_message,
                    target_resource=dict(
                        id=op.properties.target_resource.id,
                        resource_name=op.properties.target_resource.resource_name,
                        resource_type=op.properties.target_resource.resource_type
                    ) if op.properties.target_resource else None,
                    provisioning_state=op.properties.provisioning_state,
                )
                for op in self._get_failed_nested_operations(operations)
            ]
        except Exception:
            # If we fail here, the original error gets lost and user receives wrong error message/stacktrace
            pass
        self.log(dict(failed_deployment_operations=results), pretty_print=True)
        return results

    def _get_instances(self, deployment):
        dep_tree = self._build_hierarchy(deployment.properties.dependencies)
        vms = self._get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines")
        vms_and_nics = [(vm, self._get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces"))
                        for vm in vms]
        vms_and_ips = [(vm['dep'], self._nic_to_public_ips_instance(nics))
                       for vm, nics in vms_and_nics]
        return [dict(vm_name=vm.resource_name, ips=[self._get_ip_dict(ip)
                                                    for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0]

    def _get_dependencies(self, dep_tree, resource_type):
        matches = [value for value in dep_tree.values() if value['dep'].resource_type == resource_type]
        for child_tree in [value['children'] for value in dep_tree.values()]:
            matches += self._get_dependencies(child_tree, resource_type)
        return matches

    def _build_hierarchy(self, dependencies, tree=None):
        tree = dict(top=True) if tree is None else tree
        for dep in dependencies:
            if dep.resource_name not in tree:
                tree[dep.resource_name] = dict(dep=dep, children=dict())
            if isinstance(dep, self.rm_models.Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0:
                self._build_hierarchy(dep.depends_on, tree[dep.resource_name]['children'])

        if 'top' in tree:
            tree.pop('top', None)
            keys = list(tree.keys())
            for key1 in keys:
                for key2 in keys:
                    if key2 in tree and key1 in tree[key2]['children'] and key1 in tree:
                        tree[key2]['children'][key1] = tree[key1]
                        tree.pop(key1)
        return tree

    def _get_ip_dict(self, ip):
        ip_dict = dict(name=ip.name,
                       id=ip.id,
                       public_ip=ip.ip_address,
                       public_ip_allocation_method=str(ip.public_ip_allocation_method)
                       )
        if ip.dns_settings:
            ip_dict['dns_settings'] = {
                'domain_name_label': ip.dns_settings.domain_name_label,
                'fqdn': ip.dns_settings.fqdn
            }
        return ip_dict

    def _nic_to_public_ips_instance(self, nics):
        nic_list = []
        for nic in nics:
            resp = None
            try:
                resp = self.network_client.network_interfaces.get(self.resource_group, nic['dep'].resource_name)
            except ResourceNotFoundError:
                pass
            if resp is not None:
                nic_list.append(resp)

        return [self.network_client.public_ip_addresses.get(public_ip_id.split('/')[4], public_ip_id.split('/')[-1])
                for nic_obj in nic_list
                for public_ip_id in [ip_conf_instance.public_ip_address.id
                                     for ip_conf_instance in nic_obj.ip_configurations
                                     if ip_conf_instance.public_ip_address]]

    def _error_msg_from_cloud_error(self, exc):
        msg = ''
        status_code = str(exc.status_code)
        if status_code.startswith('2'):
            msg = 'Deployment failed: {0}'.format(exc.message)
        else:
            msg = 'Deployment failed with status code: {0} and message: {1}'.format(status_code, exc.message)
        return msg


def main():
    AzureRMDeploymentManager()


if __name__ == '__main__':
    main()
