# Microsoft Azure Linux Agent
#
# Copyright 2018 Microsoft Corporation
#
# 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.
#
# Requires Python 2.6+ and Openssl 1.0+
#

"""
Module conf loads and parses configuration file
"""  # pylint: disable=W0105
import os
import os.path

from azurelinuxagent.common.utils.fileutil import read_file #pylint: disable=R0401
from azurelinuxagent.common.exception import AgentConfigError

DISABLE_AGENT_FILE = 'disable_agent'


class ConfigurationProvider(object):
    """
    Parse and store key:values in /etc/waagent.conf.
    """

    def __init__(self):
        self.values = {}

    def load(self, content):
        if not content:
            raise AgentConfigError("Can't not parse empty configuration")
        for line in content.split('\n'):
            if not line.startswith("#") and "=" in line:
                parts = line.split('=', 1)
                if len(parts) < 2:
                    continue
                key = parts[0].strip()
                value = parts[1].split('#')[0].strip("\" ").strip()
                self.values[key] = value if value != "None" else None

    @staticmethod
    def _get_default(default):
        if hasattr(default, '__call__'):
            return default()
        return default

    def get(self, key, default_value):
        """
        Retrieves a string parameter by key and returns its value. If not found returns the default value,
        or if the default value is a callable returns the result of invoking the callable.
        """
        val = self.values.get(key)
        return val if val is not None else self._get_default(default_value)

    def get_switch(self, key, default_value):
        """
        Retrieves a switch parameter by key and returns its value as a boolean. If not found returns the default value,
        or if the default value is a callable returns the result of invoking the callable.
        """
        val = self.values.get(key)
        if val is not None and val.lower() == 'y':
            return True
        elif val is not None and val.lower() == 'n':
            return False
        return self._get_default(default_value)

    def get_int(self, key, default_value):
        """
        Retrieves an int parameter by key and returns its value. If not found returns the default value,
        or if the default value is a callable returns the result of invoking the callable.
        """
        try:
            return int(self.values.get(key))
        except TypeError:
            return self._get_default(default_value)
        except ValueError:
            return self._get_default(default_value)

    def is_present(self, key):
        """
        Returns True if the given flag present in the configuration file, False otherwise.
        """
        return self.values.get(key) is not None


__conf__ = ConfigurationProvider()


def load_conf_from_file(conf_file_path, conf=__conf__):
    """
    Load conf file from: conf_file_path
    """
    if os.path.isfile(conf_file_path) == False:
        raise AgentConfigError(("Missing configuration in {0}"
                                "").format(conf_file_path))
    try:
        content = read_file(conf_file_path)
        conf.load(content)
    except IOError as err:
        raise AgentConfigError(("Failed to load conf file:{0}, {1}"
                                "").format(conf_file_path, err))


__SWITCH_OPTIONS__ = {
    "OS.AllowHTTP": False,
    "OS.EnableFirewall": False,
    "OS.EnableFIPS": False,
    "OS.EnableRDMA": False,
    "OS.UpdateRdmaDriver": False,
    "OS.CheckRdmaDriver": False,
    "Logs.Verbose": False,
    "Logs.Console": True,
    "Logs.Collect": True,
    "Extensions.Enabled": True,
    "Extensions.WaitForCloudInit": False,
    "Provisioning.AllowResetSysUser": False,
    "Provisioning.RegenerateSshHostKeyPair": False,
    "Provisioning.DeleteRootPassword": False,
    "Provisioning.DecodeCustomData": False,
    "Provisioning.ExecuteCustomData": False,
    "Provisioning.MonitorHostName": False,
    "DetectScvmmEnv": False,
    "ResourceDisk.Format": False,
    "ResourceDisk.EnableSwap": False,
    "ResourceDisk.EnableSwapEncryption": False,
    "AutoUpdate.Enabled": True,
    "AutoUpdate.UpdateToLatestVersion": True,
    "EnableOverProvisioning": True,
    #
    # "Debug" options are experimental and may be removed in later
    # versions of the Agent.
    #
    "Debug.CgroupLogMetrics": False,
    "Debug.CgroupDisableOnProcessCheckFailure": True,
    "Debug.CgroupDisableOnQuotaCheckFailure": True,
    "Debug.EnableAgentMemoryUsageCheck": False,
    "Debug.EnableFastTrack": True,
    "Debug.EnableGAVersioning": True,
    "Debug.EnableCgroupV2ResourceLimiting": False,
    "Debug.EnableExtensionPolicy": False,
    "Debug.EnableSignatureValidation": False
}


