#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2017, F5 Networks Inc.
# 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 = r'''
---
module: bigip_lx_package
short_description: Manages Javascript LX packages on a BIG-IP
description:
  - Manages Javascript LX packages on a BIG-IP. This module allows
    you to deploy LX packages to the BIG-IP and manage their lifecycle.
version_added: "1.0.0"
options:
  package:
    description:
      - The LX package that you want to upload or remove. When C(state) is C(present),
        and you intend to use this module in a C(role), we recommend you use
        the C({{ role_path }}) variable. An example is provided in the C(EXAMPLES) section.
      - When C(state) is C(absent), it is not necessary for the package to exist on the
        Ansible controller. If the full path to the package is provided, the fileame will
        specifically be cherry-picked from it to properly remove the package.
    type: path
  state:
    description:
      - Whether the LX package should exist or not.
    type: str
    default: present
    choices:
      - present
      - absent
  retain_package_file:
    description:
      - Specifies whether the install file should be deleted on successful installation of the package.
    type: bool
    default: false
    version_added: "1.4.0"
notes:
  - Requires the RPM tool be installed on the host. This can be accomplished in
    different ways on each platform. On Debian based systems with C(apt);
    C(apt-get install rpm). On Mac with C(brew); C(brew install rpm).
    This command is already present on RedHat based systems.
  - Requires BIG-IP >= 12.1.0, because the required functionality is missing
    on prior versions.
  - The module name C(bigip_iapplx_package) has been deprecated in favor of C(bigip_lx_package).
requirements:
  - Requires BIG-IP >= 12.1.0
  - The 'rpm' tool installed on the Ansible controller
extends_documentation_fragment: f5networks.f5_modules.f5
author:
  - Tim Rupp (@caphrim007)
  - Wojciech Wypior (@wojtek0806)
'''

EXAMPLES = r'''
- name: Install AS3
  bigip_lx_package:
    package: f5-appsvcs-3.5.0-3.noarch.rpm
    provider:
      password: secret
      server: lb.mydomain.com
      user: admin
  delegate_to: localhost

- name: Add an LX package stored in a role
  bigip_lx_package:
    package: "{{ roles_path }}/files/MyApp-0.1.0-0001.noarch.rpm'"
    provider:
      password: secret
      server: lb.mydomain.com
      user: admin
  delegate_to: localhost

- name: Remove an LX package
  bigip_lx_package:
    package: MyApp-0.1.0-0001.noarch.rpm
    state: absent
    provider:
      password: secret
      server: lb.mydomain.com
      user: admin
  delegate_to: localhost

- name: Install AS3 and don't delete package file
  bigip_lx_package:
    package: f5-appsvcs-3.5.0-3.noarch.rpm
    retain_package_file: true
    provider:
      password: secret
      server: lb.mydomain.com
      user: admin
  delegate_to: localhost
