Skip to content

Commit

Permalink
Support Time Machine export over Samba (Fixes rockstor#1910).
Browse files Browse the repository at this point in the history
  • Loading branch information
FroggyFlox committed Mar 8, 2020
1 parent f3f6cd8 commit 7157aa2
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 32 deletions.
1 change: 1 addition & 0 deletions conf/settings.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'
UDEVADM = subprocess.check_output(["which", "udevadm"]).rstrip()
SHUTDOWN = subprocess.check_output(["which", "shutdown"]).rstrip()
CHKCONFIG_BIN = subprocess.check_output(["which", "chkconfig"]).rstrip()
AVAHID_BIN = subprocess.check_output(["which", "avahi-daemon"]).rstrip()

# Establish our OS base id, name, and version:
# Use id for code path decisions. Others are for Web-UI display purposes.
Expand Down
1 change: 1 addition & 0 deletions src/rockstor/storageadmin/models/samba_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class SambaShare(models.Model):
guest_ok = models.CharField(max_length=3, choices=BOOLEAN_CHOICES,
default=NO)
shadow_copy = models.BooleanField(default=False)
time_machine = models.BooleanField(default=False)
snapshot_prefix = models.CharField(max_length=128, null=True)

def admin_users(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,24 @@
title="Comment string to associate with the new share">
</div>
</div>

<div class="form-group">
<div class="col-sm-4"></div>
<div class="col-sm-4">
<input type="checkbox" class="mutually-exclusive" name="time_machine"
{{#if smbShareTimeMachine}}
checked="true"
{{/if}}
id="time_machine"> Is this export for Time Machine? &nbsp;&nbsp;<a id="time-machine-info" href="#" class="moreinfo"><i class="fa fa-info-circle"></i></a>
</div>
</div> <!-- closing form group -->
<div class="form-group">
<div class="col-sm-4"></div>
<div class="col-sm-4">
<input type="checkbox" name="shadow_copy"
<input type="checkbox" class="mutually-exclusive" name="shadow_copy"
{{#if smbSnapshotPrefixRule}}
checked="true"
{{/if}}
id="shadow_copy">Enable Shadow Copy? &nbsp;&nbsp;<a id="shadow-copy-info" href="#" class="moreinfo"><i class="fa fa-info-circle"></i></a>
id="shadow_copy"> Enable Shadow Copy? &nbsp;&nbsp;<a id="shadow-copy-info" href="#" class="moreinfo"><i class="fa fa-info-circle"></i></a>
</div>
</div> <!-- closing form group -->
<div class="form-group"
Expand Down Expand Up @@ -144,3 +153,24 @@
</div>
</div>
</div>

<div id="time-machine-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 id="myModalLabel">Time Machine</h4>
</div>
<div class="modal-body">
<div class="messages"></div>
<p>
This feature is needed if the Share is to be used for Time Machine backup from Mac
OS X clients. By enabling this feature, you can browse older versions of files
from Mac OS X. You can read technical details on
<a href="https://en.wikipedia.org/wiki/Time_Machine_(macOS)" target="_blank">Wikipedia</a>
or <a href="https://support.apple.com/en-us/HT201250" target="_blank">Apple's documentation</a>.
</p>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ AddSambaExportView = RockstorLayoutView.extend({
events: {
'click #cancel': 'cancel',
'click #shadow-copy-info': 'shadowCopyInfo',
'click #shadow_copy': 'toggleSnapPrefix'
'click #time-machine-info': 'TimeMachineInfo',
'click #shadow_copy': 'toggleSnapPrefix',
'click .mutually-exclusive': 'disableBoxes'
},

initialize: function() {
Expand Down Expand Up @@ -95,12 +97,14 @@ AddSambaExportView = RockstorLayoutView.extend({
var configList = '',
smbShareName,
smbShadowCopy,
smbTimeMachine,
smbComment,
smbSnapPrefix = '';
if (this.sShares != null) {
var config = this.sShares.get('custom_config');
smbShareName = this.sShares.get('share');
smbShadowCopy = this.sShares.get('shadow_copy');
smbTimeMachine = this.sShares.get('time_machine');
smbComment = this.sShares.get('comment');
smbSnapPrefix = this.sShares.get('snapshot_prefix');

Expand All @@ -120,6 +124,7 @@ AddSambaExportView = RockstorLayoutView.extend({
smbShare: this.sShares,
smbShareName: smbShareName,
smbShareShadowCopy: smbShadowCopy,
smbShareTimeMachine: smbTimeMachine,
smbShareComment: smbComment,
smbShareSnapPrefix: smbSnapPrefix,
smbSnapshotPrefixRule: smbSnapshotPrefixBool,
Expand Down Expand Up @@ -208,6 +213,16 @@ AddSambaExportView = RockstorLayoutView.extend({
$('#shadow-copy-info-modal').modal('show');
},

TimeMachineInfo: function(event) {
event.preventDefault();
$('#time-machine-info-modal').modal({
keyboard: false,
show: false,
backdrop: 'static'
});
$('#time-machine-info-modal').modal('show');
},

toggleSnapPrefix: function() {
var cbox = this.$('#shadow_copy');
if (cbox.prop('checked')) {
Expand All @@ -217,6 +232,16 @@ AddSambaExportView = RockstorLayoutView.extend({
}
},

disableBoxes: function() {
if (this.$('#shadow_copy').attr('checked')) {
this.$('#time_machine').attr('disabled', true);
} else if (this.$('#time_machine').attr('checked')) {
this.$('#shadow_copy').attr('disabled', true);
} else {
this.$('.mutually-exclusive').attr('disabled', false);
}
},

initHandlebarHelpers: function(){
Handlebars.registerHelper('display_adminUser_options', function(){
var html = '';
Expand Down
10 changes: 6 additions & 4 deletions src/rockstor/storageadmin/tests/test_samba.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import mock
from mock import patch
from rest_framework import status
from rest_framework.test import APITestCase
from mock import patch

from storageadmin.exceptions import RockStorAPIException
from storageadmin.models import Pool, Share, SambaCustomConfig, SambaShare, User
from storageadmin.models import Pool, Share, SambaShare, User
from storageadmin.tests.test_api import APITestMixin

from storageadmin.views.samba import SambaListView, logger
from storageadmin.views.samba import SambaListView


class SambaTests(APITestMixin, APITestCase, SambaListView):
Expand Down Expand Up @@ -92,12 +91,14 @@ def test_validate_input(self):
"snapshot_prefix": "",
"shares": ["9", "10"],
"shadow_copy": False,
"time_machine": True,
"guest_ok": "no",
}
expected_result = {
"comment": "Samba-Export",
"read_only": "no",
"browsable": "yes",
"time_machine": True,
"custom_config": [],
"guest_ok": "no",
"shadow_copy": False,
Expand All @@ -117,6 +118,7 @@ def test_validate_input(self):
"comment": "samba export",
"read_only": "no",
"browsable": "yes",
"time_machine": False,
"custom_config": [],
"guest_ok": "no",
"shadow_copy": False,
Expand Down
29 changes: 20 additions & 9 deletions src/rockstor/storageadmin/views/samba.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,21 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

import logging
import pwd
from rest_framework.response import Response
from rest_framework.exceptions import NotFound
from django.db import transaction

from django.conf import settings
from django.db import transaction
from rest_framework.exceptions import NotFound
from rest_framework.response import Response

import rest_framework_custom as rfc
from fs.btrfs import mount_share
from share import ShareMixin
from storageadmin.models import SambaShare, User, SambaCustomConfig
from storageadmin.serializers import SambaShareSerializer
from storageadmin.util import handle_exception
import rest_framework_custom as rfc
from share import ShareMixin
from system.samba import refresh_smb_config, status, restart_samba
from fs.btrfs import mount_share

import logging
from system.samba import refresh_smb_config, status, restart_samba, refresh_smb_discovery

logger = logging.getLogger(__name__)

Expand All @@ -44,6 +45,7 @@ class SambaMixin(object):
"custom_config": None,
"shadow_copy": False,
"snapshot_prefix": None,
"time_machine": False,
}
BOOL_OPTS = ("yes", "no")

Expand All @@ -64,6 +66,7 @@ def _validate_input(cls, rdata, smbo=None):
def_opts["guest_ok"] = smbo.guest_ok
def_opts["read_only"] = smbo.read_only
def_opts["shadow_copy"] = smbo.shadow_copy
def_opts["time_machine"] = smbo.time_machine

options["comment"] = rdata.get("comment", def_opts["comment"])
options["browsable"] = rdata.get("browsable", def_opts["browsable"])
Expand Down Expand Up @@ -97,6 +100,11 @@ def _validate_input(cls, rdata, smbo=None):
"valid non-empty string."
)
handle_exception(Exception(e_msg), rdata)
options["time_machine"] = rdata.get("time_machine", def_opts["time_machine"])
logger.debug("time_machine is = {}".format(options["time_machine"]))
if not isinstance(options["time_machine"], bool):
e_msg = "Invalid choice for time_machine. Possible options are True or False."
handle_exception(Exception(e_msg), rdata)

return options

Expand Down Expand Up @@ -136,6 +144,7 @@ def post(self, request):
else:
smb_share = self.create_samba_share(request.data)
refresh_smb_config(list(SambaShare.objects.all()))
refresh_smb_discovery(list(SambaShare.objects.all()))
self._restart_samba()
return Response(SambaShareSerializer(smb_share).data)

Expand Down Expand Up @@ -196,6 +205,7 @@ def delete(self, request, smb_id):

with self._handle_exception(request):
refresh_smb_config(list(SambaShare.objects.all()))
refresh_smb_discovery(list(SambaShare.objects.all()))
self._restart_samba()
return Response()

Expand Down Expand Up @@ -245,5 +255,6 @@ def put(self, request, smb_id):
handle_exception(Exception(e_msg), request)

refresh_smb_config(list(SambaShare.objects.all()))
refresh_smb_discovery(list(SambaShare.objects.all()))
self._restart_samba()
return Response(SambaShareSerializer(smbo).data)
60 changes: 48 additions & 12 deletions src/rockstor/system/samba.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,21 @@
from django.conf import settings

from osi import run_command
from services import service_status
from services import service_status, define_avahi_service
from storageadmin.models import SambaCustomConfig

TESTPARM = "/usr/bin/testparm"
SMB_CONFIG = "/etc/samba/smb.conf"
SYSTEMCTL = "/usr/bin/systemctl"
CHMOD = "/usr/bin/chmod"
RS_SHARES_HEADER = "####BEGIN: Rockstor SAMBA CONFIG####"
RS_SHARES_FOOTER = "####END: Rockstor SAMBA CONFIG####"
RS_AD_HEADER = "####BEGIN: Rockstor ACTIVE DIRECTORY CONFIG####"
RS_AD_FOOTER = "####END: Rockstor ACTIVE DIRECTORY CONFIG####"
RS_CUSTOM_HEADER = "####BEGIN: Rockstor SAMBA GLOBAL CUSTOM####"
RS_CUSTOM_FOOTER = "####END: Rockstor SAMBA GLOBAL CUSTOM####"

TESTPARM = '/usr/bin/testparm'
AVAHID = settings.AVAHID_BIN
SMB_CONFIG = '/etc/samba/smb.conf'
TM_CONFIG = '/etc/avahi/services/timemachine.service'
SYSTEMCTL = '/usr/bin/systemctl'
CHMOD = '/usr/bin/chmod'
RS_SHARES_HEADER = '####BEGIN: Rockstor SAMBA CONFIG####'
RS_SHARES_FOOTER = '####END: Rockstor SAMBA CONFIG####'
RS_AD_HEADER = '####BEGIN: Rockstor ACTIVE DIRECTORY CONFIG####'
RS_AD_FOOTER = '####END: Rockstor ACTIVE DIRECTORY CONFIG####'
RS_CUSTOM_HEADER = '####BEGIN: Rockstor SAMBA GLOBAL CUSTOM####'
RS_CUSTOM_FOOTER = '####END: Rockstor SAMBA GLOBAL CUSTOM####'

def test_parm(config="/etc/samba/smb.conf"):
cmd = [TESTPARM, "-s", config]
Expand Down Expand Up @@ -76,6 +77,17 @@ def rockstor_smb_config(fo, exports):
fo.write(" shadow:localtime = yes\n")
fo.write(" vfs objects = shadow_copy2\n")
fo.write(" veto files = /.%s*/\n" % e.snapshot_prefix)
elif (e.time_machine):
fo.write(' vfs objects = catia fruit streams_xattr\n')
fo.write(' fruit:timemachine = yes\n')
fo.write(' fruit:metadata = stream\n')
fo.write(' fruit:veto_appledouble = no\n')
fo.write(' fruit:posix_rename = no\n')
fo.write(' fruit:wipe_intentionally_left_blank_rfork = yes\n')
fo.write(' fruit:delete_empty_adfiles = yes\n')
fo.write(' fruit:encoding = private\n')
fo.write(' fruit:locking = none\n')
fo.write(' fruit:resource = file\n')
for cco in SambaCustomConfig.objects.filter(smb_share=e):
if cco.custom_config.strip():
fo.write(" %s\n" % cco.custom_config)
Expand Down Expand Up @@ -243,6 +255,30 @@ def restart_samba(hard=False):
return run_command([SYSTEMCTL, mode, "nmb"], log=True)


def refresh_smb_discovery(exports):
"""
This function is designed to identify the list of shares
(if any), that need to be advertised through avahi. These
will correspond to all Time Machine-enabled shares. It
then sends them to be included in the timemachine.service
avahi file.
:param exports:
:return:
"""
# Get names of SambaShares with time_machine enabled
tm_exports = [e.share.name for e in exports if e.time_machine]

# Clean existing one if exists
if os.path.isfile(TM_CONFIG):
os.remove(TM_CONFIG)

if len(tm_exports) > 0:
define_avahi_service("timemachine", share_names=tm_exports)

# Reload avahi config / or restart it
run_command([AVAHID, '--reload', ], log=True)


def update_samba_discovery():
avahi_smb_config = "/etc/avahi/services/smb.service"
if os.path.isfile(avahi_smb_config):
Expand Down
Loading

0 comments on commit 7157aa2

Please sign in to comment.