[Libguestfs] [PATCH nbdkit v2 7/7] New filter: nbdkit-block-size-constraint-filter

Eric Blake eblake at redhat.com
Thu Feb 17 18:40:23 UTC 2022


On Thu, Feb 17, 2022 at 02:36:48PM +0000, Richard W.M. Jones wrote:
> This filter can add block size constraints to plugins which don't
> already support them.  It can also enforce an error policy for badly
> behaved clients which do not obey the block size constraints.
> ---
> + nbdkit --filter=blocksize-policy PLUGIN
> +        [blocksize-error-policy=allow|error]
> +        [blocksize-minimum=N]
> +        [blocksize-preferred=N]
> +        [blocksize-maximum=N]
> +
> +=head1 DESCRIPTION
> +
> +C<nbdkit-blocksize-policy-filter> is an L<nbdkit(1)> filter that
> +can add block size constraints to plugins which don't already support
> +then.  It can also enforce an error policy for badly behaved clients

s/then/them/

> +which do not obey the block size constraints.
> +
> +For more information about block size constraints, see section
> +"Block size constraints" in
> +L<https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md>.
> +
> +The simplest usage is to place this filter on top of any plugin which
> +does not advertise block size constraints, and set the
> +C<blocksize-minimum>, C<blocksize-preferred> and C<blocksize-maximum>
> +parameters with the desired constraints.  For example:
> +
> + nbdkit --filter=blocksize-policy memory 1G \
> +        blocksize-preferred=32K
> +
> +would adjust L<nbdkit-memory-plugin(1)> so that clients should
> +prefer 32K requests.  You can query the NBD server advertised constraints
> +using L<nbdinfo(1)>:
> +
> + $ nbdinfo nbd://localhost
> + [...]
> +     block_size_minimum: 1
> +     block_size_preferred: 32768
> +     block_size_maximum: 4294967295
> +
> +The second part of this filter is adjusting the error policy when
> +badly behaved clients do not obey the minimum or maximum request size.
> +Normally nbdkit permits these requests, leaving it up to the plugin
> +whether it rejects the request with an error or tries to process the
> +request (eg. trying to split an over-large request).  With this filter

Maybe also mention 'or doing a read-modify-write for an unaligned write'?

> +you can use C<blocksize-error-policy=error> to reject these requests
> +in the filter with EIO error.  The plugin will not see them.
> +
> +=head2 Combining with L<nbdkit-blocksize-filter(1)>
> +
> +A related filter is L<nbdkit-blocksize-filter(1)>.  That filter can
> +split and combine requests for plugins that cannot handle requests
> +under or over a particular size.
> +
> +Both filters may be used together like this (note that the order of
> +the filters is important):
> +
> +  nbdkit --filter=blocksize-policy \
> +         --filter=blocksize \
> +         PLUGIN ... \
> +         blocksize-error-policy=allow \
> +         blocksize-minimum=64K minblock=64K
> +
> +This says to advertise a minimum block size of 64K.  Well-behaved
> +clients will obey this.  Badly behaved clients will send requests
> +S<E<lt> 64K> which will be converted to slow 64K read-modify-write
> +cycles to the underlying plugin.  In either case the plugin will only
> +see requests on 64K (or multiples of 64K) boundaries.

Should the blocksize filter implement .block_size?  But that's a
separate patch compared to this one adding a new filter.  The example
looks good.

> +++ b/filters/blocksize-policy/policy.c

