Merge ~newell-jensen/maas:fio-test into maas:master

Proposed by Andres Rodriguez
Status: Superseded
Proposed branch: ~newell-jensen/maas:fio-test
Merge into: maas:master
Diff against target: 325 lines (+302/-0)
3 files modified
src/metadataserver/builtin_scripts/__init__.py (+4/-0)
src/metadataserver/builtin_scripts/fio.py (+161/-0)
src/metadataserver/builtin_scripts/tests/test_fio.py (+137/-0)
Reviewer Review Type Date Requested Status
Lee Trager Pending
Review via email: mp+330234@code.launchpad.net

This proposal supersedes a proposal from 2017-08-07.

This proposal has been superseded by a proposal from 2017-09-05.

Commit message

Add builtin script fio test for hardware testing.

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote : Posted in a previous version of this proposal

Thanks for working on this. Currently this doesn't work as fio needs to be installed. I'm also a little concerned we're dropping STDERR. Can you see if fio outputs anything to STDERR?

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) wrote : Posted in a previous version of this proposal

> Thanks for working on this. Currently this doesn't work as fio needs to be
> installed. I'm also a little concerned we're dropping STDERR. Can you see if
> fio outputs anything to STDERR?

Go ahead and have another look at it. Hopefully changes in the spec will not demand too many changes with this :)

Revision history for this message
Newell Jensen (newell-jensen) wrote : Posted in a previous version of this proposal

Setting to WIP since things have been changed in the spec after meeting.

Revision history for this message
Lee Trager (ltrager) wrote : Posted in a previous version of this proposal

Updated review, we still need to add the MAAS metadata embedded YAML and modify MAAS to load the script in the database(I'm reworking that code now).

Revision history for this message
Lee Trager (ltrager) : Posted in a previous version of this proposal
Revision history for this message
Lee Trager (ltrager) : Posted in a previous version of this proposal
Revision history for this message
Lee Trager (ltrager) wrote : Posted in a previous version of this proposal

Looks good! One small thing inline and you have a merge conflict but I won't block you.

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) wrote : Posted in a previous version of this proposal

This branch has been reverted because it is dependent on https://code.launchpad.net/~ltrager/maas/+git/maas/+merge/329983 , which has also been reverted.

Unmerged commits

9d6cb80... by Newell Jensen

Review fixes.

c9b16c7... by Newell Jensen

Fix lint.

7fcca1f... by Newell Jensen

Decode bytes from regex matching.

390f153... by Newell Jensen

Update embedded YAML and add fio to builtin scripts.

1a27095... by Newell Jensen

Update results dictionary in embedded YAML and change dictionary keys used in results file to match.

1db6b27... by Newell Jensen

Update embedded YAML.

4e46174... by Newell Jensen

Delete YAML metadata title's curly brackets to make it more readable.

667f9d0... by Newell Jensen

Add YAML metadata to fio test.

027849c... by Newell Jensen

Only print stdout if it is not None.

86eaea0... by Newell Jensen

