mount.py 18 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4 nu

import os
import re
import sys
import time
import string
import random
import tempfile
11
import traceback
12

rgaudin's avatar
rgaudin committed
13
from data import data_dir, data_partition_label
14
15
from backend.content import get_content
from backend.qemu import get_qemu_image_size
16
17
18
19
20
from backend.util import (
    subprocess_pretty_check_call,
    subprocess_pretty_call,
    subprocess_external,
)
21
22
23


def system_has_exfat():
24
    """ whether system supports native exfat (not fuse based) """
25
    try:
26
27
        with open("/proc/filesystems", "r") as f:
            return "exfat" in [line.rstrip().split("\t")[1] for line in f.readlines()]
28
29
30
31
32
33
    except Exception:
        pass
    return False


if sys.platform == "win32":
34
    imdiskinst = os.path.join(data_dir, "imdiskinst")
35
36
37
38
    # Windows holds 3 folders:
    # - Sytem32: files of the system architecture: 32b on x86, 64b on x64
    # - SysWOW64: 32b files on x64
    # - SysNative: virt folder to access 64b files from 32b bins on x64
39
40
    system32 = os.path.join(os.environ["SystemRoot"], "System32")
    imdisk_exe = os.path.join(system32, "imdisk.exe")
41
elif sys.platform == "linux":
42
43
44
    udisksctl_exe = "/usr/bin/udisksctl"
    udisks_nou = "--no-user-interaction"
    mkfs_exe = "/sbin/mkfs.exfat"
45
46
47
    umount_exe = "/bin/umount"
    mount_exe = "/bin/mount"
    losetup_exe = "/sbin/losetup"
48
elif sys.platform == "darwin":
49
50
51
52
    hdiutil_exe = "/usr/bin/hdiutil"
    diskutil_exe = "/usr/sbin/diskutil"
    mount_exe = "/sbin/mount"
    umount_exe = "/sbin/umount"
53
54
55


def get_start_offset(root_size, disk_size):
56
    """ bytes start offset and bytes size of the third partition
57

58
        third partition directly follows root partition """
59
60
61
62
63
    sector_size = 512
    round_bound = 128
    end_margin = 4194304  # 4MiB

    def roundup(sector):
64
        return rounddown(sector) + round_bound if sector % round_bound != 0 else sector
65
66

    def rounddown(sector):
67
        return sector - (sector % round_bound) if sector % round_bound != 0 else sector
68
69
70
71
72
73
74
75
76
77

    nb_clusters_endofroot = root_size // sector_size
    root_end = roundup(nb_clusters_endofroot)
    data_start = root_end + 1
    data_bytes = disk_size - root_size - end_margin

    return data_start * sector_size, data_bytes


def get_partition_size(image_fpath, start_bytes, logger):
78
    """ bytes size of the data partition """
79
80
81
82
    full_size = get_qemu_image_size(image_fpath, logger)
    return full_size - start_bytes


83
def install_imdisk(logger, force=False):
84
    """ install imdisk manually (replicating steps from install.cmd) """
85
86

    # assume already installed
87
    logger.std("Looking for {}".format(imdisk_exe))
88
    if os.path.exists(imdisk_exe) and not force:
89
        logger.std("ImDisk already present.")
90
        return
91
    logger.std("Imdisk IS NOT present. Installing...")
92
93
94
95
96

    # install the driver and files
    cwd = os.getcwd()
    try:
        os.chdir(imdiskinst)
97
98
99
100
101
102
103
104
105
106
        ret, _ = subprocess_pretty_call(
            [
                "rundll32.exe",
                "setupapi.dll,InstallHinfSection",
                "DefaultInstall",
                "132",
                ".\\imdisk.inf",
            ],
            logger,
        )
107
108
109
110
111
112
    except Exception:
        ret = 1
    finally:
        os.chdir(cwd)

    if ret != 0:
113
114
115
116
        raise OSError(
            "Unable to install ImDisk driver. "
            "Please install manually from the File menu."
        )
117
118
119

    # start services
    failed = []
