udiskie: Merge into storage module

udiskie is now an essential module that will be installed along with storage.

Signed-off-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2018-09-24 11:39:43 +05:30 committed by James Valleroy
parent f172925d9d
commit a307476634
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
11 changed files with 163 additions and 304 deletions

View File

@ -1 +0,0 @@
plinth.modules.udiskie

View File

@ -27,7 +27,7 @@ from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth.menu import main_menu
from plinth.modules import udiskie
from plinth.modules import storage
from .backups import backup_apps, restore_apps
@ -118,8 +118,8 @@ def export_archive(name, location):
def get_export_locations():
"""Return a list of storage locations for exported backup archives."""
locations = [('/var/lib/freedombox/', _('Root Filesystem'))]
if udiskie.is_running():
devices = udiskie.udisks2.list_devices()
if storage.is_running():
devices = storage.udisks2.list_devices()
for device in devices:
if 'mount_points' in device and len(device['mount_points']) > 0:
name = device['label'] or device['device']

View File

@ -17,21 +17,32 @@
"""
FreedomBox app to manage storage.
"""
import json
import logging
import subprocess
from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import service as service_module
from plinth import action_utils, actions, cfg
from plinth.menu import main_menu
from plinth.utils import format_lazy
version = 2
version = 3
name = _('Storage')
description = []
managed_services = ['freedombox-udiskie']
managed_packages = ['udiskie', 'gir1.2-udisks-2.0']
description = [
format_lazy(
_('This module allows you to manage storage media attached to your '
'{box_name}. You can view the storage media currently in use, mount '
'and unmount removable media, expand the root partition etc.'),
box_name=_(cfg.box_name))
]
service = None
@ -81,8 +92,8 @@ def _get_disks_from_df():
disks = []
for line in output.splitlines()[1:]:
parts = line.split(maxsplit=6)
keys = ('device', 'file_system_type', 'size', 'used',
'free', 'percent_used', 'mount_point')
keys = ('device', 'file_system_type', 'size', 'used', 'free',
'percent_used', 'mount_point')
disk = dict(zip(keys, parts))
disk['percent_used'] = int(disk['percent_used'].rstrip('%'))
disk['size'] = int(disk['size'])
@ -168,8 +179,37 @@ def format_bytes(size):
return _('{disk_size:.1f} TiB').format(disk_size=size)
def is_running():
"""Return whether the service is running."""
return action_utils.service_is_running('freedombox-udiskie')
def is_enabled():
"""Return whether the module is enabled."""
return action_utils.service_is_enabled('freedombox-udiskie')
def enable():
"""Enable the module."""
actions.superuser_run('udiskie', ['enable'])
def disable():
"""Disable the module."""
actions.superuser_run('udiskie', ['disable'])
def setup(helper, old_version=None):
"""Expand root parition on first setup."""
"""Install and configure the module."""
helper.install(managed_packages, skip_recommends=True)
helper.call('post', actions.superuser_run, 'udiskie', ['enable'])
global service
if service is None:
service = service_module.Service(
managed_services[0], name, ports=[], is_external=True,
is_enabled=is_enabled, enable=enable, disable=disable,
is_running=is_running)
helper.call('post', service.notify_enabled, None, True)
disks = get_disks()
root_device = get_root_device(disks)
if is_expandable(root_device):

View File

