├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── AUTHORS ├── COPYING ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── examples └── state │ ├── __init__.py │ ├── app │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── templates │ │ └── test.html │ ├── views.py │ └── workflow.py │ ├── manage.py │ ├── settings.py │ └── urls.py ├── kobo ├── __init__.py ├── admin │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ ├── cmd_start_cli.py │ │ ├── cmd_start_cli_command.py │ │ ├── cmd_start_client.py │ │ ├── cmd_start_client_command.py │ │ ├── cmd_start_hub.py │ │ ├── cmd_start_worker.py │ │ └── cmd_start_worker_task.py │ ├── kobo-admin │ └── templates │ │ ├── cli │ │ ├── __init__.py.template │ │ ├── __project_name__ │ │ └── commands │ │ │ └── __init__.py.template │ │ ├── cli@cmd___project_name__.py.template │ │ ├── client │ │ ├── __init__.py.template │ │ ├── __project_name__ │ │ ├── __project_name__.conf │ │ └── commands │ │ │ └── __init__.py.template │ │ ├── client@cmd___project_name__.py.template │ │ ├── hub │ │ ├── __init__.py.template │ │ ├── __project_name__-httpd.conf │ │ ├── __project_name__.wsgi │ │ ├── manage.py.template │ │ ├── menu.py.template │ │ ├── settings.py.template │ │ ├── settings_local.py.template │ │ ├── templates │ │ │ ├── base.html │ │ │ └── index.html │ │ ├── urls.py.template │ │ └── xmlrpc │ │ │ ├── __init__.py.template │ │ │ └── urls.py.template │ │ ├── task___project_name__.py.template │ │ └── worker │ │ ├── __init__.py.template │ │ ├── __project_name__ │ │ ├── __project_name__.conf │ │ └── tasks │ │ └── __init__.py.template ├── cli.py ├── client │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ ├── cmd_add_user.py │ │ ├── cmd_cancel_tasks.py │ │ ├── cmd_create_task.py │ │ ├── cmd_create_worker.py │ │ ├── cmd_disable_worker.py │ │ ├── cmd_enable_worker.py │ │ ├── cmd_list_tasks.py │ │ ├── cmd_list_workers.py │ │ ├── cmd_resubmit_tasks.py │ │ ├── cmd_shutdown_worker.py │ │ ├── cmd_watch_log.py │ │ ├── cmd_watch_tasks.py │ │ └── cmd_worker_info.py │ ├── constants.py │ ├── default.conf │ ├── main.py │ └── task_watcher.py ├── conf.py ├── decorators.py ├── django │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── krb5.py │ │ ├── middleware.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20220203_1511.py │ │ │ ├── 0003_alter_user_username.py │ │ │ └── __init__.py │ │ └── models.py │ ├── compat.py │ ├── django_version.py │ ├── fields.py │ ├── forms.py │ ├── helpers.py │ ├── menu │ │ ├── __init__.py │ │ ├── context_processors.py │ │ └── middleware.py │ ├── upload │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alter_fileupload_size.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── views.py │ │ └── xmlrpc.py │ ├── views │ │ ├── __init__.py │ │ └── generic.py │ └── xmlrpc │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── auth.py │ │ ├── decorators.py │ │ ├── dispatcher.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ │ ├── models.py │ │ └── views.py ├── exceptions.py ├── hardlink.py ├── http.py ├── hub │ ├── __init__.py │ ├── admin.py │ ├── decorators.py │ ├── fixtures │ │ └── data.json │ ├── forms.py │ ├── menu.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20150722_0612.py │ │ ├── 0003_auto_20160202_0647.py │ │ ├── 0004_alter_task_worker.py │ │ ├── 0005_add_task_canceller.py │ │ └── __init__.py │ ├── models.py │ ├── sql │ │ └── task.postgresql.sql │ ├── static │ │ └── kobo │ │ │ ├── css │ │ │ └── screen.css │ │ │ ├── img │ │ │ ├── list-first-disabled.png │ │ │ ├── list-first.png │ │ │ ├── list-last-disabled.png │ │ │ ├── list-last.png │ │ │ ├── list-next-disabled.png │ │ │ ├── list-next.png │ │ │ ├── list-prev-disabled.png │ │ │ └── list-prev.png │ │ │ └── js │ │ │ └── log_watcher.js │ ├── templates │ │ ├── 404.html │ │ ├── 500.html │ │ ├── arch │ │ │ ├── detail.html │ │ │ ├── list.html │ │ │ └── list_include.html │ │ ├── auth │ │ │ └── login.html │ │ ├── base.html.example │ │ ├── channel │ │ │ ├── detail.html │ │ │ ├── list.html │ │ │ └── list_include.html │ │ ├── layout.html │ │ ├── pagination.html │ │ ├── task │ │ │ ├── detail.html │ │ │ ├── list.html │ │ │ ├── list_include.html │ │ │ └── log.html │ │ ├── user │ │ │ ├── detail.html │ │ │ ├── list.html │ │ │ └── list_include.html │ │ └── worker │ │ │ ├── detail.html │ │ │ ├── list.html │ │ │ └── list_include.html │ ├── urls │ │ ├── __init__.py │ │ ├── arch.py │ │ ├── auth.py │ │ ├── channel.py │ │ ├── task.py │ │ ├── user.py │ │ └── worker.py │ ├── views.py │ └── xmlrpc │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── auth.py │ │ ├── client.py │ │ ├── system.py │ │ └── worker.py ├── log.py ├── notification.py ├── pkgset.py ├── plugins.py ├── process.py ├── rpmlib.py ├── shortcuts.py ├── tback.py ├── threads.py ├── types.py ├── worker │ ├── __init__.py │ ├── default.conf │ ├── logger.py │ ├── main.py │ ├── task.py │ ├── taskmanager.py │ └── tasks │ │ ├── __init__.py │ │ └── task_shutdown_worker.py └── xmlrpc.py ├── pytest.ini ├── setup-sdist-wrapper.sh ├── setup.py ├── test-requirements.txt ├── tests ├── __init__.py ├── chunks_file ├── data │ ├── dummy-AdobeReader_enu-9.5.1-1.nosrc.rpm │ ├── dummy-basesystem-10.0-6.noarch.rpm │ └── dummy-basesystem-10.0-6.src.rpm ├── fields_test │ ├── __init__.py │ ├── fixtures │ │ └── initial_data.json │ ├── models.py │ ├── settings.py │ └── tests.py ├── hub_urls.py ├── plugins │ ├── __init__.py │ ├── plug_broken.py │ └── plug_working.py ├── rpc.py ├── settings.py ├── test_conf.py ├── test_decorators.py ├── test_fields.py ├── test_forms.py ├── test_hardlink.py ├── test_http.py ├── test_hubproxy.py ├── test_log.py ├── test_logger.py ├── test_main.py ├── test_middleware.py ├── test_models.py ├── test_pkgset.py ├── test_plugins.py ├── test_profile.py ├── test_rpmlib.py ├── test_shortcuts.py ├── test_tail.py ├── test_task_logs.py ├── test_task_shutdown_worker.py ├── test_taskbase.py ├── test_taskmanager.py ├── test_tback.py ├── test_types.py ├── test_utf8_chunk.py ├── test_view_log.py ├── test_views.py ├── test_widgets.py ├── test_xmlrpc_auth.py ├── test_xmlrpc_client.py ├── test_xmlrpc_system.py ├── test_xmlrpc_worker.py └── utils.py ├── tools ├── db_update-0.2.0-0.3.0 └── reset_sequences.py └── tox.ini /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: Run Tox tests 10 | 11 | jobs: 12 | tox_test: 13 | name: Tox test 14 | steps: 15 | - name: Checkout kobo 16 | uses: actions/checkout@v2 17 | - name: Run Tox tests 18 | id: test 19 | uses: fedora-python/tox-github-action@main 20 | with: 21 | tox_env: ${{ matrix.tox_env }} 22 | dnf_install: python3-rpm /usr/bin/gcc /usr/bin/krb5-config /etc/mime.types 23 | strategy: 24 | matrix: 25 | tox_env: [ 26 | # This list has to be maintained manually :( 27 | # You can get it from `tox -l | sed "s/$/,/"` 28 | py36-django2, 29 | py36-django3, 30 | py38-django2, 31 | py38-django3, 32 | py39-django2, 33 | py39-django3, 34 | py310-django2, 35 | py310-django3, 36 | py311-django2, 37 | py311-django3, 38 | py38-django4, 39 | py39-django4, 40 | py310-django4, 41 | py311-django4, 42 | py312-django4, 43 | py310-django5, 44 | py311-django5, 45 | py312-django5, 46 | py39-bandit, 47 | ] 48 | 49 | # Use GitHub's Linux Docker host 50 | runs-on: ubuntu-latest 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release on PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - kobo* 7 | 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install setuptools wheel twine 22 | - name: Build and publish 23 | env: 24 | TWINE_USERNAME: __token__ 25 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 26 | run: | 27 | python setup.py sdist bdist_wheel 28 | twine upload dist/* 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | build/* 3 | dist/* 4 | MANIFEST 5 | .spyderproject 6 | *.un~ 7 | .tox/ 8 | **/.pytest_cache/ 9 | /testdatabase 10 | *egg-info/ 11 | .idea/ -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Kobo was written by: 2 | Daniel Mach 3 | 4 | 5 | Patches and contributions by (alphabetically): 6 | Jan Blažek 7 | Martin Bukatovič 8 | Dennis Gregorovic 9 | Tomáš Kopeček 10 | Tomáš Tomeček 11 | Martin Mágr 12 | Nikolay Petrov 13 | 14 | 15 | Additional credits go to: 16 | Michael McLean, Michael Bonnet and Jesse Keating (for koji and bunch of scripts they wrote) 17 | Luděk Šmíd (for mentorship and useful advices) 18 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Kobo is set of python modules designed for rapid tools development. 2 | Copyright (C) 2009 Red Hat, Inc. 3 | 4 | This library is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU Lesser General Public 6 | License as published by the Free Software Foundation; 7 | version 2.1 of the License. 8 | 9 | This library is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | Lesser General Public License for more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public 15 | License along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | To install Kobo, make sure you have Python 2.4 or greater installed. 2 | Then run this command from the command prompt: 3 | 4 | python setup.py install 5 | 6 | 7 | AS AN ALTERNATIVE, you can just copy the entire "kobo" directory to Python's 8 | site-packages directory, which is located wherever your Python installation 9 | lives. Some places you might check are: 10 | 11 | /usr/lib/python2.4/site-packages (Unix, Python 2.4) 12 | /usr/lib/python2.5/site-packages (Unix, Python 2.5) 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include INSTALL 3 | include COPYING 4 | include LICENSE 5 | include MANIFEST.in 6 | include README.md 7 | include Makefile 8 | recursive-include kobo *.py *.conf *.json *.sql *.html *.css *.js *.png *.wsgi *.example *.template kobo-admin __*__ 9 | recursive-include scripts *.py *.sh 10 | recursive-include tests * 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: help 2 | 3 | 4 | help: 5 | @echo "Usage: make " 6 | @echo 7 | @echo "Available targets are:" 8 | @echo " help show this text" 9 | @echo " clean remove python bytecode and temp files" 10 | @echo " install install program on current system" 11 | @echo " log prepare changelog for spec file" 12 | @echo " source create source tarball" 13 | @echo " test run tests/run_tests.py" 14 | 15 | 16 | clean: 17 | @python setup.py clean 18 | rm -f MANIFEST 19 | find . -\( -name "*.pyc" -o -name '*.pyo' -o -name "*~" -\) -delete 20 | 21 | 22 | install: 23 | @python setup.py install 24 | 25 | 26 | log: 27 | @(LC_ALL=C date +"* %a %b %e %Y `git config --get user.name` <`git config --get user.email`> - VERSION"; git log --pretty="format:- %s (%an)" | cat) | less 28 | 29 | 30 | source: test clean 31 | @./setup-sdist-wrapper.sh 32 | 33 | 34 | test: 35 | py.test 36 | -------------------------------------------------------------------------------- /examples/state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/examples/state/__init__.py -------------------------------------------------------------------------------- /examples/state/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/examples/state/app/__init__.py -------------------------------------------------------------------------------- /examples/state/app/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import django.contrib.admin as admin 3 | from .models import SimpleState 4 | 5 | 6 | class SimpleStateAdmin(admin.ModelAdmin): 7 | list_display = ('__str__', '__unicode__', 'id', 'comment') 8 | 9 | admin.site.register(SimpleState, SimpleStateAdmin) 10 | -------------------------------------------------------------------------------- /examples/state/app/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from kobo.django.fields import StateEnumField 3 | from django.db import models 4 | from .workflow import workflow 5 | import six 6 | 7 | 8 | @six.python_2_unicode_compatible 9 | class SimpleState(models.Model): 10 | state = StateEnumField(workflow, default="NEW", null=False) 11 | comment = models.TextField(null=True, blank=True) 12 | 13 | def save(self, *args, **kwargs): 14 | super(SimpleState, self).save(*args, **kwargs) 15 | if self.state._to: 16 | self.state.change_state(None, commit=True) 17 | 18 | def __str__(self): 19 | return six.text_type(self.state._current_state) 20 | -------------------------------------------------------------------------------- /examples/state/app/templates/test.html: -------------------------------------------------------------------------------- 1 |
2 | {{ form }}
3 | 4 |
5 | -------------------------------------------------------------------------------- /examples/state/app/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from kobo.django.fields import * 3 | from django.shortcuts import render_to_response 4 | from django.template import RequestContext 5 | from django.http import HttpResponseRedirect 6 | from django.forms import ModelForm 7 | from .models import SimpleState 8 | 9 | class ModifyForm(ModelForm): 10 | class Meta: 11 | model = SimpleState 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(ModifyForm, self).__init__(*args, **kwargs) 15 | self.fields['state'].choices = self.instance.state.get_next_states_mapping() # user=xxx 16 | 17 | def save(self, *args, **kwargs): 18 | commit = kwargs.get('commit', True) # default django commit is True 19 | self.instance.state.change_state(self.cleaned_data["state"], commit=commit) 20 | return super(ModifyForm, self).save(*args, **kwargs) 21 | 22 | def form_view(request, id=None): 23 | if request.POST: 24 | if id: 25 | form = ModifyForm(request.POST, instance=SimpleState.objects.get(id=id)) 26 | else: 27 | form = ModifyForm(request.POST) 28 | 29 | if form.is_valid(): 30 | form.save() 31 | return HttpResponseRedirect('.') 32 | else: 33 | if id: 34 | form = ModifyForm(instance=SimpleState.objects.get(id=id)) 35 | else: 36 | form = ModifyForm() 37 | 38 | return render(request, 'test.html', {'form': form}) 39 | -------------------------------------------------------------------------------- /examples/state/app/workflow.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from kobo.types import State, StateEnum 3 | 4 | def perm_new(*args, **kwargs): 5 | return True 6 | 7 | def perm_on_review(**kwargs): 8 | user = kwargs.get('user') 9 | return user.has_perm('pkg.verify') 10 | 11 | def perm_verified(**kwargs): 12 | user = kwargs.get('user') 13 | next = kwargs.get('new_state', None) 14 | if next == 'REJECTED' or next == 'ACCEPTED': 15 | return user.has_perm('pkg.accept') 16 | elif next == 'ON_REVIEW': 17 | return user.has_perm('pkg.revert') 18 | else: 19 | raise ValueError('Wrong transition') 20 | 21 | def perm_change_reviewer(user, state = None): 22 | return user.has_perm('pkg.change_reviewer') 23 | 24 | def perm_edit_rest(user): 25 | return user.groups.filter(name = 'Reviewers').count() != 0 or user.is_superuser 26 | 27 | workflow = StateEnum( 28 | State( 29 | name = "NEW", 30 | next_states = ["ON_REVIEW"], 31 | check_perms = [perm_new], 32 | ), 33 | State( 34 | name = "ON_REVIEW", 35 | next_states = ["VERIFIED"], 36 | # check_perms = [perm_on_review], 37 | ), 38 | State( 39 | name = "VERIFIED", 40 | next_states = ["REJECTED", "ACCEPTED", "ON_REVIEW"], 41 | # check_perms = [perm_verified], 42 | ), 43 | State( 44 | name = "REJECTED", 45 | next_states = None, 46 | ), 47 | State( 48 | name = "ACCEPTED", 49 | next_states = None, 50 | ), 51 | ) 52 | -------------------------------------------------------------------------------- /examples/state/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | from django.core.management import execute_manager 4 | try: 5 | from . import settings # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 9 | sys.exit(1) 10 | 11 | if __name__ == "__main__": 12 | execute_manager(settings) 13 | -------------------------------------------------------------------------------- /examples/state/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | (r'^admin/', admin.site.urls), 8 | (r"(?P\d+)", "state.app.views.form_view"), 9 | (r"", "state.app.views.form_view"), 10 | ) 11 | -------------------------------------------------------------------------------- /kobo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/__init__.py -------------------------------------------------------------------------------- /kobo/admin/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/commands/__init__.py -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | import os 6 | 7 | import kobo.cli 8 | import kobo.admin 9 | 10 | 11 | class Start_CLI(kobo.cli.Command): 12 | """create a CLI project directory structure in the current directory""" 13 | enabled = True 14 | 15 | def options(self): 16 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 17 | 18 | def run(self, *args, **kwargs): 19 | if len(args) < 1: 20 | self.parser.error("Please specify a name of the project.") 21 | 22 | name = args[0] 23 | directory = os.getcwd() 24 | 25 | try: 26 | kobo.admin.copy_helper(name, directory, "cli") 27 | except kobo.admin.TemplateError as ex: 28 | self.parser.error(ex) 29 | 30 | print("Use `kobo-admin start-cli-command` to add additional commands.") 31 | -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_cli_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | import kobo.cli 7 | import kobo.admin 8 | 9 | 10 | class Start_CLI_Command(kobo.cli.Command): 11 | """create a CLI command module in the current directory""" 12 | enabled = True 13 | 14 | def options(self): 15 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 16 | self.parser.add_option("-d", "--dir", help="target directory") 17 | 18 | def run(self, *args, **kwargs): 19 | if len(args) < 1: 20 | self.parser.error("Please specify a name of the command.") 21 | 22 | name = args[0] 23 | directory = kwargs.pop("dir") 24 | if not directory: 25 | directory = os.getcwd() 26 | 27 | try: 28 | kobo.admin.copy_helper(name, directory, "cli@cmd___project_name__.py.template") 29 | except kobo.admin.TemplateError as ex: 30 | self.parser.error(ex) 31 | -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | import os 6 | 7 | import kobo.cli 8 | import kobo.admin 9 | 10 | 11 | class Start_Client(kobo.cli.Command): 12 | """create a hub client project directory structure in the current directory""" 13 | enabled = True 14 | 15 | def options(self): 16 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 17 | 18 | def run(self, *args, **kwargs): 19 | if len(args) < 1: 20 | self.parser.error("Please specify a name of the project.") 21 | 22 | name = args[0] 23 | directory = os.getcwd() 24 | 25 | try: 26 | kobo.admin.copy_helper(name, directory, "client") 27 | except kobo.admin.TemplateError as ex: 28 | self.parser.error(ex) 29 | 30 | print("Edit config file to finish setup.") 31 | print("Use `kobo-admin start-client-command` to add additional commands.") 32 | -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_client_command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | import kobo.cli 7 | import kobo.admin 8 | 9 | 10 | class Start_Client_Command(kobo.cli.Command): 11 | """create a hub client command module in the current directory""" 12 | enabled = True 13 | 14 | def options(self): 15 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 16 | self.parser.add_option("-d", "--dir", help="target directory") 17 | 18 | def run(self, *args, **kwargs): 19 | if len(args) < 1: 20 | self.parser.error("Please specify a name of the command.") 21 | 22 | name = args[0] 23 | directory = kwargs.pop("dir") 24 | if not directory: 25 | directory = os.getcwd() 26 | 27 | try: 28 | kobo.admin.copy_helper(name, directory, "client@cmd___project_name__.py.template") 29 | except kobo.admin.TemplateError as ex: 30 | self.parser.error(ex) 31 | -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_hub.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | import re 6 | 7 | import kobo.cli 8 | import kobo.admin 9 | import kobo.shortcuts 10 | 11 | 12 | class Start_Hub(kobo.cli.Command): 13 | """create a hub project directory structure in the current directory""" 14 | enabled = True 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 18 | 19 | def run(self, *args, **kwargs): 20 | if len(args) < 1: 21 | self.parser.error("Please specify a name of the project.") 22 | 23 | name = args[0].replace("-", "_") 24 | directory = os.getcwd() 25 | 26 | try: 27 | kobo.admin.copy_helper(name, directory, "hub") 28 | except kobo.admin.TemplateError as ex: 29 | self.parser.error(ex) 30 | 31 | # code from django/core/management/commands/startproject.py 32 | # Create a random SECRET_KEY hash, and put it in the main settings. 33 | main_settings_file = os.path.join(directory, name, 'settings.py') 34 | settings_contents = open(main_settings_file, 'r').read() 35 | fp = open(main_settings_file, 'w') 36 | django_alphabet = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)" 37 | secret_key = kobo.shortcuts.random_string(50, alphabet=django_alphabet) 38 | settings_contents = re.sub(r"(?<=SECRET_KEY = ')'", secret_key + "'", settings_contents) 39 | fp.write(settings_contents) 40 | fp.close() 41 | -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | import kobo.cli 7 | import kobo.admin 8 | 9 | 10 | class Start_Worker(kobo.cli.Command): 11 | """create a worker directory structure in the current directory""" 12 | enabled = True 13 | 14 | def options(self): 15 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 16 | 17 | def run(self, *args, **kwargs): 18 | if len(args) < 1: 19 | self.parser.error("Please specify a name of the project.") 20 | 21 | name = args[0] 22 | directory = os.getcwd() 23 | 24 | try: 25 | kobo.admin.copy_helper(name, directory, "worker") 26 | except kobo.admin.TemplateError as ex: 27 | self.parser.error(ex) 28 | -------------------------------------------------------------------------------- /kobo/admin/commands/cmd_start_worker_task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | import kobo.cli 7 | import kobo.admin 8 | 9 | 10 | class Start_Worker_Task(kobo.cli.Command): 11 | """create a worker task module in the current directory""" 12 | enabled = True 13 | 14 | def options(self): 15 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 16 | self.parser.add_option("-d", "--dir", help="target directory") 17 | 18 | def run(self, *args, **kwargs): 19 | if len(args) < 1: 20 | self.parser.error("Please specify a name of the task.") 21 | 22 | name = args[0] 23 | directory = kwargs.pop("dir") 24 | if not directory: 25 | directory = os.getcwd() 26 | 27 | try: 28 | kobo.admin.copy_helper(name, directory, "task___project_name__.py.template") 29 | except kobo.admin.TemplateError as ex: 30 | self.parser.error(ex) 31 | -------------------------------------------------------------------------------- /kobo/admin/kobo-admin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import sys 6 | import kobo.admin 7 | 8 | 9 | if __name__ == "__main__": 10 | sys.exit(kobo.admin.main()) 11 | -------------------------------------------------------------------------------- /kobo/admin/templates/cli/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/cli/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/cli/__project_name__: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import sys 6 | import os 7 | 8 | import kobo.exceptions 9 | import kobo.cli 10 | 11 | # assuming all commands are in {{ project_name }}/commands/cmd_*.py modules 12 | import {{ project_name }}.commands 13 | 14 | 15 | # inherit container to make sure nobody will change plugins I registered 16 | class {{ project_name|camel }}CommandContainer(kobo.cli.CommandContainer): 17 | # uncomment and update following text 18 | # to make it available in help-rst output 19 | # (which can be converted to a man page) 20 | # 21 | #_description = "brief description/purpose of the program" 22 | #_copyright = "(c) Foo Bar, Inc." 23 | #_contact = "foo.bar@example.com" 24 | #_authors = [ 25 | # "Spam Eggs ", 26 | #] 27 | pass 28 | 29 | 30 | def main(args=None): 31 | # register project specific commands 32 | {{ project_name|camel }}CommandContainer.register_module({{ project_name }}.commands, prefix="cmd_") 33 | 34 | # initialize command container 35 | command_container = {{ project_name|camel }}CommandContainer() 36 | parser = kobo.cli.CommandOptionParser( 37 | command_container=command_container, # plugin container with registered commands 38 | add_username_password_options=True, # include auth options to each command 39 | ) 40 | 41 | try: 42 | parser.run(args) 43 | except kobo.exceptions.ImproperlyConfigured, ex: 44 | sys.stderr.write("\n\nError: Improperly configured: %s\n" % ex) 45 | return 3 46 | return 0 47 | 48 | 49 | if __name__ == "__main__": 50 | sys.exit(main()) 51 | -------------------------------------------------------------------------------- /kobo/admin/templates/cli/commands/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/cli/commands/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/cli@cmd___project_name__.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import kobo.cli 5 | 6 | 7 | class {{ project_name|camel_cmd }}(kobo.cli.Command): 8 | """command description""" 9 | enabled = True 10 | admin = False # admin type account required 11 | 12 | def options(self): 13 | # specify command usage 14 | # normalized name contains a lower-case class name with underscores converted to dashes 15 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 16 | 17 | # specify command options as in optparse.OptionParser 18 | """ 19 | self.parser.add_option( 20 | "--long-option", 21 | default=None, 22 | action="store", 23 | help="" 24 | ) 25 | """ 26 | 27 | def run(self, *args, **kwargs): 28 | # optparser output is passed via *args (args) and **kwargs (opts) 29 | username = kwargs.pop("username", None) 30 | password = kwargs.pop("password", None) 31 | 32 | # if not args: 33 | # self.parser.error("please specify at least one argument") 34 | 35 | # do whatever you want 36 | 37 | raise NotImplementedError # remove this line once you're finished with this command 38 | -------------------------------------------------------------------------------- /kobo/admin/templates/client/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/client/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/client/__project_name__: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import sys 6 | import os 7 | 8 | import kobo.exceptions 9 | import kobo.client 10 | import kobo.client.commands 11 | 12 | # assuming all commands are in {{ project_name }}/commands/cmd_*.py modules 13 | import {{ project_name }}.commands 14 | 15 | 16 | # inherit container to make sure nobody will change plugins I registered 17 | class {{ project_name|camel }}CommandContainer(kobo.client.ClientCommandContainer): 18 | pass 19 | 20 | 21 | def main(args=None): 22 | # register generic kobo commands 23 | {{ project_name|camel }}CommandContainer.register_module(kobo.client.commands, prefix="cmd_") 24 | # register project specific commands 25 | {{ project_name|camel }}CommandContainer.register_module({{ project_name }}.commands, prefix="cmd_") 26 | 27 | # configuration 28 | config_env = "{{ project_name|upper }}_CONFIG_FILE" 29 | config_default = "/etc/{{ project_name }}.conf" 30 | config_file = os.environ.get(config_env, config_default) 31 | conf = kobo.conf.PyConfigParser() 32 | try: 33 | conf.load_from_file(config_file) 34 | except (IOError, TypeError): 35 | sys.stderr.write("\n\nError: The config file '%s' was not found.\nCreate the config file or specify the '%s'\nenvironment variable to override config file location.\n" % (config_default, config_env)) 36 | return 2 37 | 38 | # initialize command container 39 | command_container = {{ project_name|camel }}CommandContainer(conf) 40 | parser = kobo.cli.CommandOptionParser( 41 | command_container=command_container, # plugin container with registered commands 42 | add_username_password_options=True, # include auth options to each command 43 | ) 44 | 45 | try: 46 | parser.run(args) 47 | except kobo.exceptions.ImproperlyConfigured, ex: 48 | sys.stderr.write("\n\nError: Improperly configured: %s\n" % ex) 49 | return 3 50 | return 0 51 | 52 | 53 | if __name__ == "__main__": 54 | sys.exit(main()) 55 | -------------------------------------------------------------------------------- /kobo/admin/templates/client/__project_name__.conf: -------------------------------------------------------------------------------- 1 | # client config file for {{ project_name }} 2 | 3 | # Hub XML-RPC address. 4 | HUB_URL = "http://localhost:8000/xmlrpc" 5 | 6 | # Hub authentication method. Example: krbv, gssapi, password, worker_key, oidc, token_oidc 7 | AUTH_METHOD = "" 8 | 9 | # Username and password 10 | #USERNAME = "client" 11 | #PASSWORD = "client" 12 | 13 | # A unique worker key used for authentication. 14 | # I't recommended to use krbv auth in production environment instead. 15 | #WORKER_KEY = "" 16 | 17 | # Kerberos principal. If commented, default principal obtained by kinit is used. 18 | #KRB_PRINCIPAL = "" 19 | 20 | # Kerberos keytab file. 21 | #KRB_KEYTAB = "" 22 | 23 | # Kerberos service prefix. Example: host, HTTP 24 | #KRB_SERVICE = "HTTP" 25 | 26 | # Kerberos realm. If commented, last two parts of domain name are used. Example: MYDOMAIN.COM. 27 | #KRB_REALM = "EXAMPLE.COM" 28 | 29 | # Kerberos credential cache file. 30 | #KRB_CCACHE = "" 31 | 32 | # Kerberos proxy users. 33 | #KRB_PROXY_USERS = "" 34 | 35 | # Enables XML-RPC verbose flag 36 | DEBUG_XMLRPC = 0 37 | -------------------------------------------------------------------------------- /kobo/admin/templates/client/commands/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/client/commands/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/client@cmd___project_name__.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import kobo.client 5 | 6 | 7 | class {{ project_name|camel_cmd }}(kobo.client.ClientCommand): 8 | """command description""" 9 | enabled = True 10 | admin = False # admin type account required 11 | 12 | def options(self): 13 | # specify command usage 14 | # normalized name contains a lower-case class name with underscores converted to dashes 15 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 16 | 17 | # specify command options as in optparse.OptionParser 18 | """ 19 | self.parser.add_option( 20 | "--long-option", 21 | default=None, 22 | action="store", 23 | help="" 24 | ) 25 | """ 26 | 27 | def run(self, *args, **kwargs): 28 | # optparser output is passed via *args (args) and **kwargs (opts) 29 | username = kwargs.pop("username", None) 30 | password = kwargs.pop("password", None) 31 | hub = kwargs.pop("hub", None) 32 | 33 | # if not args: 34 | # self.parser.error("please specify at least one argument") 35 | 36 | # login to the hub 37 | self.set_hub(username, password, hub) 38 | 39 | # call hub XML-RPC calls or do whatever you want 40 | #self.hub.client.some_method(kwargs) 41 | 42 | raise NotImplementedError # remove this line once you're finished with this command 43 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/hub/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/hub/__project_name__-httpd.conf: -------------------------------------------------------------------------------- 1 | # WSGI handler 2 | WSGIScriptAlias /{{ project_name }} /var/www/django/{{ project_name }}/{{ project_name }}.wsgi 3 | 4 | 5 | # kobo media 6 | Alias /{{ project_name }}/media/kobo/ "/usr/lib/python2.4/site-packages/kobo/hub/media/" 7 | 8 | Order allow,deny 9 | Options Indexes 10 | Allow from all 11 | IndexOptions FancyIndexing 12 | 13 | 14 | 15 | # project media 16 | Alias /{{ project_name }}/media/ "/var/www/django/{{ project_name }}/media/" 17 | 18 | Order allow,deny 19 | Options Indexes 20 | Allow from all 21 | IndexOptions FancyIndexing 22 | 23 | 24 | 25 | # admin media 26 | Alias /{{ project_name }}/admin/media/ "/usr/lib/python2.4/site-packages/django/contrib/admin/media/" 27 | 28 | Order allow,deny 29 | Options Indexes 30 | Allow from all 31 | IndexOptions FancyIndexing 32 | 33 | 34 | 35 | # kerberos auth 36 | # 37 | # AuthType Kerberos 38 | # AuthName "{{ project_name|camel }} Web UI" 39 | # KrbMethodNegotiate on 40 | # KrbMethodK5Passwd off 41 | # KrbServiceName HTTP 42 | # KrbAuthRealms EXAMPLE.COM 43 | # Krb5Keytab /etc/httpd/conf/httpd.keytab 44 | # KrbSaveCredentials off 45 | # Require valid-user 46 | # 47 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/__project_name__.wsgi: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # tweak PYTHONPATH if needed (usually if project is deployed outside site-packages) 9 | # sys.path.append("/var/www/django") 10 | 11 | os.environ['DJANGO_SETTINGS_MODULE'] = '{{ project_name }}.settings' 12 | import django.core.handlers.wsgi 13 | 14 | 15 | application = django.core.handlers.wsgi.WSGIHandler() 16 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/manage.py.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/menu.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.django.menu import MenuItem, include 5 | 6 | 7 | # Create your menu here. 8 | 9 | # Example: 10 | # 11 | # menu = ( 12 | # MenuItem("MenuItem-1", "/url/path/", absolute_url=True, menu=( 13 | # MenuItem("MenuItem-1.1", "/url/path/1/", absolute_url=True), 14 | # MenuItem("MenuItem-1.2", "/url/path/2/", absolute_url=True), 15 | # )), 16 | # MenuItem("MenuItem-2", "url_label", ("Developers",), ("app.change_data",)), 17 | # include("project.app.menu"), 18 | # MenuItem.include("project.another_app.menu"), 19 | # ) 20 | # 21 | # In this example is MenuItem-1 and it's submenu submenu tree accessible for anybody. 22 | # MenuItem-2 is only for users in group Developers with specific permission. 23 | # Instead of specifying complete tree in one file, you can use include() 24 | # command in similar way as it is used in urls.py (see third menu item). 25 | # include() function is also a staticmethod of MenuItem class (see fourth menu item). 26 | 27 | # Can be specified only once in project-wide menu 28 | # css_active_class = "active_menu" 29 | 30 | # Source of example: docstring in kobo/django/menu/__init__.py 31 | 32 | 33 | menu = ( 34 | MenuItem("Home", "index"), 35 | MenuItem("Tasks", "task/index", menu=( 36 | MenuItem("All", "task/index"), 37 | MenuItem("Running", "task/running"), 38 | MenuItem("Failed", "task/failed"), 39 | MenuItem("Finished", "task/finished"), 40 | )), 41 | MenuItem("Info", "worker/list", menu=( 42 | include("kobo.hub.menu"), 43 | )), 44 | ) 45 | 46 | 47 | css_active_class = "active" 48 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/settings_local.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | """ 5 | This is a config file with instance-specific settings. 6 | Make sure it doesn't get overwritten during package installation. 7 | Uncoment and use whatever is needed. 8 | """ 9 | 10 | 11 | #DEBUG = False 12 | #TEMPLATE_DEBUG = DEBUG 13 | 14 | #ADMINS = ( 15 | # # ('Your Name', 'your_email@domain.com'), 16 | #) 17 | #MANAGERS = ADMINS 18 | 19 | #DATABASES = { 20 | # 'default': { 21 | # 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 22 | # 'NAME': '', # Or path to database file if using sqlite3. 23 | # 'USER': '', # Not used with sqlite3. 24 | # 'PASSWORD': '', # Not used with sqlite3. 25 | # 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 26 | # 'PORT': '', # Set to empty string for default. Not used with sqlite3. 27 | # } 28 | #} 29 | 30 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 31 | #TIME_ZONE = 'America/New_York' 32 | #LANGUAGE_CODE = 'en-us' 33 | #USE_I18N = False 34 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% block css %} 6 | {# change href according to your project or remove following line completely #} 7 | 8 | {% endblock %} 9 | 10 | {% block header_site_name %} 11 | Default site or project name. 12 | {% endblock %} 13 | 14 | {% block login %} 15 | login | krb5login 16 | {% endblock %} 17 | 18 | {% block header_login %} 19 | {% endblock %} 20 | 21 | {% block footer %} 22 | Default footer text. 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Home page

5 | This is the default home page text. 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/urls.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from django.conf.urls.defaults import * 5 | from django.conf import settings 6 | 7 | # Uncomment the next two lines to enable the admin: 8 | from django.contrib import admin 9 | admin.autodiscover() 10 | 11 | 12 | urlpatterns = patterns('', 13 | # Example: 14 | # (r'^{{ project_name }}/', include('{{ project_name }}.foo.urls')), 15 | 16 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 17 | # to INSTALLED_APPS to enable admin documentation: 18 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 19 | 20 | #url(r"^$", '{{ project_name }}.home.views.index_redirect', name="task/list"), 21 | url(r"^$", "django.views.generic.simple.direct_to_template", kwargs={"template": "index.html"}, name="index"), 22 | url(r"^auth/", include("kobo.hub.urls.auth")), 23 | url(r"^task/", include("kobo.hub.urls.task")), 24 | url(r"^info/arch/", include("kobo.hub.urls.arch")), 25 | url(r"^info/channel/", include("kobo.hub.urls.channel")), 26 | url(r"^info/user/", include("kobo.hub.urls.user")), 27 | url(r"^info/worker/", include("kobo.hub.urls.worker")), 28 | 29 | url(r'^admin/', include(admin.site.urls)), 30 | 31 | # Include kobo hub xmlrpc module urls: 32 | url(r"^xmlrpc/", include("{{ project_name }}.xmlrpc.urls")), 33 | ) 34 | 35 | 36 | # this is a hack to enable media (with correct prefix) while debugging 37 | if settings.DEBUG: 38 | import os 39 | import kobo 40 | import six.moves.urllib.parse as urlparse 41 | 42 | scheme, netloc, path, params, query, fragment = urlparse.urlparse(settings.STATIC_URL) 43 | if not netloc: 44 | # netloc is empty -> media is not on remote server 45 | urlpatterns.extend(patterns("", 46 | url(r"^%s/kobo/(?P.*)$" % path[1:-1], "django.views.static.serve", kwargs={"document_root": os.path.join(os.path.dirname(kobo.__file__), "hub", "media")}), 47 | url(r"^%s/(?P.*)$" % path[1:-1], "django.views.static.serve", kwargs={"document_root": settings.MEDIA_ROOT}), 48 | )) 49 | -------------------------------------------------------------------------------- /kobo/admin/templates/hub/xmlrpc/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/hub/xmlrpc/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/hub/xmlrpc/urls.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import * 4 | 5 | 6 | urlpatterns = patterns("", 7 | # customize the index XML-RPC page if needed: 8 | # url(r"^$", "django.views.generic.simple.direct_to_template", kwargs={"template": "xmlrpc_help.html"}, name="help/xmlrpc"), 9 | url(r"^upload/", "kobo.django.upload.views.file_upload"), 10 | url(r"^client/", "kobo.django.xmlrpc.views.client_handler", name="help/xmlrpc/client"), 11 | url(r"^worker/", "kobo.django.xmlrpc.views.worker_handler", name="help/xmlrpc/worker"), 12 | ) 13 | -------------------------------------------------------------------------------- /kobo/admin/templates/task___project_name__.py.template: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.worker import TaskBase 5 | 6 | 7 | class {{ project_name|camel }}(TaskBase): 8 | enabled = True 9 | 10 | arches = ["noarch"] # list of supported architectures 11 | channels = ["default"] # list of channels 12 | exclusive = False # leave False here unless you really know what you're doing 13 | foreground = True # if True the task is not forked and runs in the worker process (no matter you run worker without -f) 14 | priority = 19 15 | weight = 1.0 16 | 17 | def run(self): 18 | # argument passing 19 | # num = self.args["foo"] 20 | 21 | # do something 22 | # num = num * 2 23 | 24 | # store result (will be send to db automatically) 25 | # self.result = str(num) 26 | 27 | raise NotImplementedError 28 | 29 | @classmethod 30 | def cleanup(cls, hub, conf, task_info): 31 | pass 32 | # remove temp files, etc. 33 | 34 | @classmethod 35 | def notification(cls, hub, conf, task_info): 36 | pass 37 | # hub.worker.email__notification(task_info["id"]) 38 | -------------------------------------------------------------------------------- /kobo/admin/templates/worker/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/worker/__init__.py.template -------------------------------------------------------------------------------- /kobo/admin/templates/worker/__project_name__: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python -tt 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import os 6 | import sys 7 | 8 | import kobo.exceptions 9 | import kobo.conf 10 | import kobo.worker.main 11 | import kobo.worker.tasks 12 | 13 | # assuming all tasks are in {{ project_name }}/tasks/task_*.py modules 14 | import {{ project_name }}.tasks 15 | 16 | 17 | def main(): 18 | # register generic kobo tasks 19 | kobo.worker.main.TaskContainer.register_module(kobo.worker.tasks, prefix="task_") 20 | # register project specific tasks 21 | kobo.worker.main.TaskContainer.register_module({{ project_name }}.tasks, prefix="task_") 22 | 23 | # configuration 24 | config_env = "{{ project_name|upper }}_CONFIG_FILE" 25 | config_default = "/etc/{{ project_name }}.conf" 26 | config_file = os.environ.get(config_env, config_default) 27 | conf = kobo.conf.PyConfigParser() 28 | try: 29 | conf.load_from_file(config_file) 30 | except (IOError, TypeError): 31 | sys.stderr.write("\n\nError: The config file '%s' was not found.\nCreate the config file or specify the '%s'\nenvironment variable to override config file location.\n" % (config_default, config_env)) 32 | return 2 33 | 34 | try: 35 | kobo.worker.main.main(conf) 36 | except KeyboardInterrupt: 37 | sys.stderr.write("\n\nExiting on user cancel.\n") 38 | return 1 39 | except kobo.exceptions.ImproperlyConfigured, ex: 40 | sys.stderr.write("\n\nImproperly configured: %s\n" % ex) 41 | return 3 42 | except IOError, ex: 43 | sys.stderr.write("\n\nIO Error: %s\n" % ex) 44 | return 4 45 | 46 | 47 | if __name__ == "__main__": 48 | sys.exit(main()) 49 | -------------------------------------------------------------------------------- /kobo/admin/templates/worker/__project_name__.conf: -------------------------------------------------------------------------------- 1 | # worker config file for {{ project_name }} 2 | 3 | # Hub XML-RPC address. 4 | HUB_URL = "http://localhost.localdomain:8000/xmlrpc" 5 | 6 | # Hub authentication method. Example: krbv, gssapi, password, worker_key, oidc, token_oidc 7 | AUTH_METHOD = "" 8 | 9 | # Username and password 10 | #USERNAME = "worker/localhost.localdomain" 11 | #PASSWORD = "" 12 | 13 | # A unique worker key used for authentication. 14 | # I't recommended to use krbv auth in production environment instead. 15 | #WORKER_KEY = "" 16 | 17 | # Kerberos principal. If commented, default principal obtained by kinit is used. 18 | #KRB_PRINCIPAL = "" 19 | 20 | # Kerberos keytab file. 21 | #KRB_KEYTAB = "" 22 | 23 | # Kerberos service prefix. Example: host, HTTP 24 | #KRB_SERVICE = "HTTP" 25 | 26 | # Kerberos realm. If commented, last two parts of domain name are used. Example: MYDOMAIN.COM. 27 | #KRB_REALM = "EXAMPLE.COM" 28 | 29 | # Kerberos credential cache file. 30 | #KRB_CCACHE = "" 31 | 32 | # Kerberos proxy users. 33 | #KRB_PROXY_USERS = "" 34 | 35 | # Process pid file. 36 | PID_FILE = "/var/run/{{ project_name }}.pid" 37 | 38 | # Log level. Example: debug, info, warning, error, critical. 39 | LOG_LEVEL = "info" 40 | 41 | # Log file. 42 | LOG_FILE = "/var/log/{{ project_name }}.log" 43 | 44 | # The maximum number of jobs that a worker will handle at a time. 45 | MAX_JOBS = 10 46 | 47 | # Task manager sleep time between polls. 48 | SLEEP_TIME = 20 49 | 50 | # Enables XML-RPC verbose flag 51 | DEBUG_XMLRPC = 0 52 | -------------------------------------------------------------------------------- /kobo/admin/templates/worker/tasks/__init__.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/admin/templates/worker/tasks/__init__.py.template -------------------------------------------------------------------------------- /kobo/client/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/client/commands/__init__.py -------------------------------------------------------------------------------- /kobo/client/commands/cmd_add_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | import sys 6 | 7 | from kobo.client import ClientCommand 8 | 9 | 10 | class Add_User(ClientCommand): 11 | """add a new user""" 12 | enabled = True 13 | admin = True 14 | 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s [options] " % self.normalized_name 18 | 19 | self.parser.add_option( 20 | "--admin", 21 | default=False, 22 | action="store_true", 23 | help="grant admin privileges" 24 | ) 25 | 26 | 27 | def run(self, *args, **kwargs): 28 | if len(args) < 1: 29 | self.parser.error("Please specify a user") 30 | 31 | username = kwargs.pop("username", None) 32 | password = kwargs.pop("password", None) 33 | hub = kwargs.pop("hub", None) 34 | admin = kwargs.pop("admin", False) 35 | user = args[0] 36 | 37 | self.set_hub(username, password, hub) 38 | try: 39 | self.hub.admin.add_user(user, admin) 40 | except Exception as ex: 41 | print(str(ex)) 42 | sys.exit(1) 43 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_cancel_tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | import sys 6 | 7 | from kobo.client import ClientCommand 8 | import six 9 | 10 | 11 | class Cancel_Tasks(ClientCommand): 12 | """cancel free, assigned or open tasks""" 13 | enabled = True 14 | 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s task_id [task_id...]" % self.normalized_name 18 | 19 | 20 | def run(self, *args, **kwargs): 21 | if len(args) == 0: 22 | self.parser.error("At least one task id must be specified.") 23 | 24 | username = kwargs.pop("username", None) 25 | password = kwargs.pop("password", None) 26 | hub = kwargs.pop("hub", None) 27 | tasks = args 28 | 29 | self.set_hub(username, password, hub) 30 | 31 | failed = False 32 | for task_id in tasks: 33 | try: 34 | result = self.hub.client.cancel_task(task_id) 35 | if result and isinstance(result, six.string_types): 36 | print(result) 37 | except Exception as ex: 38 | failed = True 39 | print(ex) 40 | 41 | if failed: 42 | sys.exit(1) 43 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_create_task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | try: 5 | import json 6 | except ImportError: 7 | import simplejson as json 8 | 9 | from kobo.client import ClientCommand 10 | 11 | 12 | class Create_Task(ClientCommand): 13 | """create a new task which is either brand new or based on existing one""" 14 | enabled = True 15 | admin = True 16 | 17 | def options(self): 18 | self.parser.usage = "%%prog %s " % self.normalized_name 19 | 20 | # if not specified, value from source task is used 21 | self.parser.add_option("--clone-from", dest="task_id", type="int", help="a task ID to clone a new task from") 22 | self.parser.add_option("--args", help="task arguments in JSON serialized dictionary") 23 | self.parser.add_option("--owner", dest="owner_name", help="username of the task owner") 24 | self.parser.add_option("--worker", dest="worker_name", help="name (hostname) of the worker") 25 | self.parser.add_option("--label", help="label or description") 26 | self.parser.add_option("--method", help="method name of the task handler") 27 | self.parser.add_option("--comment", help="comment") 28 | self.parser.add_option("--arch", dest="arch_name", help="arch name") 29 | self.parser.add_option("--channel", dest="channel_name", help="channel name") 30 | self.parser.add_option("--timeout", help="timeout") 31 | self.parser.add_option("--priority", help="priority") 32 | self.parser.add_option("--weight", help="weight") 33 | 34 | def _check_task_args(self, task_args): 35 | try: 36 | arg_dict = json.loads(task_args) 37 | except ValueError: 38 | self.parser.error("Arguments have to be specified in valid JSON.") 39 | 40 | if type(arg_dict) != dict: 41 | self.parser.error("Arguments have to be specified in dictionary type.") 42 | 43 | def run(self, *args, **kwargs): 44 | username = kwargs.pop("username", None) 45 | password = kwargs.pop("password", None) 46 | hub = kwargs.pop("hub", None) 47 | 48 | task_args = kwargs.get("args", None) 49 | if task_args != None: 50 | self._check_task_args(task_args) 51 | 52 | if kwargs["task_id"] is None: 53 | if kwargs["owner_name"] is None: 54 | self.parser.error("Owner is not set.") 55 | if kwargs["method"] is None: 56 | self.parser.error("Method is not set.") 57 | 58 | for key, value in kwargs.items(): 59 | if value is None: 60 | del kwargs[key] 61 | 62 | self.set_hub(username, password, hub) 63 | self.hub.client.create_task(kwargs) 64 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_create_worker.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from xmlrpc.client import Fault 3 | import json 4 | 5 | from kobo.client import ClientCommand 6 | 7 | 8 | class Create_Worker(ClientCommand): 9 | """create a worker""" 10 | enabled = True 11 | admin = True 12 | 13 | 14 | def options(self): 15 | self.parser.usage = "%%prog %s worker_name [worker_name]" % self.normalized_name 16 | 17 | def run(self, *args, **kwargs): 18 | if not args: 19 | self.parser.error("No worker name specified.") 20 | 21 | username = kwargs.pop("username", None) 22 | password = kwargs.pop("password", None) 23 | hub = kwargs.pop("hub", None) 24 | workers = args 25 | 26 | self.set_hub(username, password, hub) 27 | for worker in workers: 28 | try: 29 | new_worker = self.hub.client.create_worker(worker) 30 | print(json.dumps(new_worker)) 31 | except Fault as ex: 32 | print(repr(ex), file=sys.stderr) 33 | # Exit on first xmlrpc failure 34 | # It's very likely user is not admin 35 | raise 36 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_disable_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import sys 5 | from six.moves.xmlrpc_client import Fault 6 | 7 | from kobo.client import ClientCommand 8 | 9 | 10 | class Disable_Worker(ClientCommand): 11 | """disable worker""" 12 | enabled = True 13 | admin = True 14 | 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s [--all] [worker_name]" % self.normalized_name 18 | 19 | self.parser.add_option( 20 | "--all", 21 | default=False, 22 | action="store_true", 23 | help="Disable all enabled workers" 24 | ) 25 | 26 | def run(self, *args, **kwargs): 27 | if len(args) == 0 and not kwargs['all']: 28 | self.parser.error("No worker (or --all) specified.") 29 | if len(args) and kwargs['all']: 30 | self.parser.error("Specify worker name or --all. From safety reasons both are not allowed.") 31 | 32 | username = kwargs.pop("username", None) 33 | password = kwargs.pop("password", None) 34 | hub = kwargs.pop("hub", None) 35 | 36 | self.set_hub(username, password, hub) 37 | if kwargs['all']: 38 | try: 39 | workers = self.hub.client.list_workers(True) 40 | except Fault as ex: 41 | print(repr(ex), file=sys.stderr) 42 | sys.exit(1) 43 | else: 44 | workers = args 45 | for worker in workers: 46 | try: 47 | self.hub.client.disable_worker(worker) 48 | except Fault as ex: 49 | print(repr(ex), file=sys.stderr) 50 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_enable_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import sys 5 | from six.moves.xmlrpc_client import Fault 6 | 7 | from kobo.client import ClientCommand 8 | 9 | 10 | class Enable_Worker(ClientCommand): 11 | """enable worker""" 12 | enabled = True 13 | admin = True 14 | 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s [--all] [worker_name]" % self.normalized_name 18 | 19 | self.parser.add_option( 20 | "--all", 21 | default=False, 22 | action="store_true", 23 | help="Enable all enabled workers" 24 | ) 25 | 26 | def run(self, *args, **kwargs): 27 | if len(args) == 0 and not kwargs['all']: 28 | self.parser.error("No worker (or --all) specified.") 29 | if len(args) and kwargs['all']: 30 | self.parser.error("Specify worker name or --all. From safety reasons both are not allowed.") 31 | 32 | username = kwargs.pop("username", None) 33 | password = kwargs.pop("password", None) 34 | hub = kwargs.pop("hub", None) 35 | 36 | self.set_hub(username, password, hub) 37 | if kwargs['all']: 38 | try: 39 | workers = self.hub.client.list_workers(True) 40 | except Fault as ex: 41 | print(repr(ex), file=sys.stderr) 42 | sys.exit(1) 43 | else: 44 | workers = args 45 | for worker in workers: 46 | try: 47 | self.hub.client.enable_worker(worker) 48 | except Fault as ex: 49 | print(repr(ex), file=sys.stderr) 50 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_list_tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | import sys 5 | try: 6 | import json 7 | except ImportError: 8 | import simplejson as json 9 | 10 | from kobo.client import ClientCommand 11 | from kobo.client.constants import TASK_STATES 12 | 13 | 14 | class List_Tasks(ClientCommand): 15 | """list RUNNING and/or FREE tasks""" 16 | enabled = True 17 | 18 | 19 | def options(self): 20 | self.parser.usage = "%%prog %s [--free] [--running] [--verbose|--json]" % self.normalized_name 21 | 22 | self.parser.add_option( 23 | "--running", 24 | default=False, 25 | action="store_true", 26 | help="list RUNNING tasks" 27 | ) 28 | 29 | self.parser.add_option( 30 | "--free", 31 | default=False, 32 | action="store_true", 33 | help="list FREE tasks", 34 | ) 35 | 36 | self.parser.add_option( 37 | "--verbose", 38 | default=False, 39 | action="store_true", 40 | help="print details", 41 | ) 42 | 43 | self.parser.add_option( 44 | "--json", 45 | default=False, 46 | action="store_true", 47 | help="print results in json", 48 | ) 49 | 50 | def run(self, *args, **kwargs): 51 | username = kwargs.pop("username", None) 52 | password = kwargs.pop("password", None) 53 | hub = kwargs.pop("hub", None) 54 | verbose = kwargs.pop("verbose", False) 55 | use_json = kwargs.pop("json", False) 56 | 57 | if verbose and use_json: 58 | self.parser.error("It has no sense to use --verbose and --json in one time.") 59 | 60 | filters = [] 61 | if kwargs['free']: 62 | filters += [TASK_STATES["FREE"], TASK_STATES["CREATED"]] 63 | if kwargs['running']: 64 | filters += [TASK_STATES["ASSIGNED"], TASK_STATES["OPEN"]] 65 | 66 | if not filters: 67 | self.parser.error("Use at least one from --free or --running options.") 68 | 69 | self.set_hub(username, password, hub) 70 | result = sorted(self.hub.client.get_tasks([], filters), key=lambda x: x["id"]) 71 | if use_json: 72 | print(json.dumps(result, indent=2, sort_keys=True)) 73 | elif verbose: 74 | fmt = "%(id)8s %(state_label)-12s %(method)-20s %(owner)-12s %(worker)s" 75 | header = dict(id="TASKID", state_label="STATE", method="METHOD", owner="OWNER", worker="WORKER") 76 | print(fmt % header, file=sys.stderr) 77 | for task in result: 78 | print(fmt % task) 79 | else: 80 | for task in result: 81 | print(task['id']) 82 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_list_workers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | from kobo.client import ClientCommand 6 | 7 | 8 | class List_Workers(ClientCommand): 9 | """list workers""" 10 | enabled = True 11 | 12 | 13 | def options(self): 14 | self.parser.usage = "%%prog %s" % self.normalized_name 15 | 16 | self.parser.add_option( 17 | "--show-disabled", 18 | default=False, 19 | action="store_true", 20 | help="show disabled workers" 21 | ) 22 | 23 | 24 | def run(self, *args, **kwargs): 25 | username = kwargs.pop("username", None) 26 | password = kwargs.pop("password", None) 27 | hub = kwargs.pop("hub", None) 28 | show_disabled = kwargs.get("show_disabled", False) 29 | 30 | self.set_hub(username, password, hub) 31 | print(self.hub.client.list_workers(not show_disabled)) 32 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_resubmit_tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import print_function 5 | import sys 6 | 7 | from kobo.client.task_watcher import TaskWatcher 8 | from kobo.client import ClientCommand 9 | 10 | 11 | class Resubmit_Tasks(ClientCommand): 12 | """resubmit failed tasks""" 13 | enabled = True 14 | 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s task_id [task_id...]" % self.normalized_name 18 | self.parser.add_option("--force", action="store_true", help="Resubmit also tasks which are closed properly.") 19 | self.parser.add_option("--nowait", default=False, action="store_true", help="Don't wait until tasks finish.") 20 | self.parser.add_option("--priority", help="priority") 21 | 22 | 23 | def run(self, *args, **kwargs): 24 | if len(args) == 0: 25 | self.parser.error("At least one task id must be specified.") 26 | 27 | username = kwargs.pop("username", None) 28 | password = kwargs.pop("password", None) 29 | hub = kwargs.pop("hub", None) 30 | force = kwargs.pop("force", False) 31 | priority = kwargs.pop("priority", None) 32 | 33 | tasks = args 34 | 35 | self.set_hub(username, password, hub) 36 | resubmitted_tasks = [] 37 | failed = False 38 | for task_id in tasks: 39 | try: 40 | resubmitted_id = self.hub.client.resubmit_task(task_id, force, *[arg for arg in [priority] if arg is not None]) 41 | resubmitted_tasks.append(resubmitted_id) 42 | except Exception as ex: 43 | failed = True 44 | print(ex) 45 | 46 | if not kwargs.get('nowait'): 47 | TaskWatcher.watch_tasks(self.hub, resubmitted_tasks) 48 | if failed: 49 | sys.exit(1) 50 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_shutdown_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import sys 5 | from six.moves.xmlrpc_client import Fault 6 | 7 | from kobo.client import ClientCommand 8 | 9 | 10 | class Shutdown_Worker(ClientCommand): 11 | """shutdown a worker""" 12 | enabled = True 13 | admin = True 14 | 15 | 16 | def options(self): 17 | self.parser.usage = "%%prog %s [--kill] worker_name [worker_name]" % self.normalized_name 18 | 19 | self.parser.add_option( 20 | "--kill", 21 | default=False, 22 | action="store_true", 23 | help="kill worker immediately" 24 | ) 25 | 26 | 27 | def run(self, *args, **kwargs): 28 | kill = kwargs.pop("kill", False) 29 | 30 | if len(args) == 0: 31 | self.parser.error("No worker specified.") 32 | 33 | username = kwargs.pop("username", None) 34 | password = kwargs.pop("password", None) 35 | hub = kwargs.pop("hub", None) 36 | workers = args 37 | 38 | self.set_hub(username, password, hub) 39 | for worker in workers: 40 | try: 41 | self.hub.client.shutdown_worker(worker, kill) 42 | except Fault as ex: 43 | print(repr(ex), file=sys.stderr) 44 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_watch_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import time 5 | import six.moves.urllib.request as urllib2 6 | try: 7 | import json 8 | except ImportError: 9 | import simplejson as json 10 | 11 | from kobo.client import ClientCommand 12 | 13 | 14 | MIN_POLL_INTERVAL = 15 15 | 16 | class Watch_Log(ClientCommand): 17 | """displays task logs incrementally""" 18 | enabled = True 19 | 20 | 21 | def options(self): 22 | self.parser.usage = "%%prog %s task_id" % self.normalized_name 23 | 24 | self.parser.add_option( 25 | "--type", 26 | default="stdout.log", 27 | action="store", 28 | help="Show log with this name, default is stdout.log" 29 | ) 30 | 31 | self.parser.add_option( 32 | "--poll", 33 | default=MIN_POLL_INTERVAL, 34 | type="int", 35 | help="Interval how often server should be polled for new info (seconds >= %s)" % MIN_POLL_INTERVAL 36 | ) 37 | 38 | self.parser.add_option( 39 | "--nowait", 40 | default=False, 41 | action="store_true", 42 | help="Return after fetching current logfile, don't wait until task finishes" 43 | ) 44 | 45 | 46 | def run(self, *args, **kwargs): 47 | if len(args) != 1: 48 | self.parser.error("Exactly one task id must be specified.") 49 | try: 50 | task_id = int(args[0]) 51 | except ValueError: 52 | self.parser.error("Task ID should be an integer") 53 | 54 | if kwargs['poll'] < MIN_POLL_INTERVAL: 55 | self.parser.error("Poll interval has to be higher than %s." % MIN_POLL_INTERVAL) 56 | 57 | # HACK: We're presuming, that urls were not touched and that base_url 58 | # is also url of web ui. As we suppose that also task.urls were not 59 | # altered it should work. 60 | hub = kwargs.pop('hub', None) or self.conf['HUB_URL'] 61 | url = hub.replace('/xmlrpc', '') + '/task/%d/log-json/%s?offset=%d' 62 | offset = 0 63 | assert url.startswith(("http:", "https:")) 64 | while True: 65 | data = json.loads( 66 | urllib2.urlopen(url % # nosec B310 67 | (task_id, kwargs['type'], offset)).read()) 68 | if data['content']: 69 | sys.stdout.write(data['content']) 70 | sys.stdout.flush() 71 | if kwargs['nowait']: 72 | break 73 | next_poll = data.get('next_poll') 74 | if data['task_finished'] == 1 and next_poll is None: 75 | break 76 | offset = data['new_offset'] 77 | 78 | # If next_poll is 0, that means there's immediately more content, so fetch 79 | # it now. Otherwise, stick with the user's requested poll interval 80 | if next_poll != 0: 81 | time.sleep(kwargs['poll']) 82 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_watch_tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.client.task_watcher import TaskWatcher 5 | from kobo.client import ClientCommand 6 | 7 | 8 | class Watch_Tasks(ClientCommand): 9 | """track progress of particular tasks""" 10 | enabled = True 11 | 12 | 13 | def options(self): 14 | self.parser.usage = "%%prog %s task_id [task_id...]" % self.normalized_name 15 | 16 | 17 | def run(self, *args, **kwargs): 18 | if len(args) == 0: 19 | self.parser.error("At least one task id must be specified.") 20 | 21 | username = kwargs.pop("username", None) 22 | password = kwargs.pop("password", None) 23 | hub = kwargs.pop("hub", None) 24 | 25 | self.set_hub(username, password, hub) 26 | TaskWatcher.watch_tasks(self.hub, args) 27 | -------------------------------------------------------------------------------- /kobo/client/commands/cmd_worker_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | from pprint import pprint 5 | from kobo.client import ClientCommand 6 | 7 | 8 | class Worker_Info(ClientCommand): 9 | """get worker info""" 10 | enabled = True 11 | admin = True 12 | 13 | 14 | def options(self): 15 | self.parser.usage = "%%prog %s worker_name" % self.normalized_name 16 | 17 | self.parser.add_option( 18 | "--oneline", 19 | default=False, 20 | action="store_true", 21 | help="Display one-line dict output instead of pretty-print" 22 | ) 23 | 24 | 25 | def run(self, *args, **kwargs): 26 | if len(args) != 1: 27 | self.parser.error("No worker specified") 28 | 29 | username = kwargs.pop("username", None) 30 | password = kwargs.pop("password", None) 31 | hub = kwargs.pop("hub", None) 32 | worker_name = args[0] 33 | 34 | self.set_hub(username, password, hub) 35 | result = self.hub.client.get_worker_info(worker_name) 36 | if kwargs.pop("oneline"): 37 | print(result) 38 | else: 39 | pprint(result) 40 | -------------------------------------------------------------------------------- /kobo/client/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.types import Enum 5 | 6 | 7 | __all__ = ( 8 | "TASK_STATES", 9 | "FINISHED_STATES", 10 | "FAILED_STATES", 11 | ) 12 | 13 | 14 | TASK_STATES = Enum( 15 | "FREE", # default state for new tasks 16 | "ASSIGNED", # assigned to a worker 17 | "OPEN", # opened by a worker and being processed 18 | "CLOSED", # successfully finished 19 | "CANCELED", # canceled by user request 20 | "FAILED", # failed 21 | "INTERRUPTED", # interrupted by an external event (power outage, process killed, etc.) 22 | "TIMEOUT", # reached timeout and killed by task manager 23 | "CREATED", # task is created, but still not ready to be processed 24 | ) 25 | 26 | 27 | FINISHED_STATES = ( 28 | TASK_STATES["CLOSED"], 29 | TASK_STATES["CANCELED"], 30 | TASK_STATES["FAILED"], 31 | TASK_STATES["INTERRUPTED"], 32 | TASK_STATES["TIMEOUT"], 33 | ) 34 | 35 | 36 | FAILED_STATES = ( 37 | TASK_STATES["CANCELED"], 38 | TASK_STATES["FAILED"], 39 | TASK_STATES["INTERRUPTED"], 40 | TASK_STATES["TIMEOUT"], 41 | ) 42 | -------------------------------------------------------------------------------- /kobo/client/default.conf: -------------------------------------------------------------------------------- 1 | # Hub xml-rpc address. 2 | HUB_URL = "https://localhost/hub/xmlrpc" 3 | 4 | # Hub authentication method. Example: krbv, gssapi, password, worker_key, oidc, token_oidc 5 | AUTH_METHOD = "krbv" 6 | 7 | # Username and password 8 | #USERNAME = "" 9 | #PASSWORD = "" 10 | 11 | # A unique worker key used for authentication. 12 | # I't recommended to use krbv auth in production environment instead. 13 | #WORKER_KEY = "" 14 | 15 | # Kerberos principal. If commented, default principal obtained by kinit is used. 16 | #KRB_PRINCIPAL = "" 17 | 18 | # Kerberos keytab file. 19 | #KRB_KEYTAB = "" 20 | 21 | # Kerberos service prefix. Example: host, HTTP 22 | #KRB_SERVICE = "HTTP" 23 | 24 | # Kerberos realm. If commented, last two parts of domain name are used. Example: MYDOMAIN.COM. 25 | #KRB_REALM = "EXAMPLE.COM" 26 | 27 | # Kerberos credential cache file. 28 | #KRB_CCACHE = "" 29 | 30 | # Kerberos proxy users. 31 | #KRB_PROXY_USERS = "" 32 | -------------------------------------------------------------------------------- /kobo/client/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import sys 5 | 6 | import kobo.cli 7 | 8 | 9 | __all__ = ( 10 | "main", 11 | ) 12 | 13 | 14 | # register default command plugins 15 | #import kobo.client.commands 16 | #CommandContainer.register_module(kobo.client.commands, prefix="cmd_") 17 | 18 | 19 | def main(): 20 | command_container = kobo.cli.CommandContainer() 21 | parser = kobo.cli.CommandOptionParser(command_container=command_container, add_username_password_options=True) 22 | parser.run() 23 | sys.exit(0) 24 | -------------------------------------------------------------------------------- /kobo/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | __all__ = ( 5 | "decorator_with_args", 6 | "well_behaved", 7 | "log_traceback", 8 | ) 9 | 10 | 11 | def decorator_with_args(old_decorator): 12 | """Enable arguments for decorators. 13 | 14 | Example: 15 | >>> @decorator_with_args 16 | def new_decorator(func, arg1, arg2): 17 | ... 18 | 19 | # it's the same as: func = new_decorator(func)("foo", "bar") 20 | @new_decorator("foo", "bar") 21 | def func(): 22 | ... 23 | """ 24 | 25 | def new_decorator_args(*nd_args, **nd_kwargs): 26 | def _new_decorator(func): 27 | return old_decorator(func, *nd_args, **nd_kwargs) 28 | 29 | _new_decorator.__name__ = old_decorator.__name__ 30 | _new_decorator.__doc__ = old_decorator.__doc__ 31 | if hasattr(old_decorator, "__dict__"): 32 | _new_decorator.__dict__.update(old_decorator.__dict__) 33 | 34 | return _new_decorator 35 | return new_decorator_args 36 | 37 | 38 | def well_behaved(decorator): 39 | """Turn a decorator into the well-behaved one.""" 40 | 41 | def new_decorator(func): 42 | new_func = decorator(func) 43 | new_func.__name__ = func.__name__ 44 | new_func.__doc__ = func.__doc__ 45 | new_func.__dict__.update(func.__dict__) 46 | return new_func 47 | 48 | new_decorator.__name__ = decorator.__name__ 49 | new_decorator.__doc__ = decorator.__doc__ 50 | new_decorator.__dict__.update(decorator.__dict__) 51 | return new_decorator 52 | 53 | 54 | @decorator_with_args 55 | def log_traceback(func, log_file): 56 | """Save tracebacks of exceptions raised in a decorated function to a file.""" 57 | 58 | def new_func(*args, **kwargs): 59 | try: 60 | return func(*args, **kwargs) 61 | except: 62 | import datetime 63 | import kobo.shortcuts 64 | import kobo.tback 65 | date = datetime.datetime.strftime(datetime.datetime.now(), "%F %R:%S") 66 | data = "--- TRACEBACK BEGIN: " + date + " ---\n" 67 | data += kobo.tback.Traceback().get_traceback() 68 | data += "--- TRACEBACK END: " + date + " ---\n\n\n" 69 | kobo.shortcuts.save_to_file(log_file, data, append=True) 70 | raise 71 | return new_func 72 | -------------------------------------------------------------------------------- /kobo/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/__init__.py -------------------------------------------------------------------------------- /kobo/django/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from kobo.django.django_version import django_version_ge 2 | 3 | if django_version_ge('3.2.0'): 4 | pass 5 | else: 6 | default_app_config = 'kobo.django.auth.apps.AuthConfig' 7 | -------------------------------------------------------------------------------- /kobo/django/auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.admin import * 2 | import django.contrib.admin as admin 3 | 4 | from kobo.django.auth.models import * 5 | 6 | # users are not displayed on admin page since migrations were introduced 7 | admin.site.register(User, UserAdmin) 8 | -------------------------------------------------------------------------------- /kobo/django/auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class AuthConfig(AppConfig): 4 | name = 'kobo.django.auth' 5 | label = 'kobo_auth' 6 | verbose_name = 'Kobo authentication' 7 | -------------------------------------------------------------------------------- /kobo/django/auth/krb5.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | """ 5 | # This is authentication backend for Django middleware. 6 | # In settings.py you need to set: 7 | 8 | MIDDLEWARE_CLASSES = ( 9 | ... 10 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 11 | 'django.contrib.auth.middleware.RemoteUserMiddleware', 12 | ... 13 | ) 14 | AUTHENTICATION_BACKENDS = ( 15 | 'kobo.django.auth.krb5.RemoteUserBackend', 16 | ) 17 | 18 | 19 | # Add login and logout adresses to urls.py: 20 | 21 | urlpatterns = [ 22 | ... 23 | url(r'^auth/krb5login/$', 24 | django.views.generic.TemplateView.as_view(template = 'auth/krb5login.html'), 25 | url(r'^auth/logout/$', 'django.contrib.auth.views.logout', kwargs={"next_page": "/"}), 26 | ... 27 | ] 28 | 29 | 30 | # Set a httpd config to protect krb5login page with kerberos. 31 | # You need to have mod_auth_kerb installed to use kerberos auth. 32 | # Httpd config /etc/httpd/conf.d/.conf should look like this: 33 | 34 | 35 | SetHandler python-program 36 | PythonHandler django.core.handlers.modpython 37 | SetEnv DJANGO_SETTINGS_MODULE .settings 38 | PythonDebug On 39 | 40 | 41 | 42 | AuthType Kerberos 43 | AuthName " Kerberos Authentication" 44 | KrbMethodNegotiate on 45 | KrbMethodK5Passwd off 46 | KrbServiceName HTTP 47 | KrbAuthRealms EXAMPLE.COM 48 | Krb5Keytab /etc/httpd/conf/http..keytab 49 | KrbSaveCredentials off 50 | Require valid-user 51 | 52 | """ 53 | 54 | 55 | from django.contrib.auth.backends import RemoteUserBackend 56 | 57 | class Krb5RemoteUserBackend(RemoteUserBackend): 58 | def clean_username(self, username): 59 | # remove @REALM from username 60 | return username.split("@")[0] 61 | -------------------------------------------------------------------------------- /kobo/django/auth/middleware.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.middleware import RemoteUserMiddleware 2 | from django.utils.deprecation import MiddlewareMixin 3 | 4 | from kobo.django.helpers import call_if_callable 5 | 6 | class LimitedRemoteUserMiddleware(RemoteUserMiddleware, MiddlewareMixin): 7 | ''' 8 | Same behaviour as RemoteUserMiddleware except that it doesn't logout user 9 | if is already logged in. 10 | Useful when you have just one authentication powered login page. 11 | ''' 12 | def process_request(self, request): 13 | if not hasattr(request, 'user') or not call_if_callable(request.user.is_authenticated): 14 | super(LimitedRemoteUserMiddleware, self).process_request(request) 15 | -------------------------------------------------------------------------------- /kobo/django/auth/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | import django.contrib.auth.models 7 | import re 8 | import django.core.validators 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('auth', '0006_require_contenttypes_0002'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(help_text='Required. 255 characters or fewer. Letters, numbers and @/./+/-/_ characters', unique=True, max_length=255, verbose_name='username', validators=[django.core.validators.RegexValidator(re.compile(b'^[\\w.@+-]+$'), 'Enter a valid username.', b'invalid')])), 26 | ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), 27 | ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), 28 | ('email', models.EmailField(max_length=254, verbose_name='email address', blank=True)), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups')), 33 | ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), 34 | ], 35 | options={ 36 | 'db_table': 'auth_user', 37 | 'verbose_name': 'user', 38 | 'verbose_name_plural': 'users', 39 | }, 40 | managers=[ 41 | ('objects', django.contrib.auth.models.UserManager()), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /kobo/django/auth/migrations/0002_auto_20220203_1511.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2022-02-03 15:11 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('kobo_auth', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='user', 16 | name='username', 17 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 255 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], verbose_name='username'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /kobo/django/auth/migrations/0003_alter_user_username.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-11-16 11:30 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('kobo_auth', '0002_auto_20220203_1511'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='user', 16 | name='username', 17 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 255 characters or fewer. Letters, digits and @.+-_/ only.', max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+\\-/]+$', 'Enter a valid username. This value may contain only letters, numbers and @.+-_/ characters.', 'invalid')], verbose_name='username'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /kobo/django/auth/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/auth/migrations/__init__.py -------------------------------------------------------------------------------- /kobo/django/auth/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django.db import models 3 | from django.core import validators 4 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager 5 | from django.core.mail import send_mail 6 | from django.utils import timezone 7 | 8 | from kobo.django.compat import gettext_lazy as _ 9 | 10 | 11 | MAX_LENGTH = 255 12 | 13 | class User(AbstractBaseUser, PermissionsMixin): 14 | """ 15 | Copy (non-abstract) of AbstractUser with longer username. Removed profile support as it 16 | is deprecated in django 1.5. 17 | 18 | Username, password and email are required. Other fields are optional. 19 | """ 20 | username = models.CharField(_('username'), max_length=MAX_LENGTH, unique=True, 21 | help_text=_('Required. %s characters or fewer. Letters, digits and @.+-_/ only.' % MAX_LENGTH), 22 | validators=[ 23 | validators.RegexValidator(r'^[\w.@+\-/]+$', 24 | _('Enter a valid username. ' 25 | 'This value may contain only letters, numbers ' 26 | 'and @.+-_/ characters.'), 'invalid'), 27 | ], 28 | error_messages={ 29 | 'unique': _("A user with that username already exists."), 30 | }) 31 | first_name = models.CharField(_('first name'), max_length=30, blank=True) 32 | last_name = models.CharField(_('last name'), max_length=30, blank=True) 33 | email = models.EmailField(_('email address'), blank=True) 34 | is_staff = models.BooleanField(_('staff status'), default=False, 35 | help_text=_('Designates whether the user can log into this admin ' 36 | 'site.')) 37 | is_active = models.BooleanField(_('active'), default=True, 38 | help_text=_('Designates whether this user should be treated as ' 39 | 'active. Unselect this instead of deleting accounts.')) 40 | date_joined = models.DateTimeField(_('date joined'), default=timezone.now) 41 | 42 | objects = UserManager() 43 | 44 | USERNAME_FIELD = 'username' 45 | REQUIRED_FIELDS = ['email'] 46 | 47 | class Meta: 48 | db_table = 'auth_user' 49 | verbose_name = _('user') 50 | verbose_name_plural = _('users') 51 | 52 | def get_full_name(self): 53 | """ 54 | Returns the first_name plus the last_name, with a space in between. 55 | """ 56 | full_name = '%s %s' % (self.first_name, self.last_name) 57 | return full_name.strip() 58 | 59 | def get_short_name(self): 60 | "Returns the short name for the user." 61 | return self.first_name 62 | 63 | def email_user(self, subject, message, from_email=None, **kwargs): 64 | """ 65 | Sends an email to this User. 66 | """ 67 | send_mail(subject, message, from_email, [self.email], **kwargs) 68 | -------------------------------------------------------------------------------- /kobo/django/compat.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | if six.PY2: 4 | # Ancient cases: force_text and ugettext are the unicode-aware variants 5 | from django.utils.translation import ugettext_lazy as gettext_lazy 6 | from django.utils.encoding import force_text as force_str 7 | else: 8 | # Modern (py3-only) case 9 | from django.utils.translation import gettext_lazy 10 | from django.utils.encoding import force_str 11 | 12 | 13 | __all__ = ["gettext_lazy", "force_str"] 14 | -------------------------------------------------------------------------------- /kobo/django/django_version.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | def django_version_ge(version_str): 4 | """ 5 | Check if current django is in higher or equal version than specified by 6 | version_str parameter 7 | """ 8 | 9 | ver1 = [int(x) for x in django.get_version().split('.')] 10 | ver2 = [int(x) for x in version_str.split('.')] 11 | 12 | # lists must have the same lenght for comparison to work 13 | max_len = max(len(ver1), len(ver2)) 14 | 15 | def append_zeros(lst): 16 | while len(lst) != max_len: 17 | lst.append(0) 18 | 19 | append_zeros(ver1) 20 | append_zeros(ver2) 21 | 22 | return ver1 >= ver2 23 | -------------------------------------------------------------------------------- /kobo/django/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | try: 5 | import json 6 | except ImportError: 7 | import simplejson as json 8 | 9 | import django.forms.fields 10 | from django.core.exceptions import ValidationError 11 | from django.utils.html import format_html 12 | 13 | try: 14 | from django.forms.utils import flatatt 15 | except ImportError: 16 | from django.forms.util import flatatt 17 | 18 | from kobo.django.compat import gettext_lazy as _, force_str 19 | 20 | 21 | class StateChoiceFormField(django.forms.fields.TypedChoiceField): 22 | ''' 23 | def __init__(self, *args, **kwargs): 24 | super(StateChoiceFormField, self).__init__(*args, **kwargs) 25 | ugly hack - get back reference to StateEnum object 26 | state = kwargs['coerce'].__self__ 27 | print 'initscff', kwargs['initial'], state, state.choices 28 | print type(kwargs['initial']) 29 | ''' 30 | 31 | def clean(self, value): 32 | value = super(StateChoiceFormField, self).clean(value) 33 | try: 34 | value = int(str(value)) 35 | except ValueError: 36 | raise ValidationError('Wrong state value.') 37 | self.widget.choices = self.choices 38 | if value in django.forms.fields.EMPTY_VALUES: 39 | return None 40 | for c in self.choices: 41 | if c[0] == value: 42 | return c[1] 43 | raise ValidationError('Selected value is not in valid choices.') 44 | 45 | 46 | class JSONWidget(django.forms.widgets.Textarea): 47 | def render(self, name, value, attrs=None, renderer=None): 48 | if value is None: value = '' 49 | final_attrs = self.build_attrs(attrs, {"name": name}) 50 | 51 | if not isinstance(value, six.string_types): 52 | value = json.dumps(value) 53 | 54 | return format_html(u'{}', 55 | flatatt(final_attrs), force_str(value)) 56 | 57 | 58 | class JSONFormField(django.forms.fields.CharField): 59 | widget = JSONWidget 60 | 61 | def from_db_value(self, value, expression, connection, context=None): 62 | return self.to_python(value) 63 | 64 | 65 | def to_python(self, value): 66 | try: 67 | result = json.loads(value) 68 | except ValueError: 69 | raise ValidationError(_("Cannot deserialize JSON data.")) 70 | else: 71 | if not isinstance(result, (dict, list)): 72 | raise ValidationError(_("Data types are restricted to JSON serialized dict or list only.")) 73 | return result 74 | -------------------------------------------------------------------------------- /kobo/django/helpers.py: -------------------------------------------------------------------------------- 1 | def call_if_callable(obj): 2 | """ 3 | Determines if an object is callable, and returns its value or value of its call. 4 | """ 5 | if callable(obj): 6 | return obj() 7 | else: 8 | return obj 9 | -------------------------------------------------------------------------------- /kobo/django/menu/context_processors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | __all__ = ( 5 | "menu_context_processor", 6 | ) 7 | 8 | 9 | def menu_context_processor(request): 10 | """ 11 | @summary: Context processor for menu object. 12 | @param request: http request object 13 | @type request: django.http.HttpRequest 14 | """ 15 | return { 16 | "menu": request.menu 17 | } 18 | -------------------------------------------------------------------------------- /kobo/django/menu/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.utils.deprecation import MiddlewareMixin 4 | 5 | from kobo.django.menu import menu 6 | 7 | 8 | __all__ = ( 9 | "MenuMiddleware", 10 | ) 11 | 12 | 13 | class LazyMenu(object): 14 | """ 15 | @summary: Cached menu object 16 | """ 17 | def __get__(self, request, obj_type=None): 18 | if not hasattr(request, "_cached_menu"): 19 | request._cached_menu = menu.setup(request) 20 | return request._cached_menu 21 | 22 | 23 | class MenuMiddleware(MiddlewareMixin): 24 | """ 25 | @summary: Middleware for menu object. 26 | """ 27 | def process_request(self, request): 28 | """ 29 | @summary: Adds menu to request object 30 | @param request: http request object 31 | @type request: django.http.HttpRequest 32 | """ 33 | request.__class__.menu = LazyMenu() 34 | -------------------------------------------------------------------------------- /kobo/django/upload/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/upload/__init__.py -------------------------------------------------------------------------------- /kobo/django/upload/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import django.contrib.admin as admin 5 | 6 | from .models import FileUpload 7 | 8 | 9 | class FileUploadAdmin(admin.ModelAdmin): 10 | list_display = ('id', 'owner', 'name', 'size', 'upload_key', 'state', 'dt_created', 'dt_finished') 11 | list_filter = ('owner', 'state') 12 | search_fields = ('id', 'upload_key', 'name', 'dt_created', 'dt_finished') 13 | 14 | admin.site.register(FileUpload, FileUploadAdmin) 15 | -------------------------------------------------------------------------------- /kobo/django/upload/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2022-02-02 16:44 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='FileUpload', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=255)), 22 | ('checksum', models.CharField(max_length=255)), 23 | ('size', models.PositiveIntegerField()), 24 | ('target_dir', models.CharField(max_length=255)), 25 | ('upload_key', models.CharField(max_length=255)), 26 | ('state', models.PositiveIntegerField(choices=[(0, 'NEW'), (1, 'STARTED'), (2, 'FINISHED'), (3, 'FAILED')], default=0)), 27 | ('dt_created', models.DateTimeField(auto_now_add=True)), 28 | ('dt_finished', models.DateTimeField(blank=True, null=True)), 29 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 30 | ], 31 | options={ 32 | 'ordering': ('-dt_created', 'name'), 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /kobo/django/upload/migrations/0002_alter_fileupload_size.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-08-22 09:45 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('upload', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='fileupload', 16 | name='size', 17 | field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(0)]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /kobo/django/upload/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/upload/migrations/__init__.py -------------------------------------------------------------------------------- /kobo/django/upload/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | from django.conf import settings 7 | from django.core.validators import MinValueValidator 8 | from django.db import models 9 | from django.contrib.auth.models import User 10 | 11 | from kobo.shortcuts import random_string 12 | from kobo.types import Enum 13 | import six 14 | 15 | 16 | UPLOAD_STATES = Enum( 17 | "NEW", 18 | "STARTED", 19 | "FINISHED", 20 | "FAILED", 21 | ) 22 | 23 | 24 | @six.python_2_unicode_compatible 25 | class FileUpload(models.Model): 26 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 27 | name = models.CharField(max_length=255) 28 | checksum = models.CharField(max_length=255) 29 | # models.PositiveBigIntegerField would be even better but it was introduced only in Django 3.1 30 | size = models.BigIntegerField(validators=[MinValueValidator(0)]) 31 | target_dir = models.CharField(max_length=255) 32 | upload_key = models.CharField(max_length=255) 33 | state = models.PositiveIntegerField(default=0, choices=UPLOAD_STATES.get_mapping()) 34 | dt_created = models.DateTimeField(auto_now_add=True) 35 | dt_finished = models.DateTimeField(null=True, blank=True) 36 | 37 | class Meta: 38 | ordering = ("-dt_created", "name", ) 39 | # unique_together = ( 40 | # ("name", "target_dir") 41 | # ) 42 | 43 | def export(self): 44 | result = { 45 | "owner": self.owner_id, 46 | "name": self.name, 47 | "checksum": self.checksum, 48 | "size": self.size, 49 | "target_dir": self.target_dir, 50 | "state": self.state, 51 | } 52 | return result 53 | 54 | def get_full_path(self): 55 | return os.path.abspath(os.path.join(self.target_dir, self.name)) 56 | 57 | def __str__(self): 58 | return six.text_type(os.path.join(self.target_dir, self.name)) 59 | 60 | def save(self, *args, **kwargs): 61 | if not self.upload_key: 62 | self.upload_key = random_string(64) 63 | if "update_fields" in kwargs: 64 | kwargs["update_fields"] = {"upload_key"}.union(kwargs["update_fields"]) 65 | if self.state == UPLOAD_STATES['FINISHED']: 66 | if FileUpload.objects.filter(state = UPLOAD_STATES['FINISHED'], name = self.name, 67 | checksum=self.checksum, target_dir=self.target_dir).exclude(id = self.id).count() != 0: 68 | # someone created same upload faster 69 | self.state = UPLOAD_STATES['FAILED'] 70 | if "update_fields" in kwargs: 71 | kwargs["update_fields"] = {"state"}.union(kwargs["update_fields"]) 72 | 73 | # execute validators 74 | self.full_clean() 75 | super(FileUpload, self).save(*args, **kwargs) 76 | 77 | def delete(self): 78 | super(FileUpload, self).delete() 79 | # if file was successfully uploaded it should be removed from 80 | # filesystem, otherwise it shouldn't be there 81 | if self.state == UPLOAD_STATES['FINISHED']: 82 | try: 83 | os.unlink(self.get_full_path()) 84 | except OSError as ex: 85 | if ex.errno != 2: 86 | raise 87 | 88 | upload_dir = getattr(settings, "UPLOAD_DIR", None) 89 | if upload_dir is not None: 90 | upload_dir = os.path.abspath(upload_dir) 91 | file_dir = os.path.dirname(self.get_full_path()) 92 | while 1: 93 | if not file_dir.startswith(upload_dir): 94 | break 95 | if file_dir == upload_dir: 96 | break 97 | try: 98 | os.rmdir(file_dir) 99 | except OSError as ex: 100 | break 101 | file_dir = os.path.split(file_dir)[0] 102 | -------------------------------------------------------------------------------- /kobo/django/upload/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from kobo.django.django_version import django_version_ge 4 | if django_version_ge("2.0"): 5 | from django.urls import re_path as url 6 | 7 | else: 8 | from django.conf.urls import url 9 | import kobo.django.upload.views 10 | 11 | 12 | urlpatterns = [ 13 | url(r"^$", kobo.django.upload.views.file_upload), 14 | ] 15 | -------------------------------------------------------------------------------- /kobo/django/upload/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import absolute_import 5 | import hashlib 6 | import tempfile 7 | import shutil 8 | import os 9 | import datetime 10 | 11 | from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseForbidden, HttpResponseServerError 12 | from django.views.decorators.csrf import csrf_exempt 13 | from kobo.decorators import well_behaved 14 | 15 | from .models import UPLOAD_STATES, FileUpload 16 | 17 | 18 | @well_behaved 19 | def catch_exceptions(old_view): 20 | """Catch exceptions in a view and return ServerError with exception text.""" 21 | def new_view(*args, **kwargs): 22 | try: 23 | return old_view(*args, **kwargs) 24 | except Exception as ex: 25 | return HttpResponseServerError(str(ex)) 26 | return new_view 27 | 28 | 29 | @csrf_exempt 30 | @catch_exceptions 31 | def file_upload(request): 32 | if request.method != "POST": 33 | return HttpResponseNotAllowed(["POST"]) 34 | 35 | upload_id = request.POST.get("upload_id") 36 | upload_key = request.POST.get("upload_key") 37 | fu = request.FILES["file"] 38 | 39 | try: 40 | upload = FileUpload.objects.get(id=upload_id, upload_key=upload_key) 41 | except: 42 | return HttpResponseForbidden(b"Not allowed to upload the file.") 43 | 44 | upload_path = os.path.join(upload.target_dir, upload.name) 45 | 46 | if os.path.isfile(upload_path): 47 | upload.state = UPLOAD_STATES["FAILED"] 48 | upload.save() 49 | # remove file 50 | return HttpResponseServerError(b"File already exists.") 51 | 52 | # TODO: check size 53 | # don't re-upload FINISHED or STARTED 54 | 55 | tmp_dir = tempfile.mkdtemp() 56 | tmp_file_name = os.path.join(tmp_dir, upload.name) 57 | tmp_file = open(tmp_file_name, "wb") 58 | sum = hashlib.sha256() 59 | 60 | # transaction, commit save() 61 | upload.state = UPLOAD_STATES["STARTED"] 62 | upload.save() 63 | 64 | for chunk in fu.chunks(): 65 | tmp_file.write(chunk) 66 | sum.update(chunk) 67 | tmp_file.close() 68 | 69 | checksum = sum.hexdigest().lower() 70 | 71 | if checksum != upload.checksum.lower(): 72 | upload.state = UPLOAD_STATES["FAILED"] 73 | upload.save() 74 | # remove file 75 | return HttpResponseServerError(b"Checksum mismatch.") 76 | 77 | if not os.path.isdir(upload.target_dir): 78 | os.makedirs(upload.target_dir) 79 | 80 | shutil.move(tmp_file_name, upload.target_dir, copy_function=shutil.copy) 81 | shutil.rmtree(tmp_dir) 82 | 83 | upload.state = UPLOAD_STATES["FINISHED"] 84 | upload.dt_finished = datetime.datetime.now() 85 | upload.save() 86 | 87 | # upload.save can modify state if there is a race 88 | if upload.state == UPLOAD_STATES['FAILED']: 89 | return HttpResponseServerError(b"File already exists.") 90 | 91 | return HttpResponse(b"Upload finished.") 92 | -------------------------------------------------------------------------------- /kobo/django/upload/xmlrpc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | from django.conf import settings 7 | 8 | from .models import FileUpload 9 | from kobo.django.xmlrpc.decorators import login_required 10 | 11 | 12 | __all__ = ( 13 | "register_upload", 14 | "delete_upload", 15 | ) 16 | 17 | 18 | @login_required 19 | def register_upload(request, name, checksum, size, target_dir): 20 | upload_dir = getattr(settings, "UPLOAD_DIR", None) 21 | if upload_dir is not None: 22 | target_dir = os.path.join(upload_dir, target_dir) 23 | if not target_dir.startswith(upload_dir): 24 | raise RuntimeError("Target directory (%s) is outside upload dir: %s" % (target_dir, upload_dir)) 25 | 26 | upload = FileUpload() 27 | upload.owner = request.user 28 | upload.name = name 29 | upload.checksum = checksum.lower() 30 | # size may be sent as a string to workaround the xmlrpc.client.MAXINT limit 31 | upload.size = int(size) 32 | upload.target_dir = target_dir 33 | upload.save() 34 | return (upload.id, upload.upload_key) 35 | 36 | @login_required 37 | def delete_upload(request, upload_id): 38 | try: 39 | FileUpload.objects.get(id = upload_id).delete() 40 | return True 41 | except: 42 | return False 43 | -------------------------------------------------------------------------------- /kobo/django/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/views/__init__.py -------------------------------------------------------------------------------- /kobo/django/xmlrpc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/xmlrpc/__init__.py -------------------------------------------------------------------------------- /kobo/django/xmlrpc/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import absolute_import 5 | import django.contrib.admin as admin 6 | 7 | from .models import XmlRpcLog 8 | 9 | 10 | admin.site.register(XmlRpcLog) 11 | -------------------------------------------------------------------------------- /kobo/django/xmlrpc/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import datetime 5 | import base64 6 | import socket 7 | 8 | import django.contrib.auth 9 | from django.conf import settings 10 | from django.contrib.auth.backends import ModelBackend 11 | from django.core.exceptions import PermissionDenied 12 | from django.contrib.sessions.models import Session 13 | 14 | from kobo.django.auth.krb5 import Krb5RemoteUserBackend 15 | from kobo.django.helpers import call_if_callable 16 | 17 | 18 | __all__ = ( 19 | "renew_session", 20 | "login_krbv", 21 | "login_password", 22 | "logout", 23 | ) 24 | 25 | 26 | def renew_session(request): 27 | """renew_session(): bool 28 | 29 | Renew current session. Return True if session is no longer valid and user should log in. 30 | """ 31 | 32 | request.session.modified = True 33 | return not call_if_callable(request.user.is_authenticated) 34 | 35 | 36 | def login_password(request, username, password): 37 | """login_password(username, password): session_id""" 38 | backend = ModelBackend() 39 | user = backend.authenticate(None, username, password) 40 | if user is None: 41 | raise PermissionDenied("Invalid username or password.") 42 | user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) 43 | django.contrib.auth.login(request, user) 44 | return request.session.session_key 45 | 46 | 47 | # TODO: proxy_user 48 | def login_krbv(request, krb_request, proxy_user=None): 49 | """login_krbv(krb_request, proxy_user=None): session_key""" 50 | import krbV 51 | 52 | context = krbV.default_context() 53 | server_principal = krbV.Principal(name=settings.KRB_AUTH_PRINCIPAL, context=context) 54 | server_keytab = krbV.Keytab(name=settings.KRB_AUTH_KEYTAB, context=context) 55 | 56 | auth_context = krbV.AuthContext(context=context) 57 | auth_context.flags = krbV.KRB5_AUTH_CONTEXT_DO_SEQUENCE | krbV.KRB5_AUTH_CONTEXT_DO_TIME 58 | auth_context.addrs = (socket.gethostbyname(request.META["HTTP_HOST"]), 0, request.META["REMOTE_ADDR"], 0) 59 | 60 | # decode and read the authentication request 61 | decode_func = base64.decodebytes if hasattr(base64, "decodebytes") else base64.decodestring 62 | decoded_request = decode_func(krb_request) 63 | auth_context, opts, server_principal, cache_credentials = context.rd_req(decoded_request, server=server_principal, keytab=server_keytab, auth_context=auth_context, options=krbV.AP_OPTS_MUTUAL_REQUIRED) 64 | cprinc = cache_credentials[2] 65 | 66 | # remove @REALM 67 | username = cprinc.name.split("@")[0] 68 | backend = Krb5RemoteUserBackend() 69 | user = backend.authenticate(None, username) 70 | if user is None: 71 | raise PermissionDenied() 72 | user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) 73 | django.contrib.auth.login(request, user) 74 | return request.session.session_key 75 | 76 | 77 | def logout(request): 78 | """logout() 79 | 80 | Delete session information. 81 | """ 82 | django.contrib.auth.logout(request) 83 | -------------------------------------------------------------------------------- /kobo/django/xmlrpc/dispatcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # Based on http://code.djangoproject.com/wiki/XML-RPC 5 | # 6 | # Credits: 7 | # Brendan W. McAdams 8 | 9 | 10 | import sys 11 | import six.moves.xmlrpc_client as xmlrpclib 12 | from six.moves.xmlrpc_server import SimpleXMLRPCDispatcher 13 | 14 | from django.conf import settings 15 | 16 | 17 | __all__ = ( 18 | 'DjangoXMLRPCDispatcher', 19 | ) 20 | 21 | 22 | class DjangoXMLRPCDispatcher(SimpleXMLRPCDispatcher): 23 | def __init__(self, allow_none=True, encoding=None): 24 | SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) 25 | 26 | self.allow_none = allow_none 27 | self.encoding = encoding 28 | self.register_multicall_functions() 29 | 30 | 31 | def system_multicall(self, request, call_list): 32 | for call in call_list: 33 | # insert request to each param list 34 | call['params'] = [request] + call['params'] 35 | 36 | return SimpleXMLRPCDispatcher.system_multicall(self, call_list) 37 | 38 | 39 | def register_module(self, module_name, function_prefix): 40 | """register all the public functions in a module with prefix prepended""" 41 | 42 | if type(module_name) is str: 43 | module = __import__(module_name, {}, {}, [""]) 44 | else: 45 | module = module_name 46 | 47 | if hasattr(module, '__all__'): 48 | fn_list = module.__all__ 49 | else: 50 | fn_list = dir(module) 51 | 52 | for fn in fn_list: 53 | if fn.startswith("_"): 54 | continue 55 | 56 | function = getattr(module, fn) 57 | if not callable(function): 58 | continue 59 | 60 | name = fn 61 | if function_prefix: 62 | name = "%s.%s" % (function_prefix, name) 63 | name = name.replace("__", ".") 64 | 65 | self.register_function(function, name) 66 | 67 | 68 | def _marshaled_dispatch(self, request, dispatch_method = None): 69 | """Dispatches an XML-RPC method from marshalled (XML) data. 70 | 71 | XML-RPC methods are dispatched from the marshalled (XML) data 72 | using the _dispatch method and the result is returned as 73 | marshalled data. For backwards compatibility, a dispatch 74 | function can be provided as an argument (see comment in 75 | SimpleXMLRPCRequestHandler.do_POST) but overriding the 76 | existing method through subclassing is the prefered means 77 | of changing method dispatch behavior. 78 | """ 79 | 80 | data = request.body 81 | params, method = xmlrpclib.loads(data) 82 | 83 | # add request to params 84 | params = (request, ) + params 85 | 86 | # generate response 87 | try: 88 | if dispatch_method is not None: 89 | response = dispatch_method(method, params) 90 | else: 91 | response = self._dispatch(method, params) 92 | # wrap response in a singleton tuple 93 | response = (response,) 94 | response = xmlrpclib.dumps(response, methodresponse=1, allow_none=self.allow_none, encoding=self.encoding) 95 | 96 | except xmlrpclib.Fault as fault: 97 | response = xmlrpclib.dumps(fault, allow_none=self.allow_none, encoding=self.encoding) 98 | 99 | except: 100 | # report exception back to server 101 | if settings.DEBUG: 102 | from kobo.tback import Traceback 103 | response = xmlrpclib.dumps( 104 | xmlrpclib.Fault(1, u"%s" % Traceback().get_traceback()), 105 | allow_none=self.allow_none, encoding=self.encoding) 106 | else: 107 | exc_info = sys.exc_info()[1] 108 | exc_type = exc_info.__class__.__name__ 109 | response = xmlrpclib.dumps( 110 | xmlrpclib.Fault(1, "%s: %s" % (exc_type, exc_info)), 111 | allow_none=self.allow_none, encoding=self.encoding) 112 | 113 | return response 114 | -------------------------------------------------------------------------------- /kobo/django/xmlrpc/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='XmlRpcLog', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('dt_inserted', models.DateTimeField(auto_now_add=True)), 20 | ('method', models.CharField(max_length=255)), 21 | ('args', models.TextField(blank=True)), 22 | ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, 23 | on_delete=models.SET_NULL)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /kobo/django/xmlrpc/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/django/xmlrpc/migrations/__init__.py -------------------------------------------------------------------------------- /kobo/django/xmlrpc/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from django.db import models 5 | from django.conf import settings 6 | import six 7 | 8 | 9 | @six.python_2_unicode_compatible 10 | class XmlRpcLog(models.Model): 11 | dt_inserted = models.DateTimeField(auto_now_add=True) 12 | user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, 13 | on_delete=models.SET_NULL) 14 | method = models.CharField(max_length=255) 15 | args = models.TextField(blank=True) 16 | 17 | def __str__(self): 18 | return u"%s: %s" % (self.user, self.method) 19 | -------------------------------------------------------------------------------- /kobo/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ImproperlyConfigured(Exception): 5 | """Program is improperly configured.""" 6 | pass 7 | 8 | 9 | class ShutdownException(Exception): 10 | """Shutdown currently running program.""" 11 | pass 12 | 13 | 14 | class AuthenticationError(Exception): 15 | """Authentication failed for some reason.""" 16 | pass 17 | -------------------------------------------------------------------------------- /kobo/hub/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import os 5 | 6 | from kobo.exceptions import ImproperlyConfigured 7 | from django.conf import settings 8 | 9 | 10 | for var in ["XMLRPC_METHODS", "TASK_DIR", "UPLOAD_DIR"]: 11 | if not hasattr(settings, var): 12 | raise ImproperlyConfigured("'%s' is missing in project settings. It must be set to run kobo.hub app." % var) 13 | 14 | 15 | if not hasattr(settings, "WORKER_DIR"): 16 | # This setting introduced in 2021 can be defaulted to ensure backwards compatibility 17 | # with existing config files. 18 | worker_dir = os.path.join(os.path.dirname(settings.TASK_DIR), 'worker') 19 | setattr(settings, "WORKER_DIR", worker_dir) 20 | 21 | 22 | for var in ["TASK_DIR", "UPLOAD_DIR", "WORKER_DIR"]: 23 | dir_path = getattr(settings, var) 24 | if not os.path.isdir(dir_path): 25 | try: 26 | os.makedirs(dir_path) 27 | except: 28 | raise ImproperlyConfigured("'%s' doesn't exist and can't be automatically created." % dir_path) 29 | elif not os.access(dir_path, os.R_OK | os.W_OK | os.X_OK): 30 | raise ImproperlyConfigured("Invalid permissions on '%s'." % dir_path) 31 | 32 | 33 | if hasattr(settings, "USERS_ACL_PERMISSION"): 34 | acl_permission = getattr(settings, "USERS_ACL_PERMISSION") 35 | valid_options = ["", "authenticated", "staff"] 36 | if acl_permission not in valid_options: 37 | raise ImproperlyConfigured( 38 | f"Invalid USERS_ACL_PERMISSION in settings: '{acl_permission}', must be one of " 39 | f"'authenticated', 'staff' or ''(empty string)" 40 | ) 41 | 42 | 43 | if getattr(settings, "MIDDLEWARE", None) is not None: 44 | # Settings defines Django>=1.10 style middleware, check that 45 | middleware_var = "MIDDLEWARE" 46 | else: 47 | # Legacy 48 | middleware_var = "MIDDLEWARE_CLASSES" 49 | 50 | for var, value in [(middleware_var, "kobo.hub.middleware.WorkerMiddleware")]: 51 | if value not in (getattr(settings, var, None) or []): 52 | raise ImproperlyConfigured("'%s' in '%s' is missing in project settings. It must be set to run kobo.hub app." % (value, var)) 53 | -------------------------------------------------------------------------------- /kobo/hub/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import absolute_import 5 | import django.contrib.admin as admin 6 | 7 | from .models import Arch, Channel, Task, Worker 8 | 9 | 10 | class ArchAdmin(admin.ModelAdmin): 11 | list_display = ("name", "pretty_name") 12 | search_fields = ("name", "pretty_name") 13 | 14 | class ChannelAdmin(admin.ModelAdmin): 15 | search_fields = ("name",) 16 | 17 | class TaskAdmin(admin.ModelAdmin): 18 | list_display = ("id", "method", "label", "state", "owner", "dt_created", "dt_finished", "time", "arch", "channel") 19 | list_filter = ("method", "state", "priority", "arch") 20 | search_fields = ("id", "method", "label", "owner__username", "dt_created", "dt_finished") 21 | raw_id_fields = ("parent", "owner", "resubmitted_by", "resubmitted_from") 22 | 23 | class WorkerAdmin(admin.ModelAdmin): 24 | list_display = ("name", "enabled", "ready", "max_load", "current_load", "task_count","min_priority") 25 | list_filter = ("enabled", "ready") 26 | search_fields = ("name",) 27 | 28 | admin.site.register(Arch, ArchAdmin) 29 | admin.site.register(Channel, ChannelAdmin) 30 | admin.site.register(Task, TaskAdmin) 31 | admin.site.register(Worker, WorkerAdmin) 32 | -------------------------------------------------------------------------------- /kobo/hub/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import socket 5 | 6 | from django.core.exceptions import PermissionDenied, SuspiciousOperation 7 | from kobo.decorators import decorator_with_args 8 | from kobo.django.helpers import call_if_callable 9 | from kobo.django.xmlrpc.decorators import * 10 | 11 | 12 | def validate_worker(func): 13 | def _new_func(request, *args, **kwargs): 14 | if not call_if_callable(request.user.is_authenticated): 15 | raise PermissionDenied("Login required.") 16 | 17 | if getattr(request, 'worker', None) is None: 18 | raise SuspiciousOperation("User doesn't match any worker: %s" % request.user.username) 19 | 20 | request.worker.update_last_seen() 21 | 22 | return func(request, *args, **kwargs) 23 | 24 | _new_func.__name__ = func.__name__ 25 | _new_func.__doc__ = func.__doc__ 26 | _new_func.__dict__.update(func.__dict__) 27 | return _new_func 28 | -------------------------------------------------------------------------------- /kobo/hub/fixtures/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | 4 | { 5 | "pk": "1", 6 | "model": "hub.arch", 7 | "fields": { 8 | "name": "noarch", 9 | "pretty_name": "noarch" 10 | } 11 | }, 12 | { 13 | "pk": "2", 14 | "model": "hub.arch", 15 | "fields": { 16 | "name": "i386", 17 | "pretty_name": "i386" 18 | } 19 | }, 20 | { 21 | "pk": "3", 22 | "model": "hub.arch", 23 | "fields": { 24 | "name": "x86_64", 25 | "pretty_name": "x86_64" 26 | } 27 | }, 28 | { 29 | "pk": "4", 30 | "model": "hub.arch", 31 | "fields": { 32 | "name": "ppc", 33 | "pretty_name": "ppc" 34 | } 35 | }, 36 | { 37 | "pk": "5", 38 | "model": "hub.arch", 39 | "fields": { 40 | "name": "ppc64", 41 | "pretty_name": "ppc64" 42 | } 43 | }, 44 | { 45 | "pk": "6", 46 | "model": "hub.arch", 47 | "fields": { 48 | "name": "s390", 49 | "pretty_name": "s390" 50 | } 51 | }, 52 | { 53 | "pk": "7", 54 | "model": "hub.arch", 55 | "fields": { 56 | "name": "s390x", 57 | "pretty_name": "s390x" 58 | } 59 | }, 60 | { 61 | "pk": "8", 62 | "model": "hub.arch", 63 | "fields": { 64 | "name": "ia64", 65 | "pretty_name": "ia64" 66 | } 67 | }, 68 | 69 | 70 | { 71 | "pk": "1", 72 | "model": "hub.channel", 73 | "fields": { 74 | "name": "default" 75 | } 76 | }, 77 | 78 | 79 | { 80 | "pk": "1", 81 | "model": "hub.worker", 82 | "fields": { 83 | "name": "localhost.localdomain", 84 | "enabled": true, 85 | "max_load": 2, 86 | "max_tasks": 0, 87 | "ready": true, 88 | "current_load": 0, 89 | "task_count": 0, 90 | "worker_key": "jItBFo7n9qPimGMvCVpx2vhS2vAcYypHKqxIDtumD8GMYeFAK4Wp758rSYsLfoQz", 91 | "arches": [1], 92 | "channels": [1] 93 | } 94 | } 95 | 96 | 97 | ] 98 | -------------------------------------------------------------------------------- /kobo/hub/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import django.forms as forms 5 | from django.db.models import Q 6 | from kobo.django.helpers import call_if_callable 7 | from kobo.hub.models import Task 8 | 9 | 10 | class TaskSearchForm(forms.Form): 11 | search = forms.CharField(required=False) 12 | my = forms.BooleanField(required=False) 13 | 14 | def __init__(self, *args, **kwargs): 15 | self.state = kwargs.pop('state', None) 16 | self.order_by = kwargs.pop('order_by', ['-id']) 17 | return super(TaskSearchForm, self).__init__(*args, **kwargs) 18 | 19 | def get_query(self, request): 20 | self.is_valid() 21 | search = self.cleaned_data["search"] 22 | my = self.cleaned_data["my"] 23 | 24 | query = Q() 25 | 26 | if search: 27 | query |= Q(method__icontains=search) 28 | query |= Q(owner__username__icontains=search) 29 | query |= Q(label__icontains=search) 30 | 31 | if my and call_if_callable(request.user.is_authenticated): 32 | query &= Q(owner=request.user) 33 | 34 | if self.state is not None: 35 | query &= Q(state__in=self.state) 36 | #if self.kwargs: 37 | # query &= Q(self.kwargs) 38 | return Task.objects.filter(parent__isnull=True).filter(query).order_by(*self.order_by).defer("result", "args").select_related("owner", "worker") 39 | -------------------------------------------------------------------------------- /kobo/hub/menu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.django.menu import MenuItem 5 | 6 | 7 | menu = ( 8 | MenuItem("Arches", "arch/list"), 9 | MenuItem("Channels", "channel/list"), 10 | MenuItem("Users", "user/list"), 11 | MenuItem("Workers", "worker/list"), 12 | ) 13 | -------------------------------------------------------------------------------- /kobo/hub/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import 4 | 5 | from django.utils.deprecation import MiddlewareMixin 6 | 7 | from .models import Worker 8 | 9 | 10 | def get_worker(request): 11 | try: 12 | if "/" not in request.user.username: 13 | return None 14 | 15 | hostname = request.user.username.split("/")[1] 16 | worker = Worker.objects.get(name=hostname) 17 | return worker 18 | except: 19 | return None 20 | 21 | 22 | class LazyWorker(object): 23 | def __get__(self, request, obj_type=None): 24 | if not hasattr(request, "_cached_worker"): 25 | request._cached_worker = get_worker(request) 26 | return request._cached_worker 27 | 28 | 29 | class WorkerMiddleware(MiddlewareMixin): 30 | """Sets a request.worker. 31 | 32 | - Worker instance if username exists in database 33 | - None otherwise 34 | """ 35 | 36 | def process_request(self, request): 37 | assert hasattr(request, "user"), "Worker middleware requires authentication middleware to be installed. Also make sure the database is set and writable." 38 | request.__class__.worker = LazyWorker() 39 | return None 40 | -------------------------------------------------------------------------------- /kobo/hub/migrations/0002_auto_20150722_0612.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('hub', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='task', 18 | name='owner', 19 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), 20 | ), 21 | migrations.AddField( 22 | model_name='task', 23 | name='parent', 24 | field=models.ForeignKey(blank=True, to='hub.Task', help_text='Parent task.', 25 | on_delete=models.CASCADE, null=True), 26 | ), 27 | migrations.AddField( 28 | model_name='task', 29 | name='resubmitted_by', 30 | field=models.ForeignKey(related_name='resubmitted_by1', blank=True, 31 | to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True), 32 | ), 33 | migrations.AddField( 34 | model_name='task', 35 | name='resubmitted_from', 36 | field=models.ForeignKey(related_name='resubmitted_from1', 37 | blank=True, to='hub.Task', on_delete=models.CASCADE, null=True), 38 | ), 39 | migrations.AddField( 40 | model_name='task', 41 | name='worker', 42 | field=models.ForeignKey(blank=True, to='hub.Worker', 43 | help_text='A worker which has this task assigned.', 44 | on_delete=models.CASCADE, null=True), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /kobo/hub/migrations/0003_auto_20160202_0647.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('hub', '0002_auto_20150722_0612'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='task', 16 | options={'ordering': ('-id',), 'permissions': (('can_see_traceback', 'Can see traceback'),)}, 17 | ), 18 | migrations.AddField( 19 | model_name='worker', 20 | name='min_priority', 21 | field=models.PositiveIntegerField(default=0, help_text='Worker will take only tasks of this or higher priority.'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /kobo/hub/migrations/0004_alter_task_worker.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.10 on 2023-08-30 08:41 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('hub', '0003_auto_20160202_0647'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='task', 16 | name='worker', 17 | field=models.ForeignKey(blank=True, help_text='A worker which has this task assigned.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hub.worker'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /kobo/hub/migrations/0005_add_task_canceller.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('hub', '0004_alter_task_worker'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='task', 16 | name='canceled_by', 17 | field=models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /kobo/hub/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/migrations/__init__.py -------------------------------------------------------------------------------- /kobo/hub/sql/task.postgresql.sql: -------------------------------------------------------------------------------- 1 | create index hub_task_archive_state on hub_task (state, archive) where archive=False; 2 | analyze; 3 | -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-first-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-first-disabled.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-first.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-last-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-last-disabled.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-last.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-next-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-next-disabled.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-next.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-prev-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-prev-disabled.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/img/list-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/static/kobo/img/list-prev.png -------------------------------------------------------------------------------- /kobo/hub/static/kobo/js/log_watcher.js: -------------------------------------------------------------------------------- 1 | // to start a log watcher, include this script and use following code: 2 | // document.log_watcher = new LogWatcher(json_url, offset, task_finished); 3 | // document.log_watcher.watch(); 4 | 5 | function getAjax() { 6 | try { return new XMLHttpRequest(); } catch (e) {} 7 | try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) {} 8 | try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} 9 | } 10 | 11 | function getElementById(id) { 12 | try { return document.getElementById(id); } catch(e) {} 13 | try { return document.all[id]; } catch(e) {} 14 | try { return document.layers[id]; } catch(e) {} 15 | } 16 | 17 | function GET_handler(log_watcher) { 18 | if (this.readyState == 4 && this.status == 200) { 19 | result = eval("(" + this.responseText + ")"); 20 | getElementById('log').innerHTML += result.content; 21 | document.log_watcher.task_finished = result.task_finished 22 | document.log_watcher.offset = result.new_offset 23 | if ((window.pageYOffset + window.innerHeight) >= document.log_watcher.page_height) { 24 | window.scroll(window.pageXOffset, document.body.clientHeight); 25 | document.log_watcher.page_height = document.body.clientHeight; 26 | } 27 | } 28 | } 29 | 30 | function doWatch() { 31 | var need_poll = (!document.log_watcher.task_finished || document.log_watcher.next_poll != null); 32 | if (!need_poll) { 33 | return; 34 | } 35 | client = getAjax(); 36 | client.onreadystatechange = GET_handler; 37 | client.open('GET', document.log_watcher.json_url + '?offset=' + document.log_watcher.offset); 38 | client.send(); 39 | setTimeout(doWatch, document.log_watcher.next_poll || 5000); 40 | } 41 | 42 | function LogWatcher(json_url, offset, task_finished, next_poll) { 43 | this.json_url = json_url; 44 | this.offset = offset; 45 | this.task_finished = task_finished; 46 | this.next_poll = next_poll; 47 | this.page_height = 0; 48 | return this; 49 | } 50 | 51 | LogWatcher.prototype.watch = function() { 52 | doWatch(); 53 | } 54 | -------------------------------------------------------------------------------- /kobo/hub/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | 404 {% trans "Page not found" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Page not found" %} (404)

10 |

{% trans "We're sorry, but the requested page could not be found." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /kobo/hub/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block head %} 5 | 500 {% trans "Internal Server Error" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Internal Server Error" %} (500)

10 |

{% trans "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /kobo/hub/templates/arch/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Arch' %} #{{ arch.id }}: {{ arch.name }}

6 | 7 |

{% trans 'Details' %}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
{% trans "ID" %}{{ arch.id }}
{% trans "Name" %}{{ arch.name }}
18 | 19 |

{% trans 'Workers' %}

20 | {% include "worker/list_include.html" %} 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /kobo/hub/templates/arch/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans "Arch list" %}

6 | 7 |
8 | {% include "pagination.html" %} 9 | {% include "arch/list_include.html" %} 10 | {% include "pagination.html" %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /kobo/hub/templates/arch/list_include.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% for arch in arch_list %} 9 | 10 | 11 | 12 | 13 | {% endfor %} 14 |
{% trans "Name" %}{% trans "Workers" %}
{{ arch.name }}{{ arch.worker_count }}
15 | -------------------------------------------------------------------------------- /kobo/hub/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans "Login" %}

6 | {% if form.errors %} 7 |

{% trans "Your username and password didn't match. Please try again." %}

8 | {% endif %} 9 | 10 |
11 | {% csrf_token %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
22 | 23 | 24 | 25 |
26 | 27 | {% endblock %} 28 | 29 | -------------------------------------------------------------------------------- /kobo/hub/templates/base.html.example: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block header_site_name %} 10 | site/tool name 11 | {% endblock %} 12 | 13 | {% block header_login %} 14 | {% endblock %} 15 | 16 | {% block footer %} 17 | email, copyright, ... 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /kobo/hub/templates/channel/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Channel' %} #{{ channel.id }}: {{ channel.name }}

6 | 7 |

{% trans 'Details' %}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
{% trans "ID" %}{{ channel.id }}
{% trans "Name" %}{{ channel.name }}
18 | 19 |

{% trans 'Workers' %}

20 | {% include "worker/list_include.html" %} 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /kobo/hub/templates/channel/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans "Channel list" %}

