Index: libtaskotron/bodhi_utils.py
===================================================================
--- /dev/null
+++ libtaskotron/bodhi_utils.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Copyright 2010-2014, Red Hat, Inc.
+# License: GNU General Public License version 2 or later
+
+'''Utility functions for dealing with Bodhi'''
+
+import fedora.client
+
+from .logger import log
+
+bodhi = fedora.client.bodhi.BodhiClient()
+
+
+def query_update(package):
+    '''Get the last Bodhi update matching criteria.
+
+       @param package package NVR or package name or update title or update ID
+                      Note: Only NVR allowed, not ENVR. See
+                      https://fedorahosted.org/bodhi/ticket/592.
+
+       @return most recent Bodhi update object matching criteria, or None
+               when no update is found.
+    '''
+    log.debug('Searching Bodhi updates for: %s', package)
+    res = bodhi.query(package=package, limit=1)
+    if res['updates']:
+        return res['updates'][0]
+    else:
+        return None
+
Index: libtaskotron/directives/bodhi_directive.py
===================================================================
--- /dev/null
+++ libtaskotron/directives/bodhi_directive.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, Red Hat, Inc.
+# License: GNU General Public License version 2 or later
+
+import libtaskotron.bodhi_utils as bodhi
+from libtaskotron.koji_utils import KojiClient
+from libtaskotron.directives import BaseDirective
+
+from libtaskotron.logger import log
+from libtaskotron.exceptions import TaskotronDirectiveError
+
+directive_class = 'BodhiDirective'
+
+class BodhiDirective(BaseDirective):
+    """The bodhi directive interfaces with Bodhi to facilitate
+    various bodhi actions.
+
+    format: "bodhi:
+               action: [download]
+               <command args>"
+
+    Valid Commands:
+
+    download
+      Downloads rpms of builds from update specified by its name or id
+      format: "bodhi:
+                 action: download
+                 bodhi-id: <name or id of update>
+                 arch=<desired arch>"
+    """
+    def __init__(self, bodhi_api=None, koji_session=None):
+
+        if bodhi_api:
+            self.bodhi_api = bodhi_api
+        else:
+            self.bodhi_api = bodhi
+
+        if koji_session:
+            self.koji_session = koji_session
+        else:
+            self.koji_session = KojiClient()
+
+    def action_download(self, update, arches, workdir):
+
+        res = self.bodhi_api.query_update(update)
+
+        if res is None:
+            raise TaskotronDirectiveError("Update with id '%s' wasn't found"
+                                    % update)
+
+        nvrs = [build['nvr'] for build in res['builds']]
+
+        downloaded_rpms = []
+
+        for nvr in nvrs:
+            res = self.koji_session.get_nvr_rpms(nvr, workdir, arches)
+            downloaded_rpms.extend(res)
+
+        return downloaded_rpms
+
+    def process(self, input_data, env_data):
+        output_data = {}
+
+        valid_actions = ['download']
+
+        action = input_data['action']
+
+        if action not in valid_actions:
+            raise TaskotronDirectiveError('%s is not a valid command for bodhi directive'
+                                 % action)
+
+        if action == 'download':
+            if 'bodhi_id' not in input_data or 'arch' not in input_data:
+                detected_args = ', '.join(input_data.keys())
+                raise TaskotronDirectiveError("The bodhi directive 'download'"
+                   " requires both 'bodhi-id' and"
+                   " 'arch' arguments. Detected arguments: %s" % detected_args)
+
+            workdir = env_data['workdir']
+            update = input_data['bodhi_id']
+            if input_data['arch'] == 'all':
+                arches = ['i386', 'x86_64', 'noarch', 'armhfp']
+            else:
+                arches = [input_data['arch']]
+            if 'noarch' not in arches and arches != ['src']:
+                arches.append('noarch')
+
+            log.info("getting rpms for update %s (%s) and downloading to %s",
+                      update, arches, workdir)
+
+            output_data['downloaded_rpms'] = self.action_download(update, arches, workdir)
+
+        return output_data
Index: libtaskotron/runner.py
===================================================================
--- libtaskotron/runner.py
+++ libtaskotron/runner.py
@@ -133,11 +133,18 @@
     parser = argparse.ArgumentParser()
     parser.add_argument("task", nargs=1, help="task to run")
     parser.add_argument("-a", "--arch",
-                        choices=["i386", "x86_64", "armhfp", "noarch"], help=""
-                        "Architecture to be used; support arches are x86,\n"
-                        "x86_64, and armhfp.\nIf omitted, defaults to noarch.")
+                        choices=["i386", "x86_64", "armhfp", "noarch", "all"],
+                        help="""
+                        Architecture to be used; support arches are x86, x86_64, and armhfp.
+                        If omitted, defaults to noarch. If every architechtures are wanted
+                        use all.
+                        """)
     parser.add_argument("-e", "--envr", help="envr to be used as input")
     parser.add_argument("-t", "--tag", help="koji tag to be used as input")
