summaryrefslogtreecommitdiff
path: root/buildstream/sandbox/_mount.py
blob: 0f96a92b7825e5380804359a0ff6919b00d070df (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
#
#  Copyright (C) 2017 Codethink Limited
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
#
#  This library 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
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
#  Authors:
#        Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>

import os
from collections import OrderedDict
from contextlib import contextmanager, ExitStack

from .. import utils
from .._fuse import SafeHardlinks


# Mount()
#
# Helper data object representing a single mount point in the mount map
#
class Mount():
    def __init__(self, sandbox, mount_point, safe_hardlinks):
        scratch_directory = sandbox._get_scratch_directory()
        # Getting external_directory here is acceptable as we're part of the sandbox code.
        root_directory = sandbox.get_virtual_directory().external_directory

        self.mount_point = mount_point
        self.safe_hardlinks = safe_hardlinks

        # FIXME: When the criteria for mounting something and it's parent
        #        mount is identical, then there is no need to mount an additional
        #        fuse layer (i.e. if the root is read-write and there is a directory
        #        marked for staged artifacts directly within the rootfs, they can
        #        safely share the same fuse layer).
        #
        #        In these cases it would be saner to redirect the sub-mount to
        #        a regular mount point within the parent's redirected mount.
        #
        if self.safe_hardlinks:
            # Redirected mount
            self.mount_origin = os.path.join(root_directory, mount_point.lstrip(os.sep))
            self.mount_base = os.path.join(scratch_directory, utils.url_directory_name(mount_point))
            self.mount_source = os.path.join(self.mount_base, 'mount')
            self.mount_tempdir = os.path.join(self.mount_base, 'temp')
            os.makedirs(self.mount_origin, exist_ok=True)
            os.makedirs(self.mount_tempdir, exist_ok=True)
        else:
            # No redirection needed
            self.mount_source = os.path.join(root_directory, mount_point.lstrip(os.sep))

        external_mount_sources = sandbox._get_mount_sources()
        external_mount_source = external_mount_sources.get(mount_point)

        if external_mount_source is None:
            os.makedirs(self.mount_source, exist_ok=True)
        else:
            if os.path.isdir(external_mount_source):
                os.makedirs(self.mount_source, exist_ok=True)
            else:
                # When mounting a regular file, ensure the parent
                # directory exists in the sandbox; and that an empty
                # file is created at the mount location.
                parent_dir = os.path.dirname(self.mount_source.rstrip('/'))
                os.makedirs(parent_dir, exist_ok=True)
                if not os.path.exists(self.mount_source):
                    with open(self.mount_source, 'w'):
                        pass

    @contextmanager
    def mounted(self, sandbox):
        if self.safe_hardlinks:
            mount = SafeHardlinks(self.mount_origin, self.mount_tempdir)
            with mount.mounted(self.mount_source):
                yield
        else:
            # Nothing to mount here
            yield


# MountMap()
#
# Helper object for mapping of the sandbox mountpoints
#
# Args:
#    sandbox (Sandbox): The sandbox object
#    root_readonly (bool): Whether the sandbox root is readonly
#
class MountMap():

    def __init__(self, sandbox, root_readonly):
        # We will be doing the mounts in the order in which they were declared.
        self.mounts = OrderedDict()

        # We want safe hardlinks on rootfs whenever root is not readonly
        self.mounts['/'] = Mount(sandbox, '/', not root_readonly)

        for mark in sandbox._get_marked_directories():
            directory = mark['directory']
            artifact = mark['artifact']

            # We want safe hardlinks for any non-root directory where
            # artifacts will be staged to
            self.mounts[directory] = Mount(sandbox, directory, artifact)

    # get_mount_source()
    #
    # Gets the host directory where the mountpoint in the
    # sandbox should be bind mounted from
    #
    # Args:
    #    mountpoint (str): The absolute mountpoint path inside the sandbox
    #
    # Returns:
    #    The host path to be mounted at the mount point
    #
    def get_mount_source(self, mountpoint):
        return self.mounts[mountpoint].mount_source

    # mounted()
    #
    # A context manager which ensures all the mount sources
    # were mounted with any fuse layers which may have been needed.
    #
    # Args:
    #    sandbox (Sandbox): The sandbox
    #
    @contextmanager
    def mounted(self, sandbox):
        with ExitStack() as stack:
            for _, mount in self.mounts.items():
                stack.enter_context(mount.mounted(sandbox))
            yield