Browse Source

Proof-of-concept

The PoC shows base feature of the toolkit:
 * execution plan is specified in scenario
 * scenario is transformed into ansible playbook which is executed
on remote hosts
 * the data is stored into mongo db
 * report is generated based on template, and it can contain charts
and table data processed by mongo aggregate pipeline
Ilya Shakhat 3 years ago
parent
commit
3bd574c45b

+ 1
- 1
doc/source/conf.py View File

@@ -38,7 +38,7 @@ master_doc = 'index'
38 38
 
39 39
 # General information about the project.
40 40
 project = u'performa'
41
-copyright = u'2013, OpenStack Foundation'
41
+copyright = u'2016, OpenStack Foundation'
42 42
 
43 43
 # If true, '()' will be appended to :func: etc. cross-reference text.
44 44
 add_function_parentheses = True

+ 1
- 1
doc/source/index.rst View File

@@ -4,7 +4,7 @@
4 4
    contain the root `toctree` directive.
5 5
 
6 6
 Welcome to performa's documentation!
7
-========================================================
7
+====================================
8 8
 
9 9
 Contents:
10 10
 

+ 0
- 19
performa/__init__.py View File

@@ -1,19 +0,0 @@
1
-# -*- coding: utf-8 -*-
2
-
3
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
-# not use this file except in compliance with the License. You may obtain
5
-# 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, WITHOUT
11
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
-# License for the specific language governing permissions and limitations
13
-# under the License.
14
-
15
-import pbr.version
16
-
17
-
18
-__version__ = pbr.version.VersionInfo(
19
-    'performa').version_string()

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


+ 139
- 0
performa/engine/ansible_runner.py View File

@@ -0,0 +1,139 @@
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
+from collections import namedtuple
17
+
18
+from ansible.executor import task_queue_manager
19
+from ansible import inventory
20
+from ansible.parsing import dataloader
21
+from ansible.playbook import play
22
+from ansible.plugins import callback
23
+from ansible.vars import VariableManager
24
+from oslo_log import log as logging
25
+
26
+LOG = logging.getLogger(__name__)
27
+
28
+
29
+class MyCallback(callback.CallbackBase):
30
+
31
+    CALLBACK_VERSION = 2.0
32
+    CALLBACK_TYPE = 'stdout'
33
+    CALLBACK_NAME = 'myown'
34
+
35
+    def __init__(self, storage, display=None):
36
+        super(MyCallback, self).__init__(display)
37
+        self.storage = storage
38
+
39
+    def _store(self, result, status):
40
+        record = dict(host=result._host.get_name(),
41
+                      status=status,
42
+                      task=result._task.get_name(),
43
+                      payload=result._result)
44
+        self.storage.append(record)
45
+
46
+    def v2_runner_on_failed(self, result, ignore_errors=False):
47
+        super(MyCallback, self).v2_runner_on_failed(result)
48
+        self._store(result, 'FAILED')
49
+
50
+    def v2_runner_on_ok(self, result):
51
+        super(MyCallback, self).v2_runner_on_ok(result)
52
+        self._store(result, 'OK')
53
+
54
+    def v2_runner_on_skipped(self, result):
55
+        super(MyCallback, self).v2_runner_on_skipped(result)
56
+        self._store(result, 'SKIPPED')
57
+
58
+    def v2_runner_on_unreachable(self, result):
59
+        super(MyCallback, self).v2_runner_on_unreachable(result)
60
+        self._store(result, 'UNREACHABLE')
61
+
62
+
63
+Options = namedtuple('Options',
64
+                     ['connection', 'password', 'module_path', 'forks',
65
+                      'remote_user',
66
+                      'private_key_file', 'ssh_common_args', 'ssh_extra_args',
67
+                      'sftp_extra_args', 'scp_extra_args', 'become',
68
+                      'become_method', 'become_user', 'verbosity', 'check'])
69
+
70
+
71
+def _run(play_source, host_list):
72
+
73
+    variable_manager = VariableManager()
74
+    loader = dataloader.DataLoader()
75
+    options = Options(connection='smart', password='swordfish',
76
+                      module_path='/path/to/mymodules',
77
+                      forks=100, remote_user='developer',
78
+                      private_key_file=None,
79
+                      ssh_common_args=None, ssh_extra_args=None,
80
+                      sftp_extra_args=None, scp_extra_args=None, become=None,
81
+                      become_method=None, become_user=None, verbosity=100,
82
+                      check=False)
83
+    passwords = dict(vault_pass='secret')
84
+
85
+    # create inventory and pass to var manager
86
+    inventory_inst = inventory.Inventory(loader=loader,
87
+                                         variable_manager=variable_manager,
88
+                                         host_list=host_list)
89
+    variable_manager.set_inventory(inventory_inst)
90
+
91
+    # create play
92
+    play_inst = play.Play().load(play_source,
93
+                                 variable_manager=variable_manager,
94
+                                 loader=loader)
95
+
96
+    storage = []
97
+    callback = MyCallback(storage)
98
+
99
+    # actually run it
100
+    tqm = None
101
+    try:
102
+        tqm = task_queue_manager.TaskQueueManager(
103
+            inventory=inventory_inst,
104
+            variable_manager=variable_manager,
105
+            loader=loader,
106
+            options=options,
107
+            passwords=passwords,
108
+            stdout_callback=callback,
109
+        )
110
+        tqm.run(play_inst)
111
+    finally:
112
+        if tqm is not None:
113
+            tqm.cleanup()
114
+
115
+    return storage
116
+
117
+
118
+def run_command(command, host_list):
119
+    hosts = ','.join(host_list) + ','
120
+    # tasks = [dict(action=dict(module='shell', args=command))]
121
+    tasks = [{'command': command}]
122
+
123
+    play_source = dict(
124
+        hosts=host_list,
125
+        gather_facts='no',
126
+        tasks=tasks,
127
+    )
128
+
129
+    return _run(play_source, hosts)
130
+
131
+
132
+def run_playbook(playbook, host_list):
133
+
134
+    for play_source in playbook:
135
+        hosts = ','.join(host_list) + ','
136
+        play_source['hosts'] = hosts
137
+        play_source['gather_facts'] = 'no'
138
+
139
+        _run(play_source, hosts)