6 | 7 |
8 | {% include "pagination.html" %} 9 | {% include "channel/list_include.html" %} 10 | {% include "pagination.html" %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /kobo/hub/templates/channel/list_include.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% for channel in channel_list %} 9 | 10 | 11 | 12 | 13 | {% endfor %} 14 |
{% trans "Name" %}{% trans "Workers" %}
{{ channel.name }}{{ channel.worker_count }}
15 | -------------------------------------------------------------------------------- /kobo/hub/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | {% if title %}{{ title }}{% endif %} 8 | 9 | 10 | {% block css %} 11 | {% endblock %} 12 | {% block head %} 13 | {% endblock %} 14 | 15 | 16 | 17 | 18 | 31 | 32 | 53 | 54 |
55 | {% if error_message %}
{{ error_message }}
{% endif %} 56 | {% if info_message %}
{{ info_message }}
{% endif %} 57 | {% block content %} 58 | {% endblock %} 59 |
60 | 61 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /kobo/hub/templates/pagination.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load static %} 3 | 4 | {% if page_obj.has_other_pages %} 5 |
6 | {% trans "Page" %} {{ page_obj.number }}/{{ paginator.num_pages }}. {% trans "Records" %} {{ page_obj.start_index }} - {{ page_obj.end_index }} {% trans "of" %} {{ paginator.count }}. 7 | {% if page_obj.has_previous %} 8 | 9 | 10 | {% else %} 11 | 12 | 13 | {% endif %} 14 | {% if page_obj.has_next %} 15 | 16 | 17 | {% else %} 18 | 19 | 20 | {% endif %} 21 |
22 |
23 | {% endif %} 24 | -------------------------------------------------------------------------------- /kobo/hub/templates/task/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Task' %} #{{ task.id }}: {{ task.method }}

