diff options
-rw-r--r-- | qtip/ansible_library/modules/apex.py | 133 | ||||
-rw-r--r-- | qtip/cli/entry.py | 3 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rwxr-xr-x | tests/data/external/apex/baremetal_info.json | 57 | ||||
-rwxr-xr-x | tests/data/external/apex/server_info.json | 37 | ||||
-rw-r--r-- | tests/unit/ansible_library/modules/apex_test.py | 30 | ||||
-rw-r--r-- | tests/unit/cli/options_test.py | 3 | ||||
-rw-r--r-- | tests/unit/reporter/test_testapi.py | 116 |
8 files changed, 378 insertions, 2 deletions
diff --git a/qtip/ansible_library/modules/apex.py b/qtip/ansible_library/modules/apex.py new file mode 100644 index 00000000..218440b2 --- /dev/null +++ b/qtip/ansible_library/modules/apex.py @@ -0,0 +1,133 @@ +#!/usr/bin/python + +############################################################### +# Copyright (c) 2017 ZTE Corporation +# +# 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 +############################################################################## + +from collections import defaultdict +import json +import re + +from ansible.module_utils.basic import AnsibleModule + + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: apex +short_description: collecting facts from apex environments +description: + - Use this module to create a dynamic inventory from apex undercloud. +version_added: "2.2" +author: "Zhihui Wu" +options: +notes: +requirements: + - Host 'apex-undercloud' is in ~/.ssh/config +''' + +RETURN = ''' +ansible_facts: + description: facts collected for ansible + returned: success + type: dictionary + contains: + hosts: + description: host grouped by hostname, cluster, role and manufacture + type: dict + hosts_meta: + description: hosts meta data indexed by hostname + type: dict +''' + +EXAMPLES = ''' +--- +- hosts: apex-undercloud + tasks: + - name: collect facts of apex hosts + apex: + - debug: var=hostvarsi + - name: add compute node to ansible inventory + add_host: + name: "{{ hosts_meta[item]['ip'] }}" + groups: compute + ansible_user: root + ansible_ssh_common_args: '-o StrictHostKeyChecking=No -o ProxyJump=apex-master' + with_items: "{{ hosts.compute }}" +- hosts: compute + tasks: + - name: check ssh connection + ping: +''' + + +def generate_inventory(baremetal_info, server_info): + """Generate ansible inventory from node list in json format + + Modified from https://github.com/martineg/ansible-apex-inventory/blob/master/apex.py + """ + + hosts = defaultdict(list) + hosts_meta = {} + + for node in baremetal_info: + if node['Provisioning State'].lower() == 'active': + role = re.findall('.+profile:(\w+)$', node['Properties']['capabilities'])[0] + for server in server_info: + if server['ID'] == node['Instance UUID']: + node_ip = re.findall('.+=(\d+.\d+.\d+.\d+)$', server['Networks'])[0] + hosts[role].append(node_ip) + # To match ssh.cfg.j2 template + hosts_meta[node_ip] = {'ansible_ssh_host': node_ip} + + for host in hosts: + hosts[host].sort() + + return {'hosts': hosts, 'hosts_meta': hosts_meta} + + +def main(): + module = AnsibleModule(argument_spec=dict()) + + (rc, out, err) = module.run_command(['source ~/stackrc']) + + if rc is not None and rc != 0: + return module.fail_json(msg=err) + + cmd = [module.get_bin_path('openstack', True), + 'baremetal', + 'list', + '--fields instance_uuid properties provision_state', + '--format json'] + (rc, out, err) = module.run_command(cmd) + + if rc is not None and rc != 0: + return module.fail_json(msg=err) + + baremetal_info = json.loads(out) + + cmd = [module.get_bin_path('openstack', True), + 'server', + 'list', + '--format json'] + (rc, out, err) = module.run_command(cmd) + + if rc is not None and rc != 0: + return module.fail_json(msg=err) + + server_info = json.loads(out) + + module.exit_json(changed=False, + ansible_facts=generate_inventory(baremetal_info, server_info)) + + +if __name__ == '__main__': + main() diff --git a/qtip/cli/entry.py b/qtip/cli/entry.py index 6cf78b58..b84a03d0 100644 --- a/qtip/cli/entry.py +++ b/qtip/cli/entry.py @@ -9,6 +9,7 @@ import click import os +import pkg_resources as pkg import sys @@ -53,7 +54,7 @@ class QtipCli(click.MultiCommand): invoke_without_command=True) @click.option('-v', '--verbose', is_flag=True, help='Enable verbose mode.') @click.option('-d', '--debug', is_flag=True, help='Enable debug mode.') -@click.version_option('dev') +@click.version_option(pkg.require("qtip")[0]) @pass_context def cli(ctx, verbose, debug): if debug: @@ -1,5 +1,6 @@ [metadata] name = qtip +version = 5.0.0 summary = Platform Performance Benchmarking description-file = README.md diff --git a/tests/data/external/apex/baremetal_info.json b/tests/data/external/apex/baremetal_info.json new file mode 100755 index 00000000..8efe4e71 --- /dev/null +++ b/tests/data/external/apex/baremetal_info.json @@ -0,0 +1,57 @@ +[ + { + "Properties": { + "memory_mb": "8192", + "cpu_arch": "x86_64", + "local_gb": "41", + "cpus": "4", + "capabilities": "boot_option:local,profile:compute" + }, + "Provisioning State": "active", + "Instance UUID": "367827af-966c-4c91-bad4-d8dc12750eac" + }, + { + "Properties": { + "memory_mb": "8192", + "cpu_arch": "x86_64", + "local_gb": "41", + "cpus": "4", + "capabilities": "boot_option:local,profile:compute" + }, + "Provisioning State": "active", + "Instance UUID": "f214f844-ec4d-4f33-9d16-9aa21ade3cb4" + }, + { + "Properties": { + "memory_mb": "8192", + "cpu_arch": "x86_64", + "local_gb": "41", + "cpus": "4", + "capabilities": "boot_option:local,profile:control" + }, + "Provisioning State": "active", + "Instance UUID": "06234a4d-45fb-4930-bf8a-9b1627b1621f" + }, + { + "Properties": { + "memory_mb": "8192", + "cpu_arch": "x86_64", + "local_gb": "41", + "cpus": "4", + "capabilities": "boot_option:local,profile:control" + }, + "Provisioning State": "active", + "Instance UUID": "6dfd4c85-8a05-49ea-bd73-15dbaf84fa9b" + }, + { + "Properties": { + "memory_mb": "8192", + "cpu_arch": "x86_64", + "local_gb": "41", + "cpus": "4", + "capabilities": "boot_option:local,profile:control" + }, + "Provisioning State": "active", + "Instance UUID": "25b73b47-7c22-4f58-8ff3-ba5d714baa7c" + } +] diff --git a/tests/data/external/apex/server_info.json b/tests/data/external/apex/server_info.json new file mode 100755 index 00000000..71bc40c5 --- /dev/null +++ b/tests/data/external/apex/server_info.json @@ -0,0 +1,37 @@ +[ + { + "Status": "ACTIVE", + "Networks": "ctlplane=192.0.2.9", + "ID": "06234a4d-45fb-4930-bf8a-9b1627b1621f", + "Image Name": "overcloud-full", + "Name": "overcloud-controller-2" + }, + { + "Status": "ACTIVE", + "Networks": "ctlplane=192.0.2.7", + "ID": "6dfd4c85-8a05-49ea-bd73-15dbaf84fa9b", + "Image Name": "overcloud-full", + "Name": "overcloud-controller-0" + }, + { + "Status": "ACTIVE", + "Networks": "ctlplane=192.0.2.8", + "ID": "25b73b47-7c22-4f58-8ff3-ba5d714baa7c", + "Image Name": "overcloud-full", + "Name": "overcloud-controller-1" + }, + { + "Status": "ACTIVE", + "Networks": "ctlplane=192.0.2.6", + "ID": "f214f844-ec4d-4f33-9d16-9aa21ade3cb4", + "Image Name": "overcloud-full", + "Name": "overcloud-novacompute-0" + }, + { + "Status": "ACTIVE", + "Networks": "ctlplane=192.0.2.5", + "ID": "367827af-966c-4c91-bad4-d8dc12750eac", + "Image Name": "overcloud-full", + "Name": "overcloud-novacompute-1" + } +]
\ No newline at end of file diff --git a/tests/unit/ansible_library/modules/apex_test.py b/tests/unit/ansible_library/modules/apex_test.py new file mode 100644 index 00000000..8a1d0673 --- /dev/null +++ b/tests/unit/ansible_library/modules/apex_test.py @@ -0,0 +1,30 @@ +############################################################### +# Copyright (c) 2017 ZTE Corporation +# +# 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 +############################################################################## + +import json +import os + +from qtip.ansible_library.modules import apex + + +def test_generate_inventory(data_root): + baremetal_info = json.load(open(os.path.join(data_root, 'external', + 'apex', 'baremetal_info.json'))) + server_info = json.load(open(os.path.join(data_root, 'external', + 'apex', 'server_info.json'))) + inventory = apex.generate_inventory(baremetal_info, server_info) + assert dict(inventory['hosts']) == { + u'compute': [u'192.0.2.5', u'192.0.2.6'], + u'control': [u'192.0.2.7', u'192.0.2.8', u'192.0.2.9']} + assert dict(inventory['hosts_meta']) == { + u'192.0.2.5': {'ansible_ssh_host': u'192.0.2.5'}, + u'192.0.2.6': {'ansible_ssh_host': u'192.0.2.6'}, + u'192.0.2.7': {'ansible_ssh_host': u'192.0.2.7'}, + u'192.0.2.8': {'ansible_ssh_host': u'192.0.2.8'}, + u'192.0.2.9': {'ansible_ssh_host': u'192.0.2.9'}} diff --git a/tests/unit/cli/options_test.py b/tests/unit/cli/options_test.py index 9dbbe6f3..d7c0f700 100644 --- a/tests/unit/cli/options_test.py +++ b/tests/unit/cli/options_test.py @@ -8,6 +8,7 @@ ############################################################################## import pytest +import re import sys from click.testing import CliRunner @@ -26,7 +27,7 @@ class TestClass(object): def test_version(self, runner): result = runner.invoke(cli, ['--version']) - assert 'dev' in result.output + assert re.search(r'\d+\.\d+\.\d+', result.output) def test_debug(self, runner): runner.invoke(cli, ['-d']) diff --git a/tests/unit/reporter/test_testapi.py b/tests/unit/reporter/test_testapi.py new file mode 100644 index 00000000..85655274 --- /dev/null +++ b/tests/unit/reporter/test_testapi.py @@ -0,0 +1,116 @@ +############################################################################## +# Copyright (c) 2017 akhil.batra@research.iiit.ac.in 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 +############################################################################## + + +import mock +import pytest + +from qtip.reporter import testapi + + +@pytest.mark.parametrize("testapi_url, payload", [ + ("http://testresults.opnfv.org/test/api/v1", {'project_name': 'qtip', + 'case_name': 'fake-case', + 'pod_name': 'fake_pod', + 'installer': 'fake_installer', + 'version': '1', + 'scenario': 'fake_scenario', + 'criteria': 'fake_criteria', + 'build_tag': 'fake_tag', + 'start_date': 'fake_date', + 'stop_date': 'fake_date', + 'details': 'fake:details'}), +]) +@mock.patch('qtip.reporter.testapi.requests') +def test_testapi_unavailable(mock_request, testapi_url, payload): + mock_request.post.return_value.status_code = testapi.requests.codes.unavailable + mock_request.post.return_value.reason = 'Service unavailable' + testapi.push_results(testapi_url, payload) + mock_request.post.assert_called_with(testapi_url + '/results', json=payload) + mock_request.post.return_value.raise_for_status.assert_called() + + +@pytest.mark.parametrize("testapi_url, payload", [ + ("http://testresults.opnfv.org/test/api/v1", {'project_name': 'qtip', + 'case_name': 'fake-case', + 'pod_name': 'fake_pod', + 'installer': 'fake_installer', + 'version': '1', + 'scenario': 'fake_scenario', + 'criteria': 'fake_criteria', + 'build_tag': 'fake_tag', + 'start_date': 'fake_date', + 'stop_date': 'fake_date', + 'details': 'fake:details'}), +]) +@mock.patch('qtip.reporter.testapi.requests') +def test_push_results_success(mock_request, testapi_url, payload): + mock_request.post.return_value.status_code = testapi.requests.codes.ok + mock_request.post.return_value.json.return_value = {'href': 'mock_url'} + push_response = testapi.push_results(testapi_url, payload) + mock_request.post.assert_called_with(testapi_url + '/results', json=payload) + assert push_response['href'] == 'mock_url' + + +@pytest.mark.parametrize("testapi_url, payload", [ + ("http://testresults.opnfv.org/test/api/v1", {'project_name': 'qtip', + 'case_name': 'fake-case', + 'pod_name': 'fake_pod', + 'installer': 'fake_installer', + 'version': '1', + 'scenario': 'fake_scenario', + 'criteria': 'fake_criteria', + 'build_tag': 'fake_tag', + 'start_date': 'fake_date', + 'stop_date': 'fake_date', + 'details': 'fake:details'}), +]) +@mock.patch('qtip.reporter.testapi.requests') +def test_push_results_not_found(mock_request, testapi_url, payload): + mock_request.post.return_value.status_code = testapi.requests.codes.not_found + mock_request.post.return_value.reason = 'Not found' + testapi.push_results(testapi_url, payload) + mock_request.post.assert_called_with(testapi_url + '/results', json=payload) + mock_request.post.return_value.raise_for_status.assert_called() + + +@pytest.mark.parametrize("testapi_url, payload", [ + ("http://testresults.opnfv.org/test/api/v1", {'project_name': 'qtip', + 'case_name': 'fake-case', + 'pod_name': 'fake_pod', + 'installer': 'fake_installer', + 'version': '', + 'scenario': None, + 'criteria': 'fake_criteria', + 'build_tag': 'fake_tag', + 'start_date': 'fake_date', + 'stop_date': 'fake_date', + 'details': 'fake:details'}), +]) +def test_push_results_invalid_params(testapi_url, payload): + with pytest.raises(testapi.InvalidParamsError) as error_invalid: + testapi.push_results(testapi_url, payload) + assert set(error_invalid.value.params) == {'version', 'scenario'} + + +@pytest.mark.parametrize("testapi_url, payload", [ + ("http://testresults.opnfv.org/test/api/v1", {'project_name': 'qtip', + 'case_name': 'fake-case', + 'pod_name': 'fake_pod', + 'installer': 'fake_installer', + 'criteria': 'fake_criteria', + 'build_tag': 'fake_tag', + 'start_date': 'fake_date', + 'stop_date': 'fake_date', + 'details': 'fake:details'}), +]) +def test_push_results_missing_params(testapi_url, payload): + with pytest.raises(testapi.MissingParamsError) as error_missing: + testapi.push_results(testapi_url, payload) + assert set(error_missing.value.params) == {'version', 'scenario'} |