+ 67
- 0
performa/engine/config.py View File

@@ -0,0 +1,67 @@
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 copy
17
+
18
+from oslo_config import cfg
19
+from oslo_config import types
20
+
21
+from performa.engine import utils
22
+
23
+
24
+SCENARIOS = 'performa/scenarios/'
25
+
26
+
27
+class Endpoint(types.String):
28
+
29
+    def __call__(self, value):
30
+        value = str(value)
31
+        utils.parse_url(value)
32
+        return value
33
+
34
+    def __repr__(self):
35
+        return "Endpoint host[:port]"
36
+
37
+
38
+MAIN_OPTS = [
39
+    cfg.StrOpt('scenario',
40
+               default=utils.env('PERFORMA_SCENARIO'),
41
+               required=True,
42
+               help=utils.make_help_options(
43
+                   'Scenario to play. Can be a file name or one of aliases: '
44
+                   '%s. Defaults to env[PERFORMA_SCENARIO].', SCENARIOS,
45
+                   type_filter=lambda x: x.endswith('.yaml'))),
46
+    cfg.Opt('mongo-url',
47
+            default=utils.env('PERFORMA_MONGO_URL'),
48
+            required=True,
49
+            type=Endpoint(),
50
+            help='Mongo URL, defaults to env[PERFORMA_MONGO_URL].'),
51
+    cfg.StrOpt('mongo-db',
52
+               default=utils.env('PERFORMA_MONGO_DB'),
53
+               required=True,
54
+               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].'),
59
+    cfg.StrOpt('book',
60
+               default=utils.env('PERFORMA_BOOK'),
61
+               help='Generate report in ReST format and store it into the '
62
+                    'specified folder, defaults to env[PERFORMA_BOOK]. '),
63
+]
64
+
65
+
66
+def list_opts():
67
+    yield (None, copy.deepcopy(MAIN_OPTS))

+ 49
- 0
performa/engine/main.py View File

@@ -0,0 +1,49 @@
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 os
17
+
18
+from oslo_config import cfg
19
+from oslo_log import log as logging
20
+
21
+from performa.engine import config
22
+from performa.engine import player
23
+from performa.engine import report
24
+from performa.engine import storage
25
+from performa.engine import utils
26
+
27
+LOG = logging.getLogger(__name__)
28
+
29
+
30
+def main():
31
+    utils.init_config_and_logging(config.MAIN_OPTS)
32
+
33
+    scenario_file_path = utils.get_absolute_file_path(
34
+        cfg.CONF.scenario,
35
+        alias_mapper=lambda f: config.SCENARIOS + '%s.yaml' % f)
36
+
37
+    scenario = utils.read_yaml_file(scenario_file_path)
38
+    base_dir = os.path.dirname(scenario_file_path)
39
+
40
+    records = player.play_scenario(scenario)
41
+
42
+    storage.store_data(records, cfg.CONF.mongo_url, cfg.CONF.mongo_db)
43
+
44
+    report.generate_report(scenario, base_dir, cfg.CONF.mongo_url,
45
+                           cfg.CONF.mongo_db, cfg.CONF.book)
46
+
47
+
48
+if __name__ == "__main__":
49
+    main()

+ 94
- 0
performa/engine/player.py View File

