Browse Source

Turn executors into Ansible modules

Ilya Shakhat 3 years ago
parent
commit
92d86d73ad

+ 13
- 5
performa/engine/ansible_runner.py View File

@@ -23,6 +23,8 @@ from ansible.plugins import callback
23 23
 from ansible.vars import VariableManager
24 24
 from oslo_log import log as logging
25 25
 
26
+from performa.engine import utils
27
+
26 28
 LOG = logging.getLogger(__name__)
27 29
 
28 30
 
@@ -70,10 +72,14 @@ Options = namedtuple('Options',
70 72
 
71 73
 def _run(play_source, host_list):
72 74
 
75
+    LOG.debug('Running play: %s on hosts: %s', play_source, host_list)
76
+
73 77
     variable_manager = VariableManager()
74 78
     loader = dataloader.DataLoader()
79
+    module_path = utils.resolve_relative_path('performa/modules')
80
+
75 81
     options = Options(connection='smart', password='swordfish',
76
-                      module_path='/path/to/mymodules',
82
+                      module_path=module_path,
77 83
                       forks=100, remote_user='developer',
78 84
                       private_key_file=None,
79 85
                       ssh_common_args=None, ssh_extra_args=None,
@@ -129,11 +135,13 @@ def run_command(command, host_list):
129 135
     return _run(play_source, hosts)
130 136
 
131 137
 
132
-def run_playbook(playbook, host_list):
138
+def run_playbook(playbook):
139
+    result = []
133 140
 
134 141
     for play_source in playbook:
135
-        hosts = ','.join(host_list) + ','
136
-        play_source['hosts'] = hosts
142
+        hosts = play_source['hosts']
137 143
         play_source['gather_facts'] = 'no'
138 144
 
139
-        _run(play_source, hosts)
145
+        result += (_run(play_source, hosts))
146
+
147
+    return result

+ 21
- 4
performa/engine/config.py View File

@@ -17,6 +17,7 @@ import copy
17 17
 
18 18
 from oslo_config import cfg
19 19
 from oslo_config import types
20
+import yaml
20 21
 
21 22
 from performa.engine import utils
22 23
 
@@ -35,6 +36,20 @@ class Endpoint(types.String):
35 36
         return "Endpoint host[:port]"
36 37
 
37 38
 
39
+class Yaml(types.String):
40
+
41
+    def __call__(self, value):
42
+        value = str(value)
43
+        try:
44
+            value = yaml.safe_load(value)
45
+        except Exception:
46
+            raise ValueError('YAML value is expected, but got: %s' % value)
47
+        return value
48
+
49
+    def __repr__(self):
50
+        return "YAML data"
51
+
52
+
38 53
 MAIN_OPTS = [
39 54
     cfg.StrOpt('scenario',
40 55
                default=utils.env('PERFORMA_SCENARIO'),
@@ -52,10 +67,12 @@ MAIN_OPTS = [
52 67
                default=utils.env('PERFORMA_MONGO_DB'),
53 68
                required=True,
54 69
                help='Mongo DB, defaults to env[PERFORMA_MONGO_DB].'),
55
-    cfg.ListOpt('hosts',
56
-                default=utils.env('PERFORMA_HOSTS'),
57
-                required=True,
58
-                help='List of hosts, defaults to env[PERFORMA_MONGO_URL].'),
70
+    cfg.Opt('hosts',
71
+            type=Yaml(),
72
+            default=utils.env('PERFORMA_HOSTS'),
73
+            required=True,
74
+            help='Hosts inventory definition in YAML format, '
75
+                 'Can be specified via env[PERFORMA_HOSTS].'),
59 76
     cfg.StrOpt('book',
60 77
                default=utils.env('PERFORMA_BOOK'),
61 78
                help='Generate report in ReST format and store it into the '

+ 16
- 2
performa/engine/main.py View File

@@ -17,6 +17,7 @@ import os
17 17
 
18 18
 from oslo_config import cfg
19 19
 from oslo_log import log as logging
20
+import yaml
20 21
 
21 22
 from performa.engine import config
22 23
 from performa.engine import player
@@ -27,6 +28,13 @@ from performa.engine import utils
27 28
 LOG = logging.getLogger(__name__)
28 29
 
29 30
 
31
+def resolve_hosts(scenario, hosts):
32
+    for k, v in hosts.items():
33
+        scenario = scenario.replace('$%s' % k, ','.join(v) + ',')
34
+
35
+    return scenario
36
+
37
+
30 38
 def main():
31 39
     utils.init_config_and_logging(config.MAIN_OPTS)
32 40
 
@@ -34,7 +42,10 @@ def main():
34 42
         cfg.CONF.scenario,
35 43
         alias_mapper=lambda f: config.SCENARIOS + '%s.yaml' % f)
36 44
 
37
-    scenario = utils.read_yaml_file(scenario_file_path)
45
+    scenario_raw = utils.read_file(scenario_file_path)
46
+    scenario_raw = resolve_hosts(scenario_raw, cfg.CONF.hosts)
47
+    scenario = yaml.safe_load(scenario_raw)
48
+
38 49
     base_dir = os.path.dirname(scenario_file_path)
39 50
 
40 51
     tag = cfg.CONF.tag
@@ -44,7 +55,10 @@ def main():
44 55
 
45 56
     records = player.play_scenario(scenario, tag)
46 57
 
47
-    storage.store_data(records, cfg.CONF.mongo_url, cfg.CONF.mongo_db)
58
+    if records:
59
+        storage.store_data(records, cfg.CONF.mongo_url, cfg.CONF.mongo_db)
60
+    else:
61
+        LOG.warning('Execution generated no records')
48 62
 
49 63
     report.generate_report(scenario, base_dir, cfg.CONF.mongo_url,
50 64
                            cfg.CONF.mongo_db, cfg.CONF.book, tag)

+ 40
- 37
performa/engine/player.py View File

@@ -14,14 +14,12 @@
14 14
 # limitations under the License.
15 15
 
16 16
 import copy
17
-import re
18 17
 
19 18
 from oslo_config import cfg
20 19
 from oslo_log import log as logging
21 20
 
22 21
 from performa.engine import ansible_runner
23 22
 from performa.engine import utils
24
-from performa import executors as executors_classes
25 23
 
26 24
 LOG = logging.getLogger(__name__)
27 25
 
@@ -30,52 +28,57 @@ def run_command(command):
30 28
     return ansible_runner.run_command(command, cfg.CONF.hosts)
31 29
 
32 30
 
33
-def _make_test_title(test, params=None):
34
-    s = test.get('title') or test.get('class')
35
-    if params:
36
-        s += ' '.join([','.join(['%s=%s' % (k, v) for k, v in params.items()
37
-                                if k != 'host'])])
38
-    return re.sub(r'[^\x20-\x7e\x80-\xff]+', '_', s)
31
+def _pick_tasks(tasks, matrix):
32
+    matrix = matrix or {}
39 33
 
34
+    for params in utils.algebraic_product(**matrix):
35
+        for task in tasks:
36
+            parametrized_task = copy.deepcopy(task)
37
+            values = parametrized_task.values()[0]
40 38
 
41
-def _pick_tests(tests, matrix):
42
-    matrix = matrix or {}
43
-    for test in tests:
44
-        for params in utils.algebraic_product(**matrix):
45
-            parametrized_test = copy.deepcopy(test)
46
-            parametrized_test.update(params)
47
-            parametrized_test['title'] = _make_test_title(test, params)
39
+            if isinstance(values, dict):
40
+                values.update(params)
48 41
 
49
-            yield parametrized_test
42
+            yield parametrized_task
50 43
 
51 44
 
52
-def play_preparation(preparation):
53
-    ansible_playbook = preparation.get('ansible-playbook')
54
-    if ansible_playbook:
55
-        ansible_runner.run_playbook(ansible_playbook, cfg.CONF.hosts)
45
+def play_setup(setup):
46
+    ansible_runner.run_playbook(setup)
56 47
 
57 48
 
58
-def play_execution(execution):
49
+def play_execution(execution_playbook):
59 50
     records = []
60
-    matrix = execution.get('matrix')
61 51
 
62
-    for test in _pick_tests(execution['tests'], matrix):
63
-        executor = executors_classes.get_executor(test)
64
-        command = executor.get_command()
52
+    for play in execution_playbook:
53
+        matrix = play.get('matrix')
54
+
55
+        for task in _pick_tasks(play['tasks'], matrix):
56
+
57
+            task_play = {
58
+                'hosts': play['hosts'],
59
+                'tasks': [task],
60
+            }
61
+            command_results = ansible_runner.run_playbook([task_play])
65 62
 
66
-        command_results = run_command(command)
67
-        for command_result in command_results:
63
+            for command_result in command_results:
64
+                if command_result.get('status') == 'OK':
65
+                    record = dict(id=utils.make_id(),
66
+                                  host=command_result['host'],
67
+                                  status=command_result['status'],
68
+                                  task=command_result['task'])
69
+                    payload = command_result['payload']
70
+                    record.update(payload['invocation']['module_args'])
71
+                    record.update(payload)
68 72
 
69
-            record = dict(id=utils.make_id(),
70
-                          host=command_result['host'],
71
-                          status=command_result['status'])
72
-            record.update(test)
73
+                    # keep flat values only
74
+                    for k, v in record.items():
75
+                        if isinstance(v, list) or isinstance(v, dict):
76
+                            del record[k]
73 77
 
74
-            if command_result.get('status') == 'OK':
75
-                er = executor.process_reply(command_result['payload'])
76
-                record.update(er)
78
+                    del record['stdout']
77 79
 
78
-            records.append(record)
80
+                    LOG.debug('Record: %s', record)
81
+                    records.append(record)
79 82
 
80 83
     return records
81 84
 
@@ -88,8 +91,8 @@ def tag_records(records, tag):
88 91
 def play_scenario(scenario, tag):
89 92
     records = {}
90 93
 
91
-    if 'preparation' in scenario:
92
-        play_preparation(scenario['preparation'])
94
+    if 'setup' in scenario:
95
+        play_setup(scenario['setup'])
93 96
 
94 97
     if 'execution' in scenario:
95 98
         execution = scenario['execution']

+ 0
- 2
performa/executors/__init__.py View File

@@ -14,10 +14,8 @@
14 14
 # limitations under the License.
15 15
 
16 16
 from performa.executors import shell
17
-from performa.executors import sysbench
18 17
 
19 18
 EXECUTORS = {
20
-    'sysbench-oltp': sysbench.SysbenchOltpExecutor,
21 19
     '_default': shell.ShellExecutor,
22 20
 }
23 21
 

+ 0
- 91
performa/executors/sysbench.py View File

@@ -1,91 +0,0 @@
1
-# Copyright (c) 2016 OpenStack Foundation
2
-#
3
-# Licensed under the Apache License, Version 2.0 (the "License");
4
-# you may not use this file except in compliance with the License.
5
-# You may obtain a copy of the License at
6
-#
7
-#   http://www.apache.org/licenses/LICENSE-2.0
8
-#
9
-# Unless required by applicable law or agreed to in writing, software
10
-# distributed under the License is distributed on an "AS IS" BASIS,
11
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
-# implied.
13
-# See the License for the specific language governing permissions and
14
-# limitations under the License.
15
-
16
-import re
17
-
18
-from oslo_log import log as logging
19
-
20
-from performa.executors import base
21
-
22
-LOG = logging.getLogger(__name__)
23
-
24
-
25
-TEST_STATS = re.compile(
26
-    '\s+queries performed:\s*\n'
27
-    '\s+read:\s+(?P<queries_read>\d+)\s*\n'
28
-    '\s+write:\s+(?P<queries_write>\d+).*\n'
29
-    '\s+other:\s+(?P<queries_other>\d+).*\n'
30
-    '\s+total:\s+(?P<queries_total>\d+).*\n',
31
-    flags=re.MULTILINE | re.DOTALL
32
-)
33
-PATTERNS = [
34
-    r'sysbench (?P<version>[\d\.]+)',
35
-    TEST_STATS,
36
-    r'\s+transactions:\s+(?P<transactions>\d+).*\n',
37
-    r'\s+deadlocks:\s+(?P<deadlocks>\d+).*\n',
38
-    r'\s+total time:\s+(?P<duration>[\d\.]+).*\n',
39
-]
40
-TRANSFORM_FIELDS = {
41
-    'queries_read': int,
42
-    'queries_write': int,
43
-    'queries_other': int,
44
-    'queries_total': int,
45
-    'duration': float,
46
-    'transactions': int,
47
-    'deadlocks': int,
48
-}
49
-
50
-
51
-def parse_sysbench_oltp(raw):
52
-    result = {}
53
-
54
-    for pattern in PATTERNS:
55
-        for parsed in re.finditer(pattern, raw):
56
-            result.update(parsed.groupdict())
57
-
58
-    for k in result.keys():
59
-        if k in TRANSFORM_FIELDS:
60
-            result[k] = TRANSFORM_FIELDS[k](result[k])
61
-
62
-    return result
63
-
64
-
65
-class SysbenchOltpExecutor(base.BaseExecutor):
66
-    def get_command(self):
67
-        cmd = base.CommandLine('sysbench')
68
-
69
-        cmd.add('--test', 'oltp')
70
-        cmd.add('--db-driver', 'mysql')
71
-        cmd.add('--mysql-table-engine', 'innodb')
72
-        cmd.add('--mysql-engine-trx', 'yes')
73
-        cmd.add('--num-threads', self.test_definition.get('threads') or 10)
74
-        cmd.add('--max-time', self.get_expected_duration())
75
-        cmd.add('--max-requests', 0)
76
-        cmd.add('--mysql-host', 'localhost')
77
-        cmd.add('--mysql-db', 'sbtest')
78
-        cmd.add('--oltp-table-name', 'sbtest')
79
-        cmd.add('--oltp-table-size',
80
-                self.test_definition.get('table_size') or 100000)
81
-        # cmd.add('--oltp-num-tables',
82
-        #         self.test_definition.get('num_tables') or 10)
83
-        # cmd.add('--oltp-auto-inc', 'off')
84
-        # cmd.add('--oltp-read-only', 'off')
85
-        cmd.add('run')
86
-
87
-        return cmd.make()
88
-
89
-    def process_reply(self, record):
90
-        stdout = record.get('stdout')
91
-        return parse_sysbench_oltp(stdout)

+ 0
- 0
performa/modules/__init__.py View File


+ 87
- 0
performa/modules/sysbench_oltp.py View File

@@ -0,0 +1,87 @@
1
+#!/usr/bin/python
2
+
3
+import re
4
+
5
+TEST_STATS = re.compile(
6
+    '\s+queries performed:\s*\n'
7
+    '\s+read:\s+(?P<queries_read>\d+)\s*\n'
8
+    '\s+write:\s+(?P<queries_write>\d+).*\n'
9
+    '\s+other:\s+(?P<queries_other>\d+).*\n'
10
+    '\s+total:\s+(?P<queries_total>\d+).*\n',
11
+    flags=re.MULTILINE | re.DOTALL
12
+)
13
+PATTERNS = [
14
+    r'sysbench (?P<version>[\d\.]+)',
15
+    TEST_STATS,
16
+    r'\s+transactions:\s+(?P<transactions>\d+).*\n',
17
+    r'\s+deadlocks:\s+(?P<deadlocks>\d+).*\n',
18
+    r'\s+total time:\s+(?P<duration>[\d\.]+).*\n',
19
+]
20
+TRANSFORM_FIELDS = {
21
+    'queries_read': int,
22
+    'queries_write': int,
23
+    'queries_other': int,
24
+    'queries_total': int,
25
+    'duration': float,
26
+    'transactions': int,
27
+    'deadlocks': int,
28
+}
29
+
30
+
31
+def parse_sysbench_oltp(raw):
32
+    result = {}
33
+
34
+    for pattern in PATTERNS:
35
+        for parsed in re.finditer(pattern, raw):
36
+            result.update(parsed.groupdict())
37
+
38
+    for k in result.keys():
39
+        if k in TRANSFORM_FIELDS:
40
+            result[k] = TRANSFORM_FIELDS[k](result[k])
41
+
42
+    return result
43
+
44
+
45
+def main():
46
+    module = AnsibleModule(
47
+        argument_spec=dict(
48
+            threads=dict(type='int', default=10),
49
+            duration=dict(type='int', default=10),
50
+            mysql_host=dict(default='localhost'),
51
+            mysql_db=dict(default='sbtest'),
52
+            oltp_table_name=dict(default='sbtest'),
53
+            oltp_table_size=dict(type='int', default=100000),
54
+        ))
55
+
56
+    cmd = ('sysbench '
57
+           '--test=oltp '
58
+           '--db-driver=mysql '
59
+           '--mysql-table-engine=innodb '
60
+           '--mysql-engine-trx=yes '
61
+           '--num-threads=%(threads)s '
62
+           '--max-time=%(duration)s '
63
+           '--max-requests=0 '
64
+           '--mysql-host=%(mysql_host)s '
65
+           '--mysql-db=%(mysql_db)s '
66
+           '--oltp-table-name=%(oltp_table_name)s '
67
+           '--oltp-table-size=%(oltp_table_size)s '
68
+           'run'
69
+           ) % module.params
70
+
71
+    rc, stdout, stderr = module.run_command(cmd)
72
+
73
+    result = dict(changed=True, rc=rc, stdout=stdout, stderr=stderr, cmd=cmd)
74
+
75
+    try:
76
+        result.update(parse_sysbench_oltp(stdout))
77
+        module.exit_json(**result)
78
+    except Exception as e:
79
+        result['exception'] = e
80
+
81
+    module.fail_json(**result)
82
+
83
+
84
+from ansible.module_utils.basic import *  # noqa
85
+
86
+if __name__ == '__main__':
87
+    main()

+ 1
- 1
performa/scenarios/db/sysbench.rst View File

@@ -17,7 +17,7 @@ Chart and table:
17 17
       y2: read queries per sec
18 18
     chart: line
19 19
     pipeline:
20
-    - { $match: { class: sysbench-oltp, status: OK }}
20
+    - { $match: { task: sysbench_oltp, status: OK }}
21 21
     - { $group: { _id: { threads: "$threads" },
22 22
                   queries_total_per_sec: { $avg: { $divide: ["$queries_total", "$duration"] }},
23 23
                   queries_read_per_sec: { $avg: { $divide: ["$queries_read", "$duration"] }}

+ 16
- 15
performa/scenarios/db/sysbench.yaml View File

@@ -1,25 +1,26 @@
1
-title: DB
1
+title: Sysbench DB
2 2
 
3 3
 description:
4 4
   This scenario uses sysbench to execute DB test plan.
5 5
 
6
-preparation:
7
-  ansible-playbook:
8
-    -
9
-      tasks:
10
-      - apt: name=sysbench
11
-        become: yes
12
-        become_user: root
13
-        become_method: sudo
6
+setup:
7
+  -
8
+    hosts: $target
9
+    tasks:
10
+    - name: installing sysbench
11
+      apt: name=sysbench
12
+      become: yes
13
+      become_user: root
14
+      become_method: sudo
14 15
 
15 16
 execution:
16
-  matrix:
17
-    threads: [ 10, 20, 30, 40, 50, 60 ]
18
-  tests:
19 17
   -
20
-    title: sysbench-oltp
21
-    class: sysbench-oltp
22
-    time: 10
18
+    hosts: $target
19
+    matrix:
20
+      threads: [ 10, 20, 30 ]
21
+    tasks:
22
+    - sysbench_oltp:
23
+        duration: 10
23 24
 
24 25
 report:
25 26
   template: sysbench.rst

+ 1
- 1
performa/tests/test_sysbench.py View File

@@ -15,7 +15,7 @@
15 15
 
16 16
 import testtools
17 17
 
18
-from performa.executors import sysbench
18
+from performa.modules import sysbench_oltp as sysbench
19 19
 
20 20
 OLTP_OUTPUT = '''
21 21
 sysbench 0.4.12:  multi-threaded system evaluation benchmark

+ 1
- 1
tox.ini View File

@@ -25,6 +25,6 @@ commands = python setup.py build_sphinx
25 25
 [flake8]
26 26
 # E123, E125 skipped as they are invalid PEP-8.
27 27
 show-source = True
28
-ignore = E123,E125
28
+ignore = E123,E125,H102
29 29
 builtins = _
30 30
 exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build

Loading…
Cancel
Save