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
1=== modified file 'src/provisioningserver/drivers/pod/tests/test_virsh.py'
2--- src/provisioningserver/drivers/pod/tests/test_virsh.py 2017-05-19 03:37:56 +0000
3+++ src/provisioningserver/drivers/pod/tests/test_virsh.py 2017-06-20 12:52:44 +0000
4@@ -105,6 +105,17 @@
5 Available: 173.84 GiB
6 """)
7
8+SAMPLE_POOLINFO_TB = dedent("""
9+ Name: default
10+ UUID: 59edc0cb-4635-449a-80e2-2c8a59afa327
11+ State: running
12+ Persistent: yes
13+ Autostart: yes
14+ Capacity: 2.21 TiB
15+ Allocation: 880.64 GiB
16+ Available: 1.35 TiB
17+ """)
18+
19 SAMPLE_IFLIST = dedent("""
20 Interface Type Source Model MAC
21 -------------------------------------------------------
22@@ -506,6 +517,18 @@
23 for pool in pools_mock.return_value
24 }, expected)
25
26+ def test_get_pod_pool_size_map_terabytes(self):
27+ conn = self.configure_virshssh(SAMPLE_POOLINFO_TB)
28+ pools_mock = self.patch(virsh.VirshSSH, 'list_pools')
29+ pools_mock.return_value = [
30+ factory.make_name('pool') for _ in range(3)]
31+ expected = conn.get_pod_pool_size_map('Capacity')
32+ capacity = int(2.21 * 2**40)
33+ self.assertEqual({
34+ pool: capacity
35+ for pool in pools_mock.return_value
36+ }, expected)
37+
38 def test_get_pod_local_storage(self):
39 conn = self.configure_virshssh(SAMPLE_POOLINFO)
40 pools_mock = self.patch(virsh.VirshSSH, 'list_pools')
41
42=== modified file 'src/provisioningserver/drivers/pod/virsh.py'
43--- src/provisioningserver/drivers/pod/virsh.py 2017-05-19 03:57:03 +0000
44+++ src/provisioningserver/drivers/pod/virsh.py 2017-06-20 12:52:44 +0000
45@@ -37,6 +37,7 @@
46 create_node,
47 )
48 from provisioningserver.utils import (
49+ convert_size_to_bytes,
50 shell,
51 typed,
52 )
53@@ -273,7 +274,13 @@
54 data = data.strip().splitlines()
55 for d in data:
56 if key == d.split(':')[0].strip():
57- return d.split(':')[1].split()[0]
58+ return d.split(':')[1].strip()
59+
60+ def get_key_value_unitless(self, data, key):
61+ """Return value based off of key with unit (if any) stripped off."""
62+ value = self.get_key_value(data, key)
63+ if value:
64+ return value.split()[0]
65
66 def list_machines(self):
67 """Lists all VMs by name."""
68@@ -336,7 +343,7 @@
69 if output is None:
70 maaslog.error("Failed to get pod CPU speed")
71 return None
72- return int(self.get_key_value(output, "CPU frequency"))
73+ return int(self.get_key_value_unitless(output, "CPU frequency"))
74
75 def get_pod_memory(self):
76 """Gets the total memory of the pod."""
77@@ -344,7 +351,7 @@
78 if output is None:
79 maaslog.error("Failed to get pod memory")
80 return None
81- KiB = int(self.get_key_value(output, "Memory size"))
82+ KiB = int(self.get_key_value_unitless(output, "Memory size"))
83 # Memory in MiB.
84 return int(KiB / 1024)
85
86@@ -354,7 +361,7 @@
87 if output is None:
88 maaslog.error("%s: Failed to get machine memory", machine)
89 return None
90- KiB = int(self.get_key_value(output, "Max memory"))
91+ KiB = int(self.get_key_value_unitless(output, "Max memory"))
92 # Memory in MiB.
93 return int(KiB / 1024)
94
95@@ -366,10 +373,8 @@
96 if output is None:
97 # Skip if cannot get more information.
98 continue
99- capacity = float(
100- self.get_key_value(output, key))
101- # Convert GiB to bytes.
102- pools[pool] = int(capacity * 2**30)
103+ pools[pool] = convert_size_to_bytes(
104+ self.get_key_value(output, "Capacity"))
105 return pools
106
107 def get_pod_local_storage(self):
108@@ -390,10 +395,10 @@
109 maaslog.error(
110 "Failed to get available pod local storage")
111 return None
112- local_storage += float(self.get_key_value(
113- output, "Available"))
114- # Local storage in bytes. GiB to bytes.
115- return int(local_storage * 2**30)
116+ local_storage += convert_size_to_bytes(
117+ self.get_key_value(output, "Available"))
118+ # Local storage in bytes.
119+ return local_storage
120
121 def get_machine_local_storage(self, machine, device):
122 """Gets the VM local storage for device."""
123
124=== modified file 'src/provisioningserver/utils/__init__.py'
125--- src/provisioningserver/utils/__init__.py 2017-05-15 17:24:04 +0000
126+++ src/provisioningserver/utils/__init__.py 2017-06-20 12:52:44 +0000
127@@ -245,3 +245,48 @@
128 return issubclass(test, query_tuple)
129 except TypeError:
130 return False
131+
132+
133+# Capacity units supported by convert_size_to_bytes() function.
134+CAPACITY_UNITS = {
135+ "KiB": 2 ** 10,
136+ "MiB": 2 ** 20,
137+ "GiB": 2 ** 30,
138+ "TiB": 2 ** 40,
139+ "PiB": 2 ** 50,
140+ "EiB": 2 ** 60,
141+ "ZiB": 2 ** 70,
142+ "YiB": 2 ** 80,
143+}
144+
145+
146+class UnknownCapacityUnitError(Exception):
147+ """Unknown capacity unit used."""
148+
149+
150+def convert_size_to_bytes(value):
151+ """
152+ Converts storage size values with units (GiB, TiB...) to bytes.
153+
154+ :param value: A string containing a number and unit separated by at least
155+ one space character. If unit is not specified, defaults to bytes.
156+ :return: An integer indicating the number of bytes for the given value in
157+ any other size unit.
158+ :raises UnknownCapacityUnitError: unsupported capacity unit.
159+ """
160+ # Split value on the first space.
161+ capacity_def = value.split(" ", 1)
162+ if len(capacity_def) == 1:
163+ # No unit specified, default to bytes.
164+ return int(capacity_def[0])
165+
166+ capacity_value, capacity_unit = capacity_def
167+ capacity_value = float(capacity_value)
168+ capacity_unit = capacity_unit.strip()
169+ if capacity_unit in CAPACITY_UNITS:
170+ multiplier = CAPACITY_UNITS[capacity_unit]
171+ else:
172+ raise UnknownCapacityUnitError(
173+ "Unknown capacity unit '%s'" % capacity_unit)
174+ # Convert value to bytes.
175+ return int(capacity_value * multiplier)
176
177=== modified file 'src/provisioningserver/utils/tests/test_utils.py'
178--- src/provisioningserver/utils/tests/test_utils.py 2017-05-30 18:36:16 +0000
179+++ src/provisioningserver/utils/tests/test_utils.py 2017-06-20 12:52:44 +0000
180@@ -21,6 +21,7 @@
181 from provisioningserver.utils import (
182 CircularDependency,
183 classify,
184+ convert_size_to_bytes,
185 flatten,
186 is_instance_or_subclass,
187 locate_config,
188@@ -30,6 +31,7 @@
189 ShellTemplate,
190 sorttop,
191 sudo,
192+ UnknownCapacityUnitError,
193 )
194 from testtools.matchers import (
195 DirExists,
196@@ -369,3 +371,39 @@
197 self.assertThat(
198 is_instance_or_subclass(
199 self.bar, *[Baz, [Bar, [Foo]]]), Equals(True))
200+
201+
202+class TestConvertSizeToBytes(MAASTestCase):
203+ """Tests for `convert_size_to_bytes`."""
204+
205+ scenarios = (
206+ ("bytes", {"value": "24111", "expected": 24111}),
207+ ("KiB", {"value": "2.21 KiB", "expected": int(2.21 * 2**10)}),
208+ ("MiB", {"value": "2.21 MiB", "expected": int(2.21 * 2**20)}),
209+ ("GiB", {"value": "2.21 GiB", "expected": int(2.21 * 2**30)}),
210+ ("TiB", {"value": "2.21 TiB", "expected": int(2.21 * 2**40)}),
211+ ("PiB", {"value": "2.21 PiB", "expected": int(2.21 * 2**50)}),
212+ ("EiB", {"value": "2.21 EiB", "expected": int(2.21 * 2**60)}),
213+ ("ZiB", {"value": "2.21 ZiB", "expected": int(2.21 * 2**70)}),
214+ ("YiB", {"value": "2.21 YiB", "expected": int(2.21 * 2**80)}),
215+ ("whitespace", {"value": "2.21 GiB", "expected": int(2.21 * 2**30)}),
216+ ("zero", {"value": "0 TiB", "expected": 0}),
217+ )
218+
219+ def test__convert_size_to_bytes(self):
220+ self.assertEqual(self.expected, convert_size_to_bytes(self.value))
221+
222+
223+class TestConvertSizeToBytesErrors(MAASTestCase):
224+ """Error handling tests for `convert_size_to_bytes`."""
225+
226+ def test__unknown_capacity_unit(self):
227+ error = self.assertRaises(
228+ UnknownCapacityUnitError, convert_size_to_bytes, "200 superbytes")
229+ self.assertEqual("Unknown capacity unit 'superbytes'", str(error))
230+
231+ def test__empty_string(self):
232+ self.assertRaises(ValueError, convert_size_to_bytes, "")
233+
234+ def test__empty_value(self):
235+ self.assertRaises(ValueError, convert_size_to_bytes, " KiB")