]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
ceph-volume: implement lvm wrapper
authorGuillaume Abrioux <gabrioux@redhat.com>
Thu, 14 Oct 2021 07:41:42 +0000 (09:41 +0200)
committerGuillaume Abrioux <gabrioux@redhat.com>
Thu, 4 Nov 2021 04:05:02 +0000 (05:05 +0100)
ceph-volume should run pv/vg/lv commands in the host namespace rather than
running them inside the container in order to avoid lvm metadata corruption.

Fixes: https://tracker.ceph.com/issues/52926
Signed-off-by: Guillaume Abrioux <gabrioux@redhat.com>
src/ceph-volume/ceph_volume/api/lvm.py
src/ceph-volume/ceph_volume/process.py
src/ceph-volume/ceph_volume/tests/api/test_lvm.py

index ca0d0e98a2142963ba2b146d7f47301e7093ab35..174bdb8d52eaa332481ca65a66eda393913ca532 100644 (file)
@@ -399,13 +399,14 @@ class PVolume(object):
         if self.tags.get(key):
             current_value = self.tags[key]
             tag = "%s=%s" % (key, current_value)
-            process.call(['pvchange', '--deltag', tag, self.pv_name])
+            process.call(['pvchange', '--deltag', tag, self.pv_name], run_on_host=True)
 
         process.call(
             [
                 'pvchange',
                 '--addtag', '%s=%s' % (key, value), self.pv_name
-            ]
+            ],
+            run_on_host=True
         )
 
 
@@ -420,7 +421,7 @@ def create_pv(device):
         '-f',  # force it
         '--yes', # answer yes to any prompts
         device
-    ])
+    ], run_on_host=True)
 
 
 def remove_pv(pv_name):
@@ -446,6 +447,7 @@ def remove_pv(pv_name):
             '-f',  # force it
             pv_name
         ],
+        run_on_host=True,
         fail_msg=fail_msg,
     )
 
@@ -470,7 +472,7 @@ def get_pvs(fields=PV_FIELDS, filters='', tags=None):
     args = ['pvs', '--noheadings', '--readonly', '--separator=";"', '-S',
             filters, '-o', fields]
 
-    stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
+    stdout, stderr, returncode = process.call(args, run_on_host=True, verbose_on_failure=False)
     pvs_report = _output_parser(stdout, fields)
     return [PVolume(**pv_report) for pv_report in pvs_report]
 
@@ -657,7 +659,8 @@ def create_vg(devices, name=None, name_prefix=None):
         'vgcreate',
         '--force',
         '--yes',
-        name] + devices
+        name] + devices,
+        run_on_host=True
     )
 
     return get_single_vg(filters={'vg_name': name})
@@ -681,7 +684,8 @@ def extend_vg(vg, devices):
         'vgextend',
         '--force',
         '--yes',
-        vg.name] + devices
+        vg.name] + devices,
+        run_on_host=True
     )
 
     return get_single_vg(filters={'vg_name': vg.name})
@@ -703,7 +707,8 @@ def reduce_vg(vg, devices):
         'vgreduce',
         '--force',
         '--yes',
-        vg.name] + devices
+        vg.name] + devices,
+        run_on_host=True
     )
 
     return get_single_vg(filter={'vg_name': vg.name})
@@ -724,6 +729,7 @@ def remove_vg(vg_name):
             '-f',  # force it
             vg_name
         ],
+        run_on_host=True,
         fail_msg=fail_msg,
     )
 
@@ -747,7 +753,7 @@ def get_vgs(fields=VG_FIELDS, filters='', tags=None):
     filters = make_filters_lvmcmd_ready(filters, tags)
     args = ['vgs'] + VG_CMD_OPTIONS + ['-S', filters, '-o', fields]
 
-    stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
+    stdout, stderr, returncode = process.call(args, run_on_host=True, verbose_on_failure=False)
     vgs_report =_output_parser(stdout, fields)
     return [VolumeGroup(**vg_report) for vg_report in vgs_report]
 
@@ -772,6 +778,7 @@ def get_single_vg(fields=VG_FIELDS, filters=None, tags=None):
 def get_device_vgs(device, name_prefix=''):
     stdout, stderr, returncode = process.call(
         ['pvs'] + VG_CMD_OPTIONS + ['-o', VG_FIELDS, device],
+        run_on_host=True,
         verbose_on_failure=False
     )
     vgs = _output_parser(stdout, VG_FIELDS)
@@ -860,7 +867,7 @@ class Volume(object):
             return
         del_tag_args = self._format_tag_args('--deltag', del_tags)
         # --deltag returns successful even if the to be deleted tag is not set
