diff --git a/PendingReleaseNotes b/PendingReleaseNotes index e23c9eb08b1..ff7a4a00af1 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -91,6 +91,13 @@ of the system. * CephFS: Full support for subvolumes and subvolume groups is now available for snap_schedule Manager module. +* CephFS: Two FS names can now be swapped, optionally along with their IDs, + using "ceph fs swap" command. The function of this API is to facilitate + file system swaps for disaster recovery. In particular, it avoids situations + where a named file system is temporarily missing which would prompt a higher + level storage operator (like Rook) to recreate the missing file system. + See https://docs.ceph.com/en/latest/cephfs/administration/#file-systems + docs for more information. * CephFS: The `subvolume snapshot clone` command now depends on the config option `snapshot_clone_no_wait` which is used to reject the clone operation when diff --git a/doc/cephfs/administration.rst b/doc/cephfs/administration.rst index cd912b42aea..dd9a8e25b16 100644 --- a/doc/cephfs/administration.rst +++ b/doc/cephfs/administration.rst @@ -92,6 +92,46 @@ The CephX IDs authorized to the old file system name need to be reauthorized to the new name. Any on-going operations of the clients using these IDs may be disrupted. Mirroring is expected to be disabled on the file system. +:: + + fs swap <fs1-name> <fs1_id> <fs2-name> <fs2_id> [--swap-fscids=yes|no] [--yes-i-really-mean-it] + +Swaps names of two Ceph file sytems and updates the application tags on all +pools of both FSs accordingly. Certain tools that track FSCIDs of the file +systems, besides the FS names, might get confused due to this operation. For +this reason, mandatory option ``--swap-fscids`` has been provided that must be +used to indicate whether or not FSCIDs must be swapped. + +.. note:: FSCID stands for "File System Cluster ID". + +Before the swap, mirroring should be disabled on both the CephFSs +(because the cephfs-mirror daemon uses the fscid internally and changing it +while the daemon is running could result in undefined behaviour), both the +CephFSs should be offline and the file system flag ``refuse_client_sessions`` +must be set for both the CephFS. + +The function of this API is to facilitate disaster recovery where a new file +system reconstructed from the previous one is ready to take over for the +possibly damaged file system. Instead of two ``fs rename`` operations, the +operator can use a swap so there is no FSMap epoch where the primary (or +production) named file system does not exist. This is important when Ceph is +monitored by automatic storage operators like (Rook) which try to reconcile +the storage system continuously. That operator may attempt to recreate the +file system as soon as it is seen to not exist. + +After the swap, CephX credentials may need to be reauthorized if the existing +mounts should "follow" the old file system to its new name. Generally, for +disaster recovery, its desirable for the existing mounts to continue using +the same file system name. Any active file system mounts for either CephFSs +must remount. Existing unflushed operations will be lost. When it is judged +that one of the swapped file systems is ready for clients, run:: + + ceph fs set <fs> joinable true + ceph fs set <fs> refuse_client_sessions false + +Keep in mind that one of the swapped file systems may be left offline for +future analysis if doing a disaster recovery swap. + Settings -------- diff --git a/doc/man/8/ceph.rst b/doc/man/8/ceph.rst index 881c0d3077b..6487b2ca8f6 100644 --- a/doc/man/8/ceph.rst +++ b/doc/man/8/ceph.rst @@ -23,7 +23,7 @@ Synopsis | **ceph** **df** *{detail}* -| **ceph** **fs** [ *ls* \| *new* \| *reset* \| *rm* \| *authorize* ] ... +| **ceph** **fs** [ *ls* \| *new* \| *reset* \| *rm* \| *authorize* \| *swap* ] ... | **ceph** **fsid** @@ -393,6 +393,15 @@ Usage:: ceph fs authorize <fs_name> client.<client_id> <path> <perms> [<path> <perms>...] +Subcommand ``swap`` swaps the names of two Ceph file system and updates +application tags on the pool of the file systems accordingly. Optionally, +FSIDs of the filesystems can also be swapped along with names by passing +``--swap-fscids``. + +Usage:: + + ceph fs swap <fs1-name> <fs1-id> <fs2-name> <fs2-id> [--swap-fscids] {--yes-i-really-meant-it} + fsid ---- diff --git a/qa/tasks/cephfs/test_admin.py b/qa/tasks/cephfs/test_admin.py index 252d85c7644..3e00b8be028 100644 --- a/qa/tasks/cephfs/test_admin.py +++ b/qa/tasks/cephfs/test_admin.py @@ -821,6 +821,813 @@ class TestRenameCommand(TestAdminCommands): self.run_ceph_cmd(f'fs mirror disable {orig_fs_name}') +class TestSwapCmd(TestAdminCommands): + """ + Tests for "ceph fs swap" command. + """ + + MDSS_REQUIRED = 3 + CLIENTS_REQUIRED = 2 + client_id = 'testuser' + client_name = f'client.{client_id}' + + def setUp(self): + super(self.__class__, self).setUp() + self.fs1 = self.fs + self.fs2 = self.mds_cluster.newfs(name='testcephfs2', create=True) + self.fs1.getinfo() + self.fs2.getinfo() + self.orig_fs_id_name = {self.fs1.id: self.fs1.name, + self.fs2.id: self.fs2.name} + + self.mount_a.remount(cephfs_name=self.fs1.name) + self.mount_b.remount(cephfs_name=self.fs2.name) + + self.captesters = (CapTester(self.mount_a), CapTester(self.mount_b)) + + self.mount_a.umount_wait() + self.mount_b.umount_wait() + + def tearDown(self): + self.mount_a.umount_wait() + self.mount_b.umount_wait() + self.run_ceph_cmd(args=f'auth rm {self.client_name}') + super(self.__class__, self).tearDown() + + def _reauthorize_client(self): + moncap = gen_mon_cap_str((("rw", self.fs1.name), + ("rw", self.fs2.name))) + osdcap = gen_osd_cap_str((("rw", self.fs1.name), + ("rw", self.fs2.name))) + mdscap = gen_mds_cap_str((("rw", self.fs1.name), + ("rw", self.fs2.name))) + self.run_ceph_cmd(args=f'auth add {self.client_name} mon "{moncap}" ' + f'osd "{osdcap}" mds "{mdscap}"') + + def _remount_both_cephfss(self): + keyring = self.fs.mon_manager.get_keyring(self.client_id) + '\n' + keyring_path_a = self.mount_a.client_remote.mktemp(data=keyring) + keyring_path_b = self.mount_b.client_remote.mktemp(data=keyring) + + self.mount_a.mount(client_id=self.client_id, + client_keyring_path=keyring_path_a, + cephfs_mntpt='/', cephfs_name=self.fs1.name) + self.mount_b.mount(client_id=self.client_id, + client_keyring_path=keyring_path_b, + cephfs_mntpt='/', cephfs_name=self.fs2.name) + + def run_rw_tests(self): + for captester in self.captesters: + captester.conduct_pos_test_for_read_caps() + captester.conduct_pos_test_for_write_caps() + + def _check_fs_name_on_fs_pools(self, fss): + for fs in fss: + for pool in fs.get_data_pool_names(True): + self.check_pool_application_metadata_key_value(pool, 'cephfs', + 'data', fs.name) + self.check_pool_application_metadata_key_value( + fs.get_metadata_pool_name(), 'cephfs', 'metadata', fs.name) + + def _are_fsnames_and_fscids_together(self): + ''' + Are FS names and FSIDs together on same the FS as they were before + running "ceph fs swap" command? + ''' + fs1_id_swapped = self.orig_fs_id_name[self.fs1.id] == self.fs1.name + fs2_id_swapped = self.orig_fs_id_name[self.fs2.id] == self.fs2.name + + if fs1_id_swapped and fs2_id_swapped: + return True + elif not fs1_id_swapped and not fs2_id_swapped: + return False + else: + raise RuntimeError( + 'Unexpected situation occured: FSID for one FS swapped but ' + 'not for the other FS.') + + def _bring_both_cephfss_down(self): + self.run_ceph_cmd(f'fs fail {self.fs1.name}') + self.run_ceph_cmd(f'fs fail {self.fs2.name}') + + def _bring_both_cephfss_up(self): + self.run_ceph_cmd(f'fs set {self.fs1.name} joinable true') + self.run_ceph_cmd(f'fs set {self.fs2.name} joinable true') + + def _refuse_clients_for_both_cephfss(self): + self.run_ceph_cmd(f'fs set {self.fs1.name} refuse_client_session true') + self.run_ceph_cmd(f'fs set {self.fs2.name} refuse_client_session true') + + def _accept_clients_for_both_cephfss(self): + self.run_ceph_cmd(f'fs set {self.fs1.name} refuse_client_session ' + 'false') + self.run_ceph_cmd(f'fs set {self.fs2.name} refuse_client_session ' + 'false') + + def test_swap_fsnames_but_not_fscids(self): + ''' + Test that "ceph fs swap --swap-fscids=no" swaps the FS names but not + the FSCIDs. + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + # log output to help debug test failures + self.run_ceph_cmd('fs', 'dump') + + self.run_ceph_cmd(f'fs swap {self.fs1.name} {self.fs1.id} ' + f'{self.fs2.name} {self.fs2.id} --swap-fscids=no ' + '--yes_i_really_mean_it') + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # FS names were swapped but not FSIDs, so both can't be together + self.assertEqual(self._are_fsnames_and_fscids_together(), False) + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + self.run_rw_tests() + + def test_swap_fsnames_and_fscids(self): + ''' + Test that "ceph fs swap --swap-fscids=yes" swaps the FS names as well + as the FSCIDs. + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.run_ceph_cmd(f'fs swap {self.fs1.name} {self.fs1.id} ' + f'{self.fs2.name} {self.fs2.id} --swap-fscids=yes ' + '--yes_i_really_mean_it') + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + # XXX: Let's ensure that FS mounted on a mountpoint is same before + # and after swapping of FS name and FSCIDs. This ensures that data + # available on mountpoints before and after the swap is same. This + # prevents self.run_rw_tests() from breaking. + # + # At the beginning of test, testcephfs1 has data (let's say) 'abc1' + # and testcephfs2 has data 'abc2'. self.fs1 is mapped to testcephfs1 + # and self.fs2 mapped to testcephfs2. After swap, data of testcephfs1 + # and testcephfs2 will be 'abc2' and 'abc1' respectively. + # + # However, running self.fs1.getinfo() after swap will map self.fs1 to + # FS with FSCID 1 i.e. testcephfs1 and not testcephfs2. Thus, data + # under self.fs1 will be different than data before swapping. This + # breaks self.run_rw_tests() because self.fs1 is always mounted on + # the mountpoint of self.mount_a. + + # To prevent this, therefore, make sure that data on + # self.fs1/self.mount_a is same after and before the swap. To ensure + # this, swap FS that is represented by self.fs1. Instead of + # testcephfs1 it should be testcephfs2 because, after swap, + # testcephfs2 containts the data of testcephfs1. This will ensure that + # self.mount_rw_tests doesn't break. + # + # Same for self.fs2. + self.fs1.id, self.fs2.id = None, None + self.fs1.name, self.fs2.name = self.fs2.name, self.fs1.name + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # both FS name and FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + self.run_rw_tests() + + def test_swap_without_confirmation_option(self): + ''' + Test that "ceph fs swap --swap-fscids=yes" without the option + "--yes-i-really-mean-it" fails. + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = ('This is a potentially disruptive operation, client\'s cephx ' + 'credentials may need to be reauthorized to access the file ' + 'systems and its pools. Add --yes-i-really-mean-it if you are ' + 'sure you wish to continue.') + self.negtest_ceph_cmd(f'fs swap {self.fs1.name} {self.fs1.id} ' + f'{self.fs2.name} {self.fs2.id} ' + '--swap-fscids=no', + retval=errno.EPERM, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + ################################################### + # + # Tests for "fs swap" when either FS name is false. + # + ################################################### + + def test_swap_when_fs1_is_absent(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when there is no CephFS on cluster by the name "<fs1name>". + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + absent_cephfs = 'random_fsname_654' + msg = (f"File system '{absent_cephfs}' doesn't exist on this Ceph " + "cluster") + self.negtest_ceph_cmd( + args=(f'fs swap {absent_cephfs} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.ENOENT, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_fs2_is_absent(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when there is no CephFS on cluster by the name "<fs2name>". + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + absent_cephfs = 'random_fsname_654' + msg = (f"File system '{absent_cephfs}' doesn't exist on this Ceph " + "cluster") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs2.id} {absent_cephfs} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.ENOENT, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_both_fss_are_absent(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when there are no CephFSs on the cluster by the name "<fs1name>" and + "<fs2name>". + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + absent_cephfs1 = 'random_fsname_65' + absent_cephfs2 = 'random_fsname_66' + msg = (f"Neither file system '{absent_cephfs1}' nor file system " + f"'{absent_cephfs2}' exists on this Ceph cluster") + self.negtest_ceph_cmd( + args=(f'fs swap {absent_cephfs1} 123 {absent_cephfs2} 1234 ' + '--swap-fscids=no --yes_i_really_mean_it'), + retval=errno.ENOENT, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + ################################################### + # + # Tests for "fs swap" when either FSCID is wrong. + # + ################################################### + + def test_swap_when_fs1_id_is_wrong(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when "<fs1id>" is not the FSCID of the CephFS named "<fs1nmae>". + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"FSCID provided for '{self.fs1.name}' is incorrect.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} 123 {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EINVAL, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_fs2_id_is_wrong(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when "<fs2id>" is not the FSCID of the CephFS named "<fs2nmae>". + ''' + self._bring_both_cephfss_down() + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"FSCID provided for '{self.fs2.name}' is incorrect.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'123 --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EINVAL, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_both_fscids_are_wrong(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when "<fs1id>" and "<fs2id>", respectively, are not the FSCIDs of the + CephFSs named "<fs1name>" and "<fs2nmae>". + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = ('FSCIDs provided for both the CephFSs is incorrect.') + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} 123 {self.fs2.name} 1234 ' + f'--swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EINVAL, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_user_swaps_fscids_in_cmd_args(self): + ''' + Test that "ceph fs swap" fails and prints relevant error message when + FSCIDs are exchange while writing the command. That is user write the + command as - + + "ceph fs swap <fs1name> <fs2id> <fs2name> <fs1id>" + + instead of writing - + + "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" + ''' + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = ('FSCIDs provided in command arguments are swapped; perhaps ' + '`ceph fs swap` has been run before.') + proc = self.run_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs2.id} {self.fs2.name} ' + f'{self.fs1.id} --swap-fscids=no --yes_i_really_mean_it'), + stderr=StringIO()) + self.assertIn(msg.lower(), proc.stderr.getvalue().lower()) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + ##################################################### + # + # Tests for "fs swap" when mirroring is enabled on FS + # + ##################################################### + + def test_swap_when_mirroring_enabled_for_1st_FS(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when mirroring is enabled for the CephFS named "<fs1name>". + ''' + self.run_ceph_cmd(f'fs mirror enable {self.fs1.name}') + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"Mirroring is enabled on file system '{self.fs1.name}'. " + "Disable mirroring on the file system after ensuring it's " + "OK to do so, and then re-try swapping.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + self.run_ceph_cmd(f'fs mirror disable {self.fs1.name}') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_mirroring_enabled_for_2nd_FS(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when mirroring is enabled for the CephFS named "<fs2name>". + ''' + self.run_ceph_cmd(f'fs mirror enable {self.fs2.name}') + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + + self.run_ceph_cmd('fs', 'dump') + msg = (f"Mirroring is enabled on file system '{self.fs2.name}'. " + "Disable mirroring on the file system after ensuring it's " + "OK to do so, and then re-try swapping.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + self.run_ceph_cmd(f'fs mirror disable {self.fs2.name}') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_mirroring_enabled_for_both_FSs(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" fails + when mirroring is enabled for both the CephFSs. + ''' + self.run_ceph_cmd(f'fs mirror enable {self.fs1.name}') + self.run_ceph_cmd(f'fs mirror enable {self.fs2.name}') + self._bring_both_cephfss_down() + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"Mirroring is enabled on file systems '{self.fs1.name}' and " + f"'{self.fs2.name}'. Disable mirroring on both the file " + "systems after ensuring it's OK to do so, and then re-try " + "swapping.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self._bring_both_cephfss_up() + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + self.run_ceph_cmd(f'fs mirror disable {self.fs1.name}') + self.run_ceph_cmd(f'fs mirror disable {self.fs2.name}') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + ########################################################## + # + # Tests for "fs swap" when either FS is not down/failed. + # + ######################################################### + + def test_swap_when_fs1_is_online(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" when + CephFS named "<fs1name>" is online (i.e. is not failed). + ''' + self.run_ceph_cmd(f'fs fail {self.fs2.name}') + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"CephFS '{self.fs1.name}' is not offline. Before swapping " + "CephFS names, both CephFSs should be marked as failed." + "See `ceph fs fail`.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self.run_ceph_cmd(f'fs set {self.fs2.name} joinable true') + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_fs2_is_not_down(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" when + CephFS named "<fs2name>" is online (i.e. is not failed). + ''' + self.run_ceph_cmd(f'fs fail {self.fs1.name}') + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"CephFS '{self.fs2.name}' is not offline. Before swapping " + "CephFS names, both CephFSs should be marked as failed. " + "See `ceph fs fail`.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self.run_ceph_cmd(f'fs set {self.fs1.name} joinable true') + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_both_FSs_are_not_down(self): + ''' + Test that "ceph fs swap <fs1name> <fs1id> <fs2name> <fs2id>" when + both the CephFSs are online (i.e. is not failed). + ''' + self._refuse_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"CephFSs '{self.fs1.name}' and '{self.fs2.name}' are not " + "offline. Before swapping CephFS names, both CephFSs should " + "be marked as failed. See `ceph fs fail`.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self._accept_clients_for_both_cephfss() + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + ################################################## + # + # Tests for "fs swap" when either FS is offline. + # + ################################################ + + + def test_swap_when_FS1_doesnt_refuse_clients(self): + ''' + Test that the command "ceph fs swap" command fails when + "refuse_client_session" is not set for the first of the two of FSs . + ''' + self._bring_both_cephfss_down() + self.run_ceph_cmd(f'fs set {self.fs2.name} refuse_client_session true') + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"CephFS '{self.fs1.name}' doesn't refuse clients. Before " + "swapping CephFS names, flag 'refuse_client_session' must " + "be set. See `ceph fs set`.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self._bring_both_cephfss_up() + self.run_ceph_cmd(f'fs set {self.fs2.name} refuse_client_session ' + 'false') + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_FS2_doesnt_refuse_clients(self): + ''' + Test that the command "ceph fs swap" command fails when + "refuse_client_session" is not set for the second of the two of FSs . + ''' + self._bring_both_cephfss_down() + self.run_ceph_cmd(f'fs set {self.fs1.name} refuse_client_session true') + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + msg = (f"CephFS '{self.fs2.name}' doesn't refuse clients. Before " + "swapping CephFS names, flag 'refuse_client_session' must " + "be set. See `ceph fs set`.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + + self._bring_both_cephfss_up() + self.run_ceph_cmd(f'fs set {self.fs1.name} refuse_client_session ' + 'false') + sleep(5) + self.run_ceph_cmd('fs', 'dump') + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + def test_swap_when_both_FSs_do_not_refuse_clients(self): + ''' + Test that the command "ceph fs swap" command fails when + "refuse_client_session" is not set for both the CephFSs. + ''' + self.run_ceph_cmd('fs', 'dump') + self._bring_both_cephfss_down() + sleep(5) + msg = (f"CephFSs '{self.fs1.name}' and '{self.fs2.name}' do not " + "refuse clients. Before swapping CephFS names, flag " + "'refuse_client_session' must be set. See `ceph fs set`.") + self.negtest_ceph_cmd( + args=(f'fs swap {self.fs1.name} {self.fs1.id} {self.fs2.name} ' + f'{self.fs2.id} --swap-fscids=no --yes_i_really_mean_it'), + retval=errno.EPERM, errmsgs=msg) + self._bring_both_cephfss_up() + self.run_ceph_cmd('fs', 'dump') + sleep(5) + + self.fs1.getinfo() + self.fs2.getinfo() + self._reauthorize_client() + self._remount_both_cephfss() + + # check that content of both CephFSs is unaffected by this failure. + self.run_rw_tests() + self._check_fs_name_on_fs_pools((self.fs1, self.fs2)) + # neither FS name nor FSIDs were swapped, so both must be together + self.assertEqual(self._are_fsnames_and_fscids_together(), True) + + class TestDump(CephFSTestCase): CLIENTS_REQUIRED = 0 MDSS_REQUIRED = 1 diff --git a/src/mds/FSMap.cc b/src/mds/FSMap.cc index 55b87a43b12..a862b0236e0 100644 --- a/src/mds/FSMap.cc +++ b/src/mds/FSMap.cc @@ -1219,3 +1219,20 @@ void FSMap::erase_filesystem(fs_cluster_id_t fscid) } } } + +void FSMap::swap_fscids(fs_cluster_id_t fscid1, fs_cluster_id_t fscid2) +{ + auto fs1 = std::move(filesystems.at(fscid1)); + filesystems[fscid1] = std::move(filesystems.at(fscid2)); + filesystems[fscid2] = std::move(fs1); + + auto set_fs1_fscid = [fscid1](auto&& fs) { + fs->set_fscid(fscid1); + }; + modify_filesystem(fscid1, std::move(set_fs1_fscid)); + + auto set_fs2_fscid = [fscid2](auto&& fs) { + fs->set_fscid(fscid2); + }; + modify_filesystem(fscid2, std::move(set_fs2_fscid)); +} diff --git a/src/mds/FSMap.h b/src/mds/FSMap.h index f57a4177aa0..85d53ef8336 100644 --- a/src/mds/FSMap.h +++ b/src/mds/FSMap.h @@ -236,9 +236,41 @@ public: return false; } + const auto& get_mirror_info() const + { + return mirror_info; + } + + auto& get_mirror_info() + { + return mirror_info; + } + + const auto& get_mds_map() const + { + return mds_map; + } + + auto& get_mds_map() + { + return mds_map; + } + + auto get_fscid() const + { + return fscid; + } + fs_cluster_id_t fscid = FS_CLUSTER_ID_NONE; MDSMap mds_map; MirrorInfo mirror_info; + +private: + friend class FSMap; + + void set_fscid(fs_cluster_id_t new_fscid) { + fscid = new_fscid; + } }; WRITE_CLASS_ENCODER_FEATURES(Filesystem) @@ -455,6 +487,11 @@ public: fs->mds_map.epoch = epoch; } + /* This is method is written for the option of "ceph fs swap" commmand + * that intiates swap of FSCIDs. + */ + void swap_fscids(fs_cluster_id_t fscid1, fs_cluster_id_t fscid2); + /** * Apply a mutation to the mds_info_t structure for a particular * daemon (identified by GID), and make appropriate updates to epochs. diff --git a/src/mon/FSCommands.cc b/src/mon/FSCommands.cc index 05d28cc4c1d..4e19d4a4658 100644 --- a/src/mon/FSCommands.cc +++ b/src/mon/FSCommands.cc @@ -1292,6 +1292,207 @@ private: Paxos *m_paxos; }; +class SwapFilesystemHandler : public FileSystemCommandHandler +{ + public: + explicit SwapFilesystemHandler(Paxos *paxos) + : FileSystemCommandHandler("fs swap"), m_paxos(paxos) + { + } + + int handle(Monitor *mon, FSMap& fsmap, MonOpRequestRef op, + const cmdmap_t& cmdmap, std::ostream &ss) override + { + ceph_assert(m_paxos->is_plugged()); + + // Check for confirmation flag + bool confirmation_flag = false; + cmd_getval(cmdmap, "yes_i_really_mean_it", confirmation_flag); + if (!confirmation_flag) { + ss << "This is a potentially disruptive operation, client\'s cephx " + "credentials may need to be reauthorized to access the file systems " + "and its pools. Add --yes-i-really-mean-it if you are sure you wish " + "to continue."; + return -EPERM; + } + + string fs1_name, fs2_name; + int64_t fs1_id = FS_CLUSTER_ID_NONE; + int64_t fs2_id = FS_CLUSTER_ID_NONE; + string swap_fscids_flag; + cmd_getval(cmdmap, "fs1_name", fs1_name); + cmd_getval(cmdmap, "fs2_name", fs2_name); + cmd_getval(cmdmap, "fs1_id", fs1_id); + cmd_getval(cmdmap, "fs2_id", fs2_id); + cmd_getval(cmdmap, "swap_fscids", swap_fscids_flag); + auto fs1p = fsmap.get_filesystem(fs1_name); + auto fs2p = fsmap.get_filesystem(fs2_name); + + // Check that CephFSs exists for both given names. + if (fs1p == nullptr || fs2p == nullptr) { + if (fs1p == nullptr && fs2p != nullptr) { + ss << "File system '" << fs1_name << "' doesn\'t exist on this " + "Ceph cluster."; + return -ENOENT; + } else if (fs1p != nullptr && fs2p == nullptr) { + ss << "File system '" << fs2_name << "' doesn\'t exist on this " + "Ceph cluster."; + return -ENOENT; + } else { + ss << "Neither file system '" << fs1_name << "' nor file " + "system '" << fs2_name << "' exists on this Ceph cluster."; + return -ENOENT; + } + } + + // Check that FSCID provided for both CephFSs is correct. + if (fs1_id != fs1p->get_fscid() || fs2_id != fs2p->get_fscid()) { + if (fs1_id != fs1p->get_fscid() && fs2_id == fs2p->get_fscid()) { + ss << "FSCID provided for '" << fs1_name << "' is incorrect."; + return -EINVAL; + } else if (fs1_id == fs1p->get_fscid() && fs2_id != fs2p->get_fscid()) { + ss << "FSCID provided for '" << fs2_name << "' is incorrect."; + return -EINVAL; + } else if (fs1_id != fs1p->get_fscid() && fs2_id != fs2p->get_fscid()) { + if (fs1_id == fs2p->get_fscid() && fs2_id == fs1p->get_fscid()) { + ss << "FSCIDs provided in command arguments are swapped; perhaps " + << "`ceph fs swap` has been run before."; + return 0; + } else { + ss << "FSCIDs provided for both the CephFSs is incorrect."; + return -EINVAL; + } + } + } + + // Check that CephFS mirroring for both CephFSs is disabled. + if (fs1p->get_mirror_info().mirrored || fs2p->get_mirror_info().mirrored) { + if (fs1p->get_mirror_info().mirrored && + !fs2p->get_mirror_info().mirrored) { + ss << "Mirroring is enabled on file system '"<< fs1_name << "'. " + << "Disable mirroring on the file system after ensuring it's OK " + << "to do so, and then re-try swapping."; + return -EPERM; + } else if (!fs1p->get_mirror_info().mirrored && + fs2p->get_mirror_info().mirrored) { + ss << "Mirroring is enabled on file system '"<< fs2_name << "'. " + << "Disable mirroring on the file system after ensuring it's OK " + << "to do so, and then re-try swapping."; + return -EPERM; + } else { + ss << "Mirroring is enabled on file systems '" << fs1_name << "' " + << "and '" << fs2_name << "'. Disable mirroring on both the " + << "file systems after ensuring it's OK to do so, and then re-try " + << "swapping."; + return -EPERM; + } + } + + if (!mon->osdmon()->is_writeable()) { + // not allowed to write yet, so retry when we can + mon->osdmon()->wait_for_writeable( + op, new PaxosService::C_RetryMessage(mon->mdsmon(), op)); + return -EAGAIN; + } + + // Check that both CephFS have been marked as down, IOW has no MDS + // associated with it. + if (fs1p->get_mds_map().get_num_up_mds() > 0 || + fs2p->get_mds_map().get_num_up_mds() > 0) { + if (fs1p->get_mds_map().get_num_up_mds() > 0 && + fs2p->get_mds_map().get_num_up_mds() == 0) { + ss << "CephFS '" << fs1_name << "' is not offline. Before swapping " + << "CephFS names, both CephFSs should be marked as failed. See " + << "`ceph fs fail`."; + return -EPERM; + } else if (fs1p->get_mds_map().get_num_up_mds() == 0 && + fs2p->get_mds_map().get_num_up_mds() > 0) { + ss << "CephFS '" << fs2_name << "' is not offline. Before swapping " + << "CephFS names, both CephFSs should be marked as failed. See " + << "`ceph fs fail`."; + return -EPERM; + } else { + ss << "CephFSs '" << fs1_name << "' and '" << fs2_name << "' " + << "are not offline. Before swapping CephFS names, both CephFSs " + << "should be marked as failed. See `ceph fs fail`."; + return -EPERM; + } + } + + // Check that refuse_client_session is set. + if (!fs1p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION) || + !fs2p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION)) { + if (!fs1p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION) && + fs2p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION)) { + ss << "CephFS '" << fs1_name << "' doesn't refuse clients. Before " + << "swapping CephFS names, flag 'refuse_client_session' must be " + << "set. See `ceph fs set`."; + return -EPERM; + } else if ( + fs1p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION) && + !fs2p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION)) { + ss << "CephFS '" << fs2_name << "' doesn't refuse clients. Before " + << "swapping CephFS names, flag 'refuse_client_session' must be " + << "set. See `ceph fs set`."; + return -EPERM; + } else if ( + !fs1p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION) && + !fs2p->get_mds_map().test_flag(CEPH_MDSMAP_REFUSE_CLIENT_SESSION)) { + ss << "CephFSs '" << fs1_name << "' and '" << fs2_name << "' do not " + << "refuse clients. Before swapping CephFS names, flag " + << "'refuse_client_session' must be set. See `ceph fs set`."; + return -EPERM; + } + } + + // Finally, the swap begins. + // Swap CephFS names on OSD pool application tag + for (const auto p : fs1p->get_mds_map().get_data_pools()) { + mon->osdmon()->do_application_enable(p, + pg_pool_t::APPLICATION_NAME_CEPHFS, + "data", fs2_name, true); + } + mon->osdmon()->do_application_enable(fs1p->get_mds_map().get_metadata_pool(), + pg_pool_t::APPLICATION_NAME_CEPHFS, + "metadata", fs2_name, true); + + for (const auto p : fs2p->get_mds_map().get_data_pools()) { + mon->osdmon()->do_application_enable(p, + pg_pool_t::APPLICATION_NAME_CEPHFS, + "data", fs1_name, true); + } + mon->osdmon()->do_application_enable(fs2p->get_mds_map().get_metadata_pool(), + pg_pool_t::APPLICATION_NAME_CEPHFS, + "metadata", fs1_name, true); + mon->osdmon()->propose_pending(); + + // Now swap CephFS names and, optionally, FSCIDs. + auto renamefunc1 = [fs2_name](auto&& fs) { + fs->get_mds_map().set_fs_name(fs2_name); + }; + fsmap.modify_filesystem(fs1_id, std::move(renamefunc1)); + auto renamefunc2 = [fs1_name](auto&& fs) { + fs->get_mds_map().set_fs_name(fs1_name); + }; + fsmap.modify_filesystem(fs2_id, std::move(renamefunc2)); + + if (swap_fscids_flag == "yes") { + fsmap.swap_fscids(fs1_id, fs2_id); + } + + ss << "File system names "; + if (swap_fscids_flag == "yes") { + ss << "and FSCIDs "; + } + ss << " have been swapped; cephx credentials may need an upgrade."; + + return 0; + } + +private: + Paxos *m_paxos; +}; + class RemoveDataPoolHandler : public FileSystemCommandHandler { public: @@ -1607,6 +1808,7 @@ FileSystemCommandHandler::load(Paxos *paxos) handlers.push_back(std::make_shared<RemoveFilesystemHandler>()); handlers.push_back(std::make_shared<ResetFilesystemHandler>()); handlers.push_back(std::make_shared<RenameFilesystemHandler>(paxos)); + handlers.push_back(std::make_shared<SwapFilesystemHandler>(paxos)); handlers.push_back(std::make_shared<SetDefaultHandler>()); handlers.push_back(std::make_shared<AliasHandler<SetDefaultHandler> >( @@ -1745,7 +1947,8 @@ int FileSystemCommandHandler::is_op_allowed( if (fs == nullptr) { auto prefix = get_prefix(); /* let "fs rm" and "fs rename" handle idempotent cases where file systems do not exist */ - if (!(prefix == "fs rm" || prefix == "fs rename") && fsmap.get_filesystem(fs_name) == nullptr) { + if (!(prefix == "fs rm" || prefix == "fs rename" || prefix == "fs swap") && + fsmap.get_filesystem(fs_name) == nullptr) { ss << "Filesystem not found: '" << fs_name << "'"; return -ENOENT; } diff --git a/src/mon/MonCommands.h b/src/mon/MonCommands.h index 58e30f64ac3..cb412c1494c 100644 --- a/src/mon/MonCommands.h +++ b/src/mon/MonCommands.h @@ -459,6 +459,14 @@ COMMAND("fs rename " "name=new_fs_name,type=CephString,goodchars=" FS_NAME_GOODCHARS " name=yes_i_really_mean_it,type=CephBool,req=false", "rename a ceph file system", "mds", "rw") +COMMAND("fs swap " + "name=fs1_name,type=CephString " + "name=fs1_id,type=CephInt,range=0 " + "name=fs2_name,type=CephString " + "name=fs2_id,type=CephInt,range=0 " + "name=swap_fscids,type=CephChoices,strings=yes|no,req=true " + "name=yes_i_really_mean_it,type=CephBool,req=false", + "swap ceph file system names", "mds", "rw") /* * Monmap commands