Add scenario for reading processor load 93/3493/5
authorJo¶rgen Karlsson <jorgen.w.karlsson@ericsson.com>
Fri, 20 Nov 2015 16:42:59 +0000 (17:42 +0100)
committerJörgen Karlsson <jorgen.w.karlsson@ericsson.com>
Tue, 8 Dec 2015 00:32:37 +0000 (00:32 +0000)
This scenario reads processor and system load statistics
and does not run any benchmark tests.

The scenario is intended to be run in parallell with
other scenarios in order to collect processor and system
load statistics.

System load is read from /proc/loadavg.

Processor usage stats is read using the 'mpstat' utility if
it has been installed on the host.

If 'mpstat' is not installed on the host processor usage stats
is read from /proc/stats.

Change-Id: I7156e0c941100023571db750de7540786a4fedb8
JIRA: YARDSTICK-181
Signed-off-by: Jo¶rgen Karlsson <jorgen.w.karlsson@ericsson.com>
samples/cpuload.yaml [new file with mode: 0644]
tests/unit/benchmark/scenarios/compute/cpuload_sample_output1.txt [new file with mode: 0644]
tests/unit/benchmark/scenarios/compute/cpuload_sample_output2.txt [new file with mode: 0644]
tests/unit/benchmark/scenarios/compute/test_cpuload.py [new file with mode: 0644]
tools/ubuntu-server-cloudimg-modify.sh
yardstick/benchmark/scenarios/compute/cpuload.py [new file with mode: 0644]

