├── tests ├── hosts ├── test_ansible_notify.yml ├── ansible.cfg ├── alembic.test.ini └── __init__.py ├── alembic ├── README ├── script.py.mako ├── versions │ ├── 53eddd07c597_add_column_to_task_t.py │ ├── 53fae050253c_add_playbook_table.py │ ├── 3736909be4d5_add_indexes.py │ ├── 16e310bda4a9_add_changed_column.py │ └── 2f3bd55d88a_base_tables.py └── env.py ├── MANIFEST.in ├── .gitignore ├── createdb.py ├── lib └── ansiblereport │ ├── __init__.py │ ├── output.py │ ├── output_plugins.py │ ├── constants.py │ ├── manager.py │ ├── model.py │ └── utils.py ├── alembic.ini.sample ├── doc └── man │ └── man3 │ ├── ansiblereport.logstalgia.3.md │ └── ansiblereport.logstalgia.3 ├── setup.py ├── hacking └── env-setup ├── packaging └── rpm │ └── ansible-report.spec ├── Makefile ├── README.md ├── plugins ├── output_plugins │ ├── screen.py │ ├── email.py │ └── logstalgia.py └── callback_plugins │ └── ansiblereport-logger.py ├── bin └── ansible-report └── COPYING /tests/hosts: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md COPYING 2 | recursive-include plugins * 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build / distutils 2 | *.py[co] 3 | *.*.swp 4 | *~ 5 | build 6 | alembic.ini 7 | # rpm 8 | MANIFEST 9 | dist 10 | rpm-build 11 | -------------------------------------------------------------------------------- /tests/test_ansible_notify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | gather_facts: no 5 | 6 | tasks: 7 | - action: ping 8 | 9 | - command: hostname 10 | notify: get uptime 11 | 12 | handlers: 13 | - name: get uptime 14 | command: uptime 15 | 16 | -------------------------------------------------------------------------------- /tests/ansible.cfg: -------------------------------------------------------------------------------- 1 | # config file for ansible -- http://ansible.github.com 2 | # nearly all parameters can be overridden in ansible-playbook or with command line flags 3 | # ansible will read ~/.ansible.cfg or /etc/ansible/ansible.cfg, whichever it finds first 4 | 5 | [defaults] 6 | 7 | # location of ansible library, eliminates need to specify --module-path 8 | 9 | library = /usr/share/ansible:./ 10 | callback_plugins = plugins/callback_plugins 11 | 12 | [ansiblereport] 13 | sqlalchemy.url = sqlite:///tests/test.sqlite 14 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /createdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | __requires__ = ['SQLAlchemy >= 0.7'] 4 | import pkg_resources 5 | 6 | import sys 7 | from optparse import OptionParser 8 | from ansiblereport.model import * 9 | 10 | def main(args): 11 | usage = "usage: %prog [options]" 12 | parser = OptionParser(usage=usage) 13 | parser.add_option('-c', '--conf', default=None, help='path to alembic.ini') 14 | parser.add_option('-d', '--debug', action='store_true', 15 | default=False, help='Debug mode') 16 | options, args = parser.parse_args() 17 | session = init_db_session(options.conf, options.debug) 18 | return 0 19 | 20 | if __name__ == '__main__': 21 | try: 22 | sys.exit(main(sys.argv[1:])) 23 | except KeyboardInterrupt, e: 24 | print >> sys.stderr, "error: %s" % str(e) 25 | sys.exit(1) 26 | -------------------------------------------------------------------------------- /alembic/versions/53eddd07c597_add_column_to_task_t.py: -------------------------------------------------------------------------------- 1 | """add column to task table 2 | 3 | Revision ID: 53eddd07c597 4 | Revises: 53fae050253c 5 | Create Date: 2013-05-01 10:34:35.388559 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '53eddd07c597' 11 | down_revision = '53fae050253c' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | import logging 16 | 17 | 18 | def upgrade(): 19 | op.add_column('task', 20 | sa.Column('playbook_id', sa.Integer, nullable=True)) 21 | try: 22 | op.create_foreign_key('fk_task_playbook', 'task', 'playbook', 23 | ['playbook_id'], ['id']) 24 | except NotImplementedError, e: 25 | logging.info("Failed to create foreign key constraint: %s", str(e)) 26 | 27 | 28 | def downgrade(): 29 | op.drop_column('task', 'playbook_id') 30 | -------------------------------------------------------------------------------- /lib/ansiblereport/__init__.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | __name__ = 'ansible-report' 20 | __author__ = 'Stephen Fromm' 21 | __dbversion__ = 1 22 | __version__ = '0.1' 23 | -------------------------------------------------------------------------------- /alembic.ini.sample: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | sqlalchemy.url = sqlite:// 15 | 16 | 17 | # Logging configuration 18 | [loggers] 19 | keys = root,sqlalchemy,alembic 20 | 21 | [handlers] 22 | keys = console 23 | 24 | [formatters] 25 | keys = generic 26 | 27 | [logger_root] 28 | level = WARN 29 | handlers = console 30 | qualname = 31 | 32 | [logger_sqlalchemy] 33 | level = WARN 34 | handlers = 35 | qualname = sqlalchemy.engine 36 | 37 | [logger_alembic] 38 | level = INFO 39 | handlers = 40 | qualname = alembic 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /tests/alembic.test.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | sqlalchemy.url = sqlite:///:memory: 15 | 16 | 17 | # Logging configuration 18 | [loggers] 19 | keys = root,sqlalchemy,alembic 20 | 21 | [handlers] 22 | keys = console 23 | 24 | [formatters] 25 | keys = generic 26 | 27 | [logger_root] 28 | level = WARN 29 | handlers = console 30 | qualname = 31 | 32 | [logger_sqlalchemy] 33 | level = WARN 34 | handlers = 35 | qualname = sqlalchemy.engine 36 | 37 | [logger_alembic] 38 | level = INFO 39 | handlers = 40 | qualname = alembic 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /alembic/versions/53fae050253c_add_playbook_table.py: -------------------------------------------------------------------------------- 1 | """add playbook table 2 | 3 | Revision ID: 53fae050253c 4 | Revises: 2f3bd55d88a 5 | Create Date: 2013-04-22 22:59:21.598422 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '53fae050253c' 11 | down_revision = '2f3bd55d88a' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_table('playbook', 19 | sa.Column('id', sa.Integer(), nullable=False), 20 | sa.Column('path', sa.String(), nullable=True), 21 | sa.Column('uuid', sa.String(), nullable=True), 22 | sa.Column('connection', sa.String(), nullable=True), 23 | sa.Column('starttime', sa.DateTime(), nullable=True), 24 | sa.Column('endtime', sa.DateTime(), nullable=True), 25 | sa.Column('checksum', sa.DateTime(), nullable=True), 26 | sa.Column('user_id', sa.Integer(), nullable=True), 27 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | 31 | 32 | def downgrade(): 33 | op.drop_table('playbook') 34 | -------------------------------------------------------------------------------- /doc/man/man3/ansiblereport.logstalgia.3.md: -------------------------------------------------------------------------------- 1 | ANSIBLEREPORT.LOGSTALGIA(3) 2 | =========================== 3 | 4 | NAME 5 | ---- 6 | logstalgia - Logstalgia output plugin for visualization 7 | 8 | SYNOPSIS 9 | -------- 10 | ansible-report -o logstalgia [options] 11 | 12 | DESCRIPTION 13 | ----------- 14 | 15 | This will query the *ansible-report* database with the provided criteria 16 | and then pipe that into Logstalgia (http://code.google.com/p/logstalgia/) 17 | for visualization. You must have Logstalgia already installed. You can 18 | provide extra arguments to Logstalgia if needed. 19 | 20 | OPTIONS 21 | ------- 22 | 23 | *-e* logstalgia_opts=options, *--extra-args* logstalgia_opts=options 24 | 25 | Provide additional options to Logstalgia when rendering the video. One 26 | example would be: 27 | 28 | -e logstalgia_opts='-1280x720 --output-ppm-stream logs.ppm' 29 | 30 | This would allow you to post-process the PPM file with something like 31 | ffmpeg. More details on recording videos can be found at: 32 | 33 | http://code.google.com/p/logstalgia/wiki/Videos 34 | 35 | COPYRIGHT 36 | --------- 37 | 38 | Copyright, 2014, University of Oregon 39 | 40 | *ansible-report* is released under the terms of the GPLv3 License. 41 | -------------------------------------------------------------------------------- /doc/man/man3/ansiblereport.logstalgia.3: -------------------------------------------------------------------------------- 1 | .TH "" "" 2 | .SH ANSIBLEREPORT.LOGSTALGIA(3) 3 | .SS NAME 4 | .PP 5 | logstalgia \- Logstalgia output plugin for visualization 6 | .SS SYNOPSIS 7 | .PP 8 | ansible\-report \-o logstalgia [options] 9 | .SS DESCRIPTION 10 | .PP 11 | This will query the \f[I]ansible\-report\f[] database with the provided 12 | criteria and then pipe that into Logstalgia 13 | (http://code.google.com/p/logstalgia/) for visualization. 14 | You must have Logstalgia already installed. 15 | You can provide extra arguments to Logstalgia if needed. 16 | .SS OPTIONS 17 | .PP 18 | \f[I]\-e\f[] logstalgia_opts=options, \f[I]\-\-extra\-args\f[] 19 | logstalgia_opts=options 20 | .PP 21 | Provide additional options to Logstalgia when rendering the video. 22 | One example would be: 23 | .IP 24 | .nf 25 | \f[C] 26 | \-e\ logstalgia_opts=\[aq]\-1280x720\ \-\-output\-ppm\-stream\ logs.ppm\[aq] 27 | \f[] 28 | .fi 29 | .PP 30 | This would allow you to post\-process the PPM file with something like 31 | ffmpeg. 32 | More details on recording videos can be found at: 33 | .IP 34 | .nf 35 | \f[C] 36 | http://code.google.com/p/logstalgia/wiki/Videos\ \ \ \ 37 | \f[] 38 | .fi 39 | .SS COPYRIGHT 40 | .PP 41 | Copyright, 2014, University of Oregon 42 | .PP 43 | \f[I]ansible\-report\f[] is released under the terms of the GPLv3 44 | License. 45 | -------------------------------------------------------------------------------- /alembic/versions/3736909be4d5_add_indexes.py: -------------------------------------------------------------------------------- 1 | """add indexes 2 | 3 | Revision ID: 3736909be4d5 4 | Revises: 53eddd07c597 5 | Create Date: 2013-05-09 10:09:49.884197 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3736909be4d5' 11 | down_revision = '53eddd07c597' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | op.create_index('task_timestamp_idx', 'task', ['timestamp']) 19 | op.create_index('task_hostname_idx', 'task', ['hostname']) 20 | op.create_index('task_module_idx', 'task', ['module']) 21 | op.create_index('task_result_idx', 'task', ['result']) 22 | op.create_index('user_username_idx', 'user', ['username']) 23 | op.create_index('playbook_path_idx', 'playbook', ['path']) 24 | op.create_index('playbook_uuid_idx', 'playbook', ['uuid']) 25 | op.create_index('playbook_connection_idx', 'playbook', ['connection']) 26 | op.create_index('playbook_starttime_idx', 'playbook', ['starttime']) 27 | 28 | def downgrade(): 29 | op.create_index('task_timestamp_idx') 30 | op.create_index('task_hostname_idx') 31 | op.create_index('task_module_idx') 32 | op.create_index('task_result_idx') 33 | op.create_index('user_username_idx') 34 | op.create_index('playbook_path_idx') 35 | op.create_index('playbook_uuid_idx') 36 | op.create_index('playbook_connection_idx') 37 | op.create_index('playbook_starttime_idx') 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import glob 6 | 7 | __requires__ = ['SQLAlchemy >= 0.7'] 8 | import pkg_resources 9 | 10 | sys.path.append( os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), 'lib') ) 12 | 13 | from ansiblereport import __version__, __author__, __name__ 14 | from distutils.core import setup 15 | 16 | data_files = [] 17 | plugins = [] 18 | ANSIBLE_PLUGIN_PATH = 'share/ansible_plugins/callback_plugins' 19 | for path in glob.glob(os.path.join('plugins', 'callback_plugins', '*.py')): 20 | plugins.append(path) 21 | data_files.append((ANSIBLE_PLUGIN_PATH, plugins)) 22 | 23 | plugins = [] 24 | OUTPUT_PLUGIN_PATH = 'share/ansible-report/plugins' 25 | for path in glob.glob(os.path.join('plugins', 'output_plugins', '*.py')): 26 | plugins.append(path) 27 | data_files.append((OUTPUT_PLUGIN_PATH, plugins)) 28 | 29 | print "output plugins=%s" % data_files 30 | 31 | setup(name=__name__, 32 | version=__version__, 33 | author=__author__, 34 | author_email='sfromm@gmail.com', 35 | url='https://github.com/sfromm/ansible-report', 36 | description='Utility to log and report ansible activity', 37 | license='GPLv3', 38 | package_dir={ 'ansiblereport': 'lib/ansiblereport' }, 39 | packages=['ansiblereport'], 40 | scripts=['bin/ansible-report'], 41 | data_files=data_files, 42 | install_requires=['SQLAlchemy>=0.6', 'alembic', 'dateutil'], 43 | ) 44 | -------------------------------------------------------------------------------- /lib/ansiblereport/output.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | class OutputModule: 20 | ''' 21 | A generic plugin that all output plugins should implement 22 | for ansible-report to recognize them. It should implement the 23 | following: 24 | 25 | name Attribute with the name of the plugin 26 | do_report Method that will take a list of events and report 27 | them in some manner. It also takes an optional 28 | set of keyword arguments 29 | ''' 30 | name = 'generic' 31 | 32 | def do_report(self, events, **kwargs): 33 | ''' take list of events and do something with them ''' 34 | pass 35 | -------------------------------------------------------------------------------- /alembic/versions/16e310bda4a9_add_changed_column.py: -------------------------------------------------------------------------------- 1 | """add changed column 2 | 3 | Revision ID: 16e310bda4a9 4 | Revises: 3736909be4d5 5 | Create Date: 2014-02-17 22:17:35.752109 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '16e310bda4a9' 11 | down_revision = '3736909be4d5' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | import json 16 | 17 | taskhelper = sa.Table( 18 | 'task', 19 | sa.MetaData(), 20 | sa.Column('id', sa.Integer()), 21 | sa.Column('hostname', sa.String()), 22 | sa.Column('module', sa.String()), 23 | sa.Column('result', sa.String()), 24 | sa.Column('data', sa.Text()), 25 | sa.Column('changed', sa.Boolean()), 26 | sa.Column('playbook_id', sa.Integer()) 27 | ) 28 | 29 | def upgrade(): 30 | op.add_column('task', 31 | sa.Column('changed', sa.Boolean, nullable=True)) 32 | op.create_index('task_changed_idx', 'task', ['changed']) 33 | connection = op.get_bind() 34 | for task in connection.execute(taskhelper.select()): 35 | data = json.loads(task.data) 36 | changed = False 37 | if 'changed' in data: 38 | changed = data['changed'] 39 | connection.execute( 40 | taskhelper.update().where( 41 | taskhelper.columns.id == task.id 42 | ).values(changed=changed) 43 | ) 44 | 45 | def downgrade(): 46 | op.drop_column('task', 'changed') 47 | op.create_index('task_changed_idx') 48 | -------------------------------------------------------------------------------- /alembic/versions/2f3bd55d88a_base_tables.py: -------------------------------------------------------------------------------- 1 | """base tables 2 | 3 | Revision ID: 2f3bd55d88a 4 | Revises: None 5 | Create Date: 2013-04-22 15:26:47.296443 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2f3bd55d88a' 11 | down_revision = None 12 | 13 | from ansiblereport.model import JSONEncodedDict 14 | from alembic import op 15 | import sqlalchemy as sa 16 | 17 | 18 | def upgrade(): 19 | ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('user', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('username', sa.String(), nullable=True), 23 | sa.Column('euid', sa.Integer(), nullable=True), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_table('task', 27 | sa.Column('id', sa.Integer(), nullable=False), 28 | sa.Column('timestamp', sa.DateTime(), nullable=True), 29 | sa.Column('hostname', sa.String(), nullable=True), 30 | sa.Column('module', sa.String(), nullable=True), 31 | sa.Column('result', sa.String(), nullable=True), 32 | sa.Column('data', JSONEncodedDict(), nullable=True), 33 | sa.Column('user_id', sa.Integer(), nullable=True), 34 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 35 | sa.PrimaryKeyConstraint('id') 36 | ) 37 | ### end Alembic commands ### 38 | 39 | 40 | def downgrade(): 41 | ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_table('task') 43 | op.drop_table('user') 44 | ### end Alembic commands ### 45 | -------------------------------------------------------------------------------- /hacking/env-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # usage: source ./hacking/env-setup [-q] 3 | # modifies environment for running Ansible from checkout 4 | 5 | # When run using source as directed, $0 gets set to bash, so we must use $BASH_SOURCE 6 | if [ -n "$BASH_SOURCE" ] ; then 7 | HACKING_DIR=`dirname $BASH_SOURCE` 8 | else 9 | HACKING_DIR="$PWD/hacking" 10 | fi 11 | # The below is an alternative to readlink -fn which doesn't exist on OS X 12 | # Source: http://stackoverflow.com/a/1678636 13 | FULL_PATH=`python -c "import os; print(os.path.realpath('$HACKING_DIR'))"` 14 | ANSIBLEREPORT_HOME=`dirname "$FULL_PATH"` 15 | 16 | PREFIX_PYTHONPATH="$ANSIBLEREPORT_HOME/lib" 17 | PREFIX_PATH="$ANSIBLEREPORT_HOME/bin" 18 | PREFIX_MANPATH="$ANSIBLEREPORT_HOME/docs/man" 19 | 20 | [[ $PYTHONPATH != ${PREFIX_PYTHONPATH}* ]] && export PYTHONPATH=$PREFIX_PYTHONPATH:$PYTHONPATH 21 | [[ $PATH != ${PREFIX_PATH}* ]] && export PATH=$PREFIX_PATH:$PATH 22 | export ANSIBLE_CALLBACK_PLUGINS="$ANSIBLEREPORT_HOME/plugins/callback_plugins" 23 | export ANSIBLEREPORT_OUTPUT_PLUGINS="$ANSIBLEREPORT_HOME/plugins/output_plugins" 24 | [[ $MANPATH != ${PREFIX_MANPATH}* ]] && export MANPATH=$PREFIX_MANPATH:$MANPATH 25 | 26 | # Print out values unless -q is set 27 | 28 | if [ $# -eq 0 -o "$1" != "-q" ] ; then 29 | echo "" 30 | echo "Setting up to run ansible-report out of checkout..." 31 | echo "" 32 | echo "PATH=$PATH" 33 | echo "PYTHONPATH=$PYTHONPATH" 34 | echo "ANSIBLE_CALLBACK_PLUGINS=$ANSIBLE_CALLBACK_PLUGINS" 35 | echo "ANSIBLEREPORT_OUTPUT_PLUGINS=$ANSIBLEREPORT_OUTPUT_PLUGINS" 36 | echo "MANPATH=$MANPATH" 37 | echo "" 38 | fi 39 | -------------------------------------------------------------------------------- /packaging/rpm/ansible-report.spec: -------------------------------------------------------------------------------- 1 | Name: ansible-report 2 | Version: 0.1 3 | Release: 1%{?dist} 4 | Summary: Reporting tool for Ansible 5 | 6 | Group: Development/Libraries 7 | License: GPLv3 8 | URL: https://github.com/sfromm/ansible-report 9 | #Source0: https://github.com/sfromm/ansible-report/archive/release%{version}.tar.gz 10 | Source0: %{name}-%{version}.tar.gz 11 | 12 | BuildArch: noarch 13 | BuildRequires: python-devel 14 | Requires: python-alembic 15 | Requires: python-dateutil 16 | Requires: ansible 17 | %if ( 0%{?rhel} && 0%{?rhel} == 6 ) 18 | BuildRequires: python-sqlalchemy0.7 19 | Requires: python-sqlalchemy0.7 20 | %else 21 | BuildRequires: python-sqlalchemy > 0.5 22 | Requires: python-sqlalchemy > 0.5 23 | %endif 24 | Provides: ansiblereport 25 | 26 | %description 27 | A utility to record events in a database via ansible callbacks and then 28 | report on them at a later date. 29 | 30 | %prep 31 | %setup -q 32 | 33 | %build 34 | %{__python} setup.py build 35 | 36 | %install 37 | %{__python} setup.py install -O1 --root=$RPM_BUILD_ROOT 38 | 39 | %clean 40 | rm -rf $RPM_BUILD_ROOT 41 | 42 | %files 43 | %defattr(-,root,root) 44 | %{python_sitelib}/ansiblereport/* 45 | %{python_sitelib}/ansible_report*egg-info 46 | %{_bindir}/ansible-report 47 | %{_datadir}/ansible_plugins/callback_plugins/ansiblereport-logger.py 48 | %{_datadir}/ansible-report/plugins/*.py 49 | %doc README.md COPYING 50 | 51 | %changelog 52 | * Fri Jun 21 2013 Stephen Fromm 53 | - Fixes to RPM spec to be consistent with name usage. 54 | 55 | * Thu May 2 2013 Stephen Fromm - 0.1-0 56 | - Initial version 57 | -------------------------------------------------------------------------------- /lib/ansiblereport/output_plugins.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | import imp 20 | import glob 21 | import os 22 | import logging 23 | 24 | class OutputPlugins: 25 | ''' manager class for ansible-report plugins ''' 26 | 27 | def __init__(self, plugin_path): 28 | ''' Initialize the plugin manager 29 | 30 | plugin_path: a list of paths to look for plugin modules 31 | ''' 32 | 33 | self.plugin_path = plugin_path 34 | self.plugins = {} 35 | self._get_plugins() 36 | 37 | def _load_plugin(self, path): 38 | ''' for a given path, try to load the module ''' 39 | dir_name = os.path.dirname(path) 40 | name, ext = os.path.splitext(os.path.basename(path)) 41 | if path in self.plugins: 42 | return 43 | try: 44 | (fp, pathname, description) = imp.find_module(name, [dir_name]) 45 | try: 46 | module = imp.load_module(name, fp, pathname, description) 47 | finally: 48 | fp.close() 49 | except Exception, e: 50 | logging.error("failed to load plugin '%s': %s", name, str(e)) 51 | return 52 | self.plugins[name] = module.OutputModule() 53 | 54 | def _get_plugins(self): 55 | ''' iterate over plugin_path and load plugins ''' 56 | for dir_name in self.plugin_path: 57 | for path in glob.glob(os.path.join(dir_name, '*.py')): 58 | self._load_plugin(path) 59 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | # target_metadata = None 19 | from sqlalchemy.types import UserDefinedType 20 | from ansiblereport.model import Base 21 | from ansiblereport.model import JSONEncodedDict 22 | target_metadata = Base.metadata 23 | 24 | # other values from the config, defined by the needs of env.py, 25 | # can be acquired: 26 | # my_important_option = config.get_main_option("my_important_option") 27 | # ... etc. 28 | 29 | def run_migrations_offline(): 30 | """Run migrations in 'offline' mode. 31 | 32 | This configures the context with just a URL 33 | and not an Engine, though an Engine is acceptable 34 | here as well. By skipping the Engine creation 35 | we don't even need a DBAPI to be available. 36 | 37 | Calls to context.execute() here emit the given string to the 38 | script output. 39 | 40 | """ 41 | url = config.get_main_option("sqlalchemy.url") 42 | context.configure(url=url) 43 | 44 | with context.begin_transaction(): 45 | context.run_migrations() 46 | 47 | def run_migrations_online(): 48 | """Run migrations in 'online' mode. 49 | 50 | In this scenario we need to create an Engine 51 | and associate a connection with the context. 52 | 53 | """ 54 | engine = engine_from_config( 55 | config.get_section(config.config_ini_section), 56 | prefix='sqlalchemy.', 57 | poolclass=pool.NullPool) 58 | 59 | connection = engine.connect() 60 | context.configure( 61 | connection=connection, 62 | target_metadata=target_metadata 63 | ) 64 | 65 | try: 66 | with context.begin_transaction(): 67 | context.run_migrations() 68 | finally: 69 | connection.close() 70 | 71 | if context.is_offline_mode(): 72 | run_migrations_offline() 73 | else: 74 | run_migrations_online() 75 | 76 | -------------------------------------------------------------------------------- /lib/ansiblereport/constants.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | import datetime 20 | 21 | import ansible.constants as AC 22 | 23 | def get_config_value(key, env_var, default): 24 | ''' Look up key in ansible.cfg 25 | This uses load_config_file() and get_config() from ansible.constants 26 | ''' 27 | config = AC.load_config_file() 28 | return AC.get_config(config, DEFAULT_SECTION, key, env_var, default) 29 | 30 | DEFAULT_SECTION = 'ansiblereport' 31 | 32 | DEFAULT_VERBOSE = False 33 | DEFAULT_STATS = False 34 | DEFAULT_INTERSECTION = get_config_value('intersection', 'ANSIBLEREPORT_INTERSECTION', False) 35 | DEFAULT_LIMIT = get_config_value('limit', 'ANSIBLEREPORT_LIMIT', 0) 36 | 37 | DEFAULT_BACKOFF_START = get_config_value('backoff.start', None, 0.5) 38 | DEFAULT_BACKOFF_MULT = get_config_value('backoff.mult', None, 1.05) 39 | DEFAULT_BACKOFF_MAX = get_config_value('backoff.max', None, 60) 40 | 41 | DEFAULT_DB_URI = get_config_value('sqlalchemy.url', 'ANSIBLEREPORT_DB_URI', 'sqlite://') 42 | 43 | DEFAULT_STRFTIME = '%Y-%m-%d %H:%M:%S' 44 | DEFAULT_SHORT_STRFTIME = '%H:%M:%S' 45 | DEFAULT_FRIENDLY_STRFTIME = '%Y-%m-%d %H:%M' 46 | 47 | DEFAULT_TASK_WARN_RESULTS = ['FAILED', 'ERROR', 'UNREACHABLE'] 48 | DEFAULT_TASK_OKAY_RESULTS = ['OK', 'SKIPPED', 'CHANGED'] 49 | 50 | DEFAULT_TASK_RESULTS = [] 51 | DEFAULT_TASK_RESULTS.extend(DEFAULT_TASK_OKAY_RESULTS) 52 | DEFAULT_TASK_RESULTS.extend(DEFAULT_TASK_WARN_RESULTS) 53 | 54 | DEFAULT_SMTP_SERVER = get_config_value('smtp.server', None, 'localhost') 55 | DEFAULT_SMTP_SUBJECT = get_config_value('smtp.subject', None, 'ansible-report') 56 | DEFAULT_SMTP_SENDER = get_config_value('smtp.sender', None, 'nobody@localhost') 57 | DEFAULT_SMTP_RECIPIENT = get_config_value('smtp.recipient', None, 'root@localhost') 58 | 59 | DEFAULT_OUTPUT = ['screen'] 60 | DEFAULT_OUTPUT_PLUGIN_PATH = AC.shell_expand_path( 61 | get_config_value('output_plugins', 62 | 'ANSIBLEREPORT_OUTPUT_PLUGINS', '/usr/share/ansible-report/plugins')) 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = 'ansible-report' 2 | PYTHON=python 3 | 4 | VERSION := $(shell grep __version lib/ansiblereport/__init__.py | sed -e 's|^.*= ||' -e "s|'||g" ) 5 | 6 | # Get the branch information from git 7 | ifneq ($(shell which git),) 8 | GIT_DATE := $(shell git log -n 1 --format="%ai") 9 | endif 10 | 11 | ifeq ($(OS), FreeBSD) 12 | DATE := $(shell date -j -f "%Y-%m-%d %H:%M:%s" "$(GIT_DATE)" +%Y%m%d%H%M) 13 | else 14 | ifeq ($(OS), Darwin) 15 | DATE := $(shell date -j -f "%Y-%m-%d %H:%M:%S" "$(GIT_DATE)" +%Y%m%d%H%M) 16 | else 17 | DATE := $(shell date --utc --date="$(GIT_DATE)" +%Y%m%d%H%M) 18 | endif 19 | endif 20 | 21 | 22 | # RPM build parameters 23 | RPMSPECDIR = packaging/rpm 24 | RPMSPEC = $(RPMSPECDIR)/$(NAME).spec 25 | RPMDIST = $(shell rpm --eval '%dist') 26 | RPMRELEASE = 1 27 | ifeq ($(OFFICIAL),) 28 | RPMRELEASE = 0.git$(DATE) 29 | endif 30 | RPMNVR = "$(NAME)-$(VERSION)-$(RPMRELEASE)$(RPMDIST)" 31 | 32 | 33 | all: clean python 34 | 35 | test: 36 | PYTHONPATH=lib nosetests -d -v --with-coverage \ 37 | --cover-erase --cover-package=ansiblereport 38 | 39 | clean: 40 | @echo "Cleaning distutils leftovers" 41 | rm -rf build 42 | rm -rf dist 43 | @echo "Cleaning up byte compiled python files" 44 | find . -type f -regex ".*\.py[co]$$" -delete 45 | @echo "Cleaning up RPM build files" 46 | rm -rf MANIFEST rpm-build 47 | 48 | python: 49 | $(PYTHON) setup.py build 50 | 51 | install: 52 | $(PYTHON) setup.py install 53 | 54 | sdist: clean 55 | $(PYTHON) setup.py sdist -t MANIFEST.in 56 | 57 | pep8: 58 | @echo "Running PEP8 compliance tests" 59 | -pep8 -r --ignore=E501,E202,E302,E303 lib/ bin/ plugins/ 60 | 61 | rpmcommon: sdist 62 | @echo "make rpmcommon" 63 | @mkdir -p rpm-build 64 | @cp dist/*.gz rpm-build/ 65 | @echo '$(VERSION)' 66 | @sed -e 's/^Version:.*/Version: $(VERSION)/' \ 67 | -e 's/^Release:.*/Release: $(RPMRELEASE)%{?dist}/' \ 68 | $(RPMSPEC) > rpm-build/$(NAME).spec 69 | 70 | srpm: rpmcommon 71 | @echo make srpm 72 | @rpmbuild --define "_topdir %(pwd)/rpm-build" \ 73 | --define "_builddir %{_topdir}" \ 74 | --define "_rpmdir %{_topdir}" \ 75 | --define "_srcrpmdir %{_topdir}" \ 76 | --define "_specdir $(RPMSPECDIR)" \ 77 | --define "_sourcedir %{_topdir}" \ 78 | -bs rpm-build/$(NAME).spec 79 | @rm -f rpm-build/$(NAME).spec 80 | @echo "$(NAME) SRPM is built:" 81 | @echo " rpm-build/$(RPMNVR).src.rpm" 82 | 83 | rpm: rpmcommon 84 | @rpmbuild --define "_topdir %(pwd)/rpm-build" \ 85 | --define "_builddir %{_topdir}" \ 86 | --define "_rpmdir %{_topdir}" \ 87 | --define "_srcrpmdir %{_topdir}" \ 88 | --define "_specdir $(RPMSPECDIR)" \ 89 | --define "_sourcedir %{_topdir}" \ 90 | -ba rpm-build/$(NAME).spec 91 | @rm -f rpm-build/$(NAME).spec 92 | @echo "$(NAME) RPM is built:" 93 | @echo " rpm-build/noarch/$(RPMNVR).noarch.rpm" 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ansible-report 2 | ============== 3 | 4 | Utility to log and report Ansible activity 5 | 6 | For information on *Ansible*, see http://ansible.cc. 7 | 8 | Requirements 9 | ============ 10 | 11 | * [Ansible](http://ansible.cc) >= 1.2 12 | * [SQLAlchemy](http://www.sqlalchemy.org/) >= 0.7 13 | * [Alembic](https://pypi.python.org/pypi/alembic) 14 | * [python-dateutil](http://labix.org/python-dateutil) 15 | 16 | Example Output 17 | ============== 18 | 19 | Here is an example output: 20 | 21 | $ ansible-report -o screen -o email 22 | =================== Playbooks ==================== 23 | 24 | /var/lib/ansible/audit.yml: 25 | User: sfromm (sfromm) 26 | Start time: 2013-05-02 10:36:02 27 | End time: 2013-05-02 10:36:06 28 | 29 | --------- Tasks --------- 30 | 31 | 10:36:02 gandalf.example.net command: OK 32 | 33 | -------- Summary -------- 34 | 35 | gandalf.example.net : ok=3 changed=1 error=0 failed=0 skipped=0 unreachable=0 36 | 37 | 38 | Callback Configuration 39 | ====================== 40 | 41 | To configure the callback plugin, place the file 42 | _ansiblereport-logger.py_ in the directory where you have *ansible* 43 | configured to look for callback plugins. The default location for this 44 | is typically: 45 | 46 | /usr/share/ansible_plugins/callback_plugins 47 | 48 | Alternatively, you can configure this directory via your _ansible.cfg_. 49 | After copying there, you need to configure the sqlalchemy url that will 50 | be used. The following is an example that uses a *sqlite* file in the 51 | current directory: 52 | 53 | [ansiblereport] 54 | sqlalchemy.url = sqlite:///ansbile.sqlite 55 | 56 | For information on configuring sqlaclhemy, one starting point is 57 | [SQLAlchemy Engines](http://docs.sqlalchemy.org/en/latest/core/engines.html). More information is available at http://docs.sqlalchemy.org/en/latest/. 58 | 59 | Report Configuration 60 | ==================== 61 | 62 | The only configuration related to reporting is the necessary SMTP 63 | settings. These are: 64 | 65 | [ansiblereport] 66 | smtp.server = localhost 67 | smtp.subject = ansible-report 68 | smtp.sender = nobody@example.net 69 | smtp.recipient = root@example.net 70 | 71 | The _smtp.server_ setting is what *ansible-report* will connect to when 72 | sending an email report to the configured recipients. 73 | 74 | Output Plugins 75 | ============== 76 | 77 | All outputs are plugins. Take a look at *lib/ansiblereport/output.py* and 78 | the existing implementations *plugins/output_plugins/screen.py* and 79 | *plugins/output_plugins/email.py*. You can easily extend 80 | *OutputModule* from _output.py_ to create your own output. If you do 81 | extend *ansible-report*, please consider sending a pull-request for the 82 | new output. 83 | 84 | Schema Migrations 85 | ================= 86 | 87 | At this time, the database schema is not finalized. While I will 88 | endeavor to provide a mechanism to keep up with schema changes, I make no 89 | guarantees at this time. Migrations will be handled with 90 | [alembic](http://alembic.readthedocs.org/en/latest/index.html). Please 91 | refer to _alembic_ documentation for how to handle migrations. In the 92 | simple case, you should be able to do: 93 | 94 | $ alembic upgrade head 95 | 96 | In order to configure alembic, you should update the _sqlalchemy.url_ 97 | key in _alembic.ini_. 98 | 99 | *Note*: If you are using SQLite, please be aware that it has limited 100 | abilities to [alter tables] [1]. You should also refer to Alembic's 101 | [note] [2] on the subject. 102 | 103 | [1]: http://www.sqlite.org/lang_altertable.html 104 | [2]: https://bitbucket.org/zzzeek/alembic 105 | -------------------------------------------------------------------------------- /lib/ansiblereport/manager.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | import ansiblereport.constants as C 20 | from ansiblereport.model import * 21 | 22 | 23 | import functools 24 | import random 25 | import time 26 | import sqlalchemy 27 | import os 28 | import sys 29 | 30 | def _db_error_decorator(callable): 31 | @functools.wraps(callable) 32 | def _wrap(self, *args, **kwargs): 33 | try: 34 | return callable(self, *args, **kwargs) 35 | except Exception as e: 36 | raise 37 | return _wrap 38 | 39 | class Manager(object): 40 | ''' db manager object ''' 41 | 42 | def __init__(self, uri, alembic_ini=None, debug=False): 43 | self.engine = create_engine(uri, echo=debug) 44 | Base.metadata.create_all(self.engine) 45 | if alembic_ini is not None: 46 | # if we have an alembic.ini, stamp the db with the head revision 47 | from alembic.config import Config 48 | from alembic import command 49 | alembic_cfg = Config(alembic_ini) 50 | command.stamp(alembic_cfg, 'head') 51 | Session = sessionmaker(bind=self.engine, autocommit=True) 52 | self.session = Session() 53 | 54 | @_db_error_decorator 55 | def save(self, model, nocommit=False): 56 | ''' save an object ''' 57 | with self.session.begin(subtransactions=True): 58 | self.session.add(model) 59 | self.session.flush() 60 | 61 | def get_or_create(self, model, **kwargs): 62 | ''' get or create an object ''' 63 | instance = self.session.query(model).filter_by(**kwargs).first() 64 | if not instance: 65 | instance = model(**kwargs) 66 | self.save(instance) 67 | return instance 68 | 69 | # The following is based on buildbot/master/buildbot/db/pool.py 70 | def run(self, callable, *args, **kwargs): 71 | backoff = C.DEFAULT_BACKOFF_START 72 | start = time.time() 73 | while True: 74 | try: 75 | try: 76 | rv = callable(self.session, *args, **kwargs) 77 | break 78 | except sqlalchemy.exc.OperationalError as e: 79 | text = e.orig.args[0] 80 | if not isinstance(text, basestring): 81 | raise 82 | if "Lost connection" in text \ 83 | or "database is locked" in text: 84 | 85 | # raise exception if have retried too often 86 | elapsed = time.time() - start 87 | if elapsed > C.DEFAULT_BACKOFF_MAX: 88 | raise 89 | 90 | # sleep and retry 91 | time.sleep(backoff) 92 | backoff *= C.DEFAULT_BACKOFF_MULT 93 | # try again 94 | continue 95 | else: 96 | raise 97 | except Exception as e: 98 | raise 99 | finally: 100 | pass 101 | return rv 102 | 103 | -------------------------------------------------------------------------------- /plugins/output_plugins/screen.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | from ansiblereport.utils import * 20 | from ansiblereport.model import * 21 | import ansiblereport.constants as C 22 | 23 | class OutputModule: 24 | ''' 25 | A simple plugin that displays report information to STDOUT. Per 26 | requirements, it implements: 27 | 28 | name Attribute with the name of the plugin 29 | do_report Method that will take a list of events and report 30 | them in some manner. It also takes an optional 31 | set of keyword arguments. 32 | The only optional keyword argument that is supported 33 | is 'verbose'. 34 | ''' 35 | name = 'screen' 36 | 37 | def _update_stats(self, host, stats): 38 | if host not in self.report_stats: 39 | self.report_stats[host] = {} 40 | if 'total' not in self.report_stats: 41 | self.report_stats['total'] = {} 42 | for key in stats: 43 | if key not in self.report_stats[host]: 44 | self.report_stats[host][key] = 0 45 | self.report_stats[host][key] += stats[key] 46 | if key not in self.report_stats['total']: 47 | self.report_stats['total'][key] = 0 48 | self.report_stats['total'][key] += stats[key] 49 | 50 | def do_report(self, events, **kwargs): 51 | ''' take list of events and report them to the screen ''' 52 | self.report_stats = {} 53 | report_tasks = [] 54 | if 'verbose' not in kwargs: 55 | kwargs['verbose'] = C.DEFAULT_VERBOSE 56 | if 'stats' not in kwargs: 57 | kwargs['stats'] = C.DEFAULT_STATS 58 | for event in events: 59 | tasks = [] 60 | if isinstance(event, AnsiblePlaybook): 61 | for task in event.tasks: 62 | if is_reportable_task(task, kwargs['verbose']): 63 | tasks.append(task) 64 | if tasks: 65 | stats = AnsiblePlaybook.get_playbook_stats(event) 66 | if kwargs['stats']: 67 | for host in stats: 68 | if host not in self.report_stats: 69 | self.report_stats[host] = stats[host] 70 | else: 71 | self._update_stats(host, stats[host]) 72 | else: 73 | print format_playbook_report(event, tasks, stats) 74 | elif isinstance(event, AnsibleTask): 75 | if is_reportable_task(event, kwargs['verbose']): 76 | if kwargs['stats']: 77 | stats = AnsibleTask.get_task_stats(event) 78 | self._update_stats(event.hostname, stats[event.hostname]) 79 | else: 80 | report_tasks.append(event) 81 | if report_tasks: 82 | print format_task_report(report_tasks, embedded=False) 83 | if self.report_stats: 84 | totals = { 'total': self.report_stats.pop('total') } 85 | print format_stats(self.report_stats, heading=False) 86 | print format_stats(totals) 87 | -------------------------------------------------------------------------------- /plugins/output_plugins/email.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | from ansiblereport.utils import * 20 | from ansiblereport.model import * 21 | 22 | class OutputModule: 23 | ''' 24 | A simple plugin that displays report information to STDOUT. Per 25 | requirements, it implements: 26 | 27 | name Attribute with the name of the plugin 28 | do_report Method that will take a list of events and report 29 | them in some manner. It also takes an optional 30 | set of keyword arguments. 31 | The only optional keyword arguments that this 32 | plugin supports are: 33 | verbose - Whether to be verbose in reporting 34 | smtp_subject - Subject for email report 35 | smtp_recipient - Recipient of email report 36 | ''' 37 | name = 'email' 38 | 39 | def _update_stats(self, host, stats): 40 | if 'total' not in self.report_stats: 41 | self.report_stats['total'] = {} 42 | for key in stats: 43 | if key not in self.report_stats[host]: 44 | self.report_stats[host][key] = 0 45 | self.report_stats[host][key] += stats[key] 46 | if key not in self.report_stats['total']: 47 | self.report_stats['total'][key] = 0 48 | self.report_stats['total'][key] += stats[key] 49 | 50 | def do_report(self, events, **kwargs): 51 | ''' take list of events and email them to recipient ''' 52 | self.report_stats = {} 53 | report = '' 54 | report_tasks = [] 55 | if 'verbose' not in kwargs: 56 | kwargs['verbose'] = C.DEFAULT_VERBOSE 57 | if 'stats' not in kwargs: 58 | kwargs['stats'] = C.DEFAULT_STATS 59 | for event in events: 60 | tasks = [] 61 | if isinstance(event, AnsiblePlaybook): 62 | for task in event.tasks: 63 | if is_reportable_task(task, kwargs['verbose']): 64 | tasks.append(task) 65 | if tasks: 66 | stats = AnsiblePlaybook.get_playbook_stats(event) 67 | if kwargs['stats']: 68 | for host in stats: 69 | if host not in self.report_stats: 70 | self.report_stats[host] = stats[host] 71 | else: 72 | self._update_stats(host, stats[host]) 73 | else: 74 | report += format_playbook_report(event, tasks, stats) 75 | elif isinstance(event, AnsibleTask): 76 | if is_reportable_task(event, kwargs['verbose']): 77 | if kwargs['stats']: 78 | stats = AnsibleTask.get_task_stats(event) 79 | self._update_stats(event.hostname, stats[event.hostname]) 80 | else: 81 | report_tasks.append(event) 82 | if report_tasks: 83 | report += format_task_report(report_tasks, embedded=False) 84 | if self.report_stats: 85 | totals = { 'total': self.report_stats.pop('total') } 86 | report += format_stats(self.report_stats, heading=False) 87 | report += format_stats(totals) 88 | for arg in kwargs.keys(): 89 | if not arg.startswith('smtp_'): 90 | del kwargs[arg] 91 | if report: 92 | email_report(report, **kwargs) 93 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import pkg_resources 5 | 6 | # fancy way to get the right version of sqlalchemy on rhel6 7 | # in case pkg_resources has already been loaded. 8 | def replace_dist(requirement): 9 | try: 10 | pkg_resources.require(requirement) 11 | except pkg_resources.VersionConflict: 12 | e = sys.exc_info()[1] 13 | dist = e.args[0] 14 | req = e.args[1] 15 | if dist.key == req.key and not dist.location.endswith('.egg'): 16 | del pkg_resources.working_set.by_key[dist.key] 17 | # We assume there is no need to adjust sys.path 18 | # and the associated pkg_resources.working_set.entries 19 | return pkg_resources.require(requirement) 20 | replace_dist('SQLAlchemy >= 0.7') 21 | 22 | import unittest 23 | import getpass 24 | import os 25 | import multiprocessing 26 | 27 | MAX_WORKERS = 75 28 | ALEMBIC_INI = os.path.join(os.path.dirname(__file__), 'alembic.test.ini') 29 | ANSIBLE_CFG = os.path.join(os.path.dirname(__file__), 'ansible.cfg') 30 | os.environ['ANSIBLE_CONFIG'] = ANSIBLE_CFG 31 | TEST_PLAYBOOK = 'tests/test_ansible_notify.yml' 32 | TEST_TRANSPORT = 'local' 33 | VERBOSITY = 0 34 | 35 | import ansiblereport 36 | import ansiblereport.constants as C 37 | from ansiblereport.manager import * 38 | from ansiblereport.model import * 39 | from ansiblereport.utils import * 40 | 41 | import ansible.runner as ans_runner 42 | import ansible.playbook as ans_playbook 43 | import ansible.callbacks as ans_callbacks 44 | 45 | class TestModel(unittest.TestCase): 46 | 47 | def test_create_db(self): 48 | mgr = Manager(C.DEFAULT_DB_URI, debug=True) 49 | self.assertEqual(mgr.session.connection().engine.name, 'sqlite') 50 | 51 | class TestPlugin(unittest.TestCase): 52 | 53 | def setUp(self): 54 | self.user = getpass.getuser() 55 | self.module_name = 'ping' 56 | self.module_args = [] 57 | self.limit = 1 58 | self.host_list = 'tests/hosts' 59 | self.transport = TEST_TRANSPORT 60 | self.runner = None 61 | self.mgr = Manager(C.DEFAULT_DB_URI, debug=True) 62 | 63 | # from ansible TestRunner.py 64 | def _run_task(self, module_name='ping', module_args=[]): 65 | self.module_name = module_name 66 | self.module_args = module_args 67 | runner_cb = ans_callbacks.DefaultRunnerCallbacks() 68 | self.runner = ans_runner.Runner( 69 | module_name=self.module_name, 70 | module_args=' '.join(self.module_args), 71 | remote_user=self.user, 72 | remote_pass=None, 73 | host_list=self.host_list, 74 | timeout=5, 75 | forks=1, 76 | background=0, 77 | pattern='all', 78 | transport=self.transport, 79 | callbacks=runner_cb, 80 | ) 81 | results = self.runner.run() 82 | assert "localhost" in results['contacted'] 83 | return results['contacted']['localhost'] 84 | 85 | def _run_playbook(self, playbook): 86 | stats = ans_callbacks.AggregateStats() 87 | playbook_cb = ans_callbacks.PlaybookCallbacks(verbose=VERBOSITY) 88 | playbook_runner_cb = ans_callbacks.PlaybookRunnerCallbacks(stats, verbose=VERBOSITY) 89 | self.playbook = ans_playbook.PlayBook( 90 | playbook=playbook, 91 | host_list=self.host_list, 92 | forks=1, 93 | timeout=5, 94 | remote_user=self.user, 95 | remote_pass=None, 96 | extra_vars=None, 97 | stats=stats, 98 | callbacks=playbook_cb, 99 | runner_callbacks=playbook_runner_cb 100 | ) 101 | result = self.playbook.run() 102 | return result 103 | 104 | def test_module_callback(self): 105 | ''' test runner module callback ''' 106 | result = self._run_task() 107 | assert 'ping' in result 108 | 109 | def test_module_callback_data(self): 110 | ''' test runner module callback data ''' 111 | def fn(conn): 112 | args = {} 113 | args['module'] = [self.module_name] 114 | results = AnsibleTask.find_tasks(conn, limit=self.limit, args=args) 115 | for r in results: 116 | self.assertEqual(r.module, self.module_name) 117 | return self.mgr.run(fn) 118 | 119 | def test_module_notify(self): 120 | ''' test handling of notify tasks ''' 121 | # This greatly depends on what is defined in the playbook. 122 | results = self._run_playbook(TEST_PLAYBOOK) 123 | assert 'localhost' in results 124 | def fn(conn): 125 | args = {} 126 | args['module'] = 'command' 127 | results = AnsibleTask.find_tasks(conn, limit=self.limit, args=args) 128 | for r in results: 129 | self.assertEqual(r.data['invocation']['module_args'], 'uptime') 130 | return self.mgr.run(fn) 131 | 132 | def test_process_concurrency(self): 133 | ''' fork N processes and test concurrent writes to db ''' 134 | workers = [] 135 | for n in range(MAX_WORKERS): 136 | prc = multiprocessing.Process(target=self._run_task, args=()) 137 | prc.start() 138 | workers.append(prc) 139 | 140 | for worker in workers: 141 | worker.join() 142 | -------------------------------------------------------------------------------- /plugins/callback_plugins/ansiblereport-logger.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | import sys 20 | import pkg_resources 21 | 22 | # fancy way to get the right version of sqlalchemy on rhel6 23 | # in case pkg_resources has already been loaded. 24 | def replace_dist(requirement): 25 | try: 26 | pkg_resources.require(requirement) 27 | except pkg_resources.VersionConflict: 28 | e = sys.exc_info()[1] 29 | dist = e.args[0] 30 | req = e.args[1] 31 | if dist.key == req.key and not dist.location.endswith('.egg'): 32 | del pkg_resources.working_set.by_key[dist.key] 33 | # We assume there is no need to adjust sys.path 34 | # and the associated pkg_resources.working_set.entries 35 | return pkg_resources.require(requirement) 36 | replace_dist('SQLAlchemy >= 0.7') 37 | 38 | import datetime 39 | import os 40 | import uuid 41 | import socket 42 | import ansiblereport.constants as C 43 | from ansiblereport.manager import * 44 | from ansiblereport.model import * 45 | from ansiblereport.utils import * 46 | 47 | class CallbackModule(object): 48 | """ 49 | Callback module to log json blobs to sql 50 | """ 51 | 52 | def __init__(self): 53 | self.uuid = uuid.uuid1() 54 | self.starttime = 0 55 | self.endtime = 0 56 | self.playbook = None 57 | self.mgr = Manager(C.DEFAULT_DB_URI) 58 | 59 | def _log_user(self): 60 | (username, euid) = get_user() 61 | if euid is None: 62 | euid = username 63 | return self.mgr.get_or_create(AnsibleUser, username=username, euid=euid) 64 | 65 | def _log_task(self, task): 66 | ''' add result to database ''' 67 | user = self._log_user() 68 | task.user = user 69 | if self.playbook: 70 | task.playbook = self.playbook 71 | self.mgr.save(task) 72 | 73 | def _log_play(self, play): 74 | ''' add play to database ''' 75 | if play.user_id is None: 76 | user = self._log_user() 77 | play.user = user 78 | self.mgr.save(play) 79 | 80 | def on_any(self, *args, **kwargs): 81 | pass 82 | 83 | def runner_on_failed(self, host, res, ignore_errors=False): 84 | module = res['invocation']['module_name'] 85 | self._log_task(AnsibleTask(host, module, 'FAILED', res)) 86 | 87 | def runner_on_ok(self, host, res): 88 | module = res['invocation']['module_name'] 89 | self._log_task(AnsibleTask(host, module, 'OK', res)) 90 | 91 | def runner_on_error(self, host, msg): 92 | res = {} 93 | self._log_task(AnsibleTask(host, None, 'ERROR', res)) 94 | 95 | def runner_on_skipped(self, host, item=None): 96 | res = {} 97 | self._log_task(AnsibleTask(host, None, 'SKIPPED', res)) 98 | 99 | def runner_on_unreachable(self, host, res): 100 | if not isinstance(res, dict): 101 | res2 = res 102 | res = {} 103 | res['msg'] = res2 104 | self._log_task(AnsibleTask(host, None, 'UNREACHABLE', res)) 105 | 106 | def runner_on_no_hosts(self): 107 | pass 108 | 109 | def runner_on_async_poll(self, host, res, jid, clock): 110 | pass 111 | 112 | def runner_on_async_ok(self, host, res, jid): 113 | pass 114 | 115 | def runner_on_async_failed(self, host, res, jid): 116 | self._log_task(AnsibleTask(host, None, 'ASYNC_FAILED', res)) 117 | 118 | def playbook_on_start(self): 119 | # start of playbook, no attrs are set yet 120 | self.starttime = datetime.datetime.now() 121 | self.playbook = AnsiblePlaybook(str(self.uuid)) 122 | 123 | def playbook_on_notify(self, host, handler): 124 | ''' reports name of host and name of handler playbook will execute ''' 125 | pass 126 | 127 | def on_no_hosts_matched(self): 128 | pass 129 | 130 | def on_no_hosts_remaining(self): 131 | pass 132 | 133 | def playbook_on_task_start(self, name, is_conditional): 134 | pass 135 | 136 | def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None): 137 | pass 138 | 139 | def playbook_on_setup(self): 140 | pass 141 | 142 | def playbook_on_import_for_host(self, host, imported_file): 143 | pass 144 | 145 | def playbook_on_not_import_for_host(self, host, missing_file): 146 | pass 147 | 148 | def playbook_on_play_start(self, pattern): 149 | play = getattr(self, 'play', None) 150 | if play is not None: 151 | path = os.path.abspath(os.path.join( 152 | play.playbook.basedir, play.playbook.filename)) 153 | self.playbook.path = path 154 | self.playbook.checksum = git_version(path) 155 | self.playbook.connection = play.playbook.transport 156 | self._log_play(self.playbook) 157 | 158 | def playbook_on_stats(self, stats): 159 | self.endtime = datetime.datetime.now() 160 | self.playbook.endtime = self.endtime 161 | self._log_play(self.playbook) 162 | -------------------------------------------------------------------------------- /plugins/output_plugins/logstalgia.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | # 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | from ansiblereport.utils import * 20 | from ansiblereport.model import * 21 | import ansiblereport.constants as C 22 | from datetime import datetime, tzinfo 23 | from dateutil.tz import * 24 | import sys 25 | import os.path 26 | import ansible.utils 27 | 28 | class OutputModule: 29 | ''' 30 | A plugin that creates log format acceptable for logstalgia and pipes 31 | into logstalgia for visualization. 32 | requirements, it implements: 33 | 34 | name Attribute with the name of the plugin 35 | do_report Method that will take a list of events and report 36 | them via logstalgia visualization. 37 | The only optional keyword arguments that this 38 | plugin supports are: 39 | logstalgia_opts - Additional options to pass to 40 | logstalgia 41 | 42 | This uses NCSA common log format: 43 | %h %l %u %t \"%r\" %s %b 44 | 45 | In this case, the following substitutions are made: 46 | %r is the module name and module_args 47 | ''' 48 | name = 'logstalgia' 49 | STRFTIME_FORMAT = '%s' 50 | 51 | def _get_module_category(self, module): 52 | ''' look up a module's category ''' 53 | path = ansible.utils.plugins.module_finder.find_plugin(module) 54 | if path is None: 55 | return 'NA' 56 | else: 57 | return os.path.basename(os.path.dirname(path)) 58 | 59 | def _mk_custom_log(self, task): 60 | ''' 61 | use logstalgia custom log format 62 | http://code.google.com/p/logstalgia/wiki/CustomLogFormat 63 | ''' 64 | module_args = '' 65 | if task.data is not None: 66 | module_args += '/%s' % task.module 67 | if 'invocation' in task.data: 68 | if 'module_args' in task.data['invocation']: 69 | args = task.data['invocation']['module_args'] 70 | if len(args) > 0: 71 | module_args += '/%s' % args.replace(' ', '%20') 72 | ts = task.timestamp.replace(tzinfo=tzlocal()) 73 | log = "{0}|{1}|{2}|{3}|{4}".format( 74 | ts.strftime(self.STRFTIME_FORMAT), 75 | task.hostname, 76 | module_args, 77 | task.result, 78 | '-' 79 | ) 80 | return log 81 | 82 | def _format_group_match(self, category_set, category): 83 | ''' format the grouping option for logstalgia ''' 84 | match = "^/(%s)" % ("|".join(category_set[category])) 85 | return "-g '{0},{1},15' ".format(category, match) 86 | 87 | def do_report(self, events, **kwargs): 88 | ''' take list of events and visualize via logstalgia ''' 89 | logs = [] 90 | modules = [] 91 | hosts = [] 92 | category = {} 93 | for event in events: 94 | if isinstance(event, AnsiblePlaybook): 95 | for task in event.tasks: 96 | t = is_reportable_task(task, kwargs['verbose']) 97 | if t is not None: 98 | if task.module not in modules: 99 | modules.append(task.module) 100 | if task.hostname not in hosts: 101 | hosts.append(task.hostname) 102 | logs.append(self._mk_custom_log(task)) 103 | elif isinstance(event, AnsibleTask): 104 | t = reportable_task(event, kwargs['verbose']) 105 | if t is not None: 106 | if task.module not in modules: 107 | modules.append(event.module) 108 | if task.hostname not in hosts: 109 | hosts.append(task.hostname) 110 | logs.append(self._mk_event_log(event)) 111 | if logs: 112 | logs.reverse() 113 | logs.append('\0') 114 | opts = '' 115 | if 'logstalgia_opts' in kwargs: 116 | opts += '%s ' % kwargs['logstalgia_opts'] 117 | for m in modules: 118 | c = self._get_module_category(m) 119 | if m is None: 120 | m = 'NA' 121 | if c in category: 122 | if m not in category[c]: 123 | category[c].append(m) 124 | else: 125 | category[c] = [m] 126 | for c in sorted(category.keys()): 127 | # special case NA category 128 | if c != 'NA': 129 | opts += self._format_group_match(category, c) 130 | c = 'NA' 131 | opts += self._format_group_match(category, c) 132 | cmd = 'logstalgia %s -' % opts 133 | rc, out, err = run_command(cmd, data='\n'.join(logs)) 134 | if rc != 0: 135 | print >> sys.stderr, 'failed to run %s: %s %s' % (cmd, err, out) 136 | print >> sys.stderr, 'outputting logs to stdout for processing elsewhere' 137 | print '\n'.join(logs) 138 | -------------------------------------------------------------------------------- /lib/ansiblereport/model.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | import json 20 | import datetime 21 | import logging 22 | import operator 23 | 24 | import ansiblereport.constants as C 25 | 26 | from sqlalchemy import * 27 | from sqlalchemy.orm import * 28 | from sqlalchemy.types import TypeDecorator, Text 29 | from sqlalchemy.ext.declarative import declarative_base 30 | 31 | import ansible.constants 32 | 33 | class JSONEncodedDict(TypeDecorator): 34 | impl = Text 35 | 36 | def process_bind_param(self, value, dialect): 37 | if value is not None: 38 | value = json.dumps(value) 39 | return value 40 | 41 | def process_result_value(self, value, dialect): 42 | if value is not None: 43 | value = json.loads(value) 44 | return value 45 | 46 | def __repr__(self): 47 | return "JSONEncodedDict(%s)" % self.value 48 | 49 | Base = declarative_base() 50 | 51 | def filter_query(session, sql, cls, col, arg, timeop): 52 | clauses = [] 53 | if not hasattr(cls, col): 54 | logging.warn('%s does not have the attribute %s' % (cls, col)) 55 | return sql 56 | if isinstance(arg, list): 57 | for n in arg: 58 | clauses.append(getattr(cls, col) == n) 59 | else: 60 | # the time comparison is hard-coded for now. 61 | # FIXME: make this user controllable. 62 | if 'time' in col: 63 | clauses.append(timeop(getattr(cls, col), arg)) 64 | else: 65 | clauses.append(getattr(cls, col) == arg) 66 | if sql is None: 67 | sql = session.query(cls).filter(or_(*clauses)) 68 | else: 69 | sql = sql.filter(or_(*clauses)) 70 | return sql 71 | 72 | class AnsibleTask(Base): 73 | __tablename__ = 'task' 74 | 75 | id = Column(Integer, primary_key=True) 76 | timestamp = Column(DateTime, default=datetime.datetime.now) 77 | hostname = Column(String) 78 | module = Column(String) 79 | result = Column(String) 80 | changed = Column(Boolean) 81 | data = Column(JSONEncodedDict) 82 | user_id = Column(Integer, ForeignKey('user.id')) 83 | playbook_id = Column(Integer, ForeignKey('playbook.id')) 84 | __table_args__ = ( 85 | Index('task_timestamp_idx', 'timestamp'), 86 | Index('task_hostname_idx', 'hostname'), 87 | Index('task_module_idx', 'module'), 88 | Index('task_changed_idx', 'changed'), 89 | Index('task_result_idx', 'result') 90 | ) 91 | 92 | def __init__(self, hostname, module, result, data): 93 | self.hostname = hostname 94 | self.module = module 95 | self.result = result 96 | self.data = data 97 | if isinstance(data, dict) and 'changed' in data: 98 | self.changed = self.data['changed'] 99 | 100 | def __repr__(self): 101 | return "" % (self.hostname, self.module, self.result) 102 | 103 | def delete(self, session): 104 | ''' remove object from database ''' 105 | session.delete(self) 106 | 107 | @classmethod 108 | def find_tasks(cls, session, args=None, limit=1, timeop=operator.gt, orderby=True): 109 | sql = None 110 | if args is not None: 111 | for col in args: 112 | if hasattr(cls, col): 113 | sql = filter_query(session, sql, cls, col, args[col], timeop) 114 | if orderby: 115 | if limit == 0: 116 | return sql.order_by(cls.timestamp.desc()) 117 | else: 118 | return sql.order_by(cls.timestamp.desc()).limit(limit) 119 | else: 120 | return sql 121 | 122 | @classmethod 123 | def get_task_stats(cls, task): 124 | results = { task.hostname : {} } 125 | for key in C.DEFAULT_TASK_RESULTS: 126 | results[task.hostname][key.lower()] = 0 127 | results[task.hostname]['changed'] = 0 128 | if task.changed: 129 | results[task.hostname]['changed'] += 1 130 | results[task.hostname][task.result.lower()] += 1 131 | return results 132 | 133 | class AnsibleUser(Base): 134 | __tablename__ = 'user' 135 | 136 | id = Column(Integer, primary_key=True) 137 | username = Column(String) 138 | euid = Column(Integer) 139 | __table_args__ = ( 140 | Index('user_username_idx', 'username'), 141 | ) 142 | 143 | tasks = relation("AnsibleTask", backref='user', 144 | cascade='all, delete, delete-orphan') 145 | playbooks = relation("AnsiblePlaybook", backref='user', 146 | cascade='all, delete, delete-orphan') 147 | 148 | def __init__(self, user, euid): 149 | self.username = user 150 | self.euid = euid 151 | 152 | def __repr__(self): 153 | return "" % (self.username, self.euid) 154 | 155 | def delete(self, session): 156 | ''' remove object from database ''' 157 | session.delete(self) 158 | 159 | @classmethod 160 | def get_user(cls, session, username, euid=None): 161 | if euid is None: 162 | euid = username 163 | return session.query(cls).filter( 164 | and_(cls.username == username, cls.euid == euid)) 165 | 166 | class AnsiblePlaybook(Base): 167 | __tablename__ = 'playbook' 168 | 169 | id = Column(Integer, primary_key=True) 170 | path = Column(String) 171 | uuid = Column(String) 172 | user_id = Column(Integer, ForeignKey('user.id')) 173 | connection = Column(String) 174 | starttime = Column(DateTime, default=datetime.datetime.now) 175 | endtime = Column(DateTime, default=datetime.datetime.now) 176 | checksum = Column(String) 177 | __table_args__ = ( 178 | Index('playbook_path_idx', 'path'), 179 | Index('playbook_uuid_idx', 'uuid'), 180 | Index('playbook_connection_idx', 'connection'), 181 | Index('playbook_starttime_idx', 'starttime') 182 | ) 183 | 184 | tasks = relation("AnsibleTask", backref='playbook', 185 | cascade='all, delete, delete-orphan') 186 | 187 | def __init__(self, uuid): 188 | self.uuid = uuid 189 | 190 | def __repr__(self): 191 | return "" % (self.path, self.uuid) 192 | 193 | def delete(self, session): 194 | ''' remove object from database ''' 195 | session.delete(self) 196 | 197 | @classmethod 198 | def by_id(cls, session, identifier): 199 | return session.query(cls).get(identifier) 200 | 201 | @classmethod 202 | def find_playbooks(cls, session, args=None, limit=1, timeop=operator.gt, orderby=True): 203 | sql = None 204 | if args is not None: 205 | for col in args: 206 | if hasattr(cls, col): 207 | sql = filter_query(session, sql, cls, col, args[col], timeop) 208 | if orderby: 209 | if limit == 0: 210 | return sql.order_by(cls.starttime.desc()) 211 | else: 212 | return sql.order_by(cls.starttime.desc()).limit(limit) 213 | else: 214 | return sql 215 | 216 | @classmethod 217 | def get_last_n_playbooks(cls, session, args=None, limit=1, timeop=operator.gt, orderby=True): 218 | sql = None 219 | if args is not None: 220 | for col in args: 221 | if hasattr(cls, col): 222 | sql = filter_query(session, sql, cls, col, args[col], timeop) 223 | if sql is None: 224 | return [] 225 | if orderby: 226 | if limit == 0: 227 | return sql.order_by(cls.starttime.desc()) 228 | else: 229 | return sql.order_by(cls.starttime.desc()).limit(limit) 230 | else: 231 | return sql 232 | 233 | @classmethod 234 | def get_playbook_stats(cls, playbook): 235 | results = {} 236 | for task in playbook.tasks: 237 | if task.hostname not in results: 238 | results[task.hostname] = {} 239 | for key in C.DEFAULT_TASK_RESULTS: 240 | results[task.hostname][key.lower()] = 0 241 | results[task.hostname]['changed'] = 0 242 | if task.changed: 243 | results[task.hostname]['changed'] += 1 244 | results[task.hostname][task.result.lower()] += 1 245 | return results 246 | -------------------------------------------------------------------------------- /bin/ansible-report: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Written by Stephen Fromm 4 | # (C) 2012 University of Oregon 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19 | # 02110-1301, USA. 20 | 21 | __requires__ = ['SQLAlchemy >= 0.7'] 22 | import pkg_resources 23 | 24 | import operator 25 | import os 26 | import json 27 | import sys 28 | import json 29 | import smtplib 30 | from optparse import OptionParser 31 | from optparse import OptionGroup 32 | 33 | import ansiblereport 34 | import ansiblereport.constants as C 35 | from ansiblereport.manager import * 36 | from ansiblereport.model import * 37 | from ansiblereport.utils import * 38 | from ansiblereport.output_plugins import * 39 | 40 | def report_tasks(options, mgr, args): 41 | ''' report on specific tasks ''' 42 | if options.age: 43 | age = parse_datetime_string(options.age) 44 | if age: 45 | args['timestamp'] = age 46 | return AnsibleTask.find_tasks(mgr.session, limit=options.limit, 47 | args=args, intersection=options.intersection) 48 | 49 | def report_playbooks(options, mgr, args): 50 | ''' report on playbook information ''' 51 | if options.age: 52 | age = parse_datetime_string(options.age) 53 | if age: 54 | args['starttime'] = age 55 | return AnsiblePlaybook.get_last_n_playbooks(mgr.session, limit=options.limit, 56 | args=args, intersection=options.intersection) 57 | 58 | def build_task_args(options): 59 | ''' return dict of search criteria for tasks ''' 60 | args = {} 61 | if options.changed: 62 | args['changed'] = options.changed 63 | if options.hostname: 64 | args['hostname'] = options.hostname 65 | if options.module_name: 66 | args['module'] = options.module_name 67 | if options.result: 68 | args['result'] = options.result 69 | return args 70 | 71 | def build_playbook_args(options): 72 | ''' return dict of search criteria for playbooks ''' 73 | args = {} 74 | if options.uuid: 75 | args['uuid'] = options.uuid 76 | if options.path: 77 | args['path'] = options.path 78 | if options.connection: 79 | args['connection'] = options.connection 80 | return args 81 | 82 | def report(options, mgr, kwargs): 83 | ''' select information from db for reporting on ''' 84 | report = [] 85 | report_data = '' 86 | args = build_task_args(options) 87 | if args: 88 | data = report_tasks(options, mgr, args) 89 | else: 90 | args = build_playbook_args(options) 91 | data = report_playbooks(options, mgr, args) 92 | outputs = OutputPlugins([C.DEFAULT_OUTPUT_PLUGIN_PATH]) 93 | for plugin in options.output: 94 | if plugin in outputs.plugins: 95 | outputs.plugins[plugin].do_report(data, **kwargs) 96 | return 97 | 98 | def prune_tasks(options, mgr, args): 99 | if options.age: 100 | age = parse_datetime_string(options.age) 101 | if age: 102 | args['timestamp'] = age 103 | return AnsibleTask.find_tasks(mgr.session, limit=options.limit, 104 | args=args, timeop=operator.le, 105 | intersection=options.intersection, 106 | orderby=False) 107 | 108 | def prune_playbooks(options, mgr, args): 109 | if options.age: 110 | age = parse_datetime_string(options.age) 111 | if age: 112 | args['starttime'] = age 113 | return AnsiblePlaybook.find_playbooks(mgr.session, limit=options.limit, 114 | args=args, timeop=operator.le, 115 | intersection=options.intersection, 116 | orderby=False) 117 | 118 | def prune(options, mgr, kwargs): 119 | args = build_playbook_args(options) 120 | try: 121 | pbs = prune_playbooks(options, mgr, args) 122 | count = pbs.delete() 123 | if options.verbose: 124 | print "Removed %s playbooks from database" % count 125 | args = {} 126 | args = build_task_args(options) 127 | tasks = prune_tasks(options, mgr, args) 128 | count = tasks.delete() 129 | if options.verbose: 130 | print "Removed %s tasks from database" % count 131 | mgr.session.commit() 132 | if 'sqlite' in mgr.engine.driver: 133 | if options.verbose: 134 | print "Running VACUUM" 135 | mgr.engine.execute("VACUUM") 136 | except Exception, e: 137 | mgr.session.rollback() 138 | print "Rolling back; failed to prune database: %s" % str(e) 139 | 140 | 141 | def version(prog): 142 | return "%s %s" % (prog, ansiblereport.__version__) 143 | 144 | def main(args): 145 | ''' main ''' 146 | kwargs = {} 147 | usage = "usage: %prog [options]" 148 | parser = OptionParser(usage=usage, version=version("%prog")) 149 | parser.add_option('-v', '--verbose', action='store_true', 150 | default=C.DEFAULT_VERBOSE, help="Be verbose") 151 | parser.add_option('-o', '--output', metavar='OUTPUT', 152 | action='append', 153 | help='Output destination for report. ' 154 | 'Default is %s.' % ( 155 | ', '.join(C.DEFAULT_OUTPUT))) 156 | parser.add_option('-e', '--extra-args', dest='extra_args', 157 | action='append', 158 | help='Set additional key=value variables from CLI. ' 159 | 'These will be passed to the output plugins') 160 | parser.add_option('--prune', action='store_true', default=False, 161 | help='Prune old events from database. ' 162 | 'Requires --age option.') 163 | parser.add_option('--stats', action='store_true', default=False, 164 | help='Only report stats of playbooks and tasks') 165 | 166 | group = OptionGroup(parser, 'Playbook search criteria') 167 | group.add_option('--uuid', dest='uuid', 168 | action='append', 169 | help="playbook uuid(s) to restrict report to") 170 | group.add_option('--path', dest='path', 171 | action='append', 172 | help="playbook path(s) to restrict report to") 173 | group.add_option('--connection', dest='connection', 174 | action='append', 175 | help="playbook connection(s) to restrict report to") 176 | parser.add_option_group(group) 177 | 178 | group = OptionGroup(parser, 'Task search criteria') 179 | group.add_option('-c', '--changed', action='store_true', 180 | help="search only for tasks that reported changed") 181 | group.add_option('-m', '--module-name', dest='module_name', 182 | action='append', 183 | help="module name(s) to restrict report to") 184 | group.add_option('-n', '--hostname', dest='hostname', 185 | action='append', 186 | help="hostname(s) to restrict report to") 187 | group.add_option('-r', '--result', action='append', 188 | help="results to restrict report to") 189 | parser.add_option_group(group) 190 | 191 | group = OptionGroup(parser, 'General search criteria') 192 | group.add_option('-l', '--limit', metavar='LIMIT', 193 | default=C.DEFAULT_LIMIT, 194 | help="limit reported events to N") 195 | group.add_option('-&', '--intersection', action='store_true', 196 | default=C.DEFAULT_INTERSECTION, 197 | help="restrict results to the intersection of search criteria") 198 | group.add_option('--age', metavar='AGE', 199 | help='Restrict report to events no older ' 200 | 'than this date string') 201 | parser.add_option_group(group) 202 | options, args = parser.parse_args() 203 | if not options.output: 204 | options.output = C.DEFAULT_OUTPUT 205 | if options.extra_args: 206 | for arg in options.extra_args: 207 | (key, val) = arg.split('=', 1) 208 | kwargs[key] = val 209 | kwargs['verbose'] = options.verbose 210 | kwargs['stats'] = options.stats 211 | mgr = Manager(C.DEFAULT_DB_URI) 212 | if options.prune: 213 | if not options.age: 214 | print "Please define an age to prune the database." 215 | return 1 216 | prune(options, mgr, kwargs) 217 | else: 218 | report(options, mgr, kwargs) 219 | return 0 220 | 221 | if __name__ == '__main__': 222 | try: 223 | sys.exit(main(sys.argv[1:])) 224 | except KeyboardInterrupt, e: 225 | # Generic handler for ansible specific errors 226 | print >> sys.stderr, "error: %s" % str(e) 227 | sys.exit(1) 228 | -------------------------------------------------------------------------------- /lib/ansiblereport/utils.py: -------------------------------------------------------------------------------- 1 | # Written by Stephen Fromm 2 | # (C) 2013 University of Oregon 3 | 4 | # This file is part of ansible-report 5 | # 6 | # ansible-report is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # ansible-report is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with ansible-report. If not, see . 18 | 19 | import os 20 | import pwd 21 | import smtplib 22 | import subprocess 23 | import traceback 24 | import logging 25 | import dateutil.parser 26 | import datetime 27 | import json 28 | 29 | import ansiblereport.constants as C 30 | 31 | import ansible.constants as AC 32 | 33 | try: 34 | from email.mime.text import MIMEText 35 | except ImportError: 36 | from email.MIMEText import MIMEText 37 | 38 | def get_user(): 39 | ''' return user information ''' 40 | try: 41 | username = os.getlogin() 42 | euid = pwd.getpwuid(os.geteuid())[0] 43 | return (username, euid) 44 | except Exception, e: 45 | return (None, None) 46 | 47 | def run_command(args, cwd=None, data=None): 48 | ''' run a command via subprocess ''' 49 | if isinstance(args, list): 50 | shell = False 51 | else: 52 | shell = True 53 | rc = 0 54 | out = '' 55 | err = '' 56 | std_in = None 57 | if data: 58 | std_in = subprocess.PIPE 59 | try: 60 | cmd = subprocess.Popen(args, shell=shell, cwd=cwd, 61 | close_fds=True, 62 | stdin=std_in, 63 | stdout=subprocess.PIPE, 64 | stderr=subprocess.PIPE) 65 | out, err = cmd.communicate(input=data) 66 | rc = cmd.returncode 67 | except (OSError, IOError), e: 68 | logging.error("failed to run command: %s" % str(e)) 69 | rc = e.errno 70 | except: 71 | logging.error("failed to run command: %s" % traceback.format_exc()) 72 | rc = 257 73 | return (rc, out, err) 74 | 75 | def git_version(path): 76 | ''' get git HEAD version for a repo ''' 77 | version = 'NA' 78 | path_dir = os.path.dirname(path) 79 | (rc, out, err) = run_command(['git', 'rev-parse', '--git-dir'], cwd=path_dir) 80 | if rc != 0 or len(out) < 1: 81 | return version 82 | git_dir = out.rstrip('\n') 83 | f = open(os.path.join(git_dir, 'HEAD')) 84 | branch = f.readline().split('/')[-1].rstrip('\n') 85 | f.close() 86 | branch_path = os.path.join(git_dir, 'refs', 'heads', branch) 87 | if os.path.exists(branch_path): 88 | f = open(branch_path) 89 | version = f.readline()[:10] 90 | f.close() 91 | return version 92 | 93 | def pretty_json(arg, indent=4): 94 | return json.dumps(arg, sort_keys=True, indent=indent) 95 | 96 | def format_task_brief(task, embedded=True): 97 | ''' summarize a task into a brief string ''' 98 | strftime = C.DEFAULT_SHORT_STRFTIME 99 | if not embedded: 100 | strftime = C.DEFAULT_STRFTIME 101 | if task.module is None: 102 | return "{0} {1}: {2}".format( 103 | task.timestamp.strftime(strftime), 104 | task.hostname, task.result) 105 | else: 106 | return "{0} {1} {2}: {3}".format( 107 | task.timestamp.strftime(strftime), 108 | task.hostname, task.module, task.result) 109 | 110 | def format_heading(title, subheading=True): 111 | ''' format a heading in a report ''' 112 | if subheading: 113 | return '\n {0:-^40}\n\n'.format(' {0} '.format(title)) 114 | else: 115 | return '{0:=^60}\n\n'.format(' {0} '.format(title)) 116 | 117 | def format_stats(stats, heading=True): 118 | ''' format playbook stat data ''' 119 | report = '' 120 | if heading: 121 | report = format_heading('Summary') 122 | for host, stats in stats.items(): 123 | summary = '' 124 | if 'ok' in stats: 125 | summary += "{0}={1} ".format('ok', stats['ok']) 126 | for stat in sorted(stats.keys()): 127 | if stat == 'ok': 128 | continue 129 | summary += "{0}={1} ".format(stat.lower(), stats[stat]) 130 | report += " {0:<20}: {1}\n".format(host, summary) 131 | return report 132 | 133 | def format_playbook_report(playbook, tasks, stats): 134 | ''' take list of data and return formatted string ''' 135 | report = '' 136 | report += format_heading('Playbook', subheading=False) 137 | report += " {0:>10}: {1}\n".format('Path', playbook.path) 138 | report += " {0:>10}: {1}\n".format('UUID', playbook.uuid) 139 | report += " {0:>10}: {1} ({2})\n".format('User', 140 | playbook.user.username, playbook.user.euid) 141 | report += " {0:>10}: {1}\n".format('Start time', 142 | playbook.starttime.strftime(C.DEFAULT_STRFTIME)) 143 | report += " {0:>10}: {1}\n".format('End time', 144 | playbook.endtime.strftime(C.DEFAULT_STRFTIME)) 145 | 146 | if tasks: 147 | report += format_task_report(tasks) 148 | 149 | if stats: 150 | report += format_stats(stats) 151 | report += "\n\n\n" 152 | return report 153 | 154 | def format_task_report(tasks, embedded=True): 155 | ''' takes list of AnsibleTask and returns string ''' 156 | report = '' 157 | report += format_heading('Tasks', subheading=embedded) 158 | for task in tasks: 159 | args = [] 160 | report += " {0}\n".format(format_task_brief(task, embedded)) 161 | if 'invocation' not in task.data: 162 | report += '\n' 163 | continue 164 | invocation = task.data['invocation'] 165 | module_name = task.data['invocation']['module_name'] 166 | 167 | if task.changed: 168 | report += " {0:>10}: {1}\n".format('Changed', 'yes') 169 | 170 | if not embedded: 171 | if task.user: 172 | args.append(('User', task.user.username)) 173 | if task.playbook: 174 | args.append(('Playbook', task.playbook.path)) 175 | 176 | if module_name == 'git': 177 | if 'after' in task.data: 178 | args.append(('SHA1', task.data['after'])) 179 | elif module_name == 'copy' or module_name == 'file': 180 | if 'path' in task.data: 181 | args.append(('Path', task.data['path'])) 182 | elif 'dest' in task.data: 183 | args.append(('Path', task.data['dest'])) 184 | if invocation['module_args']: 185 | args.append(('Arguments', invocation['module_args'])) 186 | if 'msg' in task.data and task.data['msg']: 187 | args.append(('Message', task.data['msg'])) 188 | elif 'result' in task.data and task.data['result']: 189 | results = '\n'.join(task.data['result']) 190 | args.append(('Result', results)) 191 | elif 'ansible_facts' in task.data: 192 | args.append(('Facts', 193 | pretty_json(task.data['ansible_facts'], indent=8))) 194 | for arg in args: 195 | report += " {0:>10}: {1}\n".format(arg[0], arg[1]) 196 | report += '\n' 197 | return report 198 | 199 | def is_reportable_task(task, verbose=False, embedded=True): 200 | ''' determine if task is reportable 201 | 202 | returns True if changed or failed 203 | returns True if okay and verbose is True 204 | otherwise returns False 205 | ''' 206 | if task.result in C.DEFAULT_TASK_WARN_RESULTS: 207 | return True 208 | if task.result in C.DEFAULT_TASK_OKAY_RESULTS: 209 | if task.changed: 210 | return True 211 | if verbose: 212 | return True 213 | return False 214 | 215 | def email_report(report_data, 216 | smtp_subject=C.DEFAULT_SMTP_SUBJECT, 217 | smtp_recipient=C.DEFAULT_SMTP_RECIPIENT): 218 | ''' pull together all the necessary details and send email report ''' 219 | smtp_server = C.DEFAULT_SMTP_SERVER 220 | smtp_sender = C.DEFAULT_SMTP_SENDER 221 | msg = MIMEText(report_data) 222 | msg['Subject'] = smtp_subject 223 | msg['From'] = smtp_sender 224 | msg['To'] = smtp_recipient 225 | try: 226 | s = smtplib.SMTP(smtp_server) 227 | s.sendmail(smtp_sender, [smtp_recipient], msg.as_string()) 228 | s.quit() 229 | except Exception, e: 230 | print 'failed to send email report: {0}'.format(str(e)) 231 | return False 232 | return True 233 | 234 | def parse_datetime_string(arg): 235 | ''' take string argument and convert to datetime object ''' 236 | # if parsedatetime gets packaged, it could replace this 237 | # https://github.com/bear/parsedatetime 238 | try: 239 | date = dateutil.parser.parse(arg, default=True) 240 | except ValueError: 241 | parts = arg.split() 242 | if len(parts) == 1 and parts[0] == 'now': 243 | return datetime.datetime.now() 244 | elif len(parts) != 3 or parts[2] != 'ago': 245 | return None 246 | try: 247 | interval = int(parts[0]) 248 | except ValueError: 249 | return None 250 | period = parts[1] 251 | if 'second' in period: 252 | delta = datetime.timedelta(seconds=interval) 253 | elif 'minute' in period: 254 | delta = datetime.timedelta(minutes=interval) 255 | elif 'hour' in period: 256 | delta = datetime.timedelta(hours=interval) 257 | elif 'day' in period: 258 | delta = datetime.timedelta(days=interval) 259 | elif 'week' in period: 260 | delta = datetime.timedelta(weeks=interval) 261 | date = datetime.datetime.now() - delta 262 | except: 263 | return None 264 | return date 265 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | --------------------------------------------------------------------------------