120
121
    for service in ("imdsksvc", "awealloc", "imdisk"):
        if subprocess_pretty_call(["net", "start", service], logger)[0] != 0:
122
123
            failed.append(service)
    if failed:
124
125
126
127
128
        raise OSError(
            "ImDisk installed but some "
            "service/driver failed to start:  {}.\n"
            "Please install manually from the File menu.".format(" ".join(failed))
        )
129

130
    assert os.path.exists(imdisk_exe)
131
132


133
def open_explorer_for_imdisk(logger):
134
135
136
137
138
139
140
141
    subprocess_external(
        [
            os.path.join(os.environ["SystemRoot"], "explorer.exe"),
            "/select,",
            os.path.normpath(os.path.join(imdiskinst, "install.cmd")),
        ],
        logger,
    )
142
143


144
def get_avail_drive_letter(logger):
145
    """ returns a free Windows drive letter to mount image in """
146
147
148
149
150

    if not sys.platform == "win32":
        raise NotImplementedError("only for windows")

    # get volumes from wmic
151
152
153
    wmic_out = subprocess_pretty_call(
        ["wmic", "logicaldisk", "get", "caption"], logger, check=True, decode=True
    )
154
155
156
    volumes = [line.strip()[:-1] for line in wmic_out[1:-1]]

    # get list of network mappings
157
    net_out = subprocess_pretty_call(["net", "use"], logger, check=True, decode=True)
158
    reg = r"\s+([A-Z])\:\s+\\"
159
160
161
    net_maps = [
        re.match(reg, line).groups()[0] for line in net_out if re.match(reg, line)
    ]
162
163

    # merge and sort both volumes and network shares
164
    used = sorted(list(set(["A", "B", "C"] + volumes + net_maps)))
165
166
167
168
169
170
171

    # find the next available letter in alphabet (should be free)
    for letter in string.ascii_uppercase:
        if letter not in list(used):
            return "{}:".format(letter)


172
def can_write_on(fpath):
173
    """ whether user can read+write on specified file (used with loops) """
174
175
176
177
    return os.access(fpath, os.R_OK | os.W_OK)


def allow_write_on(fpath, logger):
178
    """ sets o+rw on path and return previous perms for others (0755) """
179
180

    si = os.stat(fpath)
181
182
183
    subprocess_pretty_call(
        ["chmod", "-v", "o+rw", fpath], logger, check=True, as_admin=True
    )
184
185
186
187
188

    return oct(si.st_mode)[-4:]


def restore_mode(fpath, mode, logger):
189
190
191
192
    """ sets specified mode to specified file """
    subprocess_pretty_call(
        ["chmod", "-v", mode, fpath], logger, check=True, as_admin=True
    )
193
194
195


def guess_next_loop_device(logger):
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
    def _no_udisks(logger):
        """ guess loop device w/o udisks (using losetup/root) """
        try:
            lines = subprocess_pretty_call(
                [losetup_exe, "--find"], logger, check=True, decode=True
            )
        except Exception as exp:
            logger.err(exp)
            return None

        return re.search(r"(\/dev\/loop[0-9]+)$", lines[-1].strip()).groups()[0]

    if bool(os.getenv("NO_UDISKS", False)):
        return _no_udisks(logger)

211
    try:
212
213
214
        lines = subprocess_pretty_call(
            [udisksctl_exe, "dump"], logger, check=True, decode=True
        )
215
216
217
218
219
220
221
222
    except Exception as exp:
        logger.err(exp)
        return None

    devnum = None  # loopX
    devtype = None  # Block

    for line in lines:
223
        if line.startswith("/"):
224
            try:
225
                devnum = re.search(r"^.*loop([0-9]+)\:$", line).groups()[0]
226
227
228
            except Exception:
                devnum = None

229
        if line.startswith("  org"):
230
            try:
231
                devtype = re.search(r"\.([a-zA-Z]+)\:$", line).groups()[0]
232
233
234
            except Exception:
                devtype = None

235
236
        if devnum and devtype == "Block" and line.startswith("    Size:"):
            size = int(re.search(r"\s+Size\:\s+([0-9]+)$", line).groups()[0])
