Merge lp:~danilo/maas/virsh-storage-units-trunk into lp:~maas-committers/maas/trunk

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 6095
Proposed branch: lp:~danilo/maas/virsh-storage-units-trunk
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 235 lines (+123/-12)
4 files modified
src/provisioningserver/drivers/pod/tests/test_virsh.py (+23/-0)
src/provisioningserver/drivers/pod/virsh.py (+17/-12)
src/provisioningserver/utils/__init__.py (+45/-0)
src/provisioningserver/utils/tests/test_utils.py (+38/-0)
To merge this branch: bzr merge lp:~danilo/maas/virsh-storage-units-trunk
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+325920@code.launchpad.net

Commit message

Consider size units for storage when evaluating virsh pod storage.

Description of the change

This changes .get_key_value() on the Virsh driver to not strip the unit, and introduces a "_unitless" variant that still does that. This highlights a few potential future problems as well (memory size in KiB, CPU speed in Mhz, if those units change, we'll have similar problems).

I wonder why did we not end up using libvirt instead which would provide us stable units, but I imagine they might not be py3 bindings available or something along those lines.

Testing instructions:

I have not tested this yet (I only have one system where I have >1TiB LVM volume), so it seems easier to test this with a small storage partition instead (eg. MiB to ensure that still works), but generally you need to:

 - set up a libvirt on a system and add a storage pool of >1TiB
 - ensure "virsh pool-info" lists the storage pool details in TiB
 - add a pod to MAAS and look at available storage: it should still be correct

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

My mistake; I commented on the 2.2 branch instead of this one.

See comments here.

https://code.launchpad.net/~danilo/maas/virsh-storage-units/+merge/325913

(Usually we land a branch on trunk first and then self-approve backports.)

Revision history for this message
Mike Pontillo (mpontillo) :
review: Needs Fixing
Revision history for this message
Данило Шеган (danilo) wrote :

I'll work on moving the helper and adding the unit tests as you suggested later: marking as WIP for now.

FWIW, Dmitrii has confirmed in the bug report that this solves the problem for him.

Revision history for this message
Данило Шеган (danilo) wrote :

I've addressed your review comments: moved the function into provisioningserver.utils.convert_size_to_bytes and added unit tests (thanks for raising this, I was going to add them then I got side-tracked filing other bugs as offshoots of the original one, and it slipped!).

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks great. Thanks for the fixes.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/drivers/pod/tests/test_virsh.py'
--- src/provisioningserver/drivers/pod/tests/test_virsh.py 2017-05-19 03:37:56 +0000
+++ src/provisioningserver/drivers/pod/tests/test_virsh.py 2017-06-20 12:52:44 +0000
@@ -105,6 +105,17 @@
105 Available: 173.84 GiB105 Available: 173.84 GiB
106 """)106 """)
107107
108SAMPLE_POOLINFO_TB = dedent("""
109 Name: default
110 UUID: 59edc0cb-4635-449a-80e2-2c8a59afa327
111 State: running
112 Persistent: yes
113 Autostart: yes
114 Capacity: 2.21 TiB
115 Allocation: 880.64 GiB
116 Available: 1.35 TiB
117 """)
118
108SAMPLE_IFLIST = dedent("""119SAMPLE_IFLIST = dedent("""
109 Interface Type Source Model MAC120 Interface Type Source Model MAC
110 -------------------------------------------------------121 -------------------------------------------------------
@@ -506,6 +517,18 @@
506 for pool in pools_mock.return_value517 for pool in pools_mock.return_value
507 }, expected)518 }, expected)
508519
520 def test_get_pod_pool_size_map_terabytes(self):
521 conn = self.configure_virshssh(SAMPLE_POOLINFO_TB)
522 pools_mock = self.patch(virsh.VirshSSH, 'list_pools')
523 pools_mock.return_value = [
524 factory.make_name('pool') for _ in range(3)]
525 expected = conn.get_pod_pool_size_map('Capacity')
526 capacity = int(2.21 * 2**40)
527 self.assertEqual({
528 pool: capacity
529 for pool in pools_mock.return_value
530 }, expected)
531
509 def test_get_pod_local_storage(self):532 def test_get_pod_local_storage(self):
510 conn = self.configure_virshssh(SAMPLE_POOLINFO)533 conn = self.configure_virshssh(SAMPLE_POOLINFO)
511 pools_mock = self.patch(virsh.VirshSSH, 'list_pools')534 pools_mock = self.patch(virsh.VirshSSH, 'list_pools')
512535
=== modified file 'src/provisioningserver/drivers/pod/virsh.py'
--- src/provisioningserver/drivers/pod/virsh.py 2017-05-19 03:57:03 +0000
+++ src/provisioningserver/drivers/pod/virsh.py 2017-06-20 12:52:44 +0000
@@ -37,6 +37,7 @@
37 create_node,37 create_node,
38)38)
39from provisioningserver.utils import (39from provisioningserver.utils import (
40 convert_size_to_bytes,
40 shell,41 shell,
41 typed,42 typed,
42)43)
@@ -273,7 +274,13 @@
273 data = data.strip().splitlines()274 data = data.strip().splitlines()
274 for d in data:275 for d in data:
275 if key == d.split(':')[0].strip():276 if key == d.split(':')[0].strip():
276 return d.split(':')[1].split()[0]277 return d.split(':')[1].strip()
278
279 def get_key_value_unitless(self, data, key):
280 """Return value based off of key with unit (if any) stripped off."""
281 value = self.get_key_value(data, key)
282 if value:
283 return value.split()[0]
277284
278 def list_machines(self):285 def list_machines(self):
279 """Lists all VMs by name."""286 """Lists all VMs by name."""
@@ -336,7 +343,7 @@
336 if output is None:343 if output is None:
337 maaslog.error("Failed to get pod CPU speed")344 maaslog.error("Failed to get pod CPU speed")
338 return None345 return None
339 return int(self.get_key_value(output, "CPU frequency"))346 return int(self.get_key_value_unitless(output, "CPU frequency"))
340347
341 def get_pod_memory(self):348 def get_pod_memory(self):
342 """Gets the total memory of the pod."""349 """Gets the total memory of the pod."""
@@ -344,7 +351,7 @@
344 if output is None:351 if output is None:
345 maaslog.error("Failed to get pod memory")352 maaslog.error("Failed to get pod memory")
346 return None353 return None
347 KiB = int(self.get_key_value(output, "Memory size"))354 KiB = int(self.get_key_value_unitless(output, "Memory size"))
348 # Memory in MiB.355 # Memory in MiB.
349 return int(KiB / 1024)356 return int(KiB / 1024)
350357
@@ -354,7 +361,7 @@
354 if output is None:361 if output is None:
355 maaslog.error("%s: Failed to get machine memory", machine)362 maaslog.error("%s: Failed to get machine memory", machine)
356 return None363 return None
357 KiB = int(self.get_key_value(output, "Max memory"))364 KiB = int(self.get_key_value_unitless(output, "Max memory"))
358 # Memory in MiB.365 # Memory in MiB.
359 return int(KiB / 1024)366 return int(KiB / 1024)
360367
@@ -366,10 +373,8 @@
366 if output is None:373 if output is None:
367 # Skip if cannot get more information.374 # Skip if cannot get more information.
368 continue375 continue
369 capacity = float(376 pools[pool] = convert_size_to_bytes(
370 self.get_key_value(output, key))377 self.get_key_value(output, "Capacity"))
371 # Convert GiB to bytes.
372 pools[pool] = int(capacity * 2**30)
373 return pools378 return pools
374379
375 def get_pod_local_storage(self):380 def get_pod_local_storage(self):
@@ -390,10 +395,10 @@
390 maaslog.error(395 maaslog.error(
391 "Failed to get available pod local storage")396 "Failed to get available pod local storage")
392 return None397 return None
393 local_storage += float(self.get_key_value(398 local_storage += convert_size_to_bytes(
394 output, "Available"))399 self.get_key_value(output, "Available"))
395 # Local storage in bytes. GiB to bytes.400 # Local storage in bytes.
396 return int(local_storage * 2**30)401 return local_storage
397402
398 def get_machine_local_storage(self, machine, device):403 def get_machine_local_storage(self, machine, device):
399 """Gets the VM local storage for device."""404 """Gets the VM local storage for device."""
400405
=== modified file 'src/provisioningserver/utils/__init__.py'
--- src/provisioningserver/utils/__init__.py 2017-05-15 17:24:04 +0000
+++ src/provisioningserver/utils/__init__.py 2017-06-20 12:52:44 +0000
@@ -245,3 +245,48 @@
245 return issubclass(test, query_tuple)245 return issubclass(test, query_tuple)
246 except TypeError:246 except TypeError:
247 return False247 return False
248
249
250# Capacity units supported by convert_size_to_bytes() function.
251CAPACITY_UNITS = {
252 "KiB": 2 ** 10,
253 "MiB": 2 ** 20,
254 "GiB": 2 ** 30,
255 "TiB": 2 ** 40,
256 "PiB": 2 ** 50,
257 "EiB": 2 ** 60,
258 "ZiB": 2 ** 70,
259 "YiB": 2 ** 80,
260}
261
262
263class UnknownCapacityUnitError(Exception):
264 """Unknown capacity unit used."""
265
266
267def convert_size_to_bytes(value):
268 """
269 Converts storage size values with units (GiB, TiB...) to bytes.
270
271 :param value: A string containing a number and unit separated by at least
272 one space character. If unit is not specified, defaults to bytes.
273 :return: An integer indicating the number of bytes for the given value in
274 any other size unit.
275 :raises UnknownCapacityUnitError: unsupported capacity unit.
276 """
277 # Split value on the first space.
278 capacity_def = value.split(" ", 1)
279 if len(capacity_def) == 1:
280 # No unit specified, default to bytes.
281 return int(capacity_def[0])
282
283 capacity_value, capacity_unit = capacity_def
284 capacity_value = float(capacity_value)
285 capacity_unit = capacity_unit.strip()
286 if capacity_unit in CAPACITY_UNITS:
287 multiplier = CAPACITY_UNITS[capacity_unit]
288 else:
289 raise UnknownCapacityUnitError(
290 "Unknown capacity unit '%s'" % capacity_unit)
291 # Convert value to bytes.
292 return int(capacity_value * multiplier)
248293
=== modified file 'src/provisioningserver/utils/tests/test_utils.py'
--- src/provisioningserver/utils/tests/test_utils.py 2017-05-30 18:36:16 +0000
+++ src/provisioningserver/utils/tests/test_utils.py 2017-06-20 12:52:44 +0000
@@ -21,6 +21,7 @@
21from provisioningserver.utils import (21from provisioningserver.utils import (
22 CircularDependency,22 CircularDependency,
23 classify,23 classify,
24 convert_size_to_bytes,
24 flatten,25 flatten,
25 is_instance_or_subclass,26 is_instance_or_subclass,
26 locate_config,27 locate_config,
@@ -30,6 +31,7 @@
30 ShellTemplate,31 ShellTemplate,
31 sorttop,32 sorttop,
32 sudo,33 sudo,
34 UnknownCapacityUnitError,
33)35)
34from testtools.matchers import (36from testtools.matchers import (
35 DirExists,37 DirExists,
@@ -369,3 +371,39 @@
369 self.assertThat(371 self.assertThat(
370 is_instance_or_subclass(372 is_instance_or_subclass(
371 self.bar, *[Baz, [Bar, [Foo]]]), Equals(True))373 self.bar, *[Baz, [Bar, [Foo]]]), Equals(True))
374
375
376class TestConvertSizeToBytes(MAASTestCase):
377 """Tests for `convert_size_to_bytes`."""
378
379 scenarios = (
380 ("bytes", {"value": "24111", "expected": 24111}),
381 ("KiB", {"value": "2.21 KiB", "expected": int(2.21 * 2**10)}),
382 ("MiB", {"value": "2.21 MiB", "expected": int(2.21 * 2**20)}),
383 ("GiB", {"value": "2.21 GiB", "expected": int(2.21 * 2**30)}),
384 ("TiB", {"value": "2.21 TiB", "expected": int(2.21 * 2**40)}),
385 ("PiB", {"value": "2.21 PiB", "expected": int(2.21 * 2**50)}),
386 ("EiB", {"value": "2.21 EiB", "expected": int(2.21 * 2**60)}),
387 ("ZiB", {"value": "2.21 ZiB", "expected": int(2.21 * 2**70)}),
388 ("YiB", {"value": "2.21 YiB", "expected": int(2.21 * 2**80)}),
389 ("whitespace", {"value": "2.21 GiB", "expected": int(2.21 * 2**30)}),
390 ("zero", {"value": "0 TiB", "expected": 0}),
391 )
392
393 def test__convert_size_to_bytes(self):
394 self.assertEqual(self.expected, convert_size_to_bytes(self.value))
395
396
397class TestConvertSizeToBytesErrors(MAASTestCase):
398 """Error handling tests for `convert_size_to_bytes`."""
399
400 def test__unknown_capacity_unit(self):
401 error = self.assertRaises(
402 UnknownCapacityUnitError, convert_size_to_bytes, "200 superbytes")
403 self.assertEqual("Unknown capacity unit 'superbytes'", str(error))
404
405 def test__empty_string(self):
406 self.assertRaises(ValueError, convert_size_to_bytes, "")
407
408 def test__empty_value(self):
409 self.assertRaises(ValueError, convert_size_to_bytes, " KiB")