mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
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:
parent
f172925d9d
commit
a307476634
@ -1 +0,0 @@
|
||||
plinth.modules.udiskie
|
||||
@ -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']
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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')
|
||||
]
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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'])
|
||||
@ -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 %}
|
||||
@ -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'),
|
||||
]
|
||||
@ -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'))
|
||||
Loading…
x
Reference in New Issue
Block a user