diff --git a/samples/cpuload.yaml b/samples/cpuload.yaml
new file mode 100644 (file)
index 0000000..e28d2d2
--- /dev/null
@@ -0,0 +1,31 @@
+---
+# Sample benchmark task config file
+# Reading processor load/statistics
+
+schema: "yardstick:task:0.1"
+
+run_in_parallel: true
+
+scenarios:
+-
+  type: CPUload
+  options:
+    interval: 2
+  host: apollo.demo
+  runner:
+    type: Duration
+    duration: 60
+
+context:
+  name: demo
+  image: yardstick-trusty-server
+  flavor: yardstick-flavor
+  user: ec2-user
+
+  servers:
+    apollo:
+      floating_ip: true
+
+  networks:
+    test:
+      cidr: '10.0.1.0/24'
diff --git a/tests/unit/benchmark/scenarios/compute/cpuload_sample_output1.txt b/tests/unit/benchmark/scenarios/compute/cpuload_sample_output1.txt
new file mode 100644 (file)
index 0000000..b1723ae
--- /dev/null
@@ -0,0 +1,5 @@
+Linux 3.13.0-68-generic (elxg482ls42)  11/30/2015      _x86_64_        (12 CPU)
+
+04:53:04 PM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
+04:53:04 PM  all   11.31    0.03    1.19    0.18    0.00    0.01    0.00    5.51    0.00   81.77
+04:53:04 PM    0   20.03    0.03    1.36    0.33    0.00    0.06    0.00    6.62    0.00   71.56
diff --git a/tests/unit/benchmark/scenarios/compute/cpuload_sample_output2.txt b/tests/unit/benchmark/scenarios/compute/cpuload_sample_output2.txt
new file mode 100644 (file)
index 0000000..c66520a
--- /dev/null
@@ -0,0 +1,2 @@
+cpu  245813227 366650 17338727 1195600354 2652765 178 177114 0 80439531 0
+cpu0 32334587 35782 1659040 87008833 401178 60 73571 0 8030817 0
diff --git a/tests/unit/benchmark/scenarios/compute/test_cpuload.py b/tests/unit/benchmark/scenarios/compute/test_cpuload.py
new file mode 100644 (file)
index 0000000..22c4419
--- /dev/null
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+
+##############################################################################
+# Copyright (c) 2015 Ericsson AB and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+# Unittest for yardstick.benchmark.scenarios.compute.lmbench.Lmbench
+
+import mock
+import unittest
+import os
+
+from yardstick.benchmark.scenarios.compute import cpuload
+
+
+@mock.patch('yardstick.benchmark.scenarios.compute.cpuload.ssh')
+class CPULoadTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.ctx = {
+            'host': {
+                'ip': '172.16.0.137',
+                'user': 'cirros',
+                'key_filename': "mykey.key"
+            }
+        }
+
+        self.result = {}
+
+    def test_setup_mpstat_installed(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+
+        l.setup()
+        self.assertIsNotNone(l.client)
+        self.assertTrue(l.setup_done)
+        self.assertTrue(l.has_mpstat)
+
+    def test_setup_mpstat_not_installed(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (127, '', '')
+
+        l.setup()
+        self.assertIsNotNone(l.client)
+        self.assertTrue(l.setup_done)
+        self.assertFalse(l.has_mpstat)
+
+    def test_execute_command_success(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+        l.setup()
+
+        expected_result = 'abcdefg'
+        mock_ssh.SSH().execute.return_value = (0, expected_result, '')
+        result = l._execute_command("foo")
+        self.assertEqual(result, expected_result)
+
+    def test_execute_command_failed(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+        l.setup()
+
+        mock_ssh.SSH().execute.return_value = (127, '', 'abcdefg')
+        self.assertRaises(RuntimeError, l._execute_command,
+                          "cat /proc/loadavg")
+
+    def test_get_loadavg(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+        l.setup()
+
+        mock_ssh.SSH().execute.return_value = \
+            (0, '1.50 1.45 1.51 3/813 14322', '')
+        result = l._get_loadavg()
+        expected_result = \
+            {'loadavg': ['1.50', '1.45', '1.51', '3/813', '14322']}
+        self.assertEqual(result, expected_result)
+
+    def test_get_cpu_usage_mpstat(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+        l.setup()
+
+        l.interval = 0
+        mpstat_output = self._read_file("cpuload_sample_output1.txt")
+        mock_ssh.SSH().execute.return_value = (0, mpstat_output, '')
+        result = l._get_cpu_usage_mpstat()
+
+        expected_result = \
+            {'mpstat':
+                {'cpu':
+                    {'%gnice': '0.00',
+                     '%guest': '5.51',
+                     '%idle': '81.77',
+                     '%iowait': '0.18',
+                     '%irq': '0.00',
+                     '%nice': '0.03',
+                     '%soft': '0.01',
+                     '%steal': '0.00',
+                     '%sys': '1.19',
+                     '%usr': '11.31'},
+                 'cpu0':
+                     {'%gnice': '0.00',
+                      '%guest': '6.62',
+                      '%idle': '71.56',
+                      '%iowait': '0.33',
+                      '%irq': '0.00',
+                      '%nice': '0.03',
+                      '%soft': '0.06',
+                      '%steal': '0.00',
+                      '%sys': '1.36',
+                      '%usr': '20.03'}}}
+
+        self.assertDictEqual(result, expected_result)
+
+    def test_get_cpu_usage(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+        l.setup()
+
+        l.interval = 0
+        output = self._read_file("cpuload_sample_output2.txt")
+        mock_ssh.SSH().execute.return_value = (0, output, '')
+        result = l._get_cpu_usage()
+
+        expected_result = \
+            {'mpstat':
+                {'cpu':
+                    {'%steal': '0.00',
+                     '%usr': '11.31',
+                     '%gnice': '0.00',
+                     '%idle': '81.78',
+                     '%iowait': '0.18',
+                     '%guest': '5.50',
+                     '%sys': '1.19',
+                     '%soft': '0.01',
+                     '%irq': '0.00',
+                     '%nice': '0.03'},
+                 'cpu0':
+                    {'%steal': '0.00',
+                     '%usr': '20.00',
+                     '%gnice': '0.00',
+                     '%idle': '71.60',
+                     '%iowait': '0.33',
+                     '%guest': '6.61',
+                     '%sys': '1.37',
+                     '%soft': '0.06',
+                     '%irq': '0.00',
+                     '%nice': '0.03'}}}
+
+        self.assertDictEqual(result, expected_result)
+
+    def test_run_mpstat(self, mock_ssh):
+        l = cpuload.CPULoad({'options': {'interval': 1}}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (0, '', '')
+
+        mpstat_output = self._read_file("cpuload_sample_output1.txt")
+        mock_ssh.SSH().execute.side_effect = \
+            [(0, '', ''), (0, '1.50 1.45 1.51 3/813 14322', ''), (0, mpstat_output, '')]
+
+        l.run(self.result)
+
+        expected_result = {
+            'loadavg': ['1.50', '1.45', '1.51', '3/813', '14322'],
+            'mpstat':
+            {'cpu': {'%gnice': '0.00',
+                     '%guest': '5.51',
+                     '%idle': '81.77',
+                     '%iowait': '0.18',
+                     '%irq': '0.00',
+                     '%nice': '0.03',
+                     '%soft': '0.01',
+                     '%steal': '0.00',
+                     '%sys': '1.19',
+                     '%usr': '11.31'},
+             'cpu0': {'%gnice': '0.00',
+                      '%guest': '6.62',
+                      '%idle': '71.56',
+                      '%iowait': '0.33',
+                      '%irq': '0.00',
+                      '%nice': '0.03',
+                      '%soft': '0.06',
+                      '%steal': '0.00',
+                      '%sys': '1.36',
+                      '%usr': '20.03'}}}
+
+        self.assertDictEqual(self.result, expected_result)
+
+    def test_run_proc_stat(self, mock_ssh):
+        l = cpuload.CPULoad({}, self.ctx)
+        mock_ssh.SSH().execute.return_value = (1, '', '')
+        l.setup()
+
+        l.interval = 0
+        stat_output = self._read_file("cpuload_sample_output2.txt")
+        mock_ssh.SSH().execute.side_effect = \
+            [(0, '1.50 1.45 1.51 3/813 14322', ''), (0, stat_output, '')]
+
+        l.run(self.result)
+        expected_result = {
+            'loadavg': ['1.50', '1.45', '1.51', '3/813', '14322'],
+            'mpstat':
+                {'cpu':
+                    {'%steal': '0.00',
+                     '%usr': '11.31',
+                     '%gnice': '0.00',
+                     '%idle': '81.78',
+                     '%iowait': '0.18',
+                     '%guest': '5.50',
+                     '%sys': '1.19',
+                     '%soft': '0.01',
+                     '%irq': '0.00',
+                     '%nice': '0.03'},
+                 'cpu0':
+                    {'%steal': '0.00',
+                     '%usr': '20.00',
+                     '%gnice': '0.00',
+                     '%idle': '71.60',
+                     '%iowait': '0.33',
+                     '%guest': '6.61',
+                     '%sys': '1.37',
+                     '%soft': '0.06',
+                     '%irq': '0.00',
+                     '%nice': '0.03'}}}
+
+        self.assertDictEqual(self.result, expected_result)
+
+    def _read_file(self, filename):
+        curr_path = os.path.dirname(os.path.abspath(__file__))
+        output = os.path.join(curr_path, filename)
+        with open(output) as f:
+            sample_output = f.read()
+        return sample_output
index bdf96ba..9e1b40c 100755 (executable)
@@ -46,8 +46,8 @@ apt-get install -y \
     lmbench \
     netperf \
     rt-tests \
-    stress
+    stress \
+    sysstat
 
 # restore symlink
 ln -sf /run/resolvconf/resolv.conf /etc/resolv.conf
-
diff --git a/yardstick/benchmark/scenarios/compute/cpuload.py b/yardstick/benchmark/scenarios/compute/cpuload.py
new file mode 100644 (file)
index 0000000..2b458ff
--- /dev/null
@@ -0,0 +1,239 @@
+##############################################################################
+# Copyright (c) 2015 Ericsson AB and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+"""Processor statistics and system load."""
+
+import logging
+import time
+import re
+import yardstick.ssh as ssh
+
+from yardstick.benchmark.scenarios import base
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CPULoad(base.Scenario):
+
+    """Collect processor statistics and system load.
+
+    This scenario reads system load averages and
+    CPU usage statistics on a Linux host.
+
+    CPU usage statistics are read using the utility 'mpstat'.
+
+    If 'mpstat' is not installed on the host usage statistics
+    are instead read directly from '/proc/stat'.
+
+    Load averages are read from the file '/proc/loadavg'
+    on the Linux host.
+
+    Parameters
+          interval - Time interval to measure CPU usage. A value of 0
+                     indicates that processors statistics are to be
+                     reported for the time since system startup (boot)
+
+          type:       [int]
+          unit:       seconds
+          default:    0
+
+    """
+
+    __scenario_type__ = "CPUload"
+
+    MPSTAT_FIELD_SIZE = 10
+
+    def __init__(self, scenario_cfg, context_cfg):
+        """Scenario construction."""
+        self.scenario_cfg = scenario_cfg
+        self.context_cfg = context_cfg
+        self.setup_done = False
+        self.has_mpstat = False
+
+    def setup(self):
+        """Scenario setup."""
+        host = self.context_cfg['host']
+        user = host.get('user', 'ubuntu')
+        ip = host.get('ip', None)
+        key_filename = host.get('key_filename', '~/.ssh/id_rsa')
+
+        LOG.info("user:%s, host:%s", user, ip)
+        self.client = ssh.SSH(user, ip, key_filename=key_filename)
+        self.client.wait(timeout=600)
+
+        # Check if mpstat prog is installed
+        status, _, _ = self.client.execute("mpstat -V >/dev/null 2>&1")
+        if status != 0:
+            LOG.info("MPSTAT is NOT installed")
+            self.has_mpstat = False
+        else:
+            LOG.info("MPSTAT is installed")
+            self.has_mpstat = True
+
+        if 'options' in self.scenario_cfg:
+            self.interval = self.scenario_cfg['options'].get("interval", 0)
+        else:
+            self.interval = 0
+
+        self.setup_done = True
+
+    def _execute_command(self, cmd):
+        """Execute a command on server."""
+        LOG.info("Executing: %s" % cmd)
+        status, stdout, stderr = self.client.execute(cmd)
+        if status != 0:
+            raise RuntimeError("Failed executing command: ",
+                               cmd, stderr)
+        return stdout
+
+    def _get_loadavg(self):
+        """Get system load."""
+        return {'loadavg': self._execute_command("cat /proc/loadavg").split()}
+
+    def _get_cpu_usage_mpstat(self):
+        """Get processor usage using mpstat."""
+        if self.interval > 0:
+            cmd = "mpstat -P ON %s 1" % self.interval
+        else:
+            cmd = "mpstat -P ON"
+
+        result = self._execute_command(cmd)
+
+        fields = []
+        mpstat = {}
+
+        time_marker = re.compile("^([0-9]+):([0-9]+):([0-9]+)$")
+        ampm_marker = re.compile("(AM|PM)$")
+
+        # Parse CPU stats
+        for row in result.split('\n'):
+            line = row.split()
+
+            if line and re.match(time_marker, line[0]):
+
+                if re.match(ampm_marker, line[1]):
+                    del line[:2]
+                else:
+                    del line[:1]
+
+                if line[0] == 'CPU':
+                    # header fields
+                    fields = line[1:]
+                    if len(fields) != CPULoad.MPSTAT_FIELD_SIZE:
+                        raise RuntimeError("mpstat: unexpected field size",
+                                           fields)
+                else:
+                    # value fields
+                    cpu = 'cpu' if line[0] == 'all' else 'cpu' + line[0]
+                    values = line[1:]
+                    if values and len(values) == len(fields):
+                        mpstat[cpu] = dict(zip(fields, values))
+                    else:
+                        raise RuntimeError("mpstat: parse error", fields, line)
+
+        return {'mpstat': mpstat}
+
+    def _get_cpu_usage(self):
+        """Get processor usage from /proc/stat."""
+        fields = ['%usr', '%nice', '%sys', '%idle', '%iowait',
+                  '%irq', '%soft', '%steal', '%guest', '%gnice']
+
+        cmd = "grep '^cpu[0-9 ].' /proc/stat"
+
+        if self.interval > 0:
+            previous = self._execute_command(cmd).splitlines()
+            time.sleep(self.interval)
+            current = self._execute_command(cmd).splitlines()
+        else:
+            current = self._execute_command(cmd).splitlines()
+            previous = current
+
+        mpstat = {}
+
+        for (prev, cur) in zip(previous, current):
+
+            # Split string to list tokens
+            cur_list = cur.split()
+            prev_list = prev.split()
+
+            cpu = cur_list[0]
+
+            cur_stats = map(int, cur_list[1:])
+            if self.interval > 0:
+                prev_stats = map(int, prev_list[1:])
+            else:
+                prev_stats = [0] * len(cur_stats)
+
+            # NB: Don't add 'guest' and 'gnice' as they
+            # are already included in 'usr' and 'nice'.
+            uptime_prev = sum(prev_stats[0:8])
+            uptime_cur = sum(cur_stats[0:8])
+
+            # Remove 'guest' and 'gnice' from 'usr' and 'nice'
+            prev_stats[0] -= prev_stats[8]
+            prev_stats[1] -= prev_stats[9]
+            cur_stats[0] -= cur_stats[8]
+            cur_stats[1] -= cur_stats[9]
+
+            # number of samples (jiffies) in the interval
+            samples = (uptime_cur - uptime_prev) or 1
+
+            def _percent(x, y):
+                if x < y:
+                    return 0.0
+                else:
+                    return "%.2f" % (100.0 * (x - y) / samples)
+
+            load = map(_percent, cur_stats, prev_stats)
+
+            mpstat[cpu] = dict(zip(fields, load))
+
+        return {'mpstat': mpstat}
+
+    def run(self, result):
+        """Read processor statistics."""
+        if not self.setup_done:
+            self.setup()
+
+        result.update(self._get_loadavg())
+
+        if self.has_mpstat:
+            result.update(self._get_cpu_usage_mpstat())
+        else:
+            result.update(self._get_cpu_usage())
+
+        # Note: No SLA as this scenario is only collecting statistics
+
+# def _test():
+#     """internal test function."""
+#     import pkg_resources
+#     key_filename = pkg_resources.resource_filename('yardstick.resources',
+#                                                    'files/yardstick_key')
+#     ctx = {
+#         'host': {
+#             'ip': '172.16.0.175',
+#             'user': 'ec2-user',
+#             'key_filename': key_filename
+#         }
+#     }
+
+#     logger = logging.getLogger('yardstick')
+#     logger.setLevel(logging.DEBUG)
+
+#     args = {}
+#     result = {}
+
+#     p = CPULoad(args, ctx)
+#     p.run(result)
+#     import json
+#     print json.dumps(result)
+
+# if __name__ == '__main__':
+#     _test()