/*
  Copyright 2024 Northern.tech AS

  This file is part of CFEngine 3 - written and maintained by Northern.tech AS.

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; version 3.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA

  To the extent this program is licensed as part of the Enterprise
  versions of CFEngine, the applicable Commercial Open Source License
  (COSL) may apply to this file if you as a licensee so wish it. See
  included file COSL.txt.
*/

#include <verify_storage.h>

#include <actuator.h>
#include <dir.h>
#include <conversion.h>
#include <files_interfaces.h>
#include <files_operators.h>
#include <files_lib.h>
#include <files_links.h>
#include <files_properties.h>
#include <attributes.h>
#include <systype.h>
#include <locks.h>
#include <nfs.h>
#include <rlist.h>
#include <policy.h>
#include <verify_files.h>
#include <promiser_regex_resolver.h>
#include <ornaments.h>
#include <eval_context.h>

bool CF_MOUNTALL = false; /* GLOBAL_X */

static PromiseResult FindStoragePromiserObjects(EvalContext *ctx, const Promise *pp);
static PromiseResult VerifyFileSystem(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp);
static PromiseResult VerifyFreeSpace(EvalContext *ctx, char *file, const Attributes *a, const Promise *pp);
static PromiseResult VolumeScanArrivals(char *file, const Attributes *a, const Promise *pp);
#if !defined(__MINGW32__)
static bool FileSystemMountedCorrectly(Seq *list, char *name, const Attributes *a);
static bool IsForeignFileSystem(struct stat *childstat, char *dir);
#endif

#ifndef __MINGW32__
static PromiseResult VerifyMountPromise(EvalContext *ctx, char *file, const Attributes *a, const Promise *pp);
#endif /* !__MINGW32__ */

Seq *GetGlobalMountedFSList(void)
{
    static Seq *mounted_fs_list = NULL; /* GLOBAL_X */
    if (!mounted_fs_list)
    {
        mounted_fs_list = SeqNew(100, free);
    }

    return mounted_fs_list;
}

PromiseResult FindAndVerifyStoragePromises(EvalContext *ctx, const Promise *pp)
{
    PromiseBanner(ctx, pp);
    return FindStoragePromiserObjects(ctx, pp);
}

/*****************************************************************************/

static PromiseResult FindStoragePromiserObjects(EvalContext *ctx, const Promise *pp)
{
/* Check if we are searching over a regular expression */

    return LocateFilePromiserGroup(ctx, pp->promiser, pp, VerifyStoragePromise);
}

/*****************************************************************************/

PromiseResult VerifyStoragePromise(EvalContext *ctx, char *path, const Promise *pp)
{
    CfLock thislock;

    Attributes a = GetStorageAttributes(ctx, pp);

#ifdef __MINGW32__
    if (!a.havemount)
    {
        Log(LOG_LEVEL_VERBOSE, "storage.mount is not supported on Windows");
    }
#endif

/* No parameter conflicts here */

    /* CFE-2350: mount_source / mount_server on an unmount promise are not an
     * error - they select which specific mount to unmount (e.g. one from a
     * particular server) rather than every mount of a type.  Only a *mount*
     * promise needs both. */
    if (!a.mount.unmount && a.havemount)
    {
        if ((a.mount.mount_source == NULL) || (a.mount.mount_server == NULL))
        {
            Log(LOG_LEVEL_ERR, "Insufficient specification in mount promise - need source and server");
            return PROMISE_RESULT_NOOP;
        }
    }

    thislock = AcquireLock(ctx, path, VUQNAME, CFSTARTTIME,
        a.transaction.ifelapsed, a.transaction.expireafter, pp, false);
    if (thislock.lock == NULL)
    {
        return PROMISE_RESULT_SKIPPED;
    }

/* Do mounts first */

    PromiseResult result = PROMISE_RESULT_NOOP;

#ifndef __MINGW32__
    if (SeqLength(GetGlobalMountedFSList()) == 0 &&
        !LoadMountInfo(GetGlobalMountedFSList()))
    {
        Log(LOG_LEVEL_ERR, "Couldn't obtain a list of mounted filesystems - aborting");
        YieldCurrentLock(thislock);
        return PROMISE_RESULT_NOOP;
    }

    if (a.havemount)
    {
        result = PromiseResultUpdate(result, VerifyMountPromise(ctx, path, &a, pp));
    }
#endif /* !__MINGW32__ */

/* Then check file system */

    if (a.havevolume)
    {
        result = PromiseResultUpdate(result, VerifyFileSystem(ctx, path, &a, pp));

        if (a.volume.freespace != CF_NOINT)
        {
            result = PromiseResultUpdate(result, VerifyFreeSpace(ctx, path, &a, pp));
        }

        if (a.volume.scan_arrivals)
        {
            result = PromiseResultUpdate(result, VolumeScanArrivals(path, &a, pp));
        }
    }

    YieldCurrentLock(thislock);
    return result;
}

