diff --git a/actions/upgrades b/actions/upgrades
new file mode 100755
index 000000000..d72064b4f
--- /dev/null
+++ b/actions/upgrades
@@ -0,0 +1,111 @@
+#!/usr/bin/python3
+# -*- mode: python -*-
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+Configures or runs unattended-upgrades
+"""
+
+import argparse
+import os
+import subprocess
+
+CONF_FILE = '/etc/apt/apt.conf.d/50unattended-upgrades'
+AUTO_CONF_FILE = '/etc/apt/apt.conf.d/20auto-upgrades'
+
+
+def parse_arguments():
+ """REturn parsed command line arguments as dictionary"""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ # Run unattended-upgrades
+ subparsers.add_parser('run', help='Upgrade packages on the system')
+
+ # Enable automatic upgrades
+ subparsers.add_parser('enable-auto', help='Enable automatic upgrades')
+
+ # Disable automatic upgrades
+ subparsers.add_parser('disable-auto', help='Disable automatic upgrades.')
+
+ return parser.parse_args()
+
+
+def subcommand_run(_):
+ """Run unattended-upgrades"""
+ try:
+ setup()
+ except FileNotFoundError:
+ print('Error: Could not configure unattended-upgrades.')
+ return
+
+ try:
+ output = subprocess.check_output(['unattended-upgrades', '-v'])
+ except subprocess.CalledProcessError as error:
+ print('Error: %s', error)
+ except FileNotFoundError:
+ print('Error: unattended-upgrades is not available.')
+ else:
+ print('%s', output.decode())
+
+
+def subcommand_enable_auto(_):
+ """Enable automatic upgrades"""
+ try:
+ setup()
+ except FileNotFoundError:
+ print('Error: Could not configure unattended-upgrades.')
+ return
+
+ with open(AUTO_CONF_FILE, 'w') as conffile:
+ conffile.write('APT::Periodic::Update-Package-Lists "1";\n')
+ conffile.write('APT::Periodic::Unattended-Upgrade "1";\n')
+
+
+def subcommand_disable_auto(_):
+ """Disable automatic upgrades"""
+ os.remove(AUTO_CONF_FILE)
+
+
+def setup():
+ """Sets unattended-upgrades config to upgrade any package from Debian."""
+ with open(CONF_FILE, 'r') as conffile:
+ lines = conffile.readlines()
+
+ for line in lines:
+ if '"origin=Debian";' in line:
+ return # already configured
+
+ with open(CONF_FILE, 'w') as conffile:
+ for line in lines:
+ conffile.write(line)
+ if 'Unattended-Upgrade::Origins-Pattern {' in line:
+ conffile.write(' "origin=Debian";\n')
+
+
+def main():
+ """Parse arguments and perform all duties"""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/data/etc/plinth/modules-enabled/upgrades b/data/etc/plinth/modules-enabled/upgrades
new file mode 100644
index 000000000..259ea059e
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/upgrades
@@ -0,0 +1 @@
+plinth.modules.upgrades
diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py
new file mode 100644
index 000000000..24066ee4a
--- /dev/null
+++ b/plinth/modules/upgrades/__init__.py
@@ -0,0 +1,27 @@
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+Plinth module for upgrades
+"""
+
+from . import upgrades
+from .upgrades import init
+
+__all__ = ['upgrades', 'init']
+
+depends = ['plinth.modules.system']
diff --git a/plinth/modules/upgrades/templates/upgrades.html b/plinth/modules/upgrades/templates/upgrades.html
new file mode 100644
index 000000000..29c6d7cd3
--- /dev/null
+++ b/plinth/modules/upgrades/templates/upgrades.html
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% comment %}
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+{% endcomment %}
+
+{% block content %}
+
+
{{ title }}
+
+This will run unattended-upgrades, which will attempt to upgrade your system
+with the latest Debian packages. It may take a minute to complete.
+
+
+ Upgrade now »
+
+
+{% endblock %}
diff --git a/plinth/modules/upgrades/templates/upgrades_run.html b/plinth/modules/upgrades/templates/upgrades_run.html
new file mode 100644
index 000000000..de860a570
--- /dev/null
+++ b/plinth/modules/upgrades/templates/upgrades_run.html
@@ -0,0 +1,35 @@
+{% extends 'base.html' %}
+{% comment %}
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+{% endcomment %}
+
+{% block content %}
+
+{{title}}
+
+{% if upgrades_error %}
+ There was an error while upgrading:
+
{{ upgrades_error }}
+{% endif %}
+
+{% if upgrades_output %}
+ Output from unattended-upgrades:
+ {{ upgrades_output }}
+{% endif %}
+
+{% endblock %}
diff --git a/plinth/modules/upgrades/upgrades.py b/plinth/modules/upgrades/upgrades.py
new file mode 100644
index 000000000..d6557af43
--- /dev/null
+++ b/plinth/modules/upgrades/upgrades.py
@@ -0,0 +1,63 @@
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+Plinth module for upgrades
+"""
+
+from django.contrib.auth.decorators import login_required
+from django.template.response import TemplateResponse
+from gettext import gettext as _
+
+from plinth import actions
+from plinth import cfg
+from plinth import package
+from plinth.errors import ActionError
+
+
+def init():
+ """Initialize the module"""
+ menu = cfg.main_menu.get('system:index')
+ menu.add_urlname("Upgrades", "glyphicon-refresh",
+ "upgrades:index", 21)
+
+
+@login_required
+@package.required('unattended-upgrades')
+def index(request):
+ """Serve the index page"""
+ return TemplateResponse(request, 'upgrades.html',
+ {'title': _('Package Upgrades')})
+
+
+@login_required
+@package.required('unattended-upgrades')
+def run(request):
+ """Run upgrades and show the output page"""
+ output = ''
+ error = ''
+ try:
+ output = actions.superuser_run('upgrades', ['run'])
+ except ActionError as exception:
+ output, error = exception.args[1:]
+ except Exception as exception:
+ error = str(exception)
+
+ return TemplateResponse(request, 'upgrades_run.html',
+ {'title': _('Package Upgrades'),
+ 'upgrades_output': output,
+ 'upgrades__error': error})
diff --git a/plinth/modules/upgrades/urls.py b/plinth/modules/upgrades/urls.py
new file mode 100644
index 000000000..25c50fa85
--- /dev/null
+++ b/plinth/modules/upgrades/urls.py
@@ -0,0 +1,29 @@
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+URLs for the upgrades module
+"""
+
+from django.conf.urls import patterns, url
+
+
+urlpatterns = patterns(
+ 'plinth.modules.upgrades.upgrades',
+ url(r'^sys/upgrades/$', 'index', name='index'),
+ url(r'^sys/upgrades/run/$', 'run', name='run'),
+ )