__STRING_OPTIONS__ = {
    "Lib.Dir": "/var/lib/waagent",
    "DVD.MountPoint": "/mnt/cdrom/secure",
    "Pid.File": "/var/run/waagent.pid",
    "Extension.LogDir": "/var/log/azure",
    "OS.OpensslPath": "/usr/bin/openssl",
    "OS.SshDir": "/etc/ssh",
    "OS.HomeDir": "/home",
    "OS.PasswordPath": "/etc/shadow",
    "OS.SudoersDir": "/etc/sudoers.d",
    "OS.RootDeviceScsiTimeout": None,
    "Provisioning.Agent": "auto",
    "Provisioning.SshHostKeyPairType": "rsa",
    "Provisioning.PasswordCryptId": "6",
    "HttpProxy.Host": None,
    "ResourceDisk.MountPoint": "/mnt/resource",
    "ResourceDisk.MountOptions": None,
    "ResourceDisk.Filesystem": "ext3",
    "AutoUpdate.GAFamily": "Prod",
    "Policy.PolicyFilePath": "/etc/waagent_policy.json",
    "Protocol.EndpointDiscovery": "dhcp"
}


__INTEGER_OPTIONS__ = {
    "Extensions.GoalStatePeriod": 6,
    "Extensions.InitialGoalStatePeriod": 6,
    "Extensions.WaitForCloudInitTimeout": 3600,
    "OS.EnableFirewallPeriod": 300,
    "OS.RemovePersistentNetRulesPeriod": 30,
    "OS.RootDeviceScsiTimeoutPeriod": 30,
    "OS.MonitorDhcpClientRestartPeriod": 30,
    "OS.SshClientAliveInterval": 180,
    "Provisioning.MonitorHostNamePeriod": 30,
    "Provisioning.PasswordCryptSaltLength": 10,
    "HttpProxy.Port": None,
    "ResourceDisk.SwapSizeMB": 0,
    "Autoupdate.Frequency": 3600,
    "Logs.CollectPeriod": 3600,
    #
    # "Debug" options are experimental and may be removed in later
    # versions of the Agent.
    #
    "Debug.CgroupCheckPeriod": 300,
    "Debug.AgentCpuQuota": 50,
    "Debug.AgentCpuThrottledTimeThreshold": 120,
    "Debug.AgentMemoryQuota": 30 * 1024 ** 2,
    "Debug.EtpCollectionPeriod": 300,
    "Debug.AutoUpdateHotfixFrequency": 14400,
    "Debug.AutoUpdateNormalFrequency": 86400,
    "Debug.FirewallRulesLogPeriod": 86400,
    "Debug.LogCollectorInitialDelay": 5 * 60
}


def get_configuration(conf=__conf__):
    options = {}
    for option in __SWITCH_OPTIONS__:
        options[option] = conf.get_switch(option, __SWITCH_OPTIONS__[option])

    for option in __STRING_OPTIONS__:
        options[option] = conf.get(option, __STRING_OPTIONS__[option])

    for option in __INTEGER_OPTIONS__:
        options[option] = conf.get_int(option, __INTEGER_OPTIONS__[option])

    return options


def get_default_value(option):
    if option in __STRING_OPTIONS__:
        return __STRING_OPTIONS__[option]
    raise ValueError("{0} is not a valid configuration parameter.".format(option))


def get_int_default_value(option):
    if option in __INTEGER_OPTIONS__:
        return int(__INTEGER_OPTIONS__[option])
    raise ValueError("{0} is not a valid configuration parameter.".format(option))


def get_switch_default_value(option):
    if option in __SWITCH_OPTIONS__:
        return __SWITCH_OPTIONS__[option]
    raise ValueError("{0} is not a valid configuration parameter.".format(option))


def is_present(key, conf=__conf__):
    """
    Returns True if the given flag present in the configuration file, False otherwise.
    """
    return conf.is_present(key)


def enable_firewall(conf=__conf__):
    return conf.get_switch("OS.EnableFirewall", False)


def get_enable_firewall_period(conf=__conf__):
    return conf.get_int("OS.EnableFirewallPeriod", 300)


def get_remove_persistent_net_rules_period(conf=__conf__):
    return conf.get_int("OS.RemovePersistentNetRulesPeriod", 30)


def get_monitor_dhcp_client_restart_period(conf=__conf__):
    return conf.get_int("OS.MonitorDhcpClientRestartPeriod", 30)


def enable_rdma(conf=__conf__):
    return conf.get_switch("OS.EnableRDMA", False) or \
           conf.get_switch("OS.UpdateRdmaDriver", False) or \
           conf.get_switch("OS.CheckRdmaDriver", False)


