Skip to content

Commit

Permalink
Merge pull request #47236 from kotreshhr/wip-legacy-upgrade-config-is…
Browse files Browse the repository at this point in the history
…sue-octopus-1

octopus:  mgr/volumes: Fix subvolume discover during upgrade

Reviewed-by: Ramana Raja <rraja@redhat.com>
  • Loading branch information
yuriw committed Aug 2, 2022
2 parents 57544da + ca59322 commit 4e53463
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 12 deletions.
139 changes: 139 additions & 0 deletions qa/tasks/cephfs/test_volumes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4654,3 +4654,142 @@ def test_subvolume_snapshot_clone_cancel_pending(self):

# verify trash dir is clean
self._wait_for_trash_empty()

def test_malicious_metafile_on_legacy_to_v1_upgrade(self):
"""
Validate handcrafted .meta file on legacy subvol root doesn't break the system
on legacy subvol upgrade to v1
poor man's upgrade test -- theme continues...
"""
subvol1, subvol2 = self._generate_random_subvolume_name(2)

# emulate a old-fashioned subvolume in the default group
createpath1 = os.path.join(".", "volumes", "_nogroup", subvol1)
self.mount_a.run_shell(['mkdir', '-p', createpath1])

# add required xattrs to subvolume
default_pool = self.mount_a.getfattr(".", "ceph.dir.layout.pool")
self.mount_a.setfattr(createpath1, 'ceph.dir.layout.pool', default_pool)

# create v2 subvolume
self._fs_cmd("subvolume", "create", self.volname, subvol2)

# Create malicious .meta file in legacy subvolume root. Copy v2 subvolume
# .meta into legacy subvol1's root
subvol2_metapath = os.path.join(".", "volumes", "_nogroup", subvol2, ".meta")
self.mount_a.run_shell(["cp", subvol2_metapath, createpath1])

# Upgrade legacy subvol1 to v1
subvolpath1 = self._fs_cmd("subvolume", "getpath", self.volname, subvol1)
self.assertNotEqual(subvolpath1, None)
subvolpath1 = subvolpath1.rstrip()

# the subvolume path returned should not be of subvol2 from handcrafted
# .meta file
self.assertEqual(createpath1[1:], subvolpath1)

# ensure metadata file is in legacy location, with required version v1
self._assert_meta_location_and_version(self.volname, subvol1, version=1, legacy=True)

# Authorize alice authID read-write access to subvol1. Verify it authorizes subvol1 path and not subvol2
# path whose '.meta' file is copied to subvol1 root
authid1 = "alice"
self._fs_cmd("subvolume", "authorize", self.volname, subvol1, authid1)

# Validate that the mds path added is of subvol1 and not of subvol2
out = json.loads(self.fs.mon_manager.raw_cluster_cmd("auth", "get", "client.alice", "--format=json-pretty"))
self.assertEqual("client.alice", out[0]["entity"])
self.assertEqual("allow rw path={0}".format(createpath1[1:]), out[0]["caps"]["mds"])

# remove subvolume
self._fs_cmd("subvolume", "rm", self.volname, subvol1)
self._fs_cmd("subvolume", "rm", self.volname, subvol2)

# verify trash dir is clean
self._wait_for_trash_empty()

def test_binary_metafile_on_legacy_to_v1_upgrade(self):
"""
Validate binary .meta file on legacy subvol root doesn't break the system
on legacy subvol upgrade to v1
poor man's upgrade test -- theme continues...
"""
subvol = self._generate_random_subvolume_name()
group = self._generate_random_group_name()

# emulate a old-fashioned subvolume -- in a custom group
createpath = os.path.join(".", "volumes", group, subvol)
self.mount_a.run_shell(['mkdir', '-p', createpath])

# add required xattrs to subvolume
default_pool = self.mount_a.getfattr(".", "ceph.dir.layout.pool")
self.mount_a.setfattr(createpath, 'ceph.dir.layout.pool', default_pool)

