Skip to content

Commit

Permalink
improve quotas not enabled behaviour #1869
Browse files Browse the repository at this point in the history
Uses the existing -1/-1 default for pqgoups to represent
an unset state for the share.pqgroup and adds an active
return to this value whenever quotas are deemed to be
disabled. Pqgroup setup is moved from the bootstrap
process into the import_shares / shares refresh section.
This allows for live setting / resetting of pqgroups via
the regular share re-fresh process. Pool.quotas_enabled
and share.pqgroup_exist properties are added and all used
low level quota actions are adjusted to catch, log, and
ignore a quota disabled state.
Additionally active pqgroup assignment is added to share
resize and share refresh procedures which aid in returning
to existence the expected native 2015/n pqgroups and their
relationship to the auto generated 0/n qgroups as whenever
quotas are disabled all this info (var the 0/n) is lost.
UI elements are added in pool and share focused pages to
indicate a live status for the associated pool quotas
enabled status.
  • Loading branch information
phillxnet committed Dec 18, 2017
1 parent 980cddd commit 5dcc2fa
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 23 deletions.
165 changes: 158 additions & 7 deletions src/rockstor/fs/btrfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from system.exceptions import (CommandException)
from pool_scrub import PoolScrub
from django_ztask.decorators import task
from django.conf import settings
import logging

"""
Expand All @@ -41,7 +42,8 @@
DEFAULT_MNT_DIR = '/mnt2/'
RMDIR = '/bin/rmdir'
QID = '2015'

# The following model/db default setting is also used when quotas are disabled.
PQGROUP_DEFAULT = settings.MODEL_DEFS['pqgroup']

def add_pool(pool, disks):
"""
Expand Down Expand Up @@ -678,13 +680,71 @@ def disable_quota(pool_name):
return switch_quota(pool_name, flag='disable')


def are_quotas_enabled(mnt_pt):
"""
Simple wrapper around 'btrfs qgroup show -f --raw mnt_pt' intended
as a fast determiner of True / False status of quotas enabled
:param mnt_pt: Mount point of btrfs filesystem
:return: True on rc = 0 False otherwise.
"""
o, e, rc = run_command([BTRFS, 'qgroup', 'show', '-f', '--raw', mnt_pt])
if rc == 0:
return True
return False


def qgroup_exists(mnt_pt, qgroup):
"""
Simple wrapper around 'btrfs qgroup show --raw mnt_pt' intended to
establish if a specific qgroup exists on a btrfs filesystem.
:param mnt_pt: btrfs filesystem mount point, usually the pool.
:param qgroup: qgroup of the form 2015/n (intended for use with pqgroup)
:return: True is given qgroup exists in command output, False otherwise.
"""
o, e, rc = run_command([BTRFS, 'qgroup', 'show', '--raw', mnt_pt])
# example output:
# 'qgroupid rfer excl '
# '------- ---- ---- '
# '0/5 16384 16384 '
# ...
# '2015/12 0 0 '
if rc == 0 and len(o) > 2:
# index from 2 to miss header lines and -1 to skip end blank line = []
qgroup_list = [line.split()[0] for line in o[2:-1]]
# eg from rockstor_rockstor pool we get:
# qgroup_list=['0/5', '0/257', '0/258', '0/260', '2015/1', '2015/2']
if qgroup in qgroup_list:
return True
return False


def qgroup_id(pool, share_name):
sid = share_id(pool, share_name)
return '0/' + sid


def qgroup_max(mnt_pt):
o, e, rc = run_command([BTRFS, 'qgroup', 'show', mnt_pt], log=True)
"""
Parses the output of "btrfs qgroup show mnt_pt" to find the highest qgroup
matching QID/* if non is found then 0 will be returned.
Quotas not enabled is flagged by a -1 return value.
:param mnt_pt: A given btrfs mount point.
:return: -1 if quotas not enabled, else highest 2015/* qgroup found or 0
"""
try:
o, e, rc = run_command([BTRFS, 'qgroup', 'show', mnt_pt], log=True)
except CommandException as e:
# disabled quotas will result in o = [''], rc = 1 and e[0] =
emsg = "ERROR: can't list qgroups: quotas not enabled"
# this is non fatal so we catch this specific error and info log it.
if e.rc == 1 and e.err[0] == emsg:
logger.info('Mount Point: {} has Quotas disabled, skipping qgroup '
'show.'.format(mnt_pt))
# and return our default res
return -1
# otherwise we raise an exception as normal.
raise
# if no exception was raised find the max 2015/qgroup
res = 0
for l in o:
if (re.match('%s/' % QID, l) is not None):
Expand All @@ -694,10 +754,33 @@ def qgroup_max(mnt_pt):
return res