def enable_rdma_update(conf=__conf__):
    return conf.get_switch("OS.UpdateRdmaDriver", False)


def enable_check_rdma_driver(conf=__conf__):
    return conf.get_switch("OS.CheckRdmaDriver", True)


def get_logs_verbose(conf=__conf__):
    return conf.get_switch("Logs.Verbose", False)


def get_logs_console(conf=__conf__):
    return conf.get_switch("Logs.Console", True)


def get_collect_logs(conf=__conf__):
    return conf.get_switch("Logs.Collect", True)


def get_collect_logs_period(conf=__conf__):
    return conf.get_int("Logs.CollectPeriod", 3600)


def get_lib_dir(conf=__conf__):
    return conf.get("Lib.Dir", "/var/lib/waagent")


def get_published_hostname(conf=__conf__):
    # Some applications rely on this file; do not remove this setting
    return os.path.join(get_lib_dir(conf), 'published_hostname')


def get_dvd_mount_point(conf=__conf__):
    return conf.get("DVD.MountPoint", "/mnt/cdrom/secure")


def get_agent_pid_file_path(conf=__conf__):
    return conf.get("Pid.File", "/var/run/waagent.pid")


def get_ext_log_dir(conf=__conf__):
    return conf.get("Extension.LogDir", "/var/log/azure")


def get_agent_log_file():
    return "/var/log/waagent.log"


def get_policy_file_path(conf=__conf__):
    return conf.get("Policy.PolicyFilePath", "/etc/waagent_policy.json")


def get_fips_enabled(conf=__conf__):
    return conf.get_switch("OS.EnableFIPS", False)


def get_openssl_cmd(conf=__conf__):
    return conf.get("OS.OpensslPath", "/usr/bin/openssl")


def get_ssh_client_alive_interval(conf=__conf__):
    return conf.get("OS.SshClientAliveInterval", 180)


def get_ssh_dir(conf=__conf__):
    return conf.get("OS.SshDir", "/etc/ssh")


def get_home_dir(conf=__conf__):
    return conf.get("OS.HomeDir", "/home")


def get_passwd_file_path(conf=__conf__):
    return conf.get("OS.PasswordPath", "/etc/shadow")


def get_sudoers_dir(conf=__conf__):
    return conf.get("OS.SudoersDir", "/etc/sudoers.d")


def get_sshd_conf_file_path(conf=__conf__):
    return os.path.join(get_ssh_dir(conf), "sshd_config")


def get_ssh_key_glob(conf=__conf__):
    return os.path.join(get_ssh_dir(conf), 'ssh_host_*key*')


def get_ssh_key_private_path(conf=__conf__):
    return os.path.join(get_ssh_dir(conf),
        'ssh_host_{0}_key'.format(get_ssh_host_keypair_type(conf))) 


def get_ssh_key_public_path(conf=__conf__):
    return os.path.join(get_ssh_dir(conf),
        'ssh_host_{0}_key.pub'.format(get_ssh_host_keypair_type(conf))) 


def get_root_device_scsi_timeout(conf=__conf__):
    return conf.get("OS.RootDeviceScsiTimeout", None)


def get_root_device_scsi_timeout_period(conf=__conf__):
    return conf.get_int("OS.RootDeviceScsiTimeoutPeriod", 30)


def get_ssh_host_keypair_type(conf=__conf__):
    keypair_type = conf.get("Provisioning.SshHostKeyPairType", "rsa")
    if keypair_type == "auto":
        '''
        auto generates all supported key types and returns the
        rsa thumbprint as the default.
        '''
        return "rsa"
    return keypair_type


def get_ssh_host_keypair_mode(conf=__conf__):
    return conf.get("Provisioning.SshHostKeyPairType", "rsa")


def get_extensions_enabled(conf=__conf__):
    return conf.get_switch("Extensions.Enabled", True)


def get_wait_for_cloud_init(conf=__conf__):
    return conf.get_switch("Extensions.WaitForCloudInit", False)


def get_wait_for_cloud_init_timeout(conf=__conf__):
    return conf.get_switch("Extensions.WaitForCloudInitTimeout", 3600)


def get_goal_state_period(conf=__conf__):
    return conf.get_int("Extensions.GoalStatePeriod", 6)


def get_initial_goal_state_period(conf=__conf__):
    return conf.get_int("Extensions.InitialGoalStatePeriod", default_value=lambda: get_goal_state_period(conf=conf))


def get_allow_reset_sys_user(conf=__conf__):
    return conf.get_switch("Provisioning.AllowResetSysUser", False)