@@ -0,0 +1,94 @@
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 copy
17
+import re
18
+
19
+from oslo_config import cfg
20
+from oslo_log import log as logging
21
+
22
+from performa.engine import ansible_runner
23
+from performa.engine import utils
24
+from performa import executors as executors_classes
25
+
26
+LOG = logging.getLogger(__name__)
27
+
28
+
29
+def run_command(command):
30
+    return ansible_runner.run_command(command, cfg.CONF.hosts)
31
+
32
+
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)
39
+
40
+
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)
48
+
49
+            yield parametrized_test
50
+
51
+
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)
56
+
57
+
58
+def play_execution(execution):
59
+    records = []
60
+    matrix = execution.get('matrix')
61
+
62
+    for test in _pick_tests(execution['tests'], matrix):
63
+        executor = executors_classes.get_executor(test)
64
+        command = executor.get_command()
65
+
66
+        command_results = run_command(command)
67
+        for command_result in command_results:
68
+
69
+            record = dict(id=utils.make_id(),
70
+                          host=command_result['host'],
71
+                          status=command_result['status'])
72
+            record.update(test)
73
+
74
+            if command_result.get('status') == 'OK':
75
+                er = executor.process_reply(command_result['payload'])
76
+                record.update(er)
77
+
78
+            records.append(record)
79
+
80
+    return records
81
+
82
+
83
+def play_scenario(scenario):
84
+    records = {}
85
+
86
+    if 'preparation' in scenario:
87
+        play_preparation(scenario['preparation'])
88
+
89
+    if 'execution' in scenario:
90
+        execution = scenario['execution']
91
+
92
+        records = play_execution(execution)
93
+
94
+    return records

+ 132
- 0
performa/engine/report.py View File

@@ -0,0 +1,132 @@
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 errno
17
+import functools
18
+import os
19
+import tempfile
20
+
21
+import jinja2
22
+from oslo_config import cfg
23
+from oslo_log import log as logging
24
+import pygal
25
+from pygal import style
26
+import pymongo
27
+import yaml
28
+
29
+from performa.engine import config
30
+from performa.engine import utils
31
+
32
+LOG = logging.getLogger(__name__)
33
+
34
+
35
+def generate_chart(chart_str, records_collection, doc_folder):
36
+    chart = yaml.safe_load(chart_str)
37
+    pipeline = chart.get('pipeline')
38
+    title = chart.get('title')
39
+    axes = chart.get('axes') or dict(x='x', y='y')
40
+
41
+    chart_data = records_collection.aggregate(pipeline)
42
+
43
+    line = []
44
+
45
+    table = '''
46
+.. list-table:: %(title)s
47
+   :header-rows: 1
48
+
49
+   * - %(x)s
50
+     - %(y)s
51
+''' % dict(title=title, x=axes['x'], y=axes['y'])
52
+
53
+    for chart_rec in chart_data:
54
+        line.append((chart_rec['x'], chart_rec['y']))
55
+        table += ('   *\n' +
56
+                  '\n'.join('     - %d' % chart_rec[v] for v in ('x', 'y')) +
57
+                  '\n')
58
+
59
+    xy_chart = pygal.XY(style=style.RedBlueStyle,
60
+                        fill=True,
61
+                        legend_at_bottom=True,
62
+                        include_x_axis=True,
63
+                        x_title=axes['x'])
64
+    xy_chart.add(axes['y'], line)
65
+
66
+    chart_filename = utils.strict(title)
67
+    abs_chart_filename = '%s.svg' % os.path.join(doc_folder, chart_filename)
68
+    xy_chart.render_to_file(abs_chart_filename)
69
+
70
+    doc = '.. image:: %s.*\n\n' % chart_filename
71
+    doc += table
72
+
73
+    return doc
74
+
75
+
76
+def _make_dir(name):
77
+    try:
78
+        os.makedirs(name)
79
+    except OSError as e:
80
+        if e.errno != errno.EEXIST:
81
+            raise
82
+
83
+
84
+def generate_report(scenario, base_dir, mongo_url, db_name, doc_folder):
85
+    LOG.info('Generate report')
86
+
87
+    doc_folder = doc_folder or tempfile.mkdtemp(prefix='performa')
88
+
89
+    connection_params = utils.parse_url(mongo_url)
90
+    mongo_client = pymongo.MongoClient(**connection_params)
91
+    db = mongo_client.get_database(db_name)
92
+
93
+    records_collection = db.get_collection('records')
94
+
95
+    report_definition = scenario['report']
96
+    report_template = report_definition['template']
97
+
98
+    _make_dir(doc_folder)
99
+
100
+    jinja_env = jinja2.Environment()
101
+    jinja_env.filters['chart'] = functools.partial(
102
+        generate_chart,
103
+        records_collection=records_collection,
104
+        doc_folder=doc_folder)
105
+
106
+    template = utils.read_file(report_template, base_dir=base_dir)
107
+    compiled_template = jinja_env.from_string(template)
108
+    rendered_template = compiled_template.render()
109
+
110
+    index = open(os.path.join(doc_folder, 'index.rst'), 'w+')
111
+    index.write(rendered_template)
112
+    index.close()
113
+
114
+    LOG.info('The report is written to %s', doc_folder)
115
+
116
+
117
+def main():
118
+    utils.init_config_and_logging(config.MAIN_OPTS)
119
+
120
+    scenario_file_path = utils.get_absolute_file_path(
121
+        cfg.CONF.scenario,
122
+        alias_mapper=lambda f: config.SCENARIOS + '%s.yaml' % f)
123
+
124
+    scenario = utils.read_yaml_file(scenario_file_path)
125
+    base_dir = os.path.dirname(scenario_file_path)
126
+
127
+    generate_report(scenario, base_dir, cfg.CONF.mongo_url, cfg.CONF.mongo_db,
128
+                    cfg.CONF.book)
129
+
130
+
131
+if __name__ == "__main__":
132
+    main()