6 | 7 |

{% trans 'Details' %}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% if task.state == "CANCELED" %} 31 | 32 | 33 | {% if task.canceled_by %} 34 | 35 | {% else %} 36 | 37 | {% endif %} 38 | 39 | {% endif %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% if task.resubmitted_by %} 75 | 76 | 77 | 78 | 79 | {% endif %} 80 | {% if task.resubmitted_from %} 81 | 82 | 83 | 84 | 85 | {% endif %} 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
{% trans "ID" %}{{ task.id }}
{% trans "Method" %}{{ task.method }}
{% trans "Args" %}{% for arg in task.get_args_display.items %}{{ arg.0 }}: {{ arg.1 }}
{% endfor %}
{% trans "Label" %}{{ task.label }}
{% trans "State" %}{{ task.get_state_display }}
{% trans "Cancelled by" %}{{ task.canceled_by }}Unavailable
{% trans "Worker" %}{% if task.worker %}{{ task.worker }}{% endif %}
{% trans "Channel" %}{{ task.channel }}
{% trans "Arch" %}{{ task.arch }}
{% trans "Exclusive" %}{{ task.exclusive }}
{% trans "Priority" %}{{ task.priority }}
{% trans "Waiting" %}{{ task.waiting }}
{% trans "Awaited" %}{{ task.awaited }}
{% trans "Owner" %}{{ task.owner }}
{% trans "Resubmitted by" %}{{ task.resubmitted_by }}
{% trans "Resubmitted from" %}#{{ task.resubmitted_from.id }} task
{% trans "Created" %}{% if task.dt_created %}{{ task.dt_created|date:"Y-m-d H:i:s" }}{% endif %}
{% trans "Started" %}{% if task.dt_started %}{{ task.dt_started|date:"Y-m-d H:i:s" }}{% endif %}
{% trans "Finished" %}{% if task.dt_finished %}{{ task.dt_finished|date:"Y-m-d H:i:s" }}{% endif %}
{% trans "Spent time" %}{{ task.get_time_display }}
{% trans "Comment" %}{% if task.comment %}{{ task.comment }}{% endif %}
107 | 108 | 109 |