-        process.call(['lvchange'] + del_tag_args + [self.lv_path])
+        process.call(['lvchange'] + del_tag_args + [self.lv_path], run_on_host=True)
         for k in del_tags.keys():
             del self.tags[k]
 
@@ -879,7 +886,7 @@ class Volume(object):
         """
         self.clear_tags(tags.keys())
         add_tag_args = self._format_tag_args('--addtag', tags)
-        process.call(['lvchange'] + add_tag_args + [self.lv_path])
+        process.call(['lvchange'] + add_tag_args + [self.lv_path], run_on_host=True)
         for k, v in tags.items():
             self.tags[k] = v
 
@@ -888,7 +895,7 @@ class Volume(object):
         if self.tags.get(key):
             current_value = self.tags[key]
             tag = "%s=%s" % (key, current_value)
-            process.call(['lvchange', '--deltag', tag, self.lv_path])
+            process.call(['lvchange', '--deltag', tag, self.lv_path], run_on_host=True)
             del self.tags[key]
 
 
@@ -903,7 +910,8 @@ class Volume(object):
             [
                 'lvchange',
                 '--addtag', '%s=%s' % (key, value), self.lv_path
-            ]
+            ],
+            run_on_host=True
         )
         self.tags[key] = value
 
@@ -911,7 +919,7 @@ class Volume(object):
         """
         Deactivate the LV by calling lvchange -an
         """
-        process.call(['lvchange', '-an', self.lv_path])
+        process.call(['lvchange', '-an', self.lv_path], run_on_host=True)
 
 
 def create_lv(name_prefix,
@@ -984,7 +992,7 @@ def create_lv(name_prefix,
             '100%FREE',
             '-n', name, vg.vg_name
         ]
-    process.run(command)
+    process.run(command, run_on_host=True)
 
     lv = get_single_lv(filters={'lv_name': name, 'vg_name': vg.vg_name})
 
@@ -1079,6 +1087,7 @@ def remove_lv(lv):
             '-f',  # force it
             path
         ],
+        run_on_host=True,
         show_command=True,
         terminal_verbose=True,
     )
@@ -1106,7 +1115,7 @@ def get_lvs(fields=LV_FIELDS, filters='', tags=None):
     filters = make_filters_lvmcmd_ready(filters, tags)
     args = ['lvs'] + LV_CMD_OPTIONS + ['-S', filters, '-o', fields]
 
-    stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
+    stdout, stderr, returncode = process.call(args, run_on_host=True, verbose_on_failure=False)
     lvs_report = _output_parser(stdout, fields)
     return [Volume(**lv_report) for lv_report in lvs_report]
 
@@ -1140,6 +1149,7 @@ def get_lv_by_name(name):
     stdout, stderr, returncode = process.call(
         ['lvs', '--noheadings', '-o', LV_FIELDS, '-S',
          'lv_name={}'.format(name)],
+        run_on_host=True,
         verbose_on_failure=False
     )
     lvs = _output_parser(stdout, LV_FIELDS)
@@ -1150,6 +1160,7 @@ def get_lvs_by_tag(lv_tag):
     stdout, stderr, returncode = process.call(
         ['lvs', '--noheadings', '--separator=";"', '-a', '-o', LV_FIELDS, '-S',
          'lv_tags={{{}}}'.format(lv_tag)],
+        run_on_host=True,
         verbose_on_failure=False
     )
     lvs = _output_parser(stdout, LV_FIELDS)
@@ -1159,6 +1170,7 @@ def get_lvs_by_tag(lv_tag):
 def get_device_lvs(device, name_prefix=''):
     stdout, stderr, returncode = process.call(
         ['pvs'] + LV_CMD_OPTIONS + ['-o', LV_FIELDS, device],
+        run_on_host=True,
         verbose_on_failure=False
     )
     lvs = _output_parser(stdout, LV_FIELDS)
index e70986892b7fd578dc80b9c928b9e380f7b596ba..dd8e0dae8cd6d3cc09122bc1a11d2424d0e2f2e4 100644 (file)
@@ -1,5 +1,5 @@
 from fcntl import fcntl, F_GETFL, F_SETFL
-from os import O_NONBLOCK, read
+from os import O_NONBLOCK, read, path
 import subprocess
 from select import select
 from ceph_volume import terminal
@@ -8,7 +8,15 @@ from ceph_volume.util import as_bytes
 import logging
 
 logger = logging.getLogger(__name__)
-
+host_rootfs = '/rootfs'
+run_host_cmd = [
+        'nsenter',
+        '--root={}'.format(host_rootfs),
+        '--mount={}/proc/1/ns/mnt'.format(host_rootfs),
+        '--ipc={}/proc/1/ns/ipc'.format(host_rootfs),
+        '--net={}/proc/1/ns/net'.format(host_rootfs),
+        '--uts={}/proc/1/ns/uts'.format(host_rootfs)
+]
 
 def which(executable):
     """
