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

View File

@ -17,21 +17,32 @@
""" """
FreedomBox app to manage storage. FreedomBox app to manage storage.
""" """
import json import json
import logging import logging
import subprocess import subprocess
from django.utils.translation import ugettext_lazy as _ 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.menu import main_menu
from plinth.utils import format_lazy
version = 2 version = 3
name = _('Storage') 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 service = None
@ -81,8 +92,8 @@ def _get_disks_from_df():
disks = [] disks = []
for line in output.splitlines()[1:]: for line in output.splitlines()[1:]:
parts = line.split(maxsplit=6) parts = line.split(maxsplit=6)
keys = ('device', 'file_system_type', 'size', 'used', keys = ('device', 'file_system_type', 'size', 'used', 'free',
'free', 'percent_used', 'mount_point') 'percent_used', 'mount_point')
disk = dict(zip(keys, parts)) disk = dict(zip(keys, parts))
disk['percent_used'] = int(disk['percent_used'].rstrip('%')) disk['percent_used'] = int(disk['percent_used'].rstrip('%'))
disk['size'] = int(disk['size']) disk['size'] = int(disk['size'])
@ -168,8 +179,37 @@ def format_bytes(size):
return _('{disk_size:.1f} TiB').format(disk_size=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): 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() disks = get_disks()
root_device = get_root_device(disks) root_device = get_root_device(disks)
if is_expandable(root_device): if is_expandable(root_device):

View File

@ -36,6 +36,12 @@
<h2>{{ title }}</h2> <h2>{{ title }}</h2>
{% endblock %} {% endblock %}
{% block description %}
{% for paragraph in description %}
<p>{{ paragraph|safe }}</p>
{% endfor %}
{% endblock %}
{% if manual_page %} {% if manual_page %}
<p class="manual-page"> <p class="manual-page">
<a href="{% url 'help:manual-page' manual_page %}"> <a href="{% url 'help:manual-page' manual_page %}">
@ -44,7 +50,7 @@
</p> </p>
{% endif %} {% 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"> <table class="table table-bordered table-condensed table-striped">
<thead> <thead>
@ -66,17 +72,17 @@
{% if disk.percent_used < 75 %} {% if disk.percent_used < 75 %}
<div class="progress-bar progress-bar-striped progress-bar-success" <div class="progress-bar progress-bar-striped progress-bar-success"
{% elif disk.percent_used < 90 %} {% 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 %} {% else %}
<div class="progress-bar progress-bar-striped progress-bar-danger" <div class="progress-bar progress-bar-striped progress-bar-danger"
{% endif %} {% endif %}
role="progressbar" aria-valuenow="disk.percent_used" role="progressbar" aria-valuenow="disk.percent_used"
aria-valuemin="0" aria-valuemax="100" aria-valuemin="0" aria-valuemax="100"
style="width: {{ disk.percent_used }}%;"> style="width: {{ disk.percent_used }}%;">
{{ disk.percent_used }}% {{ disk.percent_used }}%
</div> </div>
</div> </div>
<div>{{ disk.used_str }} / {{ disk.size_str }}</div> <div>{{ disk.used_str }} / {{ disk.size_str }}</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -100,4 +106,55 @@
</p> </p>
{% endif %} {% 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 %} {% endblock %}

View File

@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
""" """
URLs for the disks module. URLs for the disks module.
""" """
@ -23,8 +22,9 @@ from django.conf.urls import url
from . import views from . import views
urlpatterns = [ urlpatterns = [
url(r'^sys/storage/$', views.index, name='index'), url(r'^sys/storage/$', views.index, name='index'),
url(r'^sys/storage/expand$', views.expand, name='expand'), 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 logging
import urllib.parse
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from plinth.modules import storage from plinth.modules import storage
from plinth.utils import format_lazy, is_user_admin from plinth.utils import format_lazy, is_user_admin
from . import udisks2
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,7 +48,9 @@ def index(request):
return TemplateResponse( return TemplateResponse(
request, 'storage.html', { request, 'storage.html', {
'title': _('Storage'), 'title': _('Storage'),
'description': storage.description,
'disks': disks, 'disks': disks,
'devices': udisks2.list_devices(),
'manual_page': storage.manual_page, 'manual_page': storage.manual_page,
'expandable_root_size': expandable_root_size 'expandable_root_size': expandable_root_size
}) })
@ -73,9 +79,10 @@ def expand_partition(request, device):
try: try:
storage.expand_partition(device) storage.expand_partition(device)
except Exception as exception: except Exception as exception:
messages.error(request, messages.error(
_('Error expanding partition: {exception}') request,
.format(exception=exception)) _('Error expanding partition: {exception}')
.format(exception=exception))
else: else:
messages.success(request, _('Partition expanded successfully.')) messages.success(request, _('Partition expanded successfully.'))
@ -106,3 +113,37 @@ def warn_about_low_disk_space(request):
messages.error(request, message) messages.error(request, message)
elif percent_used > 75 or free_gib < 2: elif percent_used > 75 or free_gib < 2:
messages.warning(request, message) 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'))