{% trans 'Result' %}

110 |
111 | {{ task.result }}
112 | 
113 | 114 | 115 | {% if logs %} 116 |

{% trans 'Logs' %}

117 | 122 | {% endif %} 123 | 124 | 125 | {% if task_list %} 126 |

{% trans 'Subtask list' %}

127 | {% include "task/list_include.html" %} 128 | {% endif %} 129 | 130 | {% endblock %} 131 | -------------------------------------------------------------------------------- /kobo/hub/templates/task/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans "Tasks" %}

6 | 7 |
8 | {{ form.search }} 9 | 10 | {% if user.is_authenticated %}
{{ form.my }} {% endif %} 11 |
12 | 13 |
14 | {% include "pagination.html" %} 15 | {% include "task/list_include.html" %} 16 | {% include "pagination.html" %} 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /kobo/hub/templates/task/list_include.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for task in task_list %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 |
{% trans "ID" %}{% trans "Method" %}{% trans "Owner" %}{% trans "State" %}{% trans "Label" %}{% trans "Worker" %}{% trans "Created" %}{% trans "Finished" %}{% trans "Subtask #" %}
{{ task.id }}{{ task.method }}{{ task.owner }}{{ task.get_state_display }}{{ task.label }}{% if task.worker %}{{ task.worker }}{% endif %}{{ task.dt_created|date:"Y-m-d H:i:s" }}{{ task.dt_finished|date:"Y-m-d H:i:s" }}{{ task.subtask_count }}
29 | -------------------------------------------------------------------------------- /kobo/hub/templates/task/log.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% block head %} 6 | 7 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