237
238
239
240
241
            if size == 0:
                return "/dev/loop{}".format(devnum)


def test_mount_procedure(image_fpath, logger, thorough=False):
242
    """ whether we are able to mount and unmount data partition
243
244

        usefull to ensure setup is OK before starting process
245
        `thorough` param tests it in two passes writing/checking a file """
246
247
248
249
250

    if sys.platform == "win32":
        install_imdisk(logger)  # make sure we have imdisk installed

    try:
251
        logger.std(". mounting {}".format(image_fpath))
252
        mount_point, device = mount_data_partition(image_fpath, logger)
253
        logger.std(". mounted {} on {}".format(device, mount_point))
254
255
256
257

        if thorough:
            # write a file on the partition
            value = random.randint(0, 1000)
258
259
260
261
262
263
            logger.std(
                ". writing `{}` inside {}".format(
                    value, os.path.join(mount_point, ".check-part")
                )
            )
            with open(os.path.join(mount_point, ".check-part"), "w") as f:
264
                f.write(str(value))
265

266
            # unmount partitition
267
            logger.std(". unmounting {}".format(mount_point))
268
269
270
            unmount_data_partition(mount_point, device, logger)

            # remount partition
271
            logger.std(". remounting {}".format(image_fpath))
272
            mount_point, device = mount_data_partition(image_fpath, logger)
273
            logger.std(". remounted {} on {}".format(device, mount_point))
274
275

            # read the file and check it's what we just wrote
276
            with open(os.path.join(mount_point, ".check-part"), "r") as f:
277
278
279
280
281
282
                content = f.read()
                logger.std(". read `{}` from file (was `{}`)".format(content, value))
                return int(content) == value
    except Exception as exp:
        logger.err(exp)
        logger.err(traceback.format_exc())
283
284
285
286
        return False
    finally:
        try:
            # unmount partition
287
            logger.std("post-test unmounting {} and {}".format(mount_point, device))
288
289
290
291
292
293
294
            unmount_data_partition(mount_point, device, logger)
        except NameError:
            pass  # was not mounted
        except Exception:
            pass  # failed to unmount (outch)


295
def get_virtual_device(image_fpath, logger):
296
    """ create and return a loop device or drive letter we can format/mount """
297
298
299

    if sys.platform == "linux":
        # find out offset for third partition from the root part size
rgaudin's avatar
rgaudin committed
300
        base_image = get_content("hotspot_master_image")
301
302
        disk_size = get_qemu_image_size(image_fpath, logger)
        offset, size = get_start_offset(
303
304
            base_image.get("root_partition_size"), disk_size
        )
305
306

        # prepare loop device
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
        if bool(os.getenv("NO_UDISKS", False)):
            loop_maker = subprocess_pretty_call(
                [
                    "/sbin/losetup",
                    "--offset",
                    str(offset),
                    "--sizelimit",
                    str(size),
                    "--find",
                    "--show",
                    image_fpath,
                ],
                logger,
                check=True,
                decode=True,
            )[0].strip()
        else:
            loop_maker = subprocess_pretty_call(
                [
                    udisksctl_exe,
                    "loop-setup",
                    "--offset",
                    str(offset),
                    "--size",
                    str(size),
                    "--file",
                    image_fpath,
                    udisks_nou,
                ],
                logger,
                check=True,
                decode=True,
            )[0].strip()
340

341
        target_dev = re.search(r"(\/dev\/loop[0-9]+)\.?$", loop_maker).groups()[0]
rgaudin's avatar
rgaudin committed
342
343
344
345

    elif sys.platform == "darwin":
        # attach image to create loop devices
        hdiutil_out = subprocess_pretty_call(
346
347
348
349
350
            [hdiutil_exe, "attach", "-nomount", image_fpath],
            logger,
            check=True,
            decode=True,
        )[0].strip()
