[Cluster-devel] [PATCH] libgfs2: Add a gfs2 block query language

Steven Whitehouse swhiteho at redhat.com
Mon Oct 15 09:48:50 UTC 2012


Hi,

Looks good. Thats a big step forward for us I think,

Steve.

On Fri, 2012-10-12 at 19:51 +0100, Andrew Price wrote:
> This patch adds a small language which we can use to write libgfs2 and
> gfs2-utils tests (among other things). It provides 'get' and 'set'
> statements which look up and modify gfs2 blocks. The language API is
> defined as:
> 
>    struct lgfs2_lang_state;
> 
>    struct lgfs2_lang_result {
>            uint64_t lr_blocknr;
>            struct gfs2_buffer_head *lr_bh;
>            const struct lgfs2_metadata *lr_mtype;
>            int lr_state; // GFS2_BLKST_*
>    };
> 
>    struct lgfs2_lang_state *lgfs2_lang_init(void);
>    void lgfs2_lang_free(struct lgfs2_lang_state **state);
> 
>    int lgfs2_lang_parsef(struct lgfs2_lang_state *state, FILE *script);
>    int lgfs2_lang_parses(struct lgfs2_lang_state *state, const char *script);
> 
>    struct lgfs2_lang_result *lgfs2_lang_result_next(struct lgfs2_lang_state *state, struct gfs2_sbd *sbd);
>    int lgfs2_lang_result_print(struct lgfs2_lang_result *result);
>    void lgfs2_lang_result_free(struct lgfs2_lang_result **result);
> 
> The lgfs2_lang_parse{s,f} functions allow you to parse a string or a
> file respectively. Using the same state object you can then run the same
> script multiple times, without parsing it again, using
> lgfs2_lang_interpret(). The intended usage of these functions can be
> shown with a simple example (error checking omitted):
> 
>    struct lgfs2_lang_state *state = lgfs2_lang_init();
>    lgfs2_lang_parses(state, "get sb; get 1234 state; set '/foo/bar' {di_entries: 3}");
>    for (result = lgfs2_lang_result_next(state, &sbd);
>         result != NULL;
>         result = lgfs2_lang_result_next(state, &sbd)) {
>            lgfs2_lang_result_print(result);
>            lgfs2_lang_result_free(&result);
>    }
>    lgfs2_lang_free(&state);
> 
> The language has a simple syntax:
> 
>    get <block_lookup> [state]
>    set <block_lookup> {<field0>:<value0>, <field1>:<value1>, ... }
> 
> A block lookup can be a file system block address (1234 or 0x1234), a
> resource group subscript (rgrp[0], rgrp[0x5]), a keyword (sb, master,
> root, rindex) or an offset from any of the above (1234+5, rgrp[1]+23,
> rindex+68, ...).
> 
> After the block lookup, the 'get' statement takes an optional keyword
> 'state' which allows you to query the bitmap state of the block.
> 
> Examples of 'get':
> 
>    get rgrp[1]+23 state
>    --> result.lr_state == GFS2_BLKST_FREE
> 
>    get rindex
>    --> result.lr_bh is a buffer containing the rindex dinode.
>    --> result.lr_mtype is the block's metatype as defined in meta.c.
> 
> The 'set' statement requires a list of field-value pairs which are to be
> modified in the block. The field names must match the names shown when
> running the 'get' statement with the same block lookup.
> 
> Examples of 'set':
> 
>    set '/foo/bar/baz' { di_entries: 3 }
>    --> result.lr_bh contains the dinode block at path /foo/bar/baz, with
>        the di_entries field modified.
>    --> result.lr_mtype is the block's metatype as defined in meta.c.
> 
>    set sb { sb_lockproto: 'lock_dlm', sb_bsize: 0x1000 }
>    --> result.lr_bh contains the superblock with the lockproto changed to
>        lock_dlm and the block size changed to 4096.
>    --> result.lr_mtype is the block's metatype as defined in meta.c.
> 
> Whitespace is insignificant in the language and semicolons can be used
> to separate multiple statements. When writing longer scripts spanning
> multiple lines, C-like // syntax can be used to insert comments.
> 
> Signed-off-by: Andrew Price <anprice at redhat.com>
> ---
>  .gitignore               |   7 +
>  configure.ac             |   2 +
>  gfs2/libgfs2/Makefile.am |  13 +-
>  gfs2/libgfs2/buf.c       |  10 +
>  gfs2/libgfs2/fs_ops.c    |   6 +-
>  gfs2/libgfs2/lang.c      | 603 +++++++++++++++++++++++++++++++++++++++++++++++
>  gfs2/libgfs2/lang.h      |  61 +++++
>  gfs2/libgfs2/lexer.l     | 101 ++++++++
>  gfs2/libgfs2/libgfs2.h   |  24 ++
>  gfs2/libgfs2/meta.c      |  50 ++++
>  gfs2/libgfs2/misc.c      |   2 -
>  gfs2/libgfs2/parser.y    | 186 +++++++++++++++
>  gfs2/libgfs2/super.c     |   2 +-
>  13 files changed, 1057 insertions(+), 10 deletions(-)
>  create mode 100644 gfs2/libgfs2/lang.c
>  create mode 100644 gfs2/libgfs2/lang.h
>  create mode 100644 gfs2/libgfs2/lexer.l
>  create mode 100644 gfs2/libgfs2/parser.y
> 
> diff --git a/.gitignore b/.gitignore
> index e8b7ea1..4e3071a 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -21,6 +21,9 @@ make/stamp-h1
>  m4
>  make/clusterautoconfig.h*
>  missing
> +ylwrap
> +cscope.out
> +.gdb_history
>  *.pc
>  .deps
>  .libs
> @@ -29,6 +32,10 @@ missing
>  *.lo
>  gfs2/convert/gfs2_convert
>  gfs2/edit/gfs2_edit
> +gfs2/libgfs2/parser.c
> +gfs2/libgfs2/parser.h
> +gfs2/libgfs2/lexer.c
> +gfs2/libgfs2/lexer.h
>  gfs2/fsck/fsck.gfs2
>  gfs2/mkfs/mkfs.gfs2
>  gfs2/mount/mount.gfs2
> diff --git a/configure.ac b/configure.ac
> index d56cfac..ef09569 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -56,6 +56,8 @@ AM_PROG_CC_C_O
>  AC_PROG_LN_S
>  AC_PROG_INSTALL
>  AC_PROG_MAKE_SET
> +AC_PROG_LEX
> +AC_PROG_YACC
>  
>  ## local helper functions
>  
> diff --git a/gfs2/libgfs2/Makefile.am b/gfs2/libgfs2/Makefile.am
> index 4e60b0a..7103e09 100644
> --- a/gfs2/libgfs2/Makefile.am
> +++ b/gfs2/libgfs2/Makefile.am
> @@ -1,15 +1,24 @@
>  MAINTAINERCLEANFILES	= Makefile.in
>  
> -noinst_HEADERS		= libgfs2.h
> +CLEANFILES		= parser.h parser.c lexer.c lexer.h
> +BUILT_SOURCES		= parser.h lexer.h
> +AM_LFLAGS		= --header-file=lexer.h
> +AM_YFLAGS		= -d
> +
> +noinst_HEADERS		= libgfs2.h lang.h
>  
>  noinst_LTLIBRARIES	= libgfs2.la
>  
>  libgfs2_la_SOURCES	= block_list.c fs_bits.c gfs1.c misc.c rgrp.c super.c \
>  			  buf.c fs_geometry.c gfs2_disk_hash.c ondisk.c \
>  			  device_geometry.c fs_ops.c gfs2_log.c recovery.c \
> -			  structures.c meta.c
> +			  structures.c meta.c lang.c parser.y lexer.l
>  
>  libgfs2_la_CPPFLAGS	= -D_FILE_OFFSET_BITS=64 \
>  			  -D_LARGEFILE64_SOURCE \
>  			  -D_GNU_SOURCE \
>  			  -I$(top_srcdir)/gfs2/include
> +
> +# Autotools can't handle header files output by flex so we have to generate it manually
> +lexer.h: lexer.l
> +	$(LEX) -o lexer.c $(AM_LFLAGS) $^
> diff --git a/gfs2/libgfs2/buf.c b/gfs2/libgfs2/buf.c
> index 956dd8b..5bc1a4e 100644
> --- a/gfs2/libgfs2/buf.c
> +++ b/gfs2/libgfs2/buf.c
> @@ -83,3 +83,13 @@ int brelse(struct gfs2_buffer_head *bh)
>  	free(bh);
>  	return error;
>  }
> +
> +uint32_t lgfs2_get_block_type(const struct gfs2_buffer_head *lbh)
> +{
> +	const struct gfs2_meta_header *mh = lbh->iov.iov_base;
> +
> +	if (be32_to_cpu(mh->mh_magic) == GFS2_MAGIC)
> +		return be32_to_cpu(mh->mh_type);
> +
> +	return 0;
> +}
> diff --git a/gfs2/libgfs2/fs_ops.c b/gfs2/libgfs2/fs_ops.c
> index ec150e8..3d027e8 100644
> --- a/gfs2/libgfs2/fs_ops.c
> +++ b/gfs2/libgfs2/fs_ops.c
> @@ -1759,11 +1759,7 @@ int gfs2_lookupi(struct gfs2_inode *dip, const char *filename, int len,
>  		return 0;
>  	}
>  	error = dir_search(dip, filename, len, NULL, &inum);
> -	if (error) {
> -		if (error == -ENOENT)
> -			return 0;
> -	}
> -	else
> +	if (!error)
>  		*ipp = lgfs2_inode_read(sdp, inum.no_addr);
>  
>  	return error;
> diff --git a/gfs2/libgfs2/lang.c b/gfs2/libgfs2/lang.c
> new file mode 100644
> index 0000000..12ca7bd
> --- /dev/null
> +++ b/gfs2/libgfs2/lang.c
> @@ -0,0 +1,603 @@
> +#include <stdint.h>
> +#include <string.h>
> +#include <stdlib.h>
> +#include <stdio.h>
> +#include <sys/queue.h>
> +#include <errno.h>
> +#include <limits.h>
> +#include <ctype.h>
> +
> +#include "parser.h"
> +#include "lang.h"
> +
> +const char* ast_type_string[] = {
> +	[AST_NONE] = "NONE",
> +	// Statements
> +	[AST_ST_SET] = "SET",
> +	[AST_ST_GET] = "GET",
> +
> +	// Expressions
> +	[AST_EX_ID] = "IDENTIFIER",
> +	[AST_EX_NUMBER] = "NUMBER",
> +	[AST_EX_STRING] = "STRING",
> +	[AST_EX_ADDRESS] = "ADDRESS",
> +	[AST_EX_PATH] = "PATH",
> +	[AST_EX_SUBSCRIPT] = "SUBSCRIPT",
> +	[AST_EX_OFFSET] = "OFFSET",
> +	[AST_EX_BLOCKSPEC] = "BLOCKSPEC",
> +	[AST_EX_STRUCTSPEC] = "STRUCTSPEC",
> +	[AST_EX_FIELDSPEC] = "FIELDSPEC",
> +
> +	// Keywords
> +	[AST_KW_STATE] = "STATE",
> +};
> +
> +/**
> + * Initialize an expression node of the given type from a source string.
> + * Currently just converts numerical values and string values where
> + * appropriate. String values are duplicted into newly allocated buffers as the
> + * text from the parser will go away.
> + * Returns 0 on success or non-zero with errno set on failure
> + */
> +static int ast_expr_init(struct ast_node *expr, ast_node_t type, const char *str)
> +{
> +	int ret = 0;
> +	switch (type) {
> +	case AST_EX_OFFSET:
> +		str++; // Cut off the +
> +	case AST_EX_NUMBER:
> +		ret = sscanf(str, "%"SCNi64, &expr->ast_num);
> +		if (ret != 1) {
> +			return 1;
> +		}
> +		break;
> +	case AST_EX_ID:
> +	case AST_EX_PATH:
> +	case AST_EX_STRING:
> +		expr->ast_str = strdup(str);
> +		if (expr->ast_str == NULL) {
> +			return 1;
> +		}
> +		break;
> +	case AST_EX_ADDRESS:
> +	case AST_EX_SUBSCRIPT:
> +	case AST_EX_BLOCKSPEC:
> +	case AST_EX_STRUCTSPEC:
> +	case AST_EX_FIELDSPEC:
> +	case AST_KW_STATE:
> +		break;
> +	default:
> +		errno = EINVAL;
> +		return 1;
> +	}
> +	return 0;
> +}
> +
> +/**
> + * Create a new AST node of a given type from a source string.
> + * Returns a pointer to the new node or NULL on failure with errno set.
> + */
> +struct ast_node *ast_new(ast_node_t type, const char *text)
> +{
> +	struct ast_node *node;
> +	node = (struct ast_node *)calloc(1, sizeof(struct ast_node));
> +	if (node == NULL) {
> +		goto return_fail;
> +	}
> +
> +	if (type > _AST_EX_START && ast_expr_init(node, type, text)) {
> +		goto return_free;
> +	}
> +
> +	node->ast_text = strdup(text);
> +	if (node->ast_text == NULL) {
> +		goto return_free;
> +	}
> +	node->ast_type = type;
> +
> +	return node;
> +
> +return_free:
> +	if (node->ast_text) {
> +		free(node->ast_text);
> +	}
> +	if (node->ast_str) {
> +		free(node->ast_str);
> +	}
> +	free(node);
> +return_fail:
> +	fprintf(stderr, "Failed to create new value from %s: %s\n", text, strerror(errno));
> +	return NULL;
> +}
> +
> +/**
> + * Free the memory allocated for an AST node and set its pointer to NULL
> + */
> +void ast_destroy(struct ast_node **node)
> +{
> +	if (*node == NULL) {
> +		return;
> +	}
> +	ast_destroy(&(*node)->ast_left);
> +	ast_destroy(&(*node)->ast_right);
> +	switch((*node)->ast_type) {
> +	case AST_EX_ID:
> +	case AST_EX_PATH:
> +	case AST_EX_STRING:
> +		free((*node)->ast_str);
> +		break;
> +	default:
> +		break;
> +	}
> +	free((*node)->ast_text);
> +	free(*node);
> +	*node = NULL;
> +}
> +
> +static void ast_string_unescape(char *str)
> +{
> +	int head, tail;
> +	for (head = tail = 0; str[head] != '\0'; head++, tail++) {
> +		if (str[head] == '\\' && str[head+1] != '\0')
> +			head++;
> +		str[tail] = str[head];
> +	}
> +	str[tail] = '\0';
> +}
> +
> +static uint64_t ast_lookup_path(char *path, struct gfs2_sbd *sbd)
> +{
> +	int err = 0;
> +	char *c;
> +	struct gfs2_inode *ip, *iptmp;
> +	char *segment;
> +	uint64_t bn = 0;
> +
> +	segment = strtok_r(path, "/", &c);
> +	ip = lgfs2_inode_read(sbd, sbd->sd_sb.sb_root_dir.no_addr);
> +
> +	while (ip != NULL) {
> +		if (segment == NULL) { // No more segments
> +			bn = ip->i_di.di_num.no_addr;
> +			inode_put(&ip);
> +			return bn;
> +		}
> +		ast_string_unescape(segment);
> +		err = gfs2_lookupi(ip, segment, strlen(segment), &iptmp);
> +		inode_put(&ip);
> +		if (err != 0) {
> +			errno = -err;
> +			break;
> +		}
> +		ip = iptmp;
> +		segment = strtok_r(NULL, "/", &c);
> +	}
> +
> +	perror("Path lookup");
> +	return 0;
> +}
> +
> +enum block_id {
> +	ID_SB	= 0,
> +	ID_MASTER,
> +	ID_ROOT,
> +	ID_RINDEX,
> +
> +	ID_END
> +};
> +
> +/**
> + * Names of blocks which can be uniquely identified in the fs
> + */
> +static const char *block_ids[] = {
> +	[ID_SB]		= "sb",
> +	[ID_MASTER]	= "master",
> +	[ID_ROOT]	= "root",
> +	[ID_RINDEX]	= "rindex",
> +
> +	[ID_END]	= NULL
> +};
> +
> +static uint64_t ast_lookup_id(const char *id, struct gfs2_sbd *sbd)
> +{
> +	uint64_t bn = 0;
> +	int i;
> +	for (i = 0; i < ID_END; i++) {
> +		if (!strcmp(id, block_ids[i])) {
> +			break;
> +		}
> +	}
> +	switch (i) {
> +	case ID_SB:
> +		bn = sbd->sb_addr;
> +		break;
> +	case ID_MASTER:
> +		bn = sbd->sd_sb.sb_master_dir.no_addr;
> +		break;
> +	case ID_ROOT:
> +		bn = sbd->sd_sb.sb_root_dir.no_addr;
> +		break;
> +	case ID_RINDEX:
> +		bn = sbd->md.riinode->i_di.di_num.no_addr;
> +		break;
> +	default:
> +		return 0;
> +	}
> +	return bn;
> +}
> +
> +static uint64_t ast_lookup_rgrp(uint64_t rgnum, struct gfs2_sbd *sbd)
> +{
> +	uint64_t i = rgnum;
> +	struct osi_node *n;
> +
> +	for (n = osi_first(&sbd->rgtree); n != NULL && i > 0; n = osi_next(n), i--);
> +	if (n != NULL && i == 0)
> +		return ((struct rgrp_tree *)n)->ri.ri_addr;
> +	fprintf(stderr, "Resource group number out of range: %"PRIu64"\n", rgnum);
> +	return 0;
> +}
> +
> +static uint64_t ast_lookup_subscript(struct ast_node *id, struct ast_node *index,
> +                                     struct gfs2_sbd *sbd)
> +{
> +	uint64_t bn = 0;
> +	const char *name = id->ast_str;
> +	if (!strcmp(name, "rgrp")) {
> +		bn = ast_lookup_rgrp(index->ast_num, sbd);
> +	} else {
> +		fprintf(stderr, "Unrecognized identifier %s\n", name);
> +	}
> +	return bn;
> +}
> +
> +/**
> + * Look up a block and return its number. The kind of lookup depends on the
> + * type of the ast node.
> + */
> +static uint64_t ast_lookup_block_num(struct ast_node *ast, struct gfs2_sbd *sbd)
> +{
> +	uint64_t bn = 0;
> +	switch (ast->ast_type) {
> +	case AST_EX_OFFSET:
> +		bn = ast_lookup_block_num(ast->ast_left, sbd) + ast->ast_num;
> +		break;
> +	case AST_EX_ADDRESS:
> +		bn = ast->ast_num;
> +		break;
> +	case AST_EX_PATH:
> +		bn = ast_lookup_path(ast->ast_str, sbd);
> +		break;
> +	case AST_EX_ID:
> +		bn = ast_lookup_id(ast->ast_str, sbd);
> +		break;
> +	case AST_EX_SUBSCRIPT:
> +		bn = ast_lookup_subscript(ast->ast_left, ast->ast_left->ast_left, sbd);
> +		break;
> +	default:
> +		break;
> +	}
> +	return bn;
> +}
> +
> +static struct gfs2_buffer_head *ast_lookup_block(struct ast_node *node, struct gfs2_sbd *sbd)
> +{
> +	uint64_t bn = ast_lookup_block_num(node, sbd);
> +	if (bn == 0) {
> +		return NULL;
> +	}
> +
> +	return bread(sbd, bn);
> +}
> +
> +static const char *bitstate_strings[] = {
> +	[GFS2_BLKST_FREE] = "Free",
> +	[GFS2_BLKST_USED] = "Used",
> +	[GFS2_BLKST_UNLINKED] = "Unlinked",
> +	[GFS2_BLKST_DINODE] = "Dinode"
> +};
> +
> +/**
> + * Print a representation of an arbitrary GFS2 block to stdout
> + */
> +int lgfs2_lang_result_print(struct lgfs2_lang_result *result)
> +{
> +	int i;
> +	if (result->lr_mtype != NULL) {
> +		for (i = 0; i < result->lr_mtype->nfields; i++) {
> +			lgfs2_field_print(result->lr_bh, result->lr_mtype, &result->lr_mtype->fields[i]);
> +		}
> +	} else {
> +		printf("%"PRIu64": %s\n", result->lr_blocknr, bitstate_strings[result->lr_state]);
> +	}
> +	return 0;
> +}
> +
> +static int ast_get_bitstate(uint64_t bn, struct gfs2_sbd *sbd)
> +{
> +	int ret = 0;
> +	int state = 0;
> +	struct rgrp_tree *rgd = gfs2_blk2rgrpd(sbd, bn);
> +	if (rgd == NULL) {
> +		fprintf(stderr, "Could not find resource group for block %"PRIu64"\n", bn);
> +		return -1;
> +	}
> +
> +	ret = gfs2_rgrp_read(sbd, rgd);
> +	if (ret != 0) {
> +		fprintf(stderr, "Failed to read resource group for block %"PRIu64": %d\n", bn, ret);
> +		return -1;
> +	}
> +
> +	state = gfs2_get_bitmap(sbd, bn, rgd);
> +	if (state == -1) {
> +		fprintf(stderr, "Failed to acquire bitmap state for block %"PRIu64"\n", bn);
> +		return -1;
> +	}
> +
> +	gfs2_rgrp_relse(rgd);
> +	return state;
> +}
> +
> +static const struct lgfs2_metadata *ast_lookup_mtype(const struct gfs2_buffer_head *bh)
> +{
> +	const struct lgfs2_metadata *mtype;
> +	const uint32_t mh_type = lgfs2_get_block_type(bh);
> +	if (mh_type == 0) {
> +		fprintf(stderr, "Could not determine type for block %"PRIu64"\n", bh->b_blocknr);
> +		return NULL;
> +	}
> +
> +	mtype = lgfs2_find_mtype(mh_type, bh->sdp->gfs1 ? LGFS2_MD_GFS1 : LGFS2_MD_GFS2);
> +	if (mtype == NULL) {
> +		fprintf(stderr, "Could not determine meta type for block %"PRIu64"\n", bh->b_blocknr);
> +		return NULL;
> +	}
> +	return mtype;
> +}
> +
> +/**
> + * Interpret the get statement.
> + */
> +static struct lgfs2_lang_result *ast_interp_get(struct lgfs2_lang_state *state,
> +                                     struct ast_node *ast, struct gfs2_sbd *sbd)
> +{
> +	struct lgfs2_lang_result *result = calloc(1, sizeof(struct lgfs2_lang_result));
> +	if (result == NULL) {
> +		fprintf(stderr, "Failed to allocate memory for result\n");
> +		return NULL;
> +	}
> +
> +	if (ast->ast_right->ast_right == NULL) {
> +		result->lr_bh = ast_lookup_block(ast->ast_right, sbd);
> +		if (result->lr_bh == NULL) {
> +			free(result);
> +			return NULL;
> +		}
> +		result->lr_blocknr = result->lr_bh->b_blocknr;
> +		result->lr_mtype = ast_lookup_mtype(result->lr_bh);
> +
> +	} else if (ast->ast_right->ast_right->ast_type == AST_KW_STATE) {
> +		result->lr_blocknr = ast_lookup_block_num(ast->ast_right, sbd);
> +		if (result->lr_blocknr == 0) {
> +			return NULL;
> +		}
> +		result->lr_state = ast_get_bitstate(result->lr_blocknr, sbd);
> +	}
> +
> +	return result;
> +}
> +
> +/**
> + * Interpret a UUID string by removing hyphens from the string and then
> + * interprets 16 pairs of hex digits as octets.
> + */
> +static int ast_str_to_uuid(const char *str, uint8_t *uuid)
> +{
> +	char s[33];
> +	int head, tail, tmp;
> +
> +	for (head = tail = 0; head < strlen(str) && tail < 33; head++) {
> +		if (str[head] == '-')
> +			continue;
> +		s[tail] = tolower(str[head]);
> +		if (!((s[tail] >= 'a' && s[tail] <= 'f') ||
> +		      (s[tail] >= '0' && s[tail] <= '9')))
> +			goto invalid;
> +		tail++;
> +	}
> +	if (tail != 32) {
> +		goto invalid;
> +	}
> +	s[tail] = '\0';
> +	for (head = 0; head < 16; head++) {
> +		if (sscanf(s+(head*2), "%02x", &tmp) != 1) {
> +			goto invalid;
> +		}
> +		*(uuid + head) = tmp;
> +	}
> +	return AST_INTERP_SUCCESS;
> +invalid:
> +	fprintf(stderr, "Invalid UUID\n");
> +	return AST_INTERP_INVAL;
> +}
> +
> +/**
> + * Set a field of a gfs2 block of a given type to a given value.
> + * Returns AST_INTERP_* to signal success, an invalid field/value or an error.
> + */
> +static int ast_field_set(struct gfs2_buffer_head *bh, const struct lgfs2_metafield *field,
> +                                                                        struct ast_node *val)
> +{
> +	char *fieldp = (char *)bh->iov.iov_base + field->offset;
> +
> +	if (field->flags & LGFS2_MFF_UUID) {
> +		uint8_t uuid[16];
> +		int ret = ast_str_to_uuid(val->ast_str, uuid);
> +
> +		if (ret != AST_INTERP_SUCCESS)
> +			return ret;
> +
> +		memcpy(fieldp, uuid, 16);
> +		bmodified(bh);
> +		return AST_INTERP_SUCCESS;
> +	}
> +
> +	if ((field->flags & LGFS2_MFF_STRING) && strlen(val->ast_str) > field->length) {
> +		fprintf(stderr, "String '%s' is too long for field '%s'\n", val->ast_str, field->name);
> +		return AST_INTERP_INVAL;
> +	}
> +
> +	if (field->flags & (LGFS2_MFF_STRING|LGFS2_MFF_UUID)) {
> +		strncpy(fieldp, val->ast_str, field->length - 1);
> +		fieldp[field->length - 1] = '\0';
> +		bmodified(bh);
> +		return AST_INTERP_SUCCESS;
> +	} else {
> +		// Numeric fields
> +		switch(field->length) {
> +		case 1:
> +			if (val->ast_num > UINT8_MAX)
> +				break;
> +			*fieldp = (uint8_t)val->ast_num;
> +			bmodified(bh);
> +			return AST_INTERP_SUCCESS;
> +		case 2:
> +			if (val->ast_num > UINT16_MAX)
> +				break;
> +			*(uint16_t *)fieldp = cpu_to_be16((uint16_t)val->ast_num);
> +			bmodified(bh);
> +			return AST_INTERP_SUCCESS;
> +		case 4:
> +			if (val->ast_num > UINT32_MAX)
> +				break;
> +			*(uint32_t *)fieldp = cpu_to_be32((uint32_t)val->ast_num);
> +			bmodified(bh);
> +			return AST_INTERP_SUCCESS;
> +		case 8:
> +			*(uint64_t *)fieldp = cpu_to_be64((uint64_t)val->ast_num);
> +			bmodified(bh);
> +			return AST_INTERP_SUCCESS;
> +		default:
> +			// This should never happen
> +			return AST_INTERP_ERR;
> +		}
> +	}
> +
> +	fprintf(stderr, "Invalid field assignment: %s (size %d) = %s\n",
> +	                     field->name, field->length, val->ast_text);
> +	return AST_INTERP_INVAL;
> +}
> +
> +/**
> + * Interpret an assignment (set)
> + */
> +static struct lgfs2_lang_result *ast_interp_set(struct lgfs2_lang_state *state,
> +                                    struct ast_node *ast, struct gfs2_sbd *sbd)
> +{
> +	struct ast_node *lookup = ast->ast_right;
> +	struct ast_node *fieldspec;
> +	struct ast_node *fieldname;
> +	struct ast_node *fieldval;
> +	uint32_t mh_type = 0;
> +	int i = 0;
> +	int ret = 0;
> +
> +	struct lgfs2_lang_result *result = calloc(1, sizeof(struct lgfs2_lang_result));
> +	if (result == NULL) {
> +		fprintf(stderr, "Failed to allocate memory for result\n");
> +		return NULL;
> +	}
> +
> +	result->lr_bh = ast_lookup_block(lookup, sbd);
> +	if (result->lr_bh == NULL) {
> +		goto out_err;
> +	}
> +
> +	mh_type = lgfs2_get_block_type(result->lr_bh);
> +	if (mh_type == 0) {
> +		goto out_err;
> +	}
> +
> +	result->lr_mtype = lgfs2_find_mtype(mh_type, sbd->gfs1 ? LGFS2_MD_GFS1 : LGFS2_MD_GFS2);
> +	if (result->lr_mtype == NULL) {
> +		goto out_err;
> +	}
> +
> +	for (fieldspec = lookup->ast_right;
> +	     fieldspec != NULL && fieldspec->ast_type == AST_EX_FIELDSPEC;
> +	     fieldspec = fieldspec->ast_left) {
> +
> +		fieldname = fieldspec->ast_right;
> +		fieldval = fieldname->ast_right;
> +		for (i = 0; i < result->lr_mtype->nfields; i++) {
> +			if (!strcmp(result->lr_mtype->fields[i].name, fieldname->ast_str)) {
> +				ret = ast_field_set(result->lr_bh, &result->lr_mtype->fields[i], fieldval);
> +				if (ret != AST_INTERP_SUCCESS) {
> +					goto out_err;
> +				}
> +				break;
> +			}
> +		}
> +	}
> +
> +	ret = bwrite(result->lr_bh);
> +	if (ret != 0) {
> +		fprintf(stderr, "Failed to write modified block %"PRIu64": %s\n",
> +		                        result->lr_bh->b_blocknr, strerror(errno));
> +		goto out_err;
> +	}
> +
> +	return result;
> +
> +out_err:
> +	lgfs2_lang_result_free(&result);
> +	return NULL;
> +}
> +
> +static struct lgfs2_lang_result *ast_interpret_node(struct lgfs2_lang_state *state,
> +                                        struct ast_node *ast, struct gfs2_sbd *sbd)
> +{
> +	struct lgfs2_lang_result *result = NULL;
> +
> +	if (ast->ast_type == AST_ST_SET) {
> +		result = ast_interp_set(state, ast, sbd);
> +	} else if (ast->ast_type == AST_ST_GET) {
> +		result = ast_interp_get(state, ast, sbd);
> +	} else {
> +		fprintf(stderr, "Invalid AST node type: %d\n", ast->ast_type);
> +	}
> +	return result;
> +}
> +
> +struct lgfs2_lang_result *lgfs2_lang_result_next(struct lgfs2_lang_state *state,
> +                                                           struct gfs2_sbd *sbd)
> +{
> +	struct lgfs2_lang_result *result;
> +	if (state->ls_interp_curr == NULL) {
> +		return NULL;
> +	}
> +	result = ast_interpret_node(state, state->ls_interp_curr, sbd);
> +	if (result == NULL) {
> +		return NULL;
> +	}
> +	state->ls_interp_curr = state->ls_interp_curr->ast_left;
> +	return result;
> +}
> +
> +void lgfs2_lang_result_free(struct lgfs2_lang_result **result)
> +{
> +	if (*result == NULL) {
> +		fprintf(stderr, "Warning: attempted to free a null result\n");
> +		return;
> +	}
> +
> +	if ((*result)->lr_mtype != NULL) {
> +		(*result)->lr_bh->b_modified = 0;
> +		brelse((*result)->lr_bh);
> +		(*result)->lr_bh = NULL;
> +	}
> +
> +	free(*result);
> +	*result = NULL;
> +}
> diff --git a/gfs2/libgfs2/lang.h b/gfs2/libgfs2/lang.h
> new file mode 100644
> index 0000000..955e52e
> --- /dev/null
> +++ b/gfs2/libgfs2/lang.h
> @@ -0,0 +1,61 @@
> +#ifndef LANG_H
> +#define LANG_H
> +#include <stdint.h>
> +#include "libgfs2.h"
> +
> +struct lgfs2_lang_state {
> +	int ls_colnum;
> +	int ls_linenum;
> +	int ls_errnum;
> +	struct ast_node *ls_ast_root;
> +	struct ast_node *ls_ast_tail;
> +	struct ast_node *ls_interp_curr;
> +};
> +
> +typedef enum {
> +	AST_NONE,
> +	// Statements
> +	AST_ST_SET,
> +	AST_ST_GET,
> +
> +	_AST_EX_START,
> +	// Expressions
> +	AST_EX_ID,
> +	AST_EX_NUMBER,
> +	AST_EX_STRING,
> +	AST_EX_ADDRESS,
> +	AST_EX_PATH,
> +	AST_EX_SUBSCRIPT,
> +	AST_EX_OFFSET,
> +	AST_EX_BLOCKSPEC,
> +	AST_EX_STRUCTSPEC,
> +	AST_EX_FIELDSPEC,
> +
> +	// Keywords
> +	AST_KW_STATE,
> +} ast_node_t;
> +
> +enum {
> +	AST_INTERP_SUCCESS	= 0, // Success
> +	AST_INTERP_FAIL		= 1, // Failure
> +	AST_INTERP_INVAL	= 2, // Invalid field/type mismatch
> +	AST_INTERP_ERR		= 3, // Something went wrong, see errno
> +};
> +
> +extern const char* ast_type_string[];
> +
> +struct ast_node {
> +	ast_node_t ast_type;
> +	struct ast_node *ast_left;
> +	struct ast_node *ast_right;
> +	char *ast_text;
> +	char *ast_str;
> +	uint64_t ast_num;
> +};
> +
> +extern struct ast_node *ast_new(ast_node_t type, const char *text);
> +extern void ast_destroy(struct ast_node **val);
> +
> +#define YYSTYPE struct ast_node *
> +
> +#endif /* LANG_H */
> diff --git a/gfs2/libgfs2/lexer.l b/gfs2/libgfs2/lexer.l
> new file mode 100644
> index 0000000..36e1c2d
> --- /dev/null
> +++ b/gfs2/libgfs2/lexer.l
> @@ -0,0 +1,101 @@
> +%{
> +#include "lang.h"
> +#include "parser.h"
> +
> +#define EXTRA ((struct lgfs2_lang_state *)yyextra)
> +
> +#define P(token, type, text) do {\
> +	*(yylval) = ast_new(type, text);\
> +	if (*(yylval) == NULL) {\
> +		EXTRA->ls_errnum = errno;\
> +		return 1;\
> +	}\
> +	return (TOK_##token);\
> +} while(0)
> +
> +#define COLNUM EXTRA->ls_colnum
> +#define YY_USER_ACTION COLNUM += yyleng;
> +
> +%}
> +%option bison-bridge reentrant
> +%option warn debug
> +%option nounput noinput
> +%option noyywrap
> +%option extra-type="struct lgfs2_lang_state *"
> +
> +letter			[a-zA-Z_]
> +decdigit		[0-9]
> +decnumber		-?{decdigit}+
> +hexdigit		[0-9a-fA-F]
> +hexnumber		-?0x{hexdigit}+
> +number			({decnumber}|{hexnumber})
> +offset			\+{number}
> +id			{letter}({letter}|{decdigit}|\.)*
> +string			\'([^\']|\\\')*\'
> +comment			\/\/.*\n
> +whitespace		[ \t\r]+
> +
> +%%
> +
> +\{			{
> +			return TOK_LBRACE;
> +			}
> +\}			{
> +			return TOK_RBRACE;
> +			}
> +\[			{
> +			return TOK_LBRACKET;
> +			}
> +\]			{
> +			P(RBRACKET, AST_EX_SUBSCRIPT, "[ ]");
> +			}
> +\,			{
> +			return TOK_COMMA;
> +			}
> +\:			{
> +			P(COLON, AST_EX_FIELDSPEC, yytext);
> +			}
> +\;			{
> +			return TOK_SEMI;
> +			}
> +set			{
> +			P(SET, AST_ST_SET, yytext);
> +			}
> +get			{
> +			P(GET, AST_ST_GET, yytext);
> +			}
> +state			{
> +			P(STATE, AST_KW_STATE, yytext);
> +			}
> +{string}		{
> +			yytext[yyleng-1] = '\0';
> +			P(STRING, AST_EX_STRING, yytext + 1);
> +			}
> +{offset}		{
> +			P(OFFSET, AST_EX_OFFSET, yytext);
> +			}
> +{number}		{
> +			P(NUMBER, AST_EX_NUMBER, yytext);
> +			}
> +{id}			{
> +			P(ID, AST_EX_ID, yytext);
> +			}
> +{comment}		{
> +			COLNUM = 0;
> +			EXTRA->ls_linenum++;
> +			}
> +<<EOF>>			{
> +			return 0;
> +			}
> +\n			{
> +			COLNUM = 0;
> +			EXTRA->ls_linenum++;
> +			}
> +{whitespace}		;
> +.			{
> +			printf("Unexpected character '%s' on line %d column %d\n",
> +			       yytext, yylineno, COLNUM);
> +			return 1;
> +			}
> +
> +%%
> diff --git a/gfs2/libgfs2/libgfs2.h b/gfs2/libgfs2/libgfs2.h
> index 74ee2d0..3045337 100644
> --- a/gfs2/libgfs2/libgfs2.h
> +++ b/gfs2/libgfs2/libgfs2.h
> @@ -356,6 +356,10 @@ extern const unsigned lgfs2_ld_type_size;
>  extern const struct lgfs2_symbolic lgfs2_ld1_types[];
>  extern const unsigned lgfs2_ld1_type_size;
>  extern int lgfs2_selfcheck(void);
> +extern const struct lgfs2_metadata *lgfs2_find_mtype(uint32_t mh_type, const unsigned versions);
> +extern int lgfs2_field_print(const struct gfs2_buffer_head *bh,
> +                             const struct lgfs2_metadata *mtype,
> +                             const struct lgfs2_metafield *field);
>  
>  /* bitmap.c */
>  struct gfs2_bmap {
> @@ -379,6 +383,7 @@ extern struct gfs2_buffer_head *__bread(struct gfs2_sbd *sdp, uint64_t num,
>  					int line, const char *caller);
>  extern int bwrite(struct gfs2_buffer_head *bh);
>  extern int brelse(struct gfs2_buffer_head *bh);
> +extern uint32_t lgfs2_get_block_type(const struct gfs2_buffer_head *lbh);
>  
>  #define bmodified(bh) do { bh->b_modified = 1; } while(0)
>  
> @@ -828,6 +833,25 @@ extern void gfs2_log_descriptor_print(struct gfs2_log_descriptor *ld);
>  extern void gfs2_statfs_change_print(struct gfs2_statfs_change *sc);
>  extern void gfs2_quota_change_print(struct gfs2_quota_change *qc);
>  
> +/* Language functions */
> +
> +struct lgfs2_lang_state;
> +
> +struct lgfs2_lang_result {
> +	uint64_t lr_blocknr;
> +	struct gfs2_buffer_head *lr_bh;
> +	const struct lgfs2_metadata *lr_mtype;
> +	int lr_state; // GFS2_BLKST_*
> +};
> +
> +extern struct lgfs2_lang_state *lgfs2_lang_init(void);
> +extern int lgfs2_lang_parsef(struct lgfs2_lang_state *state, FILE *script);
> +extern int lgfs2_lang_parses(struct lgfs2_lang_state *state, const char *script);
> +extern struct lgfs2_lang_result *lgfs2_lang_result_next(struct lgfs2_lang_state *state, struct gfs2_sbd *sbd);
> +extern int lgfs2_lang_result_print(struct lgfs2_lang_result *result);
> +extern void lgfs2_lang_result_free(struct lgfs2_lang_result **result);
> +extern void lgfs2_lang_free(struct lgfs2_lang_state **state);
> +
>  __END_DECLS
>  
>  #endif /* __LIBGFS2_DOT_H__ */
> diff --git a/gfs2/libgfs2/meta.c b/gfs2/libgfs2/meta.c
> index a677cdc..29bf239 100644
> --- a/gfs2/libgfs2/meta.c
> +++ b/gfs2/libgfs2/meta.c
> @@ -808,3 +808,53 @@ int lgfs2_selfcheck(void)
>  	return ret;
>  }
>  
> +const struct lgfs2_metadata *lgfs2_find_mtype(uint32_t mh_type, const unsigned versions)
> +{
> +	const struct lgfs2_metadata *m = lgfs2_metadata;
> +	unsigned n = 0;
> +
> +	do {
> +		if ((m[n].versions & versions) && m[n].mh_type == mh_type)
> +			return &m[n];
> +		n++;
> +	} while (n < lgfs2_metadata_size);
> +
> +	return NULL;
> +}
> +
> +/**
> + * Print a representation of an arbitrary field of an arbitrary GFS2 block to stdout
> + * Returns 0 if successful, 1 otherwise
> + */
> +int lgfs2_field_print(const struct gfs2_buffer_head *bh, const struct lgfs2_metadata *mtype,
> +                      const struct lgfs2_metafield *field)
> +{
> +	const char *fieldp = (char *)bh->iov.iov_base + field->offset;
> +
> +	printf("%s\t%"PRIu64"\t%u\t%u\t%s\t", mtype->name, bh->b_blocknr, field->offset, field->length, field->name);
> +	if (field->flags & LGFS2_MFF_UUID) {
> +		printf("'%s'\n", str_uuid((const unsigned char *)fieldp));
> +	} else if (field->flags & LGFS2_MFF_STRING) {
> +		printf("'%s'\n", fieldp);
> +	} else {
> +		switch(field->length) {
> +		case 1:
> +			printf("%"PRIu8"\n", *(uint8_t *)fieldp);
> +			break;
> +		case 2:
> +			printf("%"PRIu16"\n", be16_to_cpu(*(uint16_t *)fieldp));
> +			break;
> +		case 4:
> +			printf("%"PRIu32"\n", be32_to_cpu(*(uint32_t *)fieldp));
> +			break;
> +		case 8:
> +			printf("%"PRIu64"\n", be64_to_cpu(*(uint64_t *)fieldp));
> +			break;
> +		default:
> +			// "Reserved" field so just print 0
> +			printf("0\n");
> +			return 1;
> +		}
> +	}
> +	return 0;
> +}
> diff --git a/gfs2/libgfs2/misc.c b/gfs2/libgfs2/misc.c
> index a68da4a..c2eb245 100644
> --- a/gfs2/libgfs2/misc.c
> +++ b/gfs2/libgfs2/misc.c
> @@ -26,8 +26,6 @@
>  #define SYS_BASE "/sys/fs/gfs2" /* FIXME: Look in /proc/mounts to find this */
>  #define DIV_RU(x, y) (((x) + (y) - 1) / (y))
>  
> -static char sysfs_buf[PAGE_SIZE];
> -
>  int compute_heightsize(struct gfs2_sbd *sdp, uint64_t *heightsize,
>  	uint32_t *maxheight, uint32_t bsize1, int diptrs, int inptrs)
>  {
> diff --git a/gfs2/libgfs2/parser.y b/gfs2/libgfs2/parser.y
> new file mode 100644
> index 0000000..084d15e
> --- /dev/null
> +++ b/gfs2/libgfs2/parser.y
> @@ -0,0 +1,186 @@
> +%code top {
> +#include <errno.h>
> +#include "lang.h"
> +#include "lexer.h"
> +
> +static int yyerror(struct lgfs2_lang_state *state, yyscan_t lexer, const char *errorstr)
> +{
> +	fprintf(stderr, "%d:%d: %s\n", state->ls_linenum, state->ls_colnum, errorstr);
> +	return 1;
> +}
> +
> +}
> +%defines
> +%debug
> +%define api.pure
> +%parse-param { struct lgfs2_lang_state *state }
> +%parse-param { yyscan_t lexer }
> +%lex-param { yyscan_t lexer }
> +%start script
> +%token TOK_COLON
> +%token TOK_COMMA
> +%token TOK_ID
> +%token TOK_LBRACE
> +%token TOK_LBRACKET
> +%token TOK_NUMBER
> +%token TOK_OFFSET
> +%token TOK_RBRACE
> +%token TOK_RBRACKET
> +%token TOK_SEMI
> +%token TOK_SET
> +%token TOK_GET
> +%token TOK_STATE
> +%token TOK_STRING
> +%%
> +script:		statements		{
> +			state->ls_ast_root = $1;
> +			state->ls_interp_curr = $1;
> +		}
> +		| statements TOK_SEMI	{
> +			state->ls_ast_root = $1;
> +			state->ls_interp_curr = $1;
> +		}
> +;
> +statements:	statements TOK_SEMI statement	{
> +			state->ls_ast_tail->ast_left = $3;
> +			state->ls_ast_tail = $3;
> +			$$ = $1;
> +		}
> +		| statement		{
> +			if (state->ls_ast_tail == NULL)
> +				state->ls_ast_tail = $1;
> +			$$ = $1;
> +		}
> +;
> +
> +statement:	set_stmt		{ $$ = $1; }
> +		| get_stmt		{ $$ = $1; }
> +;
> +set_stmt:	TOK_SET blockspec structspec {
> +			$1->ast_right = $2;
> +			$2->ast_right = $3;
> +			$$ = $1;
> +		}
> +;
> +get_stmt:	TOK_GET blockspec { $1->ast_right = $2; $$ = $1; }
> +		| TOK_GET blockspec TOK_STATE {
> +			$1->ast_right = $2;
> +			$2->ast_right = $3;
> +			$$ = $1;
> +		}
> +;
> +blockspec:	offset			{ $$ = $1; }
> +		| address		{ $$ = $1; }
> +		| path			{ $$ = $1; }
> +		| block_literal		{ $$ = $1; }
> +		| subscript		{ $$ = $1; }
> +;
> +offset:		blockspec TOK_OFFSET {
> +			$2->ast_left = $1;
> +			$$ = $2;
> +		}
> +;
> +block_literal:	identifier		{ $$ = $1; }
> +;
> +subscript:	block_literal TOK_LBRACKET index TOK_RBRACKET {
> +			$4->ast_left = $1;
> +			$1->ast_left = $3;
> +			$$ = $4;
> +		}
> +;
> +index:		number			{ $$ = $1; }
> +		| identifier		{ $$ = $1; }
> +;
> +address:	number			{ $1->ast_type = AST_EX_ADDRESS; $$ = $1; }
> +;
> +path:		string			{
> +			if (*($1->ast_str) != '/') {
> +				fprintf(stderr, "Path doesn't begin with '/': %s\n", $1->ast_str);
> +				YYABORT;
> +			}
> +			$1->ast_type = AST_EX_PATH;
> +			$$ = $1;
> +		}
> +;
> +structspec:	TOK_LBRACE fieldspecs TOK_RBRACE { $$ = $2; }
> +;
> +fieldspecs:	fieldspecs TOK_COMMA fieldspec	{ $1->ast_left = $3; $$ = $1; }
> +		| fieldspec			{ $$ = $1; }
> +;
> +fieldspec:	identifier TOK_COLON fieldvalue {
> +			$2->ast_right = $1;
> +			$1->ast_right = $3;
> +			$$ = $2;
> +		}
> +;
> +fieldvalue:	number			{ $$ = $1; }
> +		| string		{ $$ = $1; }
> +;
> +number:		TOK_NUMBER		{ $$ = $1; }
> +string:		TOK_STRING		{ $$ = $1; }
> +identifier:	TOK_ID			{ $$ = $1; }
> +%%
> +
> +/**
> + * Allocate and initialize a new parse state structure. The caller must free the
> + * memory returned by this function.
> + */
> +struct lgfs2_lang_state *lgfs2_lang_init(void)
> +{
> +	struct lgfs2_lang_state *state;
> +	state = calloc(1, sizeof(struct lgfs2_lang_state));
> +	if (state == NULL) {
> +		return NULL;
> +	}
> +	state->ls_linenum = 1;
> +	return state;
> +}
> +
> +void lgfs2_lang_free(struct lgfs2_lang_state **state)
> +{
> +	ast_destroy(&(*state)->ls_ast_root);
> +	free(*state);
> +	*state = NULL;
> +}
> +
> +int lgfs2_lang_parsef(struct lgfs2_lang_state *state, FILE *src)
> +{
> +	int ret = 0;
> +	yyscan_t lexer;
> +
> +	ret = yylex_init_extra(state, &lexer);
> +	if (ret != 0) {
> +		fprintf(stderr, "Failed to initialize lexer.\n");
> +		return ret;
> +	}
> +
> +	yyset_in(src, lexer);
> +	ret = yyparse(state, lexer);
> +	yylex_destroy(lexer);
> +	return ret;
> +}
> +
> +int lgfs2_lang_parses(struct lgfs2_lang_state *state, const char *cstr)
> +{
> +	int ret;
> +	FILE *src;
> +	char *str = strdup(cstr);
> +
> +	if (str == NULL) {
> +		perror("Failed to duplicate source string");
> +		return 1;
> +	}
> +	src = fmemopen(str, strlen(str), "r");
> +	if (src == NULL) {
> +		perror("Failed to open string as source file");
> +		free(str);
> +		return 1;
> +	}
> +	ret = lgfs2_lang_parsef(state, src);
> +	fclose(src);
> +	free(str);
> +	if (ret != 0 || state->ls_errnum != 0) {
> +		return 1;
> +	}
> +	return 0;
> +}
> diff --git a/gfs2/libgfs2/super.c b/gfs2/libgfs2/super.c
> index a3c1964..fdf0e60 100644
> --- a/gfs2/libgfs2/super.c
> +++ b/gfs2/libgfs2/super.c
> @@ -25,7 +25,7 @@ int check_sb(struct gfs2_sb *sb)
>  {
>  	if (sb->sb_header.mh_magic != GFS2_MAGIC ||
>  	    sb->sb_header.mh_type != GFS2_METATYPE_SB) {
> -		errno = -EIO;
> +		errno = EIO;
>  		return -1;
>  	}
>  	if (sb->sb_fs_format == GFS_FORMAT_FS &&





More information about the Cluster-devel mailing list