/*******************************************************************/
/** Level                                                          */
/*******************************************************************/

static PromiseResult VerifyFileSystem(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp)
{
    struct stat statbuf, localstat;
    Dir *dirh;
    const struct dirent *dirp;
    off_t sizeinbytes = 0;
    long filecount = 0;
    char buff[CF_BUFSIZE];

    Log(LOG_LEVEL_VERBOSE, "Checking required filesystem %s", name);

    if (stat(name, &statbuf) == -1)
    {
        return PROMISE_RESULT_NOOP;
    }

    if (S_ISLNK(statbuf.st_mode))
    {
        PromiseResult result = PROMISE_RESULT_NOOP;
        KillGhostLink(ctx, name, a, pp, &result);
        return result;
    }

    PromiseResult result = PROMISE_RESULT_NOOP;

    if (S_ISDIR(statbuf.st_mode))
    {
        if ((dirh = DirOpen(name)) == NULL)
        {
            Log(LOG_LEVEL_ERR, "Can't open directory '%s' which checking required/disk. (opendir: %s)", name, GetErrorStr());
            return PROMISE_RESULT_NOOP;
        }

        for (dirp = DirRead(dirh); dirp != NULL; dirp = DirRead(dirh))
        {
            if (!ConsiderLocalFile(dirp->d_name, name))
            {
                continue;
            }

            filecount++;

            strcpy(buff, name);

            if (buff[strlen(buff)] != FILE_SEPARATOR)
            {
                strcat(buff, FILE_SEPARATOR_STR);
            }

            strcat(buff, dirp->d_name);

            if (lstat(buff, &localstat) == -1)
            {
                if (S_ISLNK(localstat.st_mode))
                {
                    KillGhostLink(ctx, buff, a, pp, &result);
                    continue;
                }

                Log(LOG_LEVEL_ERR, "Can't stat volume '%s'. (lstat: %s)", buff, GetErrorStr());
                continue;
            }

            sizeinbytes += localstat.st_size;
        }

        DirClose(dirh);

        if (sizeinbytes < 0)
        {
            Log(LOG_LEVEL_ERR, "Internal error: count of byte size was less than zero!");
            return result;
        }

        if (sizeinbytes < a->volume.sensible_size)
        {
            cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "File system '%s' is suspiciously small! (%jd bytes)", name,
                 (intmax_t) sizeinbytes);
            return PROMISE_RESULT_INTERRUPTED;
        }

        if (filecount < a->volume.sensible_count)
        {
            cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "Filesystem '%s' has only %ld files/directories.", name,
                 filecount);
            return PROMISE_RESULT_INTERRUPTED;
        }
    }

    cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_NOOP, pp, a, "Filesystem '%s' content seems to be sensible as promised", name);
    return result;
}

/*******************************************************************/

