Sunil Mohan Adapa afb00f98ab
upgrades: Make manual upgrade a background process
- Closes #366 and closes #304 (all sub-tasks).

- Start new process group with setsid() by sending
  start_new_session=True

- Detach from parent process fds by closing all FDs and attaching stdin,
  stdou and stderr to /dev/null.

- Don't wait for the process to complete.

- This allows for upgrading Plinth while upgrades are trigged from
  Plinth itself.

- Show log of upgrade exection instead of output and error log of the
  process which can no longer be collected.  This has the advantage of
  showing automatic executions also.

- Rewrite the mechanism to detect whether upgrades can be run.  It is
  now based on whether the package manager is busy.  This has the
  advantage of working properly if other apt processes are running,
  automatic upgrades are running, etc.

- Busy status works even if Plinth is restarted while upgrades are in
  progress.

- More descriptive messages showing that upgrades don't have to be
  triggered manually.

- Warn that other packages can't be installed while upgrades are
  running, which may take a long time.

- Warn the users of potential temporary unavailability of
  Plinth/Apache2.
2016-01-20 21:13:18 -05:00

139 lines
4.5 KiB
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 <http://www.gnu.org/licenses/>.
#
"""
Plinth module for upgrades
"""
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.translation import ugettext as _, ugettext_lazy
from django.views.decorators.http import require_POST
import subprocess
from .forms import ConfigureForm
from plinth import actions
from plinth import package
from plinth.errors import ActionError
subsubmenu = [{'url': reverse_lazy('upgrades:index'),
'text': ugettext_lazy('Automatic Upgrades')},
{'url': reverse_lazy('upgrades:upgrade'),
'text': ugettext_lazy('Upgrade Packages')}]
LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log'
LOCK_FILE = '/var/log/dpkg/lock'
def on_install():
"""Enable automatic upgrades after install."""
actions.superuser_run('upgrades', ['enable-auto'])
@package.required(['unattended-upgrades'], on_install=on_install)
def index(request):
"""Serve the configuration form."""
status = get_status()
form = None
if request.method == 'POST':
form = ConfigureForm(request.POST, prefix='upgrades')
if form.is_valid():
_apply_changes(request, status, form.cleaned_data)
status = get_status()
form = ConfigureForm(initial=status, prefix='upgrades')
else:
form = ConfigureForm(initial=status, prefix='upgrades')
return TemplateResponse(request, 'upgrades_configure.html',
{'title': _('Automatic Upgrades'),
'form': form,
'subsubmenu': subsubmenu})
def is_package_manager_busy():
"""Return whether a package manager is running."""
try:
subprocess.check_output(['lsof', '/var/lib/dpkg/lock'])
return True
except subprocess.CalledProcessError:
return False
def get_log():
"""Return the current log for unattended upgrades."""
try:
with open(LOG_FILE, 'r') as file_handle:
return file_handle.read()
except IOError:
return None
@package.required(['unattended-upgrades'], on_install=on_install)
def upgrade(request):
"""Serve the upgrade page."""
is_busy = is_package_manager_busy()
if request.method == 'POST':
try:
actions.superuser_run('upgrades', ['run'])
messages.success(request, _('Upgrade process started.'))
is_busy = True
except ActionError:
messages.error(request, _('Starting upgrade failed.'))
return TemplateResponse(request, 'upgrades.html',
{'title': _('Package Upgrades'),
'subsubmenu': subsubmenu,
'is_busy': is_busy,
'log': get_log()})
def get_status():
"""Return the current status."""
output = actions.run('upgrades', ['check-auto'])
return {'auto_upgrades_enabled': 'True' in output.split()}
def _apply_changes(request, old_status, new_status):
"""Apply the form changes."""
if old_status['auto_upgrades_enabled'] \
== new_status['auto_upgrades_enabled']:
messages.info(request, _('Setting unchanged'))
return
if new_status['auto_upgrades_enabled']:
option = 'enable-auto'
else:
option = 'disable-auto'
try:
actions.superuser_run('upgrades', [option])
except ActionError as exception:
error = exception.args[2]
messages.error(
request, _('Error when configuring unattended-upgrades: {error}')
.format(error=error))
return
if option == 'enable-auto':
messages.success(request, _('Automatic upgrades enabled'))
else:
messages.success(request, _('Automatic upgrades disabled'))