{% trans 'Task' %} #{{ task.id }} - {{ log_name }}

15 | {% trans "back to task" %} #{{ task.id }}
16 | {% trans 'download' %} 17 |
{{ content }}
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /kobo/hub/templates/user/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | 5 | {% block content %} 6 |

{% trans 'User' %} #{{ usr.id }}: {{ usr.username }}

7 | 8 |

{% trans 'Details' %}

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
{% trans "ID" %}{{ usr.id }}
{% trans "Name" %}{{ usr.username }}
{% trans "Admin" %}{% if usr.is_superuser %}YES{% else %}NO{% endif %}
{% trans "Staff" %}{% if usr.is_staff %}YES{% else %}NO{% endif %}
{% trans "Active" %}{% if usr.is_active %}YES{% else %}NO{% endif %}
{% trans "Tasks" %}{{ tasks }}
35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /kobo/hub/templates/user/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans "User list" %}

6 | 7 |
8 | {% include "pagination.html" %} 9 | {% include "user/list_include.html" %} 10 | {% include "pagination.html" %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /kobo/hub/templates/user/list_include.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for usr in usr_list %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% endfor %} 18 |
{% trans "Name" %}{% trans "Admin" %}{% trans "Staff" %}{% trans "Active" %}
{{ usr.username }}{% if usr.is_superuser %}{% trans 'YES' %}{% else %}{% trans 'NO' %}{% endif %}{% if usr.is_staff %}{% trans 'YES' %}{% else %}{% trans 'NO' %}{% endif %}{% if usr.is_active %}{% trans 'YES' %}{% else %}{% trans 'NO' %}{% endif %}
19 | -------------------------------------------------------------------------------- /kobo/hub/templates/worker/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans 'Worker' %} #{{ worker.id }}: {{ worker.name }}

