summaryrefslogtreecommitdiff
path: root/diskuse_ldap_win32.py
blob: 390a41f4252201efa3a27a2664c9e1a3e3748638 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env python3
# Periodic job to update LDAP storageAvailable[/] from Windows PCs.
# > pip install keyring ldap3
import ctypes
import enum
import keyring
import ldap3
from ldap3.utils.conv import escape_filter_chars as escape_filter
from pprint import pprint
from socket import gethostname
import ssl
import sys

BYTES_PER_kB = 1024
BYTES_PER_MB = 1024 * 1024
MAX_PATH = 255

def GetLogicalDriveStrings():
    import ctypes
    import ctypes.wintypes
    _GetLogicalDriveStrings = ctypes.windll.kernel32.GetLogicalDriveStringsW
    buffer_len = 16 * 1024
    #out_buffer = ctypes.create_unicode_buffer(buffer_len + 1)
    out_buffer = ctypes.create_string_buffer(buffer_len*2 + 2)
    ret = _GetLogicalDriveStrings(ctypes.wintypes.DWORD(buffer_len),
                                  out_buffer)
    if ret:
        return out_buffer.raw.decode("utf-16").strip("\0").split("\0")
    else:
        raise ctypes.WinError()

class DriveType(enum.IntEnum):
    Unknown = 0
    NoRootDir = 1
    Removable = 2
    Fixed = 3
    Remote = 4
    CdRom = 5
    RamDisk = 6

def GetDriveType(path):
    import ctypes
    import ctypes.wintypes
    _GetDriveType = ctypes.windll.kernel32.GetDriveTypeW
    ret = _GetDriveType(ctypes.wintypes.LPCWSTR(path))
    return DriveType(ret)

def GetDiskFreeSpaceEx(path):
    import ctypes
    import ctypes.wintypes
    _GetDiskFreeSpaceEx = ctypes.windll.kernel32.GetDiskFreeSpaceExW
    out_avail_bytes = ctypes.wintypes.ULARGE_INTEGER()
    out_total_bytes = ctypes.wintypes.ULARGE_INTEGER()
    out_free_bytes = ctypes.wintypes.ULARGE_INTEGER()
    ret = _GetDiskFreeSpaceEx(ctypes.wintypes.LPCWSTR(path),
                              ctypes.byref(out_avail_bytes),
                              ctypes.byref(out_total_bytes),
                              ctypes.byref(out_free_bytes))
    if ret:
        return (out_avail_bytes.value,
                out_total_bytes.value,
                out_free_bytes.value)
    else:
        raise ctypes.WinError()

def GetVolumeInformation(path):
    import ctypes
    import ctypes.wintypes
    _GetVolumeInformation = ctypes.windll.kernel32.GetVolumeInformationW
    out_volume_name = ctypes.create_unicode_buffer(MAX_PATH + 1)
    out_serial_number = ctypes.wintypes.DWORD()
    out_max_component_len = ctypes.wintypes.DWORD()
    out_flags = ctypes.wintypes.DWORD()
    out_fs_name = ctypes.create_unicode_buffer(MAX_PATH + 1)
    ret = _GetVolumeInformation(ctypes.wintypes.LPCWSTR(path),
                                out_volume_name,
                                ctypes.sizeof(out_volume_name),
                                ctypes.byref(out_serial_number),
                                ctypes.byref(out_max_component_len),
                                ctypes.byref(out_flags),
                                out_fs_name,
                                ctypes.sizeof(out_fs_name))
    if ret:
        return (out_volume_name.value,
                out_serial_number.value,
                out_max_component_len.value,
                out_flags.value,
                out_fs_name.value)
    else:
        raise ctypes.WinError()

def find_bind_credentials():
    server = "ldap/ldap.nullroute.lt"
    username = gethostname().lower()
    cred = keyring.get_credential(server, username) or \
           keyring.get_credential(server, None)
    if not cred:
        print("Error: bind password not found in keyring. You need to run:", file=sys.stderr)
        print(" > python -m keyring set \"%s\" \"%s\"" % (server, username), file=sys.stderr)
        exit(1)
    return cred.username, cred.password

def ldap_connect():
    username, password = find_bind_credentials()
    serv = ldap3.Server("ldaps://ldap.nullroute.lt",
                        tls=ldap3.Tls(validate=ssl.CERT_REQUIRED),
                        get_info=ldap3.DSA)
    bind_dn = username
    if "=" not in bind_dn:
        conn = ldap3.Connection(serv,
                                raise_exceptions=True)
        conn.bind()
        bind_dn = ldap_find_device_dn(conn,
                                      "(cn=%s)" % escape_filter(username))
        if not bind_dn:
            raise Exception("Could not find the bind DN for %r" % username)
    conn = ldap3.Connection(serv,
                            user=bind_dn,
                            password=password,
                            raise_exceptions=True)
    conn.bind()
    return conn

def ldap_find_device_dn(conn, filter):
    conn.search("o=Nullroute",
                "(&(objectClass=device)(|%s))" % filter,
                attributes=["1.1"])
    if len(conn.response) == 1:
        return conn.response[0]["dn"]
    elif len(conn.response) > 1:
        print("More than one device found for %s" % filter, file=sys.stderr)
    else:
        # Not necessarily an error case, just an untracked volume
        print("No devices found for %s" % filter)

def ldap_update_device(conn, device_dn, total_mb, free_mb):
    conn.modify(device_dn,
                {"storageCapacity": [(ldap3.MODIFY_REPLACE, [str(total_mb)])],
                 "storageAvailable": [(ldap3.MODIFY_REPLACE, [str(free_mb)])]})

conn = ldap_connect()
print("Bound as", conn.extend.standard.who_am_i())

if sys.platform != "win32":
    exit(f"error: Platform {sys.platform!r} not supported by this script")

unc_hostname = gethostname().lower()

for vol_path in GetLogicalDriveStrings():
    if GetDriveType(vol_path) != DriveType.Fixed:
        continue

    try:
        (avail_bytes, total_bytes, free_bytes) = GetDiskFreeSpaceEx(vol_path)
        total_mb = total_bytes // BYTES_PER_MB
        free_mb = free_bytes // BYTES_PER_MB
        # Reduce precision to reduce LDAP updates
        total_mb -= total_mb % 100
        free_mb -= free_mb % 100
        print("Volume %s has ~%s MiB free out of ~%s MiB" % (vol_path, free_mb, total_mb))
    except PermissionError as e:
        # PermissionError: [WinError 21] The device is not ready.
        continue

    (label, serial, *rest) = GetVolumeInformation(vol_path)
    serial = "%04X-%04X" % ((serial >> 16) & 0xFFFF, serial & 0xFFFF)
    unc = r"\\%s\%s" % (unc_hostname, vol_path.rstrip("\\"))

    filter = "(storageUniqueIdentifier=%s)" % escape_filter(serial)
    filter += "(cn=%s)" % escape_filter(unc)

    device_dn = ldap_find_device_dn(conn, filter)
    if device_dn:
        print("Updating", device_dn)
        ldap_update_device(conn, device_dn, total_mb, free_mb)