> +
> +static int
> +policy_config (nbdkit_next_config *next, nbdkit_backend *nxdata,
> +               const char *key, const char *value)
> +{
> +  int64_t r;
> +
> +  if (strcmp (key, "blocksize-error-policy") == 0) {
> +    if (strcmp (value, "allow") == 0)
> +      error_policy = ALLOW;
> +    else if (strcmp (value, "error") == 0)
> +      error_policy = ERROR;
> +    else {
> +      nbdkit_error ("unknown %s: %s", key, value);
> +      return -1;
> +    }
> +    return 0;
> +  }
> +  else if (strcmp (key, "blocksize-minimum") == 0) {
> +    r = nbdkit_parse_size (value);
> +    if (r == -1 || r > UINT32_MAX) {

Should this be 'r <= -1' (or 'r < 0') to flag all negative values as invalid?

/me goes back and re-reads the docs for nbdkit_parse_size

Nope, correct as is, because nbdkit_parse_size already rejects
negatives, and reserves just -1 to indicate any parse error.

> +
> +static int
> +policy_block_size (nbdkit_next *next, void *handle,
> +                   uint32_t *minimum, uint32_t *preferred, uint32_t *maximum)
> +{
> +  /* If the user has set all of the block size parameters then we
> +   * don't need to ask the plugin, we can go ahead and advertise them.
> +   */
> +  if (config_minimum && config_preferred && config_maximum) {
> +    *minimum = config_minimum;
> +    *preferred = config_preferred;
> +    *maximum = config_maximum;
> +    return 0;
> +  }
> +
> +  /* Otherwise, ask the plugin. */
> +  if (next->block_size (next, minimum, preferred, maximum) == -1)
> +    return -1;
> +
> +  /* If the user of this filter didn't configure anything, then return
> +   * the plugin values (even if unset).
> +   */
> +  if (!config_minimum && !config_preferred && !config_maximum)
> +    return 0;
> +
> +  /* Now we get to the awkward case where the user configured some
> +   * values but not others.  There's all kinds of room for things to
> +   * go wrong here, so try to check for obvious user errors as best we
> +   * can.
> +   */
> +  if (*minimum == 0) {           /* Plugin didn't set anything. */
> +    if (config_minimum)
> +      *minimum = config_minimum;
> +    else
> +      *minimum = 1;
> +
> +    if (config_preferred)
> +      *preferred = config_preferred;
> +    else
> +      *preferred = 4096;
> +
> +    if (config_maximum)
> +      *maximum = config_maximum;
> +    else
> +      *maximum = 0xffffffff;

I'd be inclined to advertise 64*1024*1024 here instead of 0xffffffff,
to match the actual buffer size that we know nbdkit is constrained by.

Then again, doing that may cause clients to self-limit calls to .zero
with at most 64M in a chunk, instead of calling it with 4G in a chunk.
It's a toss-up.  (The NBD protocol really does need a fourth value, to
distinguish between maximum buffer and maximum length)

> +  }
> +  else {                        /* Plugin set some values. */
> +    if (config_minimum)
> +      *minimum = config_minimum;
> +
> +    if (config_preferred)
> +      *preferred = config_preferred;
> +
> +    if (config_maximum)
> +      *maximum = config_maximum;
> +  }
> +
> +  if (*minimum > *preferred || *preferred > *maximum) {
> +    nbdkit_error ("computed block size values are invalid, minimum %" PRIu32
> +                  " > preferred %" PRIu32
> +                  " or preferred > maximum %" PRIu32,
> +                  *minimum, *preferred, *maximum);

Good enough for a first cut.  We may find ourselves having to refine
it over time (my biggest worry is a plugin that clamps maximum to the
advertised .get_length size, even if that is smaller than a hardcoded
preferred size - even though right now we are rejecting that in
server/plugins.c), but that is doable.

> +/* This function checks the error policy for all request functions below. */
> +static int
> +check_policy (nbdkit_next *next, const char *type,
> +              uint32_t count, uint64_t offset, int *err)

Ah, new code compared to the last draft ;)

> +{
> +  uint32_t minimum, preferred, maximum;
> +
> +  if (error_policy == ALLOW)
> +    return 0;
> +
> +  /* Get the current block size constraints.  Note these are cached in
> +   * the backend so if they've already been computed then this simply
> +   * returns the cached values.  The plugin is only asked once per
> +   * connection.
> +   */
> +  errno = 0;
> +  if (next->block_size (next, &minimum, &preferred, &maximum) == -1) {
> +    *err = errno ? : EIO;
> +    return -1;
> +  }

Correct.

> +
> +  /* If there are no constraints, allow. */
> +  if (minimum == 0)
> +    return 0;
> +
> +  /* Check constraints. */
> +  if (count < minimum) {
> +    *err = EIO;
> +    nbdkit_error ("client %s request rejected: "
> +                  "count %" PRIu32 " is smaller than minimum size %" PRIu32,
> +                  type, count, minimum);
> +    return -1;
> +  }
> +  if (count > maximum) {
> +    *err = EIO;
> +    nbdkit_error ("client %s request rejected: "
> +                  "count %" PRIu32 " is larger than maximum size %" PRIu32,
> +                  type, count, maximum);
> +    return -1;
> +  }

This is where I worry about size constraints on data (pread/pwrite)
vs. length (zero/trim/cache).  Again, I'm happy to propose a patch on
top of this, so we can use this as our starting point.

> +  if ((count % minimum) != 0) {
> +    *err = EIO;
> +    nbdkit_error ("client %s request rejected: "
> +                  "count %" PRIu32 " is not a multiple "
> +                  "of minimum size %" PRIu32,
> +                  type, count, minimum);
> +    return -1;
> +  }
> +  if ((offset % minimum) != 0) {
> +    *err = EIO;
> +    nbdkit_error ("client %s request rejected: "
> +                  "offset %" PRIu64 " is not aligned to a multiple "
> +                  "of minimum size %" PRIu32,
> +                  type, offset, minimum);
> +    return -1;
> +  }

Do we also want to override .get_size, to flag (or round down) the
case where the size reported by the plugin is not a multiple of the
plugin's minsize, at which point those trailing bytes are inaccessbile
to the client?  Could be done on top.

Overall the series looks good, I'll leave it up to you if you think it
is ready to push now or if you want to adjust for one more round of
reviews, and meanwhile I can start working on my proposed tweaks to
compare on top to see how they look.

-- 
Eric Blake, Principal Software Engineer
Red Hat, Inc.           +1-919-301-3266
Virtualization:  qemu.org | libvirt.org




More information about the Libguestfs mailing list