bundles/zfs: import bundle from work repository
This commit is contained in:
parent
b690ae25b0
commit
4934eb46fb
11 changed files with 841 additions and 0 deletions
33
bundles/zfs/files/check_zfs_auto_snapshot
Normal file
33
bundles/zfs/files/check_zfs_auto_snapshot
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
monitoring=/var/tmp/zfs-auto-snapshot.status
|
||||||
|
crit_days=1
|
||||||
|
|
||||||
|
uptime=$(cut -d. -f1 /proc/uptime)
|
||||||
|
if [ "$uptime" -lt 3600 ]
|
||||||
|
then
|
||||||
|
echo 'OK - The system has just booted'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
now=$(date +%s)
|
||||||
|
timestamp=$(cat "$monitoring")
|
||||||
|
|
||||||
|
if [ -z "$timestamp" ]
|
||||||
|
then
|
||||||
|
echo 'UNKNOWN - No status info found'
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$timestamp" = 0 ]
|
||||||
|
then
|
||||||
|
echo 'OK - Snapshots disabled'
|
||||||
|
exit 0
|
||||||
|
elif [ $(( now - timestamp )) -gt $(( 60 * 60 * 24 * crit_days )) ]
|
||||||
|
then
|
||||||
|
echo "CRITICAL - Status file indicates age greater than $crit_days day(s)"
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
echo 'OK'
|
||||||
|
exit 0
|
||||||
|
fi
|
63
bundles/zfs/files/check_zfs_old_snapshots
Normal file
63
bundles/zfs/files/check_zfs_old_snapshots
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from re import match
|
||||||
|
from subprocess import check_output
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
output = check_output(['zfs', 'get', 'creation', '-Hpr', '-t', 'snapshot'])
|
||||||
|
|
||||||
|
now = int(datetime.now().timestamp())
|
||||||
|
warn_age = now - (60 * 60 * 24 * 60)
|
||||||
|
crit_age = now - (60 * 60 * 24 * 90)
|
||||||
|
|
||||||
|
warn_snapshots = set()
|
||||||
|
crit_snapshots = set()
|
||||||
|
|
||||||
|
return_code = 0
|
||||||
|
|
||||||
|
for line in output.decode('utf-8').split("\n"):
|
||||||
|
if line.strip() == '':
|
||||||
|
continue
|
||||||
|
|
||||||
|
items = line.split("\t")
|
||||||
|
|
||||||
|
# If the snapshot name contains 'zfs-auto-snap', it's probably
|
||||||
|
# really an automated snapshot and will be cleaned up eventually.
|
||||||
|
# This check only cares about manually created snapshots, though.
|
||||||
|
if 'zfs-auto-snap' in items[0]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# These are docker-internal snapshots and should not be touched by
|
||||||
|
# us.
|
||||||
|
if match(r'^tank/docker/[a-z0-9]+(-init)?@[0-9]+', items[0]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# line should be '<snapshot> creation <timestamp> -', separated by
|
||||||
|
# tabstops.
|
||||||
|
if len(items) < 3:
|
||||||
|
print('UNKNOWN - error while parsing ' + line)
|
||||||
|
exit(3)
|
||||||
|
|
||||||
|
creation_date = int(items[2])
|
||||||
|
|
||||||
|
if creation_date < crit_age:
|
||||||
|
crit_snapshots.add(items[0])
|
||||||
|
elif creation_date < warn_age:
|
||||||
|
warn_snapshots.add(items[0])
|
||||||
|
|
||||||
|
# We have to do additional loops in here to have CRITICAL items on top.
|
||||||
|
for snap in sorted(crit_snapshots):
|
||||||
|
print('CRITICAL - {} is older than 90 days'.format(snap))
|
||||||
|
|
||||||
|
for snap in sorted(warn_snapshots):
|
||||||
|
print('WARN - {} is older than 60 days'.format(snap))
|
||||||
|
|
||||||
|
if len(crit_snapshots) > 0:
|
||||||
|
return_code = 2
|
||||||
|
elif len(warn_snapshots) > 0:
|
||||||
|
return_code = 1
|
||||||
|
else:
|
||||||
|
print('OK - no snapshots are older than 60 days')
|
||||||
|
|
||||||
|
exit(return_code)
|
26
bundles/zfs/files/check_zfs_volumes
Normal file
26
bundles/zfs/files/check_zfs_volumes
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Written by jpaul, 2017-03
|
||||||
|
# Extended by El Pinguino, 2017-07
|
||||||
|
|
||||||
|
<%
|
||||||
|
datasetlist = sorted(node.metadata.get('zfs', {}).get('datasets', {}).items())
|
||||||
|
volumes = []
|
||||||
|
for dataset, options in datasetlist:
|
||||||
|
if options.get('mountpoint', 'none') != 'none':
|
||||||
|
volumes.append(dataset)
|
||||||
|
%>\
|
||||||
|
|
||||||
|
exitcode=0
|
||||||
|
% for volume in volumes:
|
||||||
|
if [[ "$(zfs get -Hp -o value mounted '${volume}')" != "yes" ]]
|
||||||
|
then
|
||||||
|
echo 'CRITICAL - ${volume} not mounted'
|
||||||
|
exitcode=2
|
||||||
|
fi
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
if (( exitcode == 0 ))
|
||||||
|
then
|
||||||
|
echo OK
|
||||||
|
fi
|
||||||
|
exit $exitcode
|
15
bundles/zfs/files/check_zpool_online
Normal file
15
bundles/zfs/files/check_zpool_online
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ $# -eq 0 ] ; then
|
||||||
|
echo "Please provide pool name as first argument, e.g. 'tank'."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(zpool status "$1" | grep '^ state:')" = ' state: ONLINE' ]
|
||||||
|
then
|
||||||
|
echo "OK - Pool '$1' is online"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "CRITICAL - Pool '$1' is FAULTY or NOT ONLINE"
|
||||||
|
exit 2
|
||||||
|
fi
|
40
bundles/zfs/files/check_zpool_space
Normal file
40
bundles/zfs/files/check_zpool_space
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
|
from subprocess import check_output
|
||||||
|
from sys import argv, exit
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def to_bytes(size):
|
||||||
|
suffixes = ['', 'K', 'M', 'G', 'T', 'P']
|
||||||
|
number, suffix = re.match(r'([0-9\.]+)([A-Z]?)', size).groups()
|
||||||
|
assert suffix in suffixes, 'Unexpected suffix "{}" in size "{}"'.format(suffix, size)
|
||||||
|
return float(number) * 1024**suffixes.index(suffix)
|
||||||
|
|
||||||
|
|
||||||
|
pool = argv[1]
|
||||||
|
critical_perc = float(argv[2])
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = check_output(['zpool', 'list', '-Ho', 'size,alloc', pool])
|
||||||
|
except:
|
||||||
|
print('CRITICAL - "zpool" failed')
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
size, alloc = output.decode('UTF-8').strip().split()
|
||||||
|
|
||||||
|
try:
|
||||||
|
size_b = to_bytes(size)
|
||||||
|
alloc_b = to_bytes(alloc)
|
||||||
|
except:
|
||||||
|
print('CRITICAL - Could not process output of "zpool list": {}'.format(output))
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
percentage = alloc_b / size_b * 100
|
||||||
|
if percentage > critical_perc:
|
||||||
|
print('CRITICAL - Pool "{}" uses {:.2f}% of its space'.format(pool, percentage))
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
print('OK - Pool "{}" uses {:.2f}% of its space'.format(pool, percentage))
|
||||||
|
exit(0)
|
57
bundles/zfs/files/zfs-auto-snapshot
Normal file
57
bundles/zfs/files/zfs-auto-snapshot
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from json import loads
|
||||||
|
from subprocess import check_call, check_output
|
||||||
|
from sys import argv
|
||||||
|
|
||||||
|
|
||||||
|
def create_snap_and_rotate(ds, label, retain, now, all_snapshots):
|
||||||
|
new_snap = '{}@zfs-auto-snap_{}-{}'.format(ds, label, now)
|
||||||
|
check_call(['zfs', 'snapshot', new_snap])
|
||||||
|
|
||||||
|
prefix = '{}@zfs-auto-snap_{}-'.format(ds, label)
|
||||||
|
my_candidates = []
|
||||||
|
for i in sorted(all_snapshots):
|
||||||
|
if i.startswith(prefix):
|
||||||
|
my_candidates.append(i)
|
||||||
|
|
||||||
|
my_candidates.append(new_snap)
|
||||||
|
|
||||||
|
for i in my_candidates[:-retain]:
|
||||||
|
assert '@' in i, 'BUG! Dataset "{}" has no @!'.format(i)
|
||||||
|
check_call(['zfs', 'destroy', i])
|
||||||
|
|
||||||
|
|
||||||
|
label = argv[1]
|
||||||
|
|
||||||
|
with open('/etc/zfs-snapshot-config.json', 'r') as fp:
|
||||||
|
metadata = loads(fp.read())
|
||||||
|
|
||||||
|
if 'snapshot_only' in metadata:
|
||||||
|
datasets = set(metadata['snapshot_only'])
|
||||||
|
else:
|
||||||
|
output = check_output(['zfs', 'list', '-H', '-o', 'name']).decode('UTF-8')
|
||||||
|
datasets = set(output.splitlines())
|
||||||
|
|
||||||
|
for pattern in metadata.get('snapshot_never', set()):
|
||||||
|
datasets = set(filter(lambda x: not re.search(pattern, x), datasets))
|
||||||
|
|
||||||
|
default_retain = metadata['retain_defaults'][label]
|
||||||
|
now = datetime.now().strftime('%F-%H%M')
|
||||||
|
snapshots_created = False
|
||||||
|
|
||||||
|
if datasets:
|
||||||
|
all_snapshots = check_output(['zfs', 'list', '-H', '-o', 'name', '-t', 'snap']).decode('UTF-8').splitlines()
|
||||||
|
|
||||||
|
for ds in datasets:
|
||||||
|
retain = int(metadata.get('retain_per_dataset', {}).get(ds, {}).get(label, default_retain))
|
||||||
|
if retain > 0:
|
||||||
|
create_snap_and_rotate(ds, label, retain, now, all_snapshots)
|
||||||
|
snapshots_created = True
|
||||||
|
|
||||||
|
with open('/var/tmp/zfs-auto-snapshot.status', 'w') as fp:
|
||||||
|
fp.write('{}\n'.format(datetime.now().strftime('%s') if snapshots_created else 0))
|
6
bundles/zfs/files/zfs-modprobe.conf
Normal file
6
bundles/zfs/files/zfs-modprobe.conf
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<%
|
||||||
|
arc_max_mb = node.metadata.get('zfs', {}).get('module_options', {}).get('zfs_arc_max_mb', 1024)
|
||||||
|
%>\
|
||||||
|
% if arc_max_mb != 0:
|
||||||
|
options zfs zfs_arc_max=${arc_max_mb * 1024 * 1024}
|
||||||
|
% endif
|
141
bundles/zfs/items.py
Normal file
141
bundles/zfs/items.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
from json import dumps
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
|
from bundlewrap.metadata import MetadataJSONEncoder
|
||||||
|
|
||||||
|
actions = {}
|
||||||
|
|
||||||
|
pkg_apt = {
|
||||||
|
'zfs-zed': {
|
||||||
|
'needed_by': {
|
||||||
|
'zfs_dataset:',
|
||||||
|
'zfs_pool:',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'zfsutils-linux': {
|
||||||
|
'needed_by': {
|
||||||
|
'zfs_dataset:',
|
||||||
|
'zfs_pool:',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'parted': {
|
||||||
|
'needed_by': {
|
||||||
|
'zfs_pool:',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'/etc/cron.d/zfsutils-linux': {
|
||||||
|
'delete': True,
|
||||||
|
'needs': ['pkg_apt:zfsutils-linux'],
|
||||||
|
},
|
||||||
|
"/etc/modprobe.d/zfs.conf": {
|
||||||
|
'source': 'zfs-modprobe.conf',
|
||||||
|
'content_type': 'mako',
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
'/etc/zfs-snapshot-config.json': {
|
||||||
|
'content': dumps(
|
||||||
|
node.metadata.get('zfs', {}).get('snapshots', {}),
|
||||||
|
cls=MetadataJSONEncoder, # turns sets into sorted lists
|
||||||
|
indent=4,
|
||||||
|
sort_keys=True,
|
||||||
|
) + '\n',
|
||||||
|
},
|
||||||
|
'/etc/zfs/zed.d/zed.rc': {
|
||||||
|
'content': 'ZED_EMAIL_ADDR="hostmaster@kunbox.net"\nZED_EMAIL_PROG="mail"\nZED_NOTIFY_INTERVAL_SECS=3600\n',
|
||||||
|
'mode': '0600',
|
||||||
|
'triggers': ['svc_systemd:zed:restart'],
|
||||||
|
},
|
||||||
|
'/usr/local/sbin/zfs-auto-snapshot': {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
'/usr/lib/nagios/plugins/check_zfs_auto_snapshot': {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
'/usr/lib/nagios/plugins/check_zfs_old_snapshots': {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
"/usr/lib/nagios/plugins/check_zfs_volumes": {
|
||||||
|
'mode': '0755',
|
||||||
|
'content_type': 'mako',
|
||||||
|
},
|
||||||
|
"/usr/lib/nagios/plugins/check_zpool_online": {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
"/usr/lib/nagios/plugins/check_zpool_space": {
|
||||||
|
'mode': '0755',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
svc_systemd = {
|
||||||
|
'zed': {
|
||||||
|
'needs': ['pkg_apt:zfs-zed'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
zfs_datasets = node.metadata.get('zfs', {}).get('datasets', {})
|
||||||
|
zfs_pools = {}
|
||||||
|
|
||||||
|
for name, attrs in node.metadata.get('zfs', {}).get('pools', {}).items():
|
||||||
|
zfs_pools[name] = attrs
|
||||||
|
|
||||||
|
# Not yet supported on debian buster
|
||||||
|
#actions[f'pool_{name}_enable_trim'] = {
|
||||||
|
# 'command': f'zpool set autotrim=on {name}',
|
||||||
|
# 'unless': f'zpool get autotrim -H -o value {name} | grep -q on',
|
||||||
|
# 'needs': [
|
||||||
|
# f'zfs_pool:{name}'
|
||||||
|
# ]
|
||||||
|
#}
|
||||||
|
|
||||||
|
directories = {
|
||||||
|
"/etc/zfs-snapshot-backup-pre.d": {
|
||||||
|
'purge': True,
|
||||||
|
},
|
||||||
|
"/etc/zfs-snapshot-backup-post.d": {
|
||||||
|
'purge': True,
|
||||||
|
},
|
||||||
|
"/etc/zfs-snapshot-backup-final.d": {
|
||||||
|
'purge': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO implement when we start managing backups via bundlewrap
|
||||||
|
#if node.metadata.get('zfs', {}).get('snapshots', {}).get('backup', {}).get('enabled', True):
|
||||||
|
# directories["/mnt/zfs-snapshot-backup"] = {}
|
||||||
|
#
|
||||||
|
# files["/usr/local/sbin/zfs-backup-snapshot"] = {
|
||||||
|
# 'content_type': 'mako',
|
||||||
|
# 'context': {
|
||||||
|
# # Set by our own metadata processor, guaranteed to exist.
|
||||||
|
# 'filesystems': node.metadata['zfs']['snapshots']['backup']['filesystems_with_snapshot'],
|
||||||
|
# },
|
||||||
|
# 'mode': '0755',
|
||||||
|
# }
|
||||||
|
# files["/usr/local/sbin/zfs-backup-snapshot-unmount"] = {
|
||||||
|
# 'content_type': 'mako',
|
||||||
|
# 'context': {
|
||||||
|
# # Set by our own metadata processor, guaranteed to exist.
|
||||||
|
# 'filesystems': node.metadata['zfs']['snapshots']['backup']['filesystems_with_snapshot'],
|
||||||
|
# },
|
||||||
|
# 'mode': '0755',
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#else:
|
||||||
|
# files["/mnt/zfs-snapshot-backup"] = {'delete': True}
|
||||||
|
|
||||||
|
# TODO when we start using telegraf
|
||||||
|
#if node.has_bundle('telegraf'):
|
||||||
|
# files['/etc/telegraf-zfs-dataset.conf'] = {
|
||||||
|
# 'content': dumps(
|
||||||
|
# node.metadata.get('zfs', {}),
|
||||||
|
# cls=MetadataJSONEncoder,
|
||||||
|
# indent=4,
|
||||||
|
# sort_keys=True,
|
||||||
|
# ) + '\n',
|
||||||
|
# }
|
||||||
|
# files['/usr/local/bin/telegraf-zfs-dataset'] = {
|
||||||
|
# 'mode': '0775',
|
||||||
|
# }
|
139
bundles/zfs/metadata.py
Normal file
139
bundles/zfs/metadata.py
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'cron': {
|
||||||
|
'zfs-auto-snapshot-daily': '0 0 * * * root /usr/local/sbin/zfs-auto-snapshot daily',
|
||||||
|
'zfs-auto-snapshot-hourly': '0 * * * * root /usr/local/sbin/zfs-auto-snapshot hourly',
|
||||||
|
'zfs-auto-snapshot-monthly': '0 0 1 * * root /usr/local/sbin/zfs-auto-snapshot monthly',
|
||||||
|
'zfs-auto-snapshot-weekly': '0 0 * * 7 root /usr/local/sbin/zfs-auto-snapshot weekly',
|
||||||
|
},
|
||||||
|
'zfs': {
|
||||||
|
'datasets': {},
|
||||||
|
'pools': {},
|
||||||
|
'snapshots': {
|
||||||
|
# 'backup': {
|
||||||
|
# 'enabled': True,
|
||||||
|
# 'filesystems_with_snapshot': {},
|
||||||
|
# },
|
||||||
|
'retain_defaults': {
|
||||||
|
'hourly': 24,
|
||||||
|
'daily': 7,
|
||||||
|
'weekly': 2,
|
||||||
|
'monthly': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#if node.has_bundle('telegraf'):
|
||||||
|
# defaults.update({
|
||||||
|
# 'telegraf': {
|
||||||
|
# 'input_plugins': {
|
||||||
|
# 'exec': {
|
||||||
|
# 'zfs_dataset': {
|
||||||
|
# 'command': 'telegraf-zfs-dataset',
|
||||||
|
# 'interval': '120s',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# 'zfs': {},
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# })
|
||||||
|
# defaults['sudo']['verbatim'].add('telegraf ALL=(ALL) NOPASSWD:/sbin/zfs list *')
|
||||||
|
|
||||||
|
if node.has_bundle('sshmon'):
|
||||||
|
defaults.update({
|
||||||
|
'icinga2_api': {
|
||||||
|
'zfs': {
|
||||||
|
'services': {
|
||||||
|
'ZFS AUTO SNAPSHOT': {
|
||||||
|
'command_on_monitored_host': '/usr/lib/nagios/plugins/check_zfs_auto_snapshot',
|
||||||
|
},
|
||||||
|
'ZFS MOUNTED VOLUMES': {
|
||||||
|
'command_on_monitored_host': '/usr/lib/nagios/plugins/check_zfs_volumes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor
|
||||||
|
def zfs_scrub_cronjob(metadata):
|
||||||
|
when = metadata.get('zfs/scrub/cron', '{} 0 * * sun'.format((node.magic_number % 60)))
|
||||||
|
return {
|
||||||
|
'cron': {
|
||||||
|
'zfs-scrub': '{} root /usr/lib/zfs-linux/scrub'.format(when),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#@metadata_reactor
|
||||||
|
#def zfs_snapshot_backup(metadata):
|
||||||
|
# if metadata.get('zfs/snapshots/backup/enabled'):
|
||||||
|
# # Collect all filesystems/datasets (e.g., "tank/mysql") which
|
||||||
|
# # are configured for (local) snapshots. For each of them, store
|
||||||
|
# # the mountpoint. This information will be used primarily by
|
||||||
|
# # "/usr/local/sbin/zfs-backup-snapshot", but may also be used by
|
||||||
|
# # other bundles (think backup tools).
|
||||||
|
# #
|
||||||
|
# # In other words, this API allows other bundles to check whether
|
||||||
|
# # a path belongs to a ZFS dataset with snapshots enabled.
|
||||||
|
#
|
||||||
|
# filesystems = {}
|
||||||
|
#
|
||||||
|
# if metadata.get('zfs/snapshots/snapshot_only', None) is not None:
|
||||||
|
# for name in metadata.get('zfs/snapshots/snapshot_only'):
|
||||||
|
# attrs = metadata.get('zfs/datasets')[name]
|
||||||
|
# if attrs.get('mountpoint') not in (None, "none"):
|
||||||
|
# filesystems[name] = attrs['mountpoint']
|
||||||
|
# else:
|
||||||
|
# for name, attrs in metadata.get('zfs/datasets').items():
|
||||||
|
# if attrs.get('mountpoint') not in (None, "none"):
|
||||||
|
# filesystems[name] = attrs['mountpoint']
|
||||||
|
#
|
||||||
|
# for pattern in metadata.get('zfs/snapshots/snapshot_never', set()):
|
||||||
|
# filesystems = {k: v for k, v in filesystems.items() if not re.search(pattern, k)}
|
||||||
|
#
|
||||||
|
# return {
|
||||||
|
# 'zfs': {
|
||||||
|
# 'snapshots': {
|
||||||
|
# 'backup': {
|
||||||
|
# 'filesystems_with_snapshot': filesystems,
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
# else:
|
||||||
|
# return {}
|
||||||
|
|
||||||
|
|
||||||
|
@metadata_reactor
|
||||||
|
def monitoring(metadata):
|
||||||
|
if not node.has_bundle('sshmon'):
|
||||||
|
raise DoNotRunAgain
|
||||||
|
|
||||||
|
services = {}
|
||||||
|
|
||||||
|
for poolname, pool_options in metadata.get('zfs/pools').items():
|
||||||
|
services['ZFS ZPOOL ONLINE {}'.format(poolname)] = {
|
||||||
|
'command_on_monitored_host': 'sudo /usr/lib/nagios/plugins/check_zpool_online {}'.format(poolname),
|
||||||
|
}
|
||||||
|
|
||||||
|
services['ZFS ZPOOL SPACE ' + poolname] = {
|
||||||
|
'command_on_monitored_host': 'sudo /usr/lib/nagios/plugins/check_zpool_space {} 90'.format(poolname)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
services['ZFS OLD SNAPSHOTS'] = {
|
||||||
|
'command_on_monitored_host': 'sudo /usr/lib/nagios/plugins/check_zfs_old_snapshots',
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'icinga2_api': {
|
||||||
|
'zfs': {
|
||||||
|
'services': services,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
140
items/zfs_dataset.py
Normal file
140
items/zfs_dataset.py
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
from pipes import quote
|
||||||
|
|
||||||
|
from bundlewrap.items import Item
|
||||||
|
from bundlewrap.utils.text import mark_for_translation as _
|
||||||
|
|
||||||
|
|
||||||
|
def create(node, path, options):
|
||||||
|
option_list = []
|
||||||
|
for option, value in sorted(options.items()):
|
||||||
|
# We must exclude the 'mounted' property here because it's a
|
||||||
|
# read-only "informational" property.
|
||||||
|
if option != 'mounted' and value is not None:
|
||||||
|
option_list.append("-o {}={}".format(quote(option), quote(value)))
|
||||||
|
option_args = " ".join(option_list)
|
||||||
|
|
||||||
|
node.run(
|
||||||
|
"zfs create {} {}".format(
|
||||||
|
option_args,
|
||||||
|
quote(path),
|
||||||
|
),
|
||||||
|
may_fail=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if options['mounted'] == 'no':
|
||||||
|
set_option(node, path, 'mounted', 'no')
|
||||||
|
|
||||||
|
|
||||||
|
def does_exist(node, path):
|
||||||
|
status_result = node.run(
|
||||||
|
"zfs list {}".format(quote(path)),
|
||||||
|
may_fail=True,
|
||||||
|
)
|
||||||
|
return status_result.return_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_option(node, path, option):
|
||||||
|
cmd = "zfs get -Hp -o value {} {}".format(quote(option), quote(path))
|
||||||
|
# We always expect this to succeed since we don't call this function
|
||||||
|
# if we have already established that the dataset does not exist.
|
||||||
|
status_result = node.run(cmd)
|
||||||
|
return status_result.stdout.decode('utf-8').strip()
|
||||||
|
|
||||||
|
def set_option(node, path, option, value):
|
||||||
|
if option == 'mounted':
|
||||||
|
# 'mounted' is a read-only property that can not be altered by
|
||||||
|
# 'set'. We need to call 'zfs mount tank/foo'.
|
||||||
|
node.run(
|
||||||
|
"zfs {} {}".format(
|
||||||
|
"mount" if value == 'yes' else "unmount",
|
||||||
|
quote(path),
|
||||||
|
),
|
||||||
|
may_fail=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
node.run(
|
||||||
|
"zfs set {}={} {}".format(
|
||||||
|
quote(option),
|
||||||
|
quote(value),
|
||||||
|
quote(path),
|
||||||
|
),
|
||||||
|
may_fail=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ZFSDataset(Item):
|
||||||
|
"""
|
||||||
|
Creates ZFS datasets and manages their options.
|
||||||
|
"""
|
||||||
|
BUNDLE_ATTRIBUTE_NAME = "zfs_datasets"
|
||||||
|
ITEM_ATTRIBUTES = {
|
||||||
|
'atime': None,
|
||||||
|
'acltype': None,
|
||||||
|
'compression': None,
|
||||||
|
'mountpoint': None,
|
||||||
|
'quota': None,
|
||||||
|
'recordsize': None,
|
||||||
|
'dedup': None,
|
||||||
|
}
|
||||||
|
ITEM_TYPE_NAME = "zfs_dataset"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ZFSDataset name:{self.name} {' '.join(f'{k}:{v}' for k,v in self.attributes.items())}>"
|
||||||
|
|
||||||
|
def cdict(self):
|
||||||
|
cdict = {}
|
||||||
|
for option, value in self.attributes.items():
|
||||||
|
if option == 'mountpoint' and value is None:
|
||||||
|
value = "none"
|
||||||
|
if value is not None:
|
||||||
|
cdict[option] = value
|
||||||
|
cdict['mounted'] = 'no' if cdict.get('mountpoint') in (None, "none") else 'yes'
|
||||||
|
return cdict
|
||||||
|
|
||||||
|
def fix(self, status):
|
||||||
|
if status.must_be_created:
|
||||||
|
create(self.node, self.name, status.cdict)
|
||||||
|
else:
|
||||||
|
for option in status.keys_to_fix:
|
||||||
|
set_option(self.node, self.name, option, status.cdict[option])
|
||||||
|
|
||||||
|
def get_auto_deps(self, items):
|
||||||
|
pool = self.name.split("/")[0]
|
||||||
|
pool_item = "zfs_pool:{}".format(pool)
|
||||||
|
pool_item_found = False
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if item.ITEM_TYPE_NAME == "zfs_pool" and item.name == pool:
|
||||||
|
# Add dependency to the pool this dataset resides on.
|
||||||
|
pool_item_found = True
|
||||||
|
yield pool_item
|
||||||
|
elif (
|
||||||
|
item.ITEM_TYPE_NAME == "zfs_dataset" and
|
||||||
|
self.name != item.name and
|
||||||
|
self.name.startswith(item.name + "/")
|
||||||
|
):
|
||||||
|
# Find all other datasets that are parents of this
|
||||||
|
# dataset.
|
||||||
|
# XXX Could be optimized by finding the "largest"
|
||||||
|
# parent only.
|
||||||
|
yield item.id
|
||||||
|
|
||||||
|
if not pool_item_found:
|
||||||
|
raise Exception(_(
|
||||||
|
"ZFS dataset {dataset} resides on pool {pool} but item "
|
||||||
|
"{dep} does not exist"
|
||||||
|
).format(
|
||||||
|
dataset=self.name,
|
||||||
|
pool=pool,
|
||||||
|
dep=pool_item,
|
||||||
|
))
|
||||||
|
|
||||||
|
def sdict(self):
|
||||||
|
if not does_exist(self.node, self.name):
|
||||||
|
return None
|
||||||
|
|
||||||
|
sdict = {}
|
||||||
|
for option, value in self.attributes.items():
|
||||||
|
sdict[option] = get_option(self.node, self.name, option)
|
||||||
|
sdict['mounted'] = get_option(self.node, self.name, 'mounted')
|
||||||
|
return sdict
|
181
items/zfs_pool.py
Normal file
181
items/zfs_pool.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
from collections import Counter
|
||||||
|
from pipes import quote
|
||||||
|
|
||||||
|
from bundlewrap.exceptions import BundleError
|
||||||
|
from bundlewrap.items import Item
|
||||||
|
from bundlewrap.utils.text import mark_for_translation as _
|
||||||
|
|
||||||
|
|
||||||
|
def create_mirrors(node, path, mirrors):
|
||||||
|
cmd = ""
|
||||||
|
for devices in mirrors:
|
||||||
|
actual_targets = []
|
||||||
|
for device in devices:
|
||||||
|
actual_targets.append(quote(prepare_blockdevice(node, device)))
|
||||||
|
cmd += "mirror {} ".format(" ".join(actual_targets))
|
||||||
|
|
||||||
|
node.run("zpool create {} {}".format(quote(path), cmd))
|
||||||
|
node.run("zfs unmount {}".format(quote(path)))
|
||||||
|
|
||||||
|
|
||||||
|
def create_raidz(node, path, devices):
|
||||||
|
cmd = ""
|
||||||
|
actual_targets = []
|
||||||
|
for device in devices:
|
||||||
|
actual_targets.append(quote(prepare_blockdevice(node, device)))
|
||||||
|
cmd += "raidz {} ".format(" ".join(actual_targets))
|
||||||
|
|
||||||
|
node.run("zpool create {} {}".format(quote(path), cmd))
|
||||||
|
node.run("zfs unmount {}".format(quote(path)))
|
||||||
|
|
||||||
|
|
||||||
|
def create_single(node, path, device):
|
||||||
|
actual_target = prepare_blockdevice(node, device)
|
||||||
|
node.run("zpool create {} {}".format(quote(path), quote(actual_target)))
|
||||||
|
node.run("zfs unmount {}".format(quote(path)))
|
||||||
|
|
||||||
|
|
||||||
|
def does_exist(node, path):
|
||||||
|
status_result = node.run(
|
||||||
|
"zpool list {}".format(quote(path)),
|
||||||
|
may_fail=True,
|
||||||
|
)
|
||||||
|
return status_result.return_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_blockdevice(node, device):
|
||||||
|
# To increase our chances of success, we run partprobe beforehand to
|
||||||
|
# make the kernel re-scan all devices.
|
||||||
|
node.run("partprobe", may_fail=True)
|
||||||
|
|
||||||
|
# Try to find out if the device already contains some filesystem.
|
||||||
|
# Please note that there is no 100% reliable way to do this.
|
||||||
|
res = node.run("lsblk -rndo fstype {}".format(quote(device)))
|
||||||
|
detected = res.stdout.decode('UTF-8').strip()
|
||||||
|
if detected != "":
|
||||||
|
raise Exception(_("Device {} to be used for ZFS, but it is not empty! Has '{}'.").format(
|
||||||
|
device, detected))
|
||||||
|
|
||||||
|
res = node.run("lsblk -rndo type {}".format(device))
|
||||||
|
if res.stdout.decode('UTF-8').strip() == "disk":
|
||||||
|
# We create a new partition on a raw disk. That partition will
|
||||||
|
# be used for ZFS. Yes, this is on purpose. No, creating a pool
|
||||||
|
# on raw disks does not work properly on Linux.
|
||||||
|
node.run("parted {} mklabel gpt".format(quote(device)))
|
||||||
|
node.run("parted {} mkpart -a optimal primary 0% 100%".format(quote(device)))
|
||||||
|
node.run("partprobe")
|
||||||
|
|
||||||
|
# Simply append a "1" to get to the first partition.
|
||||||
|
#
|
||||||
|
# XXX I know that this fails if you're using /dev/disk/by-*.
|
||||||
|
# Yes, this is a problem if your device names are not
|
||||||
|
# predictable. Yes, we could use "lsblk" to try to find the
|
||||||
|
# first partition ... but "lsblk" still reports it as
|
||||||
|
# "/dev/vdb1" instead of "/dev/disk/by-foo/bar-part1".
|
||||||
|
#
|
||||||
|
# This is an unsolved problem. Please configure your VMs to use
|
||||||
|
# predictable device names.
|
||||||
|
if device.find('nvme') != -1: # NVME Devices have the partitions with the prefix pX
|
||||||
|
partition = "{}p1".format(device)
|
||||||
|
|
||||||
|
else:
|
||||||
|
partition = "{}1".format(device)
|
||||||
|
|
||||||
|
return partition
|
||||||
|
else:
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
class ZFSPool(Item):
|
||||||
|
"""
|
||||||
|
Creates ZFS pools and the required partitions.
|
||||||
|
"""
|
||||||
|
BUNDLE_ATTRIBUTE_NAME = "zfs_pools"
|
||||||
|
ITEM_ATTRIBUTES = {
|
||||||
|
'device': None,
|
||||||
|
'mirrors': None,
|
||||||
|
'raidz': None,
|
||||||
|
}
|
||||||
|
ITEM_TYPE_NAME = "zfs_pool"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<ZFSPool name:{} device:{} mirrors:{} raidz:{}>".format(
|
||||||
|
self.name,
|
||||||
|
self.attributes['device'],
|
||||||
|
self.attributes['mirrors'],
|
||||||
|
self.attributes['raidz'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def cdict(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def devices_used(self):
|
||||||
|
devices = []
|
||||||
|
if self.attributes['device'] is not None:
|
||||||
|
devices.append(self.attributes['device'])
|
||||||
|
if self.attributes['mirrors'] is not None:
|
||||||
|
for mirror in self.attributes['mirrors']:
|
||||||
|
devices.extend(mirror)
|
||||||
|
if self.attributes['raidz'] is not None:
|
||||||
|
devices.extend(self.attributes['raidz'])
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def fix(self, status):
|
||||||
|
if status.must_be_created:
|
||||||
|
if self.attributes['device'] is not None:
|
||||||
|
create_single(self.node, self.name, self.attributes['device'])
|
||||||
|
elif self.attributes['mirrors'] is not None:
|
||||||
|
create_mirrors(self.node, self.name, self.attributes['mirrors'])
|
||||||
|
elif self.attributes['raidz'] is not None:
|
||||||
|
create_raidz(self.node, self.name, self.attributes['raidz'])
|
||||||
|
|
||||||
|
def sdict(self):
|
||||||
|
# We don't care about the device if the pool already exists.
|
||||||
|
return {} if does_exist(self.node, self.name) else None
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
duplicate_devices = [
|
||||||
|
item for item, count in Counter(self.devices_used).items() if count > 1
|
||||||
|
]
|
||||||
|
if duplicate_devices:
|
||||||
|
raise BundleError(_(
|
||||||
|
"{item} on node {node} uses {devices} more than once as an underlying device"
|
||||||
|
).format(
|
||||||
|
item=self.id,
|
||||||
|
node=self.node.name,
|
||||||
|
devices=_(" and ").join(duplicate_devices),
|
||||||
|
))
|
||||||
|
|
||||||
|
# Have a look at all other ZFS pools on this node and check if
|
||||||
|
# multiple pools try to use the same device.
|
||||||
|
for item in self.node.items:
|
||||||
|
if (
|
||||||
|
item.ITEM_TYPE_NAME == "zfs_pool" and
|
||||||
|
item.name != self.name and
|
||||||
|
set(item.devices_used).intersection(set(self.devices_used))
|
||||||
|
):
|
||||||
|
raise BundleError(_(
|
||||||
|
"Both the ZFS pools {self} and {other} on node {node} "
|
||||||
|
"try to use {devices} as the underlying storage device"
|
||||||
|
).format(
|
||||||
|
self=self.name,
|
||||||
|
other=item.name,
|
||||||
|
node=self.node.name,
|
||||||
|
devices=_(" and ").join(set(item.devices_used).intersection(set(self.devices_used))),
|
||||||
|
))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_attributes(cls, bundle, item_id, attributes):
|
||||||
|
device_config = []
|
||||||
|
for key in ('device', 'mirrors', 'raidz'):
|
||||||
|
device_config.append(attributes.get(key))
|
||||||
|
device_config = [key for key in device_config if key is not None]
|
||||||
|
if len(device_config) != 1:
|
||||||
|
raise BundleError(_(
|
||||||
|
"{item} on node {node} must have exactly one of "
|
||||||
|
"'device', 'mirrors', or 'raidz'"
|
||||||
|
).format(
|
||||||
|
item=item_id,
|
||||||
|
node=bundle.node.name,
|
||||||
|
))
|
Loading…
Reference in a new issue