static PromiseResult VerifyFreeSpace(EvalContext *ctx, char *file, const Attributes *a, const Promise *pp)
{
    struct stat statbuf;

#ifdef __MINGW32__
    if (!a->volume.check_foreign)
    {
        Log(LOG_LEVEL_VERBOSE, "storage.volume.check_foreign is not supported on Windows (checking every mount)");
    }
#endif /* __MINGW32__ */

    if (stat(file, &statbuf) == -1)
    {
        Log(LOG_LEVEL_ERR, "Couldn't stat '%s' while checking diskspace. (stat: %s)", file, GetErrorStr());
        return PROMISE_RESULT_NOOP;
    }

#ifndef __MINGW32__
    if (!a->volume.check_foreign)
    {
        if (IsForeignFileSystem(&statbuf, file))
        {
            Log(LOG_LEVEL_INFO, "Filesystem '%s' is mounted from a foreign system, so skipping it", file);
            return PROMISE_RESULT_NOOP;
        }
    }
#endif /* !__MINGW32__ */

    if (a->volume.freespace < 0)
    {
        int threshold_percentage = -a->volume.freespace;
        int free_percentage = GetDiskUsage(file, CF_SIZE_PERCENT);

        if (free_percentage < threshold_percentage)
        {
            cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
                 "Free disk space is under %d%% for volume containing '%s', %d%% free",
                 threshold_percentage, file, free_percentage);
            return PROMISE_RESULT_FAIL;
        }
    }
    else
    {
        off_t threshold = a->volume.freespace;
        off_t free_bytes = GetDiskUsage(file, CF_SIZE_ABS);

        if (free_bytes < threshold)
        {
            cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Disk space under %jd kB for volume containing '%s' (%jd kB free)",
                 (intmax_t) (threshold / 1024), file, (intmax_t) (free_bytes / 1024));
            return PROMISE_RESULT_FAIL;
        }
    }

    return PROMISE_RESULT_NOOP;
}

/*******************************************************************/

static PromiseResult VolumeScanArrivals(ARG_UNUSED char *file, ARG_UNUSED const Attributes *a, ARG_UNUSED const Promise *pp)
{
    Log(LOG_LEVEL_VERBOSE, "Scan arrival sequence . not yet implemented");
    return PROMISE_RESULT_NOOP;
}

/*******************************************************************/

/*********************************************************************/
/*  Unix-specific implementations                                    */
/*********************************************************************/

#if !defined(__MINGW32__)
static bool FileSystemMountedCorrectly(Seq *list, char *name, const Attributes *a)
{
    assert(a != NULL);
    if (a == NULL)
    {
        return false;
    }
    bool found = false;

    for (size_t i = 0; i < SeqLength(list); i++)
    {
        Mount *mp = SeqAt(list, i);

        if (mp == NULL)
        {
            continue;
        }

        /* Give primacy to the promised / affected object */

        if (strcmp(name, mp->mounton) == 0)
        {
            /* We have found something mounted on the promiser dir */

            found = true;

            if ((a->mount.mount_source) && (strcmp(mp->source, a->mount.mount_source) != 0))
            {
                Log(LOG_LEVEL_INFO, "A different file system '%s:%s' is mounted on '%s' than what is promised",
                      mp->host, mp->source, name);
                return false;
            }

            /* CFE-2350: the server is part of mount identity, but acting on a
             * mismatch is disruptive (umount+mount), so only check it when the
             * promise opts in via remount or unmount. */
            if ((a->mount.remount || a->mount.unmount)
                && (a->mount.mount_server != NULL)
                && ((mp->host == NULL) || !StringEqual(mp->host, a->mount.mount_server)))
            {
                Log(LOG_LEVEL_INFO,
                    "Filesystem on '%s' is from server '%s', not the promised server '%s'",
                    name, (mp->host != NULL) ? mp->host : "(unknown)", a->mount.mount_server);
                return false;
            }

            /* CFE-90: option drift is only managed when 'remount' is on; without
             * it a mount with the correct source is "mounted correctly" whatever
             * the options (they still drive the initial mount and fstab). */
            if (a->mount.remount && a->mount.mount_options != NULL)
            {
                char *opts = Rlist2String(a->mount.mount_options, ",");
                if (mp->raw_opts == NULL || mp->raw_opts[0] == '\0'
                    || !OptionsSubsetMatches(opts, mp->raw_opts))
                {
                    Log(LOG_LEVEL_INFO,
                        "Mount options for '%s' do not match promise (actual: '%s', promised: '%s')",
                        name,
                        (mp->raw_opts != NULL) ? mp->raw_opts : "(none)",
                        opts);
                    free(opts);
                    return false;
                }
                free(opts);
            }

            Log(LOG_LEVEL_VERBOSE, "File system '%s' seems to be mounted correctly", mp->source);
            break;
        }
    }

    if (!found)
    {
        if (!a->mount.unmount)
        {
            /* CFE-1863: do not arm MountAll ('mount -a') here - the caller
             * mounts this one filesystem surgically.  CF_MOUNTALL is reserved
             * for the explicit 'mountfilesystems' agent control. */
            Log(LOG_LEVEL_VERBOSE, "File system '%s' seems not to be mounted correctly", name);
        }
    }

    return found;
}