+ 32
- 0
performa/engine/storage.py View File

@@ -0,0 +1,32 @@
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
+from oslo_log import log as logging
17
+import pymongo
18
+
19
+from performa.engine import utils
20
+
21
+LOG = logging.getLogger(__name__)
22
+
23
+
24
+def store_data(records, mongo_url, mongo_db):
25
+    LOG.info('Store data to Mongo: %s', mongo_url)
26
+
27
+    connection_params = utils.parse_url(mongo_url)
28
+    mongo_client = pymongo.MongoClient(**connection_params)
29
+    db = mongo_client.get_database(mongo_db)
30
+
31
+    records_collection = db.get_collection('records')
32
+    records_collection.insert_many(records)

+ 216
- 0
performa/engine/utils.py View File

@@ -0,0 +1,216 @@
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 functools
17
+import itertools
18
+import logging as std_logging
19
+import os
20
+import random
21
+import uuid
22
+
23
+from oslo_config import cfg
24
+from oslo_log import log as logging
25
+import re
26
+import six
27
+import yaml
28
+
29
+
30
+LOG = logging.getLogger(__name__)
31
+
32
+
33
+def env(*_vars, **kwargs):
34
+    """Returns the first environment variable set.
35
+
36
+    If none are non-empty, defaults to '' or keyword arg default.
37
+    """
38
+    for v in _vars:
39
+        value = os.environ.get(v)
40
+        if value:
41
+            return value
42
+    return kwargs.get('default', None)
43
+
44
+
45
+def validate_required_opts(conf, opts):
46
+    # all config parameters default to ENV values, that's why standard
47
+    # check of required options doesn't work and needs to be done manually
48
+    for opt in opts:
49
+        if opt.required and not conf[opt.dest]:
50
+            raise cfg.RequiredOptError(opt.name)
51
+
52
+
53
+def init_config_and_logging(opts):
54
+    conf = cfg.CONF
55
+    conf.register_cli_opts(opts)
56
+    conf.register_opts(opts)
57
+    logging.register_options(conf)
58
+    logging.set_defaults()
59
+
60
+    try:
61
+        conf(project='performa')
62
+        validate_required_opts(conf, opts)
63
+    except cfg.RequiredOptError as e:
64
+        print('Error: %s' % e)
65
+        conf.print_usage()
66
+        exit(1)
67
+
68
+    logging.setup(conf, 'performa')
69
+    LOG.info('Logging enabled')
70
+    conf.log_opt_values(LOG, std_logging.DEBUG)
71
+
72
+
73
+def resolve_relative_path(file_name):
74
+    path = os.path.normpath(os.path.join(
75
+        os.path.dirname(__import__('performa').__file__), '../', file_name))
76
+    if os.path.exists(path):
77
+        return path
78
+
79
+
80
+def get_absolute_file_path(file_name, base_dir='', alias_mapper=None):
81
+    full_path = os.path.normpath(os.path.join(base_dir, file_name))
82
+
83
+    if alias_mapper:  # interpret file_name as alias
84
+        alias_path = resolve_relative_path(alias_mapper(file_name))
85
+        if alias_path:
86
+            full_path = alias_path
87
+            LOG.info('Alias "%s" is resolved into file "%s"', file_name,
88
+                     full_path)
89
+
90
+    if not os.path.exists(full_path):
91
+        # treat file_name as relative to act's package root
92
+        full_path = os.path.normpath(
93
+            os.path.join(os.path.dirname(__import__('performa').__file__),
94
+                         '../', file_name))
95
+        if not os.path.exists(full_path):
96
+            msg = ('File %s not found by absolute nor by relative path' %
97
+                   file_name)
98
+            raise IOError(msg)
99
+
100
+    return full_path
101
+
102
+
103
+def read_file(file_name, base_dir='', alias_mapper=None):
104
+    full_path = get_absolute_file_path(file_name, base_dir, alias_mapper)
105
+
106
+    fd = None
107
+    try:
108
+        fd = open(full_path)
109
+        return fd.read()
110
+    except IOError as e:
111
+        LOG.error('Error reading file: %s', e)
112
+        raise
113
+    finally:
114
+        if fd:
115
+            fd.close()
116
+
117
+
118
+def write_file(data, file_name, base_dir=''):
119
+    full_path = os.path.normpath(os.path.join(base_dir, file_name))
120
+    fd = None
121
+    try:
122
+        fd = open(full_path, 'w')
123
+        return fd.write(data)
124
+    except IOError as e:
125
+        LOG.error('Error writing file: %s', e)
126
+        raise
127
+    finally:
128
+        if fd:
129
+            fd.close()
130
+
131
+
132
+def read_yaml_file(file_name, base_dir='', alias_mapper=None):
133
+    raw = read_file(file_name, base_dir, alias_mapper)
134
+    try:
135
+        parsed = yaml.safe_load(raw)
136
+        return parsed
137
+    except Exception as e:
138
+        LOG.error('Failed to parse file %(file)s in YAML format: %(err)s',
139
+                  dict(file=file_name, err=e))
140
+
141
+
142
+def read_uri(uri):
143
+    try:
144
+        req = six.moves.urllib.request.Request(url=uri)
145
+        fd = six.moves.urllib.request.urlopen(req)
146
+        raw = fd.read()
147
+        fd.close()
148
+        return raw
149
+    except Exception as e:
150
+        LOG.warn('Error "%(error)s" while reading uri %(uri)s',
151
+                 {'error': e, 'uri': uri})
152
+
153
+
154
+def random_string(length=6):
155
+    return ''.join(random.sample('adefikmoprstuz', length))
156
+
157
+
158
+def make_id():
159
+    return str(uuid.uuid4())
160
+
161
+
162
+def make_help_options(message, base, type_filter=None):
163
+    path = resolve_relative_path(base)
164
+    files = itertools.chain.from_iterable(
165
+        [map(functools.partial(os.path.join, root), files)
166
+         for root, dirs, files in os.walk(path)])  # list of files in a tree
167
+    if type_filter:
168
+        files = (f for f in files if type_filter(f))  # filtered list
169
+    rel_files = map(functools.partial(os.path.relpath, start=path), files)
170
+    return message % ', '.join('"%s"' % f.partition('.')[0]
171
+                               for f in sorted(rel_files))
172
+
173
+
174
+def algebraic_product(**kwargs):
175
+    position_to_key = {}
176
+    values = []
177
+    total = 1
178
+
179
+    for key, item in six.iteritems(kwargs):
180
+        position_to_key[len(values)] = key
181
+        if type(item) != list:
182
+            item = [item]  # enclose single item into the list
183
+
184
+        values.append(item)
185
+        total *= len(item)
186
+
187
+    LOG.debug('Total number of permutations is: %s', total)
188
+
189
+    for chain in itertools.product(*values):
190
+        result = {}
191
+        for position, key in six.iteritems(position_to_key):
192
+            result[key] = chain[position]
193
+        yield result
194
+
195
+
196
+def strict(s):
197
+    return re.sub(r'[^\w\d]+', '_', re.sub(r'\(.+\)', '', s)).lower()
198
+
199
+
200
+def parse_url(url):
201
+    arr = url.split(':')
202
+    result = {}
203
+
204
+    if len(arr) > 0:
205
+        result['host'] = arr[0]
206
+
207
+        if len(arr) > 1:
208
+            if arr[1].isdigit():
209
+                result['port'] = int(arr[1])
210
+            else:
211
+                raise ValueError('URL should be in form of host[:port], '
212
+                                 'but got: %s', url)
213
+    else:
214
+        ValueError('URL should not be empty')
215
+
216
+    return result

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

