1# Copyright 2012 OpenStack Foundation
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15import ddt
16import mock
17from oslo_concurrency import processutils
18
19from cinder.brick.local_dev import lvm as brick
20from cinder import exception
21from cinder import test
22from cinder.volume import configuration as conf
23
24
25@ddt.ddt
26class BrickLvmTestCase(test.TestCase):
27    def setUp(self):
28        if not hasattr(self, 'configuration'):
29            self.configuration = mock.Mock(conf.Configuration)
30            self.configuration.lvm_suppress_fd_warnings = False
31        self.configuration.volume_group_name = 'fake-vg'
32        super(BrickLvmTestCase, self).setUp()
33
34        self.mock_object(processutils, 'execute', self.fake_execute)
35        self.vg = brick.LVM(
36            self.configuration.volume_group_name,
37            'sudo',
38            False, None,
39            'default',
40            self.fake_execute,
41            suppress_fd_warn=self.configuration.lvm_suppress_fd_warnings)
42
43    def failed_fake_execute(obj, *cmd, **kwargs):
44        return ("\n", "fake-error")
45
46    def fake_pretend_lvm_version(obj, *cmd, **kwargs):
47        return ("  LVM version:     2.03.00 (2012-03-06)\n", "")
48
49    def fake_old_lvm_version(obj, *cmd, **kwargs):
50        # Does not support thin prov or snap activation
51        return ("  LVM version:     2.02.65(2) (2012-03-06)\n", "")
52
53    def fake_customised_lvm_version(obj, *cmd, **kwargs):
54        return ("  LVM version:     2.02.100(2)-RHEL6 (2013-09-12)\n", "")
55
56    def fake_execute(obj, *cmd, **kwargs):  # noqa
57        if obj.configuration.lvm_suppress_fd_warnings:
58            _lvm_prefix = 'env, LC_ALL=C, LVM_SUPPRESS_FD_WARNINGS=1, '
59        else:
60            _lvm_prefix = 'env, LC_ALL=C, '
61
62        cmd_string = ', '.join(cmd)
63        data = "\n"
64        if (_lvm_prefix + 'vgs, --noheadings, --unit=g, -o, name' ==
65                cmd_string):
66            data = "  fake-vg\n"
67            data += "  some-other-vg\n"
68        elif (_lvm_prefix + 'vgs, --noheadings, -o, name, fake-vg' ==
69                cmd_string):
70            data = "  fake-vg\n"
71        elif _lvm_prefix + 'vgs, --version' in cmd_string:
72            data = "  LVM version:     2.02.103(2) (2012-03-06)\n"
73        elif(_lvm_prefix + 'vgs, --noheadings, -o, uuid, fake-vg' in
74             cmd_string):
75            data = "  kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n"
76        elif(_lvm_prefix + 'vgs, --noheadings, --unit=g, '
77             '-o, name,size,free,lv_count,uuid, '
78             '--separator, :, --nosuffix' in cmd_string):
79            data = ("  test-prov-cap-vg-unit:10.00:10.00:0:"
80                    "mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z4\n")
81            if 'test-prov-cap-vg-unit' in cmd_string:
82                return (data, "")
83            data = ("  test-prov-cap-vg-no-unit:10.00:10.00:0:"
84                    "mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z4\n")
85            if 'test-prov-cap-vg-no-unit' in cmd_string:
86                return (data, "")
87            data = "  fake-vg:10.00:10.00:0:"\
88                   "kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n"
89            if 'fake-vg' in cmd_string:
90                return (data, "")
91            data += "  fake-vg-2:10.00:10.00:0:"\
92                    "lWyauW-dKpG-Rz7E-xtKY-jeju-QsYU-SLG7Z2\n"
93            data += "  fake-vg-3:10.00:10.00:0:"\
94                    "mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z3\n"
95        elif (_lvm_prefix + 'lvs, --noheadings, '
96              '--unit=g, -o, vg_name,name,size, --nosuffix, '
97              'fake-vg/lv-nothere' in cmd_string):
98            raise processutils.ProcessExecutionError(
99                stderr="One or more specified logical volume(s) not found.")
100        elif (_lvm_prefix + 'lvs, --noheadings, '
101              '--unit=g, -o, vg_name,name,size, --nosuffix, '
102              'fake-vg/lv-newerror' in cmd_string):
103            raise processutils.ProcessExecutionError(
104                stderr="Failed to find logical volume \"fake-vg/lv-newerror\"")
105        elif (_lvm_prefix + 'lvs, --noheadings, '
106              '--unit=g, -o, vg_name,name,size' in cmd_string):
107            if 'fake-unknown' in cmd_string:
108                raise processutils.ProcessExecutionError(
109                    stderr="One or more volume(s) not found."
110                )
111            if 'test-prov-cap-vg-unit' in cmd_string:
112                data = "  fake-vg test-prov-cap-pool-unit 9.50g\n"
113                data += "  fake-vg fake-volume-1 1.00g\n"
114                data += "  fake-vg fake-volume-2 2.00g\n"
115            elif 'test-prov-cap-vg-no-unit' in cmd_string:
116                data = "  fake-vg test-prov-cap-pool-no-unit 9.50\n"
117                data += "  fake-vg fake-volume-1 1.00\n"
118                data += "  fake-vg fake-volume-2 2.00\n"
119            elif 'test-found-lv-name' in cmd_string:
120                data = "  fake-vg test-found-lv-name 9.50\n"
121            else:
122                data = "  fake-vg fake-1 1.00g\n"
123                data += "  fake-vg fake-2 1.00g\n"
124        elif (_lvm_prefix + 'lvdisplay, --noheading, -C, -o, Attr' in
125              cmd_string):
126            if 'test-volumes' in cmd_string:
127                data = '  wi-a-'
128            elif 'snapshot' in cmd_string:
129                data = '  swi-a-s--'
130            elif 'open' in cmd_string:
131                data = '  -wi-ao---'
132            else:
133                data = '  owi-a-'
134        elif (_lvm_prefix + 'lvdisplay, --noheading, -C, -o, Origin' in
135              cmd_string):
136            if 'snapshot' in cmd_string:
137                data = '  fake-volume-1'
138            else:
139                data = '       '
140        elif _lvm_prefix + 'pvs, --noheadings' in cmd_string:
141            data = "  fake-vg|/dev/sda|10.00|1.00\n"
142            data += "  fake-vg|/dev/sdb|10.00|1.00\n"
143            data += "  fake-vg|/dev/sdc|10.00|8.99\n"
144            data += "  fake-vg-2|/dev/sdd|10.00|9.99\n"
145            if '--ignoreskippedcluster' not in cmd_string:
146                raise processutils.ProcessExecutionError(
147                    stderr="Skipping clustered volume group",
148                    stdout=data,
149                    exit_code=5
150                )
151        elif _lvm_prefix + 'lvs, --noheadings, --unit=g' \
152                ', -o, size,data_percent, --separator, :' in cmd_string:
153            if 'test-prov-cap-pool' in cmd_string:
154                data = "  9.5:20\n"
155            else:
156                data = "  9:12\n"
157        elif 'lvcreate, -T, -L, ' in cmd_string:
158            pass
159        elif 'lvcreate, -T, -V, ' in cmd_string:
160            pass
161        elif 'lvcreate, -n, ' in cmd_string:
162            pass
163        elif 'lvcreate, --name, ' in cmd_string:
164            pass
165        elif 'lvextend, -L, ' in cmd_string:
166            pass
167        else:
168            raise AssertionError('unexpected command called: %s' % cmd_string)
169
170        return (data, "")
171
172    def test_create_lv_snapshot(self):
173        self.assertIsNone(self.vg.create_lv_snapshot('snapshot-1', 'fake-1'))
174
175        with mock.patch.object(self.vg, 'get_volume', return_value=None):
176            try:
177                self.vg.create_lv_snapshot('snapshot-1', 'fake-non-existent')
178            except exception.VolumeDeviceNotFound as e:
179                self.assertEqual('fake-non-existent', e.kwargs['device'])
180            else:
181                self.fail("Exception not raised")
182
183    def test_vg_exists(self):
184        self.assertTrue(self.vg._vg_exists())
185
186    def test_get_all_volumes(self):
187        out = self.vg.get_volumes()
188
189        self.assertEqual('fake-1', out[0]['name'])
190        self.assertEqual('1.00g', out[0]['size'])
191        self.assertEqual('fake-vg', out[0]['vg'])
192
193    def test_get_volume(self):
194        self.assertEqual('fake-1', self.vg.get_volume('fake-1')['name'])
195
196    def test_get_volume_none(self):
197        self.assertIsNone(self.vg.get_volume('fake-unknown'))
198
199    def test_get_lv_info_notfound(self):
200        # lv-nothere will raise lvm < 2.102.112 exception
201        self.assertEqual(
202            [],
203            self.vg.get_lv_info(
204                'sudo', vg_name='fake-vg', lv_name='lv-nothere')
205        )
206        # lv-newerror will raise lvm > 2.102.112 exception
207        self.assertEqual(
208            [],
209            self.vg.get_lv_info(
210                'sudo', vg_name='fake-vg', lv_name='lv-newerror')
211        )
212
213    def test_get_lv_info_found(self):
214        lv_info = [{'size': '9.50', 'name': 'test-found-lv-name',
215                    'vg': 'fake-vg'}]
216        self.assertEqual(
217            lv_info,
218            self.vg.get_lv_info(
219                'sudo', vg_name='fake-vg',
220                lv_name='test-found-lv-name')
221        )
222
223    def test_get_lv_info_no_lv_name(self):
224        lv_info = [{'name': 'fake-1', 'size': '1.00g', 'vg': 'fake-vg'},
225                   {'name': 'fake-2', 'size': '1.00g', 'vg': 'fake-vg'}]
226        self.assertEqual(
227            lv_info,
228            self.vg.get_lv_info(
229                'sudo', vg_name='fake-vg')
230        )
231
232    def test_get_all_physical_volumes(self):
233        # Filtered VG version
234        pvs = self.vg.get_all_physical_volumes('sudo', 'fake-vg')
235        self.assertEqual(3, len(pvs))
236
237        # Non-Filtered, all VG's
238        pvs = self.vg.get_all_physical_volumes('sudo')
239        self.assertEqual(4, len(pvs))
240
241    def test_get_volume_groups(self):
242        self.assertEqual(3, len(self.vg.get_all_volume_groups('sudo')))
243        self.assertEqual(1,
244                         len(self.vg.get_all_volume_groups('sudo', 'fake-vg')))
245
246    def test_thin_support(self):
247        # lvm.supports_thin() is a static method and doesn't
248        # use the self._executor fake we pass in on init
249        # so we need to stub processutils.execute appropriately
250
251        self.assertTrue(self.vg.supports_thin_provisioning('sudo'))
252
253        with mock.patch.object(processutils, 'execute',
254                               self.fake_pretend_lvm_version):
255            self.assertTrue(self.vg.supports_thin_provisioning('sudo'))
256
257        with mock.patch.object(processutils, 'execute',
258                               self.fake_old_lvm_version):
259            self.assertFalse(self.vg.supports_thin_provisioning('sudo'))
260
261        with mock.patch.object(processutils, 'execute',
262                               self.fake_customised_lvm_version):
263            self.assertTrue(self.vg.supports_thin_provisioning('sudo'))
264
265    def test_snapshot_lv_activate_support(self):
266        self.vg._supports_snapshot_lv_activation = None
267        self.assertTrue(self.vg.supports_snapshot_lv_activation)
268
269        self.vg._supports_snapshot_lv_activation = None
270        with mock.patch.object(processutils, 'execute',
271                               self.fake_old_lvm_version):
272            self.assertFalse(self.vg.supports_snapshot_lv_activation)
273
274        self.vg._supports_snapshot_lv_activation = None
275
276    def test_lvchange_ignskipact_support_yes(self):
277        """Tests if lvchange -K is available via a lvm2 version check."""
278
279        self.vg._supports_lvchange_ignoreskipactivation = None
280        with mock.patch.object(processutils, 'execute',
281                               self.fake_pretend_lvm_version):
282            self.assertTrue(self.vg.supports_lvchange_ignoreskipactivation)
283
284        self.vg._supports_lvchange_ignoreskipactivation = None
285        with mock.patch.object(processutils, 'execute',
286                               self.fake_old_lvm_version):
287            self.assertFalse(self.vg.supports_lvchange_ignoreskipactivation)
288
289        self.vg._supports_lvchange_ignoreskipactivation = None
290
291    def test_pvs_ignoreskippedcluster_support(self):
292        """Tests if lvm support ignoreskippedcluster option."""
293
294        brick.LVM._supports_pvs_ignoreskippedcluster = None
295        with mock.patch.object(processutils, 'execute',
296                               self.fake_pretend_lvm_version):
297            self.assertTrue(brick.LVM.supports_pvs_ignoreskippedcluster(
298                'sudo'))
299
300        brick.LVM._supports_pvs_ignoreskippedcluster = None
301        with mock.patch.object(processutils, 'execute',
302                               self.fake_old_lvm_version):
303            self.assertFalse(brick.LVM.supports_pvs_ignoreskippedcluster(
304                'sudo'))
305
306        brick.LVM._supports_pvs_ignoreskippedcluster = None
307
308    def test_thin_pool_creation(self):
309
310        # The size of fake-vg volume group is 10g, so the calculated thin
311        # pool size should be 9.5g (95% of 10g).
312        self.assertEqual("9.5g", self.vg.create_thin_pool())
313
314        # Passing a size parameter should result in a thin pool of that exact
315        # size.
316        for size in ("1g", "1.2g", "1.75g"):
317            self.assertEqual(size, self.vg.create_thin_pool(size_str=size))
318
319    def test_thin_pool_provisioned_capacity(self):
320        self.vg.vg_thin_pool = "test-prov-cap-pool-unit"
321        self.vg.vg_name = 'test-prov-cap-vg-unit'
322        self.assertEqual(
323            "9.5g",
324            self.vg.create_thin_pool(name=self.vg.vg_thin_pool))
325        self.assertEqual(9.50, self.vg.vg_thin_pool_size)
326        self.assertEqual(7.6, self.vg.vg_thin_pool_free_space)
327        self.assertEqual(3.0, self.vg.vg_provisioned_capacity)
328
329        self.vg.vg_thin_pool = "test-prov-cap-pool-no-unit"
330        self.vg.vg_name = 'test-prov-cap-vg-no-unit'
331        self.assertEqual(
332            "9.5g",
333            self.vg.create_thin_pool(name=self.vg.vg_thin_pool))
334        self.assertEqual(9.50, self.vg.vg_thin_pool_size)
335        self.assertEqual(7.6, self.vg.vg_thin_pool_free_space)
336        self.assertEqual(3.0, self.vg.vg_provisioned_capacity)
337
338    def test_thin_pool_free_space(self):
339        # The size of fake-vg-pool is 9g and the allocated data sums up to
340        # 12% so the calculated free space should be 7.92
341        self.assertEqual(float("7.92"),
342                         self.vg._get_thin_pool_free_space("fake-vg",
343                                                           "fake-vg-pool"))
344
345    def test_volume_create_after_thin_creation(self):
346        """Test self.vg.vg_thin_pool is set to pool_name
347
348        See bug #1220286 for more info.
349        """
350        vg_name = "vg-name"
351        pool_name = vg_name + "-pool"
352        self.vg.create_thin_pool(pool_name, "1G")
353
354        with mock.patch.object(self.vg, '_execute'):
355            self.vg.create_volume("test", "1G", lv_type='thin')
356            if self.configuration.lvm_suppress_fd_warnings is False:
357                self.vg._execute.assert_called_once_with(
358                    'env', 'LC_ALL=C', 'lvcreate', '-T', '-V',
359                    '1G', '-n', 'test', 'fake-vg/vg-name-pool',
360                    root_helper='sudo', run_as_root=True)
361            else:
362                self.vg._execute.assert_called_once_with(
363                    'env', 'LC_ALL=C', 'LVM_SUPPRESS_FD_WARNINGS=1',
364                    'lvcreate', '-T', '-V', '1G', '-n', 'test',
365                    'fake-vg/vg-name-pool', root_helper='sudo',
366                    run_as_root=True)
367        self.assertEqual(pool_name, self.vg.vg_thin_pool)
368
369    def test_volume_create_when_executor_failed(self):
370        def fail(*args, **kwargs):
371            raise processutils.ProcessExecutionError()
372        self.vg._execute = fail
373
374        with mock.patch.object(self.vg, 'get_all_volume_groups') as m_gavg:
375            self.assertRaises(
376                processutils.ProcessExecutionError,
377                self.vg.create_volume, "test", "1G"
378            )
379            m_gavg.assert_called()
380
381    def test_lv_has_snapshot(self):
382        self.assertTrue(self.vg.lv_has_snapshot('fake-vg'))
383        self.assertFalse(self.vg.lv_has_snapshot('test-volumes'))
384
385    def test_lv_is_snapshot(self):
386        self.assertTrue(self.vg.lv_is_snapshot('fake-snapshot'))
387        self.assertFalse(self.vg.lv_is_snapshot('test-volumes'))
388
389    def test_lv_is_open(self):
390        self.assertTrue(self.vg.lv_is_open('fake-open'))
391        self.assertFalse(self.vg.lv_is_open('fake-snapshot'))
392
393    def test_lv_get_origin(self):
394        self.assertEqual('fake-volume-1',
395                         self.vg.lv_get_origin('fake-snapshot'))
396        self.assertIsNone(self.vg.lv_get_origin('test-volumes'))
397
398    def test_activate_lv(self):
399        with mock.patch.object(self.vg, '_execute'):
400            self.vg._supports_lvchange_ignoreskipactivation = True
401
402            self.vg._execute('lvchange', '-a', 'y', '--yes', '-K',
403                             'fake-vg/my-lv',
404                             root_helper='sudo', run_as_root=True)
405
406            self.vg.activate_lv('my-lv')
407
408    def test_get_mirrored_available_capacity(self):
409        self.assertEqual(2.0, self.vg.vg_mirror_free_space(1))
410
411    @ddt.data(True, False)
412    def test_lv_extend(self, has_snapshot):
413        with mock.patch.object(self.vg, '_execute'):
414            with mock.patch.object(self.vg, 'lv_has_snapshot'):
415                self.vg.deactivate_lv = mock.MagicMock()
416                self.vg.activate_lv = mock.MagicMock()
417
418                self.vg.lv_has_snapshot.return_value = has_snapshot
419                self.vg.extend_volume("test", "2G")
420
421                self.vg.lv_has_snapshot.assert_called_once_with("test")
422                if has_snapshot:
423                    self.vg.activate_lv.assert_called_once_with("test")
424                    self.vg.deactivate_lv.assert_called_once_with("test")
425                else:
426                    self.vg.activate_lv.assert_not_called()
427                    self.vg.deactivate_lv.assert_not_called()
428
429    def test_lv_deactivate(self):
430        with mock.patch.object(self.vg, '_execute'):
431            is_active_mock = mock.Mock()
432            is_active_mock.return_value = False
433            self.vg._lv_is_active = is_active_mock
434            self.vg.create_volume('test', '1G')
435            self.vg.deactivate_lv('test')
436
437    @mock.patch('time.sleep')
438    def test_lv_deactivate_timeout(self, _mock_sleep):
439        with mock.patch.object(self.vg, '_execute'):
440            is_active_mock = mock.Mock()
441            is_active_mock.return_value = True
442            self.vg._lv_is_active = is_active_mock
443            self.vg.create_volume('test', '1G')
444            self.assertRaises(exception.VolumeNotDeactivated,
445                              self.vg.deactivate_lv, 'test')
446
447
448class BrickLvmTestCaseIgnoreFDWarnings(BrickLvmTestCase):
449    def setUp(self):
450        self.configuration = mock.Mock(conf.Configuration)
451        self.configuration.lvm_suppress_fd_warnings = True
452        super(BrickLvmTestCaseIgnoreFDWarnings, self).setUp()
453