# Create unparseable binary .meta file on legacy subvol's root
meta_contents = os.urandom(4096)
meta_filepath = os.path.join(self.mount_a.mountpoint, createpath, ".meta")
sudo_write_file(self.mount_a.client_remote, meta_filepath, meta_contents)

# Upgrade legacy subvol to v1
subvolpath = self._fs_cmd("subvolume", "getpath", self.volname, subvol, group)
self.assertNotEqual(subvolpath, None)
subvolpath = subvolpath.rstrip()

# The legacy subvolume path should be returned for subvol.
# Should ignore unparseable binary .meta file in subvol's root
self.assertEqual(createpath[1:], subvolpath)

# ensure metadata file is in legacy location, with required version v1
self._assert_meta_location_and_version(self.volname, subvol, subvol_group=group, version=1, legacy=True)

# remove subvolume
self._fs_cmd("subvolume", "rm", self.volname, subvol, group)

# verify trash dir is clean
self._wait_for_trash_empty()

# remove group
self._fs_cmd("subvolumegroup", "rm", self.volname, group)

def test_unparseable_metafile_on_legacy_to_v1_upgrade(self):
"""
Validate unparseable text .meta file on legacy subvol root doesn't break the system
on legacy subvol upgrade to v1
poor man's upgrade test -- theme continues...
"""
subvol = self._generate_random_subvolume_name()
group = self._generate_random_group_name()

# emulate a old-fashioned subvolume -- in a custom group
createpath = os.path.join(".", "volumes", group, subvol)
self.mount_a.run_shell(['mkdir', '-p', createpath])

# add required xattrs to subvolume
default_pool = self.mount_a.getfattr(".", "ceph.dir.layout.pool")
self.mount_a.setfattr(createpath, 'ceph.dir.layout.pool', default_pool)

# Create unparseable text .meta file on legacy subvol's root
meta_contents = "unparseable config\nfile ...\nunparseable config\nfile ...\n"
meta_filepath = os.path.join(self.mount_a.mountpoint, createpath, ".meta")
sudo_write_file(self.mount_a.client_remote, meta_filepath, meta_contents)

# Upgrade legacy subvol to v1
subvolpath = self._fs_cmd("subvolume", "getpath", self.volname, subvol, group)
self.assertNotEqual(subvolpath, None)
subvolpath = subvolpath.rstrip()

# The legacy subvolume path should be returned for subvol.
# Should ignore unparseable binary .meta file in subvol's root
self.assertEqual(createpath[1:], subvolpath)

# ensure metadata file is in legacy location, with required version v1
self._assert_meta_location_and_version(self.volname, subvol, subvol_group=group, version=1, legacy=True)

# remove subvolume
self._fs_cmd("subvolume", "rm", self.volname, subvol, group)

# verify trash dir is clean
self._wait_for_trash_empty()

# remove group
self._fs_cmd("subvolumegroup", "rm", self.volname, group)
17 changes: 14 additions & 3 deletions src/pybind/mgr/volumes/fs/operations/versions/metadata_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ def __init__(self, fs, config_path, mode):
def refresh(self):
fd = None
conf_data = StringIO()
log.debug("opening config {0}".format(self.config_path))
try:
log.debug("opening config {0}".format(self.config_path))
fd = self.fs.open(self.config_path, os.O_RDONLY)
while True:
data = self.fs.read(fd, -1, MetadataManager.MAX_IO_BYTES)
if not len(data):
break
conf_data.write(data.decode('utf-8'))
conf_data.seek(0)
self.config.readfp(conf_data)
except UnicodeDecodeError:
raise MetadataMgrException(-errno.EINVAL,
"failed to decode, erroneous metadata config '{0}'".format(self.config_path))
except cephfs.ObjectNotFound:
raise MetadataMgrException(-errno.ENOENT, "metadata config '{0}' not found".format(self.config_path))
except cephfs.Error as e:
Expand All @@ -58,6 +59,16 @@ def refresh(self):
if fd is not None:
self.fs.close(fd)