@@ -0,0 +1,29 @@
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
+from performa.executors import shell
17
+from performa.executors import sysbench
18
+
19
+EXECUTORS = {
20
+    'sysbench-oltp': sysbench.SysbenchOltpExecutor,
21
+    '_default': shell.ShellExecutor,
22
+}
23
+
24
+
25
+def get_executor(test_definition):
26
+    # returns executor of the specified test on the specified agent
27
+    executor_class = test_definition['class']
28
+    klazz = EXECUTORS.get(executor_class, EXECUTORS['_default'])
29
+    return klazz(test_definition)

+ 61
- 0
performa/executors/base.py View File

@@ -0,0 +1,61 @@
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
+from oslo_log import log as logging
17
+
18
+
19
+LOG = logging.getLogger(__name__)
20
+
21
+
22
+class CommandLine(object):
23
+    def __init__(self, command):
24
+        self.tokens = [command]
25
+
26
+    def add(self, param_name, param_value=None):
27
+        token = param_name
28
+        if param_value is not None:
29
+            token += '=' + str(param_value)
30
+        self.tokens.append(token)
31
+
32
+    def make(self):
33
+        return ' '.join(self.tokens)
34
+
35
+
36
+class BaseExecutor(object):
37
+    def __init__(self, test_definition):
38
+        super(BaseExecutor, self).__init__()
39
+        self.test_definition = test_definition
40
+
41
+    def get_expected_duration(self):
42
+        return self.test_definition.get('time') or 60
43
+
44
+    def get_command(self):
45
+        return None
46
+
47
+    def process_reply(self, message):
48
+        LOG.debug('Test %s finished with %s',
49
+                  self.test_definition, message)
50
+        return dict(stdout=message.get('stdout'),
51
+                    stderr=message.get('stderr'),
52
+                    command=self.get_command())
53
+
54
+    def process_failure(self):
55
+        return dict(command=self.get_command())
56
+
57
+
58
+class ExecutorException(Exception):
59
+    def __init__(self, record, message):
60
+        super(ExecutorException, self).__init__(message)
61
+        self.record = record

