diff --git a/libtaskotron.spec b/libtaskotron.spec
--- a/libtaskotron.spec
+++ b/libtaskotron.spec
@@ -13,7 +13,9 @@
 
 BuildArch:      noarch
 
+# sorted alphabetically
 Requires:       createrepo
+Requires:       dnf >= 0.6.4
 Requires:       koji
 Requires:       libtaskotron-config
 Requires:       mash
diff --git a/libtaskotron/rpm_utils.py b/libtaskotron/rpm_utils.py
--- a/libtaskotron/rpm_utils.py
+++ b/libtaskotron/rpm_utils.py
@@ -6,9 +6,11 @@
 ''' Utility methods related to RPM '''
 
 from __future__ import absolute_import
-import os
 import hawkey
+import subprocess
+import pipes
 
+from libtaskotron.logger import log
 from libtaskotron import exceptions as exc
 
 
@@ -97,3 +99,48 @@
     sack = hawkey.Sack()
 
     return rpmver1.evr_cmp(rpmver2, sack)
+
+
+def install(pkgs):
+    '''Install packages from system repositories using DNF. You need to have enough permissions
+    to run ``dnf`` through ``sudo``.
+
+    :param pkgs: packages to be installed, e.g. ``['pidgin']``, or any other specifier
+                 supported by ``dnf install`` command
+    :type pkgs: list of str
+    :raise TaskotronError: if DNF returncode is not zero
+    '''
+    cmd = ['sudo', 'dnf', 'install', '-y']
+    cmd.extend(pkgs)
+
+    log.info("Running command: %s", " ".join([pipes.quote(c) for c in cmd]))
+
+    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdout, _ = proc.communicate()
+
+    if proc.returncode != 0:
+        log.error(stdout)
+        raise exc.TaskotronError("Unable to install packages: %s" % " ".join(pkgs))
+    else:
+        log.debug(stdout)
+
+
+def is_installed(pkgs):
+    '''Check if packages are installed using DNF. No additional permissions needed.
+
+    :param pkgs: packages to be checked whether they are installed, e.g. ``['pidgin']``,
+                 or any other specifier supported by ``dnf install`` command
+    :type pkgs: list of str
+    :return: True if all specified packages are installed, False otherwise
+    :rtype: bool
+    '''
+    cmd = ['dnf', '--assumeno', '--disableplugin=noroot', 'install']
+    cmd.extend(pkgs)
+
+    log.debug("Running command: %s", " ".join([pipes.quote(c) for c in cmd]))
+
+    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdout, _ = proc.communicate()
+    log.debug(stdout)
+
+    return not proc.returncode
diff --git a/libtaskotron/runner.py b/libtaskotron/runner.py
--- a/libtaskotron/runner.py
+++ b/libtaskotron/runner.py
@@ -13,14 +13,16 @@
 import collections
 import datetime
 import yaml
+import pipes
 
 import libtaskotron
 from libtaskotron import taskformula
 from libtaskotron import logger
 from libtaskotron import python_utils
 from libtaskotron import file_utils
 from libtaskotron import config
 from libtaskotron import remote_exec
+from libtaskotron import rpm_utils
 from libtaskotron.logger import log
 import libtaskotron.exceptions as exc
 
@@ -162,8 +164,24 @@
 
     def _validate_env(self):
         # TODO: implement this
-        raise NotImplementedError("Environment validation is not"
-                                  " yet implemented")
+        raise NotImplementedError("Environment validation is not yet implemented")
+
+    def prepare_task(self):
+        '''Prepare the environment for running a task. If there are packages, requirements or
+        groups specified in the task formula, this method tries to install them (in case of
+        production profile) or checks whether they are installed (in case of developer profile).
+        '''
+        rpms = self.taskdata.get('environment', {}).get('rpm', [])
+
+        profile = config.get_config().profile
+
+        if profile == config.ProfileName.PRODUCTION:  # TODO: or local/bare metal
+            rpm_utils.install(rpms)
+        else:
+            if not rpm_utils.is_installed(rpms):
+                raise exc.TaskotronError("Some packages are not installed. Please run "
+                                         "'dnf install %s' to install all required packages."
+                                         % " ".join([pipes.quote(rpm) for rpm in rpms]))
 
 
 class RemoteRunner(object):
@@ -180,11 +198,6 @@
                      'http://copr-fe.cloud.fedoraproject.org/coprs/tflink/taskotron/'
                      'repo/fedora-21/tflink-taskotron-fedora-21.repo')
         self.ssh.install_pkgs('libtaskotron')
-        #
-
-        # install required packages
-        rpms = self.task_data['environment']['rpm']
-        self.ssh.install_pkgs(rpms)
 
         # configure libtaskotron
         if config.get_config().config_filename:
@@ -303,15 +316,15 @@
 
 class Runner():
     '''Runner class that decides whether the task should be run locally or remotely,
-    intializes variables (such as logger and ssh), instantiates actuall Local or Remote Runner
+    intializes variables (such as logger and ssh), instantiates Local or Remote Runner
     and runs the task.
     '''
 
     def __init__(self, task_data, arg_data, uuid_dir):
         '''
         :param dict task_data: data loaded from task formula
         :param dict arg_data: data from command line arguments
-        :param dict uuid_dir: path to artifacts directory
+        :param str uuid_dir: path to artifacts directory
         '''
 
         self.task_data = task_data
diff --git a/testing/test_rpm_utils.py b/testing/test_rpm_utils.py
--- a/testing/test_rpm_utils.py
+++ b/testing/test_rpm_utils.py
@@ -6,10 +6,13 @@
 '''Unit tests for libtaskotron/rpm_utils.py'''
 
 import pytest
+import subprocess
 
-from libtaskotron.rpm_utils import rpmformat, cmpNEVR
+from libtaskotron.rpm_utils import rpmformat, cmpNEVR, install
 from libtaskotron import exceptions as exc
 
+from dingus import Dingus
+
 
 class TestRpmformat:
 
@@ -124,3 +127,26 @@
         '''Invalid input param'''
         with pytest.raises(exc.TaskotronValueError):
             cmpNEVR('foo-1.2-3.fc20', 'bar-1.2-3.fc20')
+
+
+@pytest.mark.usefixtures('setup')
+class TestInstall():
+
+    @pytest.fixture
+    def setup(self, monkeypatch):
+        self.stub_popen = Dingus()
+        self.stub_popen_proc = Dingus(communicate__returns=['', ''])
+
+        self.stub_popen.return_value = self.stub_popen_proc
+        monkeypatch.setattr(subprocess, 'Popen', self.stub_popen)
+
+    def test_zero_returncode(self):
+        self.stub_popen_proc.returncode = 0
+
+        install(['foo'])
+
+    def test_nonzero_returncode(self):
+        self.stub_popen_proc.returncode = 1
+
+        with pytest.raises(exc.TaskotronError):
+            install(['foo'])
diff --git a/testing/test_runner.py b/testing/test_runner.py
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -10,6 +10,8 @@
 from dingus import Dingus
 
 from libtaskotron import runner
+from libtaskotron import config
+from libtaskotron import rpm_utils
 from libtaskotron import exceptions as exc
 
 
@@ -457,17 +459,56 @@
 
 
 @pytest.mark.usefixtures('setup')
+class TestLocalRunnerPrepareTask():
+
+    @pytest.fixture
+    def setup(self, monkeypatch):
+        self.install_stub = Dingus()
+        self.is_installed_stub = Dingus()
+        self.stub_config = Dingus()
+        self.stub_get_config = Dingus()
+
+        self.stub_get_config.return_value = self.stub_config
+        monkeypatch.setattr(rpm_utils, 'install', self.install_stub)
+        monkeypatch.setattr(rpm_utils, 'is_installed', self.is_installed_stub)
+        monkeypatch.setattr(config, 'get_config', self.stub_get_config)
+
+        self.test_runner = runner.LocalRunner({}, {})
+
+    def test_devel_profile_installed(self, monkeypatch):
+        self.is_installed_stub.return_value = True
+        self.stub_config.profile = 'devel'
+
+        self.test_runner.prepare_task()
+
+        assert len(self.install_stub.calls()) == 0 and len(self.is_installed_stub.calls()) == 1
+
+    def test_devel_profile_not_installed(self, monkeypatch):
+        self.is_installed_stub.return_value = False
+        self.stub_config.profile = 'devel'
+
+        with pytest.raises(exc.TaskotronError):
+            self.test_runner.prepare_task()
+
+        assert len(self.install_stub.calls()) == 0 and len(self.is_installed_stub.calls()) == 1
+
+    def test_prod_profile(self, monkeypatch):
+        self.is_installed_stub.return_value = True
+        self.stub_config.profile = 'production'
+
+        self.test_runner.prepare_task()
+
+        assert len(self.install_stub.calls()) == 1 and len(self.is_installed_stub.calls()) == 0
+
+
+@pytest.mark.usefixtures('setup')
 class TestRunner():
 
-    def setup_method(self, method):
+    @pytest.fixture
+    def setup(self, monkeypatch):
         self.stub_local = Dingus()
         self.stub_remote = Dingus()
         self.uuid_dir = '/something/totally/random'
-
-    @pytest.fixture
-    def setup(self, monkeypatch):
-        self.stub_local.reset()
-        self.stub_remote.reset()
         monkeypatch.setattr(runner.Runner, '_run_locally', self.stub_local)
         monkeypatch.setattr(runner.Runner, '_run_remotely', self.stub_remote)
 

