diff --git a/LICENSE b/COPYING similarity index 98% rename from LICENSE rename to COPYING index 22fbe5dbacbe58748f688cac277d917aebb467b4..d159169d1050894d3ea3b98e1c965c4058208fe1 100644 --- a/LICENSE +++ b/COPYING @@ -1,7 +1,7 @@ -GNU GENERAL PUBLIC LICENSE + GNU GENERAL PUBLIC LICENSE Version 2, June 1991 - Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - {description} - Copyright (C) {year} {fullname} + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -329,11 +329,11 @@ necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. - {signature of Ty Coon}, 1 April 1989 + <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. \ No newline at end of file +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..1ab418969d5330f5eae6e31bd4a4baf6546522b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,187 @@ +PKGNAME = dnfdaemon +DATADIR=/usr/share +SYSCONFDIR=/etc +PKGDIR = $(DATADIR)/$(PKGNAME) +PKGDIR_DNF = $(DATADIR)/$(PKGNAME_DNF) +ORG_NAME = org.baseurl.DnfSystem +ORG_RO_NAME = org.baseurl.DnfSession +SUBDIRS = client/dnfdaemon +VERSION=$(shell awk '/Version:/ { print $$2 }' ${PKGNAME}.spec) +PYTHON=python +GITDATE=git$(shell date +%Y%m%d) +VER_REGEX=\(^Version:\s*[0-9]*\.[0-9]*\.\)\(.*\) +BUMPED_MINOR=${shell VN=`cat ${PKGNAME}.spec | grep Version| sed 's/${VER_REGEX}/\2/'`; echo $$(($$VN + 1))} +NEW_VER=${shell cat ${PKGNAME}.spec | grep Version| sed 's/\(^Version:\s*\)\([0-9]*\.[0-9]*\.\)\(.*\)/\2${BUMPED_MINOR}/'} +NEW_REL=0.1.${GITDATE} +DIST=${shell rpm --eval "%{dist}"} + +all: subdirs + +subdirs: + for d in $(SUBDIRS); do make -C $$d; [ $$? = 0 ] || exit 1 ; done + +clean: + @rm -fv *~ *.tar.gz *.list *.lang + for d in $(SUBDIRS); do make -C $$d clean ; done + +install: + mkdir -p $(DESTDIR)$(DATADIR)/dbus-1/system-services + mkdir -p $(DESTDIR)$(DATADIR)/dbus-1/services + mkdir -p $(DESTDIR)$(SYSCONFDIR)/dbus-1/system.d + mkdir -p $(DESTDIR)$(DATADIR)/polkit-1/actions + mkdir -p $(DESTDIR)$(PKGDIR) + install -m644 dbus/$(ORG_NAME).service $(DESTDIR)$(DATADIR)/dbus-1/system-services/. + install -m644 dbus/$(ORG_RO_NAME).service $(DESTDIR)$(DATADIR)/dbus-1/services/. + install -m644 dbus/$(ORG_NAME).conf $(DESTDIR)$(SYSCONFDIR)/dbus-1/system.d/. + install -m644 policykit1/$(ORG_NAME).policy $(DESTDIR)$(DATADIR)/polkit-1/actions/. + install -m755 dnfdaemon/dnfdaemon-system.py $(DESTDIR)/$(PKGDIR_DNF)/dnfdaemon-system + install -m755 dnfdaemon/dnfdaemon-session.py $(DESTDIR)/$(PKGDIR_DNF)/dnfdaemon-session + install -m644 dnfdaemon/common.py $(DESTDIR)/$(PKGDIR_DNF)/. + for d in $(SUBDIRS); do make DESTDIR=$(DESTDIR) -C $$d install; [ $$? = 0 ] || exit 1; done + +uninstall: + rm -f $(DESTDIR)$(DATADIR)/dbus-1/system-services/$(ORG_NAME).* + rm -f $(DESTDIR)$(DATADIR)/dbus-1/services/$(ORG_RO_NAME).* + rm -f $(DESTDIR)$(SYSCONFDIR)/dbus-1/system.d/$(ORG_NAME).* + rm -r $(DESTDIR)$(DATADIR)/polkit-1/actions/$(ORG_NAME).* + rm -rf $(DESTDIR)/$(PKGDIR)/ + +selinux: + @$(MAKE) install + semanage fcontext -a -t rpm_exec_t $(DESTDIR)/$(PKGDIR)/dnfdaemon-system + restorecon $(DESTDIR)/$(PKGDIR_DNF)/dnfdaemon-system + + +# Run as root or you will get a password prompt for each test method :) +test-verbose: FORCE + @nosetests -v -s test/ + + +# Run as root or you will get a password prompt for each test method :) +test: FORCE + @nosetests -v test/ + +# Run as root or you will get a password prompt for each test method :) +test-system: FORCE + @nosetests -v test/test-system-api.py + +# Run as root or you will get a password prompt for each test method :) +test-session: FORCE + @nosetests -v test/test-session-api.py + + +# Run as root or you will get a password prompt for each test method :) +test-devel: FORCE + @nosetests -v -s test/unit-devel.py + +instdeps: + sudo yum install python-nose python3-gobject pygobject3 + +get-builddeps: + yum install perl-TimeDate gettext intltool rpmdevtools python-devel python3-devel + +archive: + @rm -rf ${PKGNAME}-${VERSION}.tar.gz + @git archive --format=tar --prefix=$(PKGNAME)-$(VERSION)/ HEAD | gzip -9v >${PKGNAME}-$(VERSION).tar.gz + @cp ${PKGNAME}-$(VERSION).tar.gz $(shell rpm -E '%_sourcedir') + @rm -rf ${PKGNAME}-${VERSION}.tar.gz + @echo "The archive is in ${PKGNAME}-$(VERSION).tar.gz" + +# needs perl-TimeDate for git2cl +changelog: + @git log --pretty --numstat --summary --after=2008-10-22 | tools/git2cl > ChangeLog + +upload: FORCE + @scp ~/rpmbuild/SOURCES/${PKGNAME}-${VERSION}.tar.gz fedorahosted.org:yumex + +release: + @git commit -a -m "bumped version to $(VERSION)" + @git push + @git tag -f -m "Added ${PKGNAME}-${VERSION} release tag" ${PKGNAME}-${VERSION} + @git push --tags origin + @$(MAKE) archive + @$(MAKE) upload + +test-cleanup: + @rm -rf ${PKGNAME}-${VERSION}.test.tar.gz + @echo "Cleanup the git release-test local branch" + @git checkout -f + @git checkout master + @git branch -D release-test + +show-vars: + @echo ${GITDATE} + @echo ${BUMPED_MINOR} + @echo ${NEW_VER}-${NEW_REL} + +test-release: + @git checkout -b release-test + # +1 Minors version and add 0.1-gitYYYYMMDD release + @cat ${PKGNAME}.spec | sed -e 's/${VER_REGEX}/\1${BUMPED_MINOR}/' -e 's/\(^Release:\s*\)\([0-9]*\)\(.*\)./\10.1.${GITDATE}%{?dist}/' > ${PKGNAME}-test.spec ; mv ${PKGNAME}-test.spec ${PKGNAME}.spec + @git commit -a -m "bumped ${PKGNAME} version ${NEW_VER}-${NEW_REL}" + # Make Changelog + @git log --pretty --numstat --summary | ./tools/git2cl > ChangeLog + @git commit -a -m "updated ChangeLog" + # Make archive + @rm -rf ${PKGNAME}-${NEW_VER}.tar.gz + @git archive --format=tar --prefix=$(PKGNAME)-$(NEW_VER)/ HEAD | gzip -9v >${PKGNAME}-$(NEW_VER).tar.gz + # Build RPMS + @rpmbuild -ta ${PKGNAME}-${NEW_VER}.tar.gz + @$(MAKE) test-cleanup + +test-inst: + @$(MAKE) test-release + sudo yum install ~/rpmbuild/RPMS/noarch/*${PKGNAME}-${NEW_VER}*.rpm + +test-reinst: + @$(MAKE) test-release + sudo yum reinstall ~/rpmbuild/RPMS/noarch/*${PKGNAME}-${NEW_VER}*.rpm + +rpm: + @$(MAKE) archive + @rpmbuild -ba ${PKGNAME}.spec + +test-builds: + @$(MAKE) test-release + @ssh timlau.fedorapeople.org rm -f public_html/files/${PKGNAME}/* + @scp ${PKGNAME}-${NEW_VER}.tar.gz timlau.fedorapeople.org:public_html/files/${PKGNAME}/${PKGNAME}-${NEW_VER}-${GITDATE}.tar.gz + @scp ~/rpmbuild/RPMS/noarch/${PKGNAME}-${NEW_VER}*.rpm timlau.fedorapeople.org:public_html/files/${PKGNAME}/. + @scp ~/rpmbuild/RPMS/noarch/python-${PKGNAME}-${NEW_VER}*.rpm timlau.fedorapeople.org:public_html/files/${PKGNAME}/. + @scp ~/rpmbuild/RPMS/noarch/python3-${PKGNAME}-${NEW_VER}*.rpm timlau.fedorapeople.org:public_html/files/${PKGNAME}/. + @scp ~/rpmbuild/SRPMS/${PKGNAME}-${NEW_VER}*.rpm timlau.fedorapeople.org:public_html/files/${PKGNAME}/. + +review: + @ssh timlau.fedorapeople.org rm -f public_html/files/${PKGNAME}/* + @scp ~/rpmbuild/SRPMS/${PKGNAME}-${VERSION}*.src.rpm timlau.fedorapeople.org:public_html/files/${PKGNAME}/. + @scp ${PKGNAME}.spec timlau.fedorapeople.org:public_html/files/${PKGNAME}/. + + +exit-session: + @/usr/bin/dbus-send --session --print-reply --dest="org.baseurl.DnfSession" / org.baseurl.DnfSession.Exit + +exit-system: + @sudo /usr/bin/dbus-send --system --print-reply --dest="org.baseurl.DnfSystem" / org.baseurl.DnfSystem.Exit + +exit-both: + @/usr/bin/dbus-send --session --print-reply --dest="org.baseurl.DnfSession" / org.baseurl.DnfSession.Exit + @sudo /usr/bin/dbus-send --system --print-reply --dest="org.baseurl.DnfSystem" / org.baseurl.DnfSystem.Exit + +start-session: + dnfdaemon/dnfdaemon-session.py -d -v --notimeout + +kill-both: + @-sudo killall -9 -r "dnfdaemon-system\.py" &> /dev/null + @-sudo killall -9 -r "dnfdaemon-session\.py" &> /dev/null + + +start-system: + sudo dnfdaemon/dnfdaemon-system.py -d -v --notimeout + +monitor-session: + dbus-monitor "type='signal',sender='org.baseurl.DnfSession',interface='org.baseurl.DnfSession'" + +monitor-system: + dbus-monitor "type='signal',sender='org.baseurl.DnfSystem',interface='org.baseurl.DnfSystem'" + +FORCE: + diff --git a/README.md b/README.md index 6fb14cc608fc52827f42ecbc0548e1872ccc2c56..05e1f59c39408b8fc0147ff132242b84de23a200 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,101 @@ dnf-daemon -========== +=========== + +dnf-daemon is a 2 DBus services there make part for dnf's API available for application via DBus calls. + +There is a DBus session bus service runnning as current user for performing readonly actions. + +There is a DBus system bus service runnning as root for performing actions there is making changes to the system + +This make it easy to do packaging action from your application no matter what language it is written in, as long as there +is DBus binding for it. + +dnf-daemon uses PolicyKit for authentication for the system service, so when you call one of the commands (as normal users) you will get a +PolicyKit dialog to ask for password of a priviledged user like root. + +**dnf-daemon is still under heavy development and the API is not stable or complete yet** + +Source overview +---------------- + + dnfdaemon/ Contains the daemon python source + client/ Contains the client API bindings for python 2.x & 3.x + test/ Unit test for the daemon and python bindings + dbus/ DBus system service setup files + policykit1/ PolicyKit authentication setup files + + + +How to install services and python bindings: +----------------------------------------------- + +Run the following + +``` + git clone ... + cd dnf-daemon + make test-inst +``` + + +How to test: +------------- + +just run: + + make test-verbose + +to run the unit test with output to console + +or this to just run the unit tests. + + make test + +To make the daemons shutdown +------------------------------- + +Session: + + make exit-session + +System + + make exit-system + +Both + + make exit-both + + +to run the daemons in debug mode from checkout: +------------------------------------------------ + +session (readonly as current user) + + make run-session + +system (as root) + + make run-system + + +API Definitions: +==================================== + +The dnfdaemon api is documented [here](http://timlau.fedorapeople.org/dnfdaemon) + +The API is under development, so it might change, when we hit version 1.0, API methods will be frozen and +API method names, parameters and return types will not change in future releases, new API can be added, +but the old ones stays as is + + + +API Addition Checklist: +==================================== +* Add the new API methods to dnfdaemon-system.py and optional dnfdaemon-session.py +* Add client api method in DnfDaemonBase if it is available in both daemon + or in DnfDaemonClient is it is a system only api. +* Add unit tests for the api in test/test-system-api.py and optional to test/test-system-api.py if it exists in the session api +* Update docs/server.rst and docs/client-python.api ( add new api method to members ) +* All unit tests must pass (make test) before pushing to github -DBus daemon for doing package action with the dnf package manager diff --git a/client/dnfdaemon/Makefile b/client/dnfdaemon/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..2e6b51b0a018a99c02fdc5fa650a3646697e87c9 --- /dev/null +++ b/client/dnfdaemon/Makefile @@ -0,0 +1,27 @@ +PYTHON=python +PYTHON3=python3 +PACKAGE = dnfdaemon +PYFILES = $(wildcard *.py) +PYVER := $(shell $(PYTHON) -c 'import sys; print("%.3s" %(sys.version))') +PYSYSDIR := $(shell $(PYTHON) -c 'import sys; print(sys.prefix)') +PYLIBDIR = $(PYSYSDIR)/lib/python$(PYVER) +PKGDIR = $(PYLIBDIR)/site-packages/$(PACKAGE) +PYVER3 := $(shell $(PYTHON3) -c 'import sys; print("%.3s" %(sys.version))') +PYSYSDIR3 := $(shell $(PYTHON3) -c 'import sys; print(sys.prefix)') +PYLIBDIR3 = $(PYSYSDIR3)/lib/python$(PYVER3) +PKGDIR3 = $(PYLIBDIR3)/site-packages/$(PACKAGE) + +all: + echo "Nothing to do" + +clean: + rm -rf *.pyc *.pyo *~ __pycache__/ + + +install: + mkdir -p $(DESTDIR)/$(PKGDIR) + mkdir -p $(DESTDIR)/$(PKGDIR3) + for p in $(PYFILES) ; do \ + install -m 644 $$p $(DESTDIR)/$(PKGDIR)/$$p; \ + install -m 644 $$p $(DESTDIR)/$(PKGDIR3)/$$p; \ + done diff --git a/client/dnfdaemon/__init__.py b/client/dnfdaemon/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b529469d1375f446dfc3f65d513873cc637d1962 --- /dev/null +++ b/client/dnfdaemon/__init__.py @@ -0,0 +1,728 @@ +# coding: utf-8 +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# (C) 2013 - Tim Lauridsen <timlau@fedoraproject.org> + +""" +This is a Python 2.x & 3.x client API for the dnf-daemon Dbus Service + +This module gives a simple pythonic interface to doing Yum package action using the +yum-daemon Dbus service. + +It use async call to the dnf-daemon, so signal can be catched and a Gtk gui dont get unresonsive + +There is 2 classes :class:`DnfDaemonClient` & :class:`DnfDaemonReadOnlyClient` + +:class:`DnfDaemonClient` uses a system DBus service running as root and can make chages to the system. + +:class:`DnfDaemonReadOnlyClient` uses a session DBus service running as current user and can only do readonly +actions. + +Usage: (Make your own subclass based on :class:`dnfdaemon.DnfDaemonClient` and overload the signal handlers):: + + + from dnfdaemon import DnfDaemonClient + + class MyClient(DnfDaemonClient): + + def __init(self): + DnfDaemonClient.__init__(self) + # Do your stuff here + + def on_UpdateProgress(self,name,frac,fread,ftime): + # Do your stuff here + pass + + def on_TransactionEvent(self,event, data): + # Do your stuff here + pass + + def on_RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total): + # Do your stuff here + pass + + def on_GPGImport(self, pkg_id, userid, hexkeyid, keyurl, timestamp ): + # do stuff here + pass + + +Usage: (Make your own subclass based on :class:`dnfdaemon.DnfDaemonReadOnlyClient` and overload the signal handlers):: + + + from dnfdaemon import DnfDaemonReadOnlyClient + + class MyClient(DnfDaemonReadOnlyClient): + + def __init(self): + DnfDaemonClient.__init__(self) + # Do your stuff here + + def on_UpdateProgress(self,name,frac,fread,ftime): + # Do your stuff here + pass + +""" + +import json +import sys +import re +import weakref +import logging + +logger = logging.getLogger("dnfdaemon.client") + +from gi.repository import Gio, GObject + +ORG = 'org.baseurl.DnfSystem' +INTERFACE = ORG + +ORG_READONLY = 'org.baseurl.DnfSession' +INTERFACE_READONLY = ORG_READONLY + +DBUS_ERR_RE = re.compile('^GDBus.Error:([\w\.]*): (.*)$') + +############################################################################### +# Exceptions +############################################################################### + + +class DaemonError(Exception): + 'Error from the backend' + +class AccessDeniedError(DaemonError): + 'User press cancel button in policykit window' + +class LockedError(DaemonError): + 'The Yum daemon is locked' + +class TransactionError(DaemonError): + 'The yum transaction failed' + +############################################################################### +# Helper Classes +############################################################################### + + +class DBus: + ''' + Helper class to work with GDBus in a easier way + ''' + def __init__(self, conn): + self.conn = conn + + def get(self, bus, obj, iface=None): + if iface is None: + iface = bus + return Gio.DBusProxy.new_sync( + self.conn, 0, None, bus, obj, iface, None + ) + + def get_async(self, callback, bus, obj, iface=None): + if iface is None: + iface = bus + Gio.DBusProxy.new( + self.conn, 0, None, bus, obj, iface, None, callback, None + ) + +class WeakMethod: + ''' + helper class to work with a weakref class method + ''' + def __init__(self, inst, method): + self.proxy = weakref.proxy(inst) + self.method = method + + def __call__(self, *args): + return getattr(self.proxy, self.method)(*args) + + +# Get the system bus +system = DBus(Gio.bus_get_sync(Gio.BusType.SYSTEM, None)) +session = DBus(Gio.bus_get_sync(Gio.BusType.SESSION, None)) + +############################################################################### +# Main Client Class +############################################################################### +class DnfDaemonBase: + def __init__(self, bus, org, interface): + self.bus = bus + self.dbus_org = org + self.dbus_interface = interface + self.daemon = self._get_daemon(bus, org, interface) + logger.debug("%s daemon loaded - version : %s" % (interface,self.daemon.GetVersion())) + + def _get_daemon(self,bus, org, interface): + ''' Get the daemon dbus proxy object''' + try: + proxy = bus.get( org, "/", interface) + proxy.GetVersion() # Get daemon version, to check if it is alive + proxy.connect('g-signal', WeakMethod(self, '_on_g_signal')) # Connect the Dbus signal handler + return proxy + except Exception as err: + self._handle_dbus_error(err) + + def _on_g_signal(self, proxy, sender, signal, params): + ''' + DBUS signal Handler + :param proxy: DBus proxy + :param sender: DBus Sender + :param signal: DBus signal + :param params: DBus signal parameters + ''' + args = params.unpack() # unpack the glib variant + self.handle_dbus_signals(proxy, sender, signal, args) + + def handle_dbus_signals(self, proxy, sender, signal, args): + """ + Overload in child class + """ + pass + + def _handle_dbus_error(self, err): + ''' + Parse error from service and raise python Exceptions + :param err: + :type err: + ''' + exc, msg = self._parse_error() + if exc != "": + logger.error("Exception : %s",exc) + logger.error(" message : %s",msg) + if exc == self.dbus_org+'.AccessDeniedError': + raise AccessDeniedError(msg) + elif exc == self.dbus_org+'.LockedError': + raise LockedError(msg) + elif exc == self.dbus_org+'.TransactionError': + raise TransactionError(msg) + elif exc == self.dbus_org+'.NotImplementedError': + raise TransactionError(msg) + else: + raise DaemonError(str(err)) + + def _parse_error(self): + ''' + parse values from a DBus releated exception + ''' + (type, value, traceback) = sys.exc_info() + res = DBUS_ERR_RE.match(str(value)) + if res: + return res.groups() + return "","" + + def _return_handler(self, obj, result, user_data): + ''' + Async DBus call, return handler + :param obj: + :type obj: + :param result: + :type result: + :param user_data: + :type user_data: + ''' + if isinstance(result, Exception): + #print(result) + user_data['result'] = None + user_data['error'] = result + else: + user_data['result'] = result + user_data['error'] = None + user_data['main_loop'].quit() + + def _get_result(self, user_data): + ''' + Get return data from async call or handle error + :param user_data: + :type user_data: + ''' + if user_data['error']: # Errors + self._handle_dbus_error(user_data['error']) + else: + return user_data['result'] + + def _run_dbus_async(self, cmd, *args): + ''' + Make an async call to a DBus method in the yumdaemon service + :param cmd: method to run + :type cmd: string + ''' + main_loop = GObject.MainLoop() + data = {'main_loop': main_loop} + func = getattr(self.daemon,cmd) + func(*args, result_handler=self._return_handler, user_data=data, timeout=GObject.G_MAXINT) # timeout = infinite + data['main_loop'].run() + result = self._get_result(data) + return result + + + def _run_dbus_sync(self, cmd, *args): + ''' + Make a sync call to a DBus method in the yumdaemon service + :param cmd: + :type cmd: + ''' + func = getattr(self.daemon,cmd) + return func(*args) + +############################################################################### +# Dbus Signal Handlers (Overload in child class) +############################################################################### + + def on_UpdateProgress(self,name,frac,fread,ftime): + if name.startswith('repomd'): + print("repo metadata : %.2f" % frac) + elif "/" in name: + repo,file = name.split("/",1) + print("getting %s from %s repository : %.2f" % (file,repo,frac)) + else: + print("downloading : %s %s" % (name,frac)) + + def on_TransactionEvent(self,event, data): + print("TransactionEvent : %s" % event) + if data: + print("Data :\n", data) + + def on_RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total): + print("RPMProgress : %s %s" % (action, package)) + + def on_GPGImport(self, pkg_id, userid, hexkeyid, keyurl, timestamp ): + values = (pkg_id, userid, hexkeyid, keyurl, timestamp) + print("on_GPGImport : %s" % (repr(values))) + + def on_DownloadStart(self, num_files, num_bytes): + ''' Starting a new parallel download batch ''' + values = (num_files, num_bytes) + print("on_DownloadStart : %s" % (repr(values))) + + def on_DownloadProgress(self, name, frac, total_frac, total_files): + ''' Progress for a single instance in the batch ''' + values = (name, frac, total_frac, total_files) + print("on_DownloadProgress : %s" % (repr(values))) + + def on_DownloadEnd(self, name, status, msg): + ''' Download of af single instace ended ''' + values = (name, status, msg) + print("on_DownloadEnd : %s" % (repr(values))) + + def on_RepoMetaDataProgress(self, name, frac): + ''' Repository Metadata Download progress ''' + values = (name, frac) + print("on_RepoMetaDataProgress : %s" % (repr(values))) + +############################################################################### +# API Methods +############################################################################### + + + def Lock(self): + ''' + Get the yum lock, this give exclusive access to the daemon and yum + this must always be called before doing other actions + ''' + try: + return self._run_dbus_async('Lock') + except Exception as err: + self._handle_dbus_error(err) + + def Unlock(self): + ''' + Release the yum lock + ''' + try: + self.daemon.Unlock() + except Exception as err: + self._handle_dbus_error(err) + + def SetWatchdogState(self,state): + ''' + Set the Watchdog state + + :param state: True = Watchdog active, False = Watchdog disabled + :type state: boolean (b) + ''' + try: + self.daemon.SetWatchdogState("(b)",state) + except Exception as err: + self._handle_dbus_error(err) + + def GetPackageWithAttributes(self, pkg_filter, fields): + ''' + Get a list of pkg list for a given package filter + each pkg list contains [pkg_id, field,....] where field is a atrribute of the package object + Ex. summary, size etc. + + :param pkg_filter: package filter ('installed','available','updates','obsoletes','recent','extras') + :type pkg_filter: string + :param fields: yum package objects attributes to get. + :type fields: list of strings + ''' + result = self._run_dbus_async('GetPackageWithAttributes','(sas)',pkg_filter, fields) + return json.loads(result) + + + def GetRepositories(self, repo_filter): + ''' + Get a list of repository ids where name matches a filter + + :param repo_filter: filter to match + :return: list of repo id's + ''' + result = self._run_dbus_async('GetRepositories','(s)',repo_filter) + return [str(r) for r in result] + + + def GetRepo(self, repo_id): + ''' + Get a dictionary of information about a given repo id. + + :param repo_id: repo id to get information from + :return: dictionary with repo info + ''' + result = json.loads(self._run_dbus_async('GetRepo','(s)',repo_id)) + return result + + def SetEnabledRepos(self, repo_ids): + ''' + Enabled a list of repositories, disabled all other repos + + :param repo_ids: list of repo ids to enable + :param sender: + ''' + self._run_dbus_async('SetEnabledRepos','(as)', repo_ids) + + + def GetConfig(self, setting): + ''' + Read a config setting from yum.conf + + :param setting: setting to read + :type setting: string + ''' + result = json.loads(self._run_dbus_async('GetConfig','(s)',setting)) + return result + + def GetAttribute(self, pkg_id, attr): + ''' + Get yum package attribute (description, filelist, changelog etc) + + :param pkg_id: pkg_id to get attribute from + :param attr: name of attribute to get + ''' + result = self._run_dbus_async('GetAttribute','(ss)',pkg_id, attr) + if result == ':none': # illegal attribute + result = None + elif result == ':not_found': # package not found + result = None # FIXME: maybe raise an exception + else: + result = json.loads(result) + return result + + def GetUpdateInfo(self, pkg_id): + ''' + Get Updateinfo for a package + + :param pkg_id: pkg_id to get update info from + ''' + result = self._run_dbus_async('GetUpdateInfo','(s)',pkg_id) + return json.loads(result) + + def GetPackages(self, pkg_filter): + ''' + Get a list of pkg ids for a given filter (installed, updates ..) + + :param pkg_filter: package filter ('installed','available','updates','obsoletes','recent','extras') + :type pkg_filter: string + :return: list of pkg_id's + :rtype: list of strings + ''' + return self._run_dbus_async('GetPackages','(s)',pkg_filter) + + + def GetPackagesByName(self, name, newest_only=True): + ''' + Get a list of pkg ids for starts with name + + :param name: name prefix to match + :type name: string + :param newest_only: show only the newest match or every match. + :type newest_only: boolean + :return: list of pkg_is's + ''' + return self._run_dbus_async('GetPackagesByName','(sb)',name, newest_only) + + + def GetGroups(self): + ''' + Get list of Groups + ''' + return json.loads(self._run_dbus_async('GetGroups')) + + def GetGroupPackages(self, grp_id, grp_flt): + ''' + Get packages in a group + + :param grp_id: the group id to get packages for + :param grp_flt: the filter ('all' = all packages ,'default' = packages to be installed, before the group is installed) + ''' + return self._run_dbus_async('GetGroupPackages', '(ss)', grp_id, grp_flt) + + + def Search(self, fields, keys, match_all, newest_only, tags): + ''' + Search for packages where keys is matched in fields + + :param fields: yum po attributes to search in + :type fields: list of strings + :param keys: keys to search for + :type keys: list of strings + :param match_all: match all keys or only one + :type match_all: boolean + :param newest_only: return only the newest version of packages + :type newest_only: boolean + :param tags: search pkgtags + :type tags: boolean + :return: list of pkg_id's + + ''' + return self._run_dbus_async('Search','(asasbbb)',fields, keys, match_all, newest_only, tags) + + def Exit(self): + ''' + End the daemon + ''' + self._run_dbus_async('Exit') + +############################################################################### +# Helper methods +############################################################################### + + def to_pkg_tuple(self, id): + ''' split the pkg_id into a tuple''' + (n, e, v, r, a, repo_id) = str(id).split(',') + return (n, e, v, r, a, repo_id) + + def to_txmbr_tuple(self, id): + ''' split the txmbr_id into a tuple''' + (n, e, v, r, a, repo_id, ts_state) = str(id).split(',') + return (n, e, v, r, a, repo_id, ts_state) + + + +class DnfDaemonReadOnlyClient(DnfDaemonBase): + ''' + A class to communicate with the yumdaemon DBus services in a easy way + ''' + + def __init__(self): + DnfDaemonBase.__init__(self, session,ORG_READONLY,INTERFACE_READONLY) + + def handle_dbus_signals(self, proxy, sender, signal, args): + ''' + DBUS signal Handler + ''' + if signal == "UpdateProgress": + self.on_UpdateProgress(*args) + else: + print("Unhandled Signal : "+signal," Param: ",args) + + +class DnfDaemonClient(DnfDaemonBase): + ''' + A class to communicate with the yumdaemon DBus services in a easy way + ''' + + def __init__(self): + DnfDaemonBase.__init__(self, system,ORG,INTERFACE) + + def handle_dbus_signals(self, proxy, sender, signal, args): + ''' + DBUS signal Handler + ''' + if signal == "UpdateProgress": + self.on_UpdateProgress(*args) + elif signal == "TransactionEvent": + self.on_TransactionEvent(*args) + elif signal == "RPMProgress": + self.on_RPMProgress(*args) + elif signal == "GPGImport": + self.on_GPGImport(*args) + elif signal == "DownloadStart": + self.on_DownloadStart(*args) + elif signal == "DownloadEnd": + self.on_DownloadEnd(*args) + elif signal == "DownloadProgress": + self.on_DownloadProgress(*args) + elif signal == "RepoMetaDataProgress": + self.on_RepoMetaDataProgress(*args) + else: + print("Unhandled Signal : "+signal," Param: ",args) + +############################################################################### +# API Methods +############################################################################### + + def SetConfig(self, setting, value): + ''' + set a yum config setting + + :param setting: yum conf setting to set + :param value: value to set + ''' + result = self._run_dbus_async('SetConfig','(ss)',setting, json.dumps(value)) + return result + + + def ClearTransaction(self): + ''' + Clear the current transaction + ''' + return self._run_dbus_async('ClearTransaction') + + + + def GetTransaction(self): + ''' + Get the current transaction + + :return: the current transaction + ''' + return self._run_dbus_async('GetTransaction') + + + def AddTransaction(self, id, action): + ''' + Add an package to the current transaction + + :param id: package id for the package to add + :type id: string + :param action: the action to perform ( install, update, remove, obsolete, reinstall, downgrade, localinstall ) + :type action: string + ''' + return self._run_dbus_async('AddTransaction','(ss)',id, action) + + + def Install(self, pattern): + ''' + Do a install <pattern string>, same as yum install <pattern string> + + :param pattern: package pattern to install + :type pattern: string + ''' + return json.loads(self._run_dbus_async('Install','(s)',pattern)) + + + def Remove(self, pattern): + ''' + Do a install <pattern string>, same as yum remove <pattern string> + + :param pattern: package pattern to remove + :type pattern: string + ''' + return json.loads(self._run_dbus_async('Remove','(s)',pattern)) + + + def Update(self, pattern): + ''' + Do a update <pattern string>, same as yum update <pattern string> + + :param pattern: package pattern to update + :type pattern: string + + ''' + return json.loads(self._run_dbus_async('Update','(s)',pattern)) + + + def Reinstall(self, pattern): + ''' + Do a reinstall <pattern string>, same as yum reinstall <pattern string> + + :param pattern: package pattern to reinstall + :type pattern: string + + ''' + return json.loads(self._run_dbus_async('Reinstall','(s)',pattern)) + + + def Downgrade(self, pattern): + ''' + Do a install <pattern string>, same as yum remove <pattern string> + + :param pattern: package pattern to downgrade + :type pattern: string + ''' + return json.loads(self._run_dbus_async('Downgrade','(s)',pattern)) + + + + def BuildTransaction(self): + ''' + Get a list of pkg ids for the current availabe updates + ''' + return json.loads(self._run_dbus_async('BuildTransaction')) + + + def RunTransaction(self): + ''' + Get a list of pkg ids for the current availabe updates + ''' + return self._run_dbus_async('RunTransaction') + + + def GetHistoryByDays(self, start_days, end_days): + ''' + Get History transaction in a interval of days from today + + :param start_days: start of interval in days from now (0 = today) + :type start_days: integer + :param end_days:end of interval in days from now + :type end_days: integer + :return: a list of (transaction is, date-time) pairs + :type sender: json encoded string + ''' + value = self._run_dbus_async('GetHistoryByDays','(ii)', start_days, end_days) + return json.loads(value) + + def HistorySearch(self, pattern): + ''' + Search the history for transaction matching a pattern + + :param pattern: patterne to match + :type pattern: list (strings) + :return: list of (tid,isodates) + :type sender: json encoded string + ''' + value = self._run_dbus_async('HistorySearch','(as)', pattern) + return json.loads(value) + + def GetHistoryPackages(self, tid): + ''' + Get packages from a given yum history transaction id + + :param tid: history transaction id + :type tid: integer + :return: list of (pkg_id, state, installed) pairs + :rtype: list + ''' + value = self._run_dbus_async('GetHistoryPackages','(i)',tid) + return json.loads(value) + + def ConfirmGPGImport(self, hexkeyid, confirmed): + ''' + Confirm import of at GPG Key by yum + + :param hexkeyid: hex keyid for GPG key + :param confirmed: confirm import of key (True/False) + ''' + self._run_dbus_async('ConfirmGPGImport','(si)',hexkeyid, confirmed) + diff --git a/dbus/org.baseurl.DnfSession.service b/dbus/org.baseurl.DnfSession.service new file mode 100644 index 0000000000000000000000000000000000000000..b571126785ed4fa03392f5faa1bba10d2cf79ea5 --- /dev/null +++ b/dbus/org.baseurl.DnfSession.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.baseurl.DnfSession +Exec=/usr/share/dnfdaemon/yumdaemon-session diff --git a/dbus/org.baseurl.DnfSystem.conf b/dbus/org.baseurl.DnfSystem.conf new file mode 100644 index 0000000000000000000000000000000000000000..6608e9543e4c37f974919e04d725549d33c9b3e5 --- /dev/null +++ b/dbus/org.baseurl.DnfSystem.conf @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE busconfig PUBLIC + "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <!-- Only root can become service owner --> + <policy user="root"> + <allow own="org.baseurl.DnfSystem"/> + <allow send_destination="org.baseurl.DnfSystem"/> + <allow send_interface="org.baseurl.DnfSystem"/> + </policy> + + <!-- Anyone can invoke method --> + <policy context="default"> + <allow send_destination="org.baseurl.DnfSystem"/> + <allow send_interface="org.baseurl.DnfSystem"/> + </policy> +</busconfig> diff --git a/dbus/org.baseurl.DnfSystem.service b/dbus/org.baseurl.DnfSystem.service new file mode 100644 index 0000000000000000000000000000000000000000..da910db292ce57aca4260b49ea815fe642dd58f3 --- /dev/null +++ b/dbus/org.baseurl.DnfSystem.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.baseurl.DnfSystem +Exec=/usr/share/dnfdaemon/dnfdaemon-system +User=root diff --git a/dnfdaemon.spec b/dnfdaemon.spec new file mode 100644 index 0000000000000000000000000000000000000000..91d848971fd65c7b80c1fba29cea1e09726956ea --- /dev/null +++ b/dnfdaemon.spec @@ -0,0 +1,87 @@ +%global dnf_org org.baseurl.Dnf + +Name: dnfaemon +Version: 0.1.0 +Release: 1%{?dist} +Summary: DBus daemon for yum package actions + +License: GPLv2+ +URL: https://github.com/timlau/dnf-daemon +Source0: https://fedorahosted.org/releases/y/u/yumex/%{name}-%{version}.tar.gz + +BuildArch: noarch +BuildRequires: python2-devel +Requires: dbus-python +Requires: dnf >= 0.4.17 +Requires: polkit + +Requires(post): policycoreutils-python +Requires(postun): policycoreutils-python + +%description +Dbus daemon for performing package actions with the dnf package manager + +%prep +%setup -q + + +%build +# Nothing to build + +%install +make install DESTDIR=$RPM_BUILD_ROOT DATADIR=%{_datadir} SYSCONFDIR=%{_sysconfdir} + +%package -n python3-%{name} +Summary: Python 3 api for communicating with the dnf-daemon DBus service +Group: Applications/System +BuildRequires: python3-devel +Requires: %{name} = %{version}-%{release} +Requires: python3-gobject + +%description -n python3-%{name} +Python 3 api for communicating with the dnf-daemon DBus service + + +%files -n python3-%{name} +%{python3_sitelib}/%{name}/ + +%package -n python-%{name} +Summary: Python 2 api for communicating with the dnf-daemon DBus service +Group: Applications/System +BuildRequires: python2-devel +Requires: %{name} = %{version}-%{release} +Requires: pygobject3 + +%description -n python-%{name} +Python 2 api for communicating with the dnf-daemon DBus service + + +%files -n python-%{name} +%{python_sitelib}/%{name}/ + +# apply the right selinux file context +# http://fedoraproject.org/wiki/PackagingDrafts/SELinux#File_contexts + +%post +semanage fcontext -a -t rpm_exec_t '%{_datadir}/%{name}/%{name}-system' 2>/dev/null || : +restorecon -R %{_datadir}/%{name}/%{name}-system || : + +%postun +if [ $1 -eq 0 ] ; then # final removal +semanage fcontext -d -t rpm_exec_t '%{_datadir}/%{name}/%{name}-system' 2>/dev/null || : +fi + +%files +%doc README.md examples/ ChangeLog COPYING +%{_datadir}/dbus-1/system-services/%{dnf_org}* +%{_datadir}/dbus-1/services/%{dnf_org}* +%{_datadir}/%{name}/ +%{_datadir}/polkit-1/actions/%{dnf_org}* +# this should not be edited by the user, so no %%config +%{_sysconfdir}/dbus-1/system.d/%{dnf_org}* + + +%changelog + +* Sat Mar 08 2014 Tim Lauridsen <timlau@fedoraproject.org> 0.10-1 +- Initial rpm for dnfdaemon diff --git a/dnfdaemon/common.py b/dnfdaemon/common.py new file mode 100644 index 0000000000000000000000000000000000000000..35045c7b4908faadcb7a0ce17aeb3f128ccf564a --- /dev/null +++ b/dnfdaemon/common.py @@ -0,0 +1,855 @@ +# -*- coding: utf-8 -*- +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# (C) 2013 - Tim Lauridsen <timlau@fedoraproject.org> + +""" +Common stuff for the dnfdaemon dbus services +""" +from __future__ import print_function +from __future__ import absolute_import +import dbus +import dbus.service +import dbus.glib +import gobject +import json +import logging +from datetime import datetime + +import sys +from time import time + +import dnf +import dnf.yum +import dnf.const +import dnf.conf +import dnf.subject +from dnf.callback import DownloadProgress, STATUS_OK +import hawkey + +FAKE_ATTR = ['downgrades','action','pkgtags','changelog'] +NONE = json.dumps(None) + + +#------------------------------------------------------------------------------ Callback handlers + +logger = logging.getLogger('dnfdaemon.service') + +def Logger(func): + """ + This decorator catch yum exceptions and send fatal signal to frontend + """ + def newFunc(*args, **kwargs): + logger.debug("%s started args: %s " % (func.__name__, repr(args[1:]))) + rc = func(*args, **kwargs) + logger.debug("%s ended" % func.__name__) + return rc + + newFunc.__name__ = func.__name__ + newFunc.__doc__ = func.__doc__ + newFunc.__dict__.update(func.__dict__) + return newFunc + +class DownloadCallback: + ''' + Yum Download callback handler class + the updateProgress will be called while something is being downloaded + ''' + def __init__(self): + pass + + def updateProgress(self,name,frac,fread,ftime): + ''' + Update the progressbar + :param name: filename + :param frac: Progress fracment (0 -> 1) + :param fread: formated string containing BytesRead + :param ftime : formated string containing remaining or elapsed time + ''' + # send a DBus signal with progress info + self.UpdateProgress(name,frac,fread,ftime) + +# Parallel Download Progress + def downloadStart(self, num_files, num_bytes): + ''' Starting a new parallel download batch ''' + self.DownloadStart(num_files, num_bytes) # send a signal + + def downloadProgress(self, name, frac, total_frac, total_files): + ''' Progress for a single instance in the batch ''' + self.DownloadProgress(name, frac, total_frac, total_files) # send a signal + + def downloadEnd(self, name, status, msg): + ''' Download of af single instace ended ''' + self.DownloadEnd(name, status, msg) # send a signal + + def repoMetaDataProgress(self, name, frac): + ''' Repository Metadata Download progress ''' + self.RepoMetaDataProgress( name, frac) + + + +class DnfDaemonBase(dbus.service.Object, DownloadCallback): + + def __init__(self, mainloop): + self.logger = logging.getLogger('dnfdaemon.base') + self.mainloop = mainloop # use to terminate mainloop + self.authorized_sender = set() + self._lock = None + self._base = None + self._can_quit = True + self._is_working = False + self._watchdog_count = 0 + self._watchdog_disabled = False + self._timeout_idle = 20 # time to daemon is closed when unlocked + self._timeout_locked = 600 # time to daemon is closed when locked and not working + self._obsoletes_list = None # Cache for obsoletes + + + def _get_obsoletes(self): + ''' Cache a list of obsoletes''' + if not self._obsoletes_list: + self._obsoletes_list = list(self.base.packages.obsoletes) + return self._obsoletes_list + + @property + def base(self): + ''' + yumbase property so we can auto initialize it if not defined + ''' + if not self._base: + self._get_base() + return self._base + +#=============================================================================== +# Helper methods for api methods both in system & session +# Search -> _search etc +#=============================================================================== + + + def _search(self, fields, keys, match_all, newest_only, tags): + ''' + Search for for packages, where given fields contain given key words + (Helper for Search) + + :param fields: list of fields to search in + :param keys: list of keywords to search for + :param match_all: match all flag, if True return only packages matching all keys + :param newest_only: return only the newest version of a package + :param tags: seach pkgtags + ''' + # FIXME: Add support for search in pkgtags, when supported in dnf + showdups = not newest_only + result = self.base.search(fields, keys, match_all, showdups) + pkg_ids = self._to_package_id_list(result) + return pkg_ids + + + + def _get_packages_by_name(self, name, newest_only): + ''' + Get a list of pkg ids from a name pattern + (Helper for GetPackagesByName) + + :param name: name pattern + :param newest_only: True = get newest packages only + ''' + qa = self._get_po_by_name(name, newest_only) + pkg_ids = self._to_package_id_list(qa) + return pkg_ids + + def _get_po_by_name(self, name, newest_only): + ''' + Get packages matching a name pattern + + :param name: name pattern + :param newest_only: True = get newest packages only + ''' + subj = dnf.subject.Subject(name) + qa = subj.get_best_query(self.base.sack, with_provides=False) + if newest_only: + qa = qa.latest() + return list(qa) + + def _get_groups(self): + ''' + make a list with categoties and there groups + This is the old way of yum groups, where a group is a collection of mandatory, default and optional pacakges + and the group is installed when all mandatory & default packages is installed. + ''' + all_groups = [] + if not self.base.comps: # lazy load the comps metadata + self.base.read_comps() + cats = self.base.comps.categories + for category in cats: + cat = (category.name, category.ui_name, category.ui_description) + cat_grps = [] + for obj in category.group_ids: + grp = self.base.comps.group_by_pattern(obj.name) # get the dnf group obj + if grp: + elem = (grp.id, grp.ui_name, grp.ui_description, grp.installed) + cat_grps.append(elem) + cat_grps.sort() + all_groups.append((cat, cat_grps)) + all_groups.sort() + return json.dumps(all_groups) + + def _get_repositories(self, filter): + ''' + Get the value a list of repo ids + :param filter: filter to limit the listed repositories + ''' + if filter == '' or filter == 'enabled': + repos = [repo.id for repo in self.base.repos.iter_enabled()] + else: + repos = [repo.id for repo in self.base.repos.get_matching(filter)] + return repos + + + def _get_config(self, setting): + ''' + Get the value of a yum config setting + it will return a JSON string of the config + :param setting: name of setting (debuglevel etc..) + ''' + if setting == '*': # Return all config + cfg = self.base.conf + all_conf = dict([(c,getattr(cfg,c)) for c in cfg.iterkeys()]) + value = json.dumps(all_conf) + elif hasattr(self.base.conf, setting): + value = json.dumps(getattr(self.base.conf, setting)) + else: + value = json.dumps(None) + return value + return value + + def _get_repo(self, repo_id ): + ''' + Get information about a give repo_id + the repo setting will be returned as dictionary in JSON format + :param repo_id: + ''' + value = json.dumps(None) + repo = self.base.repos.get(repo_id, None) # get the repo object + if repo: + repo_conf = dict([(c,getattr(repo,c)) for c in repo.iterkeys()]) + value = json.dumps(repo_conf) + return value + + def _get_packages(self, pkg_filter): + ''' + Get a list of package ids, based on a package pkg_filterer + :param pkg_filter: pkg pkg_filter string ('installed','updates' etc) + ''' + if pkg_filter in ['installed','available','updates','obsoletes','recent','extras']: + pkgs = getattr(self.base.packages,pkg_filter) + value = self._to_package_id_list(pkgs) + else: + value = [] + return value + + def _get_package_with_attributes(self, pkg_filter, fields): + ''' + Get a list of package ids, based on a package pkg_filterer + :param pkg_filter: pkg pkg_filter string ('installed','updates' etc) + ''' + value = [] + if pkg_filter in ['installed','available','updates','obsoletes','recent','extras']: + pkgs = getattr(self.base.packages,pkg_filter) + value = [self._get_po_list(po,fields) for po in pkgs] + return value + + def _get_attribute(self, id, attr): + ''' + Get an attribute from a yum package id + it will return a python repr string of the attribute + :param id: yum package id + :param attr: name of attribute (summary, size, description, changelog etc..) + ''' + po = self._get_po(id) + if po: + if attr in FAKE_ATTR: # is this a fake attr: + value = json.dumps(self._get_fake_attributes(po, attr)) + elif hasattr(po, attr): + value = json.dumps(getattr(po,attr)) + else: + value = json.dumps(None) + else: + value = json.dumps(None) + return value + + def _get_installed_na(self,name,arch): + q = self.base.sack.query() + inst = q.installed().filter(name=po.name, arch=po.arch).run() + if inst: + return inst[0] + else: + return None + + + def _get_updateInfo(self, id): + ''' + Get an Update Infomation e from a yum package id + it will return a python repr string of the attribute + :param id: yum package id + ''' + po = self._get_po(id) + if po: + # TODO : updateinfo is not supported in DNF yet + # https://bugzilla.redhat.com/show_bug.cgi?id=850912 + value = json.dumps(None) + else: + value = json.dumps(None) + return value + + + + def _get_group_pkgs(self, grp_id, grp_flt): + ''' + Get packages for a given grp_id and group filter + ''' + pkgs = [] + grp = self.base.comps.group_by_pattern(grp_id) + if grp: + if grp_flt == 'all': + pkg_names = [] + pkg_names.extend([p.name for p in grp.mandatory_packages ]) + pkg_names.extend([p.name for p in grp.default_packages ]) + pkg_names.extend([p.name for p in grp.optional_packages ]) + best_pkgs = [] + for name in pkg_names: + best_pkgs.extend(self._get_po_by_name(name,True)) + else: + pkg_names = [] + pkg_names.extend([p.name for p in grp.mandatory_packages ]) + pkg_names.extend([p.name for p in grp.default_packages ]) + best_pkgs = [] + for name in pkg_names: + best_pkgs.extend(self._get_po_by_name(name,True)) + pkgs = self.base.packages.filter_packages(best_pkgs) + else: + pass + pkg_ids = self._to_package_id_list(pkgs) + return pkg_ids + +#=============================================================================== +# Helper methods +#=============================================================================== + + def _get_po_list(self, po, attrs): + po_list = [self._get_id(po)] + for attr in attrs: + if attr in FAKE_ATTR: # is this a fake attr: + value = self._get_fake_attributes(po, attr) + elif hasattr(po, attr): + value = getattr(po,attr) + else: + value = None + po_list.append(value) + return po_list + + def _get_id_time_list(self, hist_trans): + ''' + return a list of (tid, isodate) pairs from a list of yum history transactions + ''' + result = [] + for ht in hist_trans: + tm = datetime.fromtimestamp(ht.end_timestamp) + result.append((ht.tid, tm.isoformat())) + return result + + def _get_fake_attributes(self,po, attr): + ''' + Get Fake Attributes, a whey to useful stuff for a package there is not real + attritbutes to the yum package object. + :param attr: Fake attribute + :type attr: string + ''' + if attr == "action": + return self._get_action(po) + elif attr == 'downgrades': + return self._get_downgrades(po) + elif attr == 'pkgtags': + return self._get_pkgtags(po) + elif attr == 'changelog': + # TODO : changelog is not supported in DNF yet + # https://bugzilla.redhat.com/show_bug.cgi?id=1066867 + return None + + def _get_downgrades(self,pkg): + pkg_ids = [] + ''' Find available downgrades for a given name.arch''' + q = self.base.sack.query() + avail = q.available().filter(name=pkg.name, arch=pkg.arch).run() + for apkg in avail: + if pkg.evr_gt(apkg): + pkg_ids.append(self._get_id(apkg)) + return pkg_ids + + def _get_pkgtags(self, po): + ''' + Get pkgtags from a given po + ''' + # TODO : pkgtags is not supported in DNF yet + return [] + + def _to_package_id_list(self, pkgs): + ''' + return a sorted list of package ids from a list of packages + if and po is installed, the installed po id will be returned + :param pkgs: + ''' + result = set() + for po in sorted(pkgs): + result.add(self._get_id(po)) + return result + + def _get_po(self,id): + ''' find the real package from an package id''' + n, e, v, r, a, repo_id = id.split(',') + q = self.base.sack.query() + if repo_id.startswith('@'): # installed package + f = q.installed() + f = f.filter(name=n, version=v, release=r, arch=a) + if len(f) > 0: + return f[0] + else: + return None + else: + f = q.available() + f = f.filter(name=n, version=v, release=r, arch=a) + if len(f) > 0: + return f[0] + else: + return None + + def _get_id(self,pkg): + ''' + convert a yum package obejct to an id string containing (n,e,v,r,a,repo) + :param pkg: + ''' + values = [pkg.name, str(pkg.epoch), pkg.version, pkg.release, pkg.arch, pkg.ui_from_repo] + return ",".join(values) + + + def _get_action(self, po): + ''' + Return the available action for a given pkg_id + The action is what can be performed on the package + an installed package will return as 'remove' as action + an available update will return 'update' + an available package will return 'install' + :param po: yum package + :type po: yum package object + :return: action (remove, install, update, downgrade, obsolete) + :rtype: string + ''' + action = 'install' + n, a, e, v, r = po.pkgtup + q = self.base.sack.query() + if po.reponame.startswith('@'): + action = 'remove' + else: + upd = q.upgrades().filter(name=n, version=v, release=r, arch=a) + if upd: + action = 'update' + else: + obsoletes = self._get_obsoletes() + if po in obsoletes: + action = 'obsolete' + else: + # get installed packages with same name + ipkgs = q.installed().filter(name=po.name).run() + if ipkgs: + ipkg = ipkgs[0] + if po.evr_gt(ipkg): + action = 'downgrade' + return action + + def _get_base(self): + ''' + Get a Dnf Base object to work with + ''' + if not self._base: + self._base = DnfBase(self) + return self._base + + + def _reset_base(self): + ''' + destroy the current YumBase object + ''' + del self._base + self._base = None + + + def _setup_watchdog(self): + ''' + Setup the watchdog to run every second when idle + ''' + gobject.timeout_add(1000, self._watchdog) + + def _watchdog(self): + terminate = False + if self._watchdog_disabled or self._is_working: # is working + return True + if not self._lock: # is locked + if self._watchdog_count > self._timeout_idle: + terminate = True + else: + if self._watchdog_count > self._timeout_locked: + terminate = True + if terminate: # shall we quit + if self._can_quit: + self._reset_base() + self.mainloop.quit() + else: + self._watchdog_count += 1 + self.logger.debug("Watchdog : %i" % self._watchdog_count ) + return True + +class Packages: + + def __init__(self, base): + self._base = base + self._sack = base.sack + self._inst_na = self._sack.query().installed().na_dict() + + def filter_packages(self, pkg_list, replace=True): + ''' + Filter a list of package objects and replace + the installed ones with the installed object, instead + of the available object + ''' + pkgs = [] + for pkg in pkg_list: + key = (pkg.name, pkg.arch) + inst_pkg = self._inst_na.get(key, [None])[0] + if inst_pkg and inst_pkg.evr == pkg.evr: + if replace: + pkgs.append(inst_pkg) + else: + pkgs.append(pkg) + return pkgs + + + @property + def query(self): + return self._sack.query() + + @property + def installed(self): + ''' + instawlled packages + ''' + return self.query.installed().run() + + @property + def updates(self): + ''' + available updates + ''' + return self.query.upgrades().run() + + + @property + def all(self,showdups = False): + ''' + all packages in the repositories + installed ones are replace with the install package objects + ''' + if showdups: + return self.filter_packages(self.query.available().run()) + else: + return self.filter_packages(self.query.latest().run()) + + @property + def available(self, showdups = False): + ''' + available packages there is not installed yet + ''' + if showdups: + return self.filter_packages(self.query.available().run(), replace=False) + else: + return self.filter_packages(self.query.latest().run(), replace=False) + + @property + def extras(self): + ''' + installed packages, not in current repos + ''' + # anything installed but not in a repo is an extra + avail_dict = self.query.available().pkgtup_dict() + inst_dict = self.query.installed().pkgtup_dict() + pkgs = [] + for pkgtup in inst_dict: + if pkgtup not in avail_dict: + pkgs.extend(inst_dict[pkgtup]) + return pkgs + + @property + def obsoletes(self): + ''' + packages there is obsoleting some installed packages + ''' + inst = self.query.installed() + return self.query.filter(obsoletes=inst) + + @property + def recent(self, showdups=False): + recent = [] + now = time() + recentlimit = now-(self._base.conf.recent*86400) + if showdups: + avail = self.query.available() + else: + avail = self.query.latest() + for po in avail: + if int(po.buildtime) > recentlimit: + recent.append(po) + return recent + + +class DnfBase(dnf.Base): + + def __init__(self, parent): + dnf.Base.__init__(self) + self.parent = parent + self.md_progress = MDProgress(parent) + self.setup_cache() + self.read_all_repos() + self.progress = Progress(parent) + self.repos.all().set_progress_bar( self.md_progress) + self.fill_sack() + self._packages = Packages(self) + + @property + def packages(self): + return self._packages + + def setup_cache(self): + # perform the CLI-specific cachedir tricks + conf = self.conf + #conf.read() # Read the conf file from disk + conf.releasever = '20' # FIXME: dont hardcode fedora release + # conf.cachedir = CACHE_DIR # hardcoded cache dir + # This is not public API, but we want the same cache as dnf cli + suffix = dnf.yum.parser.varReplace(dnf.const.CACHEDIR_SUFFIX, conf.yumvar) + cli_cache = dnf.conf.CliCache(conf.cachedir, suffix) + conf.cachedir = cli_cache.cachedir + self._system_cachedir = cli_cache.system_cachedir + print("cachedir: %s" % conf.cachedir) + + def apply_transaction(self): + ''' apply the current transaction to the system''' + rc = self.resolve() + if rc: + to_dnl = self.get_packages_to_download() + # Downloading Packages + self.download_packages(to_dnl, self.progress) + rc, msg = self.do_transaction() + if rc <> 0: + return (False, "transaction-error", msg) + else: + return (True, "transaction-ok","") + else: + return (False, "depsolve-failed", "") + + def get_packages_to_download(self): + ''' Get a list of packages to download from the current transaction''' + to_dnl = [] + for tsi in self.transaction: + if tsi.installed: + to_dnl.append(tsi.installed) + return to_dnl + + def search(self, fields, values, match_all=True, showdups=False): + ''' + search in a list of package fields for a list of keys + :param fields: package attributes to search in + :param values: the values to match + :param match_all: match all values (default) + :param showdups: show duplicate packages or latest (default) + :return: a list of package objects + ''' + matches = set() + for key in values: + key_set = set() + for attr in fields: + pkgs = set(self.contains(attr,key).run()) + key_set |= pkgs + if len(matches) == 0: + matches = key_set + else: + if match_all: + matches &= key_set + else: + matches |= key_set + result = list(matches) + if not showdups: + result = self.sack.query().filter(pkg=result).latest() + return result + + def contains(self, attr, needle, ignore_case=True): + fdict = {'%s__substr' % attr : needle} + if ignore_case: + return self.sack.query().filter(hawkey.ICASE, **fdict) + else: + return self.sack.query().filter(**fdict) + + +class MDProgress(DownloadProgress): + + def __init__(self, parent): + super(MDProgress, self).__init__() + self._last = -1.0 + self.parent = parent + + def start(self, total_files, total_size): + self._last = -1.0 + + def end(self,payload, status, msg): + name = str(payload) + if status == STATUS_OK: + self.parent.repoMetaDataProgress(name, 1.0) + + def progress(self, payload, done): + name = str(payload) + cur_total_bytes = payload.download_size + if cur_total_bytes: + frac = done/float(cur_total_bytes) + else: + frac = 0.0 + if frac > self._last+0.01: + self._last = frac + self.parent.repoMetaDataProgress(name, frac) + + +class Progress(DownloadProgress): + + def __init__(self, parent): + super(Progress, self).__init__() + self.parent = parent + self.total_files = 0 + self.total_size = 0.0 + self.download_files = 0 + self.download_size = 0.0 + self.dnl = {} + self.last_frac = 0 + + def start(self, total_files, total_size): + self.total_files = total_files + self.total_size = total_size + self.download_files = 0 + self.download_size = 0.0 + self.parent.downloadStart(total_files, total_size) + + + def end(self,payload, status, msg): + if not status: # payload download complete + self.download_files += 1 + self.parent.downloadEnd(str(payload), status, msg) + + def progress(self, payload, done): + pload = str(payload) + cur_total_bytes = payload.download_size + if not pload in self.dnl: + self.dnl[pload] = 0.0 + else: + self.dnl[pload] = done + total_frac = self.get_total() + if total_frac > self.last_frac: + self.last_frac = total_frac + if cur_total_bytes: + frac = done / cur_total_bytes + else: + frac = 0.0 + self.parent.downloadProgress(pload, frac, total_frac, self.download_files) + + def get_total(self): + """ Get the total downloaded percentage""" + tot = 0.0 + for value in self.dnl.values(): + tot += value + frac = int((tot / float(self.total_size))) + return frac + + def update(self): + """ Output the current progress""" + + sys.stdout.write("Progress : %-3d %% (%d/%d)\r" % (self.last_pct,self.download_files, self.total_files)) + + +class TransactionDisplay(object): + + def __init__(self): + self.last = -1 + + def event(self, package, action, te_current, te_total, ts_current, ts_total): + """ + @param package: A yum package object or simple string of a package name + @param action: A constant transaction set state + @param te_current: current number of bytes processed in the transaction + element being processed + @param te_total: total number of bytes in the transaction element being + processed + @param ts_current: number of processes completed in whole transaction + @param ts_total: total number of processes in the transaction. + """ + # this is where a progress bar would be called + + if te_total and te_total > 0: + percent = int((float(te_current)/te_total)*100.0) + if percent == 100: + self.last=-1 + print(action, package, percent, ts_current, ts_total ) + elif percent > self.last and percent % 10 == 0: + self.last = percent + print(action, package, percent, ts_current, ts_total ) + + else: + print(action, package) + + def scriptout(self, msgs): + """msgs is the messages that were output (if any).""" + if msgs: + print("ScriptOut: ",msgs) + + def errorlog(self, msg): + """takes a simple error msg string""" + print(msg, file=sys.stderr) + + def filelog(self, package, action): + # check package object type - if it is a string - just output it + """package is the same as in event() - a package object or simple string + action is also the same as in event()""" + pass + + def verify_tsi_package(self, pkg, count, total): + print("Verifing : %s "% pkg) + + + + +def doTextLoggerSetup(logroot='dnfdaemon', logfmt='%(asctime)s: %(message)s', loglvl=logging.INFO): + ''' Setup Python logging ''' + logger = logging.getLogger(logroot) + logger.setLevel(loglvl) + formatter = logging.Formatter(logfmt, "%H:%M:%S") + handler = logging.StreamHandler(stream=sys.stdout) + handler.setFormatter(formatter) + handler.propagate = False + logger.addHandler(handler) + + diff --git a/dnfdaemon/dnfdaemon-session.py b/dnfdaemon/dnfdaemon-session.py new file mode 100755 index 0000000000000000000000000000000000000000..66b6f05eebcc87942418decd88d973a461903c3f --- /dev/null +++ b/dnfdaemon/dnfdaemon-session.py @@ -0,0 +1,440 @@ +#!/usr/bin/python -tt +#coding: utf-8 +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# (C) 2013 - Tim Lauridsen <timlau@fedoraproject.org> + + +# +# dnf session bus dBus service (Readonly) +# + +from __future__ import print_function +from __future__ import absolute_import +import dbus +import dbus.service +import dbus.glib +import gobject +import json +import logging + +import argparse + +import sys +import os + +from common import DnfDaemonBase, doTextLoggerSetup, Logger, DownloadCallback, FAKE_ATTR, NONE, \ + TransactionDisplay + +version = 902 # (00.09.02) must be integer +DAEMON_ORG = 'org.baseurl.DnfSession' +DAEMON_INTERFACE = DAEMON_ORG +FAKE_ATTR = ['downgrades','action','pkgtags'] +NONE = json.dumps(None) + +def _(msg): + return msg + +#------------------------------------------------------------------------------ DBus Exception +class AccessDeniedError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.AccessDeniedError' + +class LockedError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.LockedError' + +class NotImplementedError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.NotImplementedError' + + +logger = logging.getLogger('dnfdaemon.session') + +#------------------------------------------------------------------------------ Main class +class DnfDaemon(DnfDaemonBase): + + def __init__(self, mainloop): + DnfDaemonBase.__init__(self, mainloop) + self.logger = logging.getLogger('dnfdaemon-session') + bus_name = dbus.service.BusName(DAEMON_ORG, bus = dbus.SessionBus()) + dbus.service.Object.__init__(self, bus_name, '/') + +#=============================================================================== +# DBus Methods +#=============================================================================== + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='i') + def GetVersion(self): + ''' + Get the daemon version + ''' + return version + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='b', + sender_keyword='sender') + def Exit(self, sender=None): + ''' + Exit the daemon + :param sender: + ''' + if self._can_quit: + self.mainloop.quit() + return True + else: + return False + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='b', + sender_keyword='sender') + def Lock(self, sender=None): + ''' + Get the yum lock + :param sender: + ''' + if not self._lock: + self._lock = sender + self.logger.info('LOCK: Locked by : %s' % sender) + return True + return False + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='b', + out_signature='b', + sender_keyword='sender') + + def SetWatchdogState(self,state, sender=None): + ''' + Set the Watchdog state + :param state: True = Watchdog active, False = Watchdog disabled + :type state: boolean (b) + ''' + self._watchdog_disabled = not state + return state + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='as', + sender_keyword='sender') + + def GetRepositories(self, filter, sender=None): + ''' + Get the value a list of repo ids + :param filter: filter to limit the listed repositories + :param sender: + ''' + self.working_start(sender) + repos = self._get_repositories(filter) + return self.working_ended(repos) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='as', + out_signature='', + sender_keyword='sender') + + def SetEnabledRepos(self, repo_ids, sender=None): + ''' + Enabled a list of repositories, disabled all other repos + :param repo_ids: list of repo ids to enable + :param sender: + ''' + self.working_start(sender) + # TODO : Add dnf code + return self.working_ended() + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def GetConfig(self, setting ,sender=None): + ''' + Get the value of a yum config setting + it will return a JSON string of the config + :param setting: name of setting (debuglevel etc..) + :param sender: + ''' + self.working_start(sender) + value = self._get_config(setting) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def GetRepo(self, repo_id ,sender=None): + ''' + Get information about a give repo_id + the repo setting will be returned as dictionary in JSON format + :param repo_id: + :param sender: + ''' + self.working_start(sender) + value = self._get_repo(repo_id) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='as', + sender_keyword='sender') + def GetPackages(self, pkg_filter, sender=None): + ''' + Get a list of package ids, based on a package pkg_filterer + :param pkg_filter: pkg pkg_filter string ('installed','updates' etc) + :param sender: + ''' + self.working_start(sender) + value = self._get_packages(pkg_filter) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='sas', + out_signature='s', + sender_keyword='sender') + def GetPackageWithAttributes(self, pkg_filter, fields, sender=None): + ''' + Get a list of package ids, based on a package pkg_filterer + :param pkg_filter: pkg pkg_filter string ('installed','updates' etc) + :param sender: + ''' + self.working_start(sender) + value = self._get_package_with_attributes(pkg_filter, fields) + return self.working_ended(json.dumps(value)) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='sb', + out_signature='as', + sender_keyword='sender') + def GetPackagesByName(self, name, newest_only, sender=None): + ''' + Get a list of packages from a name pattern + :param name: name pattern + :param newest_only: True = get newest packages only + :param sender: + ''' + self.working_start(sender) + pkg_ids = self._get_packages_by_name(name, newest_only) + return self.working_ended(pkg_ids) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ss', + out_signature='s', + sender_keyword='sender') + def GetAttribute(self, id, attr,sender=None): + ''' + Get an attribute from a yum package id + it will return a python repr string of the attribute + :param id: yum package id + :param attr: name of attribute (summary, size, description, changelog etc..) + :param sender: + ''' + self.working_start(sender) + value = self._get_attribute( id, attr) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def GetUpdateInfo(self, id,sender=None): + ''' + Get an Update Infomation e from a yum package id + it will return a python repr string of the attribute + :param id: yum package id + :param sender: + ''' + self.working_start(sender) + value = self._get_updateInfo(id) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='b', + sender_keyword='sender') + def Unlock(self, sender=None): + ''' release the lock''' + if self.check_lock(sender): + # TODO : Add dnf code + self.logger.info('UNLOCK: Lock Release by %s' % self._lock) + self._lock = None + return True + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='asasbbb', + out_signature='as', + sender_keyword='sender') + def Search(self, fields, keys, match_all, newest_only, tags, sender=None ): + ''' + Search for for packages, where given fields contain given key words + :param fields: list of fields to search in + :param keys: list of keywords to search for + :param match_all: match all flag, if True return only packages matching all keys + :param newest_only: return only the newest version of a package + :param tags: seach pkgtags + ''' + self.working_start(sender) + result = self._search(fields, keys, match_all, newest_only, tags) + return self.working_ended(result) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='s', + sender_keyword='sender') + def GetGroups(self, sender=None ): + ''' + Return a category/group tree + ''' + self.working_start(sender) + value = self._get_groups() + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ss', + out_signature='as', + sender_keyword='sender') + def GetGroupPackages(self, grp_id, grp_flt, sender=None ): + ''' + Get packages in a group by grp_id and grp_flt + :param grp_id: The Group id + :param grp_flt: Group Filter (all or default) + :param sender: + ''' + self.working_start(sender) + pkg_ids = self._get_group_pkgs(grp_id, grp_flt) + return self.working_ended(pkg_ids) + + + +# +# Template for new method +# +# @dbus.service.method(DAEMON_INTERFACE, +# in_signature='', +# out_signature='', +# sender_keyword='sender') +# def NewMethod(self, sender=None ): +# ''' +# +# ''' +# self.working_start(sender) +# value = True +# return self.working_ended(value) +# + + +#=============================================================================== +# DBus signals +#=============================================================================== + @dbus.service.signal(DAEMON_INTERFACE) + def UpdateProgress(self,name,frac,fread,ftime): + ''' + DBus signal with download progress information + will send dbus signals with download progress information + :param name: filename + :param frac: Progress fracment (0 -> 1) + :param fread: formated string containing BytesRead + :param ftime : formated string containing remaining or elapsed time + ''' + pass + +# Parallel Download Progress signals + + @dbus.service.signal(DAEMON_INTERFACE) + def DownloadStart(self, num_files, num_bytes): + ''' Starting a new parallel download batch ''' + pass + + @dbus.service.signal(DAEMON_INTERFACE) + def DownloadProgress(self, name, frac, total_frac, total_files): + ''' Progress for a single instance in the batch ''' + pass + + @dbus.service.signal(DAEMON_INTERFACE) + def DownloadEnd(self, name, status, msg): + ''' Download of af single instace ended ''' + pass + + @dbus.service.signal(DAEMON_INTERFACE) + def RepoMetaDataProgress(self, name, frac): + ''' Repository Metadata Download progress ''' + + +#=============================================================================== +# Helper methods +#=============================================================================== + def working_start(self,sender): + self.check_lock(sender) + self._is_working = True + self._watchdog_count = 0 + + def working_ended(self, value=None): + self._is_working = False + return value + + def check_lock(self, sender): + ''' + Check that the current sender is owning the yum lock + :param sender: + ''' + if self._lock == sender: + return True + else: + raise LockedError('dnf is locked by another application') + + + +def main(): + parser = argparse.ArgumentParser(description='Yum D-Bus Session Daemon') + parser.add_argument('-v', '--verbose', action='store_true') + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('--notimeout', action='store_true') + args = parser.parse_args() + if args.verbose: + if args.debug: + doTextLoggerSetup(logroot='dnfdaemon',loglvl=logging.DEBUG) + else: + doTextLoggerSetup(logroot='dnfdaemon') + + # setup the DBus mainloop + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + mainloop = gobject.MainLoop() + yd = DnfDaemon(mainloop) + if not args.notimeout: + yd._setup_watchdog() + mainloop.run() + +if __name__ == '__main__': + main() diff --git a/dnfdaemon/dnfdaemon-system.py b/dnfdaemon/dnfdaemon-system.py new file mode 100755 index 0000000000000000000000000000000000000000..af30a87715336d37477638da03e7d92f7a814c2d --- /dev/null +++ b/dnfdaemon/dnfdaemon-system.py @@ -0,0 +1,954 @@ +#!/usr/bin/python -tt +#coding: utf-8 +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# (C) 2013 - Tim Lauridsen <timlau@fedoraproject.org> + +import dbus +import dbus.service +import dbus.glib +import gobject +import json +import logging +from datetime import datetime + +import argparse + +from common import DaemonBase, doTextLoggerSetup, Logger, DownloadCallback, NONE, FAKE_ATTR + +version = 902 # (00.09.02) must be integer +DAEMON_ORG = 'org.baseurl.DnfSystem' +DAEMON_INTERFACE = DAEMON_ORG + +def _(msg): + return msg + +#------------------------------------------------------------------------------ DBus Exception +class AccessDeniedError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.AccessDeniedError' + +class YumLockedError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.LockedError' + +class YumTransactionError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.TransactionError' + +class YumNotImplementedError(dbus.DBusException): + _dbus_error_name = DAEMON_ORG+'.NotImplementedError' + +#------------------------------------------------------------------------------ Callback handlers + +class ProcessTransCallback: + STATES = { PT_DOWNLOAD : "download", + PT_DOWNLOAD_PKGS : "pkg-to-download", + PT_GPGCHECK : "signature-check", + PT_TEST_TRANS : "run-test-transaction", + PT_TRANSACTION : "run-transaction"} + + def __init__(self, base): + self.base = base + + def event(self,state,data=NONE): + if state in ProcessTransCallback.STATES: + if data != NONE: + data = [self.base._get_id(po) for po in data] + self.base.TransactionEvent(ProcessTransCallback.STATES[state], data) + +class RPMCallback(RPMBaseCallback): + ''' + RPMTransaction display callback class + ''' + ACTIONS = { TS_UPDATE : 'update', + TS_ERASE: 'erase', + TS_INSTALL: 'install', + TS_TRUEINSTALL : 'install', + TS_OBSOLETED: 'obsolete', + TS_OBSOLETING: 'install', + TS_UPDATED: 'cleanup', + 'repackaging': 'repackage'} + + def __init__(self, base): + RPMBaseCallback.__init__(self) + self.base = base + + def event(self, package, action, te_current, te_total, ts_current, ts_total): + """ + :param package: A yum package object or simple string of a package name + :param action: A yum.constant transaction set state or in the obscure + rpm repackage case it could be the string 'repackaging' + :param te_current: Current number of bytes processed in the transaction + element being processed + :param te_total: Total number of bytes in the transaction element being + processed + :param ts_current: number of processes completed in whole transaction + :param ts_total: total number of processes in the transaction. + """ + if not isinstance(package, str): # package can be both str or yum package object + id = self.base._get_id(package) + else: + id = package + if action in RPMCallback.ACTIONS: + action = RPMCallback.ACTIONS[action] + self.base.RPMProgress(id, action, te_current, te_total, ts_current, ts_total) + + def scriptout(self, package, msgs): + """package is the package. msgs is the messages that were + output (if any).""" + pass + + +class DaemonBase(): + + def __init__(self, daemon): + self._daemon = daemon + + def _checkSignatures(self,pkgs,callback): + ''' The the signatures of the downloaded packages ''' + return 0 + + +logger = logging.getLogger('yumdaemon') + +#------------------------------------------------------------------------------ Main class +class DnfDaemon(DaemonBase): + + def __init__(self, mainloop): + DaemonBase.__init__(self, mainloop) + self.logger = logging.getLogger('dnfdaemon.system') + bus_name = dbus.service.BusName(DAEMON_ORG, bus = dbus.SystemBus()) + dbus.service.Object.__init__(self, bus_name, '/') + self._gpg_confirm = {} + +#=============================================================================== +# DBus Methods +#=============================================================================== + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='i') + def GetVersion(self): + ''' + Get the daemon version + ''' + return version + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='b', + sender_keyword='sender') + def Exit(self, sender=None): + ''' + Exit the daemon + :param sender: + ''' + self.check_permission(sender) + if self._can_quit: + self._reset_base() + self.mainloop.quit() + return True + else: + return False + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='b', + sender_keyword='sender') + def Lock(self, sender=None): + ''' + Get the yum lock + :param sender: + ''' + self.check_permission(sender) + if not self._lock: + self._lock = sender + self.logger.info('LOCK: Locked by : %s' % sender) + return True + return False + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='b', + out_signature='b', + sender_keyword='sender') + def SetWatchdogState(self,state, sender=None): + ''' + Set the Watchdog state + :param state: True = Watchdog active, False = Watchdog disabled + :type state: boolean (b) + ''' + self.check_permission(sender) + self._watchdog_disabled = not state + return state + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='as', + sender_keyword='sender') + + def GetRepositories(self, filter, sender=None): + ''' + Get the value a list of repo ids + :param filter: filter to limit the listed repositories + :param sender: + ''' + self.working_start(sender) + repos = self._get_repositories(filter) + return self.working_ended(repos) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='as', + out_signature='', + sender_keyword='sender') + + def SetEnabledRepos(self, repo_ids, sender=None): + ''' + Enabled a list of repositories, disabled all other repos + :param repo_ids: list of repo ids to enable + :param sender: + ''' + self.working_start(sender) + # TODO : Add dnf code (SetEnabledRepos) + return self.working_ended() + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def GetConfig(self, setting ,sender=None): + ''' + Get the value of a yum config setting + it will return a JSON string of the config + :param setting: name of setting (debuglevel etc..) + :param sender: + ''' + self.working_start(sender) + value = self._get_config(setting) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ss', + out_signature='b', + sender_keyword='sender') + def SetConfig(self, setting, value ,sender=None): + ''' + Set yum config setting for the running session + :param setting: yum conf setting to set + :param value: value to set + :param sender: + ''' + self.working_start(sender) + rc = self._set_option(setting, json.loads(value)) + return self.working_ended(rc) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def GetRepo(self, repo_id ,sender=None): + ''' + Get information about a give repo_id + the repo setting will be returned as dictionary in JSON format + :param repo_id: + :param sender: + ''' + self.working_start(sender) + value = self._get_repo(repo_id) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='as', + sender_keyword='sender') + def GetPackages(self, pkg_filter, sender=None): + ''' + Get a list of package ids, based on a package pkg_filterer + :param pkg_filter: pkg pkg_filter string ('installed','updates' etc) + :param sender: + ''' + self.working_start(sender) + value = self._get_packages(pkg_filter) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='sas', + out_signature='s', + sender_keyword='sender') + def GetPackageWithAttributes(self, pkg_filter, fields, sender=None): + ''' + Get a list of package ids, based on a package pkg_filterer + :param pkg_filter: pkg pkg_filter string ('installed','updates' etc) + :param sender: + ''' + self.working_start(sender) + value = self._get_package_with_attributes(pkg_filter, fields) + return self.working_ended(json.dumps(value)) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='sb', + out_signature='as', + sender_keyword='sender') + def GetPackagesByName(self, name, newest_only, sender=None): + ''' + Get a list of packages from a name pattern + :param name: name pattern + :param newest_only: True = get newest packages only + :param sender: + ''' + self.working_start(sender) + pkg_ids = self._get_packages_by_name(name, newest_only) + return self.working_ended(pkg_ids) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ss', + out_signature='s', + sender_keyword='sender') + def GetAttribute(self, id, attr,sender=None): + ''' + Get an attribute from a yum package id + it will return a python repr string of the attribute + :param id: yum package id + :param attr: name of attribute (summary, size, description, changelog etc..) + :param sender: + ''' + self.working_start(sender) + value = self._get_attribute( id, attr) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def GetUpdateInfo(self, id,sender=None): + ''' + Get an Update Infomation e from a yum package id + it will return a python repr string of the attribute + :param id: yum package id + :param sender: + ''' + self.working_start(sender) + value = self._get_updateInfo(id) + return self.working_ended(value) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='i', + out_signature='s', + sender_keyword='sender') + def GetHistoryPackages(self, tid,sender=None): + ''' + Get packages from a given yum history transaction id + + :param tid: history transaction id + :type tid: integer + :return: list of (pkg_id, state, installed) pairs + :rtype: json encoded string + ''' + self.working_start(sender) + value = json.dumps(self._get_history_transaction_pkgs(tid)) + return self.working_ended(value) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ii', + out_signature='s', + sender_keyword='sender') + def GetHistoryByDays(self, start_days, end_days ,sender=None): + ''' + Get History transaction in a interval of days from today + + :param start_days: start of interval in days from now (0 = today) + :type start_days: integer + :param end_days:end of interval in days from now + :type end_days: integer + :return: a list of (transaction is, date-time) pairs + :type sender: json encoded string + ''' + self.working_start(sender) + value = json.dumps(self._get_history_by_days(start_days, end_days)) + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='as', + out_signature='s', + sender_keyword='sender') + def HistorySearch(self, pattern ,sender=None): + ''' + Search the history for transaction matching a pattern + :param pattern: patterne to match + :type pattern: string + :return: list of (tid,isodates) + :type sender: json encoded string + ''' + self.working_start(sender) + value = json.dumps(self._history_search(pattern)) + return self.working_ended(value) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='b', + sender_keyword='sender') + def Unlock(self, sender=None): + ''' release the lock''' + self.check_permission(sender) + if self.check_lock(sender): + self._reset_base() + self.logger.info('UNLOCK: Lock Release by %s' % self._lock) + self._lock = None + return True + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def Install(self, cmds, sender=None): + ''' + Install packages based on command patterns separated by spaces + sinulate what 'yum install <arguments>' does + :param cmds: command patterns separated by spaces + :param sender: + ''' + self.working_start(sender) + value = 0 + for cmd in cmds.split(' '): + if cmd.endswith('.rpm'): + self.base.install_local(cmd) + else: + self.base.install(cmd) + value = self._build_transaction() + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def Remove(self, cmds, sender=None): + ''' + Remove packages based on command patterns separated by spaces + sinulate what 'yum remove <arguments>' does + :param cmds: command patterns separated by spaces + :param sender: + ''' + self.working_start(sender) + value = 0 + for cmd in cmds.split(' '): + self.base.remove(cmd) + value = self._build_transaction() + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def Update(self, cmds, sender=None): + ''' + Update packages based on command patterns separated by spaces + sinulate what 'yum update <arguments>' does + :param cmds: command patterns separated by spaces + :param sender: + ''' + self.working_start(sender) + value = 0 + for cmd in cmds.split(' '): + self.base.upgrade(cmd) + value = self._build_transaction() + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def Reinstall(self, cmds, sender=None): + ''' + Reinstall packages based on command patterns separated by spaces + sinulate what 'yum reinstall <arguments>' does + :param cmds: command patterns separated by spaces + :param sender: + ''' + self.working_start(sender) + value = 0 + # TODO : Add dnf code (Reinstall) + # no Base.reinstall, so we need to use Transaction.add_reinstall() + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='s', + out_signature='s', + sender_keyword='sender') + def Downgrade(self, cmds, sender=None): + ''' + Downgrade packages based on command patterns separated by spaces + sinulate what 'yum downgrade <arguments>' does + :param cmds: command patterns separated by spaces + :param sender: + ''' + self.working_start(sender) + value = 0 + for cmd in cmds.split(' '): + self.base.downgrade(cmd) + value = self._build_transaction() + return self.working_ended(value) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ss', + out_signature='as', + sender_keyword='sender') + + def AddTransaction(self, id, action, sender=None): + ''' + Add an package to the current transaction + + :param id: package id for the package to add + :param action: the action to perform ( install, update, remove, obsolete, reinstall, downgrade, localinstall ) + ''' + self.working_start(sender) + value = 0 + po = self._get_po(id) + # TODO : Add dnf code (AddTransaction) + # FIXME: missing dnf API of adding to hawkey.Goal object + # no easy way to add to the hawkey.Sack object in dnf + # using public api + # filed upstream bug + # https://bugzilla.redhat.com/show_bug.cgi?id=1073859 + if action == 'install': + pass + elif action == 'remove': + pass + elif action == 'update': + pass + elif action == 'obsolete': + pass + elif action == 'reinstall': + pass + elif action == 'downgrade': + pass + elif action == 'localinstall': + pass + + + return self.working_ended(value) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='', + sender_keyword='sender') + def ClearTransaction(self, sender): + ''' + Clear the transactopm + ''' + self.working_start(sender) + # TODO : Add dnf code (ClearTransaction) + return self.working_ended() + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='as', + sender_keyword='sender') + + def GetTransaction(self, sender=None): + ''' + Return the members of the current transaction + ''' + self.working_start(sender) + value = [] + # TODO : Add dnf code (GetTransaction) + return self.working_ended(value) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='s', + sender_keyword='sender') + def BuildTransaction(self, sender): + ''' + Resolve dependencies of current transaction + ''' + self.working_start(sender) + value = self._build_transaction() + return self.working_ended(value) + + + def _build_transaction(self): + ''' + Resolve dependencies of current transaction + ''' + self.TransactionEvent('start-build',NONE) + rc = self.resolve() + if rc: # OK + output = self._get_transaction_list() + else: + output = [] + self.TransactionEvent('end-build',NONE) + return json.dumps((rc,output)) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='i', + sender_keyword='sender') + def RunTransaction(self, sender = None): + ''' + Run the current yum transaction + ''' + self.working_start(sender) + self.check_permission(sender) + self.check_lock(sender) + self.TransactionEvent('start-run',NONE) + self._can_quit = False + # TODO : Add dnf code (RunTransaction) + self._can_quit = True + self._reset_base() + self.TransactionEvent('end-run',NONE) + return self.working_ended(0) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='asasbbb', + out_signature='as', + sender_keyword='sender') + def Search(self, fields, keys, match_all, newest_only, tags, sender=None ): + ''' + Search for for packages, where given fields contain given key words + :param fields: list of fields to search in + :param keys: list of keywords to search for + :param match_all: match all flag, if True return only packages matching all keys + :param newest_only: return only the newest version of a package + :param tags: seach pkgtags + + ''' + self.working_start(sender) + result = self._search(fields, keys, match_all, newest_only, tags) + return self.working_ended(result) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='', + out_signature='s', + sender_keyword='sender') + def GetGroups(self, sender=None ): + ''' + Return a category/group tree + ''' + self.working_start(sender) + value = self._get_groups() + return self.working_ended(value) + + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='ss', + out_signature='as', + sender_keyword='sender') + def GetGroupPackages(self, grp_id, grp_flt, sender=None ): + ''' + Get packages in a group by grp_id and grp_flt + :param grp_id: The Group id + :param grp_flt: Group Filter (all or default) + :param sender: + ''' + self.working_start(sender) + pkg_ids = self._get_group_pkgs(grp_id, grp_flt) + return self.working_ended(pkg_ids) + + @Logger + @dbus.service.method(DAEMON_INTERFACE, + in_signature='sb', + out_signature='', + sender_keyword='sender') + def ConfirmGPGImport(self, hexkeyid, confirmed, sender=None ): + ''' + Confirm import of at GPG Key by yum + :param hexkeyid: hex keyid for GPG key + :param confirmed: confirm import of key (True/False) + :param sender: + ''' + + self.working_start(sender) + self._gpg_confirm[hexkeyid] = confirmed # store confirmation of GPG import + return self.working_ended() + + +# +# Template for new method +# +# @dbus.service.method(DAEMON_INTERFACE, +# in_signature='', +# out_signature='', +# sender_keyword='sender') +# def NewMethod(self, sender=None ): +# ''' +# +# ''' +# self.working_start(sender) +# value = True +# return self.working_ended(value) +# + + +#=============================================================================== +# DBus signals +#=============================================================================== + @dbus.service.signal(DAEMON_INTERFACE) + def UpdateProgress(self,name,frac,fread,ftime): + ''' + DBus signal with download progress information + will send dbus signals with download progress information + :param name: filename + :param frac: Progress fracment (0 -> 1) + :param fread: formated string containing BytesRead + :param ftime : formated string containing remaining or elapsed time + ''' + pass + +# Parallel Download Progress signals + + @dbus.service.signal(DAEMON_INTERFACE) + def DownloadStart(self, num_files, num_bytes): + ''' Starting a new parallel download batch ''' + pass + + @dbus.service.signal(DAEMON_INTERFACE) + def DownloadProgress(self, name, frac, total_frac, total_files): + ''' Progress for a single instance in the batch ''' + pass + + @dbus.service.signal(DAEMON_INTERFACE) + def DownloadEnd(self, name, status, msg): + ''' Download of af single instace ended ''' + pass + + @dbus.service.signal(DAEMON_INTERFACE) + def RepoMetaDataProgress(self, name, frac): + ''' Repository Metadata Download progress ''' + + + @dbus.service.signal(DAEMON_INTERFACE) + def TransactionEvent(self,event,data): + ''' + DBus signal with Transaction event information, telling the current step in the processing of + the current transaction. + + Steps are : start-run, download, pkg-to-download, signature-check, run-test-transaction, run-transaction, fail, end-run + + :param event: current step + ''' + #print "event: %s" % event + pass + + + @dbus.service.signal(DAEMON_INTERFACE) + def RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total): + """ + RPM Progress DBus signal + :param package: A yum package object or simple string of a package name + :param action: A yum.constant transaction set state or in the obscure + rpm repackage case it could be the string 'repackaging' + :param te_current: Current number of bytes processed in the transaction + element being processed + :param te_total: Total number of bytes in the transaction element being + processed + :param ts_current: number of processes completed in whole transaction + :param ts_total: total number of processes in the transaction. + """ + pass + + + @dbus.service.signal(DAEMON_INTERFACE) + def GPGImport(self, pkg_id, userid, hexkeyid, keyurl, timestamp ): + ''' + GPG Key Import DBus signal + + :param pkg_id: pkg_id for the package needing the GPG Key to be verified + :param userid: GPG key name + :param hexkeyid: GPG key hex id + :param keyurl: Url to the GPG Key + :param timestamp: + ''' + pass + +#=============================================================================== +# Helper methods +#=============================================================================== + def working_start(self,sender): + self.check_permission(sender) + self.check_lock(sender) + self._is_working = True + self._watchdog_count = 0 + + def working_ended(self, value=None): + self._is_working = False + return value + + def handle_gpg_import(self, gpg_info): + ''' + Callback for handling af user confirmation of gpg key import + + :param gpg_info: dict with info about gpg key {"po": .., "userid": .., "hexkeyid": .., "keyurl": .., "fingerprint": .., "timestamp": ..) + + ''' + print(gpg_info) + pkg_id = self._get_id(gpg_info['po']) + userid = gpg_info['userid'] + hexkeyid = gpg_info['hexkeyid'] + keyurl = gpg_info['keyurl'] + fingerprint = gpg_info['fingerprint'] + timestamp = gpg_info['timestamp'] + if not hexkeyid in self._gpg_confirm: # the gpg key has not been confirmed by the user + self._gpg_confirm[hexkeyid] = False + self.GPGImport( pkg_id, userid, hexkeyid, keyurl, timestamp ) + return self._gpg_confirm[hexkeyid] + + + def _set_option(self, option, value): + # TODO : Add dnf code (_set_option) + pass + + + def _get_history_by_days(self, start, end): + ''' + Get the yum history transaction member located in a date interval from today + :param start: start days from today + :param end: end days from today + ''' + result = [] + # TODO : Add dnf code (_get_history_by_days) + return self._get_id_time_list(result) + + def _history_search(self, pattern): + ''' + search in yum history + :param pattern: list of search patterns + :type pattern: list + ''' + result = [] + # TODO : Add dnf code (_history_search) + return self._get_id_time_list(result) + + def _get_history_transaction_pkgs(self, tid): + ''' + return a list of (pkg_id, tx_state, installed_state) pairs from a given + yum history transaction id + ''' + result = [] + # TODO : Add dnf code (_get_history_transaction_pkgs) + return result + + def _get_transaction_list(self): + ''' + Generate a list of the current transaction + ''' + out_list = [] + # TODO : Add dnf code (_get_transaction_list) + return out_list + + def _to_transaction_id_list(self, txmbrs): + ''' + return a sorted list of package ids from a list of packages + if and po is installed, the installed po id will be returned + :param pkgs: + ''' + result = [] + # TODO : Add dnf code (_to_transaction_id_list) + return result + + def check_lock(self, sender): + ''' + Check that the current sender is owning the yum lock + :param sender: + ''' + if self._lock == sender: + return True + else: + raise LockedError('dnf is locked by another application') + + + def check_permission(self, sender): + ''' Check for senders permission to run root stuff''' + if sender in self.authorized_sender: + return + else: + self._check_permission(sender) + self.authorized_sender.add(sender) + + + def _check_permission(self, sender): + ''' + check senders permissions using PolicyKit1 + :param sender: + ''' + if not sender: raise ValueError('sender == None') + + obj = dbus.SystemBus().get_object('org.freedesktop.PolicyKit1', '/org/freedesktop/PolicyKit1/Authority') + obj = dbus.Interface(obj, 'org.freedesktop.PolicyKit1.Authority') + (granted, _, details) = obj.CheckAuthorization( + ('system-bus-name', {'name': sender}), DAEMON_ORG, {}, dbus.UInt32(1), '', timeout=600) + if not granted: + raise AccessDeniedError('Session is not authorized') + + +def main(): + parser = argparse.ArgumentParser(description='Dnf D-Bus Daemon') + parser.add_argument('-v', '--verbose', action='store_true') + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('--notimeout', action='store_true') + args = parser.parse_args() + if args.verbose: + if args.debug: + doTextLoggerSetup(logroot='dnfdaemon',loglvl=logging.DEBUG) + else: + doTextLoggerSetup(logroot='dnfdaemon') + + # setup the DBus mainloop + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + mainloop = gobject.MainLoop() + yd = DnfDaemon(mainloop) + if not args.notimeout: + yd._setup_watchdog() + mainloop.run() + +if __name__ == '__main__': + main() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..c855a954a1f6ea1566ee863602b77356322c7872 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,157 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/yum-daemon.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/yum-daemon.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/yum-daemon" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/yum-daemon" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +upload: + @scp -r _build/html/* timlau.fedorapeople.org:public_html/yumdaemon/. + diff --git a/docs/client-python.rst b/docs/client-python.rst new file mode 100644 index 0000000000000000000000000000000000000000..44871d455833af1964bdef855b44d5a855489bc6 --- /dev/null +++ b/docs/client-python.rst @@ -0,0 +1,53 @@ +========================================== +Client API for Python 2.x and 3.x +========================================== + +.. automodule:: dnfdaemon + +Classes +======== + +System API +------------- + +.. autoclass:: dnfdaemon.DnfDaemonClient + :members: Exit, Lock, Unlock, SetWatchdogState,GetPackageWithAttributes, GetRepositoriesGetRepo, GetConfig, SetConfig, + GetAttribute, GetUpdateInfo, GetPackages, GetPackagesByName, GetHistoryByDays, HistorySearch, GetHistoryPackages, + GetGroups, Search, ClearTransaction, GetTransaction, AddTransaction, Install, Remove, Update, Reinstal, Downgrade, + BuildTransaction, RunTransaction, GetEnabledRepos, GetGroupPackages, ConfirmGPGImport + +Session API +------------ + +.. autoclass:: dnfdaemon.DnfDaemonReadOnlyClient + :members: Exit, Lock, Unlock, SetWatchdogState,GetPackageWithAttributes, GetRepositoriesGetRepo, GetConfig, + GetAttribute, GetUpdateInfo, GetPackages, GetPackagesByName, GetGroups, Search + BuildTransaction, RunTransaction, GetEnabledRepos, GetGroupPackages + +Exceptions +============ + +.. class:: DnfDaemonError(Exception) + +Base Exception from the backend + +.. class:: AccessDeniedError(DnfDaemonError) + +PolicyKit access was denied. + +Ex. +User press cancel button in policykit window + +.. class:: LockedError(DnfDaemonError) + +dnf is locked by another application + +Ex. +dnf is running in a another session +You have not called the Lock method to grep the Lock + + +.. class:: YumTransactionError(YumDaemonError) + +Error in the yum transaction. + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..c4329e79e6309262605b9591bbf2cc81d35c6c50 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# yum-daemon documentation build configuration file, created by +# sphinx-quickstart on Sun May 27 11:18:17 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- +sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), "../"))) +sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), "../client"))) + + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'yum-daemon' +copyright = '2013, Tim Lauridsen' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'yum-daemondoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'yum-daemon.tex', 'yum-daemon Documentation', + 'Tim Lauridsen', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'dnfdaemon', 'dnfdaemon Documentation', + ['Tim Lauridsen'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'ydnfdaemon', 'dnfdaemon Documentation', + 'Tim Lauridsen', 'dnfdaemon', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000000000000000000000000000000000000..8a05a3d4c86774caba3fa88a61bc7c801758c225 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,9 @@ +********* +Examples +********* + +Python 2.x &3.x +================ + + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..de6170f65aa68a8500dcf3081e9d2ec04030e302 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,25 @@ +.. dnfaemon documentation master file, created by + sphinx-quickstart on Sun May 27 11:18:17 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to dnfdaemon's documentation! +====================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + server + client-python + examples + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/server.rst b/docs/server.rst new file mode 100644 index 0000000000000000000000000000000000000000..646f904b4ccc0c8d1b4753b89cc6c915177b2469 --- /dev/null +++ b/docs/server.rst @@ -0,0 +1,593 @@ +========================================== +DBus service API documentation +========================================== + +The dnfdaemon is an easy way to utililize the power of the dnf package manager from your own programs + +Data structures +---------------- + +.. table:: **Data structures** + + ================================= ================================= + Name Content + ================================= ================================= + Package Id (pkg_id) "name,epoch,ver,rel,arch,repo_id" + Transaction List (tx_list) "pkg_id,ts_state" + ================================= ================================= + +| + +.. table:: **Attribute Descriptions** + + ================ ========================================================= + Attribute Description + ================ ========================================================= + name Package Name + epoch Package Epoch + ver Package Version + rel Package Release + arch Package Architecture + repo_id Repository Id + ts_state Transaction Member state + ================ ========================================================= + +Transaction result:: + + <transaction_result> ::= <result>, <result>, ...., <result> + <result> ::= <action>, <pkg_list> + <action> ::= install | update | remove | install-deps | update-deps | remove-deps | skipped + <plg_list> ::= <pkg_info>, <pkg_info>, ......, <pkg_info> + <pkg_info> ::= <pkg_id>, size, <obs_list> + <pkg_id> ::= name, epoch, version, release, arch, repo_id + <obs_list> ::= <obs_id>, <obs_id>, ...., <obs_id> + <obs_id> ::= name, epoch, version, release, arch, repo_id for packages obsoletes by <pkg_id> + + +========================================== +System Service +========================================== + +.. table:: **DBus Names** + + ======================== ========================================================= + Attribute Value + ======================== ========================================================= + object org.baseurl.YumSystem + interface org.baseurl.YumSystem + path / + ======================== ========================================================= + +Misc methods +------------- + +.. function:: GetVersion() + + Get the API version + + :return: string with API version + +.. function:: Lock() + + Get the daemon Lock, if posible + +.. function:: Unlock() + + Get the daemon Lock, if posible + +Repository and config methods +------------------------------ + +.. py:function:: GetRepositories(filter) + + Get the value a list of repo ids + + :param filter: filter to limit the listed repositories + :type filter: string + :return: list of repo id's + :rtype: array for stings (as) + +.. py:function:: GetRepo(repo_id) + + Get information about a give repo_id + + :param repo_id: repo id + :type repo_id: string + :return: a dictionary with repo information **(JSON)** + :rtype: string (s) + +.. py:function:: SetEnabledRepos(repo_ids): + + Enabled a list of repositories, disabled all other repos + + :param repo_ids: list of repo ids to enable + + +.. py:function:: GetConfig(setting) + + Get the value of a yum config setting + + :param setting: name of setting (debuglevel etc..) + :type setting: string + :return: the config value of the requested setting **(JSON)** + :rtype: string (s) + +.. py:function:: SetConfig(setting, value) + + Get the value of a yum config setting + + :param setting: name of setting (debuglevel etc..) + :type setting: string + :param value: name of setting (debuglevel etc..) + :type value: misc types **(JSON)** + :return: did the update succed + :rtype: boolean (b) + + +Package methods +---------------- + +These methods is for getting packages and information about packages + +.. function:: GetPackages(pkg_filter) + + get a list of packages matching the filter type + + :param pkg_filter: package filter ('installed','available','updates','obsoletes','recent','extras') + :type pkg_filter: string + :return: list of pkg_id's + :rtype: array of strings (as) + + + +.. function:: GetPackageWithAttributes(pkg_filter, fields) + + | Get a list of pkg list for a given package filter + | each pkg list contains [pkg_id, field,....] where field is a atrribute of the package object + | Ex. summary, size etc. + + :param pkg_filter: package filter ('installed','available','updates','obsoletes','recent','extras') + :type pkg_filter: string + :param fields: yum package objects attributes to get. + :type fields: array of strings (as) + :return: list of (id, field1, field2...) **(JSON)**, each JSON Sting contains (id, field1, field2...) + :rtype: array of strings (as) + +.. py:function:: GetPackagesByName(name, newest_only) + + Get a list of pkg ids for starts with name + + :param name: name prefix to match + :type name: string + :param newest_only: show only the newest match or every match. + :type newest_only: boolean + :return: list of pkg_id's + :rtype: array of strings (as) + + +.. py:function:: GetAttribute(id, attr,) + + get yum package attribute (description, filelist, changelog etc) + + :param pkg_id: pkg_id to get attribute from + :type pkg_id: string + :param attr: name of attribute to get + :type attr: string + :return: the value of the attribute **(JSON)**, the content depend on attribute being read + :rtype: string (s) + +.. py:function:: GetUpdateInfo(id) + + Get Updateinfo for a package + + :param pkg_id: pkg_id to get update info from + :type pkg_id: string + :return: update info for the package **(JSON)** + :rtype: string (s) + +.. py:function:: Search(fields, keys, match_all, newest_only, tags ) + + Search for packages where keys is matched in fields + + :param fields: yum po attributes to search in + :type fields: array of strings + :param keys: keys to search for + :type keys: array of strings + :param match_all: match all keys or only one + :type match_all: boolean + :param newest_only: match all keys or only one + :type newest_only: boolean + :param tags: match all keys or only one + :type tags: boolean + :return: list of pkg_id's for matches + :rtype: array of stings (as) + + +High level methods +------------------- +The high level methods simulate basic dnf command line main functions. + +.. py:function:: Install(cmds) + +Works just like the ``dnf install <cmds>`` command line + + :param cmds: package arguments separated by spaces + :type cmds: string + :return: return code, result of resolved transaction (rc = 1 is ok, else failure) **(JSON)** + :rtype: string (s) + +.. py:function:: Remove(cmds) + + Works just like the ``dnf remove <cmds>`` command line + + :param cmds: package arguments separated by spaces + :type cmds: string + :return: return code, result of resolved transaction (rc = 1 is ok, else failure) **(JSON)** + :rtype: string (s) + + +.. py:function:: Update(cmds) + + Works just like the ``dnf update <cmds>`` command line + + :param cmds: package arguments separated by spaces + :type cmds: string + :return: return code, result of resolved transaction (rc = 1 is ok, else failure) **(JSON)** + :rtype: string (s) + + +.. py:function:: Reinstall(cmds) + + Works just like the ``dnf reinstall <cmds>`` command line + + :param cmds: package arguments separated by spaces + :type cmds: string + :return: return code, result of resolved transaction (rc = 1 is ok, else failure) **(JSON)** + :rtype: string (s) + + +.. py:function:: Downgrade(cmds) + + Works just like the ``dnf downgrade <cmds>`` command line + + :param cmds: package arguments separated by spaces + :type cmds: string + :return: return code, result of resolved transaction (rc = 1 is ok, else failure) **(JSON)** + :rtype: string (s) + + + +Transaction methods +-------------------- +These methods is for handling the current yum transaction + +.. py:function:: AddTransaction(id, action) + + Add an package to the current transaction + + :param id: package id for the package to add + :type id: string + :param action: the action to perform ( install, update, remove, obsolete, reinstall, downgrade, localinstall ) + :type action: string + :return: list of (pkg_id, transaction state) pairs for the added members (comma separated) + :rtype: array of strings (as) + +.. py:function:: ClearTransaction() + + Clear the current transaction + +.. py:function:: GetTransaction() + + Get the currrent transaction + + :return: list of (pkg_id, transaction state) pairs in the current transaction (comma separated) + :rtype: array of strings (as) + +.. py:function:: BuildTransaction() + + Depsolve the current transaction + + :return: (return code, result of resolved transaction) pair (rc = 1 is ok, else failure) **(JSON)** + :rtype: string (s) + + +.. py:function:: RunTransaction() + + Execute the current transaction + + :return: state of run transaction (0 = ok, 1 = need GPG import confirmation, 2 = error) + :rtype: int (i) + +.. py:function:: ConfirmGPGImport(self, hexkeyid, confirmed) + + Confirm import of at GPG Key by yum + + :param hexkeyid: hex keyid for GPG key + :type hexkeyid: string (s) + :param confirmed: confirm import of key (True/False) + :type confirmed: boolean (b) + + +Groups +------- + +Methods to work with yum groups and categories + +.. py:function:: GetGroups( ) + + Get available Categories & Groups + +.. py:function:: GetGroupPackages(grp_id, grp_flt ) + + Get packages in a group by grp_id and grp_flt + + :param grp_id: The Group id + :type grp_id: string (s) + :param grp_flt: Group Filter (all or default) + :type grp_flt: string (s) + :return: list of pkg_id's + :rtype: array of strings (as) + + +.. note:: Under Development + + More to come in the future, methods to install groups etc. has to be defined and implemented + +History +-------- + +Methods to work with the package transaction history + +.. py:function:: GetHistoryByDays(start_days, end_days) + + Get History transaction in a interval of days from today + + :param start_days: start of interval in days from now (0 = today) + :type start_days: integer + :param end_days: end of interval in days from now + :type end_days: integer + :return: a list of (transaction ids, date-time) pairs (JSON) + :rtype: string (s) + +.. py:function:: GetHistoryPackages(tid) + + Get packages from a given yum history transaction id + + :param tid: history transaction id + :type tid: integer + :return: list of (pkg_id, state, installed) pairs + :rtype: json encoded string + +.. py:function:: HistorySearch(pattern) + + Search the history for transaction matching a pattern + + :param pattern: patterne to match + :type pattern: string + :return: list of (tid,isodates) + :type sender: json encoded string + +Signals +-------- + +.. py:function:: TransactionEvent(self,event,data): + + Signal with Transaction event information, telling the current step in the processing of + the current transaction. + + Steps are : start-run, download, pkg-to-download, signature-check, run-test-transaction, run-transaction, fail, end-run + + :param event: current step + + +.. py:function:: RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total): + + signal with RPM Progress + + :param package: A yum package object or simple string of a package name + :param action: A yum.constant transaction set state or in the obscure + rpm repackage case it could be the string 'repackaging' + :param te_current: Current number of bytes processed in the transaction + element being processed + :param te_total: Total number of bytes in the transaction element being + processed + :param ts_current: number of processes completed in whole transaction + :param ts_total: total number of processes in the transaction. + + +.. py:function:: GPGImport(self, pkg_id, userid, hexkeyid, keyurl, timestamp ): + + signal with GPG Key information of a key there need to be confirmed to complete the + current transaction. after signal is send transaction will abort with rc=1 + Use ConfirmGPGImport method to comfirm the key and run RunTransaction again + + + :param pkg_id: pkg_id for the package needing the GPG Key to be verified + :param userid: GPG key name + :param hexkeyid: GPG key hex id + :param keyurl: Url to the GPG Key + :param timestamp: + ''' + +.. note:: Under Development + + The progress signals for download progress is not documented yet + + +========================================== +Session Service +========================================== +.. table:: **DBus Names** + + ======================== ========================================================= + Attribute Value + ======================== ========================================================= + object org.baseurl.YumSession + interface org.baseurl.YumSession + path / + ======================== ========================================================= + + + +Misc methods +------------- + +.. function:: GetVersion() + + Get the API version + + :return: string with API version + +.. function:: Lock() + + Get the daemon Lock, if posible + +.. function:: Unlock() + + Get the daemon Lock, if posible + +Repository and config methods +------------------------------ + +.. py:function:: GetRepositories(filter) + + Get the value a list of repo ids + + :param filter: filter to limit the listed repositories + :type filter: string + :return: list of repo id's + :rtype: array for stings (as) + +.. py:function:: GetRepo(repo_id) + + Get information about a give repo_id + + :param repo_id: repo id + :type repo_id: string + :return: a dictionary with repo information **(JSON)** + :rtype: string (s) + +.. py:function:: SetEnabledRepos(repo_ids): + + Enabled a list of repositories, disabled all other repos + + :param repo_ids: list of repo ids to enable + +.. py:function:: GetConfig(setting) + + Get the value of a yum config setting + + :param setting: name of setting (debuglevel etc..) + :type setting: string + :return: the config value of the requested setting **(JSON)** + :rtype: string (s) + +Package methods +---------------- + +These methods is for getting packages and information about packages + +.. function:: GetPackages(pkg_filter) + + get a list of packages matching the filter type + + :param pkg_filter: package filter ('installed','available','updates','obsoletes','recent','extras') + :type pkg_filter: string + :return: list of pkg_id's + :rtype: array of strings (as) + + + +.. function:: GetPackageWithAttributes(pkg_filter, fields) + + | Get a list of pkg list for a given package filter + | each pkg list contains [pkg_id, field,....] where field is a atrribute of the package object + | Ex. summary, size etc. + + :param pkg_filter: package filter ('installed','available','updates','obsoletes','recent','extras') + :type pkg_filter: string + :param fields: yum package objects attributes to get. + :type fields: array of strings (as) + :return: list of (id, field1, field2...) **(JSON)**, each JSON Sting contains (id, field1, field2...) + :rtype: array of strings (as) + +.. py:function:: GetPackagesByName(name, newest_only) + + Get a list of pkg ids for starts with name + + :param name: name prefix to match + :type name: string + :param newest_only: show only the newest match or every match. + :type newest_only: boolean + :return: list of pkg_id's + :rtype: array of strings (as) + + +.. py:function:: GetAttribute(id, attr,) + + get yum package attribute (description, filelist, changelog etc) + + :param pkg_id: pkg_id to get attribute from + :type pkg_id: string + :param attr: name of attribute to get + :type attr: string + :return: the value of the attribute **(JSON)**, the content depend on attribute being read + :rtype: string (s) + +.. py:function:: GetUpdateInfo(id) + + Get Updateinfo for a package + + :param pkg_id: pkg_id to get update info from + :type pkg_id: string + :return: update info for the package **(JSON)** + :rtype: string (s) + +.. py:function:: Search(fields, keys, match_all, newest_only, tags ) + + Search for packages where keys is matched in fields + + :param fields: yum po attributes to search in + :type fields: array of strings + :param keys: keys to search for + :type keys: array of strings + :param match_all: match all keys or only one + :type match_all: boolean + :param newest_only: match all keys or only one + :type newest_only: boolean + :param tags: search in pkgtags + :type tags: boolean + :return: list of pkg_id's for matches + :rtype: array of stings (as) + + +Groups +------- + +Methods to work with dnf groups and categories + +.. py:function:: GetGroups( ) + + Get available Categories & Groups + +.. py:function:: GetGroupPackages(grp_id, grp_flt ) + + Get packages in a group by grp_id and grp_flt + + :param grp_id: The Group id + :type grp_id: string (s) + :param grp_flt: Group Filter (all or default) + :type grp_flt: string (s) + :return: list of pkg_id's + :rtype: array of strings (as) + +.. note:: Under Development + + More to come in the future, methods to install groups etc. has to be defined and implemented + +Signals +-------- + +.. note:: Under Development + + Signals is not documented yet diff --git a/policykit1/org.baseurl.DnfSystem.policy b/policykit1/org.baseurl.DnfSystem.policy new file mode 100644 index 0000000000000000000000000000000000000000..b57839b13c8869ea68fcd2b24414ac756849f46a --- /dev/null +++ b/policykit1/org.baseurl.DnfSystem.policy @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE policyconfig PUBLIC + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd"> +<policyconfig> + <vendor>Yum</vendor> + <vendor_url>http://dnf.baseurl.org/</vendor_url> + <action id="org.baseurl.DnfSystem"> + <description>dnf</description> + <message>Application requesting dnf to search/modify system packages</message> + <defaults> + <allow_inactive>no</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + </action> +</policyconfig> diff --git a/test/base.py b/test/base.py new file mode 100644 index 0000000000000000000000000000000000000000..361a43652eff30aef9c300dcda4ecc94ffce838e --- /dev/null +++ b/test/base.py @@ -0,0 +1,262 @@ +import sys +import os.path +sys.path.insert(0,os.path.abspath('client')) +import unittest +from datetime import date +from dnfdaemon import DnfDaemonClient, DnfDaemonReadOnlyClient + +class TestBase(unittest.TestCase, DnfDaemonClient): + def __init__(self, methodName='runTest'): + unittest.TestCase.__init__(self, methodName) + DnfDaemonClient.__init__(self) + self._gpg_confirm = None + self._signals = [] + + def setUp(self): + self.Lock() + + def tearDown(self): + self.Unlock() + + def reset_signals(self): + self._signals = [] + + def check_signal(self, name): + if name in self._signals: + return True + else: + return False + + def show_changelog(self, changelog, max_elem=3): + i = 0 + for (c_date, c_ver, msg) in changelog: + i += 1 + if i > max_elem: + return + print("* %s %s" % (date.fromtimestamp(c_date).isoformat(), c_ver)) + for line in msg.split('\n'): + print("%s" % line) + + def show_package_list(self, pkgs): + for pkg_id in pkgs: + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg_id) + print " --> %s-%s:%s-%s.%s (%s)" % (n, e, v, r, a, repo_id) + + def show_transaction_list(self, pkgs): + for pkg_id in pkgs: + pkg_id = str(pkg_id) + (n, e, v, r, a, repo_id, ts_state) = self.to_txmbr_tuple(pkg_id) + print " --> %s-%s:%s-%s.%s (%s) - %s" % (n, e, v, r, a, repo_id, ts_state) + + def show_transaction_result(self, output): + for action, pkgs in output: + print " %s" % action + for pkg in pkgs: + print " --> %s" % str(pkg) + +# ======================== Helpers ======================= + def _add_to_transaction(self, name): + ''' + Helper to add a package to transaction + ''' + pkgs = self.GetPackagesByName(name, newest_only=True) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + self.assertEqual(len(pkgs),1) + pkg = pkgs[0] + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + if repo_id[0] == '@': + action='remove' + else: + action='install' + txmbrs = self.AddTransaction(pkg,action) + self.assertIsInstance(txmbrs, list) + return txmbrs + + def _run_transaction(self, build=True): + ''' + Desolve deps and run the current transaction + ''' + print('************** Running the current transaction *********************') + if build: + rc, output = self.BuildTransaction() + self.assertEqual(rc,2) + self.show_transaction_result(output) + self.assertGreater(len(output),0) + self.RunTransaction() + + def check_installed(self, name): + pkgs = self.GetPackagesByName(name, newest_only=True) + # pkgs should be a list instance + for pkg in pkgs: + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + if repo_id[0] == '@': + return True + return False + + def _is_installed(self, name): + pkgs = self.GetPackagesByName(name, newest_only=True) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + self.assertTrue(len(pkgs)>0) + for pkg in pkgs: + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + if repo_id[0] == '@': + return True + return False + + def _show_package(self, id): + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(id) + print "\nPackage attributes" + self.assertIsInstance(n, str) + print "Name : %s " % n + summary = self.GetAttribute(id, 'summary') + self.assertIsInstance(summary, unicode) + print "Summary : %s" % summary + print "\nDescription:" + desc = self.GetAttribute(id, 'description') + self.assertIsInstance(desc, unicode) + print desc +# print "\nChangelog:" +# changelog = self.GetAttribute(id, 'changelog') +# self.assertIsInstance(changelog, list) +# self.show_changelog(changelog, max_elem=2) + # Check a not existing attribute dont make it blow up + notfound = self.GetAttribute(id, 'notfound') + self.assertIsNone(notfound) + print " Value of attribute 'notfound' : %s" % notfound + +############################################################################### +# Dbus Signal Handlers +############################################################################### + + def on_UpdateProgress(self,name,frac,fread,ftime): + self._signals.append("UpdateProgress") + pass + + def on_TransactionEvent(self,event, data): + self._signals.append("TransactionEvent") + pass + + def on_RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total): + self._signals.append("RPMProgress") + pass + + def on_GPGImport(self, pkg_id, userid, hexkeyid, keyurl, timestamp ): + self._signals.append("GPGImport") + values = (pkg_id, userid, hexkeyid, keyurl, timestamp) + self._gpg_confirm = values + print "received signal : GPGImport%s" % (repr(values)) + + def on_DownloadStart(self, num_files, num_bytes): + ''' Starting a new parallel download batch ''' + values = (num_files, num_bytes) + print("on_DownloadStart : %s" % (repr(values))) + + def on_DownloadProgress(self, name, frac, total_frac, total_files): + ''' Progress for a single instance in the batch ''' + values = (name, frac, total_frac, total_files) + print("on_DownloadProgress : %s" % (repr(values))) + + def on_DownloadEnd(self, name, status, msg): + ''' Download of af single instace ended ''' + values = (name, status, msg) + print("on_DownloadEnd : %s" % (repr(values))) + + def on_RepoMetaDataProgress(self, name, frac): + ''' Repository Metadata Download progress ''' + values = (name, frac) + print("on_RepoMetaDataProgress : %s" % (repr(values))) + + +class TestBaseReadonly(unittest.TestCase, DnfDaemonReadOnlyClient): + def __init__(self, methodName='runTest'): + unittest.TestCase.__init__(self, methodName) + DnfDaemonReadOnlyClient.__init__(self) + + def setUp(self): + self.Lock() + + def tearDown(self): + self.Unlock() + + def show_changelog(self, changelog, max_elem=3): + i = 0 + for (c_date, c_ver, msg) in changelog: + i += 1 + if i > max_elem: + return + print("* %s %s" % (date.fromtimestamp(c_date).isoformat(), c_ver)) + for line in msg.split('\n'): + print("%s" % line) + + def show_package_list(self, pkgs): + for pkg_id in pkgs: + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg_id) + print " --> %s-%s:%s-%s.%s (%s)" % (n, e, v, r, a, repo_id) + + + def _is_installed(self, name): + pkgs = self.GetPackagesByName(name, newest_only=True) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + self.assertTrue(len(pkgs)>0) + for pkg in pkgs: + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + if repo_id[0] == '@': + return True + return False + + def _show_package(self, id): + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(id) + print "\nPackage attributes" + self.assertIsInstance(n, str) + print "Name : %s " % n + summary = self.GetAttribute(id, 'summary') + self.assertIsInstance(summary, unicode) + print "Summary : %s" % summary + print "\nDescription:" + desc = self.GetAttribute(id, 'description') + self.assertIsInstance(desc, unicode) + print desc +# print "\nChangelog:" +# changelog = self.GetAttribute(id, 'changelog') +# self.assertIsInstance(changelog, list) +# self.show_changelog(changelog, max_elem=2) + # Check a not existing attribute dont make it blow up + notfound = self.GetAttribute(id, 'notfound') + self.assertIsNone(notfound) + print " Value of attribute 'notfound' : %s" % notfound + +############################################################################### +# Dbus Signal Handlers +############################################################################### + + def on_UpdateProgress(self,name,frac,fread,ftime): + pass + + def on_TransactionEvent(self,event, data): + pass + + def on_RPMProgress(self, package, action, te_current, te_total, ts_current, ts_total): + pass + + def on_DownloadStart(self, num_files, num_bytes): + ''' Starting a new parallel download batch ''' + values = (num_files, num_bytes) + print("on_DownloadStart : %s" % (repr(values))) + + def on_DownloadProgress(self, name, frac, total_frac, total_files): + ''' Progress for a single instance in the batch ''' + values = (name, frac, total_frac, total_files) + print("on_DownloadProgress : %s" % (repr(values))) + + def on_DownloadEnd(self, name, status, msg): + ''' Download of af single instace ended ''' + values = (name, status, msg) + print("on_DownloadEnd : %s" % (repr(values))) + + def on_RepoMetaDataProgress(self, name, frac): + ''' Repository Metadata Download progress ''' + values = (name, frac) + print("on_RepoMetaDataProgress : %s" % (repr(values))) diff --git a/test/test-session-api.py b/test/test-session-api.py new file mode 100644 index 0000000000000000000000000000000000000000..81f9355bd99a6671dda2c2535560542f9c4e6edc --- /dev/null +++ b/test/test-session-api.py @@ -0,0 +1,251 @@ +import sys, os +sys.path.insert(0,os.path.abspath('client')) +#from base import TestBaseReadonly as TestBase +from base import TestBase, TestBaseReadonly +from dnfdaemon import LockedError +from subprocess import check_output, call +from nose.exc import SkipTest +import time + +""" +This module is used for testing new unit tests +When the test method is completted it is move som test-api.py + +use 'nosetest -v -s unit-devel.py' to run the tests +""" + +class TestAPIDevel(TestBaseReadonly): + + def __init__(self, methodName='runTest'): + TestBaseReadonly.__init__(self, methodName) + + def test_Locking(self): + ''' + Session: Unlock and Lock + ''' + print + # release the lock (grabbed by setUp) + self.Unlock() + # calling a method without a lock should raise a YumLockedError + # self.assertRaises(YumLockedError,self.Install, '0xFFFF') + # trying to unlock method without a lock should raise a YumLockedError + self.assertRaises(LockedError,self.Unlock) + # get the Lock again, else tearDown will fail + self.Lock() + + def test_GetPackages(self): + ''' + Session: GetPackages + ''' + print + for narrow in ['installed','available']: + print(' Getting packages : %s' % narrow) + pkgs = self.GetPackages(narrow) + self.assertIsInstance(pkgs, list) + self.assertGreater(len(pkgs),0) # the should be more than once + print(' packages found : %s ' % len(pkgs)) + pkg_id = pkgs[-1] # last pkg in list + print(pkg_id) + self._show_package(pkg_id) + for narrow in ['updates','obsoletes','recent','extras']: + print(' ==================== Getting packages : %s =============================' % narrow) + pkgs = self.GetPackages(narrow) + self.assertIsInstance(pkgs, list) + print(' packages found : %s ' % len(pkgs)) + if len(pkgs) > 0: + pkg_id = pkgs[0] # last pkg in list + print(pkg_id) + self._show_package(pkg_id) + for narrow in ['notfound']: # Dont exist, but it should not blow up + print(' Getting packages : %s' % narrow) + pkgs = self.GetPackages(narrow) + self.assertIsInstance(pkgs, list) + self.assertEqual(len(pkgs),0) # the should be notting + print(' packages found : %s ' % len(pkgs)) + + def test_GetPackagesByName(self): + ''' + Session: GetPackagesByName + ''' + print + print "Get all available versions of yum" + pkgs = self.GetPackagesByName('yum', newest_only=False) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + num1 = len(pkgs) + self.assertNotEqual(num1, 0) # yum should always be there + for pkg in pkgs: + print " Package : %s" % pkg + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + self.assertEqual(n,"yum") + print "Get newest versions of yum" + pkgs = self.GetPackagesByName('yum', newest_only=True) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + num2 = len(pkgs) + self.assertEqual(num2, 1) # there can only be one :) + for pkg in pkgs: + print " Package : %s" % pkg + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + self.assertEqual(n,"yum") + print "Get the newest packages starting with yum-plugin-" + pkgs = self.GetPackagesByName('yum-plugin-*', newest_only=True) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + num3 = len(pkgs) + self.assertGreater(num3, 1) # there should be more than one :) + for pkg in pkgs: + print " Package : %s" % pkg + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkg) + self.assertTrue(n.startswith('yum')) + + def test_Repositories(self): + ''' + Session: GetRepository and GetRepo + ''' + print + print(" Getting enabled repos") + repos = self.GetRepositories('') + self.assertIsInstance(repos, list) + for repo_id in repos: + print(" Repo : %s" % repo_id) + print " Getting *-source repos" + repos = self.GetRepositories('*-source') + self.assertIsInstance(repos, list) + for repo_id in repos: + print(" Repo : %s" % repo_id) + self.assertTrue(repo_id.endswith('-source')) + print(" \nGetting fedora repository") + repo = self.GetRepo('updates') + self.assertIsInstance(repo, dict) + print(" Repo: fedora") + print(" Name : %s " % repo['name']) + print(" Metalink :\n %s " % repo['metalink']) + print(" enabled : %s " % repo['enabled']) + print(" gpgcheck : %s " % repo['gpgcheck']) + + # check for a repo not there + repo = self.GetRepo('XYZCYZ') + self.assertIsNone(repo) + + + def test_Search(self): + ''' + Session: Search + ''' + fields = ['name','summary'] + keys = ['yum','plugin'] + pkgs = self.Search(fields, keys ,True,True,False) + self.assertIsInstance(pkgs, list) + for p in pkgs: + summary = self.GetAttribute(p,'summary') + print str(p),summary + self.assertTrue(keys[0] in str(p) or keys[0] in summary) + self.assertTrue(keys[1] in str(p) or keys[1] in summary) + keys = ['yum','zzzzddddsss'] # second key should not be found + pkgs = self.Search(fields, keys ,True,True, False) + self.assertIsInstance(pkgs, list) + print "found %i packages" % len(pkgs) + self.assertEqual(len(pkgs), 0) # when should not find any matches + keys = ['yum','zzzzddddsss'] # second key should not be found + pkgs = self.Search(fields, keys ,False, True, False) + self.assertIsInstance(pkgs, list) + print "found %i packages" % len(pkgs) + self.assertGreater(len(pkgs), 0) # we should find some matches + # retro should match some pkgtags + keys = ['retro'] # second key should not be found + pkgs = self.Search(fields, keys ,True, True, True) + self.assertIsInstance(pkgs, list) + print "found %i packages" % len(pkgs) + self.assertGreater(len(pkgs), 0) # we should find some matches + + def test_PackageActions(self): + """ + Session: GetPackageWithAttributes & GetAttribute (action) + """ + print() + flt_dict = {'installed':['remove'],'updates':['update'],'obsoletes':['obsolete'], 'available':['install','remove','update','obsolete']} + for flt in flt_dict.keys(): + now = time.time() + result = self.GetPackageWithAttributes(flt, ['summary','size']) + print("%s, # = %s, time = %.3f" % (flt, len(result),time.time()-now)) + self.assertIsInstance(result, list) # result is a list + i = 0 + for elem in result: + self.assertIsInstance(elem, list) # each elem is a list + self.assertEqual(len(elem),3) # 3 elements + i += 1 + if i > 10: break # only test the first 10 elements + now = time.time() + action = self.GetAttribute(elem[0], 'action') + name = elem[0].split(",")[0] + print(" %s = %s , time = %.3f" % (name, action, time.time()-now)) + self.assertIn(action, flt_dict[flt]) + + + def test_Groups(self): + """ + Session: Groups (GetGroups & GetGroupPackages) + """ + + result = self.GetGroups() + for cat, grps in result: + # cat: [category_id, category_name, category_desc] + self.assertIsInstance(cat, list) # cat is a list + self.assertIsInstance(grps, list) # grps is a list + self.assertEqual(len(cat),3) # cat has 3 elements + print " --> %s" % cat[0] + for grp in grps: + # [group_id, group_name, group_desc, group_is_installed] + self.assertIsInstance(grp, list) # grp is a list + self.assertEqual(len(grp),4) # grp has 4 elements + print " tag: %s name: %s \n desc: %s \n installed : %s " % tuple(grp) + # Test GetGroupPackages + grp_id = grp[0] + pkgs = self.GetGroupPackages(grp_id,'all') + self.assertIsInstance(pkgs, list) # cat is a list + print " # of Packages in group : ",len(pkgs) + pkgs = self.GetGroupPackages(grp_id,'default') + self.assertIsInstance(pkgs, list) # cat is a list + print " # of Default Packages in group : ",len(pkgs) + + def test_Downgrades(self): + ''' + Session: GetAttribute( downgrades ) + ''' + print "Get newest versions of yum" + pkgs = self.GetPackagesByName('yum', newest_only=True) + # pkgs should be a list instance + self.assertIsInstance(pkgs, list) + num2 = len(pkgs) + self.assertEqual(num2, 1) # there can only be one :) + downgrades = self.GetAttribute(pkgs[0], 'downgrades') + self.assertIsInstance(downgrades, list) + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(pkgs[0]) + inst_evr = "%s:%s.%s" % (e,v,r) + print("Installed : %s" % pkgs[0]) + for id in downgrades: + (n, e, v, r, a, repo_id) = self.to_pkg_tuple(id) + evr = "%s:%s.%s" % (e,v,r) + self.assertTrue(evr < inst_evr) + print(" Downgrade : %s" % id) + + def test_GetConfig(self): + ''' + Session: GetConfig + ''' + all_conf = self.GetConfig('*') + self.assertIsInstance(all_conf, dict) + for key in all_conf: + print " %s = %s" % (key,all_conf[key]) + fastestmirror = self.GetConfig('fastestmirror') + print("fastestmirror : %s" % fastestmirror) + self.assertIn(fastestmirror, [True,False]) + not_found = self.GetConfig('not_found') + print("not_found : %s" % not_found) + self.assertIsNone(not_found) + + + + + diff --git a/test/unit-devel.py b/test/unit-devel.py new file mode 100644 index 0000000000000000000000000000000000000000..38ee8ae808160f6bea2b3e22363c48aee8d564c6 --- /dev/null +++ b/test/unit-devel.py @@ -0,0 +1,33 @@ +import sys, os +sys.path.insert(0,os.path.abspath('client')) +#from base import TestBaseReadonly as TestBase +from base import TestBase, TestBaseReadonly +from dnfdaemon import LockedError +from subprocess import check_output, call +from nose.exc import SkipTest + +""" +This module is used for testing new unit tests +When the test method is completted it is move som test-api.py + +use 'nosetest -v -s unit-devel.py' to run the tests +""" + +class TestAPIDevel(TestBaseReadonly): + + def __init__(self, methodName='runTest'): + TestBaseReadonly.__init__(self, methodName) + + def test_Locking(self): + ''' + Session: Unlock and Lock + ''' + print + # release the lock (grabbed by setUp) + self.Unlock() + # calling a method without a lock should raise a YumLockedError + # self.assertRaises(YumLockedError,self.Install, '0xFFFF') + # trying to unlock method without a lock should raise a YumLockedError + self.assertRaises(LockedError,self.Unlock) + # get the Lock again, else tearDown will fail + self.Lock() diff --git a/tools/git2cl b/tools/git2cl new file mode 100755 index 0000000000000000000000000000000000000000..aa1e8c18d764e8976f4ebbe9a96668f21bd5aa05 --- /dev/null +++ b/tools/git2cl @@ -0,0 +1,308 @@ +#!/usr/bin/perl + +# Copyright (C) 2007 Simon Josefsson. +# +# The functions mywrap, last_line_len, wrap_log_entry are derived from +# the cvs2cl tool, see <http://www.red-bean.com/cvs2cl/>: +# Copyright (C) 2001,2002,2003,2004 Martyn J. Pearce <fluffy@cpan.org> +# Copyright (C) 1999 Karl Fogel <kfogel@red-bean.com> +# +# git2cl is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# git2cl is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with git2cl; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA. + +use strict; +use Date::Parse qw(strptime); +use POSIX qw(strftime); +use Text::Wrap qw(wrap); + +use constant EMPTY_LOG_MESSAGE => '*** empty log message ***'; + +sub mywrap { + my ($indent1, $indent2, @text) = @_; + # If incoming text looks preformatted, don't get clever + my $text = Text::Wrap::wrap($indent1, $indent2, @text); + if ( grep /^\s+/m, @text ) { + return $text; + } + my @lines = split /\n/, $text; + $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e; + $lines[0] =~ s/^$indent1\s+/$indent1/; + s/^$indent2\s+/$indent2/ + for @lines[1..$#lines]; + my $newtext = join "\n", @lines; + $newtext .= "\n" + if substr($text, -1) eq "\n"; + return $newtext; +} + +sub last_line_len { + my $files_list = shift; + my @lines = split (/\n/, $files_list); + my $last_line = pop (@lines); + return length ($last_line); +} + +# A custom wrap function, sensitive to some common constructs used in +# log entries. +sub wrap_log_entry { + my $text = shift; # The text to wrap. + my $left_pad_str = shift; # String to pad with on the left. + + # These do NOT take left_pad_str into account: + my $length_remaining = shift; # Amount left on current line. + my $max_line_length = shift; # Amount left for a blank line. + + my $wrapped_text = ''; # The accumulating wrapped entry. + my $user_indent = ''; # Inherited user_indent from prev line. + + my $first_time = 1; # First iteration of the loop? + my $suppress_line_start_match = 0; # Set to disable line start checks. + + my @lines = split (/\n/, $text); + while (@lines) # Don't use `foreach' here, it won't work. + { + my $this_line = shift (@lines); + chomp $this_line; + + if ($this_line =~ /^(\s+)/) { + $user_indent = $1; + } + else { + $user_indent = ''; + } + + # If it matches any of the line-start regexps, print a newline now... + if ($suppress_line_start_match) + { + $suppress_line_start_match = 0; + } + elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/) + || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/) + || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/) + || ($this_line =~ /^(\s+)(\S+)/) + || ($this_line =~ /^(\s*)- +/) + || ($this_line =~ /^()\s*$/) + || ($this_line =~ /^(\s*)\*\) +/) + || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/)) + { + $length_remaining = $max_line_length - (length ($user_indent)); + } + + # Now that any user_indent has been preserved, strip off leading + # whitespace, so up-folding has no ugly side-effects. + $this_line =~ s/^\s*//; + + # Accumulate the line, and adjust parameters for next line. + my $this_len = length ($this_line); + if ($this_len == 0) + { + # Blank lines should cancel any user_indent level. + $user_indent = ''; + $length_remaining = $max_line_length; + } + elsif ($this_len >= $length_remaining) # Line too long, try breaking it. + { + # Walk backwards from the end. At first acceptable spot, break + # a new line. + my $idx = $length_remaining - 1; + if ($idx < 0) { $idx = 0 }; + while ($idx > 0) + { + if (substr ($this_line, $idx, 1) =~ /\s/) + { + my $line_now = substr ($this_line, 0, $idx); + my $next_line = substr ($this_line, $idx); + $this_line = $line_now; + + # Clean whitespace off the end. + chomp $this_line; + + # The current line is ready to be printed. + $this_line .= "\n${left_pad_str}"; + + # Make sure the next line is allowed full room. + $length_remaining = $max_line_length - (length ($user_indent)); + + # Strip next_line, but then preserve any user_indent. + $next_line =~ s/^\s*//; + + # Sneak a peek at the user_indent of the upcoming line, so + # $next_line (which will now precede it) can inherit that + # indent level. Otherwise, use whatever user_indent level + # we currently have, which might be none. + my $next_next_line = shift (@lines); + if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) { + $next_line = $1 . $next_line if (defined ($1)); + # $length_remaining = $max_line_length - (length ($1)); + $next_next_line =~ s/^\s*//; + } + else { + $next_line = $user_indent . $next_line; + } + if (defined ($next_next_line)) { + unshift (@lines, $next_next_line); + } + unshift (@lines, $next_line); + + # Our new next line might, coincidentally, begin with one of + # the line-start regexps, so we temporarily turn off + # sensitivity to that until we're past the line. + $suppress_line_start_match = 1; + + last; + } + else + { + $idx--; + } + } + + if ($idx == 0) + { + # We bottomed out because the line is longer than the + # available space. But that could be because the space is + # small, or because the line is longer than even the maximum + # possible space. Handle both cases below. + + if ($length_remaining == ($max_line_length - (length ($user_indent)))) + { + # The line is simply too long -- there is no hope of ever + # breaking it nicely, so just insert it verbatim, with + # appropriate padding. + $this_line = "\n${left_pad_str}${this_line}"; + } + else + { + # Can't break it here, but may be able to on the next round... + unshift (@lines, $this_line); + $length_remaining = $max_line_length - (length ($user_indent)); + $this_line = "\n${left_pad_str}"; + } + } + } + else # $this_len < $length_remaining, so tack on what we can. + { + # Leave a note for the next iteration. + $length_remaining = $length_remaining - $this_len; + + if ($this_line =~ /\.$/) + { + $this_line .= " "; + $length_remaining -= 2; + } + else # not a sentence end + { + $this_line .= " "; + $length_remaining -= 1; + } + } + + # Unconditionally indicate that loop has run at least once. + $first_time = 0; + + $wrapped_text .= "${user_indent}${this_line}"; + } + + # One last bit of padding. + $wrapped_text .= "\n"; + + return $wrapped_text; +} + +# main + +my @date; +my $author; +my @files; +my $comment; +my $merge; + +my $state; # 0-header 1-comment 2-files +my $done = 0; + +$state = 0; + +while (<>) { + #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n"; + + if ($state == 0) { + if (m,^Author: (.*),) { + $author = $1; + } + if (m,^Date: (.*),) { + @date = strptime($1); + } + if (m,^Merge: (.*),) { + $merge = 1; + } + $state = 1 if (m,^$,); + } elsif ($state == 1) { + $state = 2 if (m,^$,); + s/^ //g; + s/\n/ /g; + $comment = $comment . $_; + } elsif ($state == 2 && $merge) { + $done = 1; + } elsif ($state == 2) { + if (m,^([-0-9]+)\t([-0-9]+)\t(.*)$,) { + push @files, $3; + } elsif (m,^[^ ],) { + # No file changes. + $done = 1; + } + $done = 1 if (m,^$,); + } + + if ($done && @date == ()) { + print STDERR "warning: could not parse entry\n"; + } elsif ($done) { + print (strftime "%Y-%m-%d $author\n\n", @date); + + my $files = join (", ", @files); + $files = mywrap ("\t", "\t", "* $files"), ": "; + + if (index($comment, EMPTY_LOG_MESSAGE) > -1 ) { + $comment = "[no log message]\n"; + } + + my $files_last_line_len = 0; + $files_last_line_len = last_line_len($files) + 1; + my $msg = wrap_log_entry($comment, "\t", 69-$files_last_line_len, 69); + + $msg =~ s/[ \t]+\n/\n/g; + + if ($merge) { + print "\t$msg\n"; + } else { + print "$files: $msg\n"; + } + + @date = (); + $author = ""; + @files = (); + $comment = ""; + $merge = 0; + + $state = 0; + $done = 0; + } +} + +if (@files) { + print (strftime "%Y-%m-%d $author\n\n", @date); + my $msg = wrap_log_entry($comment, "\t", 69, 69); + $msg =~ s/[ \t]+\n/\n/g; + print "\t* $msg\n"; +}