+ 30
- 0
performa/executors/shell.py View File

@@ -0,0 +1,30 @@
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
+from oslo_log import log as logging
17
+
18
+from performa.executors import base
19
+
20
+
21
+LOG = logging.getLogger(__name__)
22
+
23
+
24
+class ShellExecutor(base.BaseExecutor):
25
+    def get_command(self):
26
+        if 'program' in self.test_definition:
27
+            cmd = base.CommandLine(self.test_definition['program'])
28
+        elif 'script' in self.test_definition:
29
+            cmd = base.Script(self.test_definition['script'])
30
+        return cmd.make()

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

@@ -0,0 +1,91 @@
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)

+ 28
- 0
performa/scenarios/db/sysbench.rst View File

@@ -0,0 +1,28 @@
1
+Sysbench Report
2
+---------------
3
+
4
+This is the report of execution test plan
5
+:ref:`sql_db_test_plan` with `Sysbench`_ tool.
6
+
7
+Results
8
+^^^^^^^
9
+
10
+Chart and table:
11
+
12
+{{'''
13
+    title: Queries per second
14
+    axes:
15
+      x: threads
16
+      y: queries per sec
17
+    chart: line
18
+    pipeline:
19
+    - { $match: { class: sysbench-oltp, status: OK }}
20
+    - { $group: { _id: { threads: "$threads" }, queries_total_per_sec: { $avg: { $divide: ["$queries_total", "$duration"] }}}}
21
+    - { $project: { x: "$_id.threads", y: "$queries_total_per_sec" }}
22
+    - { $sort: { x: 1 }}
23
+''' | chart
24
+}}
25
+
26
+.. references:
27
+
28
+.. _Sysbench: https://github.com/akopytov/sysbench

+ 25
- 0
performa/scenarios/db/sysbench.yaml View File

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

+ 77
- 0
performa/tests/test_sysbench.py View File

@@ -0,0 +1,77 @@
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 testtools
17
+
18
+from performa.executors import sysbench
19
+
20
+OLTP_OUTPUT = '''
21
+sysbench 0.4.12:  multi-threaded system evaluation benchmark
22
+
23
+Running the test with following options:
24
+Number of threads: 20
25
+
26
+Doing OLTP test.
27
+Running mixed OLTP test
28
+Using Special distribution
29
+Using "BEGIN" for starting transactions
30
+Not using auto_inc on the id column
31
+Threads started!
32
+Time limit exceeded, exiting...
33
+(last message repeated 19 times)
34
+Done.
35
+
36
+OLTP test statistics:
37
+    queries performed:
38
+        read:                            9310
39
+        write:                           3325
40
+        other:                           1330
41
+        total:                           13965
42
+    transactions:                        665    (10.94 per sec.)
43
+    deadlocks:                           0      (0.00 per sec.)
44
+    read/write requests:                 12635  (207.79 per sec.)
45
+    other operations:                    1330   (21.87 per sec.)
46
+
47
+Test execution summary:
48
+    total time:                          60.8074s
49
+    total number of events:              665
50
+    total time taken by event execution: 1208.0577
51
+    per-request statistics:
52
+         min:                                876.31ms
53
+         avg:                               1816.63ms
54
+         max:                               3792.73ms
55
+         approx.  95 percentile:            2886.19ms
56
+
57
+Threads fairness:
58
+    events (avg/stddev):           33.2500/0.70
59
+    execution time (avg/stddev):   60.4029/0.21
60
+'''
61
+
62
+
63
+class TestUtils(testtools.TestCase):
64
+    def test_parse_oltp(self):
65
+
66
+        expected = {
67
+            'version': '0.4.12',
68
+            'queries_read': 9310,
69
+            'queries_write': 3325,
70
+            'queries_other': 1330,
71
+            'queries_total': 13965,
72
+            'transactions': 665,
73
+            'deadlocks': 0,
74
+            'duration': 60.8074,
75
+        }
76
+
77
+        self.assertEqual(expected, sysbench.parse_sysbench_oltp(OLTP_OUTPUT))