+    parser.add_argument("-b", "--bodhi-id",
+                        help="""
+                        Identification of bodhi update. Update id and name could be used.
+                        """)
     return parser
 
 
Index: testing/test_bodhi_directive.py
===================================================================
--- /dev/null
+++ testing/test_bodhi_directive.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, Red Hat, Inc.
+# License: GNU General Public License version 2 or later
+
+'''Unit tests for libtaskotron/directives/bodhi_directive.py'''
+
+import pytest
+from dingus import Dingus
+
+from libtaskotron.directives import bodhi_directive
+from libtaskotron.exceptions import TaskotronDirectiveError
+
+class TestBodhiDownloads():
+
+    def setup_method(self, method):
+        '''Run this before every test invocation'''
+        self.ref_arch = 'x86_64'
+        self.ref_bodhi_id = 'FEDORA-1234-56789'
+        self.ref_workdir = '/var/tmp/foo'
+        self.ref_taskfile = '/foo/bar/task.yml'
+        self.ref_nvr1 = 'foo-1.2.3.fc99'
+        self.ref_nvr2 = 'bar-1.2.3.fc99'
+        self.ref_update = {'title': 'Random update',
+                       'builds': [{'nvr': self.ref_nvr1, 'package': {}},
+                                  {'nvr': self.ref_nvr2, 'package': {}}
+                                  ],
+                       'other_keys': {}
+                       }
+        self.ref_input = {'action': 'download',
+                          'arch': self.ref_arch,
+                          'bodhi_id': self.ref_bodhi_id
+                         }
+        self.ref_envdata = {'workdir': self.ref_workdir}
+        self.ref_rpmfile = '%s/%s.rpm' % (self.ref_workdir, self.ref_nvr1)
+
+        self.helper = bodhi_directive.BodhiDirective()
+
+
+    def test_action_download_existing_update(self):
+        '''Test download of existing update'''
+        stub_bodhi = Dingus(query_update__returns = self.ref_update)
+        stub_koji = Dingus(get_nvr_rpms__returns = [self.ref_rpmfile])
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        output_data = test_bodhi.process(self.ref_input, self.ref_envdata)
+
+        ref_output_data = {'downloaded_rpms': [self.ref_rpmfile,
+                                               self.ref_rpmfile]}
+        getrpms_calls = stub_koji.calls()
+        requested_nvrs = map(lambda x: x[1][0], getrpms_calls)
+
+        # checks whether get_nvr_rpms was called for every update
+        # with correct nvrs
+        assert len(getrpms_calls) == 2
+        assert requested_nvrs == [self.ref_nvr1, self.ref_nvr2]
+        assert output_data == ref_output_data
+
+
+    def test_action_download_all_archs(self):
+        '''Test download when all arches are demanded'''
+        self.ref_input['arch'] = 'all'
+
+        stub_bodhi = Dingus(query_update__returns = self.ref_update)
+        stub_koji = Dingus(get_nvr_rpms__returns = [self.ref_rpmfile])
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        test_bodhi.process(self.ref_input, self.ref_envdata)
+
+        ref_arches = set(['i386', 'x86_64', 'noarch', 'armhfp'])
+        getrpms_calls = stub_koji.calls()
+        req_arches = map(lambda x: x[1][2], getrpms_calls)
+
+        # checks whether all get_nvr_rpms calls demanded all arches
+        assert True == all(map(lambda x: set(x) == ref_arches, req_arches))
+
+    def test_action_download_source(self):
+        '''Test download of source packages'''
+        self.ref_input['arch'] = 'src'
+
+        stub_bodhi = Dingus(query_update__returns = self.ref_update)
+        stub_koji = Dingus(get_nvr_rpms__returns = [self.ref_rpmfile])
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        test_bodhi.process(self.ref_input, self.ref_envdata)
+
+        getrpms_calls = stub_koji.calls()
+        req_arches = map(lambda x: x[1][2], getrpms_calls)
+
+        # checks whether all get_nvr_rpms calls demanded only source pkgs
+        assert all(map(lambda x: x == ['src'], req_arches))
+
+    def test_action_download_added_noarch(self):
+        '''Test whether noarch is automaticaly added'''
+        self.ref_input['arch'] = 'i386'
+
+        stub_bodhi = Dingus(query_update__returns = self.ref_update)
+        stub_koji = Dingus(get_nvr_rpms__returns = [self.ref_rpmfile])
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        test_bodhi.process(self.ref_input, self.ref_envdata)
+
+        ref_arches = set(['i386', 'noarch'])
+        getrpms_calls = stub_koji.calls()
+        req_arches = map(lambda x: x[1][2], getrpms_calls)
+
+        # checks whether noarch is demanded in all get_nvr_rpms calls
+        assert all(map(lambda x: 'noarch' in x, req_arches))
+
+
+    def test_action_download_nonexisting_update(self):
+        '''Test whether exception is raised when no update is found'''
+        stub_bodhi = Dingus(query_update__returns = None)
+        stub_koji = Dingus(get_nvr_rpms__returns = "It shouldn't go this far")
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        # No update is found, b. directive should raise an Exception
+        with pytest.raises(TaskotronDirectiveError):
+            test_bodhi.process(self.ref_input, self.ref_envdata)
+
+
+    def test_invalid_action(self):
+        '''Test response on non-existing action'''
+
+        self.ref_input['action'] = 'foo'
+
+        stub_bodhi = Dingus()
+        stub_koji = Dingus()
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        # Unknown action should raise an Exception
+        with pytest.raises(TaskotronDirectiveError):
+            test_bodhi.process(self.ref_input, self.ref_envdata)
+
+
+    def test_action_download_insufficient_input_data(self):
+        '''Test response on unsufficient input for download action'''
+        ref_input = {'action': 'download'}
+
+        stub_bodhi = Dingus()
+        stub_koji = Dingus()
+
+        test_bodhi = bodhi_directive.BodhiDirective(stub_bodhi, stub_koji)
+
+        # Tests with no information
+        with pytest.raises(TaskotronDirectiveError):
+            test_bodhi.process(ref_input, self.ref_envdata)
+
+        ref_input['arch'] = self.ref_arch
+
+        # Tests with only arch, or bodhi_id given
+        with pytest.raises(TaskotronDirectiveError):
+            test_bodhi.process(ref_input, self.ref_envdata)
+
+        ref_input.pop('arch')
+        ref_input['bodhi_id'] = self.ref_bodhi_id
+
+        with pytest.raises(TaskotronDirectiveError):
+            test_bodhi.process(ref_input, self.ref_envdata)
Index: testing/test_bodhi_utils.py
===================================================================
--- /dev/null
+++ testing/test_bodhi_utils.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, Red Hat, Inc.
+# License: GNU General Public License version 2 or later
+
+'''Unit tests for libtaskotron/bodhi_utils.py'''
+
+import pytest
+from dingus import Dingus
+
+from libtaskotron import bodhi_utils
+
+class TestKojiDownloads():
+
+    def setup_method(self, method):
+        '''Run this before every test invocation'''
+        self.ref_bodhi_id = 'FEDORA-1234-56789'
+        self.ref_update = {'title': 'Random update',
+                       'builds': [{'nvr': 'foo-1.2-3.fc99',
+                                   'package': {}}],
+                       'other_keys': {}
+                       }
+
+
+    def test_query_existing_update(self, monkeypatch):
+        '''Test query for existing update'''
+        ref_query_answer = {'tg_flash': None,
+                            'num_items': 1,
+                            'title': '1 update found',
+                            'updates': [self.ref_update]
+                            }
+        stub_bodhi = Dingus(query__returns = ref_query_answer)
+
+        monkeypatch.setattr(bodhi_utils, 'bodhi', stub_bodhi)
+
+        update = bodhi_utils.query_update(self.ref_bodhi_id)
+
+        assert update == self.ref_update
+
+    def test_query_non_existing_update(self, monkeypatch):
+        '''Test query for non-existing update'''
+        ref_query_answer = {'tg_flash': None,
+                            'num_items': 0,
+                            'title': '0 updates found',
+                            'updates': []
+                            }
+        stub_bodhi = Dingus(query__returns = ref_query_answer)
+
+        monkeypatch.setattr(bodhi_utils, 'bodhi', stub_bodhi)
+
+        update = bodhi_utils.query_update(self.ref_bodhi_id)
+
+        assert update is None
+
+
