#!/usr/bin/env python3 # License: CC0 https://creativecommons.org/share-your-work/public-domain/cc0/ # A messy script that figures out ZFS features. It's very messy, sorry. I am # not responsible if this script eats your laundry. # # This uses manpages, because I'm lazy. If your manpages are wrong, you have a # bug. # # If the script is wrong, or could be improved, feel free to contact me on # freenode, and tell me why it's wrong. My nick is zgrep. # # 2018-07-05: Created. # ????-??-??: Many things happened. # 2020-02-23: Applied patch by rlaager. # 2020-02-30: Show domain prefixes (via Vlad Bokov), partially apply patch by rlaager. # 2020-03-05: Patch by rlaager (allocation_classes, ZoL -> openzfs). from sys import argv if len(argv) == 1: path = '.' elif len(argv) != 2: print('Usage:', argv[0], 'path') exit(1) else: path = argv[1] from collections import defaultdict from urllib.request import urlopen from datetime import datetime from re import sub as regex, findall from json import loads as dejson def zfsonlinux(): sources = {'master':'https://raw.githubusercontent.com/openzfs/zfs/master/man/man5/zpool-features.5'} with urlopen('https://zfsonlinux.org') as web: versions = findall(r'download/zfs-([0-9.]+)', web.read().decode('utf-8', 'ignore')) for ver in set(versions): sources[ver] = 'https://raw.githubusercontent.com/openzfs/zfs/zfs-{}/man/man5/zpool-features.5'.format(ver) return sources def openzfsonosx(): sources = {'master': 'https://raw.githubusercontent.com/openzfsonosx/zfs/master/man/man5/zpool-features.5'} with urlopen('https://api.github.com/repos/openzfsonosx/zfs/tags') as web: try: tags = dejson(web.read().decode('utf-8', 'ignore')) tags = [ x['name'].lstrip('zfs-') for x in tags ] tags.sort() latest = tags[-1] tags = [ tag for tag in tags if 'rc' not in tag ] if 'rc' not in latest: tags = tags[-3:] else: tags = tags[-2:] + [latest] except: tags = [] for ver in tags: sources[ver] = 'https://raw.githubusercontent.com/openzfsonosx/zfs/zfs-{}/man/man5/zpool-features.5'.format(ver) return sources def freebsd(): sources = {'head': 'https://svnweb.freebsd.org/base/head/cddl/contrib/opensolaris/cmd/zpool/zpool-features.7?view=co'} with urlopen('https://www.freebsd.org/releases/') as web: versions = findall(r'/releases/([0-9.]+?)R', web.read().decode('utf-8', 'ignore')) with urlopen('https://svnweb.freebsd.org/base/release/') as web: data = web.read().decode('utf-8', 'ignore') actualversions = [] for ver in set(versions): found = list(sorted(findall( r'/base/release/(' + ver.replace('.', '\\.') + r'[0-9.]*)', data ))) if found: actualversions.append(found[-1]) for ver in actualversions: sources[ver] = 'https://svnweb.freebsd.org/base/release/{}/cddl/contrib/opensolaris/cmd/zpool/zpool-features.7?view=co'.format(ver) return sources def omniosce(): sources = {'master': 'https://raw.githubusercontent.com/omniosorg/illumos-omnios/master/usr/src/man/man5/zpool-features.5'} with urlopen('https://omniosce.org/releasenotes.html') as web: versions = findall(r'omnios-build/blob/(r[0-9]+)', web.read().decode('utf-8', 'ignore')) versions.sort() versions = versions[-2:] for ver in versions: sources[ver] = 'https://raw.githubusercontent.com/omniosorg/illumos-omnios/{}/usr/src/man/man5/zpool-features.5'.format(ver) return sources def joyent(): sources = {'master': 'https://raw.githubusercontent.com/joyent/illumos-joyent/master/usr/src/man/man5/zpool-features.5'} with urlopen('https://github.com/joyent/illumos-joyent') as web: versions = findall(r'data-name="release-([0-9]+)"', web.read().decode('utf-8', 'ignore')) versions.sort() versions = versions[-2:] for ver in versions: sources[ver] = 'https://raw.githubusercontent.com/joyent/illumos-joyent/release-{}/usr/src/man/man5/zpool-features.5'.format(ver) return sources def netbsd(): url = 'http://cvsweb.netbsd.org/bsdweb.cgi/~checkout~/src/external/cddl/osnet/dist/cmd/zpool/zpool-features.7?content-type=text/plain&only_with_tag={}' sources = { 'main': url.format('MAIN') } with urlopen('https://netbsd.org/releases/') as web: tags = findall(r'href="formal-.+?/NetBSD-(.+?)\.html', web.read().decode('utf-8', 'ignore')) tags = [ (v, 'netbsd-' + v.replace('.', '-') + '-RELEASE') for v in tags ] for ver, tag in tags: if int(ver.split('.')[0]) >= 9: sources[ver] = url.format(tag) return sources sources = { 'OpenZFS on Linux': zfsonlinux(), 'FreeBSD': freebsd(), 'OpenZFS on OS X': openzfsonosx(), 'OmniOS CE': omniosce(), 'Joyent': joyent(), 'NetBSD': netbsd(), 'Illumos': { 'master': 'https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/man/man5/zpool-features.5', }, # 'OpenZFS on Windows': { # 'master': 'https://raw.githubusercontent.com/openzfsonwindows/ZFSin/master/ZFSin/zfs/man/man5/zpool-features.5', # }, } features = defaultdict(list) readonly = dict() for name, sub in sources.items(): for ver, url in sub.items(): with urlopen(url) as c: if c.getcode() != 200: continue man = c.read().decode('utf-8') for line in man.split('\n'): if line.startswith('.It '): line = line[4:] if line.startswith('GUID'): guid = line.split()[-1] if guid == 'com.intel:allocation_classes': # This is wrong in the documentation for Illumos and # FreeBSD. The actual code in zfeature_common.c uses # org.zfsonlinux:allocation_classes. guid = 'org.zfsonlinux:allocation_classes' elif guid == 'org.open-zfs:large_block': guid += 's' domain, feature = guid.split(':', 1) features[(feature, domain)].append((name, ver)) elif line.startswith('READ\\-ONLY COMPATIBLE'): readonly[guid] = (line.split()[-1] == 'yes') header = list(sorted(sources.keys())) header = list(zip(header, (sorted(sources[name], key=lambda x: regex(r'[^0-9]', '', x) or x) for name in header))) header.append(('Sortix', ('current',))) html = open(path + '/zfs.html', 'w') f_len, d_len = zip(*features.keys()) f_len, d_len = max(map(len, f_len)), max(map(len, d_len)) + 1 html.write(''' ZFS Feature Matrix ''') html.write('\n') html.write('') html.write('') for name, vers in header: html.write('') html.write('\n') for _, vers in header: for ver in vers: html.write('') html.write('\n') for (feature, domain), names in sorted(features.items()): guid = domain + ':' + feature html.write(f'') if readonly[guid]: html.write('') else: html.write('') for name, vers in header: for ver in vers: if (name, ver) in names: html.write('') else: html.write('') html.write('\n') html.write('
Feature FlagRead-Only
Compatible
' + name + '
' + ver + '
{domain}:{feature}yesnoyesno
\n') now = datetime.now().isoformat() + 'Z' html.write('

This works by parsing manpages for feature flags, and is entirely dependent on good, accurate documentation.
Last updated on ' + now + ' using zfs.py.

\n') html.close()