summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/volumes/fs/operations/group.py
blob: c91969278022b1ddf195f97b1e3e4bd85576e601 (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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
import os
import errno
import logging
from contextlib import contextmanager

import cephfs

from .snapshot_util import mksnap, rmsnap
from .pin_util import pin
from .template import GroupTemplate
from ..fs_util import listdir, listsnaps, get_ancestor_xattr, create_base_dir, has_subdir
from ..exception import VolumeException

log = logging.getLogger(__name__)

class Group(GroupTemplate):
    # Reserved subvolume group name which we use in paths for subvolumes
    # that are not assigned to a group (i.e. created with group=None)
    NO_GROUP_NAME = "_nogroup"

    def __init__(self, fs, vol_spec, groupname):
        if groupname == Group.NO_GROUP_NAME:
            raise VolumeException(-errno.EPERM, "Operation not permitted for group '{0}' as it is an internal group.".format(groupname))
        if groupname in vol_spec.INTERNAL_DIRS:
            raise VolumeException(-errno.EINVAL, "'{0}' is an internal directory and not a valid group name.".format(groupname))
        self.fs = fs
        self.user_id = None
        self.group_id = None
        self.vol_spec = vol_spec
        self.groupname = groupname if groupname else Group.NO_GROUP_NAME

    @property
    def path(self):
        return os.path.join(self.vol_spec.base_dir.encode('utf-8'), self.groupname.encode('utf-8'))

    @property
    def group_name(self):
        return self.groupname

    @property
    def uid(self):
        return self.user_id

    @uid.setter
    def uid(self, val):
        self.user_id = val

    @property
    def gid(self):
        return self.group_id

    @gid.setter
    def gid(self, val):
        self.group_id = val

    def is_default_group(self):
        return self.groupname == Group.NO_GROUP_NAME

    def list_subvolumes(self):
        try:
            return listdir(self.fs, self.path)
        except VolumeException as ve:
            # listing a default group when it's not yet created
            if ve.errno == -errno.ENOENT and self.is_default_group():
                return []
            raise

    def has_subvolumes(self):
        try:
            return has_subdir(self.fs, self.path)
        except VolumeException as ve:
            # listing a default group when it's not yet created
            if ve.errno == -errno.ENOENT and self.is_default_group():
                return False
            raise

    def pin(self, pin_type, pin_setting):
        return pin(self.fs, self.path, pin_type, pin_setting)

    def create_snapshot(self, snapname):
        snappath = os.path.join(self.path,
                                self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
                                snapname.encode('utf-8'))
        mksnap(self.fs, snappath)

    def remove_snapshot(self, snapname):
        snappath = os.path.join(self.path,
                                self.vol_spec.snapshot_dir_prefix.encode('utf-8'),
                                snapname.encode('utf-8'))
        rmsnap(self.fs, snappath)

    def list_snapshots(self):
        try:
            dirpath = os.path.join(self.path,
                                   self.vol_spec.snapshot_dir_prefix.encode('utf-8'))
            return listsnaps(self.fs, self.vol_spec, dirpath, filter_inherited_snaps=True)
        except VolumeException as ve:
            if ve.errno == -errno.ENOENT:
                return []
            raise

    def info(self):
        st = self.fs.statx(self.path, cephfs.CEPH_STATX_BTIME | cephfs.CEPH_STATX_SIZE
                           | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID | cephfs.CEPH_STATX_MODE
                           | cephfs.CEPH_STATX_ATIME | cephfs.CEPH_STATX_MTIME | cephfs.CEPH_STATX_CTIME,
                           cephfs.AT_SYMLINK_NOFOLLOW)
        usedbytes = st["size"]
        try:
            nsize = int(self.fs.getxattr(self.path, 'ceph.quota.max_bytes').decode('utf-8'))
        except cephfs.NoData:
            nsize = 0

        try:
            data_pool = self.fs.getxattr(self.path, 'ceph.dir.layout.pool').decode('utf-8')
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

        return {'uid': int(st["uid"]),
                'gid': int(st["gid"]),
                'atime': str(st["atime"]),
                'mtime': str(st["mtime"]),
                'ctime': str(st["ctime"]),
                'mode': int(st["mode"]),
                'data_pool': data_pool,
                'created_at': str(st["btime"]),
                'bytes_quota': "infinite" if nsize == 0 else nsize,
                'bytes_used': int(usedbytes),
                'bytes_pcent': "undefined" if nsize == 0 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0)}

    def resize(self, newsize, noshrink):
        try:
            newsize = int(newsize)
            if newsize <= 0:
                raise VolumeException(-errno.EINVAL, "Invalid subvolume group size")
        except ValueError:
            newsize = newsize.lower()
            if not (newsize == "inf" or newsize == "infinite"):
                raise (VolumeException(-errno.EINVAL, "invalid size option '{0}'".format(newsize)))
            newsize = 0
            noshrink = False

        try:
            maxbytes = int(self.fs.getxattr(self.path, 'ceph.quota.max_bytes').decode('utf-8'))
        except cephfs.NoData:
            maxbytes = 0
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

        group_stat = self.fs.stat(self.path)
        if newsize > 0 and newsize < group_stat.st_size:
            if noshrink:
                raise VolumeException(-errno.EINVAL, "Can't resize the subvolume group. The new size"
                                      " '{0}' would be lesser than the current used size '{1}'"
                                      .format(newsize, group_stat.st_size))

        if not newsize == maxbytes:
            try:
                self.fs.setxattr(self.path, 'ceph.quota.max_bytes', str(newsize).encode('utf-8'), 0)
            except cephfs.Error as e:
                raise (VolumeException(-e.args[0],
                                       "Cannot set new size for the subvolume group. '{0}'".format(e.args[1])))
        return newsize, group_stat.st_size