/*********************************************************************/
/* Mounting */
/*********************************************************************/

static bool IsForeignFileSystem(struct stat *childstat, char *dir)
// Is file system NFS / PanFS / CIFS mounted ?
{
    struct stat parentstat;
    char vbuff[CF_BUFSIZE];

    strlcpy(vbuff, dir, CF_BUFSIZE);

    if (vbuff[strlen(vbuff) - 1] == FILE_SEPARATOR)
    {
        strcat(vbuff, "..");
    }
    else
    {
        strcat(vbuff, FILE_SEPARATOR_STR);
        strcat(vbuff, "..");
    }

    if (stat(vbuff, &parentstat) == -1)
    {
        Log(LOG_LEVEL_VERBOSE, "Unable to stat '%s'. (stat: %s)", vbuff, GetErrorStr());
        return false;
    }

    if (childstat->st_dev != parentstat.st_dev)
    {
        Log(LOG_LEVEL_DEBUG, "'%s' is on a different file system, not descending", dir);

        for (size_t i = 0; i < SeqLength(GetGlobalMountedFSList()); i++)
        {
            Mount *entry = SeqAt(GetGlobalMountedFSList(), i);

            if (!strcmp(entry->mounton, dir))
            {
                if ((entry->options != NULL)
                    && ((strstr(entry->options, "nfs") != NULL)
                        || (strstr(entry->options, "panfs") != NULL)
                        || (strstr(entry->options, "cifs") != NULL)))
                {
                    return true;
                }
            }
        }
    }

    return false;
}

