├── .gitignore ├── MANIFEST.in ├── README.rst ├── kitsune ├── __init__.py ├── admin.py ├── base.py ├── html2text.py ├── mail.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── kitsune_cron.py │ │ ├── kitsune_cron_clean.py │ │ ├── kitsune_cronserver.py │ │ ├── kitsune_nagios_check.py │ │ ├── kitsune_run_job.py │ │ └── kitsune_test_check.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_host.py │ ├── 0003_auto__del_field_job_host_name.py │ ├── 0004_auto__add_field_job_host.py │ ├── 0005_auto__add_field_job_last_result.py │ ├── 0006_auto__add_field_job_renderer.py │ ├── 0007_auto__add_field_job_log_clean_freq_unit__add_field_job_log_clean_freq_.py │ ├── 0008_auto__del_field_job_log_clean_freq_value__del_field_job_log_clean_freq.py │ ├── 0009_auto__add_notificationrule.py │ ├── 0010_auto__chg_field_notificationrule_frequency_value.py │ ├── 0011_auto__add_field_notificationrule_enabled.py │ ├── 0012_auto__del_field_notificationrule_frequency_unit__del_field_notificatio.py │ ├── 0013_auto__del_notificationrule__add_notificationgroup__add_notificationuse.py │ └── __init__.py ├── models.py ├── monitor.py ├── nagios.py ├── renderers.py ├── scripts.py ├── templates │ ├── admin │ │ └── kitsune │ │ │ ├── job │ │ │ ├── change_form.html │ │ │ └── change_list.html │ │ │ └── log │ │ │ └── change_form.html │ └── kitsune │ │ ├── error_message.txt │ │ ├── mail_base.html │ │ ├── mail_notification.html │ │ ├── nagios_status_message.html │ │ └── status_code.html ├── templatetags │ ├── __init__.py │ └── kitsune_tags.py ├── utils.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | local_settings.py 4 | *.prefs 5 | .pydevproject 6 | .project 7 | dist 8 | *.egg-info 9 | build 10 | .settings 11 | 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | recursive-include kitsune/templates * 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | :Author: 2 | Raul Garreta - Tryolabs 3 | 4 | :Project Website: 5 | https://github.com/tryolabs/django-kitsune 6 | 7 | 8 | *********** 9 | Description 10 | *********** 11 | 12 | A Django Admin app to perform host server monitoring. A control panel will be added to the Admin in order to configure hosts, checks and monitor check results. 13 | Notification rules can be defined to notify administrator users by mail. 14 | All host shall have access to a common database in order to get information about scheduled jobs and check jobs to run. 15 | 16 | 17 | *********** 18 | Screenshots 19 | *********** 20 | 21 | .. image:: http://www.tryolabs.com/static/images/kitsune_jobs.jpg 22 | 23 | .. image:: http://www.tryolabs.com/static/images/kitsune_job.jpg 24 | 25 | 26 | ******** 27 | Features 28 | ******** 29 | 30 | * Hosts 31 | 32 | * Add hosts to monitor 33 | 34 | * Checks 35 | 36 | * Add jobs with checks to be performed 37 | * Schedule 38 | * Check to be performed 39 | * Host to check 40 | * Select users or groups to be notified 41 | * Configure notification rules 42 | * Select how to render results 43 | * Set amount of log history to keep 44 | 45 | * Custom Checks 46 | 47 | * You can implement your own checks by implementing a subclass of `kitsune.base.BaseKitsuneCheck` 48 | 49 | * Nagios Checks 50 | 51 | * A builtin check is provided that wrapps any Nagios check. 52 | * You can use any existing Nagios check within django-kitsune 53 | 54 | * Logs 55 | 56 | * Log and list check results 57 | 58 | * Result Renderers 59 | 60 | * Can implement renderers by implementing a subclass of `kitsune.renderers.KitsuneJobRenderer` 61 | * Returns a html with the corresponding result that will be rendered within result listings. 62 | 63 | * List Checks 64 | 65 | * Host name, last time performed, last result, next scheduled run. 66 | 67 | * Notification Rules 68 | 69 | * Notifications through e-mail. 70 | * Configure who to notify: Groups or Users. 71 | * Configure when to trigger a notification. 72 | * Configure the frequency of notifications to avoid spam emails :) 73 | 74 | * All configurations are made through a graphic UI within admin panel. 75 | 76 | 77 | ************ 78 | Requirements 79 | ************ 80 | 81 | * Python 2.6 and higher. 82 | * Nagios plugins: ``sudo apt-get install nagios-plugins`` (if you want to use Nagios checks). 83 | 84 | 85 | ************ 86 | Installation 87 | ************ 88 | 89 | To install Kitsune: 90 | 91 | 1. ``easy_install django-kitsune`` or download package and execute ``python setup.py install`` 92 | 2. Add ``'kitsune'`` to the ``INSTALLED_APPS`` in your project's ``settings.py`` 93 | 3. Configure ``cron`` in every host to run a kitsune management command by running ``crontab`` command:: 94 | 95 | * * * * * /path/to/your/project/manage.py kitsune_cron 96 | 97 | Every minute cron will run a management command to check pending jobs. 98 | Note that both, django-kitsune and your project must be installed in each host, and each host must have access to the common database (where kitsune tables shall be stored). 99 | 100 | 101 | ************* 102 | Configuration 103 | ************* 104 | 105 | Kitsune can be configured via the following parameters, to be defined in your project settings file: 106 | 107 | * ``KITSUNE_RENDERERS``: List of modules that contain renderer classes, eg:: ``KITSUNE_RENDERERS = ['myproject.myapp.renderers']``. 108 | 109 | Kitsune comes with a default renderer ``kitsune.renderers.KitsuneJobRenderer``. 110 | 111 | 112 | ***** 113 | Usage 114 | ***** 115 | 116 | Add a new Host 117 | -------------- 118 | 119 | Add a Nagios check 120 | ------------------ 121 | 122 | For example, to add a check_disk, do the following steps: 123 | 124 | 1. Within Admin go to Kitsune -> Jobs -> Add job 125 | 2. Fill the necessary fields, eg: 126 | 127 | * Name: check_disk 128 | * Host: select a job from the combobox 129 | * Command: select nagios wrapper: ``kitsune_nagios_check`` 130 | * Args: you must provide a special parameter `check` with the name of the nagios check eg: check=check_disk. 131 | 132 | Then provide the necessary nagios check arguments, in this case: -u=GB -w=5 -c=2 -p=/ 133 | To sum up, the string of arguments will be: ``check=check_disk -u=GB -w=5 -c=2 -p=/`` 134 | 135 | 3. Select the result Renderer, eg: KitsuneJobRenderer 136 | 137 | 4. Configure scheduling options, eg: Frequency: Hourly, Params: ``interval:1``. 138 | 139 | Params are semicolon-separated list of `rrule `_ parameters. 140 | 141 | This will schedule the check to be run every 1 hour. 142 | 143 | 5. Configure log options, last logs to keep specifies the last N logs to keep. 144 | 145 | 6. Configure Notification rules. 146 | 147 | Every check returns a status code of ``0=OK, 1=WARNING, 2=CRITICAL ERROR, 3=UNKNOWN ERROR`` with its corresponding status message. 148 | With notification rules you must set the: 149 | 150 | * ``Threshold`` (the status code to be reached) 151 | * ``Rule type``: 152 | 153 | * ``Last time``: triggered when last result reached the threshold. 154 | * ``N last times``: triggered when last N results reached the threshold. 155 | * ``M of N last times``: triggered when M of the last N results reached the threshold. 156 | ``Rule N`` and ``Rule M`` parameters. 157 | 158 | 7. Notification frequency: 159 | 160 | * ``Interval unit``, ``Interval value`` sets the maximum frequency to receive email notifications. These are useful to avoid filling admin inbox with notification mails. 161 | * ``User/Group`` specifies the users or group of users to be notified. These must be staff users and shall be created within admin. 162 | 163 | 164 | Add a custom check 165 | ------------------ 166 | 167 | In order to implement a custom check, you must implement a class that is subclass of ``kitsune.base.BaseKitsuneCheck``. 168 | 169 | Within this class, you must implement the method ``check(self, *args, **options)``. For example:: 170 | 171 | from kitsune.renderers import STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN 172 | from kitsune.base import BaseKitsuneCheck 173 | 174 | class Command(BaseKitsuneCheck): 175 | help = 'A simple test check.' 176 | 177 | def check(self, *args, **options): 178 | self.status_code = STATUS_OK 179 | 180 | if self.status_code == STATUS_OK: 181 | self.status_message = 'OK message' 182 | elif self.status_code == STATUS_WARNING: 183 | self.status_message = 'WARNING message' 184 | elif self.status_code == STATUS_CRITICAL: 185 | self.status_message = 'CRITICAL message' 186 | else: 187 | self.status_message = 'UNKNOWN message' 188 | 189 | With ``*args and **options`` you will receive the arguments and options set from the Args string. 190 | Modules that implement checks are Django management commands, and must live within management.commands package of an app within your project. 191 | 192 | Add a custom renderer 193 | --------------------- 194 | 195 | Renderers are in charge to render the results within the admin panel. They will take the status code and status message and return a html. 196 | If you want to implement your own renderer, you must implement a class that is sublcass of ``kitsune.renderers.KitsuneJobRenderer``. 197 | You must implement to methods: ``get_html_status(self, log)`` that receives a log and and returns a html for status code. 198 | ``get_html_message(self, log)`` that recevies a log and returns a html for status message. 199 | For example:: 200 | 201 | from django.template.loader import render_to_string 202 | from kitsune.renderers import KitsuneJobRenderer 203 | from kitsune.base import STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN 204 | 205 | class MyJobRenderer(KitsuneJobRenderer): 206 | 207 | def get_html_status(self, log): 208 | return render_to_string('kitsune/status_code.html', dictionary={'status_code':int(log.stderr)}) 209 | 210 | def get_html_message(self, log): 211 | return 'All OK!' 212 | 213 | Then you must specify where to get this renderer with the ``KITSUNE_RENDERERS`` at your project settings (see bellow). 214 | 215 | *************** 216 | Acknowledgments 217 | *************** 218 | 219 | Kitsune scheduling system is based on `django-chronograph `_. 220 | 221 | 222 | -------------------------------------------------------------------------------- /kitsune/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 2, 3) 2 | 3 | 4 | def get_version(svn=False, limit=3): 5 | "Returns the version as a human-format string." 6 | return '.'.join([str(i) for i in VERSION[:limit]]) -------------------------------------------------------------------------------- /kitsune/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Admin interface. 8 | Based on django-chronograph. 9 | 10 | ''' 11 | 12 | __author__ = "Raul Garreta (raul@tryolabs.com)" 13 | 14 | 15 | import sys 16 | import inspect 17 | import pkgutil 18 | import os.path 19 | from datetime import datetime 20 | 21 | from django import forms 22 | from django.conf.urls import patterns, url 23 | from django.contrib import admin, messages 24 | from django.core.management import get_commands 25 | from django.core.urlresolvers import reverse, NoReverseMatch 26 | from django.db import models 27 | from django.forms.util import flatatt 28 | from django.http import HttpResponseRedirect, Http404 29 | from django.template.defaultfilters import linebreaks 30 | from django.utils import dateformat 31 | from django.utils.datastructures import MultiValueDict 32 | from django.utils.html import escape 33 | from django.utils.safestring import mark_safe 34 | from django.utils.formats import get_format 35 | from django.utils.text import capfirst 36 | from django.utils.translation import ungettext, ugettext_lazy as _ 37 | from django.core.management.base import BaseCommand 38 | from django.contrib.auth.models import User, Group 39 | 40 | from kitsune.models import Job, Log, Host, NotificationUser, NotificationGroup 41 | from kitsune.renderers import STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN 42 | from kitsune.base import BaseKitsuneCheck 43 | 44 | 45 | def get_class(kls): 46 | parts = kls.split('.') 47 | module = ".".join(parts[:-1]) 48 | m = __import__( module ) 49 | for comp in parts[1:]: 50 | m = getattr(m, comp) 51 | return m 52 | 53 | class HTMLWidget(forms.Widget): 54 | def __init__(self,rel=None, attrs=None): 55 | self.rel = rel 56 | super(HTMLWidget, self).__init__(attrs) 57 | 58 | def render(self, name, value, attrs=None): 59 | if self.rel is not None: 60 | key = self.rel.get_related_field().name 61 | obj = self.rel.to._default_manager.get(**{key: value}) 62 | related_url = '../../../%s/%s/%d/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower(), value) 63 | value = "%s" % (related_url, escape(obj)) 64 | 65 | final_attrs = self.build_attrs(attrs, name=name) 66 | return mark_safe("%s" % (flatatt(final_attrs), linebreaks(value))) 67 | 68 | class NotificationUserInline(admin.TabularInline): 69 | model = NotificationUser 70 | extra = 1 71 | 72 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 73 | if db_field.name == "user": 74 | kwargs["queryset"] = User.objects.filter(is_staff=True) 75 | return super(NotificationUserInline, self).formfield_for_foreignkey(db_field, request, **kwargs) 76 | 77 | class NotificationGroupInline(admin.TabularInline): 78 | model = NotificationGroup 79 | extra = 1 80 | 81 | #from django.contrib.admin import SimpleListFilter 82 | # 83 | #class StatusCodeListFilter(SimpleListFilter): 84 | # # Human-readable title which will be displayed in the 85 | # # right admin sidebar just above the filter options. 86 | # title = _('status_code') 87 | # 88 | # # Parameter for the filter that will be used in the URL query. 89 | # parameter_name = 'status_code' 90 | # 91 | # def lookups(self, request, model_admin): 92 | # """ 93 | # Returns a list of tuples. The first element in each 94 | # tuple is the coded value for the option that will 95 | # appear in the URL query. The second element is the 96 | # human-readable name for the option that will appear 97 | # in the right sidebar. 98 | # """ 99 | # return ( 100 | # ('0', _('OK')), 101 | # ('1', _('WARNING')), 102 | # ('2', _('ERROR')), 103 | # ('3', _('UNKNOWN')), 104 | # ) 105 | # 106 | # def queryset(self, request, queryset): 107 | # """ 108 | # Returns the filtered queryset based on the value 109 | # provided in the query string and retrievable via 110 | # `self.value()`. 111 | # """ 112 | # # Compare the requested value (either '80s' or 'other') 113 | # # to decide how to filter the queryset. 114 | # return queryset.filter(last_result__stderr=self.value()) 115 | 116 | 117 | class JobAdmin(admin.ModelAdmin): 118 | inlines = (NotificationUserInline, NotificationGroupInline) 119 | actions = ['run_selected_jobs'] 120 | list_display = ('name', 'host', 'last_run_with_link', 'get_timeuntil', 121 | 'get_frequency', 'is_running', 'run_button', 'view_logs_button', 'status_code', 'status_message') 122 | list_display_links = ('name', ) 123 | list_filter = ('host',) 124 | fieldsets = ( 125 | ('Job Details', { 126 | 'classes': ('wide',), 127 | 'fields': ('name', 'host', 'command', 'args', 'disabled', 'renderer') 128 | }), 129 | ('Scheduling options', { 130 | 'classes': ('wide',), 131 | 'fields': ('frequency', 'next_run', 'params',) 132 | }), 133 | ('Log options', { 134 | 'classes': ('wide',), 135 | 'fields': ('last_logs_to_keep',) 136 | }), 137 | ) 138 | search_fields = ('name', ) 139 | 140 | def last_run_with_link(self, obj): 141 | if not obj.last_run: 142 | return _('Not yet ran') 143 | format = get_format('DATE_FORMAT') 144 | value = capfirst(dateformat.format(obj.last_run, format)) 145 | 146 | try: 147 | log_id = obj.log_set.latest('run_date').id 148 | try: 149 | # Old way 150 | url = reverse('kitsune_log_change', args=(log_id,)) 151 | except NoReverseMatch: 152 | # New way 153 | url = reverse('admin:kitsune_log_change', args=(log_id,)) 154 | return '%s' % (url, value) 155 | except: 156 | return value 157 | last_run_with_link.admin_order_field = 'last_run' 158 | last_run_with_link.allow_tags = True 159 | last_run_with_link.short_description = 'Last run' 160 | 161 | def get_timeuntil(self, obj): 162 | format = get_format('DATE_FORMAT') 163 | value = capfirst(dateformat.format(obj.next_run, format)) 164 | return "%s
(%s)" % (value, obj.get_timeuntil()) 165 | get_timeuntil.admin_order_field = 'next_run' 166 | get_timeuntil.allow_tags = True 167 | get_timeuntil.short_description = _('next scheduled run') 168 | 169 | def get_frequency(self, obj): 170 | freq = capfirst(obj.frequency.lower()) 171 | if obj.params: 172 | return "%s (%s)" % (freq, obj.params) 173 | return freq 174 | get_frequency.admin_order_field = 'frequency' 175 | get_frequency.short_description = 'Frequency' 176 | 177 | def run_button(self, obj): 178 | on_click = "window.location='%d/run/?inline=1';" % obj.id 179 | return '' % on_click 180 | run_button.allow_tags = True 181 | run_button.short_description = 'Run' 182 | 183 | def status_code(self, obj): 184 | if obj.last_result is not None: 185 | Renderer = get_class(obj.renderer) 186 | return Renderer().get_html_status(obj.last_result) 187 | else: 188 | return '--' 189 | status_code.allow_tags = True 190 | status_code.short_description = 'Status Code' 191 | 192 | def status_message(self, obj): 193 | if obj.last_result is not None: 194 | Renderer = get_class(obj.renderer) 195 | return '' + Renderer().get_html_message(obj.last_result) + '' 196 | else: 197 | return '--' 198 | status_message.allow_tags = True 199 | status_message.short_description = 'Status Message' 200 | 201 | def view_logs_button(self, obj): 202 | on_click = "window.location='../log/?job=%d';" % obj.id 203 | return '' % on_click 204 | view_logs_button.allow_tags = True 205 | view_logs_button.short_description = 'Logs' 206 | 207 | def run_job_view(self, request, pk): 208 | """ 209 | Runs the specified job. 210 | """ 211 | try: 212 | job = Job.objects.get(pk=pk) 213 | except Job.DoesNotExist: 214 | raise Http404 215 | 216 | # Rather than actually running the Job right now, we 217 | # simply force the Job to be run by the next cron job 218 | job.force_run = True 219 | job.save() 220 | 221 | msg = _('The job "%(job)s" has been scheduled to run.') % {'job': job} 222 | messages.info(request, msg) 223 | 224 | if 'inline' in request.GET: 225 | redirect = request.path + '../../' 226 | else: 227 | redirect = request.REQUEST.get('next', request.path + "../") 228 | return HttpResponseRedirect(redirect) 229 | 230 | def get_urls(self): 231 | urls = super(JobAdmin, self).get_urls() 232 | my_urls = patterns( 233 | '', 234 | url( 235 | r'^(.+)/run/$', 236 | self.admin_site.admin_view(self.run_job_view), 237 | name="kitsune_job_run" 238 | ) 239 | ) 240 | return my_urls + urls 241 | 242 | def run_selected_jobs(self, request, queryset): 243 | rows_updated = queryset.update(next_run=datetime.now()) 244 | if rows_updated == 1: 245 | message_bit = "1 job was" 246 | else: 247 | message_bit = "%s jobs were" % rows_updated 248 | self.message_user(request, "%s successfully set to run." % message_bit) 249 | run_selected_jobs.short_description = "Run selected jobs" 250 | 251 | def formfield_for_dbfield(self, db_field, **kwargs): 252 | request = kwargs.pop("request", None) 253 | 254 | # Add a select field of available commands 255 | if db_field.name == 'command': 256 | choices_dict = MultiValueDict() 257 | #l = get_commands().items(): 258 | #l = [('kitsune_base_check', 'kitsune')] 259 | l = get_kitsune_checks() 260 | for command, app in l: 261 | choices_dict.appendlist(app, command) 262 | 263 | choices = [] 264 | for key in choices_dict.keys(): 265 | #if str(key).startswith('<'): 266 | # key = str(key) 267 | commands = choices_dict.getlist(key) 268 | commands.sort() 269 | choices.append([key, [[c,c] for c in commands]]) 270 | 271 | kwargs['widget'] = forms.widgets.Select(choices=choices) 272 | return db_field.formfield(**kwargs) 273 | kwargs['request'] = request 274 | return super(JobAdmin, self).formfield_for_dbfield(db_field, **kwargs) 275 | 276 | 277 | def get_kitsune_checks(): 278 | 279 | # Find the installed apps 280 | try: 281 | from django.conf import settings 282 | apps = settings.INSTALLED_APPS 283 | except (AttributeError, EnvironmentError, ImportError): 284 | apps = [] 285 | 286 | paths = [] 287 | choices = [] 288 | 289 | for app in apps: 290 | paths.append((app, app + '.management.commands')) 291 | 292 | for app, package in paths: 293 | try: 294 | __import__(package) 295 | m = sys.modules[package] 296 | path = os.path.dirname(m.__file__) 297 | for _, name, _ in pkgutil.iter_modules([path]): 298 | pair = (name, app) 299 | __import__(package + '.' + name) 300 | m2 = sys.modules[package + '.' + name] 301 | for _, obj in inspect.getmembers(m2): 302 | if inspect.isclass(obj) and issubclass(obj, BaseKitsuneCheck) and issubclass(obj, BaseCommand): 303 | if pair not in choices: 304 | choices.append(pair) 305 | except: 306 | pass 307 | return choices 308 | 309 | 310 | class LogAdmin(admin.ModelAdmin): 311 | list_display = ('job_name', 'run_date', 'job_success', 'output', 'errors',) 312 | search_fields = ('stdout', 'stderr', 'job__name', 'job__command') 313 | date_hierarchy = 'run_date' 314 | fieldsets = ( 315 | (None, { 316 | 'fields': ('job',) 317 | }), 318 | ('Output', { 319 | 'fields': ('stdout', 'stderr',) 320 | }), 321 | ) 322 | 323 | def job_name(self, obj): 324 | return obj.job.name 325 | job_name.short_description = _(u'Name') 326 | 327 | def job_success(self, obj): 328 | return obj.success 329 | job_success.short_description = _(u'OK') 330 | job_success.boolean = True 331 | 332 | def output(self, obj): 333 | if obj.stdout is not None and obj.stdout != '': 334 | Renderer = get_class(obj.job.renderer) 335 | return Renderer().get_html_message(obj) 336 | else: 337 | return '--' 338 | output.allow_tags = True 339 | 340 | def errors(self, obj): 341 | if obj.stderr is not None: 342 | Renderer = get_class(obj.job.renderer) 343 | return Renderer().get_html_status(obj) 344 | else: 345 | return '--' 346 | errors.allow_tags = True 347 | 348 | def has_add_permission(self, request): 349 | return False 350 | 351 | def formfield_for_dbfield(self, db_field, **kwargs): 352 | request = kwargs.pop("request", None) 353 | 354 | if isinstance(db_field, models.TextField): 355 | kwargs['widget'] = HTMLWidget() 356 | return db_field.formfield(**kwargs) 357 | 358 | if isinstance(db_field, models.ForeignKey): 359 | kwargs['widget'] = HTMLWidget(db_field.rel) 360 | return db_field.formfield(**kwargs) 361 | 362 | return super(LogAdmin, self).formfield_for_dbfield(db_field, **kwargs) 363 | 364 | try: 365 | admin.site.register(Job, JobAdmin) 366 | except admin.sites.AlreadyRegistered: 367 | pass 368 | 369 | admin.site.register(Log, LogAdmin) 370 | #admin.site.register(Log) 371 | admin.site.register(Host) 372 | -------------------------------------------------------------------------------- /kitsune/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 5, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Defines base kitsune check. 8 | All custom kitsune checks must define a Command class that extends BaseKitsuneCheck. 9 | 10 | ''' 11 | 12 | __author__ = "Raul Garreta (raul@tryolabs.com)" 13 | 14 | import sys 15 | import traceback 16 | 17 | from django.core.management.base import BaseCommand 18 | 19 | 20 | # Exit status codes (also recognized by Nagios) 21 | STATUS_OK = 0 22 | STATUS_WARNING = 1 23 | STATUS_CRITICAL = 2 24 | STATUS_UNKNOWN = 3 25 | 26 | 27 | class BaseKitsuneCheck(BaseCommand): 28 | 29 | def check(self): 30 | self.status_code = STATUS_OK 31 | 32 | def handle(self, *args, **options): 33 | try: 34 | self.check(*args, **options) 35 | #standard output to print status message 36 | print self.status_message, 37 | #standard error to print status code 38 | #note comma at the end to avoid printing a \n 39 | print >> sys.stderr, self.status_code, 40 | except Exception as e: 41 | trace = 'Trace: ' + traceback.format_exc() 42 | print str(e), trace, 'args:', args, 'options:', options 43 | print >> sys.stderr, STATUS_UNKNOWN, -------------------------------------------------------------------------------- /kitsune/html2text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """html2text: Turn HTML into equivalent Markdown-structured text.""" 3 | __version__ = "2.39" 4 | __author__ = "Aaron Swartz (me@aaronsw.com)" 5 | __copyright__ = "(C) 2004-2008 Aaron Swartz. GNU GPL 3." 6 | __contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"] 7 | 8 | # TODO: 9 | # Support decoded entities with unifiable. 10 | 11 | if not hasattr(__builtins__, 'True'): True, False = 1, 0 12 | import re, sys, urllib, htmlentitydefs, codecs, StringIO, types 13 | import sgmllib 14 | import urlparse 15 | sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]') 16 | 17 | try: from textwrap import wrap 18 | except: pass 19 | 20 | # Use Unicode characters instead of their ascii psuedo-replacements 21 | UNICODE_SNOB = 0 22 | 23 | # Put the links after each paragraph instead of at the end. 24 | LINKS_EACH_PARAGRAPH = 0 25 | 26 | # Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.) 27 | BODY_WIDTH = 78 28 | 29 | # Don't show internal links (href="#local-anchor") -- corresponding link targets 30 | # won't be visible in the plain text file anyway. 31 | SKIP_INTERNAL_LINKS = False 32 | 33 | ### Entity Nonsense ### 34 | 35 | def name2cp(k): 36 | if k == 'apos': return ord("'") 37 | if hasattr(htmlentitydefs, "name2codepoint"): # requires Python 2.3 38 | return htmlentitydefs.name2codepoint[k] 39 | else: 40 | k = htmlentitydefs.entitydefs[k] 41 | if k.startswith("&#") and k.endswith(";"): return int(k[2:-1]) # not in latin-1 42 | return ord(codecs.latin_1_decode(k)[0]) 43 | 44 | unifiable = {'rsquo':"'", 'lsquo':"'", 'rdquo':'"', 'ldquo':'"', 45 | 'copy':'(C)', 'mdash':'--', 'nbsp':' ', 'rarr':'->', 'larr':'<-', 'middot':'*', 46 | 'ndash':'-', 'oelig':'oe', 'aelig':'ae', 47 | 'agrave':'a', 'aacute':'a', 'acirc':'a', 'atilde':'a', 'auml':'a', 'aring':'a', 48 | 'egrave':'e', 'eacute':'e', 'ecirc':'e', 'euml':'e', 49 | 'igrave':'i', 'iacute':'i', 'icirc':'i', 'iuml':'i', 50 | 'ograve':'o', 'oacute':'o', 'ocirc':'o', 'otilde':'o', 'ouml':'o', 51 | 'ugrave':'u', 'uacute':'u', 'ucirc':'u', 'uuml':'u'} 52 | 53 | unifiable_n = {} 54 | 55 | for k in unifiable.keys(): 56 | unifiable_n[name2cp(k)] = unifiable[k] 57 | 58 | def charref(name): 59 | if name[0] in ['x','X']: 60 | c = int(name[1:], 16) 61 | else: 62 | c = int(name) 63 | 64 | if not UNICODE_SNOB and c in unifiable_n.keys(): 65 | return unifiable_n[c] 66 | else: 67 | return unichr(c) 68 | 69 | def entityref(c): 70 | if not UNICODE_SNOB and c in unifiable.keys(): 71 | return unifiable[c] 72 | else: 73 | try: name2cp(c) 74 | except KeyError: return "&" + c 75 | else: return unichr(name2cp(c)) 76 | 77 | def replaceEntities(s): 78 | s = s.group(1) 79 | if s[0] == "#": 80 | return charref(s[1:]) 81 | else: return entityref(s) 82 | 83 | r_unescape = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));") 84 | def unescape(s): 85 | return r_unescape.sub(replaceEntities, s) 86 | 87 | def fixattrs(attrs): 88 | # Fix bug in sgmllib.py 89 | if not attrs: return attrs 90 | newattrs = [] 91 | for attr in attrs: 92 | newattrs.append((attr[0], unescape(attr[1]))) 93 | return newattrs 94 | 95 | ### End Entity Nonsense ### 96 | 97 | def onlywhite(line): 98 | """Return true if the line does only consist of whitespace characters.""" 99 | for c in line: 100 | if c is not ' ' and c is not ' ': 101 | return c is ' ' 102 | return line 103 | 104 | def optwrap(text): 105 | """Wrap all paragraphs in the provided text.""" 106 | if not BODY_WIDTH: 107 | return text 108 | 109 | assert wrap, "Requires Python 2.3." 110 | result = '' 111 | newlines = 0 112 | for para in text.split("\n"): 113 | if len(para) > 0: 114 | if para[0] is not ' ' and para[0] is not '-' and para[0] is not '*': 115 | for line in wrap(para, BODY_WIDTH): 116 | result += line + "\n" 117 | result += "\n" 118 | newlines = 2 119 | else: 120 | if not onlywhite(para): 121 | result += para + "\n" 122 | newlines = 1 123 | else: 124 | if newlines < 2: 125 | result += "\n" 126 | newlines += 1 127 | return result 128 | 129 | def hn(tag): 130 | if tag[0] == 'h' and len(tag) == 2: 131 | try: 132 | n = int(tag[1]) 133 | if n in range(1, 10): return n 134 | except ValueError: return 0 135 | 136 | class _html2text(sgmllib.SGMLParser): 137 | def __init__(self, out=None, baseurl=''): 138 | sgmllib.SGMLParser.__init__(self) 139 | 140 | if out is None: self.out = self.outtextf 141 | else: self.out = out 142 | self.outtext = u'' 143 | self.quiet = 0 144 | self.p_p = 0 145 | self.outcount = 0 146 | self.start = 1 147 | self.space = 0 148 | self.a = [] 149 | self.astack = [] 150 | self.acount = 0 151 | self.list = [] 152 | self.blockquote = 0 153 | self.pre = 0 154 | self.startpre = 0 155 | self.lastWasNL = 0 156 | self.abbr_title = None # current abbreviation definition 157 | self.abbr_data = None # last inner HTML (for abbr being defined) 158 | self.abbr_list = {} # stack of abbreviations to write later 159 | self.baseurl = baseurl 160 | 161 | def outtextf(self, s): 162 | self.outtext += s 163 | 164 | def close(self): 165 | sgmllib.SGMLParser.close(self) 166 | 167 | self.pbr() 168 | self.o('', 0, 'end') 169 | 170 | return self.outtext 171 | 172 | def handle_charref(self, c): 173 | self.o(charref(c)) 174 | 175 | def handle_entityref(self, c): 176 | self.o(entityref(c)) 177 | 178 | def unknown_starttag(self, tag, attrs): 179 | self.handle_tag(tag, attrs, 1) 180 | 181 | def unknown_endtag(self, tag): 182 | self.handle_tag(tag, None, 0) 183 | 184 | def previousIndex(self, attrs): 185 | """ returns the index of certain set of attributes (of a link) in the 186 | self.a list 187 | 188 | If the set of attributes is not found, returns None 189 | """ 190 | if not attrs.has_key('href'): return None 191 | 192 | i = -1 193 | for a in self.a: 194 | i += 1 195 | match = 0 196 | 197 | if a.has_key('href') and a['href'] == attrs['href']: 198 | if a.has_key('title') or attrs.has_key('title'): 199 | if (a.has_key('title') and attrs.has_key('title') and 200 | a['title'] == attrs['title']): 201 | match = True 202 | else: 203 | match = True 204 | 205 | if match: return i 206 | 207 | def handle_tag(self, tag, attrs, start): 208 | attrs = fixattrs(attrs) 209 | 210 | if hn(tag): 211 | self.p() 212 | if start: self.o(hn(tag)*"#" + ' ') 213 | 214 | if tag in ['p', 'div']: self.p() 215 | 216 | if tag == "br" and start: self.o(" \n") 217 | 218 | if tag == "hr" and start: 219 | self.p() 220 | self.o("* * *") 221 | self.p() 222 | 223 | if tag in ["head", "style", 'script']: 224 | if start: self.quiet += 1 225 | else: self.quiet -= 1 226 | 227 | if tag in ["body"]: 228 | self.quiet = 0 # sites like 9rules.com never close 229 | 230 | if tag == "blockquote": 231 | if start: 232 | self.p(); self.o('> ', 0, 1); self.start = 1 233 | self.blockquote += 1 234 | else: 235 | self.blockquote -= 1 236 | self.p() 237 | 238 | if tag in ['em', 'i', 'u']: self.o("_") 239 | if tag in ['strong', 'b']: self.o("**") 240 | if tag == "code" and not self.pre: self.o('`') #TODO: `` `this` `` 241 | if tag == "abbr": 242 | if start: 243 | attrsD = {} 244 | for (x, y) in attrs: attrsD[x] = y 245 | attrs = attrsD 246 | 247 | self.abbr_title = None 248 | self.abbr_data = '' 249 | if attrs.has_key('title'): 250 | self.abbr_title = attrs['title'] 251 | else: 252 | if self.abbr_title != None: 253 | self.abbr_list[self.abbr_data] = self.abbr_title 254 | self.abbr_title = None 255 | self.abbr_data = '' 256 | 257 | if tag == "a": 258 | if start: 259 | attrsD = {} 260 | for (x, y) in attrs: attrsD[x] = y 261 | attrs = attrsD 262 | if attrs.has_key('href') and not (SKIP_INTERNAL_LINKS and attrs['href'].startswith('#')): 263 | self.astack.append(attrs) 264 | self.o("[") 265 | else: 266 | self.astack.append(None) 267 | else: 268 | if self.astack: 269 | a = self.astack.pop() 270 | if a: 271 | i = self.previousIndex(a) 272 | if i is not None: 273 | a = self.a[i] 274 | else: 275 | self.acount += 1 276 | a['count'] = self.acount 277 | a['outcount'] = self.outcount 278 | self.a.append(a) 279 | self.o("][" + `a['count']` + "]") 280 | 281 | if tag == "img" and start: 282 | attrsD = {} 283 | for (x, y) in attrs: attrsD[x] = y 284 | attrs = attrsD 285 | if attrs.has_key('src'): 286 | attrs['href'] = attrs['src'] 287 | alt = attrs.get('alt', '') 288 | i = self.previousIndex(attrs) 289 | if i is not None: 290 | attrs = self.a[i] 291 | else: 292 | self.acount += 1 293 | attrs['count'] = self.acount 294 | attrs['outcount'] = self.outcount 295 | self.a.append(attrs) 296 | self.o("![") 297 | self.o(alt) 298 | self.o("]["+`attrs['count']`+"]") 299 | 300 | if tag == 'dl' and start: self.p() 301 | if tag == 'dt' and not start: self.pbr() 302 | if tag == 'dd' and start: self.o(' ') 303 | if tag == 'dd' and not start: self.pbr() 304 | 305 | if tag in ["ol", "ul"]: 306 | if start: 307 | self.list.append({'name':tag, 'num':0}) 308 | else: 309 | if self.list: self.list.pop() 310 | 311 | self.p() 312 | 313 | if tag == 'li': 314 | if start: 315 | self.pbr() 316 | if self.list: li = self.list[-1] 317 | else: li = {'name':'ul', 'num':0} 318 | self.o(" "*len(self.list)) #TODO: line up
  1. s > 9 correctly. 319 | if li['name'] == "ul": self.o("* ") 320 | elif li['name'] == "ol": 321 | li['num'] += 1 322 | self.o(`li['num']`+". ") 323 | self.start = 1 324 | else: 325 | self.pbr() 326 | 327 | if tag in ["table", "tr"] and start: self.p() 328 | if tag == 'td': self.pbr() 329 | 330 | if tag == "pre": 331 | if start: 332 | self.startpre = 1 333 | self.pre = 1 334 | else: 335 | self.pre = 0 336 | self.p() 337 | 338 | def pbr(self): 339 | if self.p_p == 0: self.p_p = 1 340 | 341 | def p(self): self.p_p = 2 342 | 343 | def o(self, data, puredata=0, force=0): 344 | if self.abbr_data is not None: self.abbr_data += data 345 | 346 | if not self.quiet: 347 | if puredata and not self.pre: 348 | data = re.sub('\s+', ' ', data) 349 | if data and data[0] == ' ': 350 | self.space = 1 351 | data = data[1:] 352 | if not data and not force: return 353 | 354 | if self.startpre: 355 | #self.out(" :") #TODO: not output when already one there 356 | self.startpre = 0 357 | 358 | bq = (">" * self.blockquote) 359 | if not (force and data and data[0] == ">") and self.blockquote: bq += " " 360 | 361 | if self.pre: 362 | bq += " " 363 | data = data.replace("\n", "\n"+bq) 364 | 365 | if self.start: 366 | self.space = 0 367 | self.p_p = 0 368 | self.start = 0 369 | 370 | if force == 'end': 371 | # It's the end. 372 | self.p_p = 0 373 | self.out("\n") 374 | self.space = 0 375 | 376 | 377 | if self.p_p: 378 | self.out(('\n'+bq)*self.p_p) 379 | self.space = 0 380 | 381 | if self.space: 382 | if not self.lastWasNL: self.out(' ') 383 | self.space = 0 384 | 385 | if self.a and ((self.p_p == 2 and LINKS_EACH_PARAGRAPH) or force == "end"): 386 | if force == "end": self.out("\n") 387 | 388 | newa = [] 389 | for link in self.a: 390 | if self.outcount > link['outcount']: 391 | self.out(" ["+`link['count']`+"]: " + urlparse.urljoin(self.baseurl, link['href'])) 392 | if link.has_key('title'): self.out(" ("+link['title']+")") 393 | self.out("\n") 394 | else: 395 | newa.append(link) 396 | 397 | if self.a != newa: self.out("\n") # Don't need an extra line when nothing was done. 398 | 399 | self.a = newa 400 | 401 | if self.abbr_list and force == "end": 402 | for abbr, definition in self.abbr_list.items(): 403 | self.out(" *[" + abbr + "]: " + definition + "\n") 404 | 405 | self.p_p = 0 406 | self.out(data) 407 | self.lastWasNL = data and data[-1] == '\n' 408 | self.outcount += 1 409 | 410 | def handle_data(self, data): 411 | if r'\/script>' in data: self.quiet -= 1 412 | self.o(data, 1) 413 | 414 | def unknown_decl(self, data): pass 415 | 416 | def wrapwrite(text): sys.stdout.write(text.encode('utf8')) 417 | 418 | def html2text_file(html, out=wrapwrite, baseurl=''): 419 | h = _html2text(out, baseurl) 420 | h.feed(html) 421 | h.feed("") 422 | return h.close() 423 | 424 | def html2text(html, baseurl=''): 425 | return optwrap(html2text_file(html, None, baseurl)) 426 | 427 | if __name__ == "__main__": 428 | baseurl = '' 429 | if sys.argv[1:]: 430 | arg = sys.argv[1] 431 | if arg.startswith('http://') or arg.startswith('https://'): 432 | baseurl = arg 433 | j = urllib.urlopen(baseurl) 434 | try: 435 | from feedparser import _getCharacterEncoding as enc 436 | except ImportError: 437 | enc = lambda x, y: ('utf-8', 1) 438 | text = j.read() 439 | encoding = enc(j.headers, text)[0] 440 | if encoding == 'us-ascii': encoding = 'utf-8' 441 | data = text.decode(encoding) 442 | 443 | else: 444 | encoding = 'utf8' 445 | if len(sys.argv) > 2: 446 | encoding = sys.argv[2] 447 | data = open(arg, 'r').read().decode(encoding) 448 | else: 449 | data = sys.stdin.read().decode('utf8') 450 | wrapwrite(html2text(data, baseurl)) 451 | 452 | -------------------------------------------------------------------------------- /kitsune/mail.py: -------------------------------------------------------------------------------- 1 | from django.core.mail import send_mail as django_send_mail 2 | from django.core.mail import EmailMultiAlternatives 3 | from datetime import datetime as dt 4 | from time import sleep 5 | import threading 6 | 7 | def send_mail(subject, message, from_email, recipient_list,fail_silently=False, auth_user=None, auth_password=None): 8 | class Sender(threading.Thread): 9 | def run(self): 10 | django_send_mail(subject, message, from_email,recipient_list, fail_silently, auth_user, auth_password) 11 | s=Sender() 12 | s.start() 13 | return True 14 | 15 | def send_multi_mail(subject, text_content, html_content, from_email, recipient_list, fail_silently=False): 16 | class Sender(threading.Thread): 17 | def run(self): 18 | msg = EmailMultiAlternatives(subject, text_content, from_email, recipient_list) 19 | msg.attach_alternative(html_content, "text/html") 20 | msg.send() 21 | s=Sender() 22 | s.start() 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /kitsune/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/django-kitsune/54365baf54c799e86bc771fab7aaca0f84dbdbd3/kitsune/management/__init__.py -------------------------------------------------------------------------------- /kitsune/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/django-kitsune/54365baf54c799e86bc771fab7aaca0f84dbdbd3/kitsune/management/commands/__init__.py -------------------------------------------------------------------------------- /kitsune/management/commands/kitsune_cron.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Management command called by cron. 8 | Calls run for all jobs. 9 | 10 | Based on django-chronograph. 11 | 12 | ''' 13 | 14 | __author__ = "Raul Garreta (raul@tryolabs.com)" 15 | 16 | 17 | from django.core.management.base import BaseCommand 18 | 19 | class Command(BaseCommand): 20 | help = 'Runs all jobs that are due.' 21 | 22 | def handle(self, *args, **options): 23 | from kitsune.models import Job 24 | procs = [] 25 | for job in Job.objects.all(): 26 | p = job.run(False) 27 | if p is not None: 28 | procs.append(p) 29 | for p in procs: 30 | p.wait() -------------------------------------------------------------------------------- /kitsune/management/commands/kitsune_cron_clean.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | import sys 3 | 4 | class Command( BaseCommand ): 5 | help = 'Deletes old job logs.' 6 | 7 | def handle( self, *args, **options ): 8 | from kitsune.models import Log 9 | from datetime import datetime, timedelta 10 | 11 | if len( args ) != 2: 12 | sys.stderr.write('Command requires two arguments. Unit (weeks, days, hours or minutes) and interval.\n') 13 | return 14 | else: 15 | unit = str( args[ 0 ] ) 16 | if unit not in [ 'weeks', 'days', 'hours', 'minutes' ]: 17 | sys.stderr.write('Valid units are weeks, days, hours or minutes.\n') 18 | return 19 | try: 20 | amount = int( args[ 1 ] ) 21 | except ValueError: 22 | sys.stderr.write('Interval must be an integer.\n') 23 | return 24 | kwargs = { unit: amount } 25 | time_ago = datetime.now() - timedelta( **kwargs ) 26 | Log.objects.filter( run_date__lte = time_ago ).delete() -------------------------------------------------------------------------------- /kitsune/management/commands/kitsune_cronserver.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from kitsune.models import Job 3 | 4 | import sys 5 | 6 | from time import sleep 7 | 8 | help_text = ''' 9 | Emulates a reoccurring cron call to run jobs at a specified interval. 10 | This is meant primarily for development use. 11 | ''' 12 | 13 | class Command(BaseCommand): 14 | help = help_text 15 | args = "time" 16 | 17 | def handle( self, *args, **options ): 18 | from django.core.management import call_command 19 | try: 20 | t_wait = int(args[0]) 21 | except: 22 | t_wait = 60 23 | try: 24 | print "Starting cronserver. Jobs will run every %d seconds." % t_wait 25 | print "Quit the server with CONTROL-C." 26 | 27 | # Run server untill killed 28 | while True: 29 | for job in Job.objects.all(): 30 | p = job.run(False) 31 | if p is not None: 32 | print "Running: %s" % job 33 | sleep(t_wait) 34 | except KeyboardInterrupt: 35 | print "Exiting..." 36 | sys.exit() -------------------------------------------------------------------------------- /kitsune/management/commands/kitsune_nagios_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 5, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Kitsune check that wrapps any Nagios check. 8 | All necessary parameters must be passed through args field at admin interface. 9 | A special option: "check" must be passed with the name of the Nagios check to run. 10 | eg: 11 | check=check_disk -u=GB -w=5 -c=2 -p=/ 12 | 13 | ''' 14 | 15 | __author__ = "Raul Garreta (raul@tryolabs.com)" 16 | 17 | 18 | from kitsune.base import BaseKitsuneCheck 19 | from kitsune.nagios import NagiosPoller 20 | from kitsune.monitor import ArgSet 21 | 22 | 23 | class Command(BaseKitsuneCheck): 24 | help = 'A Nagios check.' 25 | 26 | 27 | def check(self, *args, **options): 28 | poller = NagiosPoller() 29 | nagios_args = ArgSet() 30 | check = options['check'] 31 | del options['check'] 32 | del options['verbosity'] 33 | 34 | new_args = [] 35 | for arg in args: 36 | if arg != 'verbosity': 37 | new_args.append(arg) 38 | args = new_args 39 | 40 | for arg in args: 41 | nagios_args.add_argument(arg) 42 | for option in options: 43 | nagios_args.add_argument_pair(str(option), str(options[option])) 44 | res = poller.run_plugin(check, nagios_args) 45 | 46 | self.status_code = res.returncode 47 | self.status_message = " NAGIOS_OUT: " + res.output + "
    NAGIOS_ERR: " + res.error 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /kitsune/management/commands/kitsune_run_job.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.management import call_command 4 | from django.core.management.base import BaseCommand 5 | 6 | from kitsune.models import Job, Log 7 | 8 | class Command(BaseCommand): 9 | help = 'Runs a specific job. The job will only run if it is not currently running.' 10 | args = "job.id" 11 | 12 | def handle(self, *args, **options): 13 | try: 14 | job_id = args[0] 15 | except IndexError: 16 | sys.stderr.write("This command requires a single argument: a job id to run.\n") 17 | return 18 | 19 | try: 20 | job = Job.objects.get(pk=job_id) 21 | except Job.DoesNotExist: 22 | sys.stderr.write("The requested Job does not exist.\n") 23 | return 24 | 25 | # Run the job and wait for it to finish 26 | job.handle_run() 27 | -------------------------------------------------------------------------------- /kitsune/management/commands/kitsune_test_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Dummy check to test functionality. 8 | 9 | ''' 10 | 11 | __author__ = "Raul Garreta (raul@tryolabs.com)" 12 | 13 | 14 | from kitsune.renderers import STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN 15 | from kitsune.base import BaseKitsuneCheck 16 | 17 | 18 | class Command(BaseKitsuneCheck): 19 | help = 'A simple test check.' 20 | 21 | 22 | def check(self, *args, **options): 23 | self.status_code = STATUS_OK 24 | 25 | if self.status_code == STATUS_OK: 26 | self.status_message = 'OK message' 27 | elif self.status_code == STATUS_WARNING: 28 | self.status_message = 'WARNING message' 29 | elif self.status_code == STATUS_CRITICAL: 30 | self.status_message = 'CRITICAL message' 31 | else: 32 | self.status_message = 'UNKNOWN message' 33 | 34 | -------------------------------------------------------------------------------- /kitsune/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Job' 12 | db.create_table('kitsune_job', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), 15 | ('frequency', self.gf('django.db.models.fields.CharField')(max_length=10)), 16 | ('params', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 17 | ('command', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)), 18 | ('args', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)), 19 | ('disabled', self.gf('django.db.models.fields.BooleanField')(default=False)), 20 | ('next_run', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 21 | ('last_run', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 22 | ('is_running', self.gf('django.db.models.fields.BooleanField')(default=False)), 23 | ('last_run_successful', self.gf('django.db.models.fields.BooleanField')(default=True)), 24 | ('pid', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 25 | ('force_run', self.gf('django.db.models.fields.BooleanField')(default=False)), 26 | ('host_name', self.gf('django.db.models.fields.CharField')(max_length=128)), 27 | )) 28 | db.send_create_signal('kitsune', ['Job']) 29 | 30 | # Adding M2M table for field subscribers on 'Job' 31 | db.create_table('kitsune_job_subscribers', ( 32 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 33 | ('job', models.ForeignKey(orm['kitsune.job'], null=False)), 34 | ('user', models.ForeignKey(orm['auth.user'], null=False)) 35 | )) 36 | db.create_unique('kitsune_job_subscribers', ['job_id', 'user_id']) 37 | 38 | # Adding model 'Log' 39 | db.create_table('kitsune_log', ( 40 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 41 | ('job', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['kitsune.Job'])), 42 | ('run_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 43 | ('stdout', self.gf('django.db.models.fields.TextField')(blank=True)), 44 | ('stderr', self.gf('django.db.models.fields.TextField')(blank=True)), 45 | ('success', self.gf('django.db.models.fields.BooleanField')(default=True)), 46 | )) 47 | db.send_create_signal('kitsune', ['Log']) 48 | 49 | 50 | def backwards(self, orm): 51 | 52 | # Deleting model 'Job' 53 | db.delete_table('kitsune_job') 54 | 55 | # Removing M2M table for field subscribers on 'Job' 56 | db.delete_table('kitsune_job_subscribers') 57 | 58 | # Deleting model 'Log' 59 | db.delete_table('kitsune_log') 60 | 61 | 62 | models = { 63 | 'auth.group': { 64 | 'Meta': {'object_name': 'Group'}, 65 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 67 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 68 | }, 69 | 'auth.permission': { 70 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 71 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 72 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 75 | }, 76 | 'auth.user': { 77 | 'Meta': {'object_name': 'User'}, 78 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 79 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 80 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 81 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 82 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 83 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 84 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 85 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 86 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 87 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 88 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 89 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 90 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 91 | }, 92 | 'contenttypes.contenttype': { 93 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 94 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 95 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 96 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 97 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 98 | }, 99 | 'kitsune.job': { 100 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 101 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 102 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 103 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 104 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 105 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 106 | 'host_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 107 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 108 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 109 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 110 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 111 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 112 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 113 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 114 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 115 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 116 | }, 117 | 'kitsune.log': { 118 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 119 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 120 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Job']"}), 121 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 122 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 123 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 124 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 125 | } 126 | } 127 | 128 | complete_apps = ['kitsune'] 129 | -------------------------------------------------------------------------------- /kitsune/migrations/0002_auto__add_host.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Host' 12 | db.create_table('kitsune_host', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=150)), 15 | ('ip', self.gf('django.db.models.fields.CharField')(max_length=15, blank=True)), 16 | ('description', self.gf('django.db.models.fields.TextField')(blank=True)), 17 | )) 18 | db.send_create_signal('kitsune', ['Host']) 19 | 20 | 21 | def backwards(self, orm): 22 | 23 | # Deleting model 'Host' 24 | db.delete_table('kitsune_host') 25 | 26 | 27 | models = { 28 | 'auth.group': { 29 | 'Meta': {'object_name': 'Group'}, 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 33 | }, 34 | 'auth.permission': { 35 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | 'auth.user': { 42 | 'Meta': {'object_name': 'User'}, 43 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 44 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 45 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 46 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 49 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 51 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 52 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 53 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 54 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 55 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 56 | }, 57 | 'contenttypes.contenttype': { 58 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 59 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 63 | }, 64 | 'kitsune.host': { 65 | 'Meta': {'object_name': 'Host'}, 66 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 67 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 68 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 69 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 70 | }, 71 | 'kitsune.job': { 72 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 73 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 74 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 75 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 76 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 77 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 78 | 'host_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 79 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 81 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 82 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 83 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 84 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 85 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 86 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 87 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 88 | }, 89 | 'kitsune.log': { 90 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 91 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 92 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Job']"}), 93 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 94 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 95 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 96 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 97 | } 98 | } 99 | 100 | complete_apps = ['kitsune'] 101 | -------------------------------------------------------------------------------- /kitsune/migrations/0003_auto__del_field_job_host_name.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting field 'Job.host_name' 12 | db.delete_column('kitsune_job', 'host_name') 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Adding field 'Job.host_name' 18 | db.add_column('kitsune_job', 'host_name', self.gf('django.db.models.fields.CharField')(default=1, max_length=128), keep_default=False) 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'kitsune.host': { 59 | 'Meta': {'object_name': 'Host'}, 60 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 64 | }, 65 | 'kitsune.job': { 66 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 67 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 68 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 69 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 72 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 74 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 75 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 76 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 77 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 78 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 79 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 80 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 81 | }, 82 | 'kitsune.log': { 83 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 84 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 85 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Job']"}), 86 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 87 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 88 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 89 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 90 | } 91 | } 92 | 93 | complete_apps = ['kitsune'] 94 | -------------------------------------------------------------------------------- /kitsune/migrations/0004_auto__add_field_job_host.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Job.host' 12 | db.add_column('kitsune_job', 'host', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['kitsune.Host']), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Job.host' 18 | db.delete_column('kitsune_job', 'host_id') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'kitsune.host': { 59 | 'Meta': {'object_name': 'Host'}, 60 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 64 | }, 65 | 'kitsune.job': { 66 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 67 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 68 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 69 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 72 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 76 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 77 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 78 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 79 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 80 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 81 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 82 | }, 83 | 'kitsune.log': { 84 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 85 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Job']"}), 87 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 88 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 89 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 90 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 91 | } 92 | } 93 | 94 | complete_apps = ['kitsune'] 95 | -------------------------------------------------------------------------------- /kitsune/migrations/0005_auto__add_field_job_last_result.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Job.last_result' 12 | db.add_column('kitsune_job', 'last_result', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='running_job', null=True, to=orm['kitsune.Log']), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Job.last_result' 18 | db.delete_column('kitsune_job', 'last_result_id') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'kitsune.host': { 59 | 'Meta': {'object_name': 'Host'}, 60 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 64 | }, 65 | 'kitsune.job': { 66 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 67 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 68 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 69 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 72 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 76 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 77 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 78 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 79 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 80 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 81 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 82 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 83 | }, 84 | 'kitsune.log': { 85 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 86 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 87 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 88 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 89 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 90 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 91 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 92 | } 93 | } 94 | 95 | complete_apps = ['kitsune'] 96 | -------------------------------------------------------------------------------- /kitsune/migrations/0006_auto__add_field_job_renderer.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Job.renderer' 12 | db.add_column('kitsune_job', 'renderer', self.gf('django.db.models.fields.CharField')(default='kitsune.models.JobRenderer', max_length=100), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Job.renderer' 18 | db.delete_column('kitsune_job', 'renderer') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'kitsune.host': { 59 | 'Meta': {'object_name': 'Host'}, 60 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 64 | }, 65 | 'kitsune.job': { 66 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 67 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 68 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 69 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 72 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 76 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 77 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 78 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 79 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 80 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 81 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 82 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.JobRenderer'", 'max_length': '100'}), 83 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 84 | }, 85 | 'kitsune.log': { 86 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 87 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 88 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 89 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 90 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 91 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 92 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 93 | } 94 | } 95 | 96 | complete_apps = ['kitsune'] 97 | -------------------------------------------------------------------------------- /kitsune/migrations/0007_auto__add_field_job_log_clean_freq_unit__add_field_job_log_clean_freq_.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Job.log_clean_freq_unit' 12 | db.add_column('kitsune_job', 'log_clean_freq_unit', self.gf('django.db.models.fields.CharField')(default='Hours', max_length=10), keep_default=False) 13 | 14 | # Adding field 'Job.log_clean_freq_value' 15 | db.add_column('kitsune_job', 'log_clean_freq_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1), keep_default=False) 16 | 17 | 18 | def backwards(self, orm): 19 | 20 | # Deleting field 'Job.log_clean_freq_unit' 21 | db.delete_column('kitsune_job', 'log_clean_freq_unit') 22 | 23 | # Deleting field 'Job.log_clean_freq_value' 24 | db.delete_column('kitsune_job', 'log_clean_freq_value') 25 | 26 | 27 | models = { 28 | 'auth.group': { 29 | 'Meta': {'object_name': 'Group'}, 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 33 | }, 34 | 'auth.permission': { 35 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | 'auth.user': { 42 | 'Meta': {'object_name': 'User'}, 43 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 44 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 45 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 46 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 49 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 51 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 52 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 53 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 54 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 55 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 56 | }, 57 | 'contenttypes.contenttype': { 58 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 59 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 63 | }, 64 | 'kitsune.host': { 65 | 'Meta': {'object_name': 'Host'}, 66 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 67 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 68 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 69 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 70 | }, 71 | 'kitsune.job': { 72 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 73 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 74 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 75 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 76 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 77 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 78 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 79 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 81 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 82 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 83 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 84 | 'log_clean_freq_unit': ('django.db.models.fields.CharField', [], {'default': "'Hours'", 'max_length': '10'}), 85 | 'log_clean_freq_value': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 86 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 87 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 88 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 89 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 90 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}), 91 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 92 | }, 93 | 'kitsune.log': { 94 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 95 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 96 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 97 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 98 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 99 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 100 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 101 | } 102 | } 103 | 104 | complete_apps = ['kitsune'] 105 | -------------------------------------------------------------------------------- /kitsune/migrations/0008_auto__del_field_job_log_clean_freq_value__del_field_job_log_clean_freq.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting field 'Job.log_clean_freq_value' 12 | db.delete_column('kitsune_job', 'log_clean_freq_value') 13 | 14 | # Deleting field 'Job.log_clean_freq_unit' 15 | db.delete_column('kitsune_job', 'log_clean_freq_unit') 16 | 17 | # Adding field 'Job.last_logs_to_keep' 18 | db.add_column('kitsune_job', 'last_logs_to_keep', self.gf('django.db.models.fields.PositiveIntegerField')(default=20), keep_default=False) 19 | 20 | 21 | def backwards(self, orm): 22 | 23 | # Adding field 'Job.log_clean_freq_value' 24 | db.add_column('kitsune_job', 'log_clean_freq_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1), keep_default=False) 25 | 26 | # Adding field 'Job.log_clean_freq_unit' 27 | db.add_column('kitsune_job', 'log_clean_freq_unit', self.gf('django.db.models.fields.CharField')(default='Hours', max_length=10), keep_default=False) 28 | 29 | # Deleting field 'Job.last_logs_to_keep' 30 | db.delete_column('kitsune_job', 'last_logs_to_keep') 31 | 32 | 33 | models = { 34 | 'auth.group': { 35 | 'Meta': {'object_name': 'Group'}, 36 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 37 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 38 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 39 | }, 40 | 'auth.permission': { 41 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 42 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 43 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 46 | }, 47 | 'auth.user': { 48 | 'Meta': {'object_name': 'User'}, 49 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 50 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 51 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 53 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 55 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 56 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 57 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 58 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 59 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 60 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 61 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 62 | }, 63 | 'contenttypes.contenttype': { 64 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 65 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 69 | }, 70 | 'kitsune.host': { 71 | 'Meta': {'object_name': 'Host'}, 72 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 75 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 76 | }, 77 | 'kitsune.job': { 78 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 79 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 80 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 81 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 82 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 83 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 84 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 85 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 87 | 'last_logs_to_keep': ('django.db.models.fields.PositiveIntegerField', [], {'default': '20'}), 88 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 89 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 90 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 91 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 92 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 93 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 94 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 95 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}), 96 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'to': "orm['auth.User']"}) 97 | }, 98 | 'kitsune.log': { 99 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 100 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 101 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 102 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 103 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 104 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 105 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 106 | } 107 | } 108 | 109 | complete_apps = ['kitsune'] 110 | -------------------------------------------------------------------------------- /kitsune/migrations/0009_auto__add_notificationrule.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'NotificationRule' 12 | db.create_table('kitsune_notificationrule', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('job', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['kitsune.Job'])), 15 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 16 | ('last_notification', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 17 | ('threshold', self.gf('django.db.models.fields.IntegerField')(max_length=10)), 18 | ('rule_type', self.gf('django.db.models.fields.CharField')(max_length=10)), 19 | ('rule_N', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 20 | ('rule_M', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), 21 | ('frequency_unit', self.gf('django.db.models.fields.CharField')(max_length=10)), 22 | ('frequency_value', self.gf('django.db.models.fields.PositiveIntegerField')(max_length=10)), 23 | )) 24 | db.send_create_signal('kitsune', ['NotificationRule']) 25 | 26 | # Removing M2M table for field subscribers on 'Job' 27 | db.delete_table('kitsune_job_subscribers') 28 | 29 | 30 | def backwards(self, orm): 31 | 32 | # Deleting model 'NotificationRule' 33 | db.delete_table('kitsune_notificationrule') 34 | 35 | # Adding M2M table for field subscribers on 'Job' 36 | db.create_table('kitsune_job_subscribers', ( 37 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 38 | ('job', models.ForeignKey(orm['kitsune.job'], null=False)), 39 | ('user', models.ForeignKey(orm['auth.user'], null=False)) 40 | )) 41 | db.create_unique('kitsune_job_subscribers', ['job_id', 'user_id']) 42 | 43 | 44 | models = { 45 | 'auth.group': { 46 | 'Meta': {'object_name': 'Group'}, 47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 49 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 50 | }, 51 | 'auth.permission': { 52 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 53 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 55 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 57 | }, 58 | 'auth.user': { 59 | 'Meta': {'object_name': 'User'}, 60 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 61 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 62 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 63 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 64 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 65 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 66 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 67 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 68 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 69 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 70 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 71 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 72 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 73 | }, 74 | 'contenttypes.contenttype': { 75 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 76 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 77 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 78 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 79 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 80 | }, 81 | 'kitsune.host': { 82 | 'Meta': {'object_name': 'Host'}, 83 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 84 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 85 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 86 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 87 | }, 88 | 'kitsune.job': { 89 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 90 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 91 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 92 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 93 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 94 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 95 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 96 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 97 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 98 | 'last_logs_to_keep': ('django.db.models.fields.PositiveIntegerField', [], {'default': '20'}), 99 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 100 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 101 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 102 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 103 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 104 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 105 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 106 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}), 107 | 'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'kitsune_jobs'", 'blank': 'True', 'through': "orm['kitsune.NotificationRule']", 'to': "orm['auth.User']"}) 108 | }, 109 | 'kitsune.log': { 110 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 111 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 112 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 113 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 114 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 115 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 116 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 117 | }, 118 | 'kitsune.notificationrule': { 119 | 'Meta': {'object_name': 'NotificationRule'}, 120 | 'frequency_unit': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 121 | 'frequency_value': ('django.db.models.fields.PositiveIntegerField', [], {'max_length': '10'}), 122 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 123 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Job']"}), 124 | 'last_notification': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 125 | 'rule_M': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), 126 | 'rule_N': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 127 | 'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 128 | 'threshold': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}), 129 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 130 | } 131 | } 132 | 133 | complete_apps = ['kitsune'] 134 | -------------------------------------------------------------------------------- /kitsune/migrations/0010_auto__chg_field_notificationrule_frequency_value.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Changing field 'NotificationRule.frequency_value' 12 | db.alter_column('kitsune_notificationrule', 'frequency_value', self.gf('django.db.models.fields.PositiveIntegerField')()) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'NotificationRule.frequency_value' 18 | db.alter_column('kitsune_notificationrule', 'frequency_value', self.gf('django.db.models.fields.PositiveIntegerField')(max_length=10)) 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'kitsune.host': { 59 | 'Meta': {'object_name': 'Host'}, 60 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 64 | }, 65 | 'kitsune.job': { 66 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 67 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 68 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 69 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 72 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_logs_to_keep': ('django.db.models.fields.PositiveIntegerField', [], {'default': '20'}), 76 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 77 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 78 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 79 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 80 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 81 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 82 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 83 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}) 84 | }, 85 | 'kitsune.log': { 86 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 87 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 88 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 89 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 90 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 91 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 92 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 93 | }, 94 | 'kitsune.notificationrule': { 95 | 'Meta': {'object_name': 'NotificationRule'}, 96 | 'frequency_unit': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 97 | 'frequency_value': ('django.db.models.fields.PositiveIntegerField', [], {}), 98 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 99 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subscribers'", 'to': "orm['kitsune.Job']"}), 100 | 'last_notification': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 101 | 'rule_M': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), 102 | 'rule_N': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 103 | 'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 104 | 'threshold': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}), 105 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 106 | } 107 | } 108 | 109 | complete_apps = ['kitsune'] 110 | -------------------------------------------------------------------------------- /kitsune/migrations/0011_auto__add_field_notificationrule_enabled.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'NotificationRule.enabled' 12 | db.add_column('kitsune_notificationrule', 'enabled', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'NotificationRule.enabled' 18 | db.delete_column('kitsune_notificationrule', 'enabled') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'kitsune.host': { 59 | 'Meta': {'object_name': 'Host'}, 60 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 63 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 64 | }, 65 | 'kitsune.job': { 66 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 67 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 68 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 69 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 71 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 72 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'last_logs_to_keep': ('django.db.models.fields.PositiveIntegerField', [], {'default': '20'}), 76 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 77 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 78 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 79 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 80 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 81 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 82 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 83 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}) 84 | }, 85 | 'kitsune.log': { 86 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 87 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 88 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 89 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 90 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 91 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 92 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 93 | }, 94 | 'kitsune.notificationrule': { 95 | 'Meta': {'object_name': 'NotificationRule'}, 96 | 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 97 | 'frequency_unit': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 98 | 'frequency_value': ('django.db.models.fields.PositiveIntegerField', [], {}), 99 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 100 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subscribers'", 'to': "orm['kitsune.Job']"}), 101 | 'last_notification': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 102 | 'rule_M': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), 103 | 'rule_N': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 104 | 'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 105 | 'threshold': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}), 106 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 107 | } 108 | } 109 | 110 | complete_apps = ['kitsune'] 111 | -------------------------------------------------------------------------------- /kitsune/migrations/0012_auto__del_field_notificationrule_frequency_unit__del_field_notificatio.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting field 'NotificationRule.frequency_unit' 12 | db.delete_column('kitsune_notificationrule', 'frequency_unit') 13 | 14 | # Deleting field 'NotificationRule.frequency_value' 15 | db.delete_column('kitsune_notificationrule', 'frequency_value') 16 | 17 | # Adding field 'NotificationRule.interval_unit' 18 | db.add_column('kitsune_notificationrule', 'interval_unit', self.gf('django.db.models.fields.CharField')(default='Hours', max_length=10), keep_default=False) 19 | 20 | # Adding field 'NotificationRule.interval_value' 21 | db.add_column('kitsune_notificationrule', 'interval_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1), keep_default=False) 22 | 23 | 24 | def backwards(self, orm): 25 | 26 | # Adding field 'NotificationRule.frequency_unit' 27 | db.add_column('kitsune_notificationrule', 'frequency_unit', self.gf('django.db.models.fields.CharField')(default=1, max_length=10), keep_default=False) 28 | 29 | # Adding field 'NotificationRule.frequency_value' 30 | db.add_column('kitsune_notificationrule', 'frequency_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1), keep_default=False) 31 | 32 | # Deleting field 'NotificationRule.interval_unit' 33 | db.delete_column('kitsune_notificationrule', 'interval_unit') 34 | 35 | # Deleting field 'NotificationRule.interval_value' 36 | db.delete_column('kitsune_notificationrule', 'interval_value') 37 | 38 | 39 | models = { 40 | 'auth.group': { 41 | 'Meta': {'object_name': 'Group'}, 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 44 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 45 | }, 46 | 'auth.permission': { 47 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 48 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 49 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 50 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 51 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 52 | }, 53 | 'auth.user': { 54 | 'Meta': {'object_name': 'User'}, 55 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 56 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 57 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 58 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 61 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 62 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 63 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 64 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 65 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 66 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 67 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 68 | }, 69 | 'contenttypes.contenttype': { 70 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 71 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 72 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 74 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 75 | }, 76 | 'kitsune.host': { 77 | 'Meta': {'object_name': 'Host'}, 78 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 79 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 81 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 82 | }, 83 | 'kitsune.job': { 84 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 85 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 86 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 87 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 88 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 89 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 90 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 91 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 92 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 93 | 'last_logs_to_keep': ('django.db.models.fields.PositiveIntegerField', [], {'default': '20'}), 94 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 95 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 96 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 97 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 98 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 99 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 100 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 101 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}) 102 | }, 103 | 'kitsune.log': { 104 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 105 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 106 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 107 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 108 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 109 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 110 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 111 | }, 112 | 'kitsune.notificationrule': { 113 | 'Meta': {'object_name': 'NotificationRule'}, 114 | 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 115 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 116 | 'interval_unit': ('django.db.models.fields.CharField', [], {'default': "'Hours'", 'max_length': '10'}), 117 | 'interval_value': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 118 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subscribers'", 'to': "orm['kitsune.Job']"}), 119 | 'last_notification': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 120 | 'rule_M': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), 121 | 'rule_N': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 122 | 'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 123 | 'threshold': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}), 124 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 125 | } 126 | } 127 | 128 | complete_apps = ['kitsune'] 129 | -------------------------------------------------------------------------------- /kitsune/migrations/0013_auto__del_notificationrule__add_notificationgroup__add_notificationuse.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting model 'NotificationRule' 12 | db.delete_table('kitsune_notificationrule') 13 | 14 | # Adding model 'NotificationGroup' 15 | db.create_table('kitsune_notificationgroup', ( 16 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 17 | ('last_notification', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 18 | ('threshold', self.gf('django.db.models.fields.IntegerField')(max_length=10)), 19 | ('rule_type', self.gf('django.db.models.fields.CharField')(max_length=10)), 20 | ('rule_N', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 21 | ('rule_M', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), 22 | ('interval_unit', self.gf('django.db.models.fields.CharField')(max_length=10)), 23 | ('interval_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 24 | ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), 25 | ('job', self.gf('django.db.models.fields.related.ForeignKey')(related_name='subscriber_groups', to=orm['kitsune.Job'])), 26 | ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])), 27 | )) 28 | db.send_create_signal('kitsune', ['NotificationGroup']) 29 | 30 | # Adding model 'NotificationUser' 31 | db.create_table('kitsune_notificationuser', ( 32 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 33 | ('last_notification', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 34 | ('threshold', self.gf('django.db.models.fields.IntegerField')(max_length=10)), 35 | ('rule_type', self.gf('django.db.models.fields.CharField')(max_length=10)), 36 | ('rule_N', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 37 | ('rule_M', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), 38 | ('interval_unit', self.gf('django.db.models.fields.CharField')(max_length=10)), 39 | ('interval_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 40 | ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), 41 | ('job', self.gf('django.db.models.fields.related.ForeignKey')(related_name='subscriber_users', to=orm['kitsune.Job'])), 42 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 43 | )) 44 | db.send_create_signal('kitsune', ['NotificationUser']) 45 | 46 | 47 | def backwards(self, orm): 48 | 49 | # Adding model 'NotificationRule' 50 | db.create_table('kitsune_notificationrule', ( 51 | ('interval_unit', self.gf('django.db.models.fields.CharField')(default='Hours', max_length=10)), 52 | ('job', self.gf('django.db.models.fields.related.ForeignKey')(related_name='subscribers', to=orm['kitsune.Job'])), 53 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 54 | ('threshold', self.gf('django.db.models.fields.IntegerField')(max_length=10)), 55 | ('last_notification', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 56 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 57 | ('rule_type', self.gf('django.db.models.fields.CharField')(max_length=10)), 58 | ('interval_value', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 59 | ('rule_M', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), 60 | ('rule_N', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), 61 | ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), 62 | )) 63 | db.send_create_signal('kitsune', ['NotificationRule']) 64 | 65 | # Deleting model 'NotificationGroup' 66 | db.delete_table('kitsune_notificationgroup') 67 | 68 | # Deleting model 'NotificationUser' 69 | db.delete_table('kitsune_notificationuser') 70 | 71 | 72 | models = { 73 | 'auth.group': { 74 | 'Meta': {'object_name': 'Group'}, 75 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 76 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 77 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 78 | }, 79 | 'auth.permission': { 80 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 81 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 82 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 83 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 84 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 85 | }, 86 | 'auth.user': { 87 | 'Meta': {'object_name': 'User'}, 88 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 89 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 90 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 91 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 92 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 93 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 94 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 95 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 96 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 97 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 98 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 99 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 100 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 101 | }, 102 | 'contenttypes.contenttype': { 103 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 104 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 105 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 106 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 107 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 108 | }, 109 | 'kitsune.host': { 110 | 'Meta': {'object_name': 'Host'}, 111 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 112 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 113 | 'ip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 114 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) 115 | }, 116 | 'kitsune.job': { 117 | 'Meta': {'ordering': "('disabled', 'next_run')", 'object_name': 'Job'}, 118 | 'args': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 119 | 'command': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 120 | 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 121 | 'force_run': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 122 | 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 123 | 'host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['kitsune.Host']"}), 124 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 125 | 'is_running': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 126 | 'last_logs_to_keep': ('django.db.models.fields.PositiveIntegerField', [], {'default': '20'}), 127 | 'last_result': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'running_job'", 'null': 'True', 'to': "orm['kitsune.Log']"}), 128 | 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 129 | 'last_run_successful': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 130 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), 131 | 'next_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 132 | 'params': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 133 | 'pid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 134 | 'renderer': ('django.db.models.fields.CharField', [], {'default': "'kitsune.models.KitsuneJobRenderer'", 'max_length': '100'}) 135 | }, 136 | 'kitsune.log': { 137 | 'Meta': {'ordering': "('-run_date',)", 'object_name': 'Log'}, 138 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 139 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['kitsune.Job']"}), 140 | 'run_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 141 | 'stderr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 142 | 'stdout': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 143 | 'success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 144 | }, 145 | 'kitsune.notificationgroup': { 146 | 'Meta': {'object_name': 'NotificationGroup'}, 147 | 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 148 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), 149 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 150 | 'interval_unit': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 151 | 'interval_value': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 152 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subscriber_groups'", 'to': "orm['kitsune.Job']"}), 153 | 'last_notification': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 154 | 'rule_M': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), 155 | 'rule_N': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 156 | 'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 157 | 'threshold': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}) 158 | }, 159 | 'kitsune.notificationuser': { 160 | 'Meta': {'object_name': 'NotificationUser'}, 161 | 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 162 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 163 | 'interval_unit': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 164 | 'interval_value': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 165 | 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subscriber_users'", 'to': "orm['kitsune.Job']"}), 166 | 'last_notification': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 167 | 'rule_M': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), 168 | 'rule_N': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), 169 | 'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), 170 | 'threshold': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}), 171 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 172 | } 173 | } 174 | 175 | complete_apps = ['kitsune'] 176 | -------------------------------------------------------------------------------- /kitsune/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/django-kitsune/54365baf54c799e86bc771fab7aaca0f84dbdbd3/kitsune/migrations/__init__.py -------------------------------------------------------------------------------- /kitsune/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Kitsune models. 8 | Based on django-chronograph. 9 | 10 | ''' 11 | 12 | __author__ = "Raul Garreta (raul@tryolabs.com)" 13 | 14 | 15 | import os 16 | import re 17 | import subprocess 18 | import sys 19 | import traceback 20 | import inspect 21 | from socket import gethostname 22 | from dateutil import rrule 23 | from StringIO import StringIO 24 | from datetime import datetime, timedelta 25 | 26 | from django.contrib.auth.models import User, Group 27 | from django.conf import settings 28 | from django.core.management import call_command 29 | from django.db import models 30 | from django.template import loader, Context 31 | from django.utils.timesince import timeuntil 32 | from django.utils.translation import ungettext, ugettext, ugettext_lazy as _ 33 | from django.utils.encoding import smart_str 34 | from django.core import urlresolvers 35 | from django.template.loader import render_to_string 36 | 37 | from kitsune.utils import get_manage_py 38 | from kitsune.renderers import KitsuneJobRenderer 39 | from kitsune.base import ( 40 | STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN 41 | ) 42 | from kitsune.mail import send_multi_mail 43 | from kitsune.html2text import html2text 44 | 45 | 46 | RRULE_WEEKDAY_DICT = { 47 | "MO": 0, "TU": 1, "WE": 2, "TH": 3, "FR": 4, "SA": 5, "SU": 6 48 | } 49 | 50 | THRESHOLD_CHOICES = ( 51 | 52 | (STATUS_OK, 'Status OK'), 53 | (STATUS_WARNING, 'Status Warning'), 54 | (STATUS_CRITICAL, 'Status Critical'), 55 | (STATUS_UNKNOWN, 'Status Unknown'), 56 | 57 | ) 58 | 59 | RULE_LAST = 'lt' 60 | RULE_LAST_N = 'n_lt' 61 | RULE_LAST_N_M = 'n_m_lt' 62 | 63 | REPETITION_CHOICES = ( 64 | 65 | (RULE_LAST, 'Last Time'), 66 | (RULE_LAST_N, 'N last times'), 67 | (RULE_LAST_N_M, 'M of N last times'), 68 | 69 | ) 70 | 71 | 72 | class JobManager(models.Manager): 73 | def due(self): 74 | """ 75 | Returns a ``QuerySet`` of all jobs waiting to be run. 76 | """ 77 | return self.filter( 78 | next_run__lte=datetime.now(), 79 | disabled=False, 80 | is_running=False 81 | ) 82 | 83 | # A lot of rrule stuff is from django-schedule 84 | freqs = ( 85 | ("YEARLY", _("Yearly")), 86 | ("MONTHLY", _("Monthly")), 87 | ("WEEKLY", _("Weekly")), 88 | ("DAILY", _("Daily")), 89 | ("HOURLY", _("Hourly")), 90 | ("MINUTELY", _("Minutely")), 91 | ("SECONDLY", _("Secondly")) 92 | ) 93 | 94 | 95 | NOTIF_INTERVAL_CHOICES = ( 96 | ("Hours", _("Hours")), 97 | ("Minutes", _("Minutes")), 98 | ) 99 | 100 | 101 | def get_render_choices(): 102 | choices = [] 103 | try: 104 | for kls in settings.KITSUNE_RENDERERS: 105 | __import__(kls) 106 | m = sys.modules[kls] 107 | for name, obj in inspect.getmembers(m): 108 | if inspect.isclass(obj) and issubclass(obj, KitsuneJobRenderer): 109 | class_name = kls + '.' + name 110 | if name != "KitsuneJobRenderer" and class_name not in choices: 111 | choices.append((class_name, class_name)) 112 | except: 113 | pass 114 | choices.append(( 115 | "kitsune.models.KitsuneJobRenderer", 116 | "kitsune.models.KitsuneJobRenderer" 117 | )) 118 | return choices 119 | 120 | 121 | class Job(models.Model): 122 | """ 123 | A recurring ``django-admin`` command to be run. 124 | """ 125 | name = models.CharField(_("name"), max_length=200) 126 | frequency = models.CharField(_("frequency"), choices=freqs, max_length=10) 127 | params = models.TextField(_("params"), null=True, blank=True, 128 | help_text=_('Comma-separated list of rrule parameters. e.g: interval:15')) 129 | command = models.CharField(_("command"), max_length=200, 130 | help_text=_("A valid django-admin command to run."), blank=True) 131 | args = models.CharField(_("args"), max_length=200, blank=True, 132 | help_text=_("Space separated list; e.g: arg1 option1=True")) 133 | disabled = models.BooleanField(default=False, help_text=_('If checked this job will never run.')) 134 | next_run = models.DateTimeField(_("next run"), blank=True, null=True, help_text=_("If you don't set this it will be determined automatically")) 135 | last_run = models.DateTimeField(_("last run"), editable=False, blank=True, null=True) 136 | is_running = models.BooleanField(default=False, editable=False) 137 | last_run_successful = models.BooleanField(default=True, blank=False, null=False, editable=False) 138 | 139 | pid = models.IntegerField(blank=True, null=True, editable=False) 140 | force_run = models.BooleanField(default=False) 141 | host = models.ForeignKey('Host') 142 | last_result = models.ForeignKey('Log', related_name='running_job', null=True, blank=True) 143 | renderer = models.CharField(choices=get_render_choices(), max_length=100, default="kitsune.models.KitsuneJobRenderer") 144 | last_logs_to_keep = models.PositiveIntegerField(default=20) 145 | 146 | objects = JobManager() 147 | 148 | class Meta: 149 | ordering = ('disabled', 'next_run',) 150 | 151 | def __unicode__(self): 152 | if self.disabled: 153 | return _(u"%(name)s - disabled") % {'name': self.name} 154 | return u"%s - %s" % (self.name, self.timeuntil) 155 | 156 | def save(self, force_insert=False, force_update=False): 157 | if not self.disabled: 158 | if self.pk: 159 | j = Job.objects.get(pk=self.pk) 160 | else: 161 | j = self 162 | if not self.next_run or j.params != self.params: 163 | self.next_run = self.rrule.after(datetime.now()) 164 | else: 165 | self.next_run = None 166 | 167 | super(Job, self).save(force_insert, force_update) 168 | 169 | def get_timeuntil(self): 170 | """ 171 | Returns a string representing the time until the next 172 | time this Job will be run. 173 | """ 174 | if self.disabled: 175 | return _('never (disabled)') 176 | 177 | delta = self.next_run - datetime.now(self.next_run.tzinfo) 178 | if delta.days < 0: 179 | # The job is past due and should be run as soon as possible 180 | return _('due') 181 | elif delta.seconds < 60: 182 | # Adapted from django.utils.timesince 183 | count = lambda n: ungettext('second', 'seconds', n) 184 | return ugettext('%(number)d %(type)s') % {'number': delta.seconds, 185 | 'type': count(delta.seconds)} 186 | return timeuntil(self.next_run) 187 | get_timeuntil.short_description = _('time until next run') 188 | timeuntil = property(get_timeuntil) 189 | 190 | def get_rrule(self): 191 | """ 192 | Returns the rrule objects for this Job. 193 | """ 194 | frequency = eval('rrule.%s' % self.frequency) 195 | return rrule.rrule( 196 | frequency, dtstart=self.last_run, **self.get_params() 197 | ) 198 | rrule = property(get_rrule) 199 | 200 | def param_to_int(self, param_value): 201 | """ 202 | Converts a valid rrule parameter to an integer if it is not already 203 | one, else raises a ``ValueError``. The following are equivalent: 204 | 205 | >>> job = Job(params = "byweekday:1,2,4,5") 206 | >>> job = Job(params = "byweekday:TU,WE,FR,SA") 207 | """ 208 | if param_value in RRULE_WEEKDAY_DICT: 209 | return RRULE_WEEKDAY_DICT[param_value] 210 | try: 211 | val = int(param_value) 212 | except ValueError: 213 | raise ValueError('rrule parameter should be integer or weekday constant (e.g. MO, TU, etc.). Error on: %s' % param_value) 214 | else: 215 | return val 216 | 217 | def get_params(self): 218 | """ 219 | >>> job = Job(params = "count:1;bysecond:1;byminute:1,2,4,5") 220 | >>> job.get_params() 221 | {'count': 1, 'byminute': [1, 2, 4, 5], 'bysecond': 1} 222 | """ 223 | if self.params is None: 224 | return {} 225 | params = self.params.split(';') 226 | param_dict = [] 227 | for param in params: 228 | if param.strip() == "": 229 | continue # skip blanks 230 | param = param.split(':') 231 | if len(param) == 2: 232 | param = ( 233 | str(param[0]).strip(), 234 | [self.param_to_int(p.strip()) for p in param[1].split(',')] 235 | ) 236 | if len(param[1]) == 1: 237 | param = (param[0], param[1][0]) 238 | param_dict.append(param) 239 | return dict(param_dict) 240 | 241 | def get_args(self): 242 | """ 243 | Processes the args and returns a tuple or (args, options) for passing 244 | to ``call_command``. 245 | """ 246 | args = [] 247 | options = {} 248 | for arg in self.args.split(): 249 | if arg.find('=') > -1: 250 | key, value = arg.split('=') 251 | options[smart_str(key)] = smart_str(value) 252 | else: 253 | args.append(arg) 254 | return (args, options) 255 | 256 | def is_due(self): 257 | reqs = ( 258 | self.next_run <= datetime.now() and self.disabled is False 259 | and self.is_running is False 260 | ) 261 | return (reqs or self.force_run) 262 | 263 | def run(self, wait=True): 264 | """ 265 | Runs this ``Job``. If ``wait`` is ``True`` any call to this function 266 | will not return untill the ``Job`` is complete (or fails). This 267 | actually calls the management command ``kitsune_run_job`` via a 268 | subprocess. If you call this and want to wait for the process to 269 | complete, pass ``wait=True``. 270 | 271 | A ``Log`` will be created if there is any output from either 272 | stdout or stderr. 273 | 274 | Returns the process, a ``subprocess.Popen`` instance, or None. 275 | """ 276 | if not self.disabled and self.host.name == gethostname(): 277 | if not self.check_is_running() and self.is_due(): 278 | p = subprocess.Popen([ 279 | 'python', get_manage_py(), 'kitsune_run_job', str(self.pk) 280 | ]) 281 | if wait: 282 | p.wait() 283 | return p 284 | return None 285 | 286 | def handle_run(self): 287 | """ 288 | This method implements the code to actually run a job. This is meant to 289 | be run, primarily, by the `kitsune_run_job` management command as a 290 | subprocess, which can be invoked by calling this job's ``run_job`` 291 | method. 292 | """ 293 | args, options = self.get_args() 294 | stdout = StringIO() 295 | stderr = StringIO() 296 | 297 | # Redirect output so that we can log it if there is any 298 | ostdout = sys.stdout 299 | ostderr = sys.stderr 300 | sys.stdout = stdout 301 | sys.stderr = stderr 302 | stdout_str, stderr_str = "", "" 303 | 304 | run_date = datetime.now() 305 | self.is_running = True 306 | self.pid = os.getpid() 307 | self.save() 308 | 309 | try: 310 | call_command(self.command, *args, **options) 311 | self.last_run_successful = True 312 | except Exception, e: 313 | # The command failed to run; log the exception 314 | t = loader.get_template('kitsune/error_message.txt') 315 | trace = ['\n'.join(traceback.format_exception(*sys.exc_info()))] 316 | c = Context({ 317 | 'exception': unicode(e), 318 | 'traceback': trace 319 | }) 320 | stderr_str += t.render(c) 321 | self.last_run_successful = False 322 | 323 | self.is_running = False 324 | self.pid = None 325 | self.last_run = run_date 326 | 327 | # If this was a forced run, then don't update the 328 | # next_run date 329 | if self.force_run: 330 | self.force_run = False 331 | else: 332 | self.next_run = self.rrule.after(run_date) 333 | 334 | # If we got any output, save it to the log 335 | stdout_str += stdout.getvalue() 336 | stderr_str += stderr.getvalue() 337 | 338 | if stderr_str: 339 | # If anything was printed to stderr, consider the run 340 | # unsuccessful 341 | self.last_run_successful = False 342 | 343 | if stdout_str or stderr_str: 344 | log = Log.objects.create( 345 | job=self, 346 | run_date=run_date, 347 | stdout=stdout_str, 348 | stderr=stderr_str 349 | ) 350 | self.last_result = log 351 | 352 | self.save() 353 | self.delete_old_logs() 354 | self.email_subscribers() 355 | 356 | # Redirect output back to default 357 | sys.stdout = ostdout 358 | sys.stderr = ostderr 359 | 360 | def email_subscribers(self): 361 | from_email = settings.DEFAULT_FROM_EMAIL 362 | subject = 'Kitsune monitoring notification' 363 | 364 | user_ids = set([]) 365 | for sub in self.subscriber_users.all(): 366 | #notify subscribed users 367 | if sub.must_notify(): 368 | sub.last_notification = datetime.now() 369 | sub.save() 370 | html_message = render_to_string( 371 | 'kitsune/mail_notification.html', {'log': self.last_result} 372 | ) 373 | text_message = html2text(html_message) 374 | user_ids.add(sub.user.id) 375 | send_multi_mail( 376 | subject, text_message, html_message, from_email, 377 | [sub.user.email], fail_silently=False 378 | ) 379 | 380 | for sub in self.subscriber_groups.all(): 381 | if sub.must_notify(): 382 | #notify subscribed groups 383 | sub.last_notification = datetime.now() 384 | sub.save() 385 | for user in sub.group.user_set.all(): 386 | if not (user.id in user_ids): 387 | #notify users that have not already being notified 388 | html_message = render_to_string( 389 | 'kitsune/mail_notification.html', 390 | {'log': self.last_result} 391 | ) 392 | text_message = html2text(html_message) 393 | send_multi_mail( 394 | subject, text_message, html_message, from_email, 395 | [user.email], fail_silently=False 396 | ) 397 | 398 | def delete_old_logs(self): 399 | log = Log.objects.filter(job=self).order_by('-run_date')[self.last_logs_to_keep] 400 | Log.objects.filter(job=self, run_date__lte=log.run_date).delete() 401 | 402 | def check_is_running(self): 403 | """ 404 | This function actually checks to ensure that a job is running. 405 | Currently, it only supports `posix` systems. On non-posix systems 406 | it returns the value of this job's ``is_running`` field. 407 | """ 408 | status = False 409 | if self.is_running and self.pid is not None: 410 | # The Job thinks that it is running, so 411 | # lets actually check 412 | if os.name == 'posix': 413 | # Try to use the 'ps' command to see if the process 414 | # is still running 415 | pid_re = re.compile(r'%d ([^\r\n]*)\n' % self.pid) 416 | p = subprocess.Popen( 417 | ["ps", "-eo", "pid args"], stdout=subprocess.PIPE 418 | ) 419 | p.wait() 420 | # If ``pid_re.findall`` returns a match it means that we have a 421 | # running process with this ``self.pid``. Now we must check for 422 | # the ``run_command`` process with the given ``self.pk`` 423 | try: 424 | pname = pid_re.findall(p.stdout.read())[0] 425 | except IndexError: 426 | pname = '' 427 | if pname.find('kitsune_run_job %d' % self.pk) > -1: 428 | # This Job is still running 429 | return True 430 | else: 431 | # This job thinks it is running, but really isn't. 432 | self.is_running = False 433 | self.pid = None 434 | self.save() 435 | else: 436 | # TODO: add support for other OSes 437 | return self.is_running 438 | return False 439 | 440 | 441 | class NotificationRule(models.Model): 442 | last_notification = models.DateTimeField( 443 | blank=True, null=True, editable=False 444 | ) 445 | threshold = models.IntegerField(choices=THRESHOLD_CHOICES, max_length=10) 446 | rule_type = models.CharField(choices=REPETITION_CHOICES, max_length=10) 447 | rule_N = models.PositiveIntegerField(default=1) 448 | rule_M = models.PositiveIntegerField(default=2) 449 | interval_unit = models.CharField( 450 | choices=NOTIF_INTERVAL_CHOICES, max_length=10 451 | ) 452 | interval_value = models.PositiveIntegerField(default=1) 453 | enabled = models.BooleanField(default=True) 454 | 455 | def __unicode__(self): 456 | return u'Notification to:' 457 | 458 | def must_notify(self): 459 | if self.enabled: 460 | 461 | if self.last_notification is not None: 462 | if self.interval_unit == 'Minutes': 463 | dt = timedelta(minutes=self.interval_value) 464 | elif self.interval_unit == 'Hours': 465 | dt = timedelta(hours=self.interval_value) 466 | else: 467 | dt = timedelta(hours=1) 468 | 469 | threshold = self.last_notification + dt 470 | tt = datetime.now() 471 | if threshold > tt: 472 | return False 473 | 474 | if self.rule_type == RULE_LAST: 475 | return self.job.last_result.get_status_code() >= self.threshold 476 | 477 | elif self.rule_type == RULE_LAST_N: 478 | n = 0 479 | logs = self.job.logs.order_by('-run_date')[:self.rule_N] 480 | for log in logs: 481 | if log.get_status_code() < self.threshold: 482 | break 483 | else: 484 | n += 1 485 | return n == self.rule_N 486 | 487 | elif self.rule_type == RULE_LAST_N_M: 488 | n = 0 489 | logs = self.job.logs.order_by('-run_date')[:self.rule_N] 490 | for log in logs: 491 | if log.get_status_code() >= self.threshold: 492 | n += 1 493 | return n >= self.rule_M 494 | return False 495 | 496 | class Meta: 497 | abstract = True 498 | 499 | 500 | class NotificationUser(NotificationRule): 501 | job = models.ForeignKey('Job', related_name='subscriber_users') 502 | user = models.ForeignKey(User) 503 | 504 | 505 | class NotificationGroup(NotificationRule): 506 | job = models.ForeignKey('Job', related_name='subscriber_groups') 507 | group = models.ForeignKey(Group) 508 | 509 | 510 | class Log(models.Model): 511 | """ 512 | A record of stdout and stderr of a ``Job``. 513 | """ 514 | job = models.ForeignKey('Job', related_name='logs') 515 | run_date = models.DateTimeField(auto_now_add=True) 516 | stdout = models.TextField(blank=True) 517 | stderr = models.TextField(blank=True) 518 | success = models.BooleanField(default=True) # , editable=False) 519 | 520 | class Meta: 521 | ordering = ('-run_date',) 522 | 523 | def __unicode__(self): 524 | return u"%s - %s" % (self.job.name, self.run_date) 525 | 526 | def admin_link(self): 527 | return urlresolvers.reverse( 528 | 'admin:kitsune_' + self.__class__.__name__.lower() + '_change', 529 | args=(self.id,) 530 | ) 531 | 532 | def get_status_code(self): 533 | return int(self.stderr) 534 | 535 | 536 | class Host(models.Model): 537 | """ 538 | The hosts to be checked. 539 | """ 540 | name = models.CharField(blank=False, max_length=150) 541 | ip = models.CharField(blank=True, max_length=15) 542 | description = models.TextField(blank=True) 543 | 544 | def __unicode__(self): 545 | return self.name 546 | -------------------------------------------------------------------------------- /kitsune/monitor.py: -------------------------------------------------------------------------------- 1 | """ Utility interface classes for passing information off to pollers (ArgSet) or receiving results to be 2 | published against existing Monitors.""" 3 | 4 | import simplejson 5 | import dateutil.parser 6 | import datetime 7 | import re 8 | 9 | 10 | class MonitoringPoller: 11 | """ Abstract base class for the pollers that Eyes runs. Each poller is expected to have 12 | the following functions: 13 | run_plugin(plugin_name, argset) 14 | plugin_help(plugin_name) 15 | plugin_list() 16 | """ 17 | 18 | def run_plugin(self, plugin_name, argset=None): 19 | """ runs the plugin and returns the results in the form of a MonitorResult object""" 20 | raise NotImplementedError 21 | 22 | def plugin_help(self, plugin_name): 23 | """ runs the given plugin function to get interactive help results. Intended to 24 | return sufficient information to create an ArgSet object to run the poller properly.""" 25 | raise NotImplementedError 26 | 27 | def plugin_list(self): 28 | """ returns a list of the plugins that this poller provides. And of the list of 29 | plugins should be able to be invoked with plugin_help(plugin_name) to get a response back 30 | that includes sufficient information to create an ArgSet and invoke the plugin 31 | to monitor a remote system.""" 32 | return None 33 | 34 | def __init__(self): 35 | """default initialization""" 36 | self.poller_kind = "eyeswebapp.util.baseclass" 37 | 38 | 39 | class MonitorResult: 40 | """ 41 | A class representation of the dictionary structure that a monitor returns as it's 42 | combined result set. A MonitorResult can be serialized and deserialized into JSON 43 | and includes a structured segment to return multiple counts/values as a part of a monitor 44 | invocation, either active of passive. Initial structure of MonitorResult is based on the 45 | data that a Nagios plugin returns. 46 | 47 | The internal dictionary structure: 48 | ** command - string 49 | ** error - string or None 50 | ** returncode - integer 51 | ** timestamp - string of a datestamp (ISO format) 52 | ** output - string or None 53 | ** decoded - a dictionary 54 | ** decoded must have the following keys: 55 | *** human - a string 56 | *** 1 or more other keys, which are strings 57 | *** for each key other than human, the following keys must exist: 58 | **** UOM - a string of [] or None 59 | **** critvalue - string repr of a number, empty string, or None 60 | **** warnvalue - string repr of a number, empty string, or None 61 | **** label - string same as the key 62 | **** maxvalue - string repr of a number, empty string, or None 63 | **** minvalue - string repr of a number, empty string, or None 64 | **** minvalue - string repr of a number 65 | 66 | Here's an example: 67 | {'command': '/opt/local/libexec/nagios/check_ping -H localhost -w 1,99% -c 1,99%', 68 | 'decoded': {'human': 'PING OK - Packet loss = 0%, RTA = 0.11 ms', 69 | 'pl': {'UOM': '%', 70 | 'critvalue': '99', 71 | 'label': 'pl', 72 | 'maxvalue': '', 73 | 'minvalue': '0', 74 | 'value': '0', 75 | 'warnvalue': '99'}, 76 | 'rta': {'UOM': 'ms', 77 | 'critvalue': '1.000000', 78 | 'label': 'rta', 79 | 'maxvalue': '', 80 | 'minvalue': '0.000000', 81 | 'value': '0.113000', 82 | 'warnvalue': '1.000000'}}, 83 | 'error': None, 84 | 'output': 'PING OK - Packet loss = 0%, RTA = 0.11 ms|rta=0.113000ms;1.000000;1.000000;0.000000 pl=0%;99;99;0', 85 | 'returncode': 0, 86 | 'timestamp': '2009-11-07T16:43:46.696214'} 87 | """ 88 | UOM_PARSECODE = re.compile('([\d\.]+)([a-zA-Z%]*)') 89 | 90 | def __init__(self): 91 | self._initialize() 92 | # def __delitem__(self,key): 93 | # del self._internal_dict[key] 94 | # def __setitem__(self,key,item): 95 | # self._internal_dict[key]=item 96 | # def __getitem__(self,key): 97 | # return self._internal_dict[key] 98 | # def __iter__(self): 99 | # return self._internal_dict.__iter__() 100 | # def __repr__(self): 101 | # return self._internal_dict.__repr__() 102 | # def has_key(self,key): 103 | # return self._internal_dict.has_key(key) 104 | # def keys(self): 105 | # return self._internal_dict.keys() 106 | 107 | def _initialize(self): 108 | self.command = "" 109 | self.output = "" 110 | self.error = "" 111 | self.returncode = 0 112 | self.timestamp = datetime.datetime.now() 113 | decoded_dict = {'human': ''} 114 | empty_label = '_' 115 | data_dict = {} 116 | data_dict['label'] = empty_label 117 | data_dict['value'] = 0 118 | data_dict['UOM'] = '' 119 | data_dict['warnvalue'] = '' 120 | data_dict['critvalue'] = '' 121 | data_dict['minvalue'] = '' 122 | data_dict['maxvalue'] = '' 123 | decoded_dict[empty_label] = data_dict 124 | self.decoded = decoded_dict 125 | 126 | @staticmethod 127 | def parse_nagios_output(nagios_output_string): 128 | """ parses the standard output of a nagios check command. The resulting dictionary as output 129 | will have at least one key: "human", indicating the human readable portion of what was parsed. 130 | There will be additional dictionaries of parsed data, each from a key based on the label of 131 | the performance data returned by the nagios check command. 132 | 133 | For notes on the guidelines for writing Nagios plugins and their expected output, see 134 | http://nagiosplug.sourceforge.net/developer-guidelines.html 135 | 136 | For example parse_nagios_output("PING OK - Packet loss = 0%, RTA = 0.18 ms|rta=0.182ms;1.00;1.00;0.00 pl=0%;99;99;0") 137 | should come back as 138 | {'human': 'PING OK - Packet loss = 0%, RTA = 0.18 ms', 139 | 'pl': {'UOM': '%', 140 | 'critvalue': '99', 141 | 'label': 'pl', 142 | 'maxvalue': '', 143 | 'minvalue': '0', 144 | 'value': '0', 145 | 'warnvalue': '99'}, 146 | 'rta': {'UOM': 'ms', 147 | 'critvalue': '1.00', 148 | 'label': 'rta', 149 | 'maxvalue': '', 150 | 'minvalue': '0.00', 151 | 'value': '0.182', 152 | 'warnvalue': '1.00'}} 153 | 154 | Each parsed performance data dictionary should have the following keys: 155 | * label 156 | * value 157 | * UOM 158 | '' - assume a number (int or float) of things (eg, users, processes, load averages) 159 | s - seconds (also us, ms) 160 | % - percentage 161 | B - bytes (also KB, MB, TB) 162 | c - a continous counter (such as bytes transmitted on an interface) 163 | * warnvalue (content may be None) 164 | * critvalue (content may be None) 165 | * minvalue (content may be None) 166 | * maxvalue (content may be None) 167 | """ 168 | if nagios_output_string is None: 169 | return None 170 | return_dict = {} 171 | try: 172 | (humandata, parsedata) = nagios_output_string.split('|') 173 | except ValueError: # output not in expected format, bail out 174 | return None 175 | return_dict['human'] = humandata 176 | list_of_parsedata = parsedata.split() # ['rta=0.182000ms;1.000000;1.000000;0.000000', 'pl=0%;99;99;0'] 177 | for dataset in list_of_parsedata: 178 | parts = dataset.split(';', 5) 179 | if (len(parts) > 0): 180 | data_dict = {} 181 | try: 182 | (label, uom_value) = parts[0].split('=') 183 | except ValueError: # output not in expected format, bail out 184 | return None 185 | data_dict['label'] = label 186 | result = MonitorResult.UOM_PARSECODE.match(uom_value) 187 | data_dict['value'] = result.groups()[0] 188 | data_dict['UOM'] = result.groups()[1] 189 | data_dict['warnvalue'] = '' 190 | data_dict['critvalue'] = '' 191 | data_dict['minvalue'] = '' 192 | data_dict['maxvalue'] = '' 193 | if len(parts) > 1: 194 | data_dict['warnvalue'] = parts[1] 195 | if len(parts) > 2: 196 | data_dict['critvalue'] = parts[2] 197 | if len(parts) > 3: 198 | data_dict['minvalue'] = parts[3] 199 | if len(parts) > 4: 200 | data_dict['maxvalue'] = parts[4] 201 | return_dict[label] = data_dict 202 | return return_dict 203 | 204 | @staticmethod 205 | def createMonitorResultFromNagios(nagios_output_string): 206 | """ creates a new MonitorResult object from a nagios output string """ 207 | if nagios_output_string is None: 208 | raise ValueError("Empty nagios output string provided to initializer") 209 | parsed_dict = MonitorResult.parse_nagios_output(nagios_output_string) 210 | if parsed_dict is None: 211 | raise ValueError("Error parsing Nagios output") 212 | new_monitor_result = MonitorResult() 213 | new_monitor_result.decoded = parsed_dict 214 | return new_monitor_result 215 | 216 | def json(self): 217 | """ return MonitorResult as a JSON representation """ 218 | dict_to_dump = {} 219 | dict_to_dump['command'] = self.command 220 | dict_to_dump['output'] = self.output 221 | dict_to_dump['error'] = self.error 222 | dict_to_dump['returncode'] = self.returncode 223 | dict_to_dump['timestamp'] = self.timestamp.isoformat() 224 | dict_to_dump['decoded'] = self.decoded 225 | return simplejson.dumps(dict_to_dump) 226 | 227 | # unicode_type = type(u'123') 228 | # string_type = type('123') 229 | # int_type = type(5) 230 | # dict_type = type({}) 231 | # list_type = type([]) 232 | # load using "isinstance" if isinstance(key,unicode_type...) 233 | 234 | def loadjson(self, json_string): 235 | """ load up an external JSON string into an ArgSet, overwriting the existing data here""" 236 | some_structure = simplejson.loads(json_string) 237 | # validate structure 238 | if not(isinstance(some_structure, type({}))): 239 | raise ValueError("json structure being loaded (%s) is not a dictionary" % some_structure) 240 | # 241 | # command validation 242 | if not('command' in some_structure): 243 | raise KeyError("dictionary must have a 'command' key") 244 | new_command = some_structure['command'] 245 | if not(isinstance(new_command, type('123')) or isinstance(new_command, type(u'123'))): 246 | raise ValueError("command value must be a string or unicode") 247 | # 248 | # error validaton 249 | if not('error' in some_structure): 250 | raise KeyError("dictionary must have an 'error' key") 251 | new_error = some_structure['error'] 252 | # 253 | # return code validaton 254 | if not('returncode' in some_structure): 255 | raise KeyError("dictionary must have a 'returncode' key") 256 | new_rc = some_structure['returncode'] 257 | if not(isinstance(new_rc, type(5))): 258 | raise ValueError("returncode must be an integer") 259 | if ((new_rc < 0) or (new_rc > 3)): 260 | raise ValueError("returncode must be between 0 and 3") 261 | # 262 | # timestamp validation 263 | if not('timestamp' in some_structure): 264 | raise KeyError("dictionary must have a 'timestamp' key") 265 | new_timestamp = dateutil.parser.parse(some_structure['timestamp']) 266 | # 267 | # output validation 268 | if not('output' in some_structure): 269 | raise KeyError("dictionary must have an 'output' key") 270 | new_output = some_structure['output'] 271 | # 272 | # decoded validation 273 | if not('decoded' in some_structure): 274 | raise KeyError("dictionary must have a 'decoded' key") 275 | # 276 | decoded_dict = some_structure['decoded'] 277 | if not(isinstance(decoded_dict, type({}))): 278 | raise ValueError("decoded value must be a dictionary") 279 | if not('human' in decoded_dict): 280 | raise KeyError("decoded dictionary must have a 'human' key") 281 | if not(isinstance(decoded_dict['human'], type('123')) or isinstance(decoded_dict['human'], type(u'123'))): 282 | raise ValueError("value for 'human' key must be a string or unicode") 283 | # 284 | keylist = decoded_dict.keys() 285 | keylist.remove('human') 286 | if len(keylist) < 1: 287 | raise KeyError("decoded dictionary must have a key other than 'human' ") 288 | for key in keylist: 289 | keydict = decoded_dict[key] 290 | if not(isinstance(keydict, type({}))): 291 | raise ValueError("keydict must be a dictionary") 292 | # 293 | if not('UOM' in keydict): 294 | raise ValueError("key dictionary must have a 'UOM' key") 295 | # 296 | if not('label' in keydict): 297 | raise ValueError("key dictionary must have a 'label' key") 298 | # 299 | if not('maxvalue' in keydict): 300 | raise ValueError("key dictionary must have a 'maxvalue' key") 301 | # 302 | if not('minvalue' in keydict): 303 | raise ValueError("key dictionary must have a 'minvalue' key") 304 | # 305 | if not('critvalue' in keydict): 306 | raise ValueError("key dictionary must have a 'critvalue' key") 307 | # 308 | if not('warnvalue' in keydict): 309 | raise ValueError("key dictionary must have a 'warnvalue' key") 310 | # 311 | if not('value' in keydict): 312 | raise ValueError("key dictionary must have a 'value' key") 313 | floatval = float(keydict['value']) 314 | # 315 | # we made it through the validation gauntlet - set the structure into place 316 | self.command = new_command 317 | self.output = new_output 318 | self.error = new_error 319 | self.returncode = new_rc 320 | self.timestamp = new_timestamp 321 | self.decoded = decoded_dict 322 | 323 | 324 | def validate_return_dictionary(result_struct): 325 | """ 326 | The returning structure should: 327 | * be a dictionary with the following mandatory keys: 328 | ** command - string 329 | ** error - string or None 330 | ** returncode - integer 331 | ** timestamp - string of a datestamp (ISO format) 332 | ** output - string or None 333 | ** decoded - a dictionary 334 | ** decoded must have the following keys: 335 | *** human - a string 336 | *** 1 or more other keys, which are strings 337 | *** for each key other than human, the following keys must exist: 338 | **** UOM - a string of [] or None 339 | **** critvalue - string repr of a number, empty string, or None 340 | **** warnvalue - string repr of a number, empty string, or None 341 | **** label - string same as the key 342 | **** maxvalue - string repr of a number, empty string, or None 343 | **** minvalue - string repr of a number, empty string, or None 344 | **** minvalue - string repr of a number 345 | """ 346 | if (result_struct.__class__ == {}.__class__): 347 | # command validation 348 | if not('command' in result_struct): 349 | return False 350 | if result_struct['command'] is None: 351 | return False 352 | if not((result_struct['command'].__class__ == '123'.__class__) or (result_struct['command'].__class__ == u'123'.__class__)): 353 | return False 354 | # error validaton 355 | if not('error' in result_struct): 356 | return False 357 | # return code validaton 358 | if not('returncode' in result_struct): 359 | return False 360 | if not((type(result_struct['returncode']) == type(3)) or type(result_struct['returncode']) == type(3.1)): 361 | return False 362 | if ((result_struct['returncode'] < 0) or (result_struct['returncode'] > 3)): 363 | return False 364 | # timestamp validation 365 | if not('timestamp' in result_struct): 366 | return False 367 | try: 368 | result = dateutil.parser.parse(result_struct['timestamp']) 369 | except ValueError: 370 | return False 371 | if not('output' in result_struct): 372 | return False 373 | if not('decoded' in result_struct): 374 | return False 375 | decoded_dict = result_struct['decoded'] 376 | if type(decoded_dict) != type({}): 377 | return False 378 | if not('human' in decoded_dict): 379 | return False 380 | if len(decoded_dict.keys()) < 2: 381 | return False 382 | keylist = decoded_dict.keys() 383 | keylist.remove('human') 384 | if len(keylist) < 1: 385 | return False 386 | for key in keylist: 387 | keydict = decoded_dict[key] 388 | if type(keydict) != type({}): 389 | return False 390 | if not('UOM' in keydict): 391 | return False 392 | # 393 | if not('critvalue' in keydict): 394 | return False 395 | # 396 | if not('label' in keydict): 397 | return False 398 | # 399 | if not('maxvalue' in keydict): 400 | return False 401 | # 402 | if not('minvalue' in keydict): 403 | return False 404 | # 405 | if not('warnvalue' in keydict): 406 | return False 407 | # 408 | if not('value' in keydict): 409 | return False 410 | try: 411 | floatval = float(keydict['value']) 412 | except ValueError: 413 | return False 414 | # 415 | return True 416 | return False 417 | 418 | 419 | def validate_poller_results(json_return_dict): 420 | """ validates a return set from a poller, returning True if the format is acceptable, False if not. 421 | This methods *expects* a JSON string as input 422 | """ 423 | if json_return_dict is None: 424 | return False 425 | try: 426 | result_struct = simplejson.loads(json_return_dict) 427 | return validate_return_dictionary(result_struct) 428 | except: 429 | return False 430 | 431 | 432 | class ArgSet: 433 | """ 434 | a class representing the set of arguments to pass into a command invocation to trigger a poller. 435 | Expected to be able to be serialized into a JSON object and back again. 436 | Suitable for a message that can pass in a queue if needed. 437 | """ 438 | def __init__(self): 439 | self._internal_list = [] 440 | 441 | def list_of_arguments(self): 442 | """returns a flat list of arguments""" 443 | return self._internal_list 444 | 445 | def add_argument(self, argument): 446 | """method for adding a single argument, such as '--help' to a call""" 447 | self._internal_list.append(argument) 448 | 449 | def add_argument_pair(self, arg_key, arg_value): 450 | """method for adding a pair of arguments, such as '-H localhost' to a call""" 451 | new_string = "%s %s" % (arg_key, arg_value) 452 | self._internal_list.append(new_string) 453 | 454 | def json(self): 455 | """ return argset as a JSON representation """ 456 | return simplejson.dumps(self._internal_list) 457 | 458 | def loadjson(self, json_string): 459 | """ load up an external JSON string into an ArgSet, overwriting the existing data here""" 460 | structure = simplejson.loads(json_string) 461 | # validate structure 462 | if (structure.__class__ == [].__class__): 463 | # outer shell is a list.. so far, so good 464 | for argument in structure: 465 | if (argument.__class__ == '123'.__class__) or (argument.__class__ == u'123'.__class__): 466 | #argument is a string 467 | pass 468 | else: 469 | raise ValueError("argument (%s) is not a string" % argument) 470 | else: 471 | raise ValueError("json structure being loaded (%s) was not a list" % structure) 472 | self._internal_list = structure 473 | 474 | def __str__(self): 475 | if len(self._internal_list) < 1: 476 | return "" 477 | else: 478 | return " ".join(self._internal_list) 479 | -------------------------------------------------------------------------------- /kitsune/nagios.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | nagios.py 5 | 6 | Class to invoke nagios plugins and return the results in a structured format. 7 | 8 | Created by Joseph Heck on 2009-10-24. 9 | 10 | """ 11 | import os 12 | import sys 13 | import re 14 | import subprocess 15 | import datetime 16 | from monitor import ArgSet 17 | from monitor import MonitorResult 18 | from monitor import MonitoringPoller 19 | 20 | 21 | class NagiosPoller(MonitoringPoller): 22 | """a class that invokes a Nagios plugin and returns the result""" 23 | def __init__(self): 24 | """default initialization""" 25 | MonitoringPoller.__init__(self) 26 | self.plugin_dir = "/usr/local/nagios/libexec" # default - aiming for RHEL5 instance of nagios-plugins 27 | if os.path.exists("/usr/lib/nagios/plugins"): # ubuntu's apt-get install nagios-plugins 28 | self.plugin_dir = "/usr/lib/nagios/plugins" 29 | if os.path.exists("/opt/local/libexec/nagios"): # MacOS X port install nagios-plugins 30 | self.plugin_dir = "/opt/local/libexec/nagios" 31 | self._internal_plugin_list = [] 32 | self._load_plugin_list() 33 | self.uom_parsecode = re.compile('([\d\.]+)([a-zA-Z%]*)') 34 | self.poller_kind = "eyeswebapp.util.nagios.NagiosPoller" 35 | 36 | def _load_plugin_list(self): 37 | """ load in the plugins from the directory 'plugin_dir' set on the poller...""" 38 | self._internal_plugin_list = [] 39 | raw_list = os.listdir(self.plugin_dir) 40 | for potential in raw_list: 41 | if potential.startswith("check_"): 42 | self._internal_plugin_list.append(potential) 43 | 44 | def plugin_list(self): 45 | """ returns the internal list of plugins available to Nagios""" 46 | return self._internal_plugin_list 47 | 48 | def plugin_help(self, plugin_name): 49 | """invoke --help on the named plugin, return the results""" 50 | argset = ArgSet() 51 | argset.add_argument('--help') 52 | return self.run_plugin(plugin_name, argset) 53 | 54 | def _invoke(self, plugin_name, list_of_args=None): 55 | """parse and invoke the plugin. method accepts the plugin name and then a list of arguments to be invoked. 56 | The return value is either None or a dictionary with the following keys: 57 | * command - the command invoked on the command line from the poller 58 | * output - the standard output from the command, strip()'d 59 | * error - the standard error from the command, strip()'d 60 | """ 61 | if plugin_name is None: 62 | return None 63 | monresult = MonitorResult() 64 | cmd = os.path.join(self.plugin_dir, plugin_name) 65 | if not os.path.exists(cmd): 66 | monresult.error = "No plugin named %s found." % plugin_name 67 | monresult.timestamp = datetime.datetime.now() 68 | monresult.returncode = 3 69 | return monresult 70 | if not(list_of_args is None): 71 | for arg in list_of_args: 72 | cmd += " %s" % arg 73 | monresult.command = cmd.strip() 74 | if sys.platform == 'win32': 75 | close_fds = False 76 | else: 77 | close_fds = True 78 | # 79 | process = subprocess.Popen(cmd, shell=True, close_fds=close_fds, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 80 | (stdoutput, stderror) = process.communicate() 81 | monresult.timestamp = datetime.datetime.now() 82 | monresult.returncode = process.returncode 83 | if (stdoutput): 84 | cleaned_out = stdoutput.strip() 85 | monresult.output = cleaned_out 86 | monresult.decoded = MonitorResult.parse_nagios_output(cleaned_out) 87 | if (stderror): 88 | cleaned_err = stderror.strip() 89 | monresult.error = cleaned_err 90 | return monresult 91 | 92 | def run_plugin(self, plugin_name, argset=None): 93 | """run_plugin is the primary means of invoking a Nagios plugin. It takes a plugin_name, such 94 | as 'check_ping' and an optional ArgSet object, which contains the arguments to run the plugin 95 | on the command line. 96 | 97 | Example results: 98 | >>> xyz = NagiosPoller() 99 | >>> ping_argset = ArgSet() 100 | >>> ping_argset.add_argument_pair("-H", "localhost") 101 | >>> ping_argset.add_argument_pair("-w", "1,99%") 102 | >>> ping_argset.add_argument_pair("-c", "1,99%") 103 | >>> monitor_result = xyz.run_plugin('check_ping', ping_argset) 104 | >>> print monitor_result.command 105 | /opt/local/libexec/nagios/check_ping -H localhost -w 1,99% -c 1,99% 106 | >>> print monitor_result.output 107 | PING OK - Packet loss = 0%, RTA = 0.14 ms|rta=0.137000ms;1.000000;1.000000;0.000000 pl=0%;99;99;0 108 | >>> print monitor_result.error 109 | 110 | >>> print monitor_result.returncode 111 | 0 112 | >>> abc = NagiosPoller() 113 | >>> http_argset = ArgSet() 114 | >>> http_argset.add_argument_pair("-H", "www.google.com") 115 | >>> http_argset.add_argument_pair("-p", "80") 116 | >>> mon_result = abc.run_plugin('check_http', http_argset) 117 | >>> print monitor_result.command 118 | Traceback (most recent call last): 119 | File "", line 1, in 120 | NameError: name 'monitor_result' is not defined 121 | >>> print mon_result.command 122 | /opt/local/libexec/nagios/check_http -H www.google.com -p 80 123 | >>> print mon_result.output 124 | HTTP OK: HTTP/1.1 200 OK - 9047 bytes in 0.289 second response time |time=0.288865s;;;0.000000 size=9047B;;;0 125 | >>> print mon_result.error 126 | 127 | >>> print mon_result.returncode 128 | 0 129 | """ 130 | if argset is None: 131 | monitor_result = self._invoke(plugin_name) # returns a MonitorResult object 132 | else: 133 | monitor_result = self._invoke(plugin_name, argset.list_of_arguments()) # returns a MonitorResult object 134 | return monitor_result 135 | 136 | # if __name__ == '__main__': 137 | # import pprint 138 | # xyz = NagiosPoller() 139 | # ping_argset = ArgSet() 140 | # ping_argset.add_argument_pair("-H", "localhost") 141 | # ping_argset.add_argument_pair("-w", "1,99%") 142 | # ping_argset.add_argument_pair("-c", "1,99%") 143 | # ping_result = xyz.run_plugin('check_ping', ping_argset) 144 | # print ping_result.json() 145 | # 146 | # abc = NagiosPoller() 147 | # http_argset = ArgSet() 148 | # http_argset.add_argument_pair("-H", "www.google.com") 149 | # http_argset.add_argument_pair("-p", "80") 150 | # http_result = abc.run_plugin('check_http', http_argset) 151 | # print http_result.json() 152 | -------------------------------------------------------------------------------- /kitsune/renderers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Defines the base Kitsune job renderer. 8 | All custom Kitsune job renderers must extend KitsuneJobRenderer. 9 | ''' 10 | 11 | __author__ = "Raul Garreta (raul@tryolabs.com)" 12 | 13 | 14 | from django.template.loader import render_to_string 15 | 16 | from kitsune.base import STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN 17 | 18 | 19 | 20 | class KitsuneJobRenderer(): 21 | 22 | def get_html_status(self, log): 23 | return render_to_string('kitsune/status_code.html', dictionary={'status_code':int(log.stderr)}) 24 | 25 | def get_html_message(self, log): 26 | result = log.stdout 27 | if len(result) > 40: 28 | result = result[:40] + '...' 29 | return result -------------------------------------------------------------------------------- /kitsune/scripts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 5, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Test scripts. 8 | 9 | ''' 10 | 11 | __author__ = "Raul Garreta (raul@tryolabs.com)" 12 | 13 | 14 | from nagios import NagiosPoller 15 | from monitor import ArgSet 16 | 17 | 18 | def check_http(): 19 | poller = NagiosPoller() 20 | args = ArgSet() 21 | args.add_argument_pair("-H", "liukang.tryolabs.com") 22 | args.add_argument_pair("-p", "80") 23 | res = poller.run_plugin('check_http', args) 24 | print "\n",res.command,"\nRET CODE:\t",res.returncode,"\nOUT:\t\t",res.output,"\nERR:\t\t",res.error 25 | 26 | 27 | def check_ping(): 28 | poller = NagiosPoller() 29 | args = ArgSet() 30 | args.add_argument_pair("-H", "google.com") 31 | args.add_argument_pair("-w", "200.0,20%") 32 | args.add_argument_pair("-c", "500.0,60%") 33 | res = poller.run_plugin('check_ping', args) 34 | print "\n",res.command,"\nRET CODE:\t",res.returncode,"\nOUT:\t\t",res.output,"\nERR:\t\t",res.error 35 | 36 | 37 | def check_disk(): 38 | poller = NagiosPoller() 39 | args = ArgSet() 40 | args.add_argument_pair("-u", "GB") 41 | args.add_argument_pair("-w", "5") 42 | args.add_argument_pair("-c", "2") 43 | args.add_argument_pair("-p", "/") 44 | res = poller.run_plugin('check_disk', args) 45 | print "\n",res.command,"\nRET CODE:\t",res.returncode,"\nOUT:\t\t",res.output,"\nERR:\t\t",res.error 46 | 47 | 48 | def check_pgsql(): 49 | poller = NagiosPoller() 50 | args = ArgSet() 51 | args.add_argument_pair("-H", "localhost") 52 | args.add_argument_pair("-d", "daywatch_db") 53 | args.add_argument_pair("-p", "postgres") 54 | res = poller.run_plugin('check_pgsql', args) 55 | print "\n",res.command,"\nRET CODE:\t",res.returncode,"\nOUT:\t\t",res.output,"\nERR:\t\t",res.error -------------------------------------------------------------------------------- /kitsune/templates/admin/kitsune/job/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n kitsune_tags %} 3 | {% block object-tools %} 4 | {% if change %}{% if not is_popup %} 5 | 15 | {% endif %}{% endif %} 16 | {% endblock %} -------------------------------------------------------------------------------- /kitsune/templates/admin/kitsune/job/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% load admin_list i18n %} 4 | 5 | {% block object-tools %} 6 | {% if has_add_permission %} 7 | 15 | {% endif %} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /kitsune/templates/admin/kitsune/log/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_modify %} 3 | 4 | {% block content %}
    5 | {% block object-tools %} 6 | {% if change %}{% if not is_popup %} 7 | 10 | {% endif %}{% endif %} 11 | {% endblock %} 12 |
    {% block form_top %}{% endblock %} 13 |
    14 | {% if is_popup %}{% endif %} 15 | {% if save_on_top %}{% submit_row %}{% endif %} 16 | {% if errors %} 17 |

    18 | {% blocktrans count errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 19 |

    20 |
      {% for error in adminform.form.non_field_errors %}
    • {{ error }}
    • {% endfor %}
    21 | {% endif %} 22 | 23 | {% for fieldset in adminform %} 24 | {% include "admin/includes/fieldset.html" %} 25 | {% endfor %} 26 | 27 | {% block after_field_sets %}{% endblock %} 28 | 29 |
    30 | {% if has_delete_permission %}{% endif %} 31 |
    32 | 33 | 34 | {# JavaScript for prepopulated fields #} 35 | {% prepopulated_fields_js %} 36 | 37 |
    38 |
    39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /kitsune/templates/kitsune/error_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% trans "The job failed to run. The exception was " %}: 3 | 4 | {{ exception|safe }} 5 | 6 | {% for line in traceback %} 7 | {{ line|safe }} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /kitsune/templates/kitsune/mail_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block htmlhead %} 5 | 6 | 7 | {% block title %}Base{% endblock title %} 8 | 9 | {% endblock htmlhead %} 10 | 11 | 12 | {% block content %} 13 | 14 | {% endblock content %} 15 | 16 | -------------------------------------------------------------------------------- /kitsune/templates/kitsune/mail_notification.html: -------------------------------------------------------------------------------- 1 | {% extends "kitsune/mail_base.html" %} 2 | 3 | {% block title %} 4 | Kitsune Monitoring Notification 5 | {% endblock title %} 6 | 7 | {% block content %} 8 | 9 | You have received a monitoring notification:

    10 | 11 | Check:
    12 | {{log.job.name}}
    13 | Host:
    14 | {{log.job.host.name}}
    15 | Date:
    16 | {{log.run_date}}
    17 | Status Code:
    18 | {{log.stderr}}
    19 | Status Message:
    20 | {{log.stdout}}
    21 | 22 | {% endblock content %} -------------------------------------------------------------------------------- /kitsune/templates/kitsune/nagios_status_message.html: -------------------------------------------------------------------------------- 1 | NAGIOS_OUT: {{output}}
    2 | NAGIOS_ERR: {{error}} -------------------------------------------------------------------------------- /kitsune/templates/kitsune/status_code.html: -------------------------------------------------------------------------------- 1 | {% if status_code == 0 %} 2 | False 3 | {% endif %} 4 | {% if status_code == 1 %} 5 | False 6 | {% endif %} 7 | {% if status_code == 2 %} 8 | False 9 | {% endif %} 10 | {% if status_code == 3 %} 11 | False 12 | {% endif %} -------------------------------------------------------------------------------- /kitsune/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryolabs/django-kitsune/54365baf54c799e86bc771fab7aaca0f84dbdbd3/kitsune/templatetags/__init__.py -------------------------------------------------------------------------------- /kitsune/templatetags/kitsune_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.core.urlresolvers import reverse, NoReverseMatch 3 | 4 | register = template.Library() 5 | 6 | class RunJobURLNode(template.Node): 7 | def __init__(self, object_id): 8 | self.object_id = template.Variable(object_id) 9 | 10 | def render(self, context): 11 | object_id = self.object_id.resolve(context) 12 | #print object_id 13 | try: 14 | # Old way 15 | url = reverse('kitsune_job_run', args=(object_id,)) 16 | except NoReverseMatch: 17 | # New way 18 | url = reverse('admin:kitsune_job_run', args=(object_id,)) 19 | return url 20 | 21 | def do_get_run_job_url(parser, token): 22 | """ 23 | Returns the URL to the view that does the 'run_job' command. 24 | 25 | Usage:: 26 | 27 | {% get_run_job_url [object_id] %} 28 | """ 29 | try: 30 | # Splitting by None == splitting by spaces. 31 | tag_name, object_id = token.contents.split(None, 1) 32 | except ValueError: 33 | raise template.TemplateSyntaxError, "%r tag requires one argument" % token.contents.split()[0] 34 | return RunJobURLNode(object_id) 35 | 36 | register.tag('get_run_job_url', do_get_run_job_url) -------------------------------------------------------------------------------- /kitsune/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Based on django-chronograph. 8 | 9 | ''' 10 | 11 | __author__ = "Raul Garreta (raul@tryolabs.com)" 12 | 13 | 14 | import os 15 | 16 | import django 17 | from django.conf import settings 18 | from django.utils.importlib import import_module 19 | 20 | 21 | def get_manage_py(): 22 | module = import_module(settings.SETTINGS_MODULE) 23 | if django.get_version().startswith('1.3'): 24 | # This is dirty, but worked in django <= 1.3 ... 25 | from django.core.management import setup_environ 26 | return os.path.join( 27 | setup_environ(module, settings.SETTINGS_MODULE), 'manage.py' 28 | ) 29 | else: 30 | # Dirty again, but this should work in django > 1.3 31 | # We should DEFINITELY do this in an elegant way ... 32 | settings_path = os.path.dirname(module.__file__) 33 | return os.path.join(settings_path, '..', 'manage.py') 34 | -------------------------------------------------------------------------------- /kitsune/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | ''' 3 | Created on Mar 3, 2012 4 | 5 | @author: Raul Garreta (raul@tryolabs.com) 6 | 7 | Based on django-chronograph. 8 | 9 | ''' 10 | 11 | __author__ = "Raul Garreta (raul@tryolabs.com)" 12 | 13 | 14 | from django.contrib import admin 15 | from django.contrib.auth.decorators import user_passes_test 16 | from admin import JobAdmin 17 | from models import Job 18 | 19 | def job_run(request, pk): 20 | return JobAdmin(Job, admin.site).run_job_view(request, pk) 21 | job_run = user_passes_test(lambda user: user.is_superuser)(job_run) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Mar 31, 2012 3 | 4 | @author: raul 5 | ''' 6 | 7 | from setuptools import setup, find_packages 8 | 9 | setup( 10 | name='django-kitsune', 11 | version=__import__('kitsune').get_version(limit=3), 12 | description='A Django Admin app to perform host server monitoring.', 13 | author='Raul Garreta - Tryolabs', 14 | author_email='raul@tryolabs.com', 15 | url='https://github.com/tryolabs/django-kitsune', 16 | packages=find_packages(), 17 | include_package_data=True, 18 | install_requires=[ 19 | 'python-dateutil', 20 | ], 21 | zip_safe=False 22 | ) 23 | --------------------------------------------------------------------------------