summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/volumes/fs/operations/versions/auth_metadata.py
blob: 259dcd0e0c3140a912dc17a87c4cb0d6c574be84 (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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
from contextlib import contextmanager
import os
import fcntl
import json
import logging
import struct
import uuid

import cephfs

from ..group import Group

log = logging.getLogger(__name__)

class AuthMetadataError(Exception):
    pass

class AuthMetadataManager(object):

    # Current version
    version = 6

    # Filename extensions for meta files.
    META_FILE_EXT = ".meta"
    DEFAULT_VOL_PREFIX = "/volumes"

    def __init__(self, fs):
        self.fs = fs
        self._id = struct.unpack(">Q", uuid.uuid1().bytes[0:8])[0]
        self.volume_prefix = self.DEFAULT_VOL_PREFIX

    def _to_bytes(self, param):
        '''
        Helper method that returns byte representation of the given parameter.
        '''
        if isinstance(param, str):
            return param.encode('utf-8')
        elif param is None:
            return param
        else:
            return str(param).encode('utf-8')

    def _subvolume_metadata_path(self, group_name, subvol_name):
        return os.path.join(self.volume_prefix, "_{0}:{1}{2}".format(
            group_name if group_name != Group.NO_GROUP_NAME else "",
            subvol_name,
            self.META_FILE_EXT))

    def _check_compat_version(self, compat_version):
        if self.version < compat_version:
            msg = ("The current version of AuthMetadataManager, version {0} "
                   "does not support the required feature. Need version {1} "
                   "or greater".format(self.version, compat_version)
                  )
            log.error(msg)
            raise AuthMetadataError(msg)

    def _metadata_get(self, path):
        """
        Return a deserialized JSON object, or None
        """
        fd = self.fs.open(path, "r")
        # TODO iterate instead of assuming file < 4MB
        read_bytes = self.fs.read(fd, 0, 4096 * 1024)
        self.fs.close(fd)
        if read_bytes:
            return json.loads(read_bytes.decode())
        else:
            return None

    def _metadata_set(self, path, data):
        serialized = json.dumps(data)
        fd = self.fs.open(path, "w")
        try:
            self.fs.write(fd, self._to_bytes(serialized), 0)
            self.fs.fsync(fd, 0)
        finally:
            self.fs.close(fd)

    def _lock(self, path):
        @contextmanager
        def fn():
            while(1):
                fd = self.fs.open(path, os.O_CREAT, 0o755)
                self.fs.flock(fd, fcntl.LOCK_EX, self._id)

                # The locked file will be cleaned up sometime. It could be
                # unlinked by consumer e.g., an another manila-share service
                # instance, before lock was applied on it. Perform checks to
                # ensure that this does not happen.
                try:
                    statbuf = self.fs.stat(path)
                except cephfs.ObjectNotFound:
                    self.fs.close(fd)
                    continue

                fstatbuf = self.fs.fstat(fd)
                if statbuf.st_ino == fstatbuf.st_ino:
                    break

            try:
                yield
            finally:
                self.fs.flock(fd, fcntl.LOCK_UN, self._id)
                self.fs.close(fd)

        return fn()

    def _auth_metadata_path(self, auth_id):
        return os.path.join(self.volume_prefix, "${0}{1}".format(
            auth_id, self.META_FILE_EXT))

    def auth_lock(self, auth_id):
        return self._lock(self._auth_metadata_path(auth_id))

    def auth_metadata_get(self, auth_id):
        """
        Call me with the metadata locked!

        Check whether a auth metadata structure can be decoded by the current
        version of AuthMetadataManager.

        Return auth metadata that the current version of AuthMetadataManager
        can decode.
        """
        auth_metadata = self._metadata_get(self._auth_metadata_path(auth_id))

        if auth_metadata:
            self._check_compat_version(auth_metadata['compat_version'])

        return auth_metadata

    def auth_metadata_set(self, auth_id, data):
        """
        Call me with the metadata locked!

        Fsync the auth metadata.

        Add two version attributes to the auth metadata,
        'compat_version', the minimum AuthMetadataManager version that can
        decode the metadata, and 'version', the AuthMetadataManager version
        that encoded the metadata.
        """
        data['compat_version'] = 6
        data['version'] = self.version
        return self._metadata_set(self._auth_metadata_path(auth_id), data)

    def create_subvolume_metadata_file(self, group_name, subvol_name):
        """
        Create a subvolume metadata file, if it does not already exist, to store
        data about auth ids having access to the subvolume
        """
        fd = self.fs.open(self._subvolume_metadata_path(group_name, subvol_name),
                          os.O_CREAT, 0o755)
        self.fs.close(fd)

    def delete_subvolume_metadata_file(self, group_name, subvol_name):
        vol_meta_path = self._subvolume_metadata_path(group_name, subvol_name)
        try:
            self.fs.unlink(vol_meta_path)
        except cephfs.ObjectNotFound:
            pass

    def subvol_metadata_lock(self, group_name, subvol_name):
        """
        Return a ContextManager which locks the authorization metadata for
        a particular subvolume, and persists a flag to the metadata indicating
        that it is currently locked, so that we can detect dirty situations
        during recovery.

        This lock isn't just to make access to the metadata safe: it's also
        designed to be used over the two-step process of checking the
        metadata and then responding to an authorization request, to
        ensure that at the point we respond the metadata hasn't changed
        in the background.  It's key to how we avoid security holes
        resulting from races during that problem ,
        """
        return self._lock(self._subvolume_metadata_path(group_name, subvol_name))

    def subvol_metadata_get(self, group_name, subvol_name):
        """
        Call me with the metadata locked!

        Check whether a subvolume metadata structure can be decoded by the current
        version of AuthMetadataManager.

        Return a subvolume_metadata structure that the current version of
        AuthMetadataManager can decode.
        """
        subvolume_metadata = self._metadata_get(self._subvolume_metadata_path(group_name, subvol_name))

        if subvolume_metadata:
            self._check_compat_version(subvolume_metadata['compat_version'])

        return subvolume_metadata

    def subvol_metadata_set(self, group_name, subvol_name, data):
        """
        Call me with the metadata locked!

        Add two version attributes to the subvolume metadata,
        'compat_version', the minimum AuthMetadataManager version that can
        decode the metadata and 'version', the AuthMetadataManager version
        that encoded the metadata.
        """
        data['compat_version'] = 1
        data['version'] = self.version
        return self._metadata_set(self._subvolume_metadata_path(group_name, subvol_name), data)