6 | 7 |

{% trans 'Details' %}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
{% trans "ID" %}{{ worker.id }}
{% trans "Name" %}{{ worker.name }}
{% trans "Arches" %}{{ worker.arches.all | join:" " }}
{% trans "Load" %}{{ worker.current_load }} / {{ worker.max_load }}
{% trans "Tasks" %}{{ worker.task_count }} / {{ worker.max_tasks | default:"unlimited" }}
{% trans "Enabled" %}{% if worker.enabled %}YES{% else %}NO{% endif %}
{% trans "Ready" %}{% if worker.ready %}YES{% else %}NO{% endif %}
{% trans "Channels" %}{{ worker.channels.all | join:" " }}
42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /kobo/hub/templates/worker/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% trans "Worker list" %}

6 | 7 |
8 | {% include "pagination.html" %} 9 | {% include "worker/list_include.html" %} 10 | {% include "pagination.html" %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /kobo/hub/templates/worker/list_include.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for worker in worker_list %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 |
{% trans "Name" %}{% trans "Arches" %}{% trans "Enabled" %}{% trans "Ready" %}{% trans "Tasks" %}
{{ worker.name }}{{ worker.arches.all|join:" " }}{% if worker.enabled %}{% trans 'YES' %}{% else %}{% trans 'NO' %}{% endif %}{% if worker.ready %}{% trans 'YES' %}{% else %}{% trans 'NO' %}{% endif %}{{ worker.task_count }} / {{ worker.max_tasks | default:"unlimited" }}
21 | -------------------------------------------------------------------------------- /kobo/hub/urls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/hub/urls/__init__.py -------------------------------------------------------------------------------- /kobo/hub/urls/arch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from kobo.django.django_version import django_version_ge 4 | if django_version_ge("2.0"): 5 | from django.urls import re_path as url 6 | 7 | else: 8 | from django.conf.urls import url 9 | from kobo.django.views.generic import ExtraListView 10 | from kobo.hub.views import ArchDetailView 11 | from kobo.hub.models import Arch 12 | from kobo.django.compat import gettext_lazy as _ 13 | 14 | 15 | urlpatterns = [ 16 | url(r"^$", ExtraListView.as_view( 17 | queryset=Arch.objects.order_by("name"), 18 | template_name="arch/list.html", 19 | context_object_name="arch_list", 20 | title=_("Architectures"), 21 | ), name="arch/list"), 22 | url(r"^(?P\d+)/$", ArchDetailView.as_view(), name="arch/detail"), 23 | ] 24 | -------------------------------------------------------------------------------- /kobo/hub/urls/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.django.django_version import django_version_ge 5 | if django_version_ge("2.0"): 6 | from django.urls import re_path as url 7 | 8 | else: 9 | from django.conf.urls import url 10 | import kobo.hub.views 11 | 12 | urlpatterns = [ 13 | url(r"^login/$", kobo.hub.views.LoginView.as_view(), name="auth/login"), 14 | url(r"^krb5login/$", kobo.hub.views.krb5login, name="auth/krb5login"), 15 | url(r"^oidclogin/$", kobo.hub.views.oidclogin, name="auth/oidclogin"), 16 | url(r"^tokenoidclogin/$", kobo.hub.views.oidclogin, name="auth/tokenoidclogin"), 17 | url(r'^logout/$', kobo.hub.views.LogoutView.as_view(), name="auth/logout"), 18 | ] 19 | -------------------------------------------------------------------------------- /kobo/hub/urls/channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from kobo.django.compat import gettext_lazy as _ 4 | from kobo.django.django_version import django_version_ge 5 | if django_version_ge("2.0"): 6 | from django.urls import re_path as url 7 | 8 | else: 9 | from django.conf.urls import url 10 | from kobo.django.views.generic import ExtraListView 11 | from kobo.hub.views import DetailViewWithWorkers 12 | from kobo.hub.models import Channel 13 | 14 | urlpatterns = [ 15 | url(r"^$", ExtraListView.as_view( 16 | queryset=Channel.objects.order_by("name"), 17 | template_name="channel/list.html", 18 | context_object_name="channel_list", 19 | title=_("Channels"), 20 | ), name="channel/list"), 21 | url(r"^(?P\d+)/$", DetailViewWithWorkers.as_view( 22 | model = Channel, 23 | template_name = "channel/detail.html", 24 | context_object_name = "channel", 25 | title = _("Channel detail"), 26 | ), name="channel/detail"), 27 | ] 28 | -------------------------------------------------------------------------------- /kobo/hub/urls/task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.django.django_version import django_version_ge 5 | if django_version_ge("2.0"): 6 | from django.urls import re_path as url 7 | 8 | else: 9 | from django.conf.urls import url 10 | from kobo.hub.models import TASK_STATES 11 | from kobo.hub.views import TaskListView, TaskDetail 12 | import kobo.hub.views 13 | from kobo.django.compat import gettext_lazy as _ 14 | 15 | 16 | urlpatterns = [ 17 | url(r"^$", TaskListView.as_view(), name="task/index"), 18 | url(r"^(?P\d+)/$", TaskDetail.as_view(), name="task/detail"), 19 | url(r"^running/$", TaskListView.as_view(state=(TASK_STATES["FREE"], TASK_STATES["ASSIGNED"], TASK_STATES["OPEN"]), title=_("Running tasks"), order_by=["id"]), name="task/running"), 20 | url(r"^failed/$", TaskListView.as_view(state=(TASK_STATES["FAILED"],), title=_("Failed tasks"), order_by=["-dt_created", "id"]), name="task/failed"), 21 | url(r"^finished/$", TaskListView.as_view(state=(TASK_STATES["CLOSED"], TASK_STATES["INTERRUPTED"], TASK_STATES["CANCELED"], TASK_STATES["FAILED"]), title=_("Finished tasks"), order_by=["-dt_created", "id"]), name="task/finished"), 22 | url(r"^(?P\d+)/log/(?P.+)$", kobo.hub.views.task_log, name="task/log"), 23 | url(r"^(?P\d+)/log-json/(?P.+)$", kobo.hub.views.task_log_json, name="task/log-json"), 24 | ] 25 | -------------------------------------------------------------------------------- /kobo/hub/urls/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from django.contrib.auth import get_user_model 5 | from kobo.django.django_version import django_version_ge 6 | if django_version_ge("2.0"): 7 | from django.urls import re_path as url 8 | 9 | else: 10 | from django.conf.urls import url 11 | from kobo.django.views.generic import UserListView 12 | from kobo.hub.views import UserDetailView 13 | from kobo.django.compat import gettext_lazy as _ 14 | 15 | 16 | urlpatterns = [ 17 | url(r"^$", UserListView.as_view( 18 | queryset=get_user_model().objects.order_by("username"), 19 | template_name="user/list.html", 20 | context_object_name="usr_list", 21 | title = _('Users'), 22 | ), name="user/list"), 23 | url(r"^(?P\d+)/$", UserDetailView.as_view(), name="user/detail"), 24 | ] 25 | -------------------------------------------------------------------------------- /kobo/hub/urls/worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.django.django_version import django_version_ge 5 | if django_version_ge("2.0"): 6 | from django.urls import re_path as url 7 | 8 | else: 9 | from django.conf.urls import url 10 | from kobo.django.views.generic import ExtraListView, ExtraDetailView 11 | from kobo.hub.models import Worker 12 | from kobo.django.compat import gettext_lazy as _ 13 | 14 | 15 | urlpatterns = [ 16 | url(r"^$", ExtraListView.as_view( 17 | queryset=Worker.objects.order_by("name"), 18 | template_name="worker/list.html", 19 | context_object_name="worker_list", 20 | title = _("Workers"), 21 | ), name="worker/list"), 22 | url(r"^(?P\d+)/$", ExtraDetailView.as_view( 23 | queryset=Worker.objects.select_related(), 24 | template_name="worker/detail.html", 25 | context_object_name="worker", 26 | title=_("Worker detail"), 27 | ), name="worker/detail"), 28 | ] 29 | -------------------------------------------------------------------------------- /kobo/hub/xmlrpc/__init__.py: -------------------------------------------------------------------------------- 1 | from kobo.django.django_version import django_version_ge 2 | 3 | if django_version_ge('3.2.0'): 4 | pass 5 | else: 6 | default_app_config = 'xmlrpc.apps.XmlrpcConfig' 7 | -------------------------------------------------------------------------------- /kobo/hub/xmlrpc/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class XmlrpcConfig(AppConfig): 4 | name = 'kobo.hub.xmlrpc' 5 | verbose_name = 'Kobo XMLRPC' 6 | -------------------------------------------------------------------------------- /kobo/hub/xmlrpc/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import datetime 5 | import base64 6 | import socket 7 | 8 | import django.contrib.auth 9 | from django.conf import settings 10 | from django.contrib.auth.backends import ModelBackend 11 | from django.core.exceptions import PermissionDenied 12 | from django.contrib.sessions.models import Session 13 | from django.core.exceptions import ObjectDoesNotExist 14 | 15 | from kobo.hub.models import Worker 16 | from kobo.django.auth.krb5 import Krb5RemoteUserBackend 17 | from kobo.django.xmlrpc.auth import * 18 | 19 | 20 | __all__ = ( 21 | "renew_session", 22 | "login_krbv", 23 | "login_password", 24 | "login_worker_key", 25 | "logout", 26 | ) 27 | 28 | 29 | def login_worker_key(request, worker_key): 30 | """login_worker_key(worker_key): session_key""" 31 | try: 32 | worker = Worker.objects.get(worker_key=worker_key) 33 | except ObjectDoesNotExist: 34 | raise PermissionDenied() 35 | 36 | username = "worker/%s" % worker.name 37 | backend = Krb5RemoteUserBackend() 38 | user = backend.authenticate(None, username) 39 | if user is None: 40 | raise PermissionDenied() 41 | user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) 42 | user = django.contrib.auth.login(request, user) 43 | return request.session.session_key 44 | -------------------------------------------------------------------------------- /kobo/hub/xmlrpc/system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | __all__ = ( 5 | "getAPIVersion", 6 | ) 7 | 8 | 9 | def getAPIVersion(request): 10 | return "0.1.0" 11 | -------------------------------------------------------------------------------- /kobo/notification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | """ 6 | Notification module. 7 | """ 8 | 9 | 10 | import smtplib 11 | import sys 12 | import optparse 13 | 14 | import kobo.shortcuts 15 | import six 16 | 17 | 18 | class EmailNotification(object): 19 | """Send notification e-mails.""" 20 | 21 | def __init__(self, smtp_host): 22 | # connect to SMTP server 23 | self.smtp_host = smtp_host 24 | self.server = smtplib.SMTP(smtp_host) 25 | 26 | def __del__(self): 27 | # disconnect from SMTP server 28 | self.server.quit() 29 | 30 | def send(self, from_addr, recipients, subject, body, reply_to=None, xheaders=None): 31 | """send a notification""" 32 | recipients = kobo.shortcuts.force_list(recipients) 33 | xheaders = xheaders or {} 34 | 35 | for to_addr in recipients: 36 | headers = [] 37 | headers.append("From: %s" % from_addr) 38 | headers.append("Subject: %s" % subject) 39 | headers.append("To: %s" % to_addr) 40 | if reply_to: 41 | headers.append("Reply-To: %s" % reply_to) 42 | 43 | for key, value in six.iteritems(xheaders): 44 | if not key.startswith("X-"): 45 | raise KeyError("X-Header has to start with 'X-': %s" % key) 46 | headers.append("%s: %s" % (key, value)) 47 | 48 | headers.append("") # blank line after headers 49 | headers.append(body) 50 | 51 | message = "\r\n".join(headers) 52 | self.server.sendmail(from_addr, to_addr, message) 53 | 54 | 55 | def main(argv): 56 | """Main function for command line usage""" 57 | parser = optparse.OptionParser("usage: %prog [to_addr]...") 58 | parser.add_option( 59 | "--server", 60 | help="specify SMTP server address" 61 | ) 62 | parser.add_option( 63 | "-f", 64 | "--from", 65 | dest="from_addr", 66 | help="set the From address" 67 | ) 68 | parser.add_option( 69 | "-s", 70 | "--subject", 71 | help="set email Subject" 72 | ) 73 | parser.add_option( 74 | "-r", 75 | "--reply-to", 76 | help="set the Reply-To address" 77 | ) 78 | parser.add_option( 79 | "-x", 80 | "--xheader", 81 | nargs=2, 82 | dest="xheaders", 83 | action="append", 84 | help="set X-Headers; takes two arguments (-x X-Spam eggs)" 85 | ) 86 | 87 | (opts, args) = parser.parse_args(argv) 88 | 89 | server = opts.server 90 | from_addr = opts.from_addr 91 | subject = opts.subject 92 | reply_to = opts.reply_to 93 | xheaders = opts.xheaders and dict(opts.xheaders) or {} 94 | recipients = args 95 | 96 | if not server: 97 | parser.error("SMTP server address required") 98 | 99 | if not from_addr or "@" not in from_addr: 100 | parser.error("invalid From address: %s" % from_addr) 101 | 102 | if not subject: 103 | parser.error("empty Subject") 104 | 105 | if len(recipients) == 0: 106 | parser.error("at least one recipient required") 107 | 108 | for to_addr in recipients: 109 | if "@" not in to_addr: 110 | parser.error("invalid To address: %s" % to_addr) 111 | 112 | for key in xheaders: 113 | if not key.startswith("X-"): 114 | parser.error("X-Header has to start with 'X-': %s" % key) 115 | 116 | notify = EmailNotification(server) 117 | body = sys.stdin.read() 118 | notify.send(from_addr, recipients, subject, body, reply_to=reply_to, xheaders=xheaders) 119 | 120 | 121 | if __name__ == "__main__": 122 | main(sys.argv[1:]) 123 | -------------------------------------------------------------------------------- /kobo/worker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from __future__ import absolute_import 5 | from .taskmanager import TaskContainer 6 | from .task import * 7 | 8 | from . import tasks 9 | 10 | 11 | TaskContainer.register_module(tasks) 12 | -------------------------------------------------------------------------------- /kobo/worker/default.conf: -------------------------------------------------------------------------------- 1 | # Hub xml-rpc address. 2 | HUB_URL = "https://localhost/hub/xmlrpc" 3 | 4 | # Hub authentication method. Example: krbv, gssapi, password, worker_key, oidc, token_oidc 5 | AUTH_METHOD = "krbv" 6 | 7 | # Username and password 8 | #USERNAME = "" 9 | #PASSWORD = "" 10 | 11 | # A unique worker key used for authentication. 12 | # I't recommended to use krbv auth in production environment instead. 13 | #WORKER_KEY = "" 14 | 15 | # Kerberos principal. If commented, default principal obtained by kinit is used. 16 | #KRB_PRINCIPAL = "" 17 | 18 | # Kerberos keytab file. 19 | #KRB_KEYTAB = "" 20 | 21 | # Kerberos service prefix. Example: host, HTTP 22 | #KRB_SERVICE = "HTTP" 23 | 24 | # Kerberos realm. If commented, last two parts of domain name are used. Example: MYDOMAIN.COM. 25 | KRB_REALM = "EXAMPLE.COM" 26 | 27 | # Kerberos credential cache file. 28 | #KRB_CCACHE = "" 29 | 30 | # Kerberos proxy users. 31 | #KRB_PROXY_USERS = "" 32 | 33 | # Process pid file. 34 | PID_FILE = "/var/run/kobo-worker.pid" 35 | 36 | # Log level. Example: debug, info, warning, error, critical. 37 | LOG_LEVEL = "debug" 38 | 39 | # Log file. 40 | LOG_FILE = "/var/log/kobo-worker.log" 41 | 42 | # The maximum number of jobs that a worker will handle at a time. 43 | MAX_JOBS = 10 44 | 45 | # Task manager sleep time between polls. 46 | SLEEP_TIME = 20 47 | -------------------------------------------------------------------------------- /kobo/worker/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/kobo/worker/tasks/__init__.py -------------------------------------------------------------------------------- /kobo/worker/tasks/task_shutdown_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from kobo.worker import TaskBase 4 | from kobo.exceptions import ShutdownException 5 | 6 | 7 | class ShutdownWorker(TaskBase): 8 | enabled = True 9 | 10 | arches = ["noarch"] 11 | channels = ["default"] 12 | exclusive = True 13 | foreground = True 14 | priority = 19 15 | weight = 0.0 16 | 17 | def run(self): 18 | kill = self.args.get("kill", False) 19 | 20 | if kill: 21 | # raise exception and terminate immediately 22 | raise ShutdownException() 23 | 24 | # lock the task manager and let it terminate all tasks 25 | self.task_manager.locked = True 26 | self.task_manager.reexec = False 27 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env = 3 | DJANGO_SETTINGS_MODULE=tests.settings 4 | -------------------------------------------------------------------------------- /setup-sdist-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Running $ python setup.py sdist on an unclean directory 4 | # may lead to including extra files in a tarball. 5 | # This script makes a clean git export to make sure 6 | # the tarball always matches git content. 7 | 8 | set -x 9 | set -e 10 | 11 | # get git repo dir 12 | git_repo=$(dirname $(readlink -f $0)) 13 | # get git branch 14 | git_branch=$(git rev-parse --abbrev-ref HEAD) 15 | 16 | current_dir=$(pwd) 17 | tmp_dir=$(mktemp -d) 18 | 19 | cd "$tmp_dir" 20 | 21 | # make a clean copy of git content 22 | git archive --remote="$git_repo" "$git_branch" | tar xf - 23 | 24 | # create tarball 25 | python setup.py sdist 26 | 27 | # copy tarball to current dir 28 | cp dist/* "$current_dir" 29 | 30 | rm -rf "$tmp_dir" 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import os 6 | 7 | import distutils.command.sdist 8 | from distutils.core import setup 9 | from distutils.command.install import INSTALL_SCHEMES 10 | 11 | 12 | # override default tarball format with bzip2 13 | distutils.command.sdist.sdist.default_format = {"posix": "bztar"} 14 | 15 | # force to install data files to site-packages 16 | for scheme in INSTALL_SCHEMES.values(): 17 | scheme["data"] = scheme["purelib"] 18 | 19 | # recursively scan for python modules to be included 20 | package_root_dirs = ["kobo"] 21 | packages = set() 22 | package_data = {} 23 | for package_root_dir in package_root_dirs: 24 | for root, dirs, files in os.walk(package_root_dir): 25 | # ignore PEP 3147 cache dirs and those whose names start with '.' 26 | dirs[:] = [i for i in dirs if not i.startswith('.') and i != '__pycache__'] 27 | parts = root.split("/") 28 | if "__init__.py" in files: 29 | package = ".".join(parts) 30 | packages.add(package) 31 | relative_path = "" 32 | elif files: 33 | relative_path = [] 34 | while ".".join(parts) not in packages: 35 | relative_path.append(parts.pop()) 36 | if not relative_path: 37 | continue 38 | relative_path.reverse() 39 | relative_path = os.path.join(*relative_path) 40 | package = ".".join(parts) 41 | else: 42 | # not a module, no files -> skip 43 | continue 44 | 45 | package_files = package_data.setdefault(package, []) 46 | package_files.extend([os.path.join(relative_path, i) for i in files if not i.endswith(".py")]) 47 | 48 | 49 | packages = sorted(packages) 50 | for package in package_data.keys(): 51 | package_data[package] = sorted(package_data[package]) 52 | 53 | 54 | setup( 55 | name = "kobo", 56 | version = "0.40.0", 57 | description = "A pile of python modules used by Red Hat release engineering to build their tools", 58 | url = "https://github.com/release-engineering/kobo/", 59 | author = "Red Hat, Inc.", 60 | author_email = "dmach@redhat.com", 61 | license = "LGPLv2.1", 62 | 63 | packages = packages, 64 | package_data = package_data, 65 | scripts = ["kobo/admin/kobo-admin"], 66 | install_requires=["six"], 67 | python_requires ='>=3.6', 68 | ) 69 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | mock==2.0.0 2 | pytest 3 | pytest-env 4 | koji 5 | requests 6 | requests_gssapi 7 | bandit==1.7.5; python_version > '3.8' 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/tests/__init__.py -------------------------------------------------------------------------------- /tests/chunks_file: -------------------------------------------------------------------------------- 1 | 1234567890 2 | 1234567890 3 | 1234567890 4 | 1234567890 5 | 1234567890 6 | 1234567890 7 | 1234567890 8 | 1234567890 9 | 1234567890 10 | 1234567890 11 | 12 | -------------------------------------------------------------------------------- /tests/data/dummy-AdobeReader_enu-9.5.1-1.nosrc.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/tests/data/dummy-AdobeReader_enu-9.5.1-1.nosrc.rpm -------------------------------------------------------------------------------- /tests/data/dummy-basesystem-10.0-6.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/tests/data/dummy-basesystem-10.0-6.noarch.rpm -------------------------------------------------------------------------------- /tests/data/dummy-basesystem-10.0-6.src.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/tests/data/dummy-basesystem-10.0-6.src.rpm -------------------------------------------------------------------------------- /tests/fields_test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /tests/fields_test/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "fields_test.dummymodel", 5 | "fields": { 6 | "field": "{\"key\": \"value\"}" 7 | } 8 | } 9 | ] -------------------------------------------------------------------------------- /tests/fields_test/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from kobo.django.fields import JSONField 4 | 5 | from django.db import models 6 | 7 | 8 | class DummyModel(models.Model): 9 | field = JSONField() 10 | 11 | 12 | class DummyDefaultModel(models.Model): 13 | field = JSONField(default={}) 14 | 15 | class DummyNotHumanModel(models.Model): 16 | field = JSONField(default={}, human_readable = True) 17 | -------------------------------------------------------------------------------- /tests/fields_test/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'NAME': ':memory:', 6 | 'ENGINE': 'django.db.backends.sqlite3' 7 | } 8 | } 9 | 10 | INSTALLED_APPS = ( 11 | 'fields_test', 12 | ) 13 | 14 | SECRET_KEY = 'whatever' 15 | -------------------------------------------------------------------------------- /tests/fields_test/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import unittest 6 | import pytest 7 | 8 | from django.core.exceptions import ValidationError 9 | from django.test import TestCase 10 | from django import VERSION 11 | from .models import DummyDefaultModel, DummyModel, DummyNotHumanModel 12 | 13 | 14 | class TestBasicJSONField(TestCase): 15 | 16 | def test_default(self): 17 | """ 18 | Test if default works as it should 19 | """ 20 | d = DummyDefaultModel() 21 | d.save() 22 | 23 | self.assertEqual(d.field, {}) 24 | 25 | def test_default2(self): 26 | """ 27 | This should raise 'AssertionError: {} != []' 28 | """ 29 | d = DummyDefaultModel() 30 | d.save() 31 | 32 | self.assertNotEqual(d.field, []) 33 | 34 | def test_nothumanreadable(self): 35 | """ 36 | Test human_readable parameter doesn't change anything on dict level 37 | """ 38 | d = DummyNotHumanModel() 39 | d.save() 40 | 41 | self.assertEqual(d.field, {}) 42 | 43 | def test_nothumanreadable2(self): 44 | """ 45 | Test human_readable parameter doesn't change anything with data 46 | """ 47 | 48 | data = {'a': 1} 49 | d = DummyNotHumanModel() 50 | d.field = data 51 | d.save() 52 | 53 | self.assertEqual(data, d.field) 54 | 55 | 56 | def test_assignment(self): 57 | """ 58 | Basic assignment 59 | """ 60 | d = DummyDefaultModel() 61 | d.field = [] 62 | 63 | self.assertEqual(d.field, []) 64 | 65 | def test_assignment_with_save(self): 66 | """ 67 | Basic assignment with save 68 | """ 69 | d = DummyDefaultModel() 70 | d.field = [] 71 | d.save() 72 | 73 | self.assertEqual(d.field, []) 74 | 75 | def test_complex_assignment_with_save(self): 76 | d = DummyDefaultModel() 77 | data = {'asd': [1, 2, 3], 'qwe': {'a': 'b'}} 78 | d.field = data 79 | d.save() 80 | 81 | self.assertEqual(d.field, data) 82 | 83 | @unittest.skipUnless(VERSION[0:3] < (1, 9, 0), 84 | "Automatic fixture loading is not possible since syncdb was removed.") 85 | class TestFixturesJSONField(unittest.TestCase): 86 | """ 87 | DO NOT ADD ANYTHING INTO DATABASE IN THIS TESTCASE 88 | 89 | these tests are meant to test loading fixtures and dumping models 90 | initial_data.json fixture should be loaded automatically and tested if it's okay 91 | """ 92 | # will be loaded from /fixtures/ 93 | #fixtures = ['fixture.json'] 94 | 95 | def test_fixture(self): 96 | """ 97 | Basic assignment 98 | """ 99 | d = DummyModel.objects.get(pk=1) 100 | try: 101 | # python 2.7+ 102 | self.assertIsInstance(d.field, dict) 103 | except AttributeError: 104 | # python <2.7 105 | self.assertTrue(isinstance(d.field, dict)) 106 | self.assertTrue('key' in d.field) 107 | self.assertEqual(d.field['key'], 'value') 108 | 109 | def test_dump(self): 110 | """ 111 | Basic assignment 112 | """ 113 | from django.core import serializers 114 | 115 | JSSerializer = serializers.get_serializer('json') 116 | js_serializer = JSSerializer() 117 | js = js_serializer.serialize(DummyModel.objects.all()) 118 | js_des = json.loads(js) 119 | des_obj = js_des[0] 120 | self.assertEqual(des_obj['pk'], 1) 121 | self.assertEqual(json.loads(des_obj['fields']['field']), {'key': 'value', }) 122 | 123 | -------------------------------------------------------------------------------- /tests/hub_urls.py: -------------------------------------------------------------------------------- 1 | from kobo.django.django_version import django_version_ge 2 | if django_version_ge("2.0"): 3 | from django.urls import re_path as url 4 | from django.urls import include 5 | 6 | else: 7 | from django.conf.urls import url, include 8 | from django.contrib import admin 9 | from django.http import HttpResponse 10 | 11 | 12 | def home(request): 13 | return HttpResponse("Index", status=200, content_type="text/plain") 14 | 15 | 16 | urlpatterns = [ 17 | url(r'^admin/', admin.site.urls), 18 | url(r"^auth/", include("kobo.hub.urls.auth")), 19 | url(r"^home/$", home, name="home/index"), 20 | url(r"^task/", include("kobo.hub.urls.task")), 21 | url(r"^info/arch/", include("kobo.hub.urls.arch")), 22 | url(r"^info/channel/", include("kobo.hub.urls.channel")), 23 | url(r"^info/user/", include("kobo.hub.urls.user")), 24 | url(r"^info/worker/", include("kobo.hub.urls.worker")), 25 | ] 26 | -------------------------------------------------------------------------------- /tests/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release-engineering/kobo/bf38e1bb26b54f3d9f8ab9bea72215b8020a2e72/tests/plugins/__init__.py -------------------------------------------------------------------------------- /tests/plugins/plug_broken.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.plugins import Plugin 5 | 6 | 7 | class BrokenPlugin(Plugin): 8 | enabled = True 9 | raise RuntimeError() 10 | -------------------------------------------------------------------------------- /tests/plugins/plug_working.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from kobo.plugins import Plugin 5 | 6 | 7 | class WorkingPlugin(Plugin): 8 | enabled = True 9 | -------------------------------------------------------------------------------- /tests/rpc.py: -------------------------------------------------------------------------------- 1 | from mock import Mock, PropertyMock 2 | 3 | from kobo.hub.xmlrpc import worker 4 | from kobo.xmlrpc import encode_xmlrpc_chunks_iterator 5 | 6 | 7 | class _RequestMock(object): 8 | 9 | def __init__(self, worker_instance): 10 | self.worker = worker_instance 11 | self.user = Mock() 12 | self.user.is_authenticated.return_value = True 13 | type(self.user).username = PropertyMock(return_value='mockuser') 14 | 15 | 16 | class RpcServiceMock(object): 17 | ''' 18 | RpcServiceMock implements all XML-RPC methods. 19 | ''' 20 | 21 | def __init__(self, worker_instance): 22 | self.worker = worker_instance 23 | self._request = _RequestMock(worker_instance) 24 | 25 | def get_worker_info(self): 26 | return worker.get_worker_info(self._request) 27 | 28 | def get_worker_id(self): 29 | return worker.get_worker_id(self._request) 30 | 31 | def get_worker_tasks(self): 32 | return worker.get_worker_tasks(self._request) 33 | 34 | def get_task(self, task_id): 35 | return worker.get_task(self._request, task_id) 36 | 37 | def get_task_no_verify(self, task_id): 38 | return worker.get_task_no_verify(self._request, task_id) 39 | 40 | def interrupt_tasks(self, task_list): 41 | return worker.interrupt_tasks(self._request, task_list) 42 | 43 | def timeout_tasks(self, task_list): 44 | return worker.timeout_tasks(self._request, task_list) 45 | 46 | def assign_task(self, task_id): 47 | return worker.assign_task(self._request, task_id) 48 | 49 | def open_task(self, task_id): 50 | return worker.open_task(self._request, task_id) 51 | 52 | def close_task(self, task_id, task_result): 53 | return worker.close_task(self._request, task_id, task_result) 54 | 55 | def cancel_task(self, task_id): 56 | return worker.cancel_task(self._request, task_id) 57 | 58 | def fail_task(self, task_id, task_result): 59 | return worker.fail_task(self._request, task_id, task_result) 60 | 61 | def set_task_weight(self, task_id, weight): 62 | return worker.set_task_weight(self._request, task_id, weight) 63 | 64 | def update_worker(self, enabled, ready, task_count): 65 | return worker.update_worker(self._request, enabled, ready, task_count) 66 | 67 | def get_tasks_to_assign(self): 68 | return worker.get_tasks_to_assign(self._request, ) 69 | 70 | def get_awaited_tasks(self, awaited_task_list): 71 | return worker.get_awaited_tasks(self._request, awaited_task_list) 72 | 73 | def create_subtask(self, label, method, args, parent_id): 74 | return worker.create_subtask(self._request, label, method, args, parent_id) 75 | 76 | def wait(self, task_id, child_list=None): 77 | return worker.wait(self._request, task_id, child_list) 78 | 79 | def check_wait(self, task_id, child_list=None): 80 | return worker.check_wait(self._request, task_id, child_list) 81 | 82 | def upload_task_log(self, task_id, relative_path, mode, chunk_start, 83 | chunk_len, chunk_checksum, encoded_chunk): 84 | return worker.upload_task_log(self._request, task_id, relative_path, mode, chunk_start, 85 | chunk_len, chunk_checksum, encoded_chunk) 86 | 87 | 88 | class HubProxyMock(object): 89 | ''' Mock for kobo.client.HubProxy ''' 90 | 91 | def __init__(self, conf, **kwargs): 92 | if 'worker' in kwargs: 93 | self.worker = kwargs.get('worker') 94 | else: 95 | self.worker = conf.get('worker') 96 | 97 | if self.worker is None: 98 | raise Exception('Missing worker argument') 99 | 100 | def upload_file(self, file_name, target_dir): 101 | # TODO: This should be implemented as in the original class. 102 | pass 103 | 104 | def upload_task_log(self, file_obj, task_id, remote_file_name, append=True, mode=0o644): 105 | # TODO: This should be implemented as in the original class. 106 | pass 107 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # Settings for Django testcases against kobo hub 2 | import os 3 | import kobo 4 | import tempfile 5 | from django import VERSION 6 | 7 | KOBO_DIR = os.path.normpath( 8 | os.path.join(os.path.dirname(__file__), '..', 'kobo') 9 | ) 10 | 11 | SECRET_KEY = "key" 12 | XMLRPC_METHODS = [] 13 | # When the following objects are destroyed 14 | # the temporary directories are deleted. 15 | TASK_DIR_OBJ = tempfile.TemporaryDirectory(prefix="kobo-test-tasks-") 16 | UPLOAD_DIR_OBJ = tempfile.TemporaryDirectory(prefix="kobo-test-dir-") 17 | WORKER_DIR_OBJ = tempfile.TemporaryDirectory(prefix="kobo-worker-") 18 | 19 | TASK_DIR = TASK_DIR_OBJ.name 20 | UPLOAD_DIR = UPLOAD_DIR_OBJ.name 21 | WORKER_DIR = WORKER_DIR_OBJ.name 22 | 23 | # Default redirects for unsafe login redirections 24 | LOGIN_REDIRECT_URL = 'home/index' 25 | LOGOUT_REDIRECT_URL = 'home/index' 26 | 27 | # The middleware and apps below are the bare minimum required 28 | # to let kobo.hub load successfully 29 | 30 | if VERSION[0:3] < (1, 10, 0): 31 | MIDDLEWARE_CLASSES = ( 32 | 'django.contrib.sessions.middleware.SessionMiddleware', 33 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 34 | 'kobo.django.auth.middleware.LimitedRemoteUserMiddleware', 35 | 'kobo.hub.middleware.WorkerMiddleware', 36 | ) 37 | if VERSION[0:3] >= (1, 10, 0): 38 | MIDDLEWARE = ( 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'kobo.django.auth.middleware.LimitedRemoteUserMiddleware', 42 | 'kobo.hub.middleware.WorkerMiddleware', 43 | ) 44 | 45 | 46 | INSTALLED_APPS = ( 47 | 'django.contrib.contenttypes', 48 | 'django.contrib.staticfiles', 49 | 'django.contrib.admin', 50 | 'django.contrib.auth', 51 | 'django.contrib.sessions', 52 | 'kobo.django', 53 | 'kobo.hub', 54 | ) 55 | 56 | DATABASES = { 57 | 'default': { 58 | 'ENGINE': 'django.db.backends.sqlite3', 59 | 'NAME': 'testdatabase', 60 | } 61 | } 62 | 63 | # We need to specify the template dirs because: 64 | # - the admin/templates don't belong to a particular django app, instead 65 | # they're intended to be copied to another app by a custom command, but 66 | # we don't want to run that during the tests 67 | # - automatic lookup of templates under kobo/hub isn't working for some reason, 68 | # not sure why, but might be related to use of deprecated arguments in 69 | # render_to_string (FIXME) 70 | # 71 | # The way to specify the template dirs differs between newer and older versions of Django 72 | if VERSION[0:3] < (1, 9, 0): 73 | TEMPLATE_DIRS = ( 74 | os.path.join(KOBO_DIR, 'admin/templates/hub/templates'), 75 | os.path.join(KOBO_DIR, 'hub/templates'), 76 | ) 77 | if VERSION[0:3] >= (1, 9, 0): 78 | TEMPLATES = [ 79 | { 80 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 81 | 'DIRS': ( 82 | os.path.join(KOBO_DIR, 'admin/templates/hub/templates'), 83 | os.path.join(KOBO_DIR, 'hub/templates') 84 | ), 85 | 'APP_DIRS': True 86 | } 87 | ] 88 | 89 | ROOT_URLCONF = 'tests.hub_urls' 90 | 91 | STATIC_URL = os.path.join(os.path.dirname(kobo.__file__), "hub", "static") + '/' 92 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import unittest 6 | 7 | import tempfile 8 | import os 9 | 10 | from kobo.decorators import log_traceback 11 | 12 | 13 | class TestDecoratorsModule(unittest.TestCase): 14 | def setUp(self): 15 | fd, self.tmp_file = tempfile.mkstemp() 16 | 17 | def tearDown(self): 18 | os.remove(self.tmp_file) 19 | 20 | def test_log_traceback(self): 21 | @log_traceback(self.tmp_file) 22 | def foo_function(): 23 | raise IOError("Some error") 24 | 25 | try: 26 | foo_function() 27 | except IOError: 28 | pass 29 | 30 | tb = open(self.tmp_file).read() 31 | self.assertTrue(tb.startswith("--- TRACEBACK BEGIN:")) 32 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | import subprocess 7 | 8 | 9 | def test_fields(): 10 | # FIXME: this test should be refactored to work within the current 11 | # process. Doing it this way, calling to a subprocess, will effectively 12 | # disable several features from the test framework. 13 | subprocess.check_call([sys.executable, __file__]) 14 | 15 | 16 | def main(): 17 | # Runs the test under fields_test directory with custom settings 18 | PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 19 | os.environ["PYTHONPATH"] = PROJECT_DIR 20 | os.environ['DJANGO_SETTINGS_MODULE'] = 'fields_test.settings' 21 | sys.path.insert(0, PROJECT_DIR) 22 | 23 | from django.core.management import call_command 24 | import django 25 | 26 | # Django >= 1.7 must call this method to initialize app registry, 27 | # while older Django do not have this method 28 | if 'setup' in dir(django): 29 | django.setup() 30 | 31 | call_command('test', 'fields_test') 32 | #call_command('syncdb') 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /tests/test_hardlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import unittest 6 | 7 | import tempfile 8 | import os.path 9 | import shutil 10 | 11 | from kobo.hardlink import Hardlink, UndoHardlink 12 | 13 | 14 | class LoggerMock(object): 15 | def __init__(self): 16 | self.loglvl = None 17 | self.msg = None 18 | 19 | def log(self, loglvl, msg): 20 | self.loglvl = loglvl 21 | self.msg = msg 22 | 23 | 24 | class TestHardlinkClass(unittest.TestCase): 25 | def setUp(self): 26 | self.tmp_dir = tempfile.mkdtemp() 27 | 28 | def tearDown(self): 29 | shutil.rmtree(self.tmp_dir) 30 | 31 | def test_link(self): 32 | path_src = os.path.join(self.tmp_dir, "a") 33 | open(path_src, 'w').write("asdf") 34 | path_dst = os.path.join(self.tmp_dir, "b") 35 | 36 | hl = Hardlink() 37 | hl.link(path_src, path_dst) 38 | 39 | self.assertEqual(os.stat(path_src), os.stat(path_dst)) 40 | 41 | def test_log(self): 42 | logger = LoggerMock() 43 | hl = Hardlink(logger=logger) 44 | log_level = 1 45 | message = "foobar" 46 | hl.log(log_level, message) 47 | 48 | self.assertEqual(logger.loglvl, log_level) 49 | self.assertEqual(logger.msg, message) 50 | 51 | 52 | class TestUndoHardlinkClass(unittest.TestCase): 53 | def setUp(self): 54 | self.tmp_dir = tempfile.mkdtemp() 55 | 56 | def tearDown(self): 57 | shutil.rmtree(self.tmp_dir) 58 | 59 | def test_undo_hardlink(self): 60 | path_src = os.path.join(self.tmp_dir, "a") 61 | path_dst = os.path.join(self.tmp_dir, "b") 62 | open(path_src, 'w').write("asdf") 63 | old_stat = os.stat(path_src) 64 | 65 | # Only one hardlink exists 66 | uhl1 = UndoHardlink() 67 | uhl1.undo_hardlink(path_src) 68 | new_stat = os.stat(path_src) 69 | self.assertEqual(old_stat.st_nlink, new_stat.st_nlink) 70 | 71 | # Two hardlinks exist 72 | os.link(path_src, path_dst) 73 | uhl2 = UndoHardlink() 74 | uhl2.undo_hardlink(path_dst) 75 | new_stat = os.stat(path_dst) 76 | self.assertNotEqual(old_stat.st_ino, new_stat.st_ino) 77 | self.assertEqual(new_stat.st_nlink, 1) # Expected num of hardlinks is 1 78 | -------------------------------------------------------------------------------- /tests/test_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import unittest 6 | 7 | import tempfile 8 | import os 9 | 10 | from kobo.http import POSTTransport 11 | 12 | 13 | class TestPOSTTransport(unittest.TestCase): 14 | def setUp(self): 15 | self.postt = POSTTransport() 16 | 17 | def test_get_content_type(self): 18 | tf0 = tempfile.mkstemp()[1] 19 | tf1 = tempfile.mkstemp(suffix=".txt")[1] 20 | tf2 = tempfile.mkstemp(suffix=".rtf")[1] 21 | tf3 = tempfile.mkstemp(suffix=".avi")[1] 22 | self.assertEqual(self.postt.get_content_type(tf0), "application/octet-stream") 23 | self.assertEqual(self.postt.get_content_type(tf1), "text/plain") 24 | # *.rtf: py2.7 returns 'application/rtf'; py2.4 returns 'text/rtf' 25 | self.assertEqual(self.postt.get_content_type(tf2).split("/")[1], "rtf") 26 | self.assertTrue(self.postt.get_content_type(tf2) in ("application/rtf", "text/rtf")) 27 | self.assertEqual(self.postt.get_content_type(tf3), "video/x-msvideo") 28 | 29 | def test_add_file(self): 30 | tf1 = tempfile.mkstemp()[1] 31 | tf2 = tempfile.mkstemp()[1] 32 | tf3 = open(tempfile.mkstemp()[1]) 33 | os.unlink(tf1) 34 | self.assertRaises(OSError, self.postt.add_file, "file", tf1) 35 | self.assertEqual(self.postt.add_file("file", tf2), None) 36 | self.assertRaises(TypeError, self.postt.add_file, "file", tf3) 37 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import unittest 6 | import six 7 | 8 | import logging 9 | from kobo.log import * 10 | 11 | 12 | 13 | 14 | class TestLog(unittest.TestCase): 15 | def setUp(self): 16 | self.logger = logging.getLogger("TestLogger") 17 | 18 | def test_verbose_hack(self): 19 | self.logger.verbose("foo") 20 | logging.verbose("foo") 21 | self.assertEqual(logging.VERBOSE, 15) 22 | if six.PY2: 23 | # There is no _levelNames attribute in Python 3 24 | self.assertTrue("VERBOSE" in logging._levelNames) 25 | self.assertEqual(logging.getLevelName(15), "VERBOSE") 26 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from mock import Mock, PropertyMock, patch 6 | 7 | from kobo.hub import middleware 8 | 9 | 10 | class DummyRequest(object): 11 | pass 12 | 13 | 14 | class DummyWorker(object): 15 | 16 | def __init__(self, name=None): 17 | self.name = name 18 | 19 | 20 | class TestGetWorker(unittest.TestCase): 21 | 22 | def test_get_worker(self): 23 | with patch('kobo.hub.middleware.Worker') as worker_mock: 24 | worker_mock.objects.get.return_value = DummyWorker() 25 | req = PropertyMock(user=PropertyMock(username='foo/bar')) 26 | worker = middleware.get_worker(req) 27 | self.assertIsInstance(worker, DummyWorker) 28 | worker_mock.objects.get.assert_called_once_with(name='bar') 29 | 30 | def test_get_worker_missing_hostname(self): 31 | req = PropertyMock(user=PropertyMock(username='username')) 32 | worker = middleware.get_worker(req) 33 | self.assertIsNone(worker) 34 | 35 | def test_get_worker_catch_exceptions(self): 36 | req = PropertyMock(user=Mock(side_effect=ValueError)) 37 | worker = middleware.get_worker(req) 38 | self.assertIsNone(worker) 39 | 40 | 41 | class TestLazyWorker(unittest.TestCase): 42 | 43 | def test_lazy_worker_set_cache_variable_if_not_set(self): 44 | with patch('kobo.hub.middleware.get_worker', return_value=DummyWorker()) as get_worker_mock: 45 | req = PropertyMock( 46 | spec=['user'], 47 | user=PropertyMock(username='foo/bar'), 48 | ) 49 | 50 | cached_worker = middleware.LazyWorker().__get__(req) 51 | self.assertIsInstance(cached_worker, DummyWorker) 52 | 53 | get_worker_mock.assert_called_once_with(req) 54 | 55 | def test_lazy_worker_do_not_set_cache_variable_if_already_set(self): 56 | with patch('kobo.hub.middleware.get_worker', return_value=DummyWorker('new-worker')) as get_worker_mock: 57 | req = PropertyMock( 58 | spec=['user', '_cached_worker'], 59 | user=PropertyMock(username='foo/bar'), 60 | _cached_worker=DummyWorker('cached-worker'), 61 | ) 62 | 63 | cached_worker = middleware.LazyWorker().__get__(req) 64 | self.assertIsInstance(cached_worker, DummyWorker) 65 | self.assertEqual(cached_worker.name, 'cached-worker') 66 | 67 | get_worker_mock.assert_not_called() 68 | 69 | 70 | class TestWorkerMiddleware(unittest.TestCase): 71 | 72 | def test_process_request(self): 73 | with patch('kobo.hub.middleware.get_worker', return_value=DummyWorker()) as get_worker_mock: 74 | req = DummyRequest() 75 | req.user = PropertyMock(username='foo/bar') 76 | 77 | middleware.WorkerMiddleware(lambda x: x).process_request(req) 78 | self.assertIsInstance(req.worker, DummyWorker) 79 | get_worker_mock.assert_called_once_with(req) 80 | 81 | def test_process_request_missing_user(self): 82 | req = DummyRequest() 83 | 84 | with self.assertRaises(AssertionError): 85 | middleware.WorkerMiddleware(lambda x: x).process_request(req) 86 | -------------------------------------------------------------------------------- /tests/test_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import os 6 | import tempfile 7 | import shutil 8 | 9 | from kobo.client import BaseClientCommandContainer, ClientCommandContainer 10 | from kobo.cli import CommandOptionParser 11 | from kobo.conf import PyConfigParser 12 | 13 | TEST_CONFIG = ''' 14 | HUB_URL = "https://localhost/hub/xmlrpc" 15 | 16 | AUTH_METHOD = "krbv" 17 | ''' 18 | 19 | 20 | class TestBaseClientCommandContainer(unittest.TestCase): 21 | def setUp(self): 22 | self.command_container = BaseClientCommandContainer() 23 | 24 | def test_profile_option_unset(self): 25 | parser = CommandOptionParser(command_container=self.command_container) 26 | option = parser.get_option("--profile") 27 | 28 | self.assertEqual(parser.default_profile, "") 29 | self.assertEqual(option, None) 30 | 31 | def test_profile_option_set(self): 32 | parser = CommandOptionParser(command_container=self.command_container, default_profile="default-profile") 33 | option = parser.get_option("--profile") 34 | 35 | self.assertEqual(parser.default_profile, "default-profile") 36 | self.assertEqual(option.get_opt_string(), "--profile") 37 | self.assertEqual(option.help, "specify profile (default: default-profile)") 38 | 39 | def test_configuration_directory_option_unset(self): 40 | parser = CommandOptionParser(command_container=self.command_container, default_profile="default-profile") 41 | # CommandOptionParser() doesn't store the configuration_file path in an instance variable, instead it's 42 | # build in _load_profile() with the line below: 43 | configuration_file = os.path.join(parser.configuration_directory, '{0}.conf'.format(parser.default_profile)) 44 | 45 | self.assertEqual(parser.configuration_directory, "/etc") 46 | self.assertEqual(configuration_file, "/etc/default-profile.conf") 47 | 48 | def test_configuration_directory_option_set(self): 49 | parser = CommandOptionParser(command_container=self.command_container, default_profile="default-profile", 50 | configuration_directory="/etc/client") 51 | 52 | configuration_file = os.path.join(parser.configuration_directory, '{0}.conf'.format(parser.default_profile)) 53 | 54 | self.assertEqual(parser.configuration_directory, "/etc/client") 55 | self.assertEqual(configuration_file, "/etc/client/default-profile.conf") 56 | 57 | 58 | class TestClientCommandContainer(unittest.TestCase): 59 | def setUp(self): 60 | self.dir = tempfile.mkdtemp() 61 | self.conf = PyConfigParser() 62 | 63 | self.file = os.path.join(self.dir, 'test.conf') 64 | 65 | with open(self.file, 'w') as f: 66 | f.write(TEST_CONFIG) 67 | 68 | self.conf.load_from_file(self.file) 69 | 70 | def tearDown(self): 71 | shutil.rmtree(self.dir) 72 | 73 | def test_config_from_file(self): 74 | container = ClientCommandContainer(self.conf) 75 | 76 | values = { 77 | 'HUB_URL': 'https://localhost/hub/xmlrpc', 78 | 'AUTH_METHOD': 'krbv' 79 | } 80 | self.assertEqual(container.conf, values) 81 | 82 | def test_config_from_kwargs(self): 83 | container = ClientCommandContainer(self.conf, USERNAME='testuser') 84 | 85 | values = { 86 | 'HUB_URL': 'https://localhost/hub/xmlrpc', 87 | 'AUTH_METHOD': 'krbv', 88 | 'USERNAME': 'testuser' 89 | } 90 | self.assertEqual(container.conf, values) 91 | -------------------------------------------------------------------------------- /tests/test_tail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | from six import BytesIO 6 | 7 | from kobo.hub.models import _tail as tail 8 | 9 | 10 | SAMPLE_LINES = [ 11 | b'this is a sample', 12 | b'string; each', 13 | b'line contains', 14 | b'16 chars exclud-', 15 | b'ing newline, and', 16 | b'six lines total' 17 | ] 18 | SAMPLE_STRING = b'\n'.join(SAMPLE_LINES) 19 | 20 | 21 | class TestTail(unittest.TestCase): 22 | 23 | def test_tail_empty(self): 24 | """tail of empty object returns empty""" 25 | (actual, offset) = tail(BytesIO(), 1024, 1024) 26 | 27 | self.assertEqual(actual, b'') 28 | self.assertEqual(offset, 0) 29 | 30 | def test_tail_noop(self): 31 | """tail returns all content if it fits in requested size""" 32 | (actual, offset) = tail(BytesIO(SAMPLE_STRING), 1024, 1024) 33 | 34 | self.assertEqual(actual, SAMPLE_STRING) 35 | self.assertEqual(offset, len(SAMPLE_STRING)) 36 | 37 | def test_tail_limit(self): 38 | """tail returns trailing lines up to given limit""" 39 | expected = b'\n'.join([ 40 | b'ing newline, and', 41 | b'six lines total', 42 | ]) 43 | (actual, offset) = tail(BytesIO(SAMPLE_STRING), 40, 1024) 44 | 45 | self.assertEqual(actual, expected) 46 | self.assertEqual(offset, len(SAMPLE_STRING)) 47 | 48 | def test_tail_line_break(self): 49 | """tail breaks in middle of line if lines are longer than max length""" 50 | expected = b'\n'.join([ 51 | # this line is partially returned 52 | b'xclud-', 53 | b'ing newline, and', 54 | b'six lines total', 55 | ]) 56 | (actual, offset) = tail(BytesIO(SAMPLE_STRING), 40, 10) 57 | 58 | self.assertEqual(actual, expected) 59 | self.assertEqual(offset, len(SAMPLE_STRING)) 60 | -------------------------------------------------------------------------------- /tests/test_task_shutdown_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from mock import Mock, PropertyMock 6 | 7 | from kobo.exceptions import ShutdownException 8 | from kobo.worker.tasks.task_shutdown_worker import ShutdownWorker 9 | 10 | class TestShutdownWorker(unittest.TestCase): 11 | 12 | def test_run(self): 13 | t = ShutdownWorker(Mock(spec=['worker']), {}, 100, {}) 14 | 15 | t.task_manager = PropertyMock(locked=False) 16 | self.assertFalse(t.task_manager.locked) 17 | 18 | t.run() 19 | self.assertTrue(t.task_manager.locked) 20 | 21 | def test_run_kill(self): 22 | t = ShutdownWorker(Mock(spec=['worker']), {}, 100, {'kill': True}) 23 | 24 | t.task_manager = PropertyMock(locked=False) 25 | self.assertFalse(t.task_manager.locked) 26 | 27 | with self.assertRaises(ShutdownException): 28 | t.run() 29 | 30 | self.assertFalse(t.task_manager.locked) 31 | -------------------------------------------------------------------------------- /tests/test_tback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import re 6 | import unittest 7 | 8 | from kobo.tback import get_traceback, Traceback 9 | 10 | 11 | class TestTraceback(unittest.TestCase): 12 | 13 | def test_empty(self): 14 | self.assertEqual('', get_traceback()) 15 | self.assertEqual('', Traceback().get_traceback()) 16 | self.assertEqual((None, None, None), Traceback().exc_info) 17 | 18 | def test_text(self): 19 | try: 20 | raise Exception('Simple text') 21 | except: 22 | regexp = re.compile(r'Traceback \(most recent call last\):\n *File ".*test_tback.py", line .+, in test_text\n *raise Exception\(\'Simple text\'\)\n *Exception: Simple text', re.M) 23 | 24 | self.assertRegex(get_traceback(), regexp) 25 | tb = Traceback(show_traceback = True, show_code = False, show_locals = False, show_environ = False, show_modules = False) 26 | self.assertRegex(tb.get_traceback(), regexp) 27 | 28 | def test_Traceback(self): 29 | try: 30 | raise Exception('Simple text') 31 | except: 32 | tb = Traceback(show_traceback = False, show_code = False, show_locals = False, show_environ = False, show_modules = False) 33 | self.assertEqual('', tb.get_traceback()) 34 | tb.show_code = True 35 | self.assertRegex(tb.get_traceback(), re.compile(r'.*--> *\d+ *raise Exception.*<\/CODE>$', re.M | re.S)) 36 | tb.show_code = False 37 | tb.show_locals = True 38 | self.assertRegex(tb.get_traceback(), re.compile(r'.*tb = .*<\/LOCALS>$', re.M | re.S)) 39 | tb.show_locals = False 40 | tb.show_environ = True 41 | self.assertRegex(tb.get_traceback(), re.compile(r'.*<\/ENVIRON>\n.*$', re.M | re.S)) 42 | tb.show_environ = False 43 | tb.show_modules = True 44 | self.assertRegex(tb.get_traceback(), re.compile(r'.*<\/MODULES>$', re.M | re.S)) 45 | 46 | def test_encoding(self): 47 | try: 48 | a = ''.join([chr(i) for i in range(256)]) 49 | b = b''.join([chr(i).encode() for i in range(65536)]) 50 | raise Exception() 51 | except: 52 | tb = Traceback(show_code = False, show_traceback = False) 53 | output = tb.get_traceback() 54 | self.assertIsInstance(output, str) 55 | 56 | def test_uninitialized_variables(self): 57 | class Foo(object): 58 | __slots__ = ( "bar", "version" ) 59 | 60 | def __init__(self): 61 | self.version = 1 62 | 63 | def test(self): 64 | try: 65 | raise 66 | except: 67 | # bar is uninitialized 68 | return Traceback().get_traceback() 69 | 70 | obj = Foo() 71 | self.assertTrue(obj.test()) 72 | -------------------------------------------------------------------------------- /tests/test_utf8_chunk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | from kobo.hub.models import _utf8_chunk as utf8_chunk 6 | 7 | class TestUtf8Chunk(unittest.TestCase): 8 | def test_noop_ascii(self): 9 | """utf8_chunk returns input bytes if byte sequence is entirely ASCII""" 10 | bytestr = b'hello world' 11 | self.assertIs(utf8_chunk(bytestr), bytestr) 12 | 13 | def test_noop_utf8_end(self): 14 | """utf8_chunk returns input bytes if byte sequence uses non-ASCII 15 | UTF-8 at the end of the string and is well-formed""" 16 | unistr = u'hello 世界' 17 | bytestr = unistr.encode('utf-8') 18 | self.assertIs(utf8_chunk(bytestr), bytestr) 19 | 20 | def test_noop_utf8_mid(self): 21 | """utf8_chunk returns input bytes if byte sequence uses non-ASCII 22 | UTF-8 in the middle of the string and is well-formed""" 23 | unistr = u'hello 世界!' 24 | bytestr = unistr.encode('utf-8') 25 | self.assertIs(utf8_chunk(bytestr), bytestr) 26 | 27 | def test_noop_invalid(self): 28 | """utf8_chunk returns input bytes if byte sequence is not valid 29 | UTF-8 and can't be fixed by truncation""" 30 | bytestr = b'hello \xff\xff\xff' 31 | self.assertIs(utf8_chunk(bytestr), bytestr) 32 | 33 | def test_fixup_end(self): 34 | """utf8_chunk returns copy of input aligned to nearest character boundary 35 | if input is a byte sequence truncated in the middle of a unicode character.""" 36 | unistr = u'hello 世界' 37 | bytestr = unistr.encode('utf-8') 38 | 39 | # this is now a broken sequence since we cut it off 40 | # partway through a character 41 | bytestr = bytestr[:-1] 42 | 43 | # proving it's broken 44 | try_decode = lambda: bytestr.decode('utf-8') 45 | self.assertRaises(UnicodeDecodeError, try_decode) 46 | 47 | # utf8_chunk unbreaks it by removing until the previous 48 | # complete character 49 | self.assertEqual(utf8_chunk(bytestr).decode('utf-8'), u'hello 世') 50 | -------------------------------------------------------------------------------- /tests/test_widgets.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from kobo.django.forms import JSONWidget 4 | 5 | 6 | @pytest.mark.parametrize(("value", "output"), [ 7 | ('{"a": "b"}', ''), 8 | ({"a": "b"}, ''), 9 | ("[1, 2, 3]", ''), 10 | ([1, 2, 3], ''), 11 | ]) 12 | def test_JSONWidget(value, output): 13 | w = JSONWidget() 14 | assert w.render("test_widget", value, attrs={"id": "noid"}) == output 15 | -------------------------------------------------------------------------------- /tests/test_xmlrpc_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import django 4 | 5 | from django.core.exceptions import PermissionDenied 6 | from mock import Mock, patch 7 | 8 | from kobo.hub.models import Worker 9 | from kobo.hub.xmlrpc import auth 10 | 11 | from .utils import DjangoRunner 12 | 13 | runner = DjangoRunner() 14 | setup_module = runner.start 15 | teardown_module = runner.stop 16 | 17 | 18 | class TestLoginWorker(django.test.TransactionTestCase): 19 | 20 | def test_login_worker_key_valid_worker_and_user(self): 21 | def login(request, user): 22 | request.session.session_key = '1234567890' 23 | return user 24 | 25 | Worker.objects.create(worker_key='key', name='name') 26 | 27 | req = Mock(spec=['session'], session=Mock()) 28 | user = Mock() 29 | krb_mock = Mock(spec=['authenticate'], authenticate=Mock(return_value=user)) 30 | 31 | with patch('kobo.hub.xmlrpc.auth.Krb5RemoteUserBackend', return_value=krb_mock): 32 | with patch.object(auth.django.contrib.auth, 'login', side_effect=login) as login_mock: 33 | session_key = auth.login_worker_key(req, 'key') 34 | 35 | login_mock.assert_called_once_with(req, user) 36 | krb_mock.authenticate.assert_called_once_with(None, 'worker/name') 37 | self.assertEqual(session_key, '1234567890') 38 | 39 | def test_login_worker_key_valid_worker_invalid_user(self): 40 | Worker.objects.create(worker_key='key', name='name') 41 | req = Mock(spec=['session'], session=Mock()) 42 | krb_mock = Mock(spec=['authenticate'], authenticate=Mock(return_value=None)) 43 | 44 | with patch('kobo.hub.xmlrpc.auth.Krb5RemoteUserBackend', return_value=krb_mock): 45 | with self.assertRaises(PermissionDenied): 46 | auth.login_worker_key(req, 'key') 47 | 48 | krb_mock.authenticate.assert_called_once_with(None, 'worker/name') 49 | 50 | def test_login_worker_key_invalid_worker(self): 51 | req = Mock() 52 | 53 | with self.assertRaises(PermissionDenied): 54 | auth.login_worker_key(req, 'key') 55 | -------------------------------------------------------------------------------- /tests/test_xmlrpc_system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from kobo.hub.xmlrpc.system import getAPIVersion 6 | 7 | 8 | class TestApiVersion(unittest.TestCase): 9 | 10 | def test_api_version(self): 11 | version = getAPIVersion(None) 12 | self.assertEqual(version, '0.1.0') 13 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test.utils import get_runner 4 | from django.conf import settings 5 | 6 | # Run test with KOBO_MEMORY_PROFILER=1 to generate memory usage reports from 7 | # tests annotated with @profile. 8 | # 9 | # The point of the memory profiler with tests is to prove that the function 10 | # is memory efficient. When using the profiler, you'll want to verify that 11 | # the peak memory usage shows no significant increase in the annotated test. 12 | if os.environ.get('KOBO_MEMORY_PROFILER', '0') == '1': 13 | from memory_profiler import profile 14 | else: 15 | # If memory_profiler is disabled, this is a no-op decorator 16 | def profile(fn): 17 | return fn 18 | 19 | 20 | class DjangoRunner(object): 21 | """Use this for tests which need an active Django environment 22 | and database. Create an instance and start/stop around the 23 | relevant test(s). 24 | 25 | Ideally, this could be set up as a pytest fixture in conftest.py. 26 | That doesn't work currently due to https://github.com/pytest-dev/pytest/issues/517 ; 27 | it clashes with django.testcase.TestCase.setUpClass. 28 | It can instead be used via setup_module/teardown_module. 29 | """ 30 | def __init__(self): 31 | self.runner = None 32 | self.old_config = None 33 | 34 | def start(self): 35 | runner_class = get_runner(settings) 36 | self.runner = runner_class() 37 | self.runner.setup_test_environment() 38 | self.old_config = self.runner.setup_databases() 39 | 40 | def stop(self): 41 | self.runner.teardown_databases(self.old_config) 42 | self.runner.teardown_test_environment() 43 | self.runner = None 44 | self.old_config = None 45 | 46 | 47 | def data_path(basename): 48 | """Returns path to a file under 'data' dir.""" 49 | this_dir = os.path.dirname(__file__) 50 | return os.path.join(this_dir, 'data', basename) 51 | 52 | 53 | class ArgumentIsInstanceOf(object): 54 | 55 | def __init__(self, classinfo): 56 | self.classinfo = classinfo 57 | 58 | def __eq__(self, other): 59 | return isinstance(other, self.classinfo) 60 | -------------------------------------------------------------------------------- /tools/db_update-0.2.0-0.3.0: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Conversion of db format from 0.2.0 format. Tracebacks were saved into db 5 | prior 0.3.0 version. From now we are saving tracebacks to TASK_DIR instead of 6 | db. 7 | ''' 8 | 9 | import os 10 | import sys 11 | import pipes 12 | from django.db import connection, transaction 13 | from django.core import management 14 | from django.conf import settings 15 | from kobo.hub.models import Task 16 | from kobo.shortcuts import save_to_file, run 17 | 18 | 19 | def main(): 20 | c = connection.cursor() 21 | 22 | 23 | # copy data from db to disk 24 | print 'Dumping task tracebacks and logs to disk...' 25 | c.execute('SELECT id, traceback, result FROM hub_task') 26 | l = c.fetchone() 27 | while l: 28 | task_id, traceback, stdout = l 29 | print 'task', task_id 30 | if traceback: 31 | fpath = os.path.join(Task.get_task_dir(task_id, create=True), 'traceback.log') 32 | save_to_file(fpath, traceback, mode=0600) 33 | 34 | if stdout: 35 | fpath = os.path.join(Task.get_task_dir(task_id, create=True), 'stdout.log') 36 | save_to_file(fpath, stdout, mode=0644) 37 | 38 | l = c.fetchone() 39 | 40 | if settings.DATABASES["default"]["ENGINE"] in ('django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql'): 41 | print "POSTGRESQL database" 42 | 43 | c.execute("ALTER TABLE hub_worker ADD COLUMN max_tasks integer CHECK (max_tasks >= 0) NOT NULL DEFAULT 0") 44 | c.execute("ALTER TABLE auth_user ALTER username TYPE VARCHAR(255)") 45 | c.execute("ALTER TABLE hub_task DROP COLUMN traceback") 46 | c.execute("UPDATE hub_task set result=''") 47 | 48 | else: 49 | print "SQLITE3 database" 50 | 51 | c.execute("ALTER TABLE hub_worker ADD COLUMN max_tasks integer CHECK (max_tasks >= 0) NOT NULL DEFAULT 0") 52 | 53 | # alter auth_user table 54 | c.execute("DROP TABLE IF EXISTS auth_user_save") 55 | c.execute("ALTER TABLE auth_user RENAME TO auth_user_save") 56 | 57 | # alter hub_task table 58 | c.execute("DROP TABLE IF EXISTS hub_task_save") 59 | c.execute("ALTER TABLE hub_task RENAME TO hub_task_save") 60 | print 'syncing db models' 61 | management.call_command('syncdb') 62 | c = connection.cursor() 63 | 64 | # copy data 65 | c.execute("INSERT INTO auth_user (id,username,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined) SELECT id,username,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined FROM auth_user_save") 66 | c.execute("INSERT INTO hub_task (id,archive,owner_id,worker_id,parent_id,state,label,exclusive,method,args,result,comment,arch_id,channel_id,timeout,waiting,awaited,dt_created,dt_started,dt_finished,priority,weight,resubmitted_by_id,resubmitted_from_id,subtask_count) SELECT id,archive,owner_id,worker_id,parent_id,state,label,exclusive,method,args,'',comment,arch_id,channel_id,timeout,waiting,awaited,dt_created,dt_started,dt_finished,priority,weight,resubmitted_by_id,resubmitted_from_id,subtask_count FROM hub_task_save") 67 | 68 | print 'cleanup' 69 | c.execute("DROP TABLE auth_user_save") 70 | c.execute("DROP TABLE hub_task_save") 71 | c.execute("VACUUM") 72 | c.execute("ANALYZE") 73 | 74 | # change TASK_DIR ownership to apache 75 | print "recursively changing ownersip of '%s'" % settings.TASK_DIR 76 | run("chown -R apache:apache %s" % pipes.quote(settings.TASK_DIR), show_cmd=True) 77 | 78 | 79 | if __name__ == "__main__": 80 | if len(sys.argv) == 2 and sys.argv[1] == '--force': 81 | try: 82 | print "BEGIN TRANSACTION" 83 | transaction.enter_transaction_management() 84 | main() 85 | except Exception, ex: 86 | print "ROLLBACK TRANSACTION" 87 | transaction.rollback() 88 | raise 89 | else: 90 | print "COMMIT TRANSACTION" 91 | transaction.commit() 92 | else: 93 | print 'If you really want to convert db, please save old one and then run this command with parameter --force.' 94 | -------------------------------------------------------------------------------- /tools/reset_sequences.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | import django.db 5 | from django.db import connections 6 | from django.db.models import get_models 7 | from django.core.management.color import no_style 8 | 9 | 10 | def reset_sequences(connection, cursor, models=None): 11 | models = models or get_models(include_auto_created=True) 12 | for sql in connection.ops.sequence_reset_sql(no_style(), models): 13 | cursor.execute(sql) 14 | 15 | 16 | if __name__ == "__main__": 17 | for model in get_models(include_auto_created=True): 18 | db_for_write = django.db.router.db_for_write(model) 19 | connection = connections[db_for_write] 20 | cursor = connection.cursor() 21 | reset_sequences(connection, cursor, models=[model]) 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Don't forget to update GA config when changing this 3 | envlist = {py36, py38, py39, py310, py311}-{django2, django3}, {py38, py39, py310, py311, py312}-django4, {py310, py311, py312}-django5, py39-bandit 4 | skip_missing_interpreters = True 5 | 6 | [testenv] 7 | commands = pytest {posargs} 8 | whitelist_externals = make 9 | deps = 10 | -rtest-requirements.txt 11 | django2: Django~=2.2.0 # Django 2 LTS (EOL 4/2022) 12 | django3: Django~=3.2.0 # Django 3 LTS (EOL 4/2024) 13 | django4: Django~=4.2.0 # Django 4 LTS (EOL 4/2026) 14 | django5: Django~=5.0.0 15 | # for testing with python-rpm 16 | sitepackages = True 17 | 18 | [testenv:py39-django3-cov] 19 | passenv = GITHUB_* 20 | deps= 21 | {[testenv]deps} 22 | pytest-cov 23 | coveralls 24 | usedevelop=true 25 | commands= 26 | pytest --cov=kobo {posargs} 27 | coveralls 28 | # for testing with python-rpm 29 | sitepackages = True 30 | 31 | [testenv:py39-bandit] 32 | deps=-rtest-requirements.txt 33 | commands=bandit -r . -ll --exclude ./.tox 34 | --------------------------------------------------------------------------------