def qgroup_create(pool):
def qgroup_create(pool, qgroup='-1/-1'):
"""
When passed only a pool an attempt will be made to ascertain if quotas is
enabled, if not '-1/-1' is returned as a flag to indicate this state.
If quotas are not enabled then the highest available quota of the form
2015/n is selected and created, if possible.
If passed both a pool and a specific qgroup an attempt is made, given the
same behaviour as above, to create this specific group: this scenario is
primarily used to re-establish prior existing qgroups post quota disable,
share manipulation, quota enable cycling.
:param pool: A pool object.
:param qgroup: native qgroup of the form 2015/n
:return: -1/-1 on quotas disabled, otherwise it will return the native
quota whose creation was attempt.
"""
# mount pool
mnt_pt = mount_root(pool)
qid = ('%s/%d' % (QID, qgroup_max(mnt_pt) + 1))
max_native_qgroup = qgroup_max(mnt_pt)
if max_native_qgroup == -1:
# We have received a quotas disabled flag so will be unable to create
# a new quota group. So return our db default which can in turn flag
# an auto updated of pqgroup upon next refresh-share-state.
return PQGROUP_DEFAULT
if qgroup != PQGROUP_DEFAULT:
qid = qgroup
else:
qid = ('%s/%d' % (QID, max_native_qgroup + 1))
try:
out, err, rc = run_command([BTRFS, 'qgroup', 'create', qid, mnt_pt],
log=True)
Expand All @@ -715,7 +798,18 @@ def qgroup_create(pool):