@@ -103,7 +111,7 @@ def obfuscate(command_, on=None):
     return "Running command: %s" % ' '.join(command)
 
 
-def run(command, **kw):
+def run(command, run_on_host=False, **kw):
     """
     A real-time-logging implementation of a remote subprocess.Popen call where
     a command is just executed on the remote end and no other handling is done.
@@ -114,6 +122,8 @@ def run(command, **kw):
     """
     executable = which(command.pop(0))
     command.insert(0, executable)
+    if run_on_host and path.isdir(host_rootfs):
+        command = run_host_cmd + command
     stop_on_error = kw.pop('stop_on_error', True)
     command_msg = obfuscate(command, kw.pop('obfuscate', None))
     fail_msg = kw.pop('fail_msg', None)
@@ -157,7 +167,7 @@ def run(command, **kw):
             logger.warning(msg)
 
 
-def call(command, **kw):
+def call(command, run_on_host=False, **kw):
     """
     Similar to ``subprocess.Popen`` with the following changes:
 
@@ -182,6 +192,8 @@ def call(command, **kw):
     """
     executable = which(command.pop(0))
     command.insert(0, executable)
+    if run_on_host and path.isdir(host_rootfs):
+        command = run_host_cmd + command
     terminal_verbose = kw.pop('terminal_verbose', False)
     logfile_verbose = kw.pop('logfile_verbose', True)
     verbose_on_failure = kw.pop('verbose_on_failure', True)
index 16e586e02b22a86aedbe4cfe21fd5b48930d99da..0bfc34075c31c41b8569f5be57d357e4b906f044 100644 (file)
@@ -195,8 +195,8 @@ class TestCreateLV(object):
     def test_uses_size(self, m_get_single_lv, m_call, m_run, monkeypatch):
         m_get_single_lv.return_value = self.foo_volume
         api.create_lv('foo', 0, vg=self.foo_group, size=419430400, tags={'ceph.type': 'data'})
-        expected = ['lvcreate', '--yes', '-l', '100', '-n', 'foo-0', 'foo_group']
-        m_run.assert_called_with(expected)
+        expected = (['lvcreate', '--yes', '-l', '100', '-n', 'foo-0', 'foo_group'])
+        m_run.assert_called_with(expected, run_on_host=True)
 
     @patch('ceph_volume.api.lvm.process.run')
     @patch('ceph_volume.api.lvm.process.call')
@@ -211,7 +211,7 @@ class TestCreateLV(object):
         # 423624704 should be just under 1% off of the available size 419430400
         api.create_lv('foo', 0, vg=foo_group, size=4232052736, tags={'ceph.type': 'data'})
         expected = ['lvcreate', '--yes', '-l', '1000', '-n', 'foo-0', 'foo_group']
-        m_run.assert_called_with(expected)
+        m_run.assert_called_with(expected, run_on_host=True)
 
     @patch('ceph_volume.api.lvm.process.run')
     @patch('ceph_volume.api.lvm.process.call')
@@ -228,7 +228,7 @@ class TestCreateLV(object):
         m_get_single_lv.return_value = self.foo_volume
         api.create_lv('foo', 0, vg=self.foo_group, extents='50', tags={'ceph.type': 'data'})
         expected = ['lvcreate', '--yes', '-l', '50', '-n', 'foo-0', 'foo_group']
-        m_run.assert_called_with(expected)
+        m_run.assert_called_with(expected, run_on_host=True)
 
     @pytest.mark.parametrize("test_input,expected",
                              [(2, 50),
@@ -240,7 +240,7 @@ class TestCreateLV(object):
         m_get_single_lv.return_value = self.foo_volume
         api.create_lv('foo', 0, vg=self.foo_group, slots=test_input, tags={'ceph.type': 'data'})
         expected = ['lvcreate', '--yes', '-l', str(expected), '-n', 'foo-0', 'foo_group']
-        m_run.assert_called_with(expected)
+        m_run.assert_called_with(expected, run_on_host=True)
 
     @patch('ceph_volume.api.lvm.process.run')
     @patch('ceph_volume.api.lvm.process.call')
@@ -249,7 +249,7 @@ class TestCreateLV(object):
         m_get_single_lv.return_value = self.foo_volume
         api.create_lv('foo', 0, vg=self.foo_group, tags={'ceph.type': 'data'})
         expected = ['lvcreate', '--yes', '-l', '100%FREE', '-n', 'foo-0', 'foo_group']
-        m_run.assert_called_with(expected)
+        m_run.assert_called_with(expected, run_on_host=True)
 
     @patch('ceph_volume.api.lvm.process.run')
     @patch('ceph_volume.api.lvm.process.call')