conf_data.seek(0)
try:
if sys.version_info >= (3, 2):
self.config.read_file(conf_data)
else:
self.config.readfp(conf_data)
except configparser.Error:
raise MetadataMgrException(-errno.EINVAL, "failed to parse, erroneous metadata config "
"'{0}'".format(self.config_path))

def flush(self):
# cull empty sections
for section in list(self.config.sections()):
Expand Down
38 changes: 32 additions & 6 deletions src/pybind/mgr/volumes/fs/operations/versions/subvolume_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import uuid
import errno
import logging
from hashlib import md5
import hashlib
from typing import Dict, Union
from pathlib import Path

import cephfs

Expand All @@ -16,6 +17,7 @@
from ...exception import MetadataMgrException, VolumeException
from .op_sm import SubvolumeOpSm
from .auth_metadata import AuthMetadataManager
from .subvolume_attrs import SubvolumeStates

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -73,9 +75,16 @@ def legacy_dir(self):

@property
def legacy_config_path(self):
m = md5()
m.update(self.base_path)
meta_config = "{0}.meta".format(m.digest().hex())
try:
m = hashlib.md5(self.base_path)
except ValueError:
try:
m = hashlib.md5(self.base_path, usedforsecurity=False) # type: ignore
except TypeError:
raise VolumeException(-errno.EINVAL,
"require python's hashlib library to support usedforsecurity flag in FIPS enabled systems")

meta_config = "{0}.meta".format(m.hexdigest())
return os.path.join(self.legacy_dir, meta_config.encode('utf-8'))

@property
Expand Down Expand Up @@ -111,7 +120,7 @@ def features(self):
@property
def state(self):
""" Subvolume state, one of SubvolumeStates """
raise NotImplementedError
return SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))

@property
def subvol_type(self):
Expand All @@ -123,6 +132,15 @@ def purgeable(self):
raise NotImplementedError

def load_config(self):
try:
self.fs.stat(self.legacy_config_path)
self.legacy_mode = True
except cephfs.Error as e:
pass

log.debug("loading config "
"'{0}' [mode: {1}]".format(self.subvolname, "legacy"
if self.legacy_mode else "new"))
if self.legacy_mode:
self.metadata_mgr = MetadataManager(self.fs, self.legacy_config_path, 0o640)
else:
Expand Down Expand Up @@ -271,8 +289,16 @@ def discover(self):
self.fs.stat(self.base_path)
self.metadata_mgr.refresh()
log.debug("loaded subvolume '{0}'".format(self.subvolname))
subvolpath = self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_PATH)
# subvolume with retained snapshots has empty path, don't mistake it for
# fabricated metadata.
if (not self.legacy_mode and self.state != SubvolumeStates.STATE_RETAINED and
self.base_path.decode('utf-8') != str(Path(subvolpath).parent)):
raise MetadataMgrException(-errno.ENOENT, 'fabricated .meta')
except MetadataMgrException as me:
if me.errno == -errno.ENOENT and not self.legacy_mode:
if me.errno in (-errno.ENOENT, -errno.EINVAL) and not self.legacy_mode:
log.warn("subvolume '{0}', {1}, "
"assuming legacy_mode".format(self.subvolname, me.error_str))
self.legacy_mode = True
self.load_config()
self.discover()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ def status(self):

@property
def state(self):
return SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))
return super(SubvolumeV1, self).state

@state.setter
def state(self, val):
Expand Down
4 changes: 2 additions & 2 deletions src/tools/cephfs/cephfs-shell
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ def get_chunks(file_size):
chunk_start = 0
chunk_size = 0x20000 # 131072 bytes, default max ssl buffer size
while chunk_start + chunk_size < file_size:
yield(chunk_start, chunk_size)
yield (chunk_start, chunk_size)
chunk_start += chunk_size
final_chunk_size = file_size - chunk_start
yield(chunk_start, final_chunk_size)
yield (chunk_start, final_chunk_size)


def to_bytes(param):
Expand Down

0 comments on commit 4e53463

Please sign in to comment.