#!/usr/bin/env python
# This script should be run via sudo.
# COPYRIGHT (c) 2013-2021. THIS CODE AND THE SOFTWARE WHICH SUCH CODE RELATES TO
# (COLLECTIVELY THE "SOLUTION") ARE PROTECTED BY COPYRIGHT OWNED BY CLOUDENDURE LTD.
# ("CLOUDENDURE"). NO PART OF THE SOLUTION MAY THEREFORE BE COPIED, DUPLICATED,
# REPRODUCED, REPUBLISHED, RE-DISSEMINATED, CHANGED, ALTERED, LOANED, LICENSED,
# TRANSFERRED, DISTRIBUTED OR OTHERWISE DISCLOSED TO ANY THIRD PARTY WITHOUT THE
# PRIOR WRITTEN CONSENT OF CLOUDENDURE. THIS CODE MUST BE USED SOLELY FOR THE
# INTENDED PURPOSE FOR WHICH IT WAS PROVIDED TO YOU IN CONNECTION WITH CLOUDENDURE'S
# SOFTWARE, AND SHOULD NOT BE USED FOR ANY OTHER PURPOSE.

# CLOUDENDURE MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED, OR STATUTORY,
# INCLUDING BUT NOT LIMITED TO ALL WARRANTIES OF MERCHANTABILITY, NON INFRINGEMENT
# OF THIRD PARTY'S INTELLECTUAL PROPERTY OR FITNESS FOR A PARTICULAR PURPOSE,
# RELATED TO THE SOLUTION. ANY USE OF THE SOLUTION, OR ANY PART THEREOF, AND ANY
# USE OR RELIANCE ON ANY OUTPUT RESULTING FROM USE OF THE SOLUTION IS AT THE USER'S
# SOLE RISK.

# IN NO EVENT WILL CLOUDENDURE BE LIABLE TO ANY USER OF THE SOLUTION OR ANY PART
# THEREOF, AND ANY OUTPUT RESULTING FROM USE OF THE SOLUTION FOR ANY DAMAGES
# WHATSOEVER, DIRECT OR INDIRECT, SPECIAL, CONSEQUENTIAL OR SIMILAR DAMAGES,
# INCLUDING ANY LOST PROFITS, REVENUES, GOODWILL OR LOST DATA ARISING OUT OF THIS
# AGREEMENT, THE USE OR INABILITY TO USE THE SOLUTION, THE PERFORMANCE OF THE
# SOLUTION, EVEN IF CLOUDENDURE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGES.


import os
import shutil
import stat
import subprocess
import sys
import tempfile


# Though it would be cleaner to use namedtuple here, python 2.4 does not support it, so we use a class
class DF(object):  # pylint: disable=too-few-public-methods
    def __init__(self, filesystem, blocks, used, available, capacity, mounted_on):
        self.filesystem = filesystem
        self.blocks = blocks
        self.used = used
        self.available = available
        self.capacity = capacity
        self.mounted_on = mounted_on


CE_INSTALLER = "cloudendure_installer"

# non-default temp dir (not '/tmp')
CE_TEMP_DIR = '/var/lib/cloudendure/.tmp_install'

IS_PYTHON2 = sys.version_info[0] == 2

REQUIRED_DISK_SPACE = 500 * 1024 * 1024