def get_regenerate_ssh_host_key(conf=__conf__):
    return conf.get_switch("Provisioning.RegenerateSshHostKeyPair", False)


def get_delete_root_password(conf=__conf__):
    return conf.get_switch("Provisioning.DeleteRootPassword", False)


def get_decode_customdata(conf=__conf__):
    return conf.get_switch("Provisioning.DecodeCustomData", False)


def get_execute_customdata(conf=__conf__):
    return conf.get_switch("Provisioning.ExecuteCustomData", False)


def get_password_cryptid(conf=__conf__):
    return conf.get("Provisioning.PasswordCryptId", "6")


def get_provisioning_agent(conf=__conf__):
    return conf.get("Provisioning.Agent", "auto")


def get_provision_enabled(conf=__conf__):
    """
    Provisioning (as far as waagent is concerned) is enabled if either the
    agent is set to 'auto' or 'waagent'. This wraps logic that was introduced
    for flexible provisioning agent configuration and detection. The replaces
    the older bool setting to turn provisioning on or off.
    """

    return get_provisioning_agent(conf) in ("auto", "waagent")


def get_password_crypt_salt_len(conf=__conf__):
    return conf.get_int("Provisioning.PasswordCryptSaltLength", 10)


def get_monitor_hostname(conf=__conf__):
    return conf.get_switch("Provisioning.MonitorHostName", False)


def get_monitor_hostname_period(conf=__conf__):
    return conf.get_int("Provisioning.MonitorHostNamePeriod", 30)


def get_httpproxy_host(conf=__conf__):
    return conf.get("HttpProxy.Host", None)


def get_httpproxy_port(conf=__conf__):
    return conf.get_int("HttpProxy.Port", None)


def get_detect_scvmm_env(conf=__conf__):
    return conf.get_switch("DetectScvmmEnv", False)


def get_resourcedisk_format(conf=__conf__):
    return conf.get_switch("ResourceDisk.Format", False)


def get_resourcedisk_enable_swap(conf=__conf__):
    return conf.get_switch("ResourceDisk.EnableSwap", False)


def get_resourcedisk_enable_swap_encryption(conf=__conf__):
    return conf.get_switch("ResourceDisk.EnableSwapEncryption", False)


def get_resourcedisk_mountpoint(conf=__conf__):
    return conf.get("ResourceDisk.MountPoint", "/mnt/resource")


def get_resourcedisk_mountoptions(conf=__conf__):
    return conf.get("ResourceDisk.MountOptions", None)


def get_resourcedisk_filesystem(conf=__conf__):
    return conf.get("ResourceDisk.Filesystem", "ext3")


def get_resourcedisk_swap_size_mb(conf=__conf__):
    return conf.get_int("ResourceDisk.SwapSizeMB", 0)


def get_autoupdate_gafamily(conf=__conf__):
    return conf.get("AutoUpdate.GAFamily", "Prod")


def get_autoupdate_enabled(conf=__conf__):
    return conf.get_switch("AutoUpdate.Enabled", True)


def get_autoupdate_frequency(conf=__conf__):
    return conf.get_int("Autoupdate.Frequency", 3600)


def get_enable_overprovisioning(conf=__conf__):
    return conf.get_switch("EnableOverProvisioning", True)


def get_allow_http(conf=__conf__):
    return conf.get_switch("OS.AllowHTTP", False)


def get_disable_agent_file_path(conf=__conf__):
    return os.path.join(get_lib_dir(conf), DISABLE_AGENT_FILE)


def get_cgroups_enabled(conf=__conf__):
    return conf.get_switch("CGroups.Enabled", True)


def get_monitor_network_configuration_changes(conf=__conf__):
    return conf.get_switch("Monitor.NetworkConfigurationChanges", False)


def get_auto_update_to_latest_version(conf=__conf__):
    """
    If set to True, agent will update to the latest version
    NOTE:
        when both turned on, both AutoUpdate.Enabled and AutoUpdate.UpdateToLatestVersion same meaning: update to latest version
        when turned off, AutoUpdate.Enabled: reverts to pre-installed agent, AutoUpdate.UpdateToLatestVersion: uses latest version already installed on the vm and does not download new agents
        Even we are deprecating AutoUpdate.Enabled, we still need to support if users explicitly setting it instead new flag.
        If AutoUpdate.UpdateToLatestVersion is present, it overrides any value set for AutoUpdate.Enabled (if present).
        If AutoUpdate.UpdateToLatestVersion is not present but AutoUpdate.Enabled is present and set to 'n', we adhere to AutoUpdate.Enabled flag's behavior
        if both not present, we default to True.
    """
    default = get_autoupdate_enabled(conf=conf)
    return conf.get_switch("AutoUpdate.UpdateToLatestVersion", default)