Review fixes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/metadataserver/builtin_scripts/__init__.py b/src/metadataserver/builtin_scripts/__init__.py
index c47f1a2..bfade7a 100644
--- a/src/metadataserver/builtin_scripts/__init__.py
+++ b/src/metadataserver/builtin_scripts/__init__.py
@@ -131,6 +131,10 @@ BUILTIN_SCRIPTS = [
131 name='7z',131 name='7z',
132 filename='seven_z.py',132 filename='seven_z.py',
133 ),133 ),
134 BuiltinScript(
135 name='fio',
136 filename='fio.py',
137 ),
134 ]138 ]
135139
136140
diff --git a/src/metadataserver/builtin_scripts/fio.py b/src/metadataserver/builtin_scripts/fio.py
137new file mode 100644141new file mode 100644
index 0000000..0cb5204
--- /dev/null
+++ b/src/metadataserver/builtin_scripts/fio.py
@@ -0,0 +1,161 @@
1#!/usr/bin/env python3
2#
3# fio - Run fio on supplied drive.
4#
5# Author: Newell Jensen <newell.jensen@canonical.com>
6#
7# Copyright (C) 2017 Canonical
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU Affero General Public License as
11# published by the Free Software Foundation, either version 3 of the
12# License, or (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU Affero General Public License for more details.
18#
19# You should have received a copy of the GNU Affero General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21#
22# --- Start MAAS 1.0 script metadata ---
23# name: fio
24# title: Storage benchmark
25# description: Run Fio benchmarking against selected storage devices.
26# tags: storage
27# script_type: testing
28# hardware_type: storage
29# parallel: instance
30# results:
31# random_read:
32# title: Random read
33# description: Read speed when reading randomly from the disk.
34# random_read_iops:
35# title: Random read IOPS
36# description: IOPS when reading randomly from the disk.
37# sequential_read:
38# title: Sequential read
39# description: Read speed when reading sequentialy from the disk.
40# sequential_read_iops:
41# title: Sequential read IOPS
42# description: IOPS when reading sequentialy from the disk.
43# random_write:
44# title: Random write
45# description: Write speed when reading randomly from the disk.
46# random_write_iops:
47# title: Random write IOPS
48# description: IOPS when reading randomly from the disk.
49# sequential_write:
50# title: Sequential write
51# description: Write speed when reading sequentialy from the disk.
52# sequential_write_iops:
53# title: Sequential write IOPS
54# description: IOPS when reading sequentialy from the disk.
55# parameters:
56# storage: {type: storage}
57# packages: {apt: fio}
58# destructive: true
59# --- End MAAS 1.0 script metadata ---
60
61
62import argparse
63from copy import deepcopy
64import os
65import re
66from subprocess import (
67 PIPE,
68 Popen,
69 STDOUT,
70)
71import sys
72
73import yaml
74
75
76CMD = [
77 'sudo', '-n', 'fio', '--randrepeat=1', '--ioengine=libaio',
78 '--direct=1', '--gtod_reduce=1', '--name=fio_test', '--bs=4k',
79 '--iodepth=64', '--size=4G'
80]
81
82REGEX = b"bw=([0-9]+[a-zA-Z]+/s), iops=([0-9]+)"
83
84
85def run_cmd(cmd):
86 """Execute `cmd` and return output or exit if error."""
87 proc = Popen(cmd, stdout=PIPE, stderr=STDOUT)
88 # Currently, we are piping stderr to STDOUT.
89 stdout, _ = proc.communicate()
90
91 # Print stdout to the console.
92 if stdout is not None:
93 print('Running command: %s\n' % ' '.join(cmd))
94 print(stdout.decode())
95 print('-' * 80)
96 if proc.returncode == 0:
97 return stdout, proc.returncode
98 sys.exit(proc.returncode)
99
100
101def run_fio_test(readwrite, result_path):
102 """Run fio for the given type of test specified by `cmd`."""
103 cmd = deepcopy(CMD)
104 cmd.append('--readwrite=%s' % readwrite)
105 stdout, returncode = run_cmd(cmd)
106 if result_path is not None:
107 # Parse the results for the desired information and
108 # then wrtie this to the results file.
109 match = re.search(REGEX, stdout)
110 if match is None:
111 print("Warning: results could not be found.")
112 return match
113
114
115def run_fio(storage):
116 """Execute fio tests for supplied storage device.
117
118 Performs random and sequential read and write tests.
119 """
120 result_path = os.environ.get("RESULT_PATH")
121 CMD.append('--filename=%s' % storage)
122 random_read_match = run_fio_test("randread", result_path)
123 sequential_read_match = run_fio_test("read", result_path)
124 random_write_match = run_fio_test("randwrite", result_path)
125 sequential_write_match = run_fio_test("write", result_path)
126
127 # Write out YAML file if RESULT_PATH is set.
128 # The status is hardcoded at the moment because there isn't a
129 # degraded state yet for fio. This most likely will change in
130 # the future when there is an agreed upon crtteria for fio to
131 # mark a storage device in the degraded state based on one of
132 # its tests.
133 if all(var is not None for var in [
134 result_path, random_read_match, sequential_read_match,
135 random_write_match, sequential_write_match]):
136 results = {
137 'status': "passed",
138 'results': {
139 'random_read': random_read_match.group(1).decode(),
140 'random_read_iops': random_read_match.group(2).decode(),
141 'sequential_read': sequential_read_match.group(1).decode(),
142 'sequential_read_iops': (
143 sequential_read_match.group(2).decode()),
144 'random_write': random_write_match.group(1).decode(),
145 'random_write_iops': random_write_match.group(2).decode(),
146 'sequential_write': sequential_write_match.group(1).decode(),
147 'sequential_write_iops': (
148 sequential_write_match.group(2).decode()),
149 }
150 }
151 with open(result_path, 'w') as results_file:
152 yaml.safe_dump(results, results_file)
153
154
155if __name__ == '__main__':
156 parser = argparse.ArgumentParser(description='Fio Hardware Testing.')
157 parser.add_argument(
158 '--storage', dest='storage',
159 help='path to storage device you want to test. e.g. /dev/sda')
160 args = parser.parse_args()
161 sys.exit(run_fio(args.storage))
diff --git a/src/metadataserver/builtin_scripts/tests/test_fio.py b/src/metadataserver/builtin_scripts/tests/test_fio.py
0new file mode 100644162new file mode 100644
index 0000000..1e39a9b
--- /dev/null
+++ b/src/metadataserver/builtin_scripts/tests/test_fio.py
@@ -0,0 +1,137 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test builtin_script fio."""
5
6__all__ = []
7
8from copy import deepcopy
9import io
10import os
11import re
12from subprocess import (
13 PIPE,
14 Popen,
15 STDOUT,
16)
17from textwrap import dedent
18from unittest.mock import ANY
19
20from maastesting.factory import factory
21from maastesting.matchers import MockCalledOnceWith
22from maastesting.testcase import MAASTestCase
23from metadataserver.builtin_scripts import fio
24import yaml
25
26
27FIO_READ_OUTPUT = dedent("""
28 ...
29 Starting 1 process
30 Jobs: 1 (f=1): [r] [100.0% done] [62135K/0K /s] [15.6K/0 iops]
31 test: (groupid=0, jobs=1): err= 0: pid=31181: Fri May 9 15:38:57 2014
32 read : io=1024.0MB, bw=62748KB/s, iops=15686 , runt= 16711msec
33 ...
34 """)
35
36FIO_WRITE_OUTPUT = dedent("""
37 ...
38 Starting 1 process
39 Jobs: 1 (f=1): [w] [100.0% done] [0K/26326K /s] [0 /6581 iops]
40 test: (groupid=0, jobs=1): err= 0: pid=31235: Fri May 9 16:16:21 2014
41 write: io=1024.0MB, bw=29195KB/s, iops=7298 , runt= 35916msec
42 ...
43 """)
44
45
46class TestFioTest(MAASTestCase):
47
48 def test_run_cmd_runs_cmd_and_returns_output(self):
49 cmd = factory.make_string()
50 output = factory.make_string()
51 mock_popen = self.patch(fio, "Popen")
52 mock_popen.return_value = Popen(
53 ['echo', '-n', output], stdout=PIPE, stderr=PIPE)
54
55 cmd_output = fio.run_cmd(cmd)
56
57 self.assertEquals((output.encode(), 0), cmd_output)
58 self.assertThat(mock_popen, MockCalledOnceWith(
59 cmd, stdout=PIPE, stderr=STDOUT))
60
61 def test_run_cmd_runs_cmd_and_exits_on_error(self):
62 cmd = factory.make_string()
63 mock_popen = self.patch(fio, "Popen")
64 proc = mock_popen.return_value
65 proc.communicate.return_value = (b"Output", None)
66 proc.returncode = 1
67
68 self.assertRaises(SystemExit, fio.run_cmd, cmd)
69 self.assertThat(mock_popen, MockCalledOnceWith(
70 cmd, stdout=PIPE, stderr=STDOUT))
71
72 def test_run_fio_test_runs_test(self):
73 result_path = factory.make_string()
74 readwrite = factory.make_string()
75 cmd = deepcopy(fio.CMD)
76 cmd.append('--readwrite=%s' % readwrite)
77 returncode = 0
78 mock_run_cmd = self.patch(fio, "run_cmd")
79 mock_run_cmd.return_value = (
80 FIO_READ_OUTPUT.encode('utf-8'), returncode)
81 mock_re_search = self.patch(re, "search")
82 fio.run_fio_test(readwrite, result_path)
83
84 self.assertThat(mock_run_cmd, MockCalledOnceWith(cmd))
85 self.assertThat(mock_re_search, MockCalledOnceWith(
86 fio.REGEX, FIO_READ_OUTPUT.encode('utf-8')))
87
88 def test_run_fio_test_exits_if_no_match_found(self):
89 result_path = factory.make_string()
90 readwrite = factory.make_string()
91 cmd = deepcopy(fio.CMD)
92 cmd.append('--readwrite=%s' % readwrite)
93 returncode = 0
94 mock_run_cmd = self.patch(fio, "run_cmd")
95 mock_run_cmd.return_value = (
96 FIO_WRITE_OUTPUT.encode('utf-8'), returncode)
97 mock_re_search = self.patch(re, "search")
98 mock_re_search.return_value = None
99 fio.run_fio_test(readwrite, result_path)
100
101 self.assertThat(mock_run_cmd, MockCalledOnceWith(cmd))
102 self.assertThat(mock_re_search, MockCalledOnceWith(
103 fio.REGEX, FIO_WRITE_OUTPUT.encode('utf-8')))
104
105 def test_run_fio_writes_yaml_file(self):
106 self.patch(os, 'environ', {
107 "RESULT_PATH": factory.make_name()
108 })
109 disk = factory.make_name('disk')
110 read_match = re.search(fio.REGEX, FIO_READ_OUTPUT.encode('utf-8'))
111 write_match = re.search(fio.REGEX, FIO_WRITE_OUTPUT.encode('utf-8'))
112 mock_run_fio_test = self.patch(fio, "run_fio_test")
113 mock_run_fio_test.side_effect = [
114 read_match, read_match, write_match, write_match]
115 mock_open = self.patch(fio, "open")
116 mock_open.return_value = io.StringIO()
117 mock_yaml_safe_dump = self.patch(yaml, "safe_dump")
118 # For the test, we will just use the same fio result for both
119 # random and sequential.
120 results = {
121 'status': "passed",
122 'results': {
123 'random_read': read_match.group(1).decode(),
124 'random_read_iops': read_match.group(2).decode(),
125 'sequential_read': read_match.group(1).decode(),
126 'sequential_read_iops': read_match.group(2).decode(),
127 'random_write': write_match.group(1).decode(),
128 'random_write_iops': write_match.group(2).decode(),
129 'sequential_write': write_match.group(1).decode(),
130 'sequential_write_iops': write_match.group(2).decode(),
131 }
132 }
133 fio.run_fio(disk)
134
135 self.assertThat(mock_open, MockCalledOnceWith(ANY, "w"))
136 self.assertThat(mock_yaml_safe_dump, MockCalledOnceWith(
137 results, mock_open.return_value))

Subscribers

People subscribed via source and target branches