def qgroup_destroy(qid, mnt_pt):
o, e, rc = run_command([BTRFS, 'qgroup', 'show', mnt_pt])
cmd = [BTRFS, 'qgroup', 'show', mnt_pt]
try:
o, e, rc = run_command(cmd, log=True)
except CommandException as e:
# we may have quotas disabled so catch and deal.
emsg = "ERROR: can't list qgroups: quotas not enabled"
if e.rc == 1 and e.err[0] == emsg:
# we have quotas disabled so can't destroy any anyway so skip
# and deal by returning False so our caller moves on.
return False
# otherwise we raise an exception as normal
raise e
for l in o:
if (re.match(qid, l) is not None and l.split()[0] == qid):
return run_command([BTRFS, 'qgroup', 'destroy', qid, mnt_pt],
Expand All @@ -726,7 +820,19 @@ def qgroup_destroy(qid, mnt_pt):
def qgroup_is_assigned(qid, pqid, mnt_pt):
# Returns true if the given qgroup qid is already assigned to pqid for the
# path(mnt_pt)
o, e, rc = run_command([BTRFS, 'qgroup', 'show', '-pc', mnt_pt])
cmd = [BTRFS, 'qgroup', 'show', '-pc', mnt_pt]
try:
o, e, rc = run_command(cmd, log=True)
except CommandException as e:
# we may have quotas disabled so catch and deal.
emsg = "ERROR: can't list qgroups: quotas not enabled"
if e.rc == 1 and e.err[0] == emsg:
# No deed to scan output as nothing to see with quotas disabled.
# And since no quota capability can be enacted we return True
# to avoid our caller trying any further with quotas.
return True
# otherwise we raise an exception as normal
raise e
for l in o:
fields = l.split()
if (len(fields) > 3 and
Expand All @@ -736,15 +842,34 @@ def qgroup_is_assigned(qid, pqid, mnt_pt):
return False


def share_pqgroup_assign(pqgroup, share):
"""
Convenience wrapper to qgroup_assign() for use with a share object where
we wish to assign / reassign it's current db held qgroup to a passed
pqgroup.
:param pqgroup: pqgroup to use as parent.
:param share: share object
:return: qgroup_assign() result.
"""
mnt_pt = '{}/{}'.format(settings.MNT_PT, share.pool.name)
return qgroup_assign(share.qgroup, pqgroup, mnt_pt)


def qgroup_assign(qid, pqid, mnt_pt):
"""
Wrapper for 'BTRFS, qgroup, assign, qid, pqid, mnt_pt'
:param qid: qgroup to assign as child of pqgroup
:param pqid: pqgroup to use as parent
:param mnt_pt: btrfs filesystem mountpoint (usually the associated pool)
"""
if (qgroup_is_assigned(qid, pqid, mnt_pt)):
return True

# since btrfs-progs 4.2, qgroup assign succeeds but throws a warning:
# "WARNING: # quotas may be inconsistent, rescan needed" and returns with
# exit code 1.
try:
run_command([BTRFS, 'qgroup', 'assign', qid, pqid, mnt_pt])
run_command([BTRFS, 'qgroup', 'assign', qid, pqid, mnt_pt], log=True)
except CommandException as e:
wmsg = 'WARNING: quotas may be inconsistent, rescan needed'
if (e.rc == 1 and e.err[0] == wmsg):
Expand All @@ -766,14 +891,21 @@ def qgroup_assign(qid, pqid, mnt_pt):


def update_quota(pool, qgroup, size_bytes):
# TODO: consider changing qgroup to pqgroup if we are only used this way.
root_pool_mnt = mount_root(pool)
# Until btrfs adds better support for qgroup limits. We'll not set limits.
# It looks like we'll see the fixes in 4.2 and final ones by 4.3.
# Update: Further quota improvements look to be landing in 4.15.
# cmd = [BTRFS, 'qgroup', 'limit', str(size_bytes), qgroup, root_pool_mnt]
cmd = [BTRFS, 'qgroup', 'limit', 'none', qgroup, root_pool_mnt]
# Set defaults in case our run_command fails to assign them.
out = err = ['']
rc = 0
if qgroup == '-1/-1':
# We have a 'quotas disabled' qgroup value flag, log and return blank.
logger.info('Pool: {} ignoring '
'update_quota on {}'.format(pool.name, qgroup))
return out, err, rc
try:
out, err, rc = run_command(cmd, log=True)
except CommandException as e:
Expand All @@ -785,6 +917,25 @@ def update_quota(pool, qgroup, size_bytes):
logger.info('Pool: {} is Read-only, skipping qgroup '
'limit.'.format(pool.name))
return out, err, rc
# quotas disabled results in o = [''], rc = 1 and e[0] =
emsg2 = 'ERROR: unable to limit requested quota group: ' \
'Invalid argument'
# quotas disabled is not a fatal failure but here we key from what
# is a non specific error: 'Invalid argument'.
# TODO: improve this clause as currently too broad.
# TODO: we could for example use if qgroup_max(mnt) == -1
if e.rc == 1 and e.err[0] == emsg2:
logger.info('Pool: {} has encountered a qgroup limit issue, '
'skipping qgroup limit. Disabled quotas can cause '
'this error'.format(pool.name))
return out, err, rc
emsg3 = 'ERROR: unable to limit requested quota group: ' \
'No such file or directory'
if e.rc == 1 and e.err[0] == emsg3:
logger.info('Pool: {} is missing expected '
'qgroup {}'.format(pool.name, qgroup))
logger.info('Previously disabled quotas can cause this issue')
return out, err, rc
# raise an exception as usual otherwise
raise
return out, err, rc
Expand Down
11 changes: 10 additions & 1 deletion src/rockstor/storageadmin/models/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

from django.db import models
from django.conf import settings
from fs.btrfs import pool_usage, usage_bound
from fs.btrfs import pool_usage, usage_bound, \
are_quotas_enabled
from system.osi import mount_status

RETURN_BOOLEAN = True
Expand Down Expand Up @@ -74,5 +75,13 @@ def is_mounted(self, *args, **kwargs):
except:
return False

@property
def quotas_enabled(self, *args, **kwargs):
# Calls are_quotas_enabled for boolean response
try:
return are_quotas_enabled('%s%s' % (settings.MNT_PT, self.name))
except:
return False

class Meta:
app_label = 'storageadmin'
14 changes: 14 additions & 0 deletions src/rockstor/storageadmin/models/share.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from django.db.models.signals import (post_save, post_delete)
from django.dispatch import receiver

from fs.btrfs import qgroup_exists
from storageadmin.models import Pool
from system.osi import mount_status
from .netatalk_share import NetatalkShare
Expand Down Expand Up @@ -81,6 +82,19 @@ def is_mounted(self, *args, **kwargs):
except:
return False

@property
def pqgroup_exist(self, *args, **kwargs):
# Returns boolean status of pqgroup existence
try:
if str(self.pqgroup) == '-1/-1':
return False
else:
return qgroup_exists(
'%s%s' % (settings.MNT_PT, self.pool.name),
'%s' % self.pqgroup)
except:
return False

class Meta:
app_label = 'storageadmin'

Expand Down
2 changes: 2 additions & 0 deletions src/rockstor/storageadmin/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class PoolInfoSerializer(serializers.ModelSerializer):
reclaimable = serializers.IntegerField()
mount_status = serializers.CharField()
is_mounted = serializers.BooleanField()
quotas_enabled = serializers.BooleanField()

class Meta:
model = Pool
Expand Down Expand Up @@ -145,6 +146,7 @@ class ShareSerializer(serializers.ModelSerializer):
nfs_exports = NFSExportSerializer(many=True, source='nfsexport_set')
mount_status = serializers.CharField()
is_mounted = serializers.BooleanField()
pqgroup_exist = serializers.BooleanField()

class Meta:
model = Share
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
{{else}}
<span style="color:red">{{model.mount_status}}</span>
{{/if}}
</strong><br/>
Quotas: <strong>
{{#if model.quotas_enabled}}
Enabled
{{else}}
<span style="color:red">Disabled</span>
{{/if}}
</strong>
</div> <!-- module-content -->

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<th>Name</th>
<th>Size</th>
<th>Usage</th>
<th>Quotas</th>
<th>Raid</th>
<th>Active mount options / Status</th>
<th>Compression</th>
Expand Down Expand Up @@ -32,7 +33,12 @@
<td>{{humanReadableSize 'usage' this.size this.reclaimable this.free}}
<strong>({{humanReadableSize 'usagePercent' this.size this.reclaimable this.free}} %)</strong>
</td>

<td>{{#if this.quotas_enabled}}
Enabled
{{else}}
<strong><span style="color:red">Disabled</span></strong>
{{/if}}
</td>
<td>{{this.raid}}
{{#unless (isRoot this.role)}}
&nbsp;<a href="#pools/{{this.id}}/?cView=resize"><i class="fa fa-pencil-square-o"></i></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
(<strong><span style="color:red">{{pool_mount_status}}</span></strong>)
{{/if}}
<br>
Pool Quotas:&nbsp;
{{#if pool_quotas_enabled}}
Enabled
{{else}}
<strong><span style="color:red">Disabled</span></strong>
{{/if}}
<br>
Active mount options / Status:
<strong>
{{#if share_is_mounted}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
<tr>
<th>Name</th>
<th>Size</th>
<th>Usage <i class="fa fa-info-circle" title="Current share content"></th>
<th>Btrfs Usage <i class="fa fa-info-circle" title="Current share content including snapshots"></th>
<th>Usage <i class="fa fa-info-circle" title="Share content - uses Quotas" /></th>
<th>Btrfs Usage <i class="fa fa-info-circle" title="Share content inc snapshots - uses Quotas" /></th>
<th>Active mount options / Status</th>
<th>Pool (Active mount options / Status)</th>
<th>Compression <i class="fa fa-info-circle" title="Inherits pool setting if not specified on share"></th>
<th>Pool (Active mount options / Status) Quotas</th>
<th>Compression <i class="fa fa-info-circle" title="Inherits pool setting if not specified on share" /></th>
<th>Actions</th>
</tr>
</thead>
Expand All @@ -40,6 +40,11 @@
{{else}}
(<strong><span style="color:red">{{this.pool.mount_status}}</span></strong>)
{{/if}}
{{# if this.pool.quotas_enabled}}
Enabled
{{else}}
<strong><span style="color:red">Disabled</span></strong>
{{/if}}
</td>
<td>
{{displayCompressionAlgo this.compression_algo this.id}}
Expand Down
Loading

0 comments on commit 5dcc2fa

Please sign in to comment.