Add the gnt-storage client
[ganeti-local] / test / ganeti.bdev_unittest.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2012 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Script for unittesting the bdev module"""
23
24
25 import os
26 import unittest
27
28 from ganeti import bdev
29 from ganeti import errors
30 from ganeti import constants
31 from ganeti import utils
32 from ganeti import pathutils
33
34 import testutils
35
36
37 class TestBaseDRBD(testutils.GanetiTestCase):
38   def testGetVersion(self):
39     data = [
40       ["version: 8.0.12 (api:76/proto:86-91)"],
41       ["version: 8.2.7 (api:88/proto:0-100)"],
42       ["version: 8.3.7.49 (api:188/proto:13-191)"],
43     ]
44     result = [
45       {
46       "k_major": 8,
47       "k_minor": 0,
48       "k_point": 12,
49       "api": 76,
50       "proto": 86,
51       "proto2": "91",
52       },
53       {
54       "k_major": 8,
55       "k_minor": 2,
56       "k_point": 7,
57       "api": 88,
58       "proto": 0,
59       "proto2": "100",
60       },
61       {
62       "k_major": 8,
63       "k_minor": 3,
64       "k_point": 7,
65       "api": 188,
66       "proto": 13,
67       "proto2": "191",
68       }
69     ]
70     for d,r in zip(data, result):
71       self.assertEqual(bdev.BaseDRBD._GetVersion(d), r)
72
73
74 class TestDRBD8Runner(testutils.GanetiTestCase):
75   """Testing case for DRBD8"""
76
77   @staticmethod
78   def _has_disk(data, dname, mname):
79     """Check local disk corectness"""
80     retval = (
81       "local_dev" in data and
82       data["local_dev"] == dname and
83       "meta_dev" in data and
84       data["meta_dev"] == mname and
85       "meta_index" in data and
86       data["meta_index"] == 0
87       )
88     return retval
89
90   @staticmethod
91   def _has_net(data, local, remote):
92     """Check network connection parameters"""
93     retval = (
94       "local_addr" in data and
95       data["local_addr"] == local and
96       "remote_addr" in data and
97       data["remote_addr"] == remote
98       )
99     return retval
100
101   def testParserCreation(self):
102     """Test drbdsetup show parser creation"""
103     bdev.DRBD8._GetShowParser()
104
105   def testParser80(self):
106     """Test drbdsetup show parser for disk and network version 8.0"""
107     data = self._ReadTestData("bdev-drbd-8.0.txt")
108     result = bdev.DRBD8._GetDevInfo(data)
109     self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
110                                    "/dev/xenvg/test.meta"),
111                     "Wrong local disk info")
112     self.failUnless(self._has_net(result, ("192.0.2.1", 11000),
113                                   ("192.0.2.2", 11000)),
114                     "Wrong network info (8.0.x)")
115
116   def testParser83(self):
117     """Test drbdsetup show parser for disk and network version 8.3"""
118     data = self._ReadTestData("bdev-drbd-8.3.txt")
119     result = bdev.DRBD8._GetDevInfo(data)
120     self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
121                                    "/dev/xenvg/test.meta"),
122                     "Wrong local disk info")
123     self.failUnless(self._has_net(result, ("192.0.2.1", 11000),
124                                   ("192.0.2.2", 11000)),
125                     "Wrong network info (8.0.x)")
126
127   def testParserNetIP4(self):
128     """Test drbdsetup show parser for IPv4 network"""
129     data = self._ReadTestData("bdev-drbd-net-ip4.txt")
130     result = bdev.DRBD8._GetDevInfo(data)
131     self.failUnless(("local_dev" not in result and
132                      "meta_dev" not in result and
133                      "meta_index" not in result),
134                     "Should not find local disk info")
135     self.failUnless(self._has_net(result, ("192.0.2.1", 11002),
136                                   ("192.0.2.2", 11002)),
137                     "Wrong network info (IPv4)")
138
139   def testParserNetIP6(self):
140     """Test drbdsetup show parser for IPv6 network"""
141     data = self._ReadTestData("bdev-drbd-net-ip6.txt")
142     result = bdev.DRBD8._GetDevInfo(data)
143     self.failUnless(("local_dev" not in result and
144                      "meta_dev" not in result and
145                      "meta_index" not in result),
146                     "Should not find local disk info")
147     self.failUnless(self._has_net(result, ("2001:db8:65::1", 11048),
148                                   ("2001:db8:66::1", 11048)),
149                     "Wrong network info (IPv6)")
150
151   def testParserDisk(self):
152     """Test drbdsetup show parser for disk"""
153     data = self._ReadTestData("bdev-drbd-disk.txt")
154     result = bdev.DRBD8._GetDevInfo(data)
155     self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
156                                    "/dev/xenvg/test.meta"),
157                     "Wrong local disk info")
158     self.failUnless(("local_addr" not in result and
159                      "remote_addr" not in result),
160                     "Should not find network info")
161
162   def testBarriersOptions(self):
163     """Test class method that generates drbdsetup options for disk barriers"""
164     # Tests that should fail because of wrong version/options combinations
165     should_fail = [
166       (8, 0, 12, "bfd", True),
167       (8, 0, 12, "fd", False),
168       (8, 0, 12, "b", True),
169       (8, 2, 7, "bfd", True),
170       (8, 2, 7, "b", True)
171     ]
172
173     for vmaj, vmin, vrel, opts, meta in should_fail:
174       self.assertRaises(errors.BlockDeviceError,
175                         bdev.DRBD8._ComputeDiskBarrierArgs,
176                         vmaj, vmin, vrel, opts, meta)
177
178     # get the valid options from the frozenset(frozenset()) in constants.
179     valid_options = [list(x)[0] for x in constants.DRBD_VALID_BARRIER_OPT]
180
181     # Versions that do not support anything
182     for vmaj, vmin, vrel in ((8, 0, 0), (8, 0, 11), (8, 2, 6)):
183       for opts in valid_options:
184         self.assertRaises(errors.BlockDeviceError,
185                           bdev.DRBD8._ComputeDiskBarrierArgs,
186                           vmaj, vmin, vrel, opts, True)
187
188     # Versions with partial support (testing only options that are supported)
189     tests = [
190       (8, 0, 12, "n", False, []),
191       (8, 0, 12, "n", True, ["--no-md-flushes"]),
192       (8, 2, 7, "n", False, []),
193       (8, 2, 7, "fd", False, ["--no-disk-flushes", "--no-disk-drain"]),
194       (8, 0, 12, "n", True, ["--no-md-flushes"]),
195       ]
196
197     # Versions that support everything
198     for vmaj, vmin, vrel in ((8, 3, 0), (8, 3, 12)):
199       tests.append((vmaj, vmin, vrel, "bfd", True,
200                     ["--no-disk-barrier", "--no-disk-drain",
201                      "--no-disk-flushes", "--no-md-flushes"]))
202       tests.append((vmaj, vmin, vrel, "n", False, []))
203       tests.append((vmaj, vmin, vrel, "b", True,
204                     ["--no-disk-barrier", "--no-md-flushes"]))
205       tests.append((vmaj, vmin, vrel, "fd", False,
206                     ["--no-disk-flushes", "--no-disk-drain"]))
207       tests.append((vmaj, vmin, vrel, "n", True, ["--no-md-flushes"]))
208
209     # Test execution
210     for test in tests:
211       vmaj, vmin, vrel, disabled_barriers, disable_meta_flush, expected = test
212       args = \
213         bdev.DRBD8._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
214                                            disabled_barriers,
215                                            disable_meta_flush)
216       self.failUnless(set(args) == set(expected),
217                       "For test %s, got wrong results %s" % (test, args))
218
219     # Unsupported or invalid versions
220     for vmaj, vmin, vrel in ((0, 7, 25), (9, 0, 0), (7, 0, 0), (8, 4, 0)):
221       self.assertRaises(errors.BlockDeviceError,
222                         bdev.DRBD8._ComputeDiskBarrierArgs,
223                         vmaj, vmin, vrel, "n", True)
224
225     # Invalid options
226     for option in ("", "c", "whatever", "nbdfc", "nf"):
227       self.assertRaises(errors.BlockDeviceError,
228                         bdev.DRBD8._ComputeDiskBarrierArgs,
229                         8, 3, 11, option, True)
230
231
232 class TestDRBD8Status(testutils.GanetiTestCase):
233   """Testing case for DRBD8 /proc status"""
234
235   def setUp(self):
236     """Read in txt data"""
237     testutils.GanetiTestCase.setUp(self)
238     proc_data = self._TestDataFilename("proc_drbd8.txt")
239     proc80e_data = self._TestDataFilename("proc_drbd80-emptyline.txt")
240     proc83_data = self._TestDataFilename("proc_drbd83.txt")
241     proc83_sync_data = self._TestDataFilename("proc_drbd83_sync.txt")
242     proc83_sync_krnl_data = \
243       self._TestDataFilename("proc_drbd83_sync_krnl2.6.39.txt")
244     self.proc_data = bdev.DRBD8._GetProcData(filename=proc_data)
245     self.proc80e_data = bdev.DRBD8._GetProcData(filename=proc80e_data)
246     self.proc83_data = bdev.DRBD8._GetProcData(filename=proc83_data)
247     self.proc83_sync_data = bdev.DRBD8._GetProcData(filename=proc83_sync_data)
248     self.proc83_sync_krnl_data = \
249       bdev.DRBD8._GetProcData(filename=proc83_sync_krnl_data)
250     self.mass_data = bdev.DRBD8._MassageProcData(self.proc_data)
251     self.mass80e_data = bdev.DRBD8._MassageProcData(self.proc80e_data)
252     self.mass83_data = bdev.DRBD8._MassageProcData(self.proc83_data)
253     self.mass83_sync_data = bdev.DRBD8._MassageProcData(self.proc83_sync_data)
254     self.mass83_sync_krnl_data = \
255       bdev.DRBD8._MassageProcData(self.proc83_sync_krnl_data)
256
257   def testIOErrors(self):
258     """Test handling of errors while reading the proc file."""
259     temp_file = self._CreateTempFile()
260     os.unlink(temp_file)
261     self.failUnlessRaises(errors.BlockDeviceError,
262                           bdev.DRBD8._GetProcData, filename=temp_file)
263
264   def testHelper(self):
265     """Test reading usermode_helper in /sys."""
266     sys_drbd_helper = self._TestDataFilename("sys_drbd_usermode_helper.txt")
267     drbd_helper = bdev.DRBD8.GetUsermodeHelper(filename=sys_drbd_helper)
268     self.failUnlessEqual(drbd_helper, "/bin/true")
269
270   def testHelperIOErrors(self):
271     """Test handling of errors while reading usermode_helper in /sys."""
272     temp_file = self._CreateTempFile()
273     os.unlink(temp_file)
274     self.failUnlessRaises(errors.BlockDeviceError,
275                           bdev.DRBD8.GetUsermodeHelper, filename=temp_file)
276
277   def testMinorNotFound(self):
278     """Test not-found-minor in /proc"""
279     self.failUnless(9 not in self.mass_data)
280     self.failUnless(9 not in self.mass83_data)
281     self.failUnless(3 not in self.mass80e_data)
282
283   def testLineNotMatch(self):
284     """Test wrong line passed to DRBD8Status"""
285     self.assertRaises(errors.BlockDeviceError, bdev.DRBD8Status, "foo")
286
287   def testMinor0(self):
288     """Test connected, primary device"""
289     for data in [self.mass_data, self.mass83_data]:
290       stats = bdev.DRBD8Status(data[0])
291       self.failUnless(stats.is_in_use)
292       self.failUnless(stats.is_connected and stats.is_primary and
293                       stats.peer_secondary and stats.is_disk_uptodate)
294
295   def testMinor1(self):
296     """Test connected, secondary device"""
297     for data in [self.mass_data, self.mass83_data]:
298       stats = bdev.DRBD8Status(data[1])
299       self.failUnless(stats.is_in_use)
300       self.failUnless(stats.is_connected and stats.is_secondary and
301                       stats.peer_primary and stats.is_disk_uptodate)
302
303   def testMinor2(self):
304     """Test unconfigured device"""
305     for data in [self.mass_data, self.mass83_data, self.mass80e_data]:
306       stats = bdev.DRBD8Status(data[2])
307       self.failIf(stats.is_in_use)
308
309   def testMinor4(self):
310     """Test WFconn device"""
311     for data in [self.mass_data, self.mass83_data]:
312       stats = bdev.DRBD8Status(data[4])
313       self.failUnless(stats.is_in_use)
314       self.failUnless(stats.is_wfconn and stats.is_primary and
315                       stats.rrole == "Unknown" and
316                       stats.is_disk_uptodate)
317
318   def testMinor6(self):
319     """Test diskless device"""
320     for data in [self.mass_data, self.mass83_data]:
321       stats = bdev.DRBD8Status(data[6])
322       self.failUnless(stats.is_in_use)
323       self.failUnless(stats.is_connected and stats.is_secondary and
324                       stats.peer_primary and stats.is_diskless)
325
326   def testMinor8(self):
327     """Test standalone device"""
328     for data in [self.mass_data, self.mass83_data]:
329       stats = bdev.DRBD8Status(data[8])
330       self.failUnless(stats.is_in_use)
331       self.failUnless(stats.is_standalone and
332                       stats.rrole == "Unknown" and
333                       stats.is_disk_uptodate)
334
335   def testDRBD83SyncFine(self):
336     stats = bdev.DRBD8Status(self.mass83_sync_data[3])
337     self.failUnless(stats.is_in_resync)
338     self.failUnless(stats.sync_percent is not None)
339
340   def testDRBD83SyncBroken(self):
341     stats = bdev.DRBD8Status(self.mass83_sync_krnl_data[3])
342     self.failUnless(stats.is_in_resync)
343     self.failUnless(stats.sync_percent is not None)
344
345
346 class TestRADOSBlockDevice(testutils.GanetiTestCase):
347   def test_ParseRbdShowmappedOutput(self):
348     volume_name = "abc9778-8e8ace5b.rbd.disk0"
349     output_ok = \
350       ("0\trbd\te69f28e5-9817.rbd.disk0\t-\t/dev/rbd0\n"
351        "1\t/dev/rbd0\tabc9778-8e8ace5b.rbd.disk0\t-\t/dev/rbd16\n"
352        "line\twith\tfewer\tfields\n"
353        "")
354     output_empty = ""
355     output_no_matches = \
356       ("0\trbd\te69f28e5-9817.rbd.disk0\t-\t/dev/rbd0\n"
357        "1\trbd\tabcdef01-9817.rbd.disk0\t-\t/dev/rbd10\n"
358        "2\trbd\tcdef0123-9817.rbd.disk0\t-\t/dev/rbd12\n"
359        "something\twith\tfewer\tfields"
360        "")
361     output_extra_matches = \
362       ("0\t/dev/rbd0\tabc9778-8e8ace5b.rbd.disk0\t-\t/dev/rbd11\n"
363        "1\trbd\te69f28e5-9817.rbd.disk0\t-\t/dev/rbd0\n"
364        "2\t/dev/rbd0\tabc9778-8e8ace5b.rbd.disk0\t-\t/dev/rbd16\n"
365        "something\twith\tfewer\tfields"
366        "")
367
368     parse_function = bdev.RADOSBlockDevice._ParseRbdShowmappedOutput
369     self.assertEqual(parse_function(output_ok, volume_name), "/dev/rbd16")
370     self.assertRaises(errors.BlockDeviceError, parse_function,
371                       output_empty, volume_name)
372     self.assertEqual(parse_function(output_no_matches, volume_name), None)
373     self.assertRaises(errors.BlockDeviceError, parse_function,
374                       output_extra_matches, volume_name)
375
376
377 class TestComputeWrongFileStoragePathsInternal(unittest.TestCase):
378   def testPaths(self):
379     paths = bdev._GetForbiddenFileStoragePaths()
380
381     for path in ["/bin", "/usr/local/sbin", "/lib64", "/etc", "/sys"]:
382       self.assertTrue(path in paths)
383
384     self.assertEqual(set(map(os.path.normpath, paths)), paths)
385
386   def test(self):
387     vfsp = bdev._ComputeWrongFileStoragePaths
388     self.assertEqual(vfsp([]), [])
389     self.assertEqual(vfsp(["/tmp"]), [])
390     self.assertEqual(vfsp(["/bin/ls"]), ["/bin/ls"])
391     self.assertEqual(vfsp(["/bin"]), ["/bin"])
392     self.assertEqual(vfsp(["/usr/sbin/vim", "/srv/file-storage"]),
393                      ["/usr/sbin/vim"])
394
395
396 class TestComputeWrongFileStoragePaths(testutils.GanetiTestCase):
397   def test(self):
398     tmpfile = self._CreateTempFile()
399
400     utils.WriteFile(tmpfile, data="""
401       /tmp
402       x/y///z/relative
403       # This is a test file
404       /srv/storage
405       /bin
406       /usr/local/lib32/
407       relative/path
408       """)
409
410     self.assertEqual(bdev.ComputeWrongFileStoragePaths(_filename=tmpfile), [
411       "/bin",
412       "/usr/local/lib32",
413       "relative/path",
414       "x/y/z/relative",
415       ])
416
417
418 class TestCheckFileStoragePathInternal(unittest.TestCase):
419   def testNonAbsolute(self):
420     for i in ["", "tmp", "foo/bar/baz"]:
421       self.assertRaises(errors.FileStoragePathError,
422                         bdev._CheckFileStoragePath, i, ["/tmp"])
423
424     self.assertRaises(errors.FileStoragePathError,
425                       bdev._CheckFileStoragePath, "/tmp", ["tmp", "xyz"])
426
427   def testNoAllowed(self):
428     self.assertRaises(errors.FileStoragePathError,
429                       bdev._CheckFileStoragePath, "/tmp", [])
430
431   def testNoAdditionalPathComponent(self):
432     self.assertRaises(errors.FileStoragePathError,
433                       bdev._CheckFileStoragePath, "/tmp/foo", ["/tmp/foo"])
434
435   def testAllowed(self):
436     bdev._CheckFileStoragePath("/tmp/foo/a", ["/tmp/foo"])
437     bdev._CheckFileStoragePath("/tmp/foo/a/x", ["/tmp/foo"])
438
439
440 class TestCheckFileStoragePath(testutils.GanetiTestCase):
441   def testNonExistantFile(self):
442     filename = "/tmp/this/file/does/not/exist"
443     assert not os.path.exists(filename)
444     self.assertRaises(errors.FileStoragePathError,
445                       bdev.CheckFileStoragePath, "/bin/", _filename=filename)
446     self.assertRaises(errors.FileStoragePathError,
447                       bdev.CheckFileStoragePath, "/srv/file-storage",
448                       _filename=filename)
449
450   def testAllowedPath(self):
451     tmpfile = self._CreateTempFile()
452
453     utils.WriteFile(tmpfile, data="""
454       /srv/storage
455       """)
456
457     bdev.CheckFileStoragePath("/srv/storage/inst1", _filename=tmpfile)
458
459     # No additional path component
460     self.assertRaises(errors.FileStoragePathError,
461                       bdev.CheckFileStoragePath, "/srv/storage",
462                       _filename=tmpfile)
463
464     # Forbidden path
465     self.assertRaises(errors.FileStoragePathError,
466                       bdev.CheckFileStoragePath, "/usr/lib64/xyz",
467                       _filename=tmpfile)
468
469
470 class TestLoadAllowedFileStoragePaths(testutils.GanetiTestCase):
471   def testDevNull(self):
472     self.assertEqual(bdev._LoadAllowedFileStoragePaths("/dev/null"), [])
473
474   def testNonExistantFile(self):
475     filename = "/tmp/this/file/does/not/exist"
476     assert not os.path.exists(filename)
477     self.assertEqual(bdev._LoadAllowedFileStoragePaths(filename), [])
478
479   def test(self):
480     tmpfile = self._CreateTempFile()
481
482     utils.WriteFile(tmpfile, data="""
483       # This is a test file
484       /tmp
485       /srv/storage
486       relative/path
487       """)
488
489     self.assertEqual(bdev._LoadAllowedFileStoragePaths(tmpfile), [
490       "/tmp",
491       "/srv/storage",
492       "relative/path",
493       ])
494
495
496 if __name__ == "__main__":
497   testutils.GanetiTestProgram()