rgaudin's avatar
rgaudin committed
351
352
353
354
355
356
357
358
359
        target_dev = str(hdiutil_out.splitlines()[0].split()[0])

    elif sys.platform == "win32":
        # make sure we have imdisk installed
        install_imdisk(logger)

        # get an available letter
        target_dev = get_avail_drive_letter(logger)

360
361
362
363
364
365
366
367
368
369
    return target_dev


def release_virtual_device(device, logger):
    if bool(os.getenv("NO_UDISKS", False)):
        subprocess_pretty_call([losetup_exe, "--detach", device], logger)
    else:
        subprocess_pretty_call(
            [udisksctl_exe, "loop-delete", "--block-device", device, udisks_nou], logger
        )
rgaudin's avatar
rgaudin committed
370
371


372
def format_data_partition(image_fpath, logger):
373
    """ format the QEMU image's 3rd part in exfat on host """
rgaudin's avatar
rgaudin committed
374
375
376
377

    target_dev = get_virtual_device(image_fpath, logger)

    if sys.platform == "linux":
378
379
380
381
382
383
384
385
386

        # make sure it's not mounted (gnome automounts)
        if bool(os.getenv("NO_UDISKS", False)):
            subprocess_pretty_call([umount_exe, target_dev], logger)
        else:
            subprocess_pretty_call(
                [udisksctl_exe, "unmount", "--block-device", target_dev, udisks_nou],
                logger,
            )
rgaudin's avatar
rgaudin committed
387

388
389
390
391
392
        # change mode via elevation if we can't format it
        previous_mode = None
        if not can_write_on(target_dev):
            previous_mode = allow_write_on(target_dev, logger)

rgaudin's avatar
rgaudin committed
393
394
        # format the data partition
        try:
rgaudin's avatar
rgaudin committed
395
396
397
            subprocess_pretty_check_call(
                [mkfs_exe, "-n", data_partition_label, target_dev], logger
            )
rgaudin's avatar
rgaudin committed
398
        finally:
399
400
401
402
            # remove write rights we just added
            if previous_mode:
                restore_mode(target_dev, previous_mode, logger)

rgaudin's avatar
rgaudin committed
403
            # ensure we release the loop device on mount failure
404
            unmount_data_partition(None, target_dev, logger)
rgaudin's avatar
rgaudin committed
405
406
407
408
409
410

    elif sys.platform == "darwin":
        target_part = "{dev}s3".format(dev=target_dev)

        try:
            subprocess_pretty_check_call(
rgaudin's avatar
rgaudin committed
411
412
413
414
415
416
417
418
                [
                    diskutil_exe,
                    "eraseVolume",
                    "exfat",
                    data_partition_label,
                    target_part,
                ],
                logger,
419
            )
rgaudin's avatar
rgaudin committed
420
421
        finally:
            # ensure we release the loop device on mount failure
422
            unmount_data_partition(None, target_dev, logger)
rgaudin's avatar
rgaudin committed
423
424
425
426
427

    elif sys.platform == "win32":
        # mount into specified path AND format
        try:
            subprocess_pretty_check_call(
428
429
430
431
432
433
434
435
436
437
438
439
                [
                    imdisk_exe,
                    "-a",
                    "-f",
                    image_fpath,
                    "-o",
                    "rw",
                    "-t",
                    "file",
                    "-v",
                    "3",
                    "-p",
rgaudin's avatar
rgaudin committed
440
                    "/fs:exfat /V:{} /q /y".format(data_partition_label),
441
442
443
444
445
                    "-m",
                    target_dev,
                ],
                logger,
            )
rgaudin's avatar
rgaudin committed
446
447
        finally:
            # ensure we release the loop device on mount failure
448
            unmount_data_partition(None, target_dev, logger)
rgaudin's avatar
rgaudin committed
449
450


451
def mount_data_partition(image_fpath, logger):
452
    """ mount the QEMU image's 3rd part and return its mount point/drive """
rgaudin's avatar
rgaudin committed
453
454
455

    target_dev = get_virtual_device(image_fpath, logger)