def main():
    print("The installation of the CloudEndure Agent has started.")  # pylint: disable=superfluous-parens

    if IS_PYTHON2:
        from urllib2 import urlopen  # pylint: disable=no-name-in-module,import-error
        if sys.maxint > (2 ** 31):  # pylint: disable=no-member
            arch = 64
        else:
            arch = 32
    else:
        from urllib.request import urlopen  # pylint: disable=no-name-in-module,import-error
        if sys.maxsize > (2 ** 31):
            arch = 64
        else:
            arch = 32

    print("Running the Agent Installer for a " + str(arch) + " bit system...")  # pylint: disable=superfluous-parens

    the_temp_dir = tempfile.gettempdir()
    if not can_execute_from_default_temp_dir():
        set_alternative_temp_dir()
        the_temp_dir = CE_TEMP_DIR

    doing = ""
    try:
        doing = 'checking disk free space'
        if not check_free_space(REQUIRED_DISK_SPACE):
            print('Not enough available disk space, exiting.')  # pylint: disable=superfluous-parens
            return -1

        doing = "downloading installer"

        # need to use temp dir returned from the 'handle_noexec_tmp()' above
        # reason: the tempfile.gettempdir() uses cached 'global tempdir' variable for current process
        # the 'CE_INSTALLER' cannot run from '/tmp' temp dir in case it is a mount with 'noexec' flags -
        #   - when agent installation is run from /tmp
        ce_installer_path = os.path.join(the_temp_dir, CE_INSTALLER)

        content = dowload_installer_content(urlopen, arch)
        open(ce_installer_path, "wb").write(content)

        doing = "preparing installer to run"
        currentStat = os.stat(ce_installer_path).st_mode
        os.chmod(ce_installer_path, currentStat | stat.S_IEXEC)

        doing = "running installer"
        return subprocess.call([ce_installer_path] + sys.argv[1:])

    except Exception:  # pylint: disable=broad-except
        # We have to retrieve exception info this way due to the need to remain
        # compatible with both Python 2.4 and Python 3.6
        _, e, _ = sys.exc_info()

        if doing == "downloading installer":
            # pylint: disable=line-too-long, superfluous-parens
            print('''Connecting to console.cloudendure.com on port 443 failed.
This is usually caused by lack of routing to console.cloudedure.com, firewall configuration (local or in a firewall appliance) or incorrect web proxy configuration on this server. 
Please resolve the connectivity issue and run the installer again.
For further assistance, please contact CloudEndure support at https://bit.ly/2T54hSc''')
        else:
            print("Error " + doing + "! Please contact CloudEndure support at https://bit.ly/2T54hSc")  # pylint: disable=superfluous-parens

        print("Error details: " + str(e))  # pylint: disable=superfluous-parens

        return -1


def dowload_installer_content(urlopen_function, arch):
    # do not delete this line {{MOCK_CODE_PLACEHOLDER}} - it is used from 'test_installer_linux_compile.py'

    url = "https://console.cloudendure.com/api/v5/static/installer_linux" + str(arch)
    content = urlopen_function(url).read()
    return content


def check_free_space(required):
    fs = get_fs('.')
    return required <= int(fs.available) * 1024  # the fields are in 1024-blocks


def can_execute_from_default_temp_dir():
    """
    This function checks if the 'tempfile.gettempdir()' is a directory from which executable script can be run

    Note: The '/tmp' directory is required to (and can) be not executable according to CIS standards
    The directory 'my_dir' can be made 'noexec' mount by 'mount -t tmpfs -o noexec /dev/shm my_dir' for testing purposes
    """
    try:
        tmpdir = tempfile.gettempdir()
        test_file_name = os.path.join(tmpdir, "b1276480-7edb-47e4-bbc4-171a1f7db157.sh")

        # not using 'with' construct here because of support of python2.4
        open(test_file_name, "w").write("#!/bin/bash\nls")

        curr_stat = os.stat(test_file_name)
        os.chmod(test_file_name, curr_stat.st_mode | stat.S_IEXEC)

        ret_code = subprocess.call(test_file_name, stdout=open(os.devnull, 'wb'))

        os.remove(test_file_name)

        return ret_code == 0
    except Exception:  # pylint: disable=broad-except
        # in case script could not be written/run etc presume that temp directory has no 'execute' permissions
        return False


def set_alternative_temp_dir():
    try:
        if os.path.isdir(CE_TEMP_DIR):
            shutil.rmtree(CE_TEMP_DIR)

        os.makedirs(CE_TEMP_DIR)

        for var in ['TEMP', 'TMP', 'TMPDIR']:
            os.environ[var] = CE_TEMP_DIR
    except Exception:  # pylint: disable=broad-except
        pass


def get_fs(directory):
    """
    get filesystem data for a directory as in 'df -P' command and parse the result
    :type directory: str
    :rtype DF # namedtuple('DF', 'filesystem blocks used available capacity mounted_on')
    """
    p = subprocess.Popen(["df", "-P", directory], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, _ = p.communicate()
    if p.returncode != 0:
        return None

    res = out.splitlines()[-1]  # take the last line
    return DF(*res.split())


if __name__ == '__main__':
    ret = main()
    sys.exit(ret)