def get_protocol_endpoint_discovery(conf=__conf__):
    return conf.get("Protocol.EndpointDiscovery", "dhcp")


def get_dhcp_discovery_enabled(conf=__conf__):
    """
    Determines how the agent will discover the wireserver endpoint.
    If set to 'dhcp', the agent will use DHCP to get the wireserver endpoint.
    Otherwise, the agent will use the known wireserver endpoint (168.63.129.16).
    """
    return get_protocol_endpoint_discovery(conf) == "dhcp"


def get_cgroup_check_period(conf=__conf__):
    """
    How often to perform checks on cgroups (are the processes in the cgroups as expected,
    has the agent exceeded its quota, etc)

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.CgroupCheckPeriod", 300)


def get_cgroup_log_metrics(conf=__conf__):
    """
    If True, resource usage metrics are written to the local log

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.CgroupLogMetrics", False)


def get_cgroup_disable_on_process_check_failure(conf=__conf__):
    """
    If True, cgroups will be disabled if the process check fails

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.CgroupDisableOnProcessCheckFailure", True)


def get_cgroup_disable_on_quota_check_failure(conf=__conf__):
    """
    If True, cgroups will be disabled if the CPU quota check fails

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.CgroupDisableOnQuotaCheckFailure", True)


def get_agent_cpu_quota(conf=__conf__):
    """
    CPU quota for the agent as a percentage of 1 CPU (100% == 1 CPU)

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.AgentCpuQuota", 50)


def get_agent_cpu_throttled_time_threshold(conf=__conf__):
    """
    Throttled time threshold for agent cpu in seconds.

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.AgentCpuThrottledTimeThreshold", 120)


def get_agent_memory_quota(conf=__conf__):
    """
    Memory quota for the agent in bytes.

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.AgentMemoryQuota", 30 * 1024 ** 2)


def get_enable_agent_memory_usage_check(conf=__conf__):
    """
    If True, Agent checks it's Memory usage.

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.EnableAgentMemoryUsageCheck", False)


def get_enable_fast_track(conf=__conf__):
    """
    If True, the agent use FastTrack when retrieving goal states

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.EnableFastTrack", True)


def get_etp_collection_period(conf=__conf__):
    """
    Determines the frequency to perform ETP collection on extensions telemetry events.
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.EtpCollectionPeriod", 300)


def get_self_update_hotfix_frequency(conf=__conf__):
    """
    Determines the frequency to check for Hotfix upgrades (<Build> version changed in new upgrades).
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.SelfUpdateHotfixFrequency", 4 * 60 * 60)


def get_self_update_regular_frequency(conf=__conf__):
    """
    Determines the frequency to check for regular upgrades (<Major>.<Minor>.<patch> version changed in new upgrades).
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.SelfUpdateRegularFrequency", 24 * 60 * 60)


def get_enable_ga_versioning(conf=__conf__):
    """
    If True, the agent looks for rsm updates(checking requested version in GS) otherwise it will fall back to self-update and finds the highest version from PIR.
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.EnableGAVersioning", True)


def get_firewall_rules_log_period(conf=__conf__):
    """
    Determine the frequency to perform the periodic operation of logging firewall rules.

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.FirewallRulesLogPeriod", 86400)

  
def get_extension_policy_enabled(conf=__conf__):
    """
    Determine whether extension policy is enabled. If true, policy will be enforced before installing any extensions.
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.EnableExtensionPolicy", False)

  
def get_enable_cgroup_v2_resource_limiting(conf=__conf__):
    """
    If True, the agent will enable resource monitoring and enforcement for the log collector on machines using cgroup v2.
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.EnableCgroupV2ResourceLimiting", True)


def get_log_collector_initial_delay(conf=__conf__):
    """
    Determine the initial delay at service start before the first periodic log collection.

    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_int("Debug.LogCollectorInitialDelay", 5 * 60)

  
def get_signature_validation_enabled(conf=__conf__):
    """
    Determine whether signature validation is enabled. If true, package signature will be validated before
    installing any signed extensions.
    NOTE: This option is experimental and may be removed in later versions of the Agent.
    """
    return conf.get_switch("Debug.EnableSignatureValidation", False)

  
def get_enable_rsm_downgrade(conf=__conf__):
    """
    If False, the agent will not downgrade to a lower version when a lower version is requested in the goal state.

    Todo: Flag will be removed once we have a fix for rsm downgrade scenario.
    """
    return conf.get_switch("Debug.EnableRsmDowngrade", False)
