aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--qtip/ansible_library/modules/apex.py133
-rw-r--r--qtip/ansible_library/plugins/action/collect.py2
-rw-r--r--qtip/cli/entry.py3
-rw-r--r--resources/metric/nDPI.yaml80
-rw-r--r--resources/template/dpi-metrics.j27
-rw-r--r--setup.cfg1
-rwxr-xr-xtests/data/external/apex/baremetal_info.json57
-rwxr-xr-xtests/data/external/apex/server_info.json37
-rw-r--r--tests/integration/ansible.cfg2
-rw-r--r--tests/integration/group_vars/all1
-rw-r--r--tests/integration/run.yaml11
-rw-r--r--tests/unit/ansible_library/modules/apex_test.py30
-rw-r--r--tests/unit/cli/options_test.py3
-rw-r--r--tests/unit/reporter/test_testapi.py116
15 files changed, 483 insertions, 5 deletions
diff --git a/.gitignore b/.gitignore
index 2b3bc644..a098d85d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,7 +75,8 @@ ChangeLog
!/tests/data/**/*
# integration data
+/tests/integration/*.retry
+/tests/integration/dump
/tests/integration/hosts
+/tests/integration/reports
/tests/integration/ssh.cfg
-/tests/integration/*.retry
-/tests/integration/reports/*
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/ansible_library/plugins/action/collect.py b/qtip/ansible_library/plugins/action/collect.py
index 26e813b8..8c825317 100644
--- a/qtip/ansible_library/plugins/action/collect.py
+++ b/qtip/ansible_library/plugins/action/collect.py
@@ -29,7 +29,7 @@ class ActionModule(ActionBase):
dump = self._task.args.get('dump')
if dump is not None:
- dump_facts(task_vars['inventory_hostname'], [{'name': 'inxi.log', 'content': string}])
+ dump_facts(task_vars['inventory_hostname'], [{'name': dump, 'content': string}])
return collect(patterns, string)
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:
diff --git a/resources/metric/nDPI.yaml b/resources/metric/nDPI.yaml
new file mode 100644
index 00000000..0391302a
--- /dev/null
+++ b/resources/metric/nDPI.yaml
@@ -0,0 +1,80 @@
+##############################################################################
+# Copyright (c) 2017 ZTE Corporation 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
+##############################################################################
+
+
+- name: prepare sample pcap file
+ get_url:
+ url: "https://build.opnfv.org/artifacts.opnfv.org/qtip/utilities/test.pcap"
+ dest: "{{ qtip_dump }}/localhost/{{ nDPI_file }}"
+ validate_certs: no # required when using proxy for https
+ run_once: yes
+ delegate_to: localhost
+
+- name: installing nDPI dependencies if CentOS
+ yum:
+ name: '{{ item }}'
+ state: present
+ when: ansible_os_family == "RedHat"
+ with_items:
+ - git
+ - gcc
+ - patch
+ - perl-Time-HiRes
+ - autofconf
+ - automake
+ - libpcap-devel libtool
+
+- name: installing nDPI dependencies if Ubuntu
+ apt:
+ name: build-essential
+ state: present
+ when: ansible_os_family == "Debian"
+
+- name: making nDPI temporary directory
+ file:
+ path: "{{ nDPI_cwd }}"
+ state: directory
+
+- name: clone nDPI
+ git:
+ repo: https://github.com/ntop/nDPI.git
+ dest: "{{ nDPI_cwd }}"
+ depth: 1
+ update: no
+
+- name: build nDPI library
+ command: '{{ item }}'
+ with_items:
+ - ./autogen.sh
+ - ./configure
+ - make
+ args:
+ chdir: "{{ nDPI_cwd }}"
+ creates: example/ndpiReader
+
+- name: copy sample packet file
+ copy:
+ src: "{{ qtip_dump }}/localhost/{{ nDPI_file }}"
+ dest: "{{ nDPI_cwd }}/example/{{ nDPI_file }}"
+
+- name:
+ command: "./ndpiReader -i {{ nDPI_file }}"
+ args:
+ chdir: "{{ nDPI_cwd }}/example/"
+ register: nDPI_out
+
+- name: collect DPI metrics from nDPI
+ collect:
+ string: "{{ nDPI_out.stdout }}"
+ patterns:
+ # nDPI throughput: 1.46 M pps / 13.69 Gb/sec
+ # TODO(yujunz) convert "M pps" and "K pps" to number
+ - 'nDPI throughput:\s+?(?P<dpi_pps>\d+.\d+.*) \/ (?P<dpi_bps>\d+.\d+.*)$'
+ dump: 'nDPI.log'
+ register: dpi_metrics
diff --git a/resources/template/dpi-metrics.j2 b/resources/template/dpi-metrics.j2
new file mode 100644
index 00000000..4de200b6
--- /dev/null
+++ b/resources/template/dpi-metrics.j2
@@ -0,0 +1,7 @@
+nDPI throughput
+===============
+
+{% for host in groups['compute'] %}
+{% set dpi_metrics=hostvars[host].dpi_metrics %}
+{{ (hostvars[host].ansible_hostname, "{} / {}".format(dpi_metrics.dpi_pps[0], dpi_metrics.dpi_bps[0]))|justify }}
+{% endfor %}
diff --git a/setup.cfg b/setup.cfg
index 23103fc5..71b8c8fd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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/integration/ansible.cfg b/tests/integration/ansible.cfg
index 40e28bcf..30e28480 100644
--- a/tests/integration/ansible.cfg
+++ b/tests/integration/ansible.cfg
@@ -26,3 +26,5 @@ filter_plugins = ../../qtip/ansible_library/plugins/filter
#test_plugins = /usr/share/ansible/plugins/test
#terminal_plugins = /usr/share/ansible/plugins/terminal
#strategy_plugins = /usr/share/ansible/plugins/strategy
+
+callback_whitelist = profile_tasks
diff --git a/tests/integration/group_vars/all b/tests/integration/group_vars/all
index 16a93f46..a84ab51f 100644
--- a/tests/integration/group_vars/all
+++ b/tests/integration/group_vars/all
@@ -1,3 +1,4 @@
qtip_resources: ../../resources
qtip_reports: ./reports
qtip_fixtures: ./fixtures
+qtip_dump: ./dump
diff --git a/tests/integration/run.yaml b/tests/integration/run.yaml
index 7545a65e..f02ebf89 100644
--- a/tests/integration/run.yaml
+++ b/tests/integration/run.yaml
@@ -21,6 +21,12 @@
- name: ssl metrics
include: "{{ qtip_resources }}/metric/openssl.yaml"
tags: [ssl]
+ - name: DPI metrics
+ include: "{{ qtip_resources }}/metric/nDPI.yaml"
+ vars:
+ nDPI_cwd: "{{ ansible_env.HOME }}/qtip/nDPI"
+ nDPI_file: "dpi.pcap"
+ tags: [dpi]
- hosts: compute
tasks:
@@ -57,6 +63,11 @@
src: "{{ qtip_resources }}/template/qpi-report.j2"
dest: "{{ qtip_reports }}/qpi-report"
tags: [report]
+ - name: create dpi metrics report
+ template:
+ src: "{{ qtip_resources }}/template/dpi-metrics.j2"
+ dest: "{{ qtip_reports }}/dpi-metrics"
+ tags: [report, dpi]
- name: push result to testapi
uri:
url: "{{ testapi_url }}/results"
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'}