@ -36,6 +36,12 @@
<h2>{{ title }}</h2>
{% endblock %}
{% block description %}
{% for paragraph in description %}
<p>{{ paragraph|safe }}</p>
{% endfor %}
{% endblock %}
{% if manual_page %}
<p class="manual-page">
<a href="{% url 'help:manual-page' manual_page %}">
@ -44,7 +50,7 @@
</p>
{% endif %}
<p>{% trans "The following disks are in use:" %}</p>
<p>{% trans "The following storage devices are in use:" %}</p>
<table class="table table-bordered table-condensed table-striped">
<thead>
@ -66,17 +72,17 @@
{% if disk.percent_used < 75 %}
<div class="progress-bar progress-bar-striped progress-bar-success"
{% elif disk.percent_used < 90 %}
<div class="progress-bar progress-bar-striped progress-bar-warning"
<div class="progress-bar progress-bar-striped progress-bar-warning"
{% else %}
<div class="progress-bar progress-bar-striped progress-bar-danger"
<div class="progress-bar progress-bar-striped progress-bar-danger"
{% endif %}
role="progressbar" aria-valuenow="disk.percent_used"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ disk.percent_used }}%;">
{{ disk.percent_used }}%
</div>
</div>
<div>{{ disk.used_str }} / {{ disk.size_str }}</div>
role="progressbar" aria-valuenow="disk.percent_used"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ disk.percent_used }}%;">
{{ disk.percent_used }}%
</div>
</div>
<div>{{ disk.used_str }} / {{ disk.size_str }}</div>
</td>
</tr>
{% endfor %}
@ -100,4 +106,55 @@
</p>
{% endif %}
{% block status %}
{{ block.super }}
<h3>{% trans "Removable Devices" %}</h3>
{% if not devices %}
<p>
{% blocktrans trimmed %}
There are no additional storage devices attached.
{% endblocktrans %}
</p>
{% else %}
<table class="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>{% trans "Device" %}</th>
<th>{% trans "Label" %}</th>
<th>{% trans "Mount Point" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for device in devices %}
<tr>
<td>{{ device.device }}</td>
<td>{{ device.label }}</td>
<td>{{ device.mount_points|join:', ' }}</td>
<td>{{ device.filesystem_type }}</td>
<td>{{ device.size }}</td>
<td>
{% if device.mount_points %}
<form class="form" method="post"
action="{% url 'storage:eject' device.device|urlencode:"" %}">
{% csrf_token %}
<button type="submit"
class="btn btn-sm btn-default glyphicon glyphicon-eject">
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -14,7 +14,6 @@
# 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/>.
#
"""
URLs for the disks module.
"""
@ -23,8 +22,9 @@ from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^sys/storage/$', views.index, name='index'),
url(r'^sys/storage/expand$', views.expand, name='expand'),
url(r'^sys/storage/eject/(?P<device_path>[\w%]+)/$', views.eject,
name='eject')
]

View File

