[blkctl] Add FVM commands

This CL adds the FVM function objects, allowing blkctl to create, query,
and destroy FVM volumes, and allocate, free, query, extend, and shrink
FVM partitions.

Change-Id: I6f053524ae5f77a91914d5c352dc29c2ba4a1de4
diff --git a/system/ulib/blkctl/blkctl.cpp b/system/ulib/blkctl/blkctl.cpp
index 41b5267..c62c8c2 100644
--- a/system/ulib/blkctl/blkctl.cpp
+++ b/system/ulib/blkctl/blkctl.cpp
@@ -16,6 +16,7 @@
 #include <zircon/errors.h>
 #include <zircon/types.h>
 
+#include "fvm.h"
 #include "generic.h"
 #include "ramdisk.h"
 
@@ -40,6 +41,7 @@
 // Then simply #include the appropriate header and add a DEVICE_TYPE to the list below.
 constexpr CmdType kTypes[] = {
     ADD_CMD_TYPE(ramdisk),
+    ADD_CMD_TYPE(fvm),
     // The generic commands should be last, so that various routines use them if no type matches
     ADD_CMD_TYPE(generic),
 };
diff --git a/system/ulib/blkctl/fvm.cpp b/system/ulib/blkctl/fvm.cpp
new file mode 100644
index 0000000..ef58ebd
--- /dev/null
+++ b/system/ulib/blkctl/fvm.cpp
@@ -0,0 +1,345 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stddef.h>
+#include <stdio.h>
+
+#include <blkctl/blkctl.h>
+#include <blkctl/command.h>
+#include <fs-management/fvm.h>
+#include <fs-management/ramdisk.h>
+#include <fvm/fvm.h>
+#include <lib/zx/time.h>
+#include <zircon/device/block.h>
+#include <zircon/device/device.h>
+#include <zircon/errors.h>
+#include <zircon/status.h>
+#include <zircon/types.h>
+
+#include "fvm.h"
+#include "generic.h"
+
+namespace blkctl {
+namespace fvm {
+namespace {
+
+bool SupportsFvmQuery(int fd) {
+    if (fd < 0) {
+        return false;
+    }
+    fvm_info_t info;
+    memset(&info, 0, sizeof(info));
+    return ioctl_block_fvm_query(fd, &info) != ZX_ERR_NOT_SUPPORTED;
+}
+
+bool SupportsFvmVSliceQuery(int fd) {
+    if (fd < 0) {
+        return false;
+    }
+    query_request_t request;
+    query_response_t response;
+    memset(&request, 0, sizeof(request));
+    memset(&response, 0, sizeof(response));
+    return ioctl_block_fvm_vslice_query(fd, &request, &response) != ZX_ERR_NOT_SUPPORTED;
+}
+
+zx_status_t CheckFvm(int fd) {
+    if (!SupportsFvmQuery(fd) && !SupportsFvmVSliceQuery(fd)) {
+        fprintf(stderr, "device does not appear to be an FVM volume or partition\n");
+        return ZX_ERR_INVALID_ARGS;
+    }
+    return ZX_OK;
+}
+
+zx_status_t CheckFvmVolume(int fd) {
+    if (!SupportsFvmQuery(fd)) {
+        fprintf(stderr, "device does not appear to be an FVM volume\n");
+        return ZX_ERR_INVALID_ARGS;
+    }
+    return ZX_OK;
+}
+
+zx_status_t CheckFvmPartition(int fd) {
+    if (!SupportsFvmVSliceQuery(fd)) {
+        fprintf(stderr, "device does not appear to be an FVM partition\n");
+        return ZX_ERR_INVALID_ARGS;
+    }
+    return ZX_OK;
+}
+
+} // namespace
+
+zx_status_t Init::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    size_t slice_size;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("slice_size", &slice_size)) != ZX_OK ||
+        (rc = cmdline->ArgsDone()) != ZX_OK || (rc = OpenReadable(dev, &fd)) != ZX_OK) {
+        return rc;
+    }
+    char path[PATH_MAX];
+    if ((res = ioctl_device_get_topo_path(fd, path, sizeof(path))) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "failed to get topological path: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    if ((rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((rc = fvm_init(fd, slice_size)) != ZX_OK) {
+        fprintf(stderr, "fvm_init failed: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    if ((res = ioctl_device_bind(fd, kDriver, strlen(kDriver))) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "could not bind fvm driver: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    char name[PATH_MAX];
+    snprintf(name, sizeof(name), "%s/fvm", path);
+    if (wait_for_device(name, zx::sec(3).get()) != 0) {
+        fprintf(stderr, "timed out waiting for fvm driver to bind\n");
+        return ZX_ERR_TIMED_OUT;
+    }
+    printf("%s created\n", name);
+    return ZX_OK;
+}
+
+zx_status_t Dump::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    fvm_info_t info;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = CheckFvm(fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((res = ioctl_block_fvm_query(fd, &info)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "ioctl_block_fvm_query failed: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    generic::Dump baseCmd(cmdline);
+    if ((rc = cmdline->UngetArgs(1)) != ZX_OK || (rc = baseCmd.Run()) != ZX_OK) {
+        return rc;
+    }
+    printf("%16s: %zu\n", "FVM slice size", info.slice_size);
+    printf("%16s: %zu\n", "FVM slice count", info.vslice_count);
+    return ZX_OK;
+}
+
+zx_status_t Add::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    alloc_req_t request;
+    memset(&request, 0, sizeof(request));
+    const char* name;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetStrArg("name", &name)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("slices", &request.slice_count)) != ZX_OK) {
+        return rc;
+    }
+    const char* guid;
+
+    rc = cmdline->GetStrArg("guid", &guid, true /* optional */);
+
+    if (rc == ZX_ERR_NOT_FOUND) {
+        GenerateGuid(request.type, sizeof(request.type));
+    } else if (rc != ZX_OK || (rc = ParseGuid(guid, request.type, sizeof(request.type))) != ZX_OK) {
+        return rc;
+    }
+    if ((rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = CheckFvmVolume(fd)) != ZX_OK ||
+        (rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+
+    GenerateGuid(request.guid, sizeof(request.guid));
+    if ((res = ioctl_block_fvm_alloc_partition(fd, &request)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "ioctl_block_fvm_alloc_partition failed: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    printf("added partition '%s' as ", name);
+    PrintGuid(request.guid, sizeof(request.guid));
+    printf("\n");
+    return ZX_OK;
+}
+
+zx_status_t Query::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = CheckFvmPartition(fd)) != ZX_OK) {
+        return rc;
+    }
+    fvm_info_t info;
+    if ((res = ioctl_block_fvm_query(fd, &info)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "ioctl_block_fvm_query failed: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    query_request_t request;
+    query_response_t response;
+    request.count = 1;
+    printf("Allocated ranges:\n");
+    size_t end = 0;
+    for (size_t off = 0; off < info.vslice_count; off = end) {
+        request.vslice_start[0] = off;
+        if ((res = ioctl_block_fvm_vslice_query(fd, &request, &response)) < 0) {
+            rc = static_cast<zx_status_t>(res);
+            fprintf(stderr, "ioctl_block_fvm_vslice_query failed: %s\n", zx_status_get_string(rc));
+            return rc;
+        }
+        ZX_DEBUG_ASSERT(response.count == 1);
+        ZX_DEBUG_ASSERT(response.vslice_range[0].count != 0);
+        end = off + response.vslice_range[0].count;
+        if (response.vslice_range[0].allocated) {
+            printf("  <0x%016" PRIx64 ", 0x%016" PRIx64 ">:   slices %zu through %zu\n",
+                   static_cast<uint64_t>(off * info.slice_size),
+                   static_cast<uint64_t>((end * info.slice_size) - 1), off, end);
+        }
+    }
+    return ZX_OK;
+}
+
+zx_status_t Extend::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    extend_request_t request;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("start", &request.offset)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("length", &request.length)) != ZX_OK ||
+        (rc = cmdline->ArgsDone()) != ZX_OK || (rc = OpenReadable(dev, &fd)) != ZX_OK ||
+        (rc = CheckFvmPartition(fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((res = ioctl_block_fvm_extend(fd, &request)) < 0) {
+        fprintf(stderr, "ioctl_block_fvm_extend failed: %s\n", zx_status_get_string(rc));
+        rc = static_cast<zx_status_t>(res);
+        return rc;
+    }
+    printf("partition extended\n");
+    return ZX_OK;
+}
+
+zx_status_t Shrink::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    extend_request_t request;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("start", &request.offset)) != ZX_OK ||
+        (rc = cmdline->GetNumArg("length", &request.length)) != ZX_OK ||
+        (rc = cmdline->ArgsDone()) != ZX_OK || (rc = OpenReadable(dev, &fd)) != ZX_OK ||
+        (rc = CheckFvmPartition(fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((res = ioctl_block_fvm_shrink(fd, &request)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "ioctl_block_fvm_shrink failed: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    printf("partition shrunk\n");
+    return ZX_OK;
+}
+
+zx_status_t Remove::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK || (rc = CheckFvmPartition(fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((res = ioctl_block_fvm_destroy_partition(fd)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "ioctl_block_fvm_destroy_partition failed: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    printf("partition removed\n");
+    return ZX_OK;
+}
+
+zx_status_t Destroy::Run() {
+    zx_status_t rc;
+    ssize_t res;
+    BlkCtl* cmdline = this->cmdline();
+
+    const char* dev;
+    int fd;
+    if ((rc = cmdline->GetStrArg("device", &dev)) != ZX_OK || (rc = cmdline->ArgsDone()) != ZX_OK ||
+        (rc = OpenReadable(dev, &fd)) != ZX_OK) {
+        return rc;
+    }
+    char path[PATH_MAX];
+    constexpr const char* suffix = "/fvm";
+    ZX_DEBUG_ASSERT(strlen(suffix) < sizeof(path));
+    size_t path_max = sizeof(path) - (strlen(suffix) + 1);
+    if ((res = ioctl_device_get_topo_path(fd, path, path_max)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "failed to get topological path: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    strcat(path, suffix); // Safe to due to size limit above
+    fbl::unique_fd fvm_fd(open(path, O_RDONLY));
+    if ((rc = CheckFvmVolume(fvm_fd.get())) != ZX_OK) {
+        return rc;
+    }
+    fvm_fd.reset();
+    if ((rc = cmdline->Confirm()) != ZX_OK || (rc = ReopenWritable(&fd)) != ZX_OK) {
+        return rc;
+    }
+    if ((rc = fvm_destroy(devname())) != ZX_OK) {
+        fprintf(stderr, "fvm_destroy failed\n");
+        return rc;
+    }
+    if ((res = ioctl_block_rr_part(fd)) < 0) {
+        rc = static_cast<zx_status_t>(res);
+        fprintf(stderr, "failed to unbind FVM driver: %s\n", zx_status_get_string(rc));
+        return rc;
+    }
+    printf("FVM volume metadata destroyed\n");
+    return ZX_OK;
+}
+
+} // namespace fvm
+} // namespace blkctl
diff --git a/system/ulib/blkctl/fvm.h b/system/ulib/blkctl/fvm.h
new file mode 100644
index 0000000..2fc4aa6
--- /dev/null
+++ b/system/ulib/blkctl/fvm.h
@@ -0,0 +1,38 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <blkctl/command.h>
+
+namespace blkctl {
+namespace fvm {
+
+DEFINE_COMMAND(Init);
+DEFINE_COMMAND(Dump);
+DEFINE_COMMAND(Add);
+DEFINE_COMMAND(Query);
+DEFINE_COMMAND(Extend);
+DEFINE_COMMAND(Shrink);
+DEFINE_COMMAND(Remove);
+DEFINE_COMMAND(Destroy);
+
+constexpr const char* kType = "fvm";
+
+constexpr Cmd kCommands[] = {
+    {"init", "<device> <slice_size>", "Format a block device to be an empty FVM volume.", Instantiate<Init>},
+    {"dump", "<device>", "Dump block device and FVM volume information.", Instantiate<Dump>},
+    {"add", "<device> <name> <slices> [type-guid]", "Allocates a new partition in the FVM volume.", Instantiate<Add>},
+    {"query", "<device>", "List ranges of allocated slices.", Instantiate<Query>},
+    {"extend", "<device> <start> <length>", "Allocates slices for the partition.", Instantiate<Extend>},
+    {"shrink", "<device> <start> <length>", "Free slices from the partition.", Instantiate<Shrink>},
+    {"remove", "<device>", "Removes partition from the FVM volume.", Instantiate<Remove>},
+    {"destroy", "<device>", "Overwrites and unbinds an FVM volume.", Instantiate<Destroy>},
+};
+constexpr size_t kNumCommands = sizeof(kCommands) / sizeof(kCommands[0]);
+
+constexpr const char *kDriver = "/boot/driver/fvm.so";
+
+} // namespace fvm
+} // namespace blkctl
diff --git a/system/ulib/blkctl/rules.mk b/system/ulib/blkctl/rules.mk
index 2a61007..ab9d488 100644
--- a/system/ulib/blkctl/rules.mk
+++ b/system/ulib/blkctl/rules.mk
@@ -15,6 +15,7 @@
     $(LOCAL_DIR)/command.cpp \
     $(LOCAL_DIR)/generic.cpp \
     $(LOCAL_DIR)/ramdisk.cpp \
+    $(LOCAL_DIR)/fvm.cpp \
 
 MODULE_LIBS := \
     system/ulib/c \
diff --git a/system/utest/blkctl/fvm.cpp b/system/utest/blkctl/fvm.cpp
new file mode 100644
index 0000000..7efc820
--- /dev/null
+++ b/system/utest/blkctl/fvm.cpp
@@ -0,0 +1,262 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <limits.h>
+#include <stddef.h>
+
+#include <fbl/unique_ptr.h>
+#include <unittest/unittest.h>
+#include <zircon/types.h>
+
+#include "utils.h"
+
+namespace blkctl {
+namespace testing {
+namespace {
+
+bool TestBadCommand(void) {
+    BEGIN_TEST;
+
+    // Missing command
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm"));
+
+    // Gibberish
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm booplesnoot"));
+
+    END_TEST;
+}
+
+bool TestInitDestroy(void) {
+    BEGIN_TEST;
+    ScopedRamdisk ramdisk;
+    ScopedRamdisk nonfvm;
+    ASSERT_TRUE(ramdisk.Init());
+    ASSERT_TRUE(nonfvm.Init());
+
+    // Missing/bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init booplesnoot"));
+
+    // Missing/bad slice size
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s", ramdisk.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s foo", ramdisk.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s -1", ramdisk.path()));
+
+    // Too many args
+    EXPECT_TRUE(
+        ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm init %s %zu foo", ramdisk.path(), kSliceSize));
+
+    // Valid
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm init --force %s %zu", ramdisk.path(), kSliceSize));
+
+    // Missing/bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy booplesnoot"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy %s", nonfvm.path()));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm destroy %s foo", ramdisk.path()));
+
+    // Valid
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm destroy --force %s", ramdisk.path()));
+
+    END_TEST;
+}
+
+bool TestDump(void) {
+    BEGIN_TEST;
+    ScopedFvmPartition partition;
+    ASSERT_TRUE(partition.Init());
+    const ScopedFvmVolume& volume = partition.volume();
+    const ScopedRamdisk& ramdisk = volume.ramdisk();
+
+    // Missing/bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm dump"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm dump booplesnoot"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s", ramdisk.path()));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm dump %s foo", volume.path()));
+
+    // Valid for volume
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm dump %s", volume.path()));
+
+    // Valid for partition
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm dump %s", partition.path()));
+
+    END_TEST;
+}
+
+bool TestAdd(void) {
+    BEGIN_TEST;
+    ScopedFvmVolume volume;
+    ASSERT_TRUE(volume.Init());
+    const ScopedRamdisk& ramdisk = volume.ramdisk();
+
+    // Missing device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add booplesnoot"));
+
+    // Missing/bad name and/or slices
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s", volume.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s foo", volume.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s foo bar", volume.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s foo -1", volume.path()));
+
+    // GUID is optional
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm add --force %s foo %zu", volume.path(),
+                            volume.slices() / 2));
+
+    // Bad GUID
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS,
+                            "blkctl fvm add %s bar %zu 00000000-0000-0000-0000000000000000",
+                            volume.path(), volume.slices()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS,
+                            "blkctl fvm add %s bar %zu thisisno-thex-adec-imal-anditmustbe!",
+                            volume.path(), volume.slices()));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS,
+                            "blkctl fvm add %s bar %zu deadbeef-dead-beef-dead-beefdeadbeef bar",
+                            volume.path(), volume.slices()));
+
+    // Bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm add %s bar %zu", ramdisk.path(),
+                            volume.slices() / 2));
+
+    // Valid
+    EXPECT_TRUE(
+        ParseAndRun(ZX_OK, "blkctl fvm add --force %s bar %zu deadbeef-dead-beef-dead-beefdeadbeef",
+                    volume.path(), volume.slices() / 2));
+
+    END_TEST;
+}
+
+bool TestQuery(void) {
+    BEGIN_TEST;
+    ScopedFvmPartition partition;
+    ASSERT_TRUE(partition.Init());
+    const ScopedFvmVolume& volume = partition.volume();
+
+    // Missing/bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query booplesnoot"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query %s", volume.path()));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm query %s foo", partition.path()));
+
+    // Valid
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm query %s", partition.path()));
+
+    END_TEST;
+}
+
+bool TestExtend(void) {
+    BEGIN_TEST;
+    ScopedFvmPartition partition;
+    ASSERT_TRUE(partition.Init());
+    const ScopedFvmVolume& volume = partition.volume();
+
+    // Missing device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend booplesnoot"));
+
+    // Missing/bad start and/or length
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s", partition.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu", partition.path(),
+                            partition.slices()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s foo 1", partition.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s -1 1", partition.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu foo", partition.path(),
+                            partition.slices()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu -1", partition.path(),
+                            partition.slices()));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu 1 foo", partition.path(),
+                            partition.slices()));
+
+    // Bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm extend %s %zu 1", volume.path(),
+                            partition.slices()));
+
+    // Valid
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm extend --force %s %zu 1", partition.path(),
+                            partition.slices()));
+
+    END_TEST;
+}
+
+bool TestShrink(void) {
+    BEGIN_TEST;
+    ScopedFvmPartition partition;
+    ASSERT_TRUE(partition.Init());
+    const ScopedFvmVolume& volume = partition.volume();
+
+    // Missing device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink booplesnoot"));
+
+    // Missing/bad start and/or length
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s", partition.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu", partition.path(),
+                            partition.slices() - 1));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s foo 1", partition.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s -1 1", partition.path()));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu foo", partition.path(),
+                            partition.slices() - 1));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu -1", partition.path(),
+                            partition.slices() - 1));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu 1 foo", partition.path(),
+                            partition.slices() - 1));
+
+    // Bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm shrink %s %zu 1", volume.path(),
+                            partition.slices() - 1));
+
+    // Valid
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm shrink --force %s %zu 1", partition.path(),
+                            partition.slices() - 1));
+
+    END_TEST;
+}
+
+bool TestRemove(void) {
+    BEGIN_TEST;
+    ScopedFvmPartition partition;
+    ASSERT_TRUE(partition.Init());
+    const ScopedFvmVolume& volume = partition.volume();
+
+    // Missing device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove"));
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove booplesnoot"));
+
+    // Too many args
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove %s foo", partition.path()));
+
+    // Bad device
+    EXPECT_TRUE(ParseAndRun(ZX_ERR_INVALID_ARGS, "blkctl fvm remove %s", volume.path()));
+
+    // Valid
+    EXPECT_TRUE(ParseAndRun(ZX_OK, "blkctl fvm remove --force %s", partition.path()));
+
+    END_TEST;
+}
+
+BEGIN_TEST_CASE(FvmCommandTest)
+RUN_TEST(TestInitDestroy)
+RUN_TEST(TestDump)
+RUN_TEST(TestAdd)
+RUN_TEST(TestQuery)
+RUN_TEST(TestExtend)
+RUN_TEST(TestShrink)
+RUN_TEST(TestRemove)
+END_TEST_CASE(FvmCommandTest)
+
+} // namespace
+} // namespace testing
+} // namespace blkctl
diff --git a/system/utest/blkctl/rules.mk b/system/utest/blkctl/rules.mk
index fcaed07..bd148f2 100644
--- a/system/utest/blkctl/rules.mk
+++ b/system/utest/blkctl/rules.mk
@@ -19,6 +19,7 @@
 MODULE_SRCS += \
     $(LOCAL_DIR)/command.cpp \
     $(LOCAL_DIR)/ramdisk.cpp \
+    $(LOCAL_DIR)/fvm.cpp \
 
 MODULE_LIBS := \
     system/ulib/blkctl \