move bmc code into bmc folder

Signed-off-by: Patrick Venture <venture@google.com>
Change-Id: I2407359461a1a2f0e733ff8cd0cb1744b6304c32
diff --git a/bmc/Makefile.am b/bmc/Makefile.am
new file mode 100644
index 0000000..c46a8cd
--- /dev/null
+++ b/bmc/Makefile.am
@@ -0,0 +1,58 @@
+AM_DEFAULT_SOURCE_EXT = .cpp
+
+noinst_LTLIBRARIES = libfirmwareblob_common.la
+libfirmwareblob_common_la_SOURCES = \
+	$(top_srcdir)/util.cpp \
+	firmware_handler.cpp \
+	file_handler.cpp \
+	$(top_srcdir)/internal/sys.cpp \
+	prepare_systemd.cpp \
+	verify_systemd.cpp \
+	update_systemd.cpp
+
+if ENABLE_LPC_BRIDGE
+libfirmwareblob_common_la_SOURCES += lpc_handler.cpp
+if ENABLE_ASPEED_LPC
+libfirmwareblob_common_la_SOURCES += lpc_aspeed.cpp
+endif
+if ENABLE_NUVOTON_LPC
+libfirmwareblob_common_la_SOURCES += lpc_nuvoton.cpp
+endif
+endif
+
+if ENABLE_PCI_BRIDGE
+if ENABLE_ASPEED_P2A
+libfirmwareblob_common_la_SOURCES += pci_handler.cpp
+endif
+endif
+
+libfirmwareblob_common_la_CXXFLAGS = \
+	-I$(top_srcdir) \
+	$(SDBUSPLUS_CFLAGS) \
+        $(PHOSPHOR_LOGGING_CFLAGS) \
+        $(CODE_COVERAGE_CXXFLAGS) \
+        -flto
+libfirmwareblob_common_la_LDFLAGS = \
+        $(SDBUSPLUS_LIBS) \
+        $(PHOSPHOR_LOGGING_LIBS) \
+        $(CODE_COVERAGE_LIBS) \
+        -lstdc++fs
+
+libfirmwareblobdir = ${libdir}/ipmid-providers
+libfirmwareblob_LTLIBRARIES = libfirmwareblob.la
+libfirmwareblob_la_SOURCES = main.cpp
+libfirmwareblob_la_LIBADD = libfirmwareblob_common.la
+libfirmwareblob_la_LDFLAGS = \
+	$(SDBUSPLUS_LIBS) \
+	$(PHOSPHOR_LOGGING_LIBS) \
+	$(CODE_COVERAGE_LIBS) \
+	-lstdc++fs \
+	-version-info 0:0:0 -shared
+libfirmwareblob_la_CXXFLAGS = \
+	-I$(top_srcdir) \
+	$(SDBUSPLUS_CFLAGS) \
+	$(PHOSPHOR_LOGGING_CFLAGS) \
+	$(CODE_COVERAGE_CXXFLAGS) \
+	-flto
+
+SUBDIRS = . test
diff --git a/bmc/data_handler.hpp b/bmc/data_handler.hpp
new file mode 100644
index 0000000..bf17934
--- /dev/null
+++ b/bmc/data_handler.hpp
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <cstdint>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+/**
+ * Each data transport mechanism must implement the DataInterface.
+ */
+class DataInterface
+{
+  public:
+    virtual ~DataInterface() = default;
+
+    /**
+     * Initialize data transport mechanism.  Calling this should be idempotent
+     * if possible.
+     *
+     * @return true if successful
+     */
+    virtual bool open() = 0;
+
+    /**
+     * Close the data transport mechanism.
+     *
+     * @return true if successful
+     */
+    virtual bool close() = 0;
+
+    /**
+     * Copy bytes from external interface (blocking call).
+     *
+     * @param[in] length - number of bytes to copy
+     * @return the bytes read
+     */
+    virtual std::vector<std::uint8_t> copyFrom(std::uint32_t length) = 0;
+
+    /**
+     * set configuration.
+     *
+     * @param[in] configuration - byte vector of data.
+     * @return bool - returns true on success.
+     */
+    virtual bool writeMeta(const std::vector<std::uint8_t>& configuration) = 0;
+
+    /**
+     * read configuration.
+     *
+     * @return bytes - whatever bytes are required configuration information for
+     * the mechanism.
+     */
+    virtual std::vector<std::uint8_t> readMeta() = 0;
+};
+
+struct DataHandlerPack
+{
+    std::uint16_t bitmask;
+    DataInterface* handler;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/file_handler.cpp b/bmc/file_handler.cpp
new file mode 100644
index 0000000..e81730d
--- /dev/null
+++ b/bmc/file_handler.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "file_handler.hpp"
+
+#include <cstdint>
+#include <filesystem>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+namespace fs = std::filesystem;
+
+bool FileHandler::open(const std::string& path)
+{
+    this->path = path;
+
+    if (file.is_open())
+    {
+        /* This wasn't properly closed somehow.
+         * TODO: Throw an error or just reset the state?
+         */
+        return false;
+    }
+
+    /* using ofstream no need to set out */
+    file.open(filename, std::ios::binary);
+    if (file.bad())
+    {
+        /* TODO: Oh no! Care about this. */
+        return false;
+    }
+
+    /* We were able to open the file for staging.
+     * TODO: We'll need to do other stuff to eventually.
+     */
+    return true;
+}
+
+void FileHandler::close()
+{
+    if (file.is_open())
+    {
+        file.close();
+    }
+    return;
+}
+
+bool FileHandler::write(std::uint32_t offset,
+                        const std::vector<std::uint8_t>& data)
+{
+    if (!file.is_open())
+    {
+        return false;
+    }
+
+    /* We could track this, but if they write in a scattered method, this is
+     * easier.
+     */
+    file.seekp(offset, std::ios_base::beg);
+    if (!file.good())
+    {
+        /* the documentation wasn't super clear on fail vs bad in these cases,
+         * so let's only be happy with goodness.
+         */
+        return false;
+    }
+
+    file.write(reinterpret_cast<const char*>(data.data()), data.size());
+    if (!file.good())
+    {
+        return false;
+    }
+
+    return true;
+}
+
+int FileHandler::getSize()
+{
+    try
+    {
+        return static_cast<int>(fs::file_size(filename));
+    }
+    catch (const fs::filesystem_error& e)
+    {
+    }
+
+    return 0;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/file_handler.hpp b/bmc/file_handler.hpp
new file mode 100644
index 0000000..9cb2819
--- /dev/null
+++ b/bmc/file_handler.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "image_handler.hpp"
+
+#include <cstdint>
+#include <fstream>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+class FileHandler : public ImageHandlerInterface
+{
+  public:
+    /**
+     * Create a FileHandler.  This object is basically a filewriter.
+     *
+     * @param[in] filename - file to use for the contents, fully
+     * qualified file system path.
+     */
+    explicit FileHandler(const std::string& filename) : filename(filename)
+    {
+    }
+
+    bool open(const std::string& path) override;
+    void close() override;
+    bool write(std::uint32_t offset,
+               const std::vector<std::uint8_t>& data) override;
+    int getSize() override;
+
+  private:
+    /** the active hash path, ignore. */
+    std::string path;
+
+    /** The file handle. */
+    std::ofstream file;
+
+    /** The filename (including path) to use to write bytes. */
+    std::string filename;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware_handler.cpp b/bmc/firmware_handler.cpp
new file mode 100644
index 0000000..aaabe83
--- /dev/null
+++ b/bmc/firmware_handler.cpp
@@ -0,0 +1,794 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "firmware_handler.hpp"
+
+#include "image_handler.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <blobs-ipmid/blobs.hpp>
+#include <cstdint>
+#include <cstring>
+#include <fstream>
+#include <memory>
+#include <phosphor-logging/log.hpp>
+#include <string>
+#include <vector>
+
+using namespace phosphor::logging;
+
+namespace ipmi_flash
+{
+
+std::unique_ptr<blobs::GenericBlobInterface>
+    FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        const std::vector<HandlerPack>& firmwares,
+        const std::vector<DataHandlerPack>& transports,
+        std::unique_ptr<TriggerableActionInterface> preparation,
+        std::unique_ptr<TriggerableActionInterface> verification,
+        std::unique_ptr<TriggerableActionInterface> update)
+{
+    /* There must be at least one. */
+    if (!firmwares.size())
+    {
+        log<level::ERR>("Must provide at least one firmware handler.");
+        return nullptr;
+    }
+    if (!transports.size())
+    {
+        return nullptr;
+    }
+
+    std::vector<std::string> blobs;
+    for (const auto& item : firmwares)
+    {
+        blobs.push_back(item.blobName);
+    }
+
+    if (0 == std::count(blobs.begin(), blobs.end(), hashBlobId))
+    {
+        return nullptr;
+    }
+
+    std::uint16_t bitmask = 0;
+    for (const auto& item : transports)
+    {
+        /* TODO: can use std::accumulate() unless I'm mistaken. :D */
+        bitmask |= item.bitmask;
+    }
+
+    return std::make_unique<FirmwareBlobHandler>(
+        firmwares, blobs, transports, bitmask, std::move(preparation),
+        std::move(verification), std::move(update));
+}
+
+/* Check if the path is in our supported list (or active list). */
+bool FirmwareBlobHandler::canHandleBlob(const std::string& path)
+{
+    return (std::count(blobIDs.begin(), blobIDs.end(), path) > 0);
+}
+
+/*
+ * Grab the list of supported firmware.
+ *
+ * If there's an open firmware session, it'll already be present in the
+ * list as "/flash/active/image", and if the hash has started,
+ * "/flash/active/hash" regardless of mechanism.  This is done in the open
+ * comamnd, no extra work is required here.
+ */
+std::vector<std::string> FirmwareBlobHandler::getBlobIds()
+{
+    return blobIDs;
+}
+
+/*
+ * Per the design, this mean abort, and this will trigger whatever
+ * appropriate actions are required to abort the process.
+ */
+bool FirmwareBlobHandler::deleteBlob(const std::string& path)
+{
+    /* This cannot be called if you have an open session to the path.
+     * You can have an open session to verify/update/hash/image, but not active*
+     *
+     * Therefore, if this is called, it's either on a blob that isn't presently
+     * open.  However, there could be open blobs, so we need to close all open
+     * sessions. This closing on our is an invalid handler behavior.  Therefore,
+     * we cannot close an active session.  To enforce this, we only allow
+     * deleting if there isn't a file open.
+     */
+    if (fileOpen)
+    {
+        return false;
+    }
+
+    /* only includes states where fileOpen == false */
+    switch (state)
+    {
+        case UpdateState::notYetStarted:
+            /* Trying to delete anything at this point has no effect and returns
+             * false.
+             */
+            return false;
+        case UpdateState::verificationPending:
+            abortProcess();
+            return true;
+        case UpdateState::updatePending:
+            abortProcess();
+            return true;
+        default:
+            break;
+    }
+
+    return false;
+}
+
+/*
+ * Stat on the files will return information such as what supported
+ * transport mechanisms are available.
+ *
+ * Stat on an active file or hash will return information such as the size
+ * of the data cached, and any additional pertinent information.  The
+ * blob_state on the active files will return the state of the update.
+ */
+bool FirmwareBlobHandler::stat(const std::string& path, blobs::BlobMeta* meta)
+{
+    /* We know we support this path because canHandle is called ahead */
+    if (path == verifyBlobId || path == activeImageBlobId ||
+        path == activeHashBlobId || path == updateBlobId)
+    {
+        /* These blobs are placeholders that indicate things, or allow actions,
+         * but are not stat-able as-is.
+         */
+        return false;
+    }
+
+    /* They are requesting information about the generic blob_id. */
+    meta->blobState = bitmask;
+    meta->size = 0;
+
+    /* The generic blob_ids state is only the bits related to the transport
+     * mechanisms.
+     */
+    return true;
+}
+
+ActionStatus FirmwareBlobHandler::getActionStatus()
+{
+    ActionStatus value = ActionStatus::unknown;
+
+    switch (state)
+    {
+        case UpdateState::verificationPending:
+            value = ActionStatus::unknown;
+            break;
+        case UpdateState::verificationStarted:
+            value = verification->status();
+            lastVerificationStatus = value;
+            break;
+        case UpdateState::verificationCompleted:
+            value = lastVerificationStatus;
+            break;
+        case UpdateState::updatePending:
+            value = ActionStatus::unknown;
+            break;
+        case UpdateState::updateStarted:
+            value = update->status();
+            lastUpdateStatus = value;
+            break;
+        case UpdateState::updateCompleted:
+            value = lastUpdateStatus;
+            break;
+        default:
+            break;
+    }
+
+    return value;
+}
+
+/*
+ * Return stat information on an open session.  It therefore must be an active
+ * handle to either the active image or active hash.
+ */
+bool FirmwareBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+    auto item = lookup.find(session);
+    if (item == lookup.end())
+    {
+        return false;
+    }
+
+    /* The size here refers to the size of the file -- of something analagous.
+     */
+    meta->size = (item->second->imageHandler)
+                     ? item->second->imageHandler->getSize()
+                     : 0;
+
+    meta->metadata.clear();
+
+    if (item->second->activePath == verifyBlobId ||
+        item->second->activePath == updateBlobId)
+    {
+        ActionStatus value = getActionStatus();
+
+        meta->metadata.push_back(static_cast<std::uint8_t>(value));
+
+        /* Change the firmware handler's state and the blob's stat value
+         * depending.
+         */
+        if (value == ActionStatus::success || value == ActionStatus::failed)
+        {
+            if (item->second->activePath == verifyBlobId)
+            {
+                changeState(UpdateState::verificationCompleted);
+            }
+            else
+            {
+                /* item->second->activePath == updateBlobId */
+                changeState(UpdateState::updateCompleted);
+            }
+
+            item->second->flags &= ~blobs::StateFlags::committing;
+
+            if (value == ActionStatus::success)
+            {
+                item->second->flags |= blobs::StateFlags::committed;
+            }
+            else
+            {
+                item->second->flags |= blobs::StateFlags::commit_error;
+            }
+        }
+    }
+
+    /* The blobState here relates to an active sesion, so we should return the
+     * flags used to open this session.
+     */
+    meta->blobState = item->second->flags;
+
+    /* The metadata blob returned comes from the data handler... it's used for
+     * instance, in P2A bridging to get required information about the mapping,
+     * and is the "opposite" of the lpc writemeta requirement.
+     */
+    if (item->second->dataHandler)
+    {
+        auto bytes = item->second->dataHandler->readMeta();
+        meta->metadata.insert(meta->metadata.begin(), bytes.begin(),
+                              bytes.end());
+    }
+
+    return true;
+}
+
+/*
+ * If you open /flash/image or /flash/tarball, or /flash/hash it will
+ * interpret the open flags and perform whatever actions are required for
+ * that update process.  The session returned can be used immediately for
+ * sending data down, without requiring one to open the new active file.
+ *
+ * If you open the active flash image or active hash it will let you
+ * overwrite pieces, depending on the state.
+ *
+ * Once the verification process has started the active files cannot be
+ * opened.
+ *
+ * You can only have one open session at a time.  Which means, you can only
+ * have one file open at a time.  Trying to open the hash blob_id while you
+ * still have the flash image blob_id open will fail.  Opening the flash
+ * blob_id when it is already open will fail.
+ */
+bool FirmwareBlobHandler::open(uint16_t session, uint16_t flags,
+                               const std::string& path)
+{
+    /* Is there an open session already? We only allow one at a time.
+     *
+     * Further on this, if there's an active session to the hash we don't allow
+     * re-opening the image, and if we have the image open, we don't allow
+     * opening the hash.  This design decision may be re-evaluated, and changed
+     * to only allow one session per object type (of the two types).  But,
+     * consider if the hash is open, do we want to allow writing to the image?
+     * And why would we?  But, really, the point of no-return is once the
+     * verification process has begun -- which is done via commit() on the hash
+     * blob_id, we no longer want to allow updating the contents.
+     */
+    if (fileOpen)
+    {
+        return false;
+    }
+
+    /* The active blobs are only meant to indicate status that something has
+     * opened the image file or the hash file.
+     */
+    if (path == activeImageBlobId || path == activeHashBlobId)
+    {
+        /* 2a) are they opening the active image? this can only happen if they
+         * already started one (due to canHandleBlob's behavior).
+         */
+        /* 2b) are they opening the active hash? this can only happen if they
+         * already started one (due to canHandleBlob's behavior).
+         */
+        return false;
+    }
+
+    /* Check that they've opened for writing - read back not currently
+     * supported.
+     */
+    if ((flags & blobs::OpenFlags::write) == 0)
+    {
+        return false;
+    }
+
+    /* Because canHandleBlob is called before open, we know that if they try to
+     *  open the verifyBlobId, they're in a state where it's present.
+     */
+
+    switch (state)
+    {
+        case UpdateState::uploadInProgress:
+            /* Unreachable code because if it's started a file is open. */
+            break;
+        case UpdateState::verificationPending:
+            /* Handle opening the verifyBlobId --> we know the image and hash
+             * aren't open because of the fileOpen check.  They can still open
+             * other files from this state to transition back into
+             * uploadInProgress.
+             *
+             * The file must be opened for writing, but no transport mechanism
+             * specified since it's irrelevant.
+             */
+            if (path == verifyBlobId)
+            {
+                verifyImage.flags = flags;
+
+                lookup[session] = &verifyImage;
+
+                fileOpen = true;
+                return true;
+            }
+            break;
+        case UpdateState::verificationStarted:
+        case UpdateState::verificationCompleted:
+            /* Unreachable code because if it's started a file is open. */
+            return false;
+        case UpdateState::updatePending:
+        {
+            /* When in this state, they can only open the updateBlobId */
+            if (path == updateBlobId)
+            {
+                updateImage.flags = flags;
+
+                lookup[session] = &updateImage;
+
+                fileOpen = true;
+                return true;
+            }
+            else
+            {
+                return false;
+            }
+        }
+        case UpdateState::updateStarted:
+        case UpdateState::updateCompleted:
+            /* Unreachable code because if it's started a file is open. */
+            break;
+        default:
+            break;
+    }
+
+    /* There are two abstractions at play, how you get the data and how you
+     * handle that data. such that, whether the data comes from the PCI bridge
+     * or LPC bridge is not connected to whether the data goes into a static
+     * layout flash update or a UBI tarball.
+     */
+
+    /* Check the flags for the transport mechanism: if none match we don't
+     * support what they request.
+     */
+    if ((flags & bitmask) == 0)
+    {
+        return false;
+    }
+
+    /* How are they expecting to copy this data? */
+    auto d = std::find_if(
+        transports.begin(), transports.end(),
+        [&flags](const auto& iter) { return (iter.bitmask & flags); });
+    if (d == transports.end())
+    {
+        return false;
+    }
+
+    /* We found the transport handler they requested, no surprise since
+     * above we verify they selected at least one we wanted.
+     */
+
+    /* Elsewhere I do this check by checking "if ::ipmi" because that's the
+     * only non-external data pathway -- but this is just a more generic
+     * approach to that.
+     */
+    if (d->handler)
+    {
+        /* If the data handler open call fails, open fails. */
+        if (!d->handler->open())
+        {
+            return false;
+        }
+    }
+
+    /* Do we have a file handler for the type of file they're opening.
+     * Note: This should only fail if something is somehow crazy wrong.
+     * Since the canHandle() said yes, and that's tied into the list of explicit
+     * firmware handers (and file handlers, like this'll know where to write the
+     * tarball, etc).
+     */
+    auto h = std::find_if(
+        handlers.begin(), handlers.end(),
+        [&path](const auto& iter) { return (iter.blobName == path); });
+    if (h == handlers.end())
+    {
+        return false;
+    }
+
+    /* Ok, so we found a handler that matched, so call open() */
+    if (!h->handler->open(path))
+    {
+        return false;
+    }
+
+    Session* curr;
+    const std::string* active;
+
+    if (path == hashBlobId)
+    {
+        /* 2c) are they opening the /flash/hash ? (to start the process) */
+        curr = &activeHash;
+        active = &activeHashBlobId;
+    }
+    else
+    {
+        curr = &activeImage;
+        active = &activeImageBlobId;
+    }
+
+    curr->flags = flags;
+    curr->dataHandler = d->handler;
+    curr->imageHandler = h->handler;
+
+    lookup[session] = curr;
+
+    addBlobId(*active);
+    removeBlobId(verifyBlobId);
+
+    changeState(UpdateState::uploadInProgress);
+    fileOpen = true;
+
+    return true;
+}
+
+/**
+ * The write command really just grabs the data from wherever it is and sends it
+ * to the image handler.  It's the image handler's responsibility to deal with
+ * the data provided.
+ *
+ * This receives a session from the blob manager, therefore it is always called
+ * between open() and close().
+ */
+bool FirmwareBlobHandler::write(uint16_t session, uint32_t offset,
+                                const std::vector<uint8_t>& data)
+{
+    auto item = lookup.find(session);
+    if (item == lookup.end())
+    {
+        return false;
+    }
+
+    /* Prevent writing during verification. */
+    if (state == UpdateState::verificationStarted)
+    {
+        return false;
+    }
+
+    /* Prevent writing to the verification or update blobs. */
+    if (item->second->activePath == verifyBlobId ||
+        item->second->activePath == updateBlobId)
+    {
+        return false;
+    }
+
+    std::vector<std::uint8_t> bytes;
+
+    if (item->second->flags & UpdateFlags::ipmi)
+    {
+        bytes = data;
+    }
+    else
+    {
+        /* little endian required per design, and so on, but TODO: do endianness
+         * with boost.
+         */
+        struct ExtChunkHdr header;
+
+        if (data.size() != sizeof(header))
+        {
+            return false;
+        }
+
+        std::memcpy(&header, data.data(), data.size());
+        bytes = item->second->dataHandler->copyFrom(header.length);
+    }
+
+    return item->second->imageHandler->write(offset, bytes);
+}
+
+/*
+ * If the active session (image or hash) is over LPC, this allows
+ * configuring it.  This option is only available before you start
+ * writing data for the given item (image or hash).  This will return
+ * false at any other part. -- the lpc handler portion will know to return
+ * false.
+ */
+bool FirmwareBlobHandler::writeMeta(uint16_t session, uint32_t offset,
+                                    const std::vector<uint8_t>& data)
+{
+    auto item = lookup.find(session);
+    if (item == lookup.end())
+    {
+        return false;
+    }
+
+    if (item->second->flags & UpdateFlags::ipmi)
+    {
+        return false;
+    }
+
+    /* Prevent writing meta to the verification blob (it has no data handler).
+     */
+    if (item->second->dataHandler)
+    {
+        return item->second->dataHandler->writeMeta(data);
+    }
+
+    return false;
+}
+
+/*
+ * If this command is called on the session for the verifyBlobId, it'll
+ * trigger a systemd service `verify_image.service` to attempt to verify
+ * the image.
+ *
+ * For this file to have opened, the other two must be closed, which means any
+ * out-of-band transport mechanism involved is closed.
+ */
+bool FirmwareBlobHandler::commit(uint16_t session,
+                                 const std::vector<uint8_t>& data)
+{
+    auto item = lookup.find(session);
+    if (item == lookup.end())
+    {
+        return false;
+    }
+
+    /* You can only commit on the verifyBlodId or updateBlobId */
+    if (item->second->activePath != verifyBlobId &&
+        item->second->activePath != updateBlobId)
+    {
+        std::fprintf(stderr, "path: '%s' not expected for commit\n",
+                     item->second->activePath.c_str());
+        return false;
+    }
+
+    switch (state)
+    {
+        case UpdateState::verificationPending:
+            /* Set state to committing. */
+            item->second->flags |= blobs::StateFlags::committing;
+            return triggerVerification();
+        case UpdateState::verificationStarted:
+            /* Calling repeatedly has no effect within an update process. */
+            return true;
+        case UpdateState::verificationCompleted:
+            /* Calling after the verification process has completed returns
+             * failure. */
+            return false;
+        case UpdateState::updatePending:
+            item->second->flags |= blobs::StateFlags::committing;
+            return triggerUpdate();
+        case UpdateState::updateStarted:
+            /* Calling repeatedly has no effect within an update process. */
+            return true;
+        default:
+            return false;
+    }
+}
+
+/*
+ * Close must be called on the firmware image before triggering
+ * verification via commit. Once the verification is complete, you can
+ * then close the hash file.
+ *
+ * If the `verify_image.service` returned success, closing the hash file
+ * will have a specific behavior depending on the update. If it's UBI,
+ * it'll perform the install. If it's static layout, it'll do nothing. The
+ * verify_image service in the static layout case is responsible for placing
+ * the file in the correct staging position.
+ */
+bool FirmwareBlobHandler::close(uint16_t session)
+{
+    auto item = lookup.find(session);
+    if (item == lookup.end())
+    {
+        return false;
+    }
+
+    switch (state)
+    {
+        case UpdateState::uploadInProgress:
+            /* They are closing a data pathway (image, tarball, hash). */
+            changeState(UpdateState::verificationPending);
+
+            /* Add verify blob ID now that we can expect it. */
+            addBlobId(verifyBlobId);
+            break;
+        case UpdateState::verificationPending:
+            /* They haven't triggered, therefore closing is uninteresting.
+             */
+            break;
+        case UpdateState::verificationStarted:
+            /* Abort without checking to see if it happened to finish. Require
+             * the caller to stat() deliberately.
+             */
+            abortVerification();
+            abortProcess();
+            break;
+        case UpdateState::verificationCompleted:
+            if (lastVerificationStatus == ActionStatus::success)
+            {
+                changeState(UpdateState::updatePending);
+                addBlobId(updateBlobId);
+                removeBlobId(verifyBlobId);
+            }
+            else
+            {
+                /* Verification failed, and the host-tool knows this by calling
+                 * stat(), which triggered the state change to
+                 * verificationCompleted.
+                 *
+                 * Therefore, let's abort the process at this point.
+                 */
+                abortProcess();
+            }
+            break;
+        case UpdateState::updatePending:
+            /* They haven't triggered the update, therefore this is
+             * uninteresting. */
+            break;
+        case UpdateState::updateStarted:
+            /* Abort without checking to see if it happened to finish. Require
+             * the caller to stat() deliberately.
+             */
+            abortUpdate();
+            abortProcess();
+            break;
+        case UpdateState::updateCompleted:
+            if (lastUpdateStatus == ActionStatus::failed)
+            {
+                /* TODO: lOG something? */
+                std::fprintf(stderr, "Update failed\n");
+            }
+
+            abortProcess();
+            break;
+        default:
+            break;
+    }
+
+    if (item->second->dataHandler)
+    {
+        item->second->dataHandler->close();
+    }
+    if (item->second->imageHandler)
+    {
+        item->second->imageHandler->close();
+    }
+
+    lookup.erase(item);
+    fileOpen = false;
+    return true;
+}
+
+void FirmwareBlobHandler::changeState(UpdateState next)
+{
+    state = next;
+
+    if (state == UpdateState::notYetStarted)
+    {
+        /* Going back to notyetstarted, let them trigger preparation again. */
+        preparationTriggered = false;
+    }
+    else if (state == UpdateState::uploadInProgress)
+    {
+        /* Store this transition logic here instead of ::open() */
+        if (!preparationTriggered)
+        {
+            preparation->trigger();
+            preparationTriggered = true;
+        }
+    }
+}
+
+bool FirmwareBlobHandler::expire(uint16_t session)
+{
+    return false;
+}
+
+/*
+ * Currently, the design does not provide this with a function, however,
+ * it will likely change to support reading data back.
+ */
+std::vector<uint8_t> FirmwareBlobHandler::read(uint16_t session,
+                                               uint32_t offset,
+                                               uint32_t requestedSize)
+{
+    return {};
+}
+
+void FirmwareBlobHandler::abortProcess()
+{
+    /* Closing of open files is handled from close() -- Reaching here from
+     * delete may never be supported.
+     */
+    removeBlobId(verifyBlobId);
+    removeBlobId(updateBlobId);
+    removeBlobId(activeImageBlobId);
+    removeBlobId(activeHashBlobId);
+
+    changeState(UpdateState::notYetStarted);
+}
+
+void FirmwareBlobHandler::abortVerification()
+{
+    verification->abort();
+}
+
+bool FirmwareBlobHandler::triggerVerification()
+{
+    bool result = verification->trigger();
+    if (result)
+    {
+        changeState(UpdateState::verificationStarted);
+    }
+
+    return result;
+}
+
+void FirmwareBlobHandler::abortUpdate()
+{
+    update->abort();
+}
+
+bool FirmwareBlobHandler::triggerUpdate()
+{
+    bool result = update->trigger();
+    if (result)
+    {
+        changeState(UpdateState::updateStarted);
+    }
+
+    return result;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware_handler.hpp b/bmc/firmware_handler.hpp
new file mode 100644
index 0000000..8c7cb04
--- /dev/null
+++ b/bmc/firmware_handler.hpp
@@ -0,0 +1,244 @@
+#pragma once
+
+#include "config.h"
+
+#include "data_handler.hpp"
+#include "image_handler.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <blobs-ipmid/blobs.hpp>
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+/**
+ * Representation of a session, includes how to read/write data.
+ */
+struct Session
+{
+    /**
+     * Built a session object.
+     *
+     * @param[in] the active path to which this corresponds.
+     */
+    explicit Session(const std::string& path) :
+        dataHandler(nullptr), imageHandler(nullptr), flags(0), activePath(path)
+    {
+    }
+
+    /**
+     * Pointer to the correct Data handler interface. (nullptr on BT (or KCS))
+     */
+    DataInterface* dataHandler;
+
+    /**
+     * Pointer to the correct image handler interface.  (nullptr on hash
+     * blob_id)
+     */
+    ImageHandlerInterface* imageHandler;
+
+    /** The flags used to open the session. */
+    std::uint16_t flags;
+
+    /** The active path. */
+    std::string activePath;
+};
+
+struct ExtChunkHdr
+{
+    std::uint32_t length; /* Length of the data queued (little endian). */
+} __attribute__((packed));
+
+/**
+ * Register only one firmware blob handler that will manage all sessions.
+ */
+class FirmwareBlobHandler : public blobs::GenericBlobInterface
+{
+  public:
+    enum UpdateFlags : std::uint16_t
+    {
+        openRead = (1 << 0),  /* Flag for reading. */
+        openWrite = (1 << 1), /* Flag for writing. */
+        ipmi = (1 << 8), /* Expect to send contents over IPMI BlockTransfer. */
+        p2a = (1 << 9),  /* Expect to send contents over P2A bridge. */
+        lpc = (1 << 10), /* Expect to send contents over LPC bridge. */
+    };
+
+    /** The state of the firmware update process. */
+    enum class UpdateState
+    {
+        /** The initial state. */
+        notYetStarted = 0,
+        /** The BMC is expecting to receive bytes. */
+        uploadInProgress,
+        /** The BMC is ready for verification or more bytes. */
+        verificationPending,
+        /** The verification process has started, no more writes allowed. */
+        verificationStarted,
+        /** The verification process has completed. */
+        verificationCompleted,
+        /** The update process is pending. */
+        updatePending,
+        /** The update process has started. */
+        updateStarted,
+        /** The update has completed (optional state to reach) */
+        updateCompleted,
+    };
+
+    /**
+     * Create a FirmwareBlobHandler.
+     *
+     * @param[in] firmwares - list of firmware blob_ids to support.
+     * @param[in] transports - list of transports to support.
+     * @param[in] verification - pointer to object for triggering verification
+     * @param[in] update - point to object for triggering the update
+     */
+    static std::unique_ptr<blobs::GenericBlobInterface>
+        CreateFirmwareBlobHandler(
+            const std::vector<HandlerPack>& firmwares,
+            const std::vector<DataHandlerPack>& transports,
+            std::unique_ptr<TriggerableActionInterface> preparation,
+            std::unique_ptr<TriggerableActionInterface> verification,
+            std::unique_ptr<TriggerableActionInterface> update);
+
+    /**
+     * Create a FirmwareBlobHandler.
+     *
+     * @param[in] firmwares - list of firmware types and their handlers
+     * @param[in] blobs - list of blobs_ids to support
+     * @param[in] transports - list of transport types and their handlers
+     * @param[in] bitmask - bitmask of transports to support
+     * @param[in] verification - pointer to object for triggering verification
+     * @param[in] update - point to object for triggering the update
+     */
+    FirmwareBlobHandler(
+        const std::vector<HandlerPack>& firmwares,
+        const std::vector<std::string>& blobs,
+        const std::vector<DataHandlerPack>& transports, std::uint16_t bitmask,
+        std::unique_ptr<TriggerableActionInterface> preparation,
+        std::unique_ptr<TriggerableActionInterface> verification,
+        std::unique_ptr<TriggerableActionInterface> update) :
+        handlers(firmwares),
+        blobIDs(blobs), transports(transports), bitmask(bitmask),
+        activeImage(activeImageBlobId), activeHash(activeHashBlobId),
+        verifyImage(verifyBlobId), updateImage(updateBlobId), lookup(),
+        state(UpdateState::notYetStarted), preparation(std::move(preparation)),
+        verification(std::move(verification)), update(std::move(update))
+    {
+    }
+    ~FirmwareBlobHandler() = default;
+    FirmwareBlobHandler(const FirmwareBlobHandler&) = delete;
+    FirmwareBlobHandler& operator=(const FirmwareBlobHandler&) = delete;
+    FirmwareBlobHandler(FirmwareBlobHandler&&) = default;
+    FirmwareBlobHandler& operator=(FirmwareBlobHandler&&) = default;
+
+    bool canHandleBlob(const std::string& path) override;
+    std::vector<std::string> getBlobIds() override;
+    bool deleteBlob(const std::string& path) override;
+    bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+    bool open(uint16_t session, uint16_t flags,
+              const std::string& path) override;
+    std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+                              uint32_t requestedSize) override;
+    bool write(uint16_t session, uint32_t offset,
+               const std::vector<uint8_t>& data) override;
+    bool writeMeta(uint16_t session, uint32_t offset,
+                   const std::vector<uint8_t>& data) override;
+    bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+    bool close(uint16_t session) override;
+    bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+    bool expire(uint16_t session) override;
+
+    void abortProcess();
+
+    void abortVerification();
+    bool triggerVerification();
+    void abortUpdate();
+    bool triggerUpdate();
+
+    /** Allow grabbing the current state. */
+    UpdateState getCurrentState() const
+    {
+        return state;
+    };
+
+    /** Provide for any state change triggers in convenience handler. */
+    void changeState(UpdateState next);
+
+  private:
+    void addBlobId(const std::string& blob)
+    {
+        auto blobIdMatch = std::find_if(
+            blobIDs.begin(), blobIDs.end(),
+            [&blob](const std::string& iter) { return (iter == blob); });
+        if (blobIdMatch == blobIDs.end())
+        {
+            blobIDs.push_back(blob);
+        }
+    }
+
+    void removeBlobId(const std::string& blob)
+    {
+        blobIDs.erase(std::remove(blobIDs.begin(), blobIDs.end(), blob),
+                      blobIDs.end());
+    }
+
+    ActionStatus getVerifyStatus();
+    ActionStatus getActionStatus();
+
+    /** List of handlers by type. */
+    std::vector<HandlerPack> handlers;
+
+    /** Active list of blobIDs. */
+    std::vector<std::string> blobIDs;
+
+    /** List of handlers by transport type. */
+    std::vector<DataHandlerPack> transports;
+
+    /** The bits set indicate what transport mechanisms are supported. */
+    std::uint16_t bitmask;
+
+    /** Active image session. */
+    Session activeImage;
+
+    /** Active hash session. */
+    Session activeHash;
+
+    /** Session for verification. */
+    Session verifyImage;
+
+    /** Session for update. */
+    Session updateImage;
+
+    /** A quick method for looking up a session's mechanisms and details. */
+    std::map<std::uint16_t, Session*> lookup;
+
+    /** The firmware update state. */
+    UpdateState state;
+
+    /* preparation is triggered once we go into uploadInProgress(), but only
+     * once per full cycle, going back to notYetStarted resets this.
+     */
+    bool preparationTriggered = false;
+    std::unique_ptr<TriggerableActionInterface> preparation;
+
+    std::unique_ptr<TriggerableActionInterface> verification;
+
+    std::unique_ptr<TriggerableActionInterface> update;
+
+    /** Temporary variable to track whether a blob is open. */
+    bool fileOpen = false;
+
+    ActionStatus lastVerificationStatus = ActionStatus::unknown;
+
+    ActionStatus lastUpdateStatus = ActionStatus::unknown;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/image_handler.hpp b/bmc/image_handler.hpp
new file mode 100644
index 0000000..b7d682b
--- /dev/null
+++ b/bmc/image_handler.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+/**
+ * Each image update mechanism must implement the ImageHandlerInterface.
+ */
+class ImageHandlerInterface
+{
+  public:
+    virtual ~ImageHandlerInterface() = default;
+
+    /**
+     * open the firmware update mechanism.
+     *
+     * @param[in] path - the path passed to the handler (the blob_id).
+     * @return bool - returns true on success.
+     */
+    virtual bool open(const std::string& path) = 0;
+
+    /**
+     * close the image.
+     */
+    virtual void close() = 0;
+
+    /**
+     * write data to the staged file.
+     *
+     * @param[in] offset - 0-based offset into the file.
+     * @param[in] data - the data to write.
+     * @return bool - returns true on success.
+     */
+    virtual bool write(std::uint32_t offset,
+                       const std::vector<std::uint8_t>& data) = 0;
+
+    /**
+     * return the size of the file (if that notion makes sense).
+     *
+     * @return the size in bytes of the image staged.
+     */
+    virtual int getSize() = 0;
+};
+
+struct HandlerPack
+{
+    std::string blobName;
+    ImageHandlerInterface* handler;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/lpc_aspeed.cpp b/bmc/lpc_aspeed.cpp
new file mode 100644
index 0000000..364aff5
--- /dev/null
+++ b/bmc/lpc_aspeed.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "lpc_aspeed.hpp"
+
+#include "mapper_errors.hpp"
+#include "window_hw_interface.hpp"
+
+#include <fcntl.h>
+#include <linux/aspeed-lpc-ctrl.h>
+#include <linux/kernel.h>
+
+#include <cerrno>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+
+namespace ipmi_flash
+{
+
+const std::string LpcMapperAspeed::lpcControlPath = "/dev/aspeed-lpc-ctrl";
+
+std::unique_ptr<HardwareMapperInterface>
+    LpcMapperAspeed::createAspeedMapper(std::uint32_t regionAddress,
+                                        std::size_t regionSize)
+{
+    /* NOTE: considered using a joint factory to create one or the other, for
+     * now, separate factories.
+     */
+    return std::make_unique<LpcMapperAspeed>(regionAddress, regionSize);
+}
+
+void LpcMapperAspeed::close()
+{
+    if (mappedRegion)
+    {
+        sys->munmap(mappedRegion, regionSize);
+        mappedRegion = nullptr;
+    }
+
+    if (mappedFd != -1)
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+    }
+}
+
+WindowMapResult LpcMapperAspeed::mapWindow(std::uint32_t address,
+                                           std::uint32_t length)
+{
+    WindowMapResult result = {};
+    static const std::uint32_t MASK_64K = 0xFFFFU;
+    const std::uint32_t offset = address & MASK_64K;
+
+    if (offset + length > regionSize)
+    {
+        std::fprintf(stderr,
+                     "requested window size %" PRIu32 ", offset %#" PRIx32
+                     " is too large for mem region"
+                     " of size %zu\n",
+                     length, offset, regionSize);
+
+        result.response = EFBIG;
+        result.windowSize = regionSize - offset;
+        return result;
+    }
+
+    struct aspeed_lpc_ctrl_mapping map = {
+        .window_type = ASPEED_LPC_CTRL_WINDOW_MEMORY,
+        .window_id = 0,
+        .flags = 0,
+        .addr = address & ~MASK_64K,
+        .offset = 0,
+        .size = __ALIGN_KERNEL_MASK(offset + length, MASK_64K),
+    };
+
+    std::fprintf(stderr,
+                 "requesting Aspeed LPC window at %#" PRIx32 " of size %" PRIu32
+                 "\n",
+                 map.addr, map.size);
+
+    const auto lpcControlFd = sys->open(lpcControlPath.c_str(), O_RDWR);
+    if (lpcControlFd == -1)
+    {
+        std::fprintf(stderr,
+                     "cannot open Aspeed LPC kernel control dev \"%s\"\n",
+                     lpcControlPath.c_str());
+
+        result.response = EINVAL;
+        return result;
+    }
+
+    if (sys->ioctl(lpcControlFd, ASPEED_LPC_CTRL_IOCTL_MAP, &map) == -1)
+    {
+        std::fprintf(stderr, "Failed to ioctl Aspeed LPC map with error %s\n",
+                     std::strerror(errno));
+        sys->close(lpcControlFd);
+
+        result.response = EINVAL;
+        return result;
+    }
+
+    sys->close(lpcControlFd);
+
+    result.response = 0;
+    result.windowOffset = offset;
+    result.windowSize = length;
+    return result;
+}
+
+MemorySet LpcMapperAspeed::open()
+{
+    if (mapRegion())
+    {
+        MemorySet output;
+        output.mappedFd = mappedFd;
+        output.mapped = mappedRegion;
+        return output;
+    }
+
+    throw MapperException("Unable to memory-map region");
+}
+
+bool LpcMapperAspeed::mapRegion()
+{
+    /* Open the file to map. */
+    mappedFd = sys->open(lpcControlPath.c_str(), O_RDONLY | O_SYNC);
+    if (mappedFd == -1)
+    {
+        std::fprintf(stderr, "ipmiflash: unable to open %s\n",
+                     lpcControlPath.c_str());
+        return false;
+    }
+
+    mappedRegion = reinterpret_cast<uint8_t*>(
+        sys->mmap(0, regionSize, PROT_READ, MAP_SHARED, mappedFd, 0));
+
+    if (mappedRegion == MAP_FAILED)
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+        std::fprintf(stderr, "Mmap failure: '%s'\n", std::strerror(errno));
+        return false;
+    }
+
+    /* TOOD: There is no close() method here, to close mappedFd, or mappedRegion
+     * -- therefore, a good next step will be to evaluate whether or not the
+     * other pieces should go here...
+     */
+    return true;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/lpc_aspeed.hpp b/bmc/lpc_aspeed.hpp
new file mode 100644
index 0000000..ac87c22
--- /dev/null
+++ b/bmc/lpc_aspeed.hpp
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "internal/sys.hpp"
+#include "window_hw_interface.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+class LpcMapperAspeed : public HardwareMapperInterface
+{
+  public:
+    static std::unique_ptr<HardwareMapperInterface>
+        createAspeedMapper(std::uint32_t regionAddress, std::size_t regionSize);
+
+    /* NOTE: This object is created and then never destroyed (unless ipmid
+     * stops/crashes, etc)
+     */
+    LpcMapperAspeed(std::uint32_t regionAddress, std::size_t regionSize,
+                    const internal::Sys* sys = &internal::sys_impl) :
+        regionAddress(regionAddress),
+        regionSize(regionSize), sys(sys){};
+
+    LpcMapperAspeed(const LpcMapperAspeed&) = delete;
+    LpcMapperAspeed& operator=(const LpcMapperAspeed&) = delete;
+    LpcMapperAspeed(LpcMapperAspeed&&) = default;
+    LpcMapperAspeed& operator=(LpcMapperAspeed&&) = default;
+
+    /* throws MapperException */
+    MemorySet open() override;
+
+    void close() override;
+
+    WindowMapResult mapWindow(std::uint32_t address,
+                              std::uint32_t length) override;
+
+    /**
+     * Attempt to mmap the region.
+     *
+     * @return true on success, false otherwise.
+     */
+    bool mapRegion();
+
+  private:
+    static const std::string lpcControlPath;
+    int mappedFd = -1;
+    std::uint8_t* mappedRegion = nullptr;
+    std::uint32_t regionAddress;
+    std::size_t regionSize;
+    const internal::Sys* sys;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/lpc_handler.cpp b/bmc/lpc_handler.cpp
new file mode 100644
index 0000000..e3a5e08
--- /dev/null
+++ b/bmc/lpc_handler.cpp
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "lpc_handler.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+bool LpcDataHandler::open()
+{
+    /* For the ASPEED LPC CTRL driver, the ioctl is required to set up the
+     * window, with information from writeMeta() below.
+     */
+    return true;
+}
+
+bool LpcDataHandler::close()
+{
+    mapper->close();
+
+    return setInitializedAndReturn(false);
+}
+
+std::vector<std::uint8_t> LpcDataHandler::copyFrom(std::uint32_t length)
+{
+    /* TODO: implement this -- in an earlier and different version of this that
+     * didn't use BLOBs, the region was memory-mapped and the writes to the data
+     * were just done directly from the memory-mapped region instead of a
+     * copyFrom() first call.  The idea with this change is that we may not be
+     * able to get a memory-mapped handle from the driver from which to
+     * automatically read data, but rather must perform some ioctl or other
+     * access to get the data from the driver.
+     */
+    if (!initialized)
+    {
+        /* TODO: Consider designing some exceptions we can catch for when there
+         * is an error.
+         */
+        return {};
+    }
+
+    std::vector<std::uint8_t> results(length);
+    std::memcpy(results.data(), memory.mapped + mappingResult.windowOffset,
+                length);
+
+    return results;
+}
+
+bool LpcDataHandler::writeMeta(const std::vector<std::uint8_t>& configuration)
+{
+    struct LpcRegion lpcRegion;
+
+    if (configuration.size() != sizeof(lpcRegion))
+    {
+        return false;
+    }
+
+    std::memcpy(&lpcRegion, configuration.data(), configuration.size());
+
+    /* TODO: LpcRegion sanity checking. */
+    mappingResult = mapper->mapWindow(lpcRegion.address, lpcRegion.length);
+    if (mappingResult.response != 0)
+    {
+        std::fprintf(stderr, "mappingResult.response %u\n",
+                     mappingResult.response);
+        /* Failed to map region. */
+        return false;
+    }
+
+    return setInitializedAndReturn(true);
+}
+
+std::vector<std::uint8_t> LpcDataHandler::readMeta()
+{
+    /* Return the MemoryResult structure packed. */
+    std::vector<std::uint8_t> output(
+        sizeof(std::uint8_t) + sizeof(std::uint32_t) + sizeof(std::uint32_t));
+
+    int index = 0;
+    std::memcpy(&output[index], &mappingResult.response,
+                sizeof(mappingResult.response));
+
+    index += sizeof(mappingResult.response);
+    std::memcpy(&output[index], &mappingResult.windowOffset,
+                sizeof(mappingResult.windowOffset));
+
+    index += sizeof(mappingResult.windowOffset);
+    std::memcpy(&output[index], &mappingResult.windowSize,
+                sizeof(mappingResult.windowSize));
+
+    return output;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/lpc_handler.hpp b/bmc/lpc_handler.hpp
new file mode 100644
index 0000000..4158b9b
--- /dev/null
+++ b/bmc/lpc_handler.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "data_handler.hpp"
+#include "mapper_errors.hpp"
+#include "window_hw_interface.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+struct LpcRegion
+{
+    /* Host LPC address where the chunk is to be mapped. */
+    std::uint32_t address;
+
+    /* Size of the chunk to be mapped. */
+    std::uint32_t length;
+} __attribute__((packed));
+
+/**
+ * Data Handler for configuration the ASPEED LPC memory region, reading and
+ * writing data.
+ */
+class LpcDataHandler : public DataInterface
+{
+  public:
+    /**
+     * Create an LpcDataHandler.
+     *
+     * @param[in] mapper - pointer to a mapper implementation to use.
+     */
+    explicit LpcDataHandler(std::unique_ptr<HardwareMapperInterface> mapper) :
+        mapper(std::move(mapper)), initialized(false)
+    {
+    }
+
+    bool open() override;
+    bool close() override;
+    std::vector<std::uint8_t> copyFrom(std::uint32_t length) override;
+    bool writeMeta(const std::vector<std::uint8_t>& configuration) override;
+    std::vector<std::uint8_t> readMeta() override;
+
+  private:
+    bool setInitializedAndReturn(bool value)
+    {
+        if (value)
+        {
+            try
+            {
+                /* Try really opening the map. */
+                memory = mapper->open();
+            }
+            catch (const MapperException& e)
+            {
+                std::fprintf(stderr, "received mapper exception: %s\n",
+                             e.what());
+                return false;
+            }
+        }
+
+        initialized = value;
+        return value;
+    }
+
+    std::unique_ptr<HardwareMapperInterface> mapper;
+    bool initialized;
+    /* The LPC Handler does not take ownership of this, in case there's cleanup
+     * required for close()
+     */
+    MemorySet memory = {};
+
+    /* Offset in reserved memory at which host data arrives. */
+    /* Size of the chunk of the memory region in use by the host (e.g.
+     * mapped over external block mechanism).
+     */
+    WindowMapResult mappingResult = {};
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/lpc_nuvoton.cpp b/bmc/lpc_nuvoton.cpp
new file mode 100644
index 0000000..eaa316f
--- /dev/null
+++ b/bmc/lpc_nuvoton.cpp
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "lpc_nuvoton.hpp"
+
+#include "mapper_errors.hpp"
+#include "window_hw_interface.hpp"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+
+#include <cerrno>
+#include <cinttypes>
+#include <cstdint>
+#include <cstdio>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace ipmi_flash
+{
+using std::uint16_t;
+using std::uint32_t;
+using std::uint8_t;
+
+std::unique_ptr<HardwareMapperInterface>
+    LpcMapperNuvoton::createNuvotonMapper(std::uint32_t regionAddress,
+                                          std::uint32_t regionSize)
+{
+    /* NOTE: Considered making one factory for both types. */
+    return std::make_unique<LpcMapperNuvoton>(regionAddress, regionSize);
+}
+
+MemorySet LpcMapperNuvoton::open()
+{
+    static constexpr auto devmem = "/dev/mem";
+
+    mappedFd = sys->open(devmem, O_RDWR | O_SYNC);
+    if (mappedFd == -1)
+    {
+        throw MapperException("Unable to open /dev/mem");
+    }
+
+    mapped = reinterpret_cast<uint8_t*>(sys->mmap(
+        0, memoryRegionSize, PROT_READ, MAP_SHARED, mappedFd, regionAddress));
+    if (mapped == MAP_FAILED)
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+        mapped = nullptr;
+
+        throw MapperException("Unable to map region");
+    }
+
+    MemorySet output;
+    output.mappedFd = mappedFd;
+    output.mapped = mapped;
+
+    return output;
+}
+
+void LpcMapperNuvoton::close()
+{
+    if (mapped)
+    {
+        sys->munmap(mapped, memoryRegionSize);
+        mapped = nullptr;
+    }
+
+    if (mappedFd != -1)
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+    }
+}
+
+/*
+ * The host buffer address is configured by host through
+ * SuperIO. On BMC side the max memory can be mapped is 4kB with the caveat that
+ * first byte of the buffer is reserved as host/BMC semaphore and not usable as
+ * shared memory.
+ *
+ * Mapper returns success for (addr, len) where (addr & 0x7) == 4 and len <=
+ * (4096 - 4). Otherwise, mapper returns either
+ *   - WindowOffset = 4 and WindowSize = len - 4 if (addr & 0x7) == 0
+ *   - WindowSize = 0 means that the region cannot be mapped otherwise
+ */
+WindowMapResult LpcMapperNuvoton::mapWindow(std::uint32_t address,
+                                            std::uint32_t length)
+{
+    WindowMapResult result = {};
+
+    /* We reserve the first 4 bytes from the mapped region; the first byte
+     * is shared semaphore, and the number of 4 is for alignment.
+     */
+    const uint32_t bmcMapReserveBytes = 4;
+    const uint32_t bmcMapMaxSizeBytes = 4 * 1024 - bmcMapReserveBytes;
+
+    if (length <= bmcMapReserveBytes)
+    {
+        std::fprintf(stderr, "window size %" PRIx32 " too small to map.\n",
+                     length);
+        result.response = EINVAL;
+        return result;
+    }
+
+    if (length > bmcMapMaxSizeBytes)
+    {
+        std::fprintf(stderr,
+                     "window size %" PRIx32 " not supported. Max size 4k.\n",
+                     length);
+        length = bmcMapMaxSizeBytes;
+    }
+
+    /* If host requested region starts at an aligned address, return offset of 4
+     * bytes so as to skip the semaphore register.
+     */
+    uint32_t windowOffset = bmcMapReserveBytes;
+    // uint32_t windowSize = length;
+
+    result.response = 0;
+    result.windowOffset = windowOffset;
+    result.windowSize = length;
+
+    const uint32_t addressOffset = address & 0x7;
+
+    if (addressOffset == 0)
+    {
+        std::fprintf(stderr, "Map address offset should be 4 for Nuvoton.\n");
+
+        result.response = EFBIG;
+        return result;
+    }
+    else if (addressOffset != bmcMapReserveBytes)
+    {
+        std::fprintf(stderr, "Map address offset should be 4 for Nuvoton.\n");
+
+        result.response = EINVAL;
+        return result;
+    }
+
+    /* TODO: need a kernel driver to handle mapping configuration.
+     * Until then program the register through /dev/mem.
+     */
+    int fd;
+    if ((fd = sys->open("/dev/mem", O_RDWR | O_SYNC)) == -1)
+    {
+        std::fprintf(stderr, "Failed to open /dev/mem\n");
+        sys->close(fd);
+
+        result.response = EINVAL;
+        return result;
+    }
+
+    const uint32_t bmcMapConfigBaseAddr = 0xc0001000;
+    const uint32_t bmcMapConfigWindowSizeOffset = 0x7;
+    const uint32_t bmcMapConfigWindowBaseOffset = 0xa;
+    const uint8_t bmcWindowSizeValue = 0xc;     // 4k
+    const uint16_t bmcWindowBaseValue = 0x8000; // BMC phyAddr from 0xc0008000
+
+    int pageSize = sys->getpagesize();
+
+    auto mapBasePtr = reinterpret_cast<uint8_t*>(
+        sys->mmap(nullptr, pageSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
+                  bmcMapConfigBaseAddr));
+
+    uint8_t* bmcWindowSize = mapBasePtr + bmcMapConfigWindowSizeOffset;
+    uint16_t* bmcWindowBase =
+        reinterpret_cast<uint16_t*>(mapBasePtr + bmcMapConfigWindowBaseOffset);
+
+    *bmcWindowSize = bmcWindowSizeValue;
+    *bmcWindowBase = bmcWindowBaseValue;
+
+    sys->munmap(mapBasePtr, pageSize);
+    sys->close(fd);
+
+    return result;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/lpc_nuvoton.hpp b/bmc/lpc_nuvoton.hpp
new file mode 100644
index 0000000..c66653b
--- /dev/null
+++ b/bmc/lpc_nuvoton.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "internal/sys.hpp"
+#include "window_hw_interface.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+class LpcMapperNuvoton : public HardwareMapperInterface
+{
+  public:
+    static std::unique_ptr<HardwareMapperInterface>
+        createNuvotonMapper(std::uint32_t regionAddress,
+                            std::uint32_t regionSize);
+
+    /**
+     * Create an LpcMapper for Nuvoton.
+     *
+     * @param[in] regionAddress - where to map the window into BMC memory.
+     * @param[in] regionSize - the size to map for copying data.
+     * @param[in] a sys call interface pointer.
+     * @todo Needs reserved memory region's physical address and size.
+     */
+    LpcMapperNuvoton(std::uint32_t regionAddress, std::uint32_t regionSize,
+                     const internal::Sys* sys = &internal::sys_impl) :
+        regionAddress(regionAddress),
+        memoryRegionSize(regionSize), sys(sys){};
+
+    /** Attempt to map the window for copying bytes, after mapWindow is called.
+     * throws MapperException
+     */
+    MemorySet open() override;
+
+    void close() override;
+
+    WindowMapResult mapWindow(std::uint32_t address,
+                              std::uint32_t length) override;
+
+  private:
+    std::uint32_t regionAddress;
+    std::uint32_t memoryRegionSize;
+    const internal::Sys* sys;
+
+    /* The file handle to /dev/mem. */
+    int mappedFd = -1;
+
+    /* The pointer to the memory-mapped region. */
+    std::uint8_t* mapped = nullptr;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/main.cpp b/bmc/main.cpp
new file mode 100644
index 0000000..2dd9531
--- /dev/null
+++ b/bmc/main.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "config.h"
+
+#include "file_handler.hpp"
+#include "firmware_handler.hpp"
+#include "image_handler.hpp"
+#include "lpc_aspeed.hpp"
+#include "lpc_handler.hpp"
+#include "lpc_nuvoton.hpp"
+#include "pci_handler.hpp"
+#include "prepare_systemd.hpp"
+#include "status.hpp"
+#include "update_systemd.hpp"
+#include "util.hpp"
+#include "verify_systemd.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <phosphor-logging/log.hpp>
+#include <sdbusplus/bus.hpp>
+
+namespace ipmi_flash
+{
+namespace
+{
+FileHandler hashHandler(HASH_FILENAME);
+#ifdef ENABLE_STATIC_LAYOUT
+FileHandler staticLayoutHandler(STATIC_HANDLER_STAGED_NAME);
+#endif
+#ifdef ENABLE_TARBALL_UBI
+FileHandler ubitarballHandler(TARBALL_STAGED_NAME);
+#endif
+
+/* The maximum external buffer size we expect is 64KB. */
+static constexpr std::size_t memoryRegionSize = 64 * 1024UL;
+
+#ifdef ENABLE_LPC_BRIDGE
+#if defined(ASPEED_LPC)
+LpcDataHandler lpcDataHandler(
+    LpcMapperAspeed::createAspeedMapper(MAPPED_ADDRESS, memoryRegionSize));
+#elif defined(NUVOTON_LPC)
+LpcDataHandler lpcDataHandler(
+    LpcMapperNuvoton::createNuvotonMapper(MAPPED_ADDRESS, memoryRegionSize));
+#else
+#error "You must specify a hardware implementation."
+#endif
+#endif
+
+#ifdef ENABLE_PCI_BRIDGE
+#if defined(ASPEED_P2A)
+PciDataHandler pciDataHandler(MAPPED_ADDRESS, memoryRegionSize);
+#else
+#error "You must specify a hardware implementation."
+#endif
+#endif
+
+std::vector<HandlerPack> supportedFirmware = {
+    {hashBlobId, &hashHandler},
+#ifdef ENABLE_STATIC_LAYOUT
+    {staticLayoutBlobId, &staticLayoutHandler},
+#endif
+#ifdef ENABLE_TARBALL_UBI
+    {ubiTarballBlobId, &ubitarballHandler},
+#endif
+};
+
+std::vector<DataHandlerPack> supportedTransports = {
+    {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+#ifdef ENABLE_PCI_BRIDGE
+    {FirmwareBlobHandler::UpdateFlags::p2a, &pciDataHandler},
+#endif
+#ifdef ENABLE_LPC_BRIDGE
+    {FirmwareBlobHandler::UpdateFlags::lpc, &lpcDataHandler},
+#endif
+};
+
+} // namespace
+
+} // namespace ipmi_flash
+
+extern "C" {
+std::unique_ptr<blobs::GenericBlobInterface> createHandler();
+}
+
+std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+    using namespace phosphor::logging;
+
+#ifdef ENABLE_REBOOT_UPDATE
+    static constexpr auto rebootTarget = "reboot.target";
+    static constexpr auto rebootMode = "replace-irreversibly";
+
+    auto updater = ipmi_flash::SystemdUpdateMechanism::CreateSystemdUpdate(
+        sdbusplus::bus::new_default(), rebootTarget, rebootMode);
+#else
+    auto updater = ipmi_flash::SystemdUpdateMechanism::CreateSystemdUpdate(
+        sdbusplus::bus::new_default(), UPDATE_DBUS_SERVICE);
+#endif
+
+    auto handler = ipmi_flash::FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        ipmi_flash::supportedFirmware, ipmi_flash::supportedTransports,
+        ipmi_flash::SystemdPreparation::CreatePreparation(
+            sdbusplus::bus::new_default(), PREPARATION_DBUS_SERVICE),
+        ipmi_flash::SystemdVerification::CreateVerification(
+            sdbusplus::bus::new_default(), VERIFY_STATUS_FILENAME,
+            VERIFY_DBUS_SERVICE),
+        std::move(updater));
+
+    if (!handler)
+    {
+        log<level::ERR>("Firmware Handler has invalid configuration");
+        return nullptr;
+    }
+
+    return handler;
+}
diff --git a/bmc/mapper_errors.hpp b/bmc/mapper_errors.hpp
new file mode 100644
index 0000000..8f30f30
--- /dev/null
+++ b/bmc/mapper_errors.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <exception>
+#include <string>
+
+namespace ipmi_flash
+{
+
+class MapperException : public std::exception
+{
+  public:
+    explicit MapperException(const std::string& message) : message(message)
+    {
+    }
+
+    virtual const char* what() const noexcept override
+    {
+        return message.c_str();
+    }
+
+  private:
+    std::string message;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/pci_handler.cpp b/bmc/pci_handler.cpp
new file mode 100644
index 0000000..436c63e
--- /dev/null
+++ b/bmc/pci_handler.cpp
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pci_handler.hpp"
+
+#include <fcntl.h>
+#include <linux/aspeed-p2a-ctrl.h>
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+const std::string PciDataHandler::p2aControlPath = "/dev/aspeed-p2a-ctrl";
+
+bool PciDataHandler::open()
+{
+    mappedFd = sys->open(p2aControlPath.c_str(), O_RDWR);
+    if (mappedFd == -1)
+    {
+        return false;
+    }
+
+    struct aspeed_p2a_ctrl_mapping map;
+    map.addr = regionAddress;
+    map.length = memoryRegionSize;
+    map.flags = ASPEED_P2A_CTRL_READWRITE;
+
+    if (sys->ioctl(mappedFd, ASPEED_P2A_CTRL_IOCTL_SET_WINDOW, &map))
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+
+        return false;
+    }
+
+    if (sys->ioctl(mappedFd, ASPEED_P2A_CTRL_IOCTL_GET_MEMORY_CONFIG, &map))
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+
+        return false;
+    }
+
+    /* The length of the region reserved is reported, and it's important
+     * because the offset + memory region could be beyond that reserved
+     * region.
+     */
+    std::uint64_t offset = regionAddress - map.addr;
+
+    mapped = reinterpret_cast<std::uint8_t*>(
+        mmap(0, memoryRegionSize, PROT_READ, MAP_SHARED, mappedFd, offset));
+    if (mapped == MAP_FAILED)
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+        mapped = nullptr;
+
+        return false;
+    }
+
+    return true;
+}
+
+bool PciDataHandler::close()
+{
+    /* TODO: Turn off the P2A bridge and region to disable host-side access.
+     */
+    if (mapped)
+    {
+        sys->munmap(mapped, memoryRegionSize);
+        mapped = nullptr;
+    }
+
+    if (mappedFd != -1)
+    {
+        sys->close(mappedFd);
+        mappedFd = -1;
+    }
+
+    return true;
+}
+
+std::vector<std::uint8_t> PciDataHandler::copyFrom(std::uint32_t length)
+{
+    std::vector<std::uint8_t> results(length);
+    std::memcpy(results.data(), mapped, length);
+
+    return results;
+}
+
+bool PciDataHandler::writeMeta(const std::vector<std::uint8_t>& configuration)
+{
+    /* PCI handler doesn't require configuration write, only read. */
+    return false;
+}
+
+std::vector<std::uint8_t> PciDataHandler::readMeta()
+{
+    /* PCI handler does require returning a configuration from read. */
+    struct PciConfigResponse reply;
+    reply.address = regionAddress;
+
+    std::vector<std::uint8_t> bytes;
+    bytes.resize(sizeof(reply));
+    std::memcpy(bytes.data(), &reply, sizeof(reply));
+
+    return bytes;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/pci_handler.hpp b/bmc/pci_handler.hpp
new file mode 100644
index 0000000..1cafc32
--- /dev/null
+++ b/bmc/pci_handler.hpp
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "data_handler.hpp"
+#include "internal/sys.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+/** P2A configuration response. */
+struct PciConfigResponse
+{
+    std::uint32_t address;
+} __attribute__((packed));
+
+/**
+ * Data handler for reading and writing data via the P2A bridge.
+ *
+ * @note: Currently implemented to support only aspeed-p2a-ctrl.
+ */
+class PciDataHandler : public DataInterface
+{
+  public:
+    PciDataHandler(std::uint32_t regionAddress, std::size_t regionSize,
+                   const internal::Sys* sys = &internal::sys_impl) :
+        regionAddress(regionAddress),
+        memoryRegionSize(regionSize), sys(sys){};
+
+    bool open() override;
+    bool close() override;
+    std::vector<std::uint8_t> copyFrom(std::uint32_t length) override;
+    bool writeMeta(const std::vector<std::uint8_t>& configuration) override;
+    std::vector<std::uint8_t> readMeta() override;
+
+  private:
+    std::uint32_t regionAddress;
+    std::uint32_t memoryRegionSize;
+    const internal::Sys* sys;
+
+    int mappedFd = -1;
+    std::uint8_t* mapped = nullptr;
+    static const std::string p2aControlPath;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/prepare_systemd.cpp b/bmc/prepare_systemd.cpp
new file mode 100644
index 0000000..b16bc99
--- /dev/null
+++ b/bmc/prepare_systemd.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "prepare_systemd.hpp"
+
+#include "status.hpp"
+
+#include <memory>
+
+namespace ipmi_flash
+{
+
+std::unique_ptr<TriggerableActionInterface>
+    SystemdPreparation::CreatePreparation(sdbusplus::bus::bus&& bus,
+                                          const std::string& service)
+{
+    return std::make_unique<SystemdPreparation>(std::move(bus), service);
+}
+
+bool SystemdPreparation::trigger()
+{
+    static constexpr auto systemdService = "org.freedesktop.systemd1";
+    static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
+    static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
+
+    auto method = bus.new_method_call(systemdService, systemdRoot,
+                                      systemdInterface, "StartUnit");
+    method.append(triggerService);
+    method.append("replace");
+
+    try
+    {
+        bus.call_noreply(method);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        /* TODO: Once logging supports unit-tests, add a log message to test
+         * this failure.
+         */
+        state = ActionStatus::failed;
+        return false;
+    }
+
+    state = ActionStatus::success;
+    return true;
+}
+
+void SystemdPreparation::abort()
+{
+    return;
+}
+
+ActionStatus SystemdPreparation::status()
+{
+    return state;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/prepare_systemd.hpp b/bmc/prepare_systemd.hpp
new file mode 100644
index 0000000..af8e065
--- /dev/null
+++ b/bmc/prepare_systemd.hpp
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "status.hpp"
+
+#include <memory>
+#include <sdbusplus/bus.hpp>
+#include <string>
+
+namespace ipmi_flash
+{
+
+class SystemdPreparation : public TriggerableActionInterface
+{
+  public:
+    static std::unique_ptr<TriggerableActionInterface>
+        CreatePreparation(sdbusplus::bus::bus&& bus,
+                          const std::string& service);
+
+    SystemdPreparation(sdbusplus::bus::bus&& bus, const std::string& service) :
+        bus(std::move(bus)), triggerService(service)
+    {
+    }
+
+    ~SystemdPreparation() = default;
+    SystemdPreparation(const SystemdPreparation&) = delete;
+    SystemdPreparation& operator=(const SystemdPreparation&) = delete;
+    SystemdPreparation(SystemdPreparation&&) = default;
+    SystemdPreparation& operator=(SystemdPreparation&&) = default;
+
+    bool trigger() override;
+    void abort() override;
+    ActionStatus status() override;
+
+  private:
+    sdbusplus::bus::bus bus;
+    const std::string triggerService;
+    ActionStatus state = ActionStatus::unknown;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/test/Makefile.am b/bmc/test/Makefile.am
new file mode 100644
index 0000000..1dcc3ae
--- /dev/null
+++ b/bmc/test/Makefile.am
@@ -0,0 +1,102 @@
+@VALGRIND_CHECK_RULES@
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/ \
+	-I$(top_srcdir)/tools/ \
+	-I$(top_srcdir)/bmc/ \
+	$(GTEST_CFLAGS) \
+	$(GMOCK_CFLAGS) \
+	$(CODE_COVERAGE_CPPFLAGS)
+AM_CXXFLAGS = \
+	$(SDBUSPLUS_CFLAGS) \
+	$(PHOSPHOR_LOGGING_CFLAGS) \
+	$(CODE_COVERAGE_CXXFLAGS)
+AM_LDFLAGS = \
+	$(GTEST_LIBS) \
+	$(GMOCK_LIBS) \
+	-lgmock_main \
+	$(OESDK_TESTCASE_FLAGS) \
+	$(SDBUSPLUS_LIBS) \
+	$(PHOSPHOR_LOGGING_LIBS) \
+	$(CODE_COVERAGE_LIBS)
+
+# Run all 'check' test programs
+check_PROGRAMS = \
+	firmware_createhandler_unittest \
+	firmware_handler_unittest \
+	firmware_stat_unittest \
+	firmware_canhandle_unittest \
+	firmware_write_unittest \
+	firmware_writemeta_unittest \
+	firmware_close_unittest \
+	firmware_sessionstat_unittest \
+	firmware_commit_unittest \
+	file_handler_unittest \
+	firmware_state_notyetstarted_unittest \
+	firmware_state_uploadinprogress_unittest \
+	firmware_state_verificationpending_unittest \
+	firmware_state_verificationstarted_unittest \
+	firmware_state_verificationcompleted_unittest \
+	firmware_state_updatepending_unittest \
+	firmware_state_updatestarted_unittest \
+	firmware_state_updatecompleted_unittest \
+	firmware_state_notyetstarted_tarball_unittest
+
+TESTS = $(check_PROGRAMS)
+
+firmware_createhandler_unittest_SOURCES = firmware_createhandler_unittest.cpp
+firmware_createhandler_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_handler_unittest_SOURCES = firmware_handler_unittest.cpp
+firmware_handler_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_stat_unittest_SOURCES = firmware_stat_unittest.cpp
+firmware_stat_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_canhandle_unittest_SOURCES = firmware_canhandle_unittest.cpp
+firmware_canhandle_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_write_unittest_SOURCES = firmware_write_unittest.cpp
+firmware_write_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_writemeta_unittest_SOURCES = firmware_writemeta_unittest.cpp
+firmware_writemeta_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_close_unittest_SOURCES = firmware_close_unittest.cpp
+firmware_close_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_sessionstat_unittest_SOURCES = firmware_sessionstat_unittest.cpp
+firmware_sessionstat_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_commit_unittest_SOURCES = firmware_commit_unittest.cpp
+firmware_commit_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+file_handler_unittest_SOURCES = file_handler_unittest.cpp
+file_handler_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la -lstdc++fs
+
+firmware_state_notyetstarted_unittest_SOURCES = firmware_state_notyetstarted_unittest.cpp
+firmware_state_notyetstarted_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_uploadinprogress_unittest_SOURCES = firmware_state_uploadinprogress_unittest.cpp
+firmware_state_uploadinprogress_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_verificationpending_unittest_SOURCES = firmware_state_verificationpending_unittest.cpp
+firmware_state_verificationpending_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_verificationstarted_unittest_SOURCES = firmware_state_verificationstarted_unittest.cpp
+firmware_state_verificationstarted_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_verificationcompleted_unittest_SOURCES = firmware_state_verificationcompleted_unittest.cpp
+firmware_state_verificationcompleted_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_updatepending_unittest_SOURCES = firmware_state_updatepending_unittest.cpp
+firmware_state_updatepending_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_updatestarted_unittest_SOURCES = firmware_state_updatestarted_unittest.cpp
+firmware_state_updatestarted_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_updatecompleted_unittest_SOURCES = firmware_state_updatecompleted_unittest.cpp
+firmware_state_updatecompleted_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
+
+firmware_state_notyetstarted_tarball_unittest_SOURCES = firmware_state_notyetstarted_tarball_unittest.cpp
+firmware_state_notyetstarted_tarball_unittest_LDADD = $(top_builddir)/bmc/libfirmwareblob_common.la
diff --git a/bmc/test/crc_mock.hpp b/bmc/test/crc_mock.hpp
new file mode 100644
index 0000000..293ec24
--- /dev/null
+++ b/bmc/test/crc_mock.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <cstdint>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+class CrcInterface
+{
+  public:
+    virtual ~CrcInterface() = default;
+
+    virtual std::uint16_t
+        generateCrc(const std::vector<std::uint8_t>& data) const = 0;
+};
+
+class CrcMock : public CrcInterface
+{
+  public:
+    virtual ~CrcMock() = default;
+    MOCK_CONST_METHOD1(generateCrc,
+                       std::uint16_t(const std::vector<std::uint8_t>&));
+};
diff --git a/bmc/test/data_mock.hpp b/bmc/test/data_mock.hpp
new file mode 100644
index 0000000..c8109ad
--- /dev/null
+++ b/bmc/test/data_mock.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "data_handler.hpp"
+
+#include <gmock/gmock.h>
+
+namespace ipmi_flash
+{
+
+class DataHandlerMock : public DataInterface
+{
+  public:
+    virtual ~DataHandlerMock() = default;
+
+    MOCK_METHOD0(open, bool());
+    MOCK_METHOD0(close, bool());
+    MOCK_METHOD1(copyFrom, std::vector<std::uint8_t>(std::uint32_t));
+    MOCK_METHOD1(writeMeta, bool(const std::vector<std::uint8_t>&));
+    MOCK_METHOD0(readMeta, std::vector<std::uint8_t>());
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/test/file_handler_unittest.cpp b/bmc/test/file_handler_unittest.cpp
new file mode 100644
index 0000000..ab12125
--- /dev/null
+++ b/bmc/test/file_handler_unittest.cpp
@@ -0,0 +1,58 @@
+#include "file_handler.hpp"
+
+#include <cstdint>
+#include <cstdio>
+#include <fstream>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+static constexpr auto TESTPATH = "test.output";
+
+class FileHandlerOpenTest : public ::testing::Test
+{
+  protected:
+    void TearDown() override
+    {
+        (void)std::remove(TESTPATH);
+    }
+};
+
+TEST_F(FileHandlerOpenTest, VerifyItIsHappy)
+{
+    /* Opening a fail may create it? */
+
+    FileHandler handler(TESTPATH);
+    EXPECT_TRUE(handler.open(""));
+
+    /* Calling open twice fails the second time. */
+    EXPECT_FALSE(handler.open(""));
+}
+
+TEST_F(FileHandlerOpenTest, VerifyWriteDataWrites)
+{
+    /* Verify writing bytes writes them... flushing data can be an issue here,
+     * so we close first.
+     */
+    FileHandler handler(TESTPATH);
+    EXPECT_TRUE(handler.open(""));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    std::uint32_t offset = 0;
+
+    EXPECT_TRUE(handler.write(offset, bytes));
+    handler.close();
+
+    std::ifstream data;
+    data.open(TESTPATH, std::ios::binary);
+    char expectedBytes[2];
+    data.read(&expectedBytes[0], sizeof(expectedBytes));
+    EXPECT_EQ(expectedBytes[0], bytes[0]);
+    EXPECT_EQ(expectedBytes[1], bytes[1]);
+    /* annoyingly the memcmp was failing... but it's the same data. */
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_canhandle_unittest.cpp b/bmc/test/firmware_canhandle_unittest.cpp
new file mode 100644
index 0000000..dabf8ef
--- /dev/null
+++ b/bmc/test/firmware_canhandle_unittest.cpp
@@ -0,0 +1,44 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+TEST(FirmwareHandlerCanHandleTest, VerifyItemsInListAreOk)
+{
+    struct ListItem
+    {
+        std::string name;
+        bool expected;
+    };
+
+    std::vector<ListItem> items = {
+        {"asdf", true}, {"nope", false}, {"123123", false}, {"bcdf", true}};
+
+    ImageHandlerMock imageMock;
+
+    std::vector<HandlerPack> blobs = {
+        {hashBlobId, &imageMock},
+        {"asdf", &imageMock},
+        {"bcdf", &imageMock},
+    };
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+    };
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+
+    for (const auto& item : items)
+    {
+        EXPECT_EQ(item.expected, handler->canHandleBlob(item.name));
+    }
+}
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_close_unittest.cpp b/bmc/test/firmware_close_unittest.cpp
new file mode 100644
index 0000000..66b9c83
--- /dev/null
+++ b/bmc/test/firmware_close_unittest.cpp
@@ -0,0 +1,78 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Eq;
+using ::testing::Return;
+using ::testing::StrEq;
+
+class FirmwareHandlerCloseTest : public FakeLpcFirmwareTest
+{
+};
+
+TEST_F(FirmwareHandlerCloseTest, CloseSucceedsWithDataHandler)
+{
+    /* Boring test where you open a blob_id, then verify that when it's closed
+     * everything looks right.
+     */
+    EXPECT_CALL(dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(imageMock, open(StrEq(hashBlobId))).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::lpc,
+        hashBlobId));
+
+    /* The active hash blob_id was added. */
+    auto currentBlobs = handler->getBlobIds();
+    EXPECT_EQ(3, currentBlobs.size());
+    EXPECT_EQ(1, std::count(currentBlobs.begin(), currentBlobs.end(),
+                            activeHashBlobId));
+
+    /* Set up close() expectations. */
+    EXPECT_CALL(dataMock, close());
+    EXPECT_CALL(imageMock, close());
+    EXPECT_TRUE(handler->close(0));
+
+    /* Close does not delete the active blob id.  This indicates that there is
+     * data queued.
+     */
+}
+
+TEST_F(FirmwareHandlerCloseTest, CloseSucceedsWithoutDataHandler)
+{
+    /* Boring test where you open a blob_id using ipmi, so there's no data
+     * handler, and it's closed and everything looks right.
+     */
+    EXPECT_CALL(imageMock, open(StrEq(hashBlobId))).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi,
+        hashBlobId));
+
+    /* The active hash blob_id was added. */
+    auto currentBlobs = handler->getBlobIds();
+    EXPECT_EQ(3, currentBlobs.size());
+    EXPECT_EQ(1, std::count(currentBlobs.begin(), currentBlobs.end(),
+                            activeHashBlobId));
+
+    /* Set up close() expectations. */
+    EXPECT_CALL(imageMock, close());
+    EXPECT_TRUE(handler->close(0));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_commit_unittest.cpp b/bmc/test/firmware_commit_unittest.cpp
new file mode 100644
index 0000000..b8c651e
--- /dev/null
+++ b/bmc/test/firmware_commit_unittest.cpp
@@ -0,0 +1,87 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+using ::testing::_;
+using ::testing::IsNull;
+using ::testing::NotNull;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::StrictMock;
+
+class FirmwareHandlerCommitTest : public ::testing::Test
+{
+  protected:
+    ImageHandlerMock imageMock1, imageMock2;
+    std::vector<HandlerPack> blobs;
+    std::vector<DataHandlerPack> data;
+
+    void SetUp() override
+    {
+        blobs = {
+            {hashBlobId, &imageMock1},
+            {"asdf", &imageMock2},
+        };
+
+        data = {
+            {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+        };
+    }
+};
+
+TEST_F(FirmwareHandlerCommitTest, VerifyCannotCommitOnFlashImage)
+{
+    /* Verify the flash image returns failure on this command.  It's a fairly
+     * artificial test.
+     */
+
+    /* Verify it doesn't get called by using StrictMock. */
+    std::unique_ptr<TriggerableActionInterface> verifyMock =
+        std::make_unique<StrictMock<TriggerMock>>();
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), std::move(verifyMock),
+        CreateTriggerMock());
+
+    EXPECT_CALL(imageMock2, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi,
+        "asdf"));
+
+    EXPECT_FALSE(handler->commit(0, {}));
+}
+
+TEST_F(FirmwareHandlerCommitTest, VerifyCannotCommitOnHashFile)
+{
+    /* Verify the hash file returns failure on this command.  It's a fairly
+     * artificial test.
+     */
+
+    /* Verify it doesn't get called by using StrictMock. */
+    std::unique_ptr<TriggerableActionInterface> verifyMock =
+        std::make_unique<StrictMock<TriggerMock>>();
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), std::move(verifyMock),
+        CreateTriggerMock());
+
+    EXPECT_CALL(imageMock1, open(StrEq(hashBlobId))).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi,
+        hashBlobId));
+
+    EXPECT_FALSE(handler->commit(0, {}));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_createhandler_unittest.cpp b/bmc/test/firmware_createhandler_unittest.cpp
new file mode 100644
index 0000000..dd09cbe
--- /dev/null
+++ b/bmc/test/firmware_createhandler_unittest.cpp
@@ -0,0 +1,41 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::StrictMock;
+
+TEST(FirmwareHandlerBlobTest, VerifyFirmwareCounts)
+{
+    /* Verify the firmware count must be greater than zero. */
+
+    DataHandlerMock dataMock;
+    ImageHandlerMock imageMock;
+    //    StrictMock<SdJournalMock> journalMock;
+    //    SwapJouralHandler(&journalMock);
+
+    std::vector<HandlerPack> blobs = {
+        {hashBlobId, &imageMock},
+    };
+
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+        {FirmwareBlobHandler::UpdateFlags::lpc, &dataMock},
+    };
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+
+    //    EXPECT_EQ(handler, nullptr);
+    EXPECT_FALSE(handler == nullptr);
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_handler_unittest.cpp b/bmc/test/firmware_handler_unittest.cpp
new file mode 100644
index 0000000..27bd517
--- /dev/null
+++ b/bmc/test/firmware_handler_unittest.cpp
@@ -0,0 +1,71 @@
+#include "firmware_handler.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::UnorderedElementsAreArray;
+
+TEST(FirmwareHandlerTest, CreateEmptyListVerifyFails)
+{
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+    };
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        {}, data, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+    EXPECT_EQ(handler, nullptr);
+}
+TEST(FirmwareHandlerTest, CreateEmptyDataHandlerListFails)
+{
+    ImageHandlerMock imageMock;
+
+    std::vector<HandlerPack> blobs = {
+        {hashBlobId, &imageMock},
+        {"asdf", &imageMock},
+    };
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, {}, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+    EXPECT_EQ(handler, nullptr);
+}
+TEST(FirmwareHandlerTest, VerifyHashRequiredForHappiness)
+{
+    /* This works fine only if you also pass in the hash handler. */
+    ImageHandlerMock imageMock;
+
+    std::vector<HandlerPack> blobs = {
+        {"asdf", &imageMock},
+    };
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+    };
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+    EXPECT_EQ(handler, nullptr);
+
+    blobs.push_back({hashBlobId, &imageMock});
+
+    handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+    auto result = handler->getBlobIds();
+    std::vector<std::string> expectedBlobs = {"asdf", hashBlobId};
+    EXPECT_THAT(result, UnorderedElementsAreArray(expectedBlobs));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_sessionstat_unittest.cpp b/bmc/test/firmware_sessionstat_unittest.cpp
new file mode 100644
index 0000000..d2d1896
--- /dev/null
+++ b/bmc/test/firmware_sessionstat_unittest.cpp
@@ -0,0 +1,75 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+using ::testing::Eq;
+using ::testing::Return;
+
+class FirmwareSessionStateTestIpmiOnly : public IpmiOnlyFirmwareTest
+{
+};
+
+class FirmwareSessionStateTestLpc : public FakeLpcFirmwareTest
+{
+};
+
+TEST_F(FirmwareSessionStateTestIpmiOnly, DataTypeIpmiNoMetadata)
+{
+    /* Verifying running stat if the type of data session is IPMI returns no
+     * metadata.
+     */
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi,
+        "asdf"));
+
+    int size = 512;
+    EXPECT_CALL(imageMock, getSize()).WillOnce(Return(size));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(handler->stat(0, &meta));
+    EXPECT_EQ(meta.blobState,
+              blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi);
+    EXPECT_EQ(meta.size, size);
+    EXPECT_EQ(meta.metadata.size(), 0);
+}
+
+TEST_F(FirmwareSessionStateTestLpc, DataTypeP2AReturnsMetadata)
+{
+    /* Really any type that isn't IPMI can return metadata, but we only expect
+     * P2A to for now.  Later, LPC may have reason to provide data, and can by
+     * simply implementing read().
+     */
+    EXPECT_CALL(dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::lpc,
+        "asdf"));
+
+    int size = 512;
+    EXPECT_CALL(imageMock, getSize()).WillOnce(Return(size));
+    std::vector<std::uint8_t> mBytes = {0x01, 0x02};
+    EXPECT_CALL(dataMock, readMeta()).WillOnce(Return(mBytes));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(handler->stat(0, &meta));
+    EXPECT_EQ(meta.blobState,
+              blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::lpc);
+    EXPECT_EQ(meta.size, size);
+    EXPECT_EQ(meta.metadata.size(), mBytes.size());
+    EXPECT_EQ(meta.metadata[0], mBytes[0]);
+    EXPECT_EQ(meta.metadata[1], mBytes[1]);
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_stat_unittest.cpp b/bmc/test/firmware_stat_unittest.cpp
new file mode 100644
index 0000000..99ae770
--- /dev/null
+++ b/bmc/test/firmware_stat_unittest.cpp
@@ -0,0 +1,40 @@
+#include "firmware_handler.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+TEST(FirmwareHandlerStatTest, StatOnInactiveBlobIDReturnsTransport)
+{
+    /* Test that the metadata information returned matches expectations for this
+     * case.
+     *
+     * canHandle has already been called at this point, so we don't need to test
+     * the input for this function.
+     */
+
+    ImageHandlerMock imageMock;
+
+    std::vector<HandlerPack> blobs = {
+        {hashBlobId, &imageMock},
+        {"asdf", &imageMock},
+    };
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+    };
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+        CreateTriggerMock());
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(handler->stat("asdf", &meta));
+    EXPECT_EQ(FirmwareBlobHandler::UpdateFlags::ipmi, meta.blobState);
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_notyetstarted_tarball_unittest.cpp b/bmc/test/firmware_state_notyetstarted_tarball_unittest.cpp
new file mode 100644
index 0000000..275763c
--- /dev/null
+++ b/bmc/test/firmware_state_notyetstarted_tarball_unittest.cpp
@@ -0,0 +1,84 @@
+/**
+ * The goal of these tests is to verify opening the ubi tarball changes state
+ * as expected and does not regress.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::UnorderedElementsAreArray;
+
+class FirmwareHandlerNotYetStartedUbitTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        blobs = {
+            {hashBlobId, &imageMock},
+            {ubiTarballBlobId, &imageMock},
+        };
+
+        std::unique_ptr<TriggerableActionInterface> verifyMock =
+            std::make_unique<TriggerMock>();
+        verifyMockPtr = reinterpret_cast<TriggerMock*>(verifyMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> updateMock =
+            std::make_unique<TriggerMock>();
+        updateMockPtr = reinterpret_cast<TriggerMock*>(updateMock.get());
+
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            blobs, data, CreateTriggerMock(), std::move(verifyMock),
+            std::move(updateMock));
+    }
+
+    void expectedState(FirmwareBlobHandler::UpdateState state)
+    {
+        auto realHandler = dynamic_cast<FirmwareBlobHandler*>(handler.get());
+        EXPECT_EQ(state, realHandler->getCurrentState());
+    }
+
+    void openToInProgress(const std::string& blobId)
+    {
+        EXPECT_CALL(imageMock, open(blobId)).WillOnce(Return(true));
+        EXPECT_TRUE(handler->open(session, flags, blobId));
+        expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+    }
+
+    ImageHandlerMock imageMock;
+    std::vector<HandlerPack> blobs;
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr}};
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+    TriggerMock* verifyMockPtr;
+    TriggerMock* updateMockPtr;
+
+    std::uint16_t session = 1;
+    std::uint16_t flags =
+        blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi;
+};
+
+TEST_F(FirmwareHandlerNotYetStartedUbitTest,
+       OpeningTarballMovesToUploadInProgress)
+{
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray({hashBlobId, ubiTarballBlobId}));
+
+    openToInProgress(ubiTarballBlobId);
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(
+                    {hashBlobId, ubiTarballBlobId, activeImageBlobId}));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_notyetstarted_unittest.cpp b/bmc/test/firmware_state_notyetstarted_unittest.cpp
new file mode 100644
index 0000000..54ab7f8
--- /dev/null
+++ b/bmc/test/firmware_state_notyetstarted_unittest.cpp
@@ -0,0 +1,118 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is notYetStarted.  The initial state.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+class FirmwareHandlerNotYetStartedTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * There are the following calls (parameters may vary):
+ * Note: you cannot have a session yet, so only commands that don't take a
+ * session parameter are valid. Once you open() from this state, we will vary
+ * you transition out of this state (ensuring the above is true). Technically
+ * the firmware handler receives the session number with open(), but the blob
+ * manager is providing this normally.
+ *
+ * canHandleBlob
+ * getBlobIds
+ * deleteBlob
+ * stat
+ * open
+ *
+ * canHandleBlob is just a count check (or something similar) against what is
+ * returned by getBlobIds.  It is tested in firmware_canhandle_unittest
+ */
+
+/*
+ * deleteBlob()
+ */
+TEST_F(FirmwareHandlerNotYetStartedTest, DeleteBlobInStateReturnsFalse)
+{
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/* canHandleBlob, getBlobIds */
+TEST_F(FirmwareHandlerNotYetStartedTest, GetBlobListValidateListContents)
+{
+    /* By only checking that the hash and static blob ids are present to start
+     * with, we're also verifying others aren't.
+     */
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    /* Verify canHandleBlob is reading from the same list (basically) */
+    for (const auto& blob : startingBlobs)
+    {
+        EXPECT_TRUE(handler->canHandleBlob(blob));
+    }
+}
+
+/* stat(blob_id) */
+TEST_F(FirmwareHandlerNotYetStartedTest, StatEachBlobIdVerifyResults)
+{
+    /* In this original state, calling stat() on the blob ids will return the
+     * transported supported.
+     */
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/* open(each blob id) (verifyblobid will no longer be available at this state.
+ */
+TEST_F(FirmwareHandlerNotYetStartedTest, OpenStaticImageFileVerifyStateChange)
+{
+    EXPECT_CALL(imageMock, open(staticLayoutBlobId)).WillOnce(Return(true));
+    EXPECT_CALL(*prepareMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(session, flags, staticLayoutBlobId));
+
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_TRUE(handler->canHandleBlob(activeImageBlobId));
+}
+
+TEST_F(FirmwareHandlerNotYetStartedTest, OpenHashFileVerifyStateChange)
+{
+    EXPECT_CALL(imageMock, open(hashBlobId)).WillOnce(Return(true));
+    EXPECT_CALL(*prepareMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(session, flags, hashBlobId));
+
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_TRUE(handler->canHandleBlob(activeHashBlobId));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_updatecompleted_unittest.cpp b/bmc/test/firmware_state_updatecompleted_unittest.cpp
new file mode 100644
index 0000000..9e43edb
--- /dev/null
+++ b/bmc/test/firmware_state_updatecompleted_unittest.cpp
@@ -0,0 +1,254 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is UpdateCompleted.  This state is achieved as an exit from
+ * updateStarted.
+ *
+ * This can be reached with success or failure from an update, and is reached
+ * via a stat() call from updatedStarted.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ */
+
+class FirmwareHandlerUpdateCompletedTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * open(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       AttemptToOpenFilesReturnsFailureAfterSuccess)
+{
+    /* In state updateCompleted a file is open, which means no others can be. */
+    getToUpdateCompleted(ActionStatus::success);
+
+    auto blobsToOpen = handler->getBlobIds();
+    for (const auto& blob : blobsToOpen)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/*
+ * stat(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CallingStatSessionAfterCompletedSuccessReturnsStateWithoutRechecking)
+{
+    getToUpdateCompleted(ActionStatus::success);
+    EXPECT_CALL(*updateMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CallingStatSessionAfterCompletedFailureReturnsStateWithoutRechecking)
+{
+    getToUpdateCompleted(ActionStatus::failed);
+    EXPECT_CALL(*updateMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, StatOnActiveImageReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest, StatOnUpdateBlobReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(updateBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, WriteMetaToUpdateBlobReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_FALSE(handler->writeMeta(session, 0, {0x01}));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, WriteToUpdateBlobReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_FALSE(handler->write(session, 0, {0x01}));
+}
+
+/*
+ * commit(session) - returns failure
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CommitOnUpdateBlobAfterSuccessReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).Times(0);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CommitOnUpdateBlobAfterFailureReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::failed);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).Times(0);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+/*
+ * read(session) - nothing to read here.
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, ReadingFromUpdateBlobReturnsNothing)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * getBlobIds
+ * canHandleBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, GetBlobListProvidesExpectedBlobs)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_THAT(
+        handler->getBlobIds(),
+        UnorderedElementsAreArray(
+            {updateBlobId, hashBlobId, activeImageBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * close(session) - closes everything out and returns to state not-yet-started.
+ * It doesn't go and clean out any update artifacts that may be present on the
+ * system.  It's up to the update implementation to deal with this before
+ * marking complete.
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       ClosingOnUpdateBlobIdAfterSuccessReturnsToNotYetStartedAndCleansBlobList)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       ClosingOnUpdateBlobIdAfterFailureReturnsToNotYetStartedAndCleansBlobList)
+{
+    getToUpdateCompleted(ActionStatus::failed);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToUpdateCompleted(ActionStatus::success);
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_updatepending_unittest.cpp b/bmc/test/firmware_state_updatepending_unittest.cpp
new file mode 100644
index 0000000..d09c820
--- /dev/null
+++ b/bmc/test/firmware_state_updatepending_unittest.cpp
@@ -0,0 +1,299 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is updatePending.  This state is achieved as an exit from
+ * verificationCompleted.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+
+class FirmwareHandlerUpdatePendingTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, GetBlobsListHasExpectedValues)
+{
+    getToUpdatePending();
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray({updateBlobId, activeImageBlobId,
+                                           hashBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * open(blob) - because updatePending is in a fileOpen==false state, one can
+ * then open blobs. However, because we're in a special state, we will restrict
+ * them s.t. they can only open the updateBlobId.
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       OpenUpdateBlobIdIsSuccessfulAndDoesNotChangeState)
+{
+    getToUpdatePending();
+
+    /* Opening the update blob isn't interesting, except it's required for
+     * commit() which triggers the update process.
+     */
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, OpenAnyBlobOtherThanUpdateFails)
+{
+    getToUpdatePending();
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        if (blob == updateBlobId)
+        {
+            continue;
+        }
+        EXPECT_FALSE(handler->open(session, flags, blob));
+    }
+}
+
+/*
+ * close(session) - close from this state is uninteresting.
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, CloseUpdateBlobDoesNotChangeState)
+{
+    /* Verify nothing changes when one just opens, then closes the updateBlobId.
+     */
+    getToUpdatePending();
+
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+
+    handler->close(session);
+
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+    EXPECT_TRUE(handler->canHandleBlob(updateBlobId));
+}
+
+/*
+ * writemeta(session) - this will return failure.
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, WriteMetaToUpdateBlobReturnsFailure)
+{
+    getToUpdatePending();
+
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    EXPECT_FALSE(handler->writeMeta(session, 0, {0x01}));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, WriteToUpdateBlobReturnsFailure)
+{
+    getToUpdatePending();
+
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    EXPECT_FALSE(handler->write(session, 0, {0x01}));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, ReadFromUpdateBlobIdReturnsEmpty)
+{
+    getToUpdatePending();
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, StatOnActiveImageReturnsFailure)
+{
+    getToUpdatePending();
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, StatOnUpdateBlobReturnsFailure)
+{
+    getToUpdatePending();
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(updateBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToUpdatePending();
+
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    for (const auto& blob : startingBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * stat(session)
+ * In this case, you can open updateBlobId without changing state, therefore,
+ * let's call stat() against a session against this file. This done, ahead of
+ * commit() should report the state as "other."
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       SessionStatOnUpdateBlobIdReturnsFailure)
+{
+    getToUpdatePending();
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::unknown));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       CommitOnUpdateBlobTriggersUpdateAndChangesState)
+{
+    /* Commit triggers the update mechanism (similarly for the verifyBlobId) and
+     * changes state to updateStarted.
+     */
+    getToUpdatePending();
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       CommitOnUpdateBlobTriggersUpdateAndReturnsFailureDoesNotChangeState)
+{
+    getToUpdatePending();
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).WillOnce(Return(false));
+
+    EXPECT_FALSE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteUpdateAbortsProcess)
+{
+    /* It doesn't matter what blob id is used to delete in the design, so just
+     * delete the update blob id
+     */
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+    EXPECT_TRUE(handler->deleteBlob(updateBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteActiveImageAbortsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+    EXPECT_TRUE(handler->deleteBlob(activeImageBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteStaticLayoutAbortsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(staticLayoutBlobId));
+    EXPECT_TRUE(handler->deleteBlob(staticLayoutBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteHashAbortsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(hashBlobId));
+    EXPECT_TRUE(handler->deleteBlob(hashBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_updatestarted_unittest.cpp b/bmc/test/firmware_state_updatestarted_unittest.cpp
new file mode 100644
index 0000000..237e0e9
--- /dev/null
+++ b/bmc/test/firmware_state_updatestarted_unittest.cpp
@@ -0,0 +1,245 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is updateStarted.  This state is achieved as an exit from
+ * updatePending.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ */
+
+class FirmwareHandlerUpdateStartedTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * open(blob)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, AttemptToOpenFilesReturnsFailure)
+{
+    /* In state updateStarted a file is open, which means no others can be. */
+    getToUpdateStarted();
+
+    auto blobsToOpen = handler->getBlobIds();
+    for (const auto& blob : blobsToOpen)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/* canHandleBlob(blob)
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, VerifyListOfBlobs)
+{
+    getToUpdateStarted();
+
+    EXPECT_THAT(
+        handler->getBlobIds(),
+        UnorderedElementsAreArray(
+            {updateBlobId, hashBlobId, activeImageBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToUpdateStarted();
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, StatOnActiveImageReturnsFailure)
+{
+    getToUpdateStarted();
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest, StatOnUpdateBlobReturnsFailure)
+{
+    getToUpdateStarted();
+
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(updateBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToUpdateStarted();
+
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, WriteMetaToUpdateBlobReturnsFailure)
+{
+    getToUpdateStarted();
+    EXPECT_FALSE(handler->writeMeta(session, 0, {0x01}));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, WriteToUpdateBlobReturnsFailure)
+{
+    getToUpdateStarted();
+    EXPECT_FALSE(handler->write(session, 0, {0x01}));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, ReadFromUpdateBlobReturnsEmpty)
+{
+    getToUpdateStarted();
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallingCommitShouldReturnTrueAndHaveNoEffect)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, trigger()).Times(0);
+
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+}
+
+/*
+ * stat(session) - this will trigger a check, and the state may change.
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallStatChecksActionStatusReturnsRunningDoesNotChangeState)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, status())
+        .WillOnce(Return(ActionStatus::running));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committing;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::running));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallStatChecksActionStatusReturnsFailedChangesStateToCompleted)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, status())
+        .WillOnce(Return(ActionStatus::failed));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallStatChecksActionStatusReturnsSuccessChangesStateToCompleted)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, status())
+        .WillOnce(Return(ActionStatus::success));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+/*
+ * close(session) - this will abort.
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, CloseOnUpdateDuringUpdateAbortsProcess)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, abort()).Times(1);
+
+    EXPECT_TRUE(handler->close(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_uploadinprogress_unittest.cpp b/bmc/test/firmware_state_uploadinprogress_unittest.cpp
new file mode 100644
index 0000000..005ca3f
--- /dev/null
+++ b/bmc/test/firmware_state_uploadinprogress_unittest.cpp
@@ -0,0 +1,284 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is uploadInProgress.  This state is achieved when an image
+ * or hash blob is opened and the handler is expected to receive bytes.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::ContainerEq;
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+class FirmwareHandlerUploadInProgressTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+TEST_F(FirmwareHandlerUploadInProgressTest, GetBlobIdsVerifyOutputActiveImage)
+{
+    /* Opening the image file will add the active image blob id */
+    openToInProgress(staticLayoutBlobId);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(
+                    {staticLayoutBlobId, hashBlobId, activeImageBlobId}));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, GetBlobIdsVerifyOutputActiveHash)
+{
+    /* Opening the image file will add the active image blob id */
+    openToInProgress(hashBlobId);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(
+                    {staticLayoutBlobId, hashBlobId, activeHashBlobId}));
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, StatOnActiveImageReturnsFailure)
+{
+    /* you cannot call stat() on the active image or the active hash or the
+     * verify blob.
+     */
+    openToInProgress(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, StatOnActiveHashReturnsFailure)
+{
+    /* this test is separate from the active image one so that the state doesn't
+     * change from close.
+     */
+    openToInProgress(hashBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeHashBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeHashBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, StatOnNormalBlobsReturnsSuccess)
+{
+    /* Calling stat() on the normal blobs (not the active) ones will work and
+     * return the same information as in the notYetStarted state.
+     */
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    openToInProgress(staticLayoutBlobId);
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * stat(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       CallingStatOnActiveImageOrHashSessionReturnsDetails)
+{
+    /* This test will verify that the underlying image handler is called with
+     * this stat, in addition to the normal information.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    EXPECT_CALL(imageMock, getSize()).WillOnce(Return(32));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 32;
+    expectedMeta.blobState =
+        blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi;
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+/*
+ * open(blob) - While any blob is open, all other fail.
+ *
+ * The fullBlobsList is all the blob_ids present if both /flash/image and
+ * /flash/hash are opened, and one is left open (so there's no verify blob). if
+ * left closed, we'd be in verificationPending, not uploadInProgress.
+ */
+const std::vector<std::string> fullBlobsList = {
+    activeHashBlobId, activeImageBlobId, hashBlobId, staticLayoutBlobId};
+
+TEST_F(FirmwareHandlerUploadInProgressTest, OpeningHashFileWhileImageOpenFails)
+{
+    /* To be in this state, something must be open (and specifically either an
+     * active image (or tarball) or the hash file. Also verifies you can't just
+     * re-open the currently open file.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    for (const auto& blob : fullBlobsList)
+    {
+        EXPECT_FALSE(handler->open(2, flags, blob));
+    }
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, OpeningImageFileWhileHashOpenFails)
+{
+    openToInProgress(hashBlobId);
+
+    for (const auto& blob : fullBlobsList)
+    {
+        EXPECT_FALSE(handler->open(2, flags, blob));
+    }
+}
+
+/*
+ * close(session) - closing the hash or image will trigger a state transition to
+ * verificationPending.
+ *
+ * NOTE: Re-opening /flash/image will transition back to uploadInProgress, but
+ * that is verified in the verificationPending::open tests.
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       ClosingImageFileTransitionsToVerificationPending)
+{
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+    openToInProgress(staticLayoutBlobId);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    EXPECT_TRUE(handler->canHandleBlob(verifyBlobId));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       ClosingHashFileTransitionsToVerificationPending)
+{
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+    openToInProgress(hashBlobId);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    EXPECT_TRUE(handler->canHandleBlob(verifyBlobId));
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       WriteMetaAgainstImageReturnsFailureIfNoDataHandler)
+{
+    /* Calling write/read/writeMeta are uninteresting against the open blob in
+     * this case because the blob will just pass the call along.  Whereas
+     * calling against the verify or update blob may be more interesting.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    /* TODO: Consider adding a test that has a data handler, but that test
+     * already exists under the general writeMeta test suite.
+     */
+    /* Note: with IPMI as the transport there's no data handler, so this should
+     * fail nicely. */
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, WriteToImageReturnsSuccess)
+{
+    openToInProgress(staticLayoutBlobId);
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_CALL(imageMock, write(0, ContainerEq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(session, 0, bytes));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, WriteToHashReturnsSuccess)
+{
+    openToInProgress(hashBlobId);
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_CALL(imageMock, write(0, ContainerEq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(session, 0, bytes));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, ReadImageFileReturnsFailure)
+{
+    /* Read is not supported. */
+    openToInProgress(staticLayoutBlobId);
+    EXPECT_THAT(handler->read(session, 0, 32), IsEmpty());
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       CommitAgainstImageFileReturnsFailure)
+{
+    /* Commit is only valid against specific blobs. */
+    openToInProgress(staticLayoutBlobId);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, CommitAgainstHashFileReturnsFailure)
+{
+    openToInProgress(hashBlobId);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    openToInProgress(staticLayoutBlobId);
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_verificationcompleted_unittest.cpp b/bmc/test/firmware_state_verificationcompleted_unittest.cpp
new file mode 100644
index 0000000..8754e15
--- /dev/null
+++ b/bmc/test/firmware_state_verificationcompleted_unittest.cpp
@@ -0,0 +1,316 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is verificationCompleted.  This state is achieved as a out
+ * of verificationStarted.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Like the state verificationStarted, there is a file open in
+ * verificationCompleted.  This state is transitioned to after a stat() command
+ * indicates a successful verification.
+ */
+
+class FirmwareHandlerVerificationCompletedTest
+    : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * canHandleBlob
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OnVerificationCompleteSuccessUpdateBlobIdNotPresent)
+{
+    /* the uploadBlobId is only added on close() of the verifyBlobId.  This is a
+     * consistent behavior with verifyBlobId only added when closing the image
+     * or hash.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_FALSE(handler->canHandleBlob(updateBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OnVerificationCompleteFailureUpdateBlobIdNotPresent)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    EXPECT_FALSE(handler->canHandleBlob(updateBlobId));
+}
+
+/*
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, GetBlobIdsReturnsExpectedList)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_THAT(
+        handler->getBlobIds(),
+        UnorderedElementsAreArray(
+            {verifyBlobId, hashBlobId, activeImageBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       StatOnActiveImageReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       VerifyActiveHashIdMissingInThisCase)
+{
+    /* The path taken to get to this state never opened the hash blob Id, which
+     * is fine.  But let's verify it behaved as intended.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_FALSE(handler->canHandleBlob(activeHashBlobId));
+}
+
+/* TODO: Add sufficient warning that you can get to verificationCompleted
+ * without ever opening the image blob id (or the tarball one).
+ *
+ * Although in this case, it's expected that any verification triggered would
+ * certainly fail.  So, although it's possible, it's uninteresting.
+ */
+
+TEST_F(FirmwareHandlerVerificationCompletedTest, StatOnVerifyBlobReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(verifyBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       StatOnNormalBlobsReturnsSuccess)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * stat(session) - the verify blobid is open in this state, so stat on that once
+ * completed should have no effect.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       SessionStatOnVerifyAfterSuccessDoesNothing)
+{
+    /* Every time you stat() once it's triggered, it checks the state again
+     * until it's completed.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_CALL(*verifyMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       SessionStatOnVerifyAfterFailureDoesNothing)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    EXPECT_CALL(*verifyMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+/*
+ * open(blob) - all open should fail
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OpeningAnyBlobAvailableFailsAfterSuccess)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OpeningAnyBlobAvailableFailsAfterFailure)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/*
+ * writemeta(session) - write meta should fail.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       WriteMetaToVerifyBlobReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session) - write should fail.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       WriteToVerifyBlobReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->write(session, 0, bytes));
+}
+
+/*
+ * read(session) - read returns empty.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, ReadOfVerifyBlobReturnsEmpty)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * commit(session) - returns failure
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       CommitOnVerifyBlobAfterSuccessReturnsFailure)
+{
+    /* If you've started this'll return success, but if it's finished, it won't
+     * let you try-again.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_CALL(*verifyMockPtr, trigger()).Times(0);
+
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       CommitOnVerifyBlobAfterFailureReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    EXPECT_CALL(*verifyMockPtr, trigger()).Times(0);
+
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+/*
+ * close(session) - close on the verify blobid:
+ *   1. if successful adds update blob id, changes state to UpdatePending
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       CloseAfterSuccessChangesStateAddsUpdateBlob)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    ASSERT_FALSE(handler->canHandleBlob(updateBlobId));
+
+    handler->close(session);
+    EXPECT_TRUE(handler->canHandleBlob(updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+/*
+ * close(session) - close on the verify blobid:
+ *   2. if unsuccessful it aborts.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, CloseAfterFailureAborts)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    ASSERT_FALSE(handler->canHandleBlob(updateBlobId));
+
+    handler->close(session);
+    ASSERT_FALSE(handler->canHandleBlob(updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_verificationpending_unittest.cpp b/bmc/test/firmware_state_verificationpending_unittest.cpp
new file mode 100644
index 0000000..dd2df27
--- /dev/null
+++ b/bmc/test/firmware_state_verificationpending_unittest.cpp
@@ -0,0 +1,330 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is verificationPending.  This state is achieved as a
+ * transition out of uploadInProgress.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+
+class FirmwareHandlerVerificationPendingTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, VerifyBlobIdAvailableInState)
+{
+    /* Only in the verificationPending state (and later), should the
+     * verifyBlobId be present.
+     */
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->canHandleBlob(verifyBlobId));
+    EXPECT_TRUE(handler->canHandleBlob(activeImageBlobId));
+    EXPECT_FALSE(handler->canHandleBlob(updateBlobId));
+}
+
+/*
+ * delete(blob)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteVerifyPendingAbortsProcess)
+{
+    /* It doesn't matter what blob id is used to delete in the design, so just
+     * delete the verify blob id
+     */
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+    EXPECT_TRUE(handler->deleteBlob(verifyBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteActiveImageAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+    EXPECT_TRUE(handler->deleteBlob(activeImageBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteStaticLayoutAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(staticLayoutBlobId));
+    EXPECT_TRUE(handler->deleteBlob(staticLayoutBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteHashAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(hashBlobId));
+    EXPECT_TRUE(handler->deleteBlob(hashBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnActiveImageReturnsFailure)
+{
+    getToVerificationPending(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnActiveHashReturnsFailure)
+{
+    getToVerificationPending(hashBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeHashBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeHashBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       StatOnVerificationBlobReturnsFailure)
+{
+    getToVerificationPending(hashBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(verifyBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * open(blob)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, OpenVerifyBlobSucceeds)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    /* the session is safe because it was already closed to get to this state.
+     */
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, OpenActiveBlobsFail)
+{
+    /* Try opening the active blob Id.  This test is equivalent to trying to
+     * open the active hash blob id, in that neither are ever allowed.
+     */
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_FALSE(handler->open(session, flags, activeImageBlobId));
+    EXPECT_FALSE(handler->open(session, flags, activeHashBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       OpenImageBlobTransitionsToUploadInProgress)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    /* Verify the active blob for the image is in the list once to start.
+     * Note: This is truly tested under the notYetStarted::open() test.
+     */
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId,
+                                              verifyBlobId, activeImageBlobId};
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+
+    /* Verifies it isn't triggered again. */
+    EXPECT_CALL(*prepareMockPtr, trigger()).Times(0);
+
+    EXPECT_CALL(imageMock, open(staticLayoutBlobId)).WillOnce(Return(true));
+    EXPECT_TRUE(handler->open(session, flags, staticLayoutBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    expectedBlobs.erase(
+        std::remove(expectedBlobs.begin(), expectedBlobs.end(), verifyBlobId),
+        expectedBlobs.end());
+
+    /* Verify the active blob ID was not added to the list twice and
+     * verifyBlobId is removed
+     */
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+}
+
+/*
+ * close(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       ClosingVerifyBlobWithoutCommitDoesNotChangeState)
+{
+    /* commit() will change the state, closing post-commit is part of
+     * verificationStarted testing.
+     */
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       CommitOnVerifyBlobTriggersVerificationAndStateTransition)
+{
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    EXPECT_CALL(*verifyMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+}
+
+/*
+ * stat(session) - in this state, you can only open(verifyBlobId) without
+ * changing state.
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnVerifyBlobIdReturnsState)
+{
+    /* If this is called before commit(), it's still verificationPending, so it
+     * just returns the state is other
+     */
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    EXPECT_CALL(*verifyMockPtr, trigger()).Times(0);
+    EXPECT_CALL(*verifyMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::unknown));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, WriteMetaAgainstVerifyFails)
+{
+    /* The verifyBlobId has no data handler, which means write meta fails. */
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, WriteAgainstVerifyBlobIdFails)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->write(session, 0, bytes));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       ReadAgainstVerifyBlobIdReturnsEmpty)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_state_verificationstarted_unittest.cpp b/bmc/test/firmware_state_verificationstarted_unittest.cpp
new file mode 100644
index 0000000..74e24b2
--- /dev/null
+++ b/bmc/test/firmware_state_verificationstarted_unittest.cpp
@@ -0,0 +1,293 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is verificationStarted.  This state is achieved as a out of
+ * verificationPending.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+
+class FirmwareHandlerVerificationStartedTest : public IpmiOnlyFirmwareStaticTest
+{
+};
+
+/*
+ * canHandleBlob(blob)
+ * getBlobIds()
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, GetBlobIdsReturnsExpectedList)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    auto blobs = handler->getBlobIds();
+    EXPECT_THAT(
+        blobs, UnorderedElementsAreArray({activeImageBlobId, staticLayoutBlobId,
+                                          hashBlobId, verifyBlobId}));
+
+    for (const auto& blob : blobs)
+    {
+        EXPECT_TRUE(handler->canHandleBlob(blob));
+    }
+}
+
+/*
+ * stat(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitChecksStateAndReturnsRunning)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::running));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committing;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::running));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitChecksStateAndReturnsOther)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::unknown));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committing;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::unknown));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitCheckStateAndReturnsFailed)
+{
+    /* If the returned state from the verification handler is failed, it sets
+     * commit_error and transitions to verificationCompleted.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::failed));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitCheckStateAndReturnsSuccess)
+{
+    /* If the returned state from the verification handler is success, it sets
+     * committed and transitions to verificationCompleted.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::success));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnActiveImageReturnsFailure)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnActiveHashReturnsFailure)
+{
+    getToVerificationStarted(hashBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeHashBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeHashBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnVerifyBlobReturnsFailure)
+{
+    /* the verifyBlobId is available starting at verificationPending. */
+    getToVerificationStarted(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(verifyBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    blobs::BlobMeta expected;
+    expected.blobState = FirmwareBlobHandler::UpdateFlags::ipmi;
+    expected.size = 0;
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expected, meta);
+    }
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       WriteMetaOnVerifySessionReturnsFailure)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       WriteOnVerifySessionReturnsFailure)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->write(session, 0, bytes));
+}
+
+/*
+ * open(blob) - there is nothing you can open, this state has an open file.
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       AttemptToOpenImageFileReturnsFailure)
+{
+    /* Attempt to open a file one normally can open, however, as there is
+     * already a file open, this will fail.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+
+    auto blobsToOpen = handler->getBlobIds();
+    for (const auto& blob : blobsToOpen)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, ReadOfVerifyBlobReturnsEmpty)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       CommitOnVerifyDuringVerificationHasNoImpact)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+}
+
+/*
+ * close(session) - close while state if verificationStarted without calling
+ * stat first will abort.
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       CloseOnVerifyDuringVerificationAbortsProcess)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(1);
+
+    EXPECT_TRUE(handler->close(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_unittest.hpp b/bmc/test/firmware_unittest.hpp
new file mode 100644
index 0000000..8d92fe8
--- /dev/null
+++ b/bmc/test/firmware_unittest.hpp
@@ -0,0 +1,184 @@
+#pragma once
+
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+
+class IpmiOnlyFirmwareStaticTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        blobs = {
+            {hashBlobId, &imageMock},
+            {staticLayoutBlobId, &imageMock},
+        };
+
+        std::unique_ptr<TriggerableActionInterface> prepareMock =
+            std::make_unique<TriggerMock>();
+        prepareMockPtr = reinterpret_cast<TriggerMock*>(prepareMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> verifyMock =
+            std::make_unique<TriggerMock>();
+        verifyMockPtr = reinterpret_cast<TriggerMock*>(verifyMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> updateMock =
+            std::make_unique<TriggerMock>();
+        updateMockPtr = reinterpret_cast<TriggerMock*>(updateMock.get());
+
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            blobs, data, std::move(prepareMock), std::move(verifyMock),
+            std::move(updateMock));
+    }
+
+    void expectedState(FirmwareBlobHandler::UpdateState state)
+    {
+        auto realHandler = dynamic_cast<FirmwareBlobHandler*>(handler.get());
+        EXPECT_EQ(state, realHandler->getCurrentState());
+    }
+
+    void openToInProgress(const std::string& blobId)
+    {
+        EXPECT_CALL(imageMock, open(blobId)).WillOnce(Return(true));
+        EXPECT_CALL(*prepareMockPtr, trigger()).WillOnce(Return(true));
+        EXPECT_TRUE(handler->open(session, flags, blobId));
+        expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+    }
+
+    void getToVerificationPending(const std::string& blobId)
+    {
+        openToInProgress(blobId);
+
+        EXPECT_CALL(imageMock, close()).WillRepeatedly(Return());
+        handler->close(session);
+        expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+    }
+
+    void getToVerificationStarted(const std::string& blobId)
+    {
+        getToVerificationPending(blobId);
+
+        EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+        EXPECT_CALL(*verifyMockPtr, trigger()).WillOnce(Return(true));
+
+        EXPECT_TRUE(handler->commit(session, {}));
+        expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+    }
+
+    void getToVerificationCompleted(ActionStatus checkResponse)
+    {
+        getToVerificationStarted(staticLayoutBlobId);
+
+        EXPECT_CALL(*verifyMockPtr, status()).WillOnce(Return(checkResponse));
+        blobs::BlobMeta meta;
+        EXPECT_TRUE(handler->stat(session, &meta));
+        expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+    }
+
+    void getToUpdatePending()
+    {
+        getToVerificationCompleted(ActionStatus::success);
+
+        handler->close(session);
+        expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+    }
+
+    void getToUpdateStarted()
+    {
+        getToUpdatePending();
+        EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+
+        EXPECT_CALL(*updateMockPtr, trigger()).WillOnce(Return(true));
+        EXPECT_TRUE(handler->commit(session, {}));
+        expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+    }
+
+    void getToUpdateCompleted(ActionStatus result)
+    {
+        getToUpdateStarted();
+        EXPECT_CALL(*updateMockPtr, status()).WillOnce(Return(result));
+
+        blobs::BlobMeta meta;
+        EXPECT_TRUE(handler->stat(session, &meta));
+        expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+    }
+
+    ImageHandlerMock imageMock;
+    std::vector<HandlerPack> blobs;
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr}};
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+    TriggerMock* prepareMockPtr;
+    TriggerMock* verifyMockPtr;
+    TriggerMock* updateMockPtr;
+
+    std::uint16_t session = 1;
+    std::uint16_t flags =
+        blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi;
+
+    std::vector<std::string> startingBlobs = {staticLayoutBlobId, hashBlobId};
+};
+
+class IpmiOnlyFirmwareTest : public ::testing::Test
+{
+  protected:
+    ImageHandlerMock imageMock;
+    std::vector<HandlerPack> blobs;
+    std::vector<DataHandlerPack> data = {
+        {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr}};
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+
+    void SetUp() override
+    {
+        blobs = {
+            {hashBlobId, &imageMock},
+            {"asdf", &imageMock},
+        };
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+            CreateTriggerMock());
+    }
+};
+
+class FakeLpcFirmwareTest : public ::testing::Test
+{
+  protected:
+    DataHandlerMock dataMock;
+    ImageHandlerMock imageMock;
+    std::vector<HandlerPack> blobs;
+    std::vector<DataHandlerPack> data;
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+
+    void SetUp() override
+    {
+        blobs = {
+            {hashBlobId, &imageMock},
+            {"asdf", &imageMock},
+        };
+        data = {
+            {FirmwareBlobHandler::UpdateFlags::ipmi, nullptr},
+            {FirmwareBlobHandler::UpdateFlags::lpc, &dataMock},
+        };
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            blobs, data, CreateTriggerMock(), CreateTriggerMock(),
+            CreateTriggerMock());
+    }
+};
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_write_unittest.cpp b/bmc/test/firmware_write_unittest.cpp
new file mode 100644
index 0000000..d74f3c8
--- /dev/null
+++ b/bmc/test/firmware_write_unittest.cpp
@@ -0,0 +1,88 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+using ::testing::Eq;
+using ::testing::Return;
+
+class FirmwareHandlerWriteTestIpmiOnly : public IpmiOnlyFirmwareTest
+{
+};
+
+class FirmwareHandlerWriteTestLpc : public FakeLpcFirmwareTest
+{
+};
+
+TEST_F(FirmwareHandlerWriteTestIpmiOnly, DataTypeIpmiWriteSuccess)
+{
+    /* Verify if data type ipmi, it calls write with the bytes. */
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi,
+        "asdf"));
+
+    std::vector<std::uint8_t> bytes = {0xaa, 0x55};
+
+    EXPECT_CALL(imageMock, write(0, Eq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(0, 0, bytes));
+}
+
+TEST_F(FirmwareHandlerWriteTestLpc, DataTypeNonIpmiWriteSuccess)
+{
+    /* Verify if data type non-ipmi, it calls write with the length. */
+    EXPECT_CALL(dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::lpc,
+        "asdf"));
+
+    struct ExtChunkHdr request;
+    request.length = 4; /* number of bytes to read. */
+    std::vector<std::uint8_t> ipmiRequest;
+    ipmiRequest.resize(sizeof(request));
+    std::memcpy(ipmiRequest.data(), &request, sizeof(request));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(dataMock, copyFrom(request.length)).WillOnce(Return(bytes));
+    EXPECT_CALL(imageMock, write(0, Eq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(0, 0, ipmiRequest));
+}
+
+TEST_F(FirmwareHandlerWriteTestLpc, DataTypeNonIpmiWriteFailsBadRequest)
+{
+    /* Verify the data type non-ipmi, if the request's structure doesn't match,
+     * return failure. */
+    EXPECT_CALL(dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::lpc,
+        "asdf"));
+
+    struct ExtChunkHdr request;
+    request.length = 4; /* number of bytes to read. */
+
+    std::vector<std::uint8_t> ipmiRequest;
+    ipmiRequest.resize(sizeof(request));
+    std::memcpy(ipmiRequest.data(), &request, sizeof(request));
+    ipmiRequest.push_back(1);
+
+    /* ipmiRequest is too large by one byte. */
+    EXPECT_FALSE(handler->write(0, 0, ipmiRequest));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/firmware_writemeta_unittest.cpp b/bmc/test/firmware_writemeta_unittest.cpp
new file mode 100644
index 0000000..491d669
--- /dev/null
+++ b/bmc/test/firmware_writemeta_unittest.cpp
@@ -0,0 +1,50 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+using ::testing::Eq;
+using ::testing::Return;
+
+class FirmwareHandlerWriteMetaTest : public FakeLpcFirmwareTest
+{
+};
+
+TEST_F(FirmwareHandlerWriteMetaTest, WriteConfigParametersFailIfOverIPMI)
+{
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::ipmi,
+        "asdf"));
+
+    std::vector<std::uint8_t> bytes = {0xaa, 0x55};
+
+    EXPECT_FALSE(handler->writeMeta(0, 0, bytes));
+}
+
+TEST_F(FirmwareHandlerWriteMetaTest, WriteConfigParametersPassedThrough)
+{
+    EXPECT_CALL(dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareBlobHandler::UpdateFlags::lpc,
+        "asdf"));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(dataMock, writeMeta(Eq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->writeMeta(0, 0, bytes));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/image_mock.hpp b/bmc/test/image_mock.hpp
new file mode 100644
index 0000000..fb10c39
--- /dev/null
+++ b/bmc/test/image_mock.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "image_handler.hpp"
+
+#include <gmock/gmock.h>
+
+namespace ipmi_flash
+{
+
+class ImageHandlerMock : public ImageHandlerInterface
+{
+  public:
+    virtual ~ImageHandlerMock() = default;
+
+    MOCK_METHOD1(open, bool(const std::string&));
+    MOCK_METHOD0(close, void());
+    MOCK_METHOD2(write, bool(std::uint32_t, const std::vector<std::uint8_t>&));
+    MOCK_METHOD0(getSize, int());
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/test/triggerable_mock.hpp b/bmc/test/triggerable_mock.hpp
new file mode 100644
index 0000000..c485d8e
--- /dev/null
+++ b/bmc/test/triggerable_mock.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "status.hpp"
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+// TriggerableActionInterface
+
+class TriggerMock : public TriggerableActionInterface
+{
+  public:
+    MOCK_METHOD0(trigger, bool());
+    MOCK_METHOD0(abort, void());
+    MOCK_METHOD0(status, ActionStatus());
+};
+
+std::unique_ptr<TriggerableActionInterface> CreateTriggerMock()
+{
+    return std::make_unique<TriggerMock>();
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/test/window_mapper_mock.hpp b/bmc/test/window_mapper_mock.hpp
new file mode 100644
index 0000000..dd3a89b
--- /dev/null
+++ b/bmc/test/window_mapper_mock.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "window_hw_interface.hpp"
+
+#include <cstdint>
+#include <utility>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+namespace ipmi_flash
+{
+
+class HardwareInterfaceMock : public HardwareMapperInterface
+{
+  public:
+    virtual ~HardwareInterfaceMock() = default;
+
+    MOCK_METHOD0(open, MemorySet());
+    MOCK_METHOD0(close, ());
+    MOCK_METHOD2(mapWindow, WindowMapResult(std::uint32_t, std::uint32_t));
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/update_systemd.cpp b/bmc/update_systemd.cpp
new file mode 100644
index 0000000..d8f2329
--- /dev/null
+++ b/bmc/update_systemd.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "update_systemd.hpp"
+
+#include "status.hpp"
+
+#include <memory>
+#include <sdbusplus/bus.hpp>
+#include <string>
+
+namespace ipmi_flash
+{
+
+std::unique_ptr<TriggerableActionInterface>
+    SystemdUpdateMechanism::CreateSystemdUpdate(sdbusplus::bus::bus&& bus,
+                                                const std::string& target,
+                                                const std::string& mode)
+{
+    return std::make_unique<SystemdUpdateMechanism>(std::move(bus), target,
+                                                    mode);
+}
+
+bool SystemdUpdateMechanism::trigger()
+{
+    /* TODO: Add a util method for triggering a service with optional additional
+     * parameter. */
+    static constexpr auto systemdService = "org.freedesktop.systemd1";
+    static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
+    static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
+
+    auto method = bus.new_method_call(systemdService, systemdRoot,
+                                      systemdInterface, "StartUnit");
+    method.append(target);
+
+    if (!mode.empty())
+    {
+        method.append(mode);
+    }
+
+    try
+    {
+        bus.call_noreply(method);
+        return true;
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        return false;
+    }
+}
+
+void SystemdUpdateMechanism::abort()
+{
+    return;
+}
+
+ActionStatus SystemdUpdateMechanism::status()
+{
+    /* For now, don't check if the target failed. */
+    return ActionStatus::running;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/update_systemd.hpp b/bmc/update_systemd.hpp
new file mode 100644
index 0000000..15d616f
--- /dev/null
+++ b/bmc/update_systemd.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "status.hpp"
+
+#include <memory>
+#include <sdbusplus/bus.hpp>
+#include <string>
+
+namespace ipmi_flash
+{
+
+/**
+ * Implements the update interface by simply triggering a systemd unit.
+ */
+class SystemdUpdateMechanism : public TriggerableActionInterface
+{
+  public:
+    static std::unique_ptr<TriggerableActionInterface>
+        CreateSystemdUpdate(sdbusplus::bus::bus&& bus,
+                            const std::string& target,
+                            const std::string& mode = "");
+
+    SystemdUpdateMechanism(sdbusplus::bus::bus&& bus, const std::string& target,
+                           const std::string& mode) :
+        bus(std::move(bus)),
+        target(target), mode(mode)
+    {
+    }
+
+    ~SystemdUpdateMechanism() = default;
+    SystemdUpdateMechanism(const SystemdUpdateMechanism&) = delete;
+    SystemdUpdateMechanism& operator=(const SystemdUpdateMechanism&) = delete;
+    SystemdUpdateMechanism(SystemdUpdateMechanism&&) = default;
+    SystemdUpdateMechanism& operator=(SystemdUpdateMechanism&&) = default;
+
+    bool trigger() override;
+    void abort() override;
+    ActionStatus status() override;
+
+  private:
+    sdbusplus::bus::bus bus;
+    const std::string target;
+    const std::string mode;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/verify_systemd.cpp b/bmc/verify_systemd.cpp
new file mode 100644
index 0000000..f7b88b0
--- /dev/null
+++ b/bmc/verify_systemd.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "verify_systemd.hpp"
+
+#include "status.hpp"
+
+#include <fstream>
+#include <memory>
+#include <sdbusplus/bus.hpp>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+std::unique_ptr<TriggerableActionInterface>
+    SystemdVerification::CreateVerification(sdbusplus::bus::bus&& bus,
+                                            const std::string& path,
+                                            const std::string& service)
+{
+    return std::make_unique<SystemdVerification>(std::move(bus), path, service);
+}
+
+bool SystemdVerification::trigger()
+{
+    static constexpr auto systemdService = "org.freedesktop.systemd1";
+    static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
+    static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
+
+    auto method = bus.new_method_call(systemdService, systemdRoot,
+                                      systemdInterface, "StartUnit");
+    method.append(triggerService);
+    method.append("replace");
+
+    try
+    {
+        bus.call_noreply(method);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        /* TODO: Once logging supports unit-tests, add a log message to test
+         * this failure.
+         */
+        return false;
+    }
+
+    return true;
+}
+
+void SystemdVerification::abort()
+{
+    /* TODO: Implement this. */
+}
+
+ActionStatus SystemdVerification::status()
+{
+    ActionStatus result = ActionStatus::unknown;
+
+    std::ifstream ifs;
+    ifs.open(checkPath);
+    if (ifs.good())
+    {
+        /*
+         * Check for the contents of the file, accepting:
+         * running, success, or failed.
+         */
+        std::string status;
+        ifs >> status;
+        if (status == "running")
+        {
+            result = ActionStatus::running;
+        }
+        else if (status == "success")
+        {
+            result = ActionStatus::success;
+        }
+        else if (status == "failed")
+        {
+            result = ActionStatus::failed;
+        }
+    }
+
+    return result;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/verify_systemd.hpp b/bmc/verify_systemd.hpp
new file mode 100644
index 0000000..894b534
--- /dev/null
+++ b/bmc/verify_systemd.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "status.hpp"
+
+#include <memory>
+#include <sdbusplus/bus.hpp>
+#include <string>
+
+namespace ipmi_flash
+{
+
+/**
+ * Representation of what is used for verification.  Currently, this reduces the
+ * chance of error by using an object instead of two strings to control the
+ * verification step, however, it leaves room for a future possibility out
+ * something wholly configurable.
+ */
+class SystemdVerification : public TriggerableActionInterface
+{
+  public:
+    /**
+     * Create a default Verification object that uses systemd to trigger the
+     * process.
+     *
+     * @param[in] bus - an sdbusplus handler for a bus to use.
+     * @param[in] path - the path to check for verification status.
+     * @param[in[ service - the systemd service to start to trigger
+     * verification.
+     */
+    static std::unique_ptr<TriggerableActionInterface>
+        CreateVerification(sdbusplus::bus::bus&& bus, const std::string& path,
+                           const std::string& service);
+
+    SystemdVerification(sdbusplus::bus::bus&& bus, const std::string& path,
+                        const std::string& service) :
+        bus(std::move(bus)),
+        checkPath(path), triggerService(service)
+    {
+    }
+
+    ~SystemdVerification() = default;
+    SystemdVerification(const SystemdVerification&) = delete;
+    SystemdVerification& operator=(const SystemdVerification&) = delete;
+    SystemdVerification(SystemdVerification&&) = default;
+    SystemdVerification& operator=(SystemdVerification&&) = default;
+
+    bool trigger() override;
+    void abort() override;
+    ActionStatus status() override;
+
+  private:
+    sdbusplus::bus::bus bus;
+    const std::string checkPath;
+    const std::string triggerService;
+};
+} // namespace ipmi_flash
diff --git a/bmc/window_hw_interface.hpp b/bmc/window_hw_interface.hpp
new file mode 100644
index 0000000..7d15521
--- /dev/null
+++ b/bmc/window_hw_interface.hpp
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <cstdint>
+#include <utility>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+struct MemorySet
+{
+    int mappedFd = -1;
+    std::uint8_t* mapped = nullptr;
+};
+
+/** The result from the mapWindow command. */
+struct WindowMapResult
+{
+    /* The response can validly be 0, or EFBIG.  If it's EFBIG that means the
+     * region available is within the requested region. If the value is anything
+     * else, it's a complete failure.
+     */
+    std::uint8_t response;
+    std::uint32_t windowOffset;
+    std::uint32_t windowSize;
+};
+
+/**
+ * Different LPC (or P2a) memory map implementations may require different
+ * mechanisms for specific tasks such as mapping the memory window or copying
+ * out data.
+ */
+class HardwareMapperInterface
+{
+  public:
+    virtual ~HardwareMapperInterface() = default;
+
+    /**
+     * Open the driver or whatever and map the region.
+     */
+    virtual MemorySet open() = 0;
+
+    /**
+     * Close the mapper.  This could mean, send an ioctl to turn off the region,
+     * or unmap anything mmapped.
+     */
+    virtual void close() = 0;
+
+    /**
+     * Returns a windowOffset and windowSize if the requested window was mapped.
+     *
+     * TODO: If the length requested is too large, windowSize will be written
+     * with the max size that the BMC can map and returns false.
+     *
+     * @param[in] address - The address for mapping (passed to LPC window)
+     * @param[in] length - The length of the region
+     * @return WindowMapResult - the result of the call
+     */
+    virtual WindowMapResult mapWindow(std::uint32_t address,
+                                      std::uint32_t length) = 0;
+};
+
+} // namespace ipmi_flash