static PromiseResult VerifyMountPromise(EvalContext *ctx, char *name, const Attributes *a, const Promise *pp)
{
    assert(a != NULL);
    if (a == NULL)
    {
        return PROMISE_RESULT_NOOP;
    }
    char dir[CF_BUFSIZE];
    int changes = 0;

    Log(LOG_LEVEL_VERBOSE, "Verifying mounted file systems on '%s'", name);

    snprintf(dir, CF_BUFSIZE, "%s/.", name);

    if (!IsPrivileged())
    {
        cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_INTERRUPTED, pp, a, "Only root can mount filesystems");
        return PROMISE_RESULT_INTERRUPTED;
    }

    PromiseResult result = PROMISE_RESULT_NOOP;
    Seq *mounted_fs = GetGlobalMountedFSList();
    if (!FileSystemMountedCorrectly(mounted_fs, name, a))
    {
        /* Whether something is mounted at the promiser at all (vs. nothing
         * mounted there).  Computed for both mount and unmount: the unmount
         * path uses it to tell "a different filesystem is mounted here" from
         * "nothing is mounted here". */
        bool already_mounted = false;
        const size_t n_mounted = SeqLength(mounted_fs);
        for (size_t i = 0; i < n_mounted; i++)
        {
            Mount *mp = SeqAt(mounted_fs, i);
            if (mp != NULL && mp->mounton != NULL && StringEqual(name, mp->mounton))
            {
                already_mounted = true;
                break;
            }
        }

        if (!a->mount.unmount)
        {
            /* Ensure the mount point exists before mounting or remounting.
             * dir is "<name>/.", so this creates the mount point directory. */
            if (!MakeParentDirectory(dir, a->move_obstructions, NULL))
            {
                // Could not create parent directory, assume this is okay,
                // verbose logging in MakeParentDirectory()
                Log(LOG_LEVEL_DEBUG,
                    "Could not create parent directory '%s' for mount promise",
                    dir);
            }

            if (already_mounted)
            {
                /* CFE-90: mounted but not as promised.  Correct the live mount
                 * first (only when remount is enabled), then update fstab. */
                if (a->mount.remount)
                {
                    result = PromiseResultUpdate(result, ReconcileMountOptions(ctx, name, a, pp));
                    changes++;
                }
                else
                {
                    /* Reachable only for a wrong-source mount: option drift with
                     * remount disabled is reported as mounted correctly. */
                    cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a,
                         "A different filesystem is mounted on '%s' than promised; enable 'remount' to correct", name);
                    result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
                }

                /* Persist to fstab regardless of the live outcome (reported
                 * above); it converges at the next remount/reboot. */
                if (a->mount.editfstab)
                {
                    changes += VerifyInFstab(ctx, name, a, pp, &result);
                }
            }
            else
            {
                /* CFE-1863: not mounted - mount just THIS filesystem instead of
                 * arming MountAll ('mount -a'), which would also mount every
                 * other unmounted fstab entry. Then persist to fstab. */
                result = PromiseResultUpdate(result, VerifyMount(ctx, name, a, pp));
                if (a->mount.editfstab)
                {
                    changes += VerifyInFstab(ctx, name, a, pp, &result);
                }
            }
        }
        else
        {
            /* CFE-2350: a *different* filesystem here is not ours to unmount -
             * leave it (and its fstab entry) alone; otherwise nothing of ours
             * is mounted, so just ensure it is not in fstab. */
            if (already_mounted)
            {
                cfPS(ctx, LOG_LEVEL_VERBOSE, PROMISE_RESULT_NOOP, pp, a,
                     "A different filesystem is mounted on '%s' than the unmount promise targets; leaving it untouched",
                     name);
            }
            else if (a->mount.editfstab)
            {
                changes += VerifyNotInFstab(ctx, name, a, pp, &result);
            }
        }
    }
    else
    {
        if (a->mount.unmount)
        {
            VerifyUnmount(ctx, name, a, pp);
            if (a->mount.editfstab)
            {
                VerifyNotInFstab(ctx, name, a, pp, &result);
            }
        }
        else
        {
            /* CFE-1539: mounted correctly, but still maintain fstab (add a
             * missing entry, correct drifted options) so the promise persists
             * across reboots. Independent of 'remount' - keeping fstab correct
             * is documented mount_options behavior; remounting live is not. */
            if (a->mount.editfstab)
            {
                changes += VerifyInFstab(ctx, name, a, pp, &result);
            }
            if (changes == 0)
            {
                cfPS(ctx, LOG_LEVEL_INFO, PROMISE_RESULT_NOOP, pp, a, "Filesystem '%s' seems to be mounted as promised", name);
            }
        }
    }

    return result;
}

#endif /* !__MINGW32__ */

void DeleteStorageContext(void)
{
#ifndef __MINGW32__
    CleanupNFS();

    if (!DONTDO && CF_MOUNTALL)
    {
        Log(LOG_LEVEL_VERBOSE, "Mounting all filesystems");
        MountAll();
    }
#endif /* !__MINGW32__ */
}
