diff --git a/README.rst b/README.rst index b4d44d18..4e7eead1 100644 --- a/README.rst +++ b/README.rst @@ -24,19 +24,18 @@ This is python3 only right now. **TL;DR**: Using tox is convenient for the time being:: # Use the source Luke - git clone https://github.com/dmsimard/ara-django - cd ara-django + git clone https://github.com/openstack/ara-server + cd ara-server # Install tox pip install tox # (or the tox python library from your distro packages) + # Create data from a test playbook and callback + tox -e ansible-playbook + # Run test server -> http://127.0.0.1:8000/api/v1/ tox -e runserver - # Create mock data - source .tox/runserver/bin/activate - python standalone/mockdata.py - # Run actual tests or get coverage tox -e pep8 tox -e py35 @@ -45,12 +44,14 @@ This is python3 only right now. # Build docs tox -e docs +See the ``hacking`` directory for testing resources. + Contributors ============ See contributors on GitHub_. -.. _GitHub: https://github.com/dmsimard/ara-django/graphs/contributors +.. _GitHub: https://github.com/openstack/ara-server/graphs/contributors Copyright ========= diff --git a/doc/source/_static/screenshot.png b/doc/source/_static/screenshot.png index 99aa9763..06dbe786 100644 Binary files a/doc/source/_static/screenshot.png and b/doc/source/_static/screenshot.png differ diff --git a/hacking/callback_plugins/ara.py b/hacking/callback_plugins/ara.py new file mode 100644 index 00000000..c78a68d0 --- /dev/null +++ b/hacking/callback_plugins/ara.py @@ -0,0 +1,262 @@ +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA: Ansible Run Analysis. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + +from __future__ import (absolute_import, division, print_function) + +import datetime +import json +import logging +import os +import six + +from ansible import __version__ as ansible_version +from ansible.plugins.callback import CallbackBase +# To retrieve Ansible CLI options +try: + from __main__ import cli +except ImportError: + cli = None + +try: + # Default to offline API client + from django import setup as django_setup + from django.core.management import execute_from_command_line + from django.test import Client as offline_client + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ara.settings') + + # Automatically create the database and run migrations (is there a better way?) + execute_from_command_line(['django', 'migrate']) + + # Set up the things Django needs + django_setup() + +except ImportError: + # Default to online API client + # TODO + pass + + +class AraOfflineClient(object): + def __init__(self): + self.log = logging.getLogger('ara.client.offline') + self.client = offline_client() + + def _request(self, method, endpoint, **kwargs): + func = getattr(self.client, method) + response = func( + endpoint, + json.dumps(kwargs), + content_type='application/json' + ) + + self.log.debug('HTTP {status}: {method} on {endpoint}'.format( + status=response.status_code, + method=method, + endpoint=endpoint + )) + + if response.status_code not in [200, 201]: + self.log.error( + 'Failed to {method} on {endpoint}: {content}'.format( + method=method, + endpoint=endpoint, + content=kwargs + ) + ) + self.log.fatal(response.content) + + return response.json() + + def get(self, endpoint, **kwargs): + return self._request('get', endpoint, **kwargs) + + def patch(self, endpoint, **kwargs): + return self._request('patch', endpoint, **kwargs) + + def post(self, endpoint, **kwargs): + return self._request('post', endpoint, **kwargs) + + def put(self, endpoint, **kwargs): + return self._request('put', endpoint, **kwargs) + + def delete(self, endpoint, **kwargs): + return self._request('delete', endpoint, **kwargs) + + +class CallbackModule(CallbackBase): + """ + Saves data from an Ansible run into a database + """ + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'awesome' + CALLBACK_NAME = 'ara' + + def __init__(self): + super(CallbackModule, self).__init__() + self.log = logging.getLogger('ara.callback') + + # TODO: logic for picking between offline and online client + self.client = AraOfflineClient() + + self.result = None + self.task = None + self.play = None + self.playbook = None + self.stats = None + self.loop_items = [] + + if cli: + self._options = cli.options + else: + self._options = None + + def v2_playbook_on_start(self, playbook): + self.log.debug('v2_playbook_on_start') + + path = os.path.abspath(playbook._file_name) + if self._options is not None: + parameters = self._options.__dict__.copy() + else: + parameters = {} + + # Create the playbook + self.playbook = self.client.post( + '/api/v1/playbooks/', + ansible_version=ansible_version, + parameters=parameters, + file=dict( + path=path, + content=self._read_file(path) + ) + ) + + # Record all the files involved in the playbook + self._load_files(playbook._loader._FILE_CACHE.keys()) + + return self.playbook + + def v2_playbook_on_play_start(self, play): + self.log.debug('v2_playbook_on_play_start') + self._end_task() + self._end_play() + + # Record all the files involved in the play + self._load_files(play._loader._FILE_CACHE.keys()) + + # Create the play + self.play = self.client.post( + '/api/v1/plays/', + name=play.name, + playbook=self.playbook['id'] + ) + + return self.play + + def v2_playbook_on_task_start(self, task, is_conditional, handler=False): + self.log.debug('v2_playbook_on_task_start') + self._end_task() + + pathspec = task.get_path() + if pathspec: + path, lineno = pathspec.split(':', 1) + lineno = int(lineno) + else: + # Task doesn't have a path, default to "something" + path = self.playbook['path'] + lineno = 1 + + # Ensure this task's file was added to the playbook -- files that are + # dynamically included do not show up in the playbook or play context + self._load_files([path]) + + # Find the task file (is there a better way?) + task_file = self.playbook['file']['id'] + for file in self.playbook['files']: + if file['path'] == path: + task_file = file['id'] + break + + self.task = self.client.post( + '/api/v1/tasks/', + name=task.get_name(), + action=task.action, + play=self.play['id'], + playbook=self.playbook['id'], + file=task_file, + tags=task._attributes['tags'], + lineno=lineno, + handler=handler + ) + + return self.task + + def v2_playbook_on_stats(self, stats): + self.log.debug('v2_playbook_on_stats') + + self._end_task() + self._end_play() + self._end_playbook() + + def _end_task(self): + if self.task is not None: + self.client.patch( + '/api/v1/tasks/%s/' % self.task['id'], + completed=True, + ended=datetime.datetime.now().isoformat() + ) + self.task = None + self.loop_items = [] + + def _end_play(self): + if self.play is not None: + self.client.patch( + '/api/v1/plays/%s/' % self.play['id'], + completed=True, + ended=datetime.datetime.now().isoformat() + ) + self.play = None + + def _end_playbook(self): + self.playbook = self.client.patch( + '/api/v1/playbooks/%s/' % self.playbook['id'], + completed=True, + ended=datetime.datetime.now().isoformat() + ) + + def _load_files(self, files): + self.log.debug('Loading %s file(s)...' % len(files)) + playbook_files = [file['path'] for file in self.playbook['files']] + for file in files: + if file not in playbook_files: + self.client.post( + '/api/v1/playbooks/%s/files/' % self.playbook['id'], + path=file, + content=self._read_file(file) + ) + + def _read_file(self, path): + try: + with open(path, 'r') as fd: + content = fd.read() + except IOError as e: + self.log.error("Unable to open {0} for reading: {1}".format( + path, six.text_type(e) + )) + content = """ARA was not able to read this file successfully. + Refer to the logs for more information""" + return content diff --git a/hacking/roles/hacking/defaults/main.yml b/hacking/roles/hacking/defaults/main.yml new file mode 100644 index 00000000..5f4601c1 --- /dev/null +++ b/hacking/roles/hacking/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + +filename: /tmp/ara-hacking \ No newline at end of file diff --git a/hacking/roles/hacking/handlers/main.yml b/hacking/roles/hacking/handlers/main.yml new file mode 100644 index 00000000..99628bca --- /dev/null +++ b/hacking/roles/hacking/handlers/main.yml @@ -0,0 +1,22 @@ +--- +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + +- name: Create a file + template: + src: hacking.j2 + dest: "{{ filename }}" diff --git a/hacking/roles/hacking/meta/main.yml b/hacking/roles/hacking/meta/main.yml new file mode 100644 index 00000000..391f042a --- /dev/null +++ b/hacking/roles/hacking/meta/main.yml @@ -0,0 +1,42 @@ +--- +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + +galaxy_info: + author: OpenStack community + description: Hacking + company: Red Hat + license: GPL v3 + min_ansible_version: 2.5 + platforms: + - name: EL + versions: + - all + - name: Fedora + versions: + - all + - name: Ubuntu + versions: + - all + - name: Debian + versions: + - all + galaxy_tags: + - installer + - application + - system +dependencies: [] diff --git a/hacking/roles/hacking/tasks/main.yml b/hacking/roles/hacking/tasks/main.yml new file mode 100644 index 00000000..e162a94d --- /dev/null +++ b/hacking/roles/hacking/tasks/main.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of ARA Records Ansible. +# +# ARA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ARA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ARA. If not, see . + +- name: echo something + command: echo something + register: echo + +- name: debug something + debug: + var: echo + +- name: Ensure a file is absent + file: + path: "{{ filename }}" + state: absent + notify: + - create a file diff --git a/hacking/roles/hacking/templates/hacking.j2 b/hacking/roles/hacking/templates/hacking.j2 new file mode 100644 index 00000000..70c379b6 --- /dev/null +++ b/hacking/roles/hacking/templates/hacking.j2 @@ -0,0 +1 @@ +Hello world \ No newline at end of file diff --git a/hacking/test-playbook.yml b/hacking/test-playbook.yml new file mode 100644 index 00000000..07dff788 --- /dev/null +++ b/hacking/test-playbook.yml @@ -0,0 +1,22 @@ +- name: A test play + hosts: localhost + tasks: + - name: Include a role + include_role: + name: hacking + +- name: Another test play + hosts: localhost + gather_facts: no + pre_tasks: + - name: Pre task + debug: + msg: Task from a pre-task + tasks: + - name: Task + debug: + msg: Task from a task + post_tasks: + - name: Post Task + debug: + msg: Task from a post-task diff --git a/standalone/mockdata.py b/standalone/mockdata.py deleted file mode 100644 index 9fbcc7e4..00000000 --- a/standalone/mockdata.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2018 Red Hat, Inc. -# -# This file is part of ARA Records Ansible. -# -# ARA is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ARA is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with ARA. If not, see . - -# Creates mock data offline leveraging the API -import django -import json -import os -import sys - -parent_directory = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -sys.path.append(parent_directory) -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ara.settings') -django.setup() - -from django.test import Client - - -def post(endpoint, data): - client = Client() - print("Posting to %s..." % endpoint) - obj = client.post(endpoint, - json.dumps(data), - content_type="application/json") - print("HTTP %s" % obj.status_code) - print("Got: %s" % json.dumps(obj.json(), indent=2)) - print("#" * 40) - return obj.json() - - -playbook = post( - '/api/v1/playbooks/', - { - 'started': '2016-05-06T17:20:25.749489-04:00', - 'ansible_version': '2.3.4', - 'completed': False, - 'parameters': {'foo': 'bar'} - } -) - -playbook_with_files = post( - '/api/v1/playbooks/', - { - 'started': '2016-05-06T17:20:25.749489-04:00', - 'ansible_version': '2.3.4', - 'completed': False, - 'parameters': {'foo': 'bar'}, - 'files': [ - {'path': '/tmp/1/playbook.yml', 'content': '# playbook'}, - {'path': '/tmp/2/playbook.yml', 'content': '# playbook'} - ], - } -) - -play = post( - '/api/v1/plays/', - { - 'started': '2016-05-06T17:20:25.749489-04:00', - 'name': 'Test play', - 'playbook': playbook['id'] - } -) diff --git a/tox.ini b/tox.ini index 7ad72e6d..6259d470 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,14 @@ commands = setenv = DJANGO_DEBUG=1 +[testenv:ansible-playbook] +deps = ansible +commands = + ansible-playbook {toxinidir}/hacking/test-playbook.yml +setenv = + DJANGO_DEBUG=1 + DJANGO_LOG_LEVEL=DEBUG + [testenv:cover] commands = coverage erase