# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Determine what filesystems are mounted."""

# It's better to look at /etc/mnttab on solaris or /proc/mounts on
# linux than to use output from mount or df.  The former is easier to
# parse and on Solaris is has the filesystem types.

# On solaris, /etc/mnttab is a special file.  According to
# itself, it has a filesystem of type mnttab mounted on it.  This is
# good because it means /etc/mnttab will always be accurate.

# By creative use of chroot, I can cause the Linux mount program to
# mount things while updating the wrong /etc/mtab, thus leaving you
# with two or more files named /etc/mtab that are all wrong.
# Then the output from the "mount" command is also wrong.
# /proc/mounts is always right (assuming you have a /proc mounted), so
# on linux it is better than /etc/fstab.

# df tends not to give useful information when a file system is
# mounted from a machine that is down at the moment.

# This gather has one row per mounted filesystem.  The columns are:

# localdir -- the local directory on which the file system is mounted.

# machinename -- the remote machine the filesystem is mounted from,
# if it's a network filesystem, otherwise the empty string.

# machineip -- the IP that DNS gives us for the machine the filesystem
# is mounted from, if it's a network filesystem, otherwise the empty
# string.

# datasource -- Whatever is given in the file we're reading as the
# source of the data.  For local filesystems, it's the device name.
# For NFS filesystems, it looks like build-nfs-1:/export/DevToolImages
# if we've mounted a directory /export/DevToolImages from a machine
# named build-nfs-1.  For Samba file systems, it looks like
# //earth/elementalpub if we've mounted a directory /elementalpub from
# a machine named earth.  For odd things like /proc or swap files it's
# some random word that hints what kind of odd thing it is.  The
# intent is to show this to the user, or perhaps ignore it; if we need
# to parse it I'd rather add more columns with the proper parsed
# values than parse it on the server.

# fstype -- The type of the filesystem: ext2, ext3, smbfs, nfs, swap,
# proc, etc.

if __name__ == "__main__":
    import os
    import sys
    try:
        import sigma
    except ImportError:
        sys.path.append(os.path.join(os.pardir, os.pardir))
    del sys
    del os

import socket
import platform
import os
import re
import string

def linecheck(regex, name, testpattern):
    assert regex.search(testpattern), name + " is broken"
    return regex

# If I make a directory /tmp/x<tab>y, where there's just one tab
# character between the x and the y, then on Linux /proc/mounts gets
# /tmp/x\011y in it.  So we have to decode escape characters in
# /proc/mounts.  Experimentation shows that a lone backslash in the filename
# becomes \134 in /proc/mounts, so all we have to decode is octal
# numbers appearing after a backslash.
_escape_re = re.compile(r"\\(\d\d\d)")
def escaperepl(matchobj):
    (octnum,) = matchobj.groups()
    return chr(int(octnum, 8))

def decode_escape(name):
    return _escape_re.sub(escaperepl, name)

assert decode_escape("ab\\134\\134c") == "ab\\\\c", "decode_escape is broken"

_nfs_re = re.compile(r"^([^:]+):")
assert _nfs_re.match("asdf:/home/foo")

_samba_re = re.compile(r"^//([^/]+)/")
assert _samba_re.match("//earth/whatever/directory")

# Extract the machinename from a datasource.
def decode_datasource(datasource, type):
    if type == "nfs":
        match = _nfs_re.match(datasource)
        if match:
            return match.group(1)
        else:
            return None
    elif type == "smbfs":
        match = _samba_re.match(datasource)
        if match:
            return match.group(1)
        else:
            return None
    else:
        return None

assert decode_datasource("build-nfs-1:/export/DevToolImages", "nfs") \
       == "build-nfs-1"

assert decode_datasource("//earth/elementalpub", "smbfs") == "earth"

_columns = ["localdir", "machinename", "machineip", "datasource", "fstype"]

def unix_gather_from_file(name, collector):
    table = collector.table("mount", _columns)
    file = open(name, encoding="utf-8", errors="replace")
    try:
        for line in file:
            (datasource, directory, type, junk) = line.split(None, 3)
            datasource = decode_escape(datasource)
            directory = decode_escape(directory)
            machinename = decode_datasource(datasource, type)
            if machinename:
                machineip = socket.gethostbyname(machinename)
            else:
                machineip = None
            table.add([directory, machinename, machineip, datasource, type])
    finally:
        file.close()

def osx_gather(collector):
    # XXX This only gathers local mounted devices
    table = collector.table("mount", _columns)
    pipe = os.popen("/sbin/mount", "r")
    regex = re.compile(r"(.*) +on +([^()]*[^()\s])(?: +\((.*)\))?")
    try:
        for line in pipe:
            m = regex.match(line)
            if not m:
                continue
            datasource, localdir = m.group(1, 2)
            table.add([localdir, "", "", datasource, ""])
    finally:
        pipe.close()

def aix_gather(collector):
    regex = re.compile(r"^(\S*)\s+(/\S+)\s+(/\S*)\s+(\S+)")
    pipe = os.popen("/usr/sbin/mount", "r")
    table = collector.table("mount", _columns)
    try:
        for line in pipe:
            m = regex.match(line)
            if m:
                node, device, directory, fstype = m.group(1, 2, 3, 4)
                ip = None
                if not node:
                    node = None
                else:
                    try:
                        ip = socket.gethostbyname(node)
                    except socket.error as err:
                        pass
                table.add([directory, node, ip, device, fstype])
    finally:
        pipe.close()

def Gather(collector):
    basicos = platform.uname()[0]
    if basicos in ("SunOS", "HP-UX"):
        unix_gather_from_file("/etc/mnttab", collector)
    elif basicos == "Linux":
        unix_gather_from_file("/proc/mounts", collector)
    elif basicos == "Darwin":
        osx_gather(collector)
    elif basicos == "AIX":
        aix_gather(collector)
    elif basicos in ("Windows", "Microsoft Windows", "Microsoft"):
        raise Exception("gather_mount is not implemented for Windows yet.")
    else:
        raise Exception("Can't figure out operating system: %r" % basicos)

def _test():
    from sigma.gather import gathering
    gathering.test(Gather)

if __name__ == "__main__":
    _test()