+ 101
- 0
performa/tests/test_utils.py View File

@@ -0,0 +1,101 @@
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 mock
17
+import testtools
18
+
19
+from performa.engine import utils
20
+
21
+
22
+class TestUtils(testtools.TestCase):
23
+
24
+    @mock.patch('os.walk')
25
+    @mock.patch('performa.engine.utils.resolve_relative_path')
26
+    def test_make_help_options(self, resolve_mock, walk_mock):
27
+        base_dir = 'abc/def'
28
+        abs_dir = '/files/' + base_dir
29
+        walk_mock.side_effect = [
30
+            [(abs_dir, [], ['klm.yaml']), (abs_dir, [], ['ijk.yaml'])],
31
+        ]
32
+        resolve_mock.side_effect = [abs_dir]
33
+
34
+        expected = 'List: "ijk", "klm"'
35
+        observed = utils.make_help_options('List: %s', base_dir)
36
+        self.assertEqual(expected, observed)
37
+
38
+    @mock.patch('os.walk')
39
+    @mock.patch('performa.engine.utils.resolve_relative_path')
40
+    def test_make_help_options_subdir(self, resolve_mock, walk_mock):
41
+        base_dir = 'abc/def'
42
+        abs_dir = '/files/' + base_dir
43
+        walk_mock.side_effect = [
44
+            [(abs_dir + '/sub', [], ['klm.yaml']),
45
+             (abs_dir + '/sub', [], ['ijk.yaml'])],
46
+        ]
47
+        resolve_mock.side_effect = [abs_dir]
48
+
49
+        expected = 'List: "sub/ijk", "sub/klm"'
50
+        observed = utils.make_help_options('List: %s', base_dir)
51
+        self.assertEqual(expected, observed)
52
+
53
+    @mock.patch('os.walk')
54
+    @mock.patch('performa.engine.utils.resolve_relative_path')
55
+    def test_make_help_options_with_filter(self, resolve_mock, walk_mock):
56
+        base_dir = 'abc/def'
57
+        abs_dir = '/files/' + base_dir
58
+        walk_mock.side_effect = [
59
+            [(abs_dir + '/sub', [], ['klm.yaml']),
60
+             (abs_dir + '/sub', [], ['ijk.html']),
61
+             (abs_dir + '/sub', [], ['mno.yaml'])],
62
+        ]
63
+        resolve_mock.side_effect = [abs_dir]
64
+
65
+        expected = 'List: "sub/klm", "sub/mno"'
66
+        observed = utils.make_help_options(
67
+            'List: %s', base_dir, type_filter=lambda x: x.endswith('.yaml'))
68
+        self.assertEqual(expected, observed)
69
+
70
+    def test_algebraic_product_empty(self):
71
+        expected = [{}]
72
+
73
+        observed = list(utils.algebraic_product())
74
+
75
+        self.assertEqual(expected, observed)
76
+
77
+    def test_algebraic_product_string(self):
78
+        expected = [{'a': 1, 'b': 'zebra'}, {'a': 2, 'b': 'zebra'}]
79
+
80
+        observed = list(utils.algebraic_product(a=[1, 2], b='zebra'))
81
+
82
+        self.assertEqual(expected, observed)
83
+
84
+    def test_algebraic_product_number(self):
85
+        expected = [{'a': 'x', 'b': 4}, {'a': 2, 'b': 4}]
86
+
87
+        observed = list(utils.algebraic_product(a=['x', 2], b=4))
88
+
89
+        self.assertEqual(expected, observed)
90
+
91
+    def test_strict(self):
92
+        self.assertEqual('some_01_string_a',
93
+                         utils.strict('Some 01-string (brr!) + %% A'))
94
+
95
+    def test_parse_url(self):
96
+        self.assertEqual(dict(host='aa', port=123),
97
+                         utils.parse_url('aa:123'))
98
+
99
+    def test_parse_url_host_only(self):
100
+        self.assertEqual(dict(host='aa'),
101
+                         utils.parse_url('aa'))