456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
    if sys.platform == "linux" and bool(os.getenv("NO_UDISKS", False)):
        # create a mount point in /tmp
        mount_point = tempfile.mkdtemp()

        try:
            subprocess_pretty_check_call(
                [mount_exe, "-t", "exfat", target_dev, mount_point], logger
            )
        except Exception:
            # ensure we release the loop device on mount failure
            unmount_data_partition(mount_point, target_dev, logger)
            raise
        return mount_point, target_dev

    elif sys.platform == "linux":
471
472
        # mount the loop-device (udisksctl sets the mount point)
        udisks_mount_ret, udisks_mount = subprocess_pretty_call(
473
474
475
476
477
            [udisksctl_exe, "mount", "--block-device", target_dev, udisks_nou],
            logger,
            check=False,
            decode=True,
        )
478
479
480
481
        udisks_mount = udisks_mount[0].strip()

        if udisks_mount_ret != 0 and "AlreadyMounted" in udisks_mount:
            # was automatically mounted (gnome default)
482
            mount_point = re.search(r"at `(\/media\/.*)'\.$", udisks_mount).groups()[0]
483
484
        elif udisks_mount_ret == 0:
            # udisksctl always mounts under /media/
485
            mount_point = re.search(r"at (\/media\/.+)\.$", udisks_mount).groups()[0]
486
        else:
487
            release_virtual_device(target_dev, logger)  # release loop if attached
488
489
490
491
492
493
494
495
496
497
            raise OSError("failed to mount {}".format(target_dev))

        return mount_point, target_dev

    elif sys.platform == "darwin":
        target_part = "{dev}s3".format(dev=target_dev)

        # create a mount point in /tmp
        mount_point = tempfile.mkdtemp()
        try:
498
            subprocess_pretty_check_call(
499
500
                [mount_exe, "-t", "exfat", target_part, mount_point], logger
            )
501
502
        except Exception:
            # ensure we release the loop device on mount failure
503
            unmount_data_partition(mount_point, target_dev, logger)
504
505
506
507
508
509
510
511
            raise
        return mount_point, target_dev

    elif sys.platform == "win32":
        mount_point = "{}\\".format(target_dev)

        # mount into the specified drive
        subprocess_pretty_check_call(
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
            [
                imdisk_exe,
                "-a",
                "-f",
                image_fpath,
                "-o",
                "rw",
                "-t",
                "file",
                "-v",
                "3",
                "-m",
                target_dev,
            ],
            logger,
        )
528
529
530
        return mount_point, target_dev


531
def unmount_data_partition(mount_point, device, logger):
532
    """ unmount data partition and free virtual resources """
533
534

    if sys.platform == "linux":
rgaudin's avatar
rgaudin committed
535

536
537
        # sleep to prevent unmount failures
        time.sleep(5)
rgaudin's avatar
rgaudin committed
538
539
540

        if mount_point:
            # unmount using device path
541
542
543
544
545
546
547
            if bool(os.getenv("NO_UDISKS", False)):
                subprocess_pretty_call([umount_exe, device], logger)
            else:
                subprocess_pretty_call(
                    [udisksctl_exe, "unmount", "--block-device", device, udisks_nou],
                    logger,
                )
rgaudin's avatar
rgaudin committed
548
549
            try:
                os.rmdir(mount_point)
550
            except (FileNotFoundError, PermissionError):
rgaudin's avatar
rgaudin committed
551
                pass
552
553
554

        # delete the loop device (might have already been deleted)
        release_virtual_device(device, logger)
555
556

    elif sys.platform == "darwin":
rgaudin's avatar
rgaudin committed
557
558
559
560
561
562
563
564

        if mount_point:
            # unmount
            subprocess_pretty_call([umount_exe, mount_point], logger)
            try:
                os.rmdir(mount_point)
            except FileNotFoundError:
                pass
565
        # detach image file (also unmounts if not already done)
566
        subprocess_pretty_call([hdiutil_exe, "detach", device], logger)
567
    elif sys.platform == "win32":
rgaudin's avatar
rgaudin committed
568

569
        # unmount using force (-D) as -d is not reliable
570
        subprocess_pretty_call([imdisk_exe, "-D", "-m", device], logger)