@ -19,16 +19,20 @@ Views for storage module.
"""
import logging
import urllib.parse
from django.contrib import messages
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from plinth.modules import storage
from plinth.utils import format_lazy, is_user_admin
from . import udisks2
logger = logging.getLogger(__name__)
@ -44,7 +48,9 @@ def index(request):
return TemplateResponse(
request, 'storage.html', {
'title': _('Storage'),
'description': storage.description,
'disks': disks,
'devices': udisks2.list_devices(),
'manual_page': storage.manual_page,
'expandable_root_size': expandable_root_size
})
@ -73,9 +79,10 @@ def expand_partition(request, device):
try:
storage.expand_partition(device)
except Exception as exception:
messages.error(request,
_('Error expanding partition: {exception}')
.format(exception=exception))
messages.error(
request,
_('Error expanding partition: {exception}')
.format(exception=exception))
else:
messages.success(request, _('Partition expanded successfully.'))
@ -106,3 +113,37 @@ def warn_about_low_disk_space(request):
messages.error(request, message)
elif percent_used > 75 or free_gib < 2:
messages.warning(request, message)
@require_POST
def eject(request, device_path):
"""Eject a device, given its path.
Device path is quoted with slashes written as %2F.
"""
device_path = urllib.parse.unquote(device_path)
try:
drive = udisks2.eject_drive_of_device(device_path)
if drive:
messages.success(
request,
_('{drive_vendor} {drive_model} can be safely unplugged.')
.format(drive_vendor=drive['vendor'],
drive_model=drive['model']))
else:
messages.success(request, _('Device can be safely unplugged.'))
except Exception as exception:
try:
message = udisks2.get_error_message(exception)
except AttributeError:
message = str(exception)
logger.exception('Error ejecting device - %s', message)
messages.error(
request,
_('Error ejecting device: {error_message}').format(
error_message=message))
return redirect(reverse('storage:index'))

View File

@ -1,90 +0,0 @@
#
# This file is part of FreedomBox.
#
# 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/>.
#
"""
FreedomBox app for udiskie.
"""
from django.utils.translation import ugettext_lazy as _
from plinth import action_utils, actions
from plinth import service as service_module
from plinth.menu import main_menu
version = 1
managed_services = ['freedombox-udiskie']
managed_packages = ['udiskie', 'gir1.2-udisks-2.0']
name = _('udiskie')
short_description = _('Removable Media')
description = [
_('udiskie allows automatic mounting of removable media, such as flash '
'drives.'),
]
service = None
def init():
"""Intialize the module."""
menu = main_menu.get('system')
menu.add_urlname(name, 'glyphicon-floppy-disk', 'udiskie:index',
short_description)
global service
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup':
service = service_module.Service(
managed_services[0], name, ports=[], is_external=True,
is_enabled=is_enabled, enable=enable, disable=disable,
is_running=is_running)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages, skip_recommends=True)
helper.call('post', actions.superuser_run, 'udiskie', ['enable'])
global service
if service is None:
service = service_module.Service(
managed_services[0], name, ports=[], is_external=True,
is_enabled=is_enabled, enable=enable, disable=disable,
is_running=is_running)
helper.call('post', service.notify_enabled, None, True)
def is_running():
"""Return whether the service is running."""
return action_utils.service_is_running('freedombox-udiskie')
def is_enabled():
"""Return whether the module is enabled."""
return action_utils.service_is_enabled('freedombox-udiskie')
def enable():
"""Enable the module."""
actions.superuser_run('udiskie', ['enable'])
def disable():
"""Disable the module."""
actions.superuser_run('udiskie', ['disable'])

View File

@ -1,75 +0,0 @@
{% extends "service.html" %}
{% comment %}
#
# This file is part of FreedomBox.
#
# 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/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block status %}
{{ block.super }}
<h3>{% trans "Devices" %}</h3>
{% if not devices %}
<p>
{% blocktrans trimmed %}
There are no additional storage devices attached.
{% endblocktrans %}
</p>
{% else %}
<table class="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>{% trans "Device" %}</th>
<th>{% trans "Label" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "Filesystem" %}</th>
<th>{% trans "Mount Point" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for device in devices %}
<tr>
<td>{{ device.device }}</td>
<td>{{ device.label }}</td>
<td>{{ device.size }}</td>
<td>{{ device.filesystem_type }}</td>
<td>{{ device.mount_points|join:', ' }}</td>
<td>
{% if device.mount_points %}
<form class="form" method="post"
action="{% url 'udiskie:eject' device.device|urlencode:"" %}">
{% csrf_token %}
<button type="submit"
class="btn btn-sm btn-default glyphicon glyphicon-eject">
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -1,34 +0,0 @@
#
# This file is part of FreedomBox.
#
# 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/>.
#
"""
URLs for the udiskie module.
"""
from django.conf.urls import url
from plinth.modules import udiskie
from .views import Index, eject
urlpatterns = [
url(
r'^sys/udiskie/$',
Index.as_view(service_id=udiskie.managed_services[0],
description=udiskie.description, show_status_block=True),
name='index'),
url(r'^sys/udiskie/eject/(?P<device_path>[\w%]+)/$', eject, name='eject'),
]

View File

@ -1,79 +0,0 @@
#
# This file is part of FreedomBox.
#
# 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/>.
#
"""
Views for udiskie module.
"""
import urllib.parse
from logging import Logger
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from plinth.views import ServiceView
from . import udisks2
logger = Logger(__name__)
class Index(ServiceView):
"""View to show devices."""
template_name = 'udiskie.html'
def get_context_data(self, *args, **kwargs):
"""Return the context data rendering the template."""
context = super().get_context_data(**kwargs)
context['devices'] = udisks2.list_devices()
return context
@require_POST
def eject(request, device_path):
"""Eject a device, given its path.
Device path is quoted with slashes written as %2F.
"""
device_path = urllib.parse.unquote(device_path)
try:
drive = udisks2.eject_drive_of_device(device_path)
if drive:
messages.success(
request,
_('{drive_vendor} {drive_model} can be safely unplugged.')
.format(drive_vendor=drive['vendor'],
drive_model=drive['model']))
else:
messages.success(request, _('Device can be safely unplugged.'))
except Exception as exception:
try:
message = udisks2.get_error_message(exception)
except AttributeError:
message = str(exception)
logger.exception('Error ejecting device - %s', message)
messages.error(
request,
_('Error ejecting device: {error_message}').format(
error_message=message))
return redirect(reverse('udiskie:index'))