def set_group_attrs(fs, path, attrs):
    # set subvolume group attrs
    # set size
    quota = attrs.get("quota")
    if quota is not None:
        try:
            fs.setxattr(path, 'ceph.quota.max_bytes', str(quota).encode('utf-8'), 0)
        except cephfs.InvalidValue:
            raise VolumeException(-errno.EINVAL, "invalid size specified: '{0}'".format(quota))
        except cephfs.Error as e:
            raise VolumeException(-e.args[0], e.args[1])

    # set pool layout
    pool = attrs.get("data_pool")
    if not pool:
        pool = get_ancestor_xattr(fs, path, "ceph.dir.layout.pool")
    try:
        fs.setxattr(path, 'ceph.dir.layout.pool', pool.encode('utf-8'), 0)
    except cephfs.InvalidValue:
        raise VolumeException(-errno.EINVAL,
                              "Invalid pool layout '{0}'. It must be a valid data pool".format(pool))

    # set uid/gid
    uid = attrs.get("uid")
    if uid is None:
        uid = 0
    else:
        try:
            uid = int(uid)
            if uid < 0:
                raise ValueError
        except ValueError:
            raise VolumeException(-errno.EINVAL, "invalid UID")

    gid = attrs.get("gid")
    if gid is None:
        gid = 0
    else:
        try:
            gid = int(gid)
            if gid < 0:
                raise ValueError
        except ValueError:
            raise VolumeException(-errno.EINVAL, "invalid GID")
    fs.chown(path, uid, gid)

    # set mode
    mode = attrs.get("mode", None)
    if mode is not None:
        fs.lchmod(path, mode)

def create_group(fs, vol_spec, groupname, size, pool, mode, uid, gid):
    """
    create a subvolume group.

    :param fs: ceph filesystem handle
    :param vol_spec: volume specification
    :param groupname: subvolume group name
    :param size: In bytes, or None for no size limit
    :param pool: the RADOS pool where the data objects of the subvolumes will be stored
    :param mode: the user permissions
    :param uid: the user identifier
    :param gid: the group identifier
    :return: None
    """
    group = Group(fs, vol_spec, groupname)
    path = group.path
    vol_spec_base_dir = group.vol_spec.base_dir.encode('utf-8')

    # create vol_spec base directory with default mode(0o755) if it doesn't exist
    create_base_dir(fs, vol_spec_base_dir, vol_spec.DEFAULT_MODE)
    fs.mkdir(path, mode)
    try:
        attrs = {
            'uid': uid,
            'gid': gid,
            'data_pool': pool,
            'quota': size
        }
        set_group_attrs(fs, path, attrs)
    except (cephfs.Error, VolumeException) as e:
        try:
            # cleanup group path on best effort basis
            log.debug("cleaning up subvolume group path: {0}".format(path))
            fs.rmdir(path)
        except cephfs.Error as ce:
            log.debug("failed to clean up subvolume group {0} with path: {1} ({2})".format(groupname, path, ce))
        if isinstance(e, cephfs.Error):
            e = VolumeException(-e.args[0], e.args[1])
        raise e

def remove_group(fs, vol_spec, groupname):
    """
    remove a subvolume group.

    :param fs: ceph filesystem handle
    :param vol_spec: volume specification
    :param groupname: subvolume group name
    :return: None
    """
    group = Group(fs, vol_spec, groupname)
    try:
        fs.rmdir(group.path)
    except cephfs.Error as e:
        if e.args[0] == errno.ENOENT:
            raise VolumeException(-errno.ENOENT, "subvolume group '{0}' does not exist".format(groupname))
        raise VolumeException(-e.args[0], e.args[1])

@contextmanager
def open_group(fs, vol_spec, groupname):
    """
    open a subvolume group. This API is to be used as a context manager.

    :param fs: ceph filesystem handle
    :param vol_spec: volume specification
    :param groupname: subvolume group name
    :return: yields a group object (subclass of GroupTemplate)
    """
    group = Group(fs, vol_spec, groupname)
    try:
        st = fs.stat(group.path)
        group.uid = int(st.st_uid)
        group.gid = int(st.st_gid)
    except cephfs.Error as e:
        if e.args[0] == errno.ENOENT:
            if not group.is_default_group():
                raise VolumeException(-errno.ENOENT, "subvolume group '{0}' does not exist".format(groupname))
        else:
            raise VolumeException(-e.args[0], e.args[1])
    yield group

@contextmanager
def open_group_unique(fs, vol_spec, groupname, c_group, c_groupname):
    if groupname == c_groupname:
        yield c_group
    else:
        with open_group(fs, vol_spec, groupname) as group:
            yield group