+ 12
- 0
requirements.txt View File

@@ -3,3 +3,15 @@
3 3
 # process, which may cause wedges in the gate later.
4 4
 
5 5
 pbr>=1.6
6
+
7
+ansible
8
+iso8601>=0.1.9
9
+oslo.config>=2.6.0 # Apache-2.0
10
+oslo.i18n>=1.5.0 # Apache-2.0
11
+oslo.log>=1.12.0 # Apache-2.0
12
+oslo.serialization>=1.10.0 # Apache-2.0
13
+oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0
14
+pygal
15
+pymongo
16
+PyYAML>=3.1.0
17
+six>=1.9.0

+ 11
- 4
setup.cfg View File

@@ -5,9 +5,10 @@ description-file =
5 5
     README.rst
6 6
 author = OpenStack
7 7
 author-email = openstack-dev@lists.openstack.org
8
-home-page = http://www.openstack.org/
8
+home-page = https://github.com/openstack/performance-docs
9 9
 classifier =
10 10
     Environment :: OpenStack
11
+    Intended Audience :: Developers
11 12
     Intended Audience :: Information Technology
12 13
     Intended Audience :: System Administrators
13 14
     License :: OSI Approved :: Apache Software License
@@ -15,14 +16,20 @@ classifier =
15 16
     Programming Language :: Python
16 17
     Programming Language :: Python :: 2
17 18
     Programming Language :: Python :: 2.7
18
-    Programming Language :: Python :: 3
19
-    Programming Language :: Python :: 3.3
20
-    Programming Language :: Python :: 3.4
21 19
 
22 20
 [files]
23 21
 packages =
24 22
     performa
25 23
 
24
+[entry_points]
25
+console_scripts =
26
+    performa = performa.engine.main:main
27
+    performa-report = performa.engine.report:main
28
+
29
+oslo.config.opts =
30
+    oslo_log = oslo_log._options:list_opts
31
+    performa.engine.config = performa.engine.config:list_opts
32
+
26 33
 [build_sphinx]
27 34
 source-dir = doc/source
28 35
 build-dir = doc/build

+ 1
- 0
test-requirements.txt View File

@@ -6,6 +6,7 @@ hacking<0.11,>=0.10.0
6 6
 
7 7
 coverage>=3.6
8 8
 python-subunit>=0.0.18
9
+rst2pdf
9 10
 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
10 11
 oslosphinx>=2.5.0 # Apache-2.0
11 12
 oslotest>=1.10.0 # Apache-2.0

+ 4
- 34
tox.ini View File

@@ -1,6 +1,6 @@
1 1
 [tox]
2
-minversion = 2.0
3
-envlist = py34-constraints,py27-constraints,pypy-constraints,pep8-constraints
2
+envlist = docs,py27,pep8
3
+minversion = 1.6
4 4
 skipsdist = True
5 5
 
6 6
 [testenv]
@@ -13,47 +13,17 @@ setenv =
13 13
 deps = -r{toxinidir}/test-requirements.txt
14 14
 commands = python setup.py test --slowest --testr-args='{posargs}'
15 15
 
16
-[testenv:common-constraints]
17
-install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
18
-
19
-[testenv:pep8]
20
-commands = flake8 {posargs}
21
-
22
-[testenv:pep8-constraints]
23
-install_command = {[testenv:common-constraints]install_command}
24
-commands = flake8 {posargs}
25
-
26 16
 [testenv:venv]
27 17
 commands = {posargs}
28 18
 
29
-[testenv:venv-constraints]
30
-install_command = {[testenv:common-constraints]install_command}
31
-commands = {posargs}
32
-
33
-[testenv:cover]
34
-commands = python setup.py test --coverage --testr-args='{posargs}'
35
-
36
-[testenv:cover-constraints]
37
-install_command = {[testenv:common-constraints]install_command}
38
-commands = python setup.py test --coverage --testr-args='{posargs}'
19
+[testenv:pep8]
20
+commands = flake8
39 21
 
40 22
 [testenv:docs]
41 23
 commands = python setup.py build_sphinx
42 24
 
43
-[testenv:docs-constraints]
44
-install_command = {[testenv:common-constraints]install_command}
45
-commands = python setup.py build_sphinx
46
-
47
-[testenv:debug]
48
-commands = oslo_debug_helper {posargs}
49
-
50
-[testenv:debug-constraints]
51
-install_command = {[testenv:common-constraints]install_command}
52
-commands = oslo_debug_helper {posargs}
53
-
54 25
 [flake8]
55 26
 # E123, E125 skipped as they are invalid PEP-8.
56
-
57 27
 show-source = True
58 28
 ignore = E123,E125
59 29
 builtins = _

Loading…
Cancel
Save