'''

RETURN = r'''
# only common fields returned
'''

import os
import time
import traceback
from datetime import datetime

try:
    from packaging.version import Version
except ImportError:
    HAS_PACKAGING = False
    Version = None
    PACKAGING_IMPORT_ERROR = traceback.format_exc()
else:
    HAS_PACKAGING = True
    PACKAGING_IMPORT_ERROR = None

from ansible.module_utils.basic import (
    AnsibleModule, missing_required_lib
)
from ansible.module_utils.urls import urlparse

from ..module_utils.bigip import F5RestClient
from ..module_utils.common import (
    F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean
)
from ..module_utils.icontrol import (
    tmos_version, upload_file
)
from ..module_utils.teem import send_teem


class Parameters(AnsibleF5Parameters):
    api_attributes = []
    returnables = []

    @property
    def package(self):
        if self._values['package'] is None:
            return None
        return self._values['package']

    @property
    def package_file(self):
        if self._values['package'] is None:
            return None
        return os.path.basename(self._values['package'])

    @property
    def package_name(self):
        """Return a valid name for the package

        BIG-IP determines the package name by the content of the RPM info.
        It does not use the filename. Therefore, we do the same. This method
        is only used though when the file actually exists on your Ansible
        controller.

        If the package does not exist, then we instead use the filename
        portion of the 'package' argument that is provided.

        Non-existence typically occurs when using 'state' = 'absent'

        :return:
        """
        cmd = ['rpm', '-qp', '--queryformat', '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}', self.package]
        rc, out, err = self._module.run_command(cmd)
        if not out:
            return str(self.package_file)
        return out

    @property
    def package_root(self):
        if self._values['package'] is None:
            return None
        base = os.path.basename(self._values['package'])
        result = os.path.splitext(base)
        return result[0]

    @property
    def retain_package_file(self):
        return flatten_boolean(self._values['retain_package_file'])


class ApiParameters(Parameters):
    pass


class ModuleParameters(Parameters):
    pass


class Changes(Parameters):
    def to_return(self):
        result = {}
        try:
            for returnable in self.returnables:
                result[returnable] = getattr(self, returnable)
            result = self._filter_params(result)
        except Exception:
            raise
        return result


class UsableChanges(Changes):
    pass


class ReportableChanges(Changes):
    pass


class ModuleManager(object):
    def __init__(self, *args, **kwargs):
        self.module = kwargs.get('module', None)
        self.client = F5RestClient(**self.module.params)
        self.want = ModuleParameters(module=self.module, params=self.module.params)
        self.changes = UsableChanges()

    def exec_module(self):
        result = dict()
        start = datetime.now().isoformat()
        version = tmos_version(self.client)
        changed = False
        state = self.want.state
        if Version(version) <= Version('12.0.0'):
            raise F5ModuleError(
                "This version of BIG-IP is not supported."
            )

        if state == "present":
            changed = self.present()
        elif state == "absent":
            changed = self.absent()

        changes = self.changes.to_return()
        result.update(**changes)
        result.update(dict(changed=changed))
        send_teem(start, self.client, self.module, version)
        return result

    def present(self):
        if self.exists():
            return False
        else:
            return self.create()

    def absent(self):
        changed = False
        if self.exists():
            changed = self.remove()
        return changed

    def remove(self):
        if self.module.check_mode:
            return True
        self.remove_from_device()
        if self.exists():
            raise F5ModuleError("Failed to delete the LX package.")
        return True

    def create(self):
        if self.module.check_mode:
            return True
        if not os.path.exists(self.want.package):
            if self.want.package.startswith('/'):
                raise F5ModuleError(
                    "The specified LX package was not found at {0}.".format(self.want.package)
                )
            else:
                raise F5ModuleError(
                    "The specified LX package was not found in {0}.".format(os.getcwd())
                )
        if not self.check_file_exists_on_device():
            self.upload_to_device()
        self.create_on_device()
        self.enable_iapplx_on_device()
        if self.want.retain_package_file == 'no':
            self.remove_package_file_from_device()
        if self.exists():
            return True
        else:
            raise F5ModuleError("Failed to install LX package.")

    def exists(self):
        exists = False
        packages = self.get_installed_packages_on_device()
        if os.path.exists(self.want.package):
            exists = True
        for package in packages:
            if exists:
                if self.want.package_name == package['packageName']:
                    return True
            else:
                if self.want.package_root == package['packageName']:
                    return True
        return False

    def get_installed_packages_on_device(self):
        uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        params = dict(operation='QUERY')
        resp = self.client.api.post(uri, json=params)

        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]:
            raise F5ModuleError(resp.content)

        path = urlparse(response["selfLink"]).path
        task = self._wait_for_task(path)

        if task['status'] == 'FINISHED':
            return task['queryResponse']
        raise F5ModuleError(
            "Failed to find the installed packages on the device."
        )

    def _wait_for_task(self, path):
        task = None
        for x in range(0, 60):
            task = self.check_task_on_device(path)
            if task['status'] in ['FINISHED', 'FAILED']:
                return task
            time.sleep(1)
        return task

    def check_task_on_device(self, path):
        uri = "https://{0}:{1}{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            path
        )
        resp = self.client.api.get(uri)

        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]:
            return response
        raise F5ModuleError(resp.content)

    def upload_to_device(self):
        url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        try:
            upload_file(self.client, url, self.want.package)
        except F5ModuleError:
            raise F5ModuleError(
                "Failed to upload the file."
            )

    def check_file_exists_on_device(self):
        params = dict(
            command="run",
            utilCmdArgs="/var/config/rest/downloads/{0}".format(self.want.package_file)
        )
        uri = "https://{0}:{1}/mgmt/tm/util/unix-ls".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        resp = self.client.api.post(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))
        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            if 'commandResult' in response:
                if 'No such file or directory' in response['commandResult']:
                    return False
                elif self.want.package_file in response['commandResult']:
                    return True
        raise F5ModuleError(resp.content)

    def remove_package_file_from_device(self):
        params = dict(
            command="run",
            utilCmdArgs="/var/config/rest/downloads/{0}".format(self.want.package_file)
        )
        uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        resp = self.client.api.post(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]:
            return True
        raise F5ModuleError(resp.content)

    def create_on_device(self):
        remote_path = "/var/config/rest/downloads/{0}".format(self.want.package_file)
        params = dict(
            operation='INSTALL', packageFilePath=remote_path
        )
        uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )

        resp = self.client.api.post(uri, json=params)

        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]:
            raise F5ModuleError(resp.content)

        path = urlparse(response["selfLink"]).path
        task = self._wait_for_task(path)

        if task['status'] == 'FINISHED':
            return True
        else:
            raise F5ModuleError(task['errorMessage'])

    def remove_from_device(self):
        params = dict(
            operation='UNINSTALL',
            packageName=self.want.package_root
        )
        uri = "https://{0}:{1}/mgmt/shared/iapp/package-management-tasks".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )

        resp = self.client.api.post(uri, json=params)

        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status not in [200, 201, 202] or 'code' in response and response['code'] not in [200, 201, 202]:
            raise F5ModuleError(resp.content)

        path = urlparse(response["selfLink"]).path
        task = self._wait_for_task(path)

        if task['status'] == 'FINISHED':
            return True
        return False

    def enable_iapplx_on_device(self):
        params = dict(
            command="run",
            utilCmdArgs='-c "touch /var/config/rest/iapps/enable"'
        )
        uri = "https://{0}:{1}/mgmt/tm/util/bash".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        resp = self.client.api.post(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201, 202] or 'code' in response and response['code'] in [200, 201, 202]:
            return True
        raise F5ModuleError(resp.content)


class ArgumentSpec(object):
    def __init__(self):
        self.supports_check_mode = True
        argument_spec = dict(
            state=dict(
                default='present',
                choices=['present', 'absent']
            ),
            package=dict(type='path'),
            retain_package_file=dict(
                default='no',
                type='bool'
            ),
        )
        self.argument_spec = {}
        self.argument_spec.update(f5_argument_spec)
        self.argument_spec.update(argument_spec)
        self.required_if = [
            ['state', 'present', ['package']]
        ]


def main():
    spec = ArgumentSpec()

    module = AnsibleModule(
        argument_spec=spec.argument_spec,
        supports_check_mode=spec.supports_check_mode,
        required_if=spec.required_if
    )

    if not HAS_PACKAGING:
        module.fail_json(
            msg=missing_required_lib('packaging'),
            exception=PACKAGING_IMPORT_ERROR
        )

    try:
        mm = ModuleManager(module=module)
        results = mm.exec_module()
        module.exit_json(**results)
    except F5ModuleError as ex:
        module.fail_json(msg=str(ex))


if __name__ == '__main__':
    main()
