diff options
author | Patrick Venture <venture@google.com> | 2019-03-05 14:01:00 -0800 |
---|---|---|
committer | Patrick Venture <venture@google.com> | 2019-03-06 07:51:32 -0800 |
commit | 123b5c0910e000cf9b00a37146aae99a835f3063 (patch) | |
tree | e1aacf85711d9c5f7e8ad87a1c8e671e09f61db8 | |
parent | 85e320906546f3e5a0dfe1ab54a826517dae2a0d (diff) | |
download | ipmi-blob-tool-123b5c0910e000cf9b00a37146aae99a835f3063.tar.gz ipmi-blob-tool-123b5c0910e000cf9b00a37146aae99a835f3063.zip |
initial commit
Add initial code from phosphor-ipmi-flash/tools that was not specific to
firmware update over ipmi-blobs.
Change-Id: I360537a7392347fe989397a699f6a712bc36e62c
Signed-off-by: Patrick Venture <venture@google.com>
-rw-r--r-- | .clang-format | 99 | ||||
-rw-r--r-- | .gitignore | 52 | ||||
-rw-r--r-- | Makefile.am | 42 | ||||
-rw-r--r-- | README.md | 26 | ||||
-rwxr-xr-x | bootstrap.sh | 4 | ||||
-rw-r--r-- | configure.ac | 140 | ||||
-rw-r--r-- | src/Makefile.am | 21 | ||||
-rw-r--r-- | src/ipmiblob.pc.in | 10 | ||||
-rw-r--r-- | src/ipmiblob/blob_errors.hpp | 23 | ||||
-rw-r--r-- | src/ipmiblob/blob_handler.cpp | 315 | ||||
-rw-r--r-- | src/ipmiblob/blob_handler.hpp | 108 | ||||
-rw-r--r-- | src/ipmiblob/blob_interface.hpp | 91 | ||||
-rw-r--r-- | src/ipmiblob/crc.cpp | 44 | ||||
-rw-r--r-- | src/ipmiblob/crc.hpp | 20 | ||||
-rw-r--r-- | src/ipmiblob/internal/sys.cpp | 70 | ||||
-rw-r--r-- | src/ipmiblob/internal/sys.hpp | 61 | ||||
-rw-r--r-- | src/ipmiblob/ipmi_errors.hpp | 47 | ||||
-rw-r--r-- | src/ipmiblob/ipmi_handler.cpp | 165 | ||||
-rw-r--r-- | src/ipmiblob/ipmi_handler.hpp | 46 | ||||
-rw-r--r-- | src/ipmiblob/ipmi_interface.hpp | 25 | ||||
-rw-r--r-- | src/ipmiblob/test/ipmi_interface_mock.hpp | 18 | ||||
-rw-r--r-- | test/Makefile.am | 25 | ||||
-rw-r--r-- | test/crc_mock.hpp | 23 | ||||
-rw-r--r-- | test/internal_sys_mock.hpp | 27 | ||||
-rw-r--r-- | test/tools_blob_unittest.cpp | 289 | ||||
-rw-r--r-- | test/tools_ipmi_error_unittest.cpp | 28 | ||||
-rw-r--r-- | test/tools_ipmi_unittest.cpp | 22 |
27 files changed, 1841 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ea71ad6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,99 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +PointerAlignment: Left +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^[<"](gtest|gmock)' + Priority: 5 + - Regex: '^"config.h"' + Priority: -1 + - Regex: '^".*\.hpp"' + Priority: 1 + - Regex: '^<.*\.h>' + Priority: 2 + - Regex: '^<.*' + Priority: 3 + - Regex: '.*' + Priority: 4 +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never +... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89226f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Test suite logs +*.log + +# Code Coverage +*.gcda +*.gcno +*.trs +*-coverage* + +# Compiler +*.o + +# Libtool +*.la +*.lo + +# Autotools +.deps +.dirstamp +.libs +/aclocal.m4 +/autom4te.cache +/build-aux +/config.h +/config.h.in +/config.h.in~ +/config.log +/config.status +/configure +/m4 +/libtool +Makefile +Makefile.in +/stamp-h1 +ar-lib +compile +config.guess +config.sub +depcomp +install-sh +ltmain.sh +missing +test-driver +*_unittest +*_unittest.log +*_unittest.trs +test-suite.log + +# Custom generated files +ipmiblob.pc + +# Output binaries diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..c4335da --- /dev/null +++ b/Makefile.am @@ -0,0 +1,42 @@ +ACLOCAL_AMFLAGS = -I m4 + +# Ignore system headers +CODE_COVERAGE_IGNORE_PATTERN = '/include/*' '/usr/include/*' '$(includedir)/*' +# Ignore the real implementation sources for sys +CODE_COVERAGE_IGNORE_PATTERN += \ + '$(abs_builddir)/src/ipmiblob/internal/sys.*' +export CODE_COVERAGE_IGNORE_PATTERN + +CODE_COVERAGE_LCOV_SHOPTS = $(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) +# Use our configuration file for lcov +CODE_COVERAGE_LCOV_SHOPTS += --config-file $(abs_srcdir)/.lcovrc +export CODE_COVERAGE_LCOV_SHOPTS + +CODE_COVERAGE_LCOV_OPTIONS = $(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) +# Use our configuration file for lcov +CODE_COVERAGE_LCOV_OPTIONS += --config-file $(abs_srcdir)/.lcovrc +export CODE_COVERAGE_LCOV_OPTIONS + +CODE_COVERAGE_LCOV_RMOPTS = $(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) +# Use our configuration file for lcov +CODE_COVERAGE_LCOV_RMOPTS += --config-file $(abs_srcdir)/.lcovrc +export CODE_COVERAGE_LCOV_RMOPTS + +CODE_COVERAGE_GENHTML_OPTIONS = $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +# Use our configuration file for genhtml +CODE_COVERAGE_GENHTML_OPTIONS += --config-file $(abs_srcdir)/.lcovrc +# Don't generate the absolute path for each file in the HTML output +CODE_COVERAGE_GENHTML_OPTIONS += --prefix $(abs_srcdir) --prefix $(abs_builddir) +export CODE_COVERAGE_GENHTML_OPTIONS + +export AM_CPPFLAGS = -I$(abs_builddir)/src -I$(abs_srcdir)/src \ + $(CODE_COVERAGE_CPPFLAGS) +export AM_CFLAGS = $(CODE_COVERAGE_CFLAGS) +export AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS) + +export COMMON_LIBS = $(CODE_COVERAGE_LIBS) +export IPMIBLOB_LIBS = $(abs_builddir)/src/libipmiblob.la $(COMMON_LIBS) + + +EXTRA_DIST = LICENSE MAINTAINERS README.md +SUBDIRS = src test diff --git a/README.md b/README.md new file mode 100644 index 0000000..6de2c1a --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# ipmi-blob-tool + +ipmi-blob-tool is a host-side tool that speaks the BLOB protocol over IPMI. + +## Dependencies + +Test cases require google{test,mock}, valgrind, and lcov. + +## Building +For a standard release build, you want something like: +``` +./bootstrap.sh +./configure --disable-tests +make +make install +``` + +For a test / debug build, a typical configuration is +``` +./bootstrap.sh +./configure --enable-tests --enable-coverage --enable-valgrind +make +make check +make check-valgrind +make check-code-coverage +``` diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..30ef75a --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +set -x +autoreconf -v -f -i diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..b1e8145 --- /dev/null +++ b/configure.ac @@ -0,0 +1,140 @@ +# Initialization +AC_PREREQ([2.69]) +AC_INIT([ipmi-blob-tool], [0.1], [https://github.com/openbmc/ipmi-blob-tool/issues]) +AC_LANG([C++]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_MACRO_DIRS([m4]) +AC_CONFIG_AUX_DIR([build-aux]) +AM_INIT_AUTOMAKE([nostdinc foreign subdir-objects -Wall -Werror dist-xz tar-ustar]) +AM_SILENT_RULES([yes]) + +# Make sure the default CFLAGS of `-O2 -g` don't override CODE_COVERAGE_CFLAGS +# It is important that this comes before AC_PROG_C{C,XX}, as we are attempting +# to stop them from populating default CFLAGS and CXXFLAGS. +AS_IF([test "x$enable_tests" = "xno"], [enable_code_coverage=no]) +AS_IF([test "x$enable_code_coverage" != "xno"], [ + AS_IF([test "x${CXXFLAGS+set}" != "xset"], [ + AC_SUBST(CXXFLAGS, [""]) + ]) + AS_IF([test "x${CFLAGS+set}" != "xset"], [ + AC_SUBST(CFLAGS, [""]) + ]) +]) + +# Checks for programs. +AC_PROG_CXX +AC_PROG_CC +AM_PROG_AR +AC_PROG_INSTALL +AC_PROG_MAKE_SET + +# Checks for libtool +LT_INIT # Removes 'unrecognized options: --with-libtool-sysroot' + +# Make sure the pkgconfigdata is configured for automake +PKG_INSTALLDIR + +# Checks for typedefs, structures, and compiler characteristics. +AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory]) +AX_APPEND_COMPILE_FLAGS([-Wall -Wextra -Wpedantic], [CFLAGS]) +AX_APPEND_COMPILE_FLAGS([-Wall -Wextra -Wpedantic], [CXXFLAGS]) + +# Make it possible for users to choose if they want test support +# explicitly or not at all +AC_ARG_ENABLE([tests], AC_HELP_STRING([--disable-tests], + [Build test cases])) + +# Check/set gtest specific functions. +AS_IF([test "x$enable_tests" != "xno"], [ + PKG_CHECK_MODULES([GTEST], [gtest], [], [true]) + PKG_CHECK_MODULES([GMOCK], [gmock], [], [true]) + AX_PTHREAD + + AX_SAVE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS]) + AX_APPEND_COMPILE_FLAGS([$GTEST_CFLAGS], [CPPFLAGS]) + AC_LANG_PUSH([C++]) + AC_CHECK_HEADERS([gtest/gtest.h], [ + AS_IF([test "x$GTEST_CFLAGS" = "x"], [ + AS_IF([test "x$PTHREAD_CFLAGS" = "x"], [ + AX_APPEND_COMPILE_FLAGS(["-DGTEST_HAS_PTHREAD=0"], [GTEST_CFLAGS]) + ], [ + AX_APPEND_COMPILE_FLAGS(["-DGTEST_HAS_PTHREAD=1"], [GTEST_CFLAGS]) + AX_APPEND_COMPILE_FLAGS([$PTHREAD_CFLAGS], [GTEST_CFLAGS]) + ]) + ]) + ], [ + AS_IF([test "x$enable_tests" = "xyes"], [ + AC_MSG_ERROR([Testing enabled but could not find gtest/gtest.h]) + ]) + ]) + AC_LANG_POP([C++]) + AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS]) + + AX_SAVE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS]) + AX_APPEND_COMPILE_FLAGS([$GMOCK_CFLAGS], [CPPFLAGS]) + AC_LANG_PUSH([C++]) + AC_CHECK_HEADERS([gmock/gmock.h], [], [ + AS_IF([test "x$enable_tests" = "xyes"], [ + AC_MSG_ERROR([Testing enabled but could not find gmock/gmock.h]) + ]) + ]) + AC_LANG_POP([C++]) + AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS]) + + AX_SAVE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS]) + AX_APPEND_COMPILE_FLAGS([$GTEST_LIBS], [LDFLAGS]) + AC_CHECK_LIB([gtest], [main], [ + AS_IF([test "x$GTEST_LIBS" = "x"], [ + AX_APPEND_COMPILE_FLAGS([-lgtest], [GTEST_LIBS]) + ]) + ], [ + AS_IF([test "x$enable_tests" = "xyes"], [ + AC_MSG_ERROR([Testing enabled but couldn't find gtest libs]) + ]) + ]) + AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS]) + + AX_SAVE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS]) + AX_APPEND_COMPILE_FLAGS([$GMOCK_LIBS], [LDFLAGS]) + AC_CHECK_LIB([gmock], [main], [ + AS_IF([test "x$GMOCK_LIBS" = "x"], [ + AX_APPEND_COMPILE_FLAGS([-lgmock], [GMOCK_LIBS]) + ]) + ], [ + AS_IF([test "x$enable_tests" = "xyes"], [ + AC_MSG_ERROR([Testing enabled but couldn't find gmock libs]) + ]) + ]) + AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS]) +]) + +# Check for valgrind +AS_IF([test "x$enable_tests" = "xno"], [enable_valgrind=no]) +m4_foreach([vgtool], [valgrind_tool_list], + [AX_VALGRIND_DFLT(vgtool, [off])]) +AX_VALGRIND_DFLT([memcheck], [on]) +AX_VALGRIND_CHECK +AM_EXTRA_RECURSIVE_TARGETS([check-valgrind]) +m4_foreach([vgtool], [valgrind_tool_list], + [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)]) + +# Code coverage +AX_CODE_COVERAGE +AM_EXTRA_RECURSIVE_TARGETS([check-code-coverage]) +AS_IF([test "x$CODE_COVERAGE_ENABLED" = "xyes"], [ + AX_APPEND_COMPILE_FLAGS([-DHAVE_GCOV], [CODE_COVERAGE_CPPFLAGS]) +]) + +# Append -Werror after doing autoconf compiler checks +# Otherwise some perfectly valid checks can fail and cause our +# final configuratin to be broken. +AC_ARG_ENABLE([werror], AC_HELP_STRING([--disable-werror], [Whether to automatically add -Werror CFLAGS])) +AS_IF([test "x$enable_tests" != "xno"], [ + AX_APPEND_COMPILE_FLAGS([-Werror], [CFLAGS]) + AX_APPEND_COMPILE_FLAGS([-Werror], [CXXFLAGS]) +]) + +# Create configured output +AC_CONFIG_FILES([Makefile src/Makefile test/Makefile]) +AC_CONFIG_FILES([src/ipmiblob.pc]) +AC_OUTPUT diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..38a8564 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,21 @@ +nobase_include_HEADERS = +pkgconfig_DATA = ipmiblob.pc +lib_LTLIBRARIES = libipmiblob.la +libipmiblob_la_SOURCES = +libipmiblob_la_LIBADD = $(COMMON_LIBS) + +# Don't install the crc header. +libipmiblob_la_SOURCES += ipmiblob/crc.cpp + +nobase_include_HEADERS += ipmiblob/blob_interface.hpp +nobase_include_HEADERS += ipmiblob/blob_handler.hpp +libipmiblob_la_SOURCES += ipmiblob/blob_handler.cpp + +nobase_include_HEADERS += ipmiblob/ipmi_interface.hpp +nobase_include_HEADERS += ipmiblob/ipmi_handler.hpp +libipmiblob_la_SOURCES += ipmiblob/ipmi_handler.cpp + +nobase_include_HEADERS += ipmiblob/internal/sys.hpp +libipmiblob_la_SOURCES += ipmiblob/internal/sys.cpp + +nobase_include_HEADERS += ipmiblob/test/ipmi_interface_mock.hpp diff --git a/src/ipmiblob.pc.in b/src/ipmiblob.pc.in new file mode 100644 index 0000000..2b8dd0f --- /dev/null +++ b/src/ipmiblob.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +exec_prefix=@prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: ipmiblob +Description: C++ library for talking to BLOB handlers over IPMI +Version: @VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -lipmiblob diff --git a/src/ipmiblob/blob_errors.hpp b/src/ipmiblob/blob_errors.hpp new file mode 100644 index 0000000..45f0e46 --- /dev/null +++ b/src/ipmiblob/blob_errors.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include <exception> +#include <string> + +namespace host_tool +{ + +class BlobException : public std::exception +{ + public: + explicit BlobException(const std::string& message) : message(message){}; + + virtual const char* what() const noexcept override + { + return message.c_str(); + } + + private: + std::string message; +}; + +} // namespace host_tool diff --git a/src/ipmiblob/blob_handler.cpp b/src/ipmiblob/blob_handler.cpp new file mode 100644 index 0000000..5be0b2d --- /dev/null +++ b/src/ipmiblob/blob_handler.cpp @@ -0,0 +1,315 @@ +/* + * 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 "blob_handler.hpp" + +#include "blob_errors.hpp" +#include "crc.hpp" +#include "ipmi_errors.hpp" + +#include <array> +#include <cstring> + +namespace host_tool +{ + +namespace +{ +const std::array<std::uint8_t, 3> ipmiPhosphorOen = {0xcf, 0xc2, 0x00}; +} + +std::vector<std::uint8_t> + BlobHandler::sendIpmiPayload(BlobOEMCommands command, + const std::vector<std::uint8_t>& payload) +{ + std::vector<std::uint8_t> request, reply, bytes; + + std::copy(ipmiPhosphorOen.begin(), ipmiPhosphorOen.end(), + std::back_inserter(request)); + request.push_back(command); + + if (payload.size() > 0) + { + /* Grow the vector to hold the bytes. */ + request.reserve(request.size() + sizeof(std::uint16_t)); + + /* CRC required. */ + std::uint16_t crc = generateCrc(payload); + auto src = reinterpret_cast<const std::uint8_t*>(&crc); + + std::copy(src, src + sizeof(crc), std::back_inserter(request)); + + /* Copy the payload. */ + std::copy(payload.begin(), payload.end(), std::back_inserter(request)); + } + + try + { + reply = ipmi->sendPacket(request); + } + catch (const IpmiException& e) + { + throw BlobException(e.what()); + } + + /* IPMI_CC was OK, and it returned no bytes, so let's be happy with that for + * now. + */ + if (reply.size() == 0) + { + return reply; + } + + /* This cannot be a response because it's smaller than the smallest + * response. + */ + if (reply.size() < ipmiPhosphorOen.size()) + { + throw BlobException("Invalid response length"); + } + + /* Verify the OEN. */ + if (std::memcmp(ipmiPhosphorOen.data(), reply.data(), + ipmiPhosphorOen.size()) != 0) + { + throw BlobException("Invalid OEN received"); + } + + /* In this case there was no data, as there was no CRC. */ + std::size_t headerSize = ipmiPhosphorOen.size() + sizeof(std::uint16_t); + if (reply.size() < headerSize) + { + return {}; + } + + /* Validate CRC. */ + std::uint16_t crc; + auto ptr = reinterpret_cast<std::uint8_t*>(&crc); + std::memcpy(ptr, &reply[ipmiPhosphorOen.size()], sizeof(crc)); + + for (const auto& byte : reply) + { + std::fprintf(stderr, "0x%02x ", byte); + } + std::fprintf(stderr, "\n"); + + bytes.insert(bytes.begin(), reply.begin() + headerSize, reply.end()); + + auto computed = generateCrc(bytes); + if (crc != computed) + { + std::fprintf(stderr, "Invalid CRC, received: 0x%x, computed: 0x%x\n", + crc, computed); + throw BlobException("Invalid CRC on received data."); + } + + return bytes; +} + +int BlobHandler::getBlobCount() +{ + std::uint32_t count; + try + { + auto resp = sendIpmiPayload(BlobOEMCommands::bmcBlobGetCount, {}); + if (resp.size() != sizeof(count)) + { + return 0; + } + + /* LE to LE (need to make this portable as some point. */ + std::memcpy(&count, resp.data(), sizeof(count)); + } + catch (const BlobException& b) + { + return 0; + } + + std::fprintf(stderr, "BLOB Count: %d\n", count); + return count; +} + +std::string BlobHandler::enumerateBlob(std::uint32_t index) +{ + std::vector<std::uint8_t> payload; + auto data = reinterpret_cast<const std::uint8_t*>(&index); + + std::copy(data, data + sizeof(std::uint32_t), std::back_inserter(payload)); + + try + { + auto resp = sendIpmiPayload(BlobOEMCommands::bmcBlobEnumerate, payload); + return (resp.size() > 0) ? std::string(&resp[0], &resp[resp.size() - 1]) + : ""; + } + catch (const BlobException& b) + { + return ""; + } +} + +void BlobHandler::writeGeneric(BlobOEMCommands command, std::uint16_t session, + std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) +{ + std::vector<std::uint8_t> payload; + + payload.reserve(sizeof(std::uint16_t) + sizeof(std::uint32_t) + + bytes.size()); + + auto data = reinterpret_cast<const std::uint8_t*>(&session); + std::copy(data, data + sizeof(std::uint16_t), std::back_inserter(payload)); + + data = reinterpret_cast<const std::uint8_t*>(&offset); + std::copy(data, data + sizeof(std::uint32_t), std::back_inserter(payload)); + + std::copy(bytes.begin(), bytes.end(), std::back_inserter(payload)); + + auto resp = sendIpmiPayload(command, payload); +} + +void BlobHandler::writeMeta(std::uint16_t session, std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) +{ + return writeGeneric(BlobOEMCommands::bmcBlobWriteMeta, session, offset, + bytes); +} + +void BlobHandler::writeBytes(std::uint16_t session, std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) +{ + return writeGeneric(BlobOEMCommands::bmcBlobWrite, session, offset, bytes); +} + +std::vector<std::string> BlobHandler::getBlobList() +{ + std::vector<std::string> list; + int blobCount = getBlobCount(); + + for (int i = 0; i < blobCount; i++) + { + auto name = enumerateBlob(i); + /* Currently ignore failures. */ + if (!name.empty()) + { + list.push_back(name); + } + } + + return list; +} + +StatResponse BlobHandler::getStat(const std::string& id) +{ + StatResponse meta; + std::vector<std::uint8_t> name, resp; + + std::copy(id.begin(), id.end(), std::back_inserter(name)); + name.push_back(0x00); /* need to add nul-terminator. */ + + try + { + resp = sendIpmiPayload(BlobOEMCommands::bmcBlobStat, name); + } + catch (const BlobException& b) + { + throw; + } + + std::memcpy(&meta.blob_state, &resp[0], sizeof(meta.blob_state)); + std::memcpy(&meta.size, &resp[sizeof(meta.blob_state)], sizeof(meta.size)); + int offset = sizeof(meta.blob_state) + sizeof(meta.size); + std::uint8_t len = resp[offset]; + if (len > 0) + { + std::copy(&resp[offset + 1], &resp[resp.size()], + std::back_inserter(meta.metadata)); + } + + return meta; +} + +std::uint16_t BlobHandler::openBlob(const std::string& id, + std::uint16_t handlerFlags) +{ + std::uint16_t session; + std::vector<std::uint8_t> request, resp; + auto addrFlags = reinterpret_cast<const std::uint8_t*>(&handlerFlags); + + std::copy(addrFlags, addrFlags + sizeof(handlerFlags), + std::back_inserter(request)); + std::copy(id.begin(), id.end(), std::back_inserter(request)); + request.push_back(0x00); /* need to add nul-terminator. */ + + try + { + resp = sendIpmiPayload(BlobOEMCommands::bmcBlobOpen, request); + } + catch (const BlobException& b) + { + throw; + } + + if (resp.size() != sizeof(session)) + { + throw BlobException("Did not receive session."); + } + + std::memcpy(&session, resp.data(), sizeof(session)); + return session; +} + +void BlobHandler::closeBlob(std::uint16_t session) +{ + std::vector<std::uint8_t> request; + auto addrSession = reinterpret_cast<const std::uint8_t*>(&session); + std::copy(addrSession, addrSession + sizeof(session), + std::back_inserter(request)); + + try + { + sendIpmiPayload(BlobOEMCommands::bmcBlobClose, request); + } + catch (const BlobException& b) + { + std::fprintf(stderr, "Received failure on close: %s\n", b.what()); + } + + return; +} + +std::vector<std::uint8_t> BlobHandler::readBytes(std::uint16_t session, + std::uint32_t offset, + std::uint32_t length) +{ + std::vector<std::uint8_t> payload; + + payload.reserve(sizeof(std::uint16_t) + sizeof(std::uint32_t) + + sizeof(std::uint32_t)); + + auto data = reinterpret_cast<const std::uint8_t*>(&session); + std::copy(data, data + sizeof(std::uint16_t), std::back_inserter(payload)); + + data = reinterpret_cast<const std::uint8_t*>(&offset); + std::copy(data, data + sizeof(std::uint32_t), std::back_inserter(payload)); + + data = reinterpret_cast<const std::uint8_t*>(&length); + std::copy(data, data + sizeof(std::uint32_t), std::back_inserter(payload)); + + return sendIpmiPayload(BlobOEMCommands::bmcBlobRead, payload); +} + +} // namespace host_tool diff --git a/src/ipmiblob/blob_handler.hpp b/src/ipmiblob/blob_handler.hpp new file mode 100644 index 0000000..cbac9d4 --- /dev/null +++ b/src/ipmiblob/blob_handler.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include "blob_interface.hpp" +#include "ipmi_interface.hpp" + +namespace host_tool +{ + +class BlobHandler : public BlobInterface +{ + public: + enum BlobOEMCommands + { + bmcBlobGetCount = 0, + bmcBlobEnumerate = 1, + bmcBlobOpen = 2, + bmcBlobRead = 3, + bmcBlobWrite = 4, + bmcBlobCommit = 5, + bmcBlobClose = 6, + bmcBlobDelete = 7, + bmcBlobStat = 8, + bmcBlobSessionStat = 9, + bmcBlobWriteMeta = 10, + }; + + explicit BlobHandler(IpmiInterface* ipmi) : ipmi(ipmi){}; + + /** + * Retrieve the blob count. + * + * @return the number of blob_ids found (0 on failure). + */ + int getBlobCount(); + + /** + * Given an index into the list of blobs, return the name. + * + * @param[in] index - the index into the list of blob ids. + * @return the name as a string or empty on failure. + */ + std::string enumerateBlob(std::uint32_t index); + + /** + * @throws BlobException. + */ + void writeMeta(std::uint16_t session, std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) override; + + /** + * @throw BlobException. + */ + void writeBytes(std::uint16_t session, std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) override; + + std::vector<std::string> getBlobList() override; + + /** + * @throws BlobException. + */ + StatResponse getStat(const std::string& id) override; + + /** + * @throws BlobException. + */ + std::uint16_t openBlob(const std::string& id, + std::uint16_t handlerFlags) override; + + void closeBlob(std::uint16_t session) override; + + /** + * @throws BlobException. + */ + std::vector<std::uint8_t> readBytes(std::uint16_t session, + std::uint32_t offset, + std::uint32_t length) override; + + private: + /** + * Send the contents of the payload to IPMI, this method handles wrapping + * with the OEN, subcommand and CRC. + * + * @param[in] command - the blob command. + * @param[in] payload - the payload bytes. + * @return the bytes returned from the ipmi interface. + * @throws BlobException. + */ + std::vector<std::uint8_t> + sendIpmiPayload(BlobOEMCommands command, + const std::vector<std::uint8_t>& payload); + + /** + * Generic blob byte writer. + * + * @param[in] command - the command associated with this write. + * @param[in] session - the session id. + * @param[in] offset - the offset for the metadata to write. + * @param[in] bytes - the bytes to send. + * @throws BlobException on failure. + */ + void writeGeneric(BlobOEMCommands command, std::uint16_t session, + std::uint32_t offset, + const std::vector<std::uint8_t>& bytes); + + IpmiInterface* ipmi; +}; + +} // namespace host_tool diff --git a/src/ipmiblob/blob_interface.hpp b/src/ipmiblob/blob_interface.hpp new file mode 100644 index 0000000..f85be59 --- /dev/null +++ b/src/ipmiblob/blob_interface.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include <cstdint> +#include <string> +#include <vector> + +namespace host_tool +{ + +struct StatResponse +{ + std::uint16_t blob_state; + std::uint32_t size; + std::vector<std::uint8_t> metadata; +}; + +class BlobInterface +{ + public: + virtual ~BlobInterface() = default; + + /** + * Write metadata to a blob. + * + * @param[in] session - the session id. + * @param[in] offset - the offset for the metadata to write. + * @param[in] bytes - the bytes to send. + * @throws BlobException on failure. + */ + virtual void writeMeta(std::uint16_t session, std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) = 0; + + /** + * Write bytes to a blob. + * + * @param[in] session - the session id. + * @param[in] offset - the offset to which to write the bytes. + * @param[in] bytes - the bytes to send. + * @throws BlobException on failure. + */ + virtual void writeBytes(std::uint16_t session, std::uint32_t offset, + const std::vector<std::uint8_t>& bytes) = 0; + + /** + * Get a list of the blob_ids provided by the BMC. + * + * @return list of strings, each representing a blob_id returned. + */ + virtual std::vector<std::string> getBlobList() = 0; + + /** + * Get the stat() on the blob_id. + * + * @param[in] id - the blob_id. + * @return metadata structure. + */ + virtual StatResponse getStat(const std::string& id) = 0; + + /** + * Attempt to open the file using the specific data interface flag. + * + * @param[in] blob - the blob_id to open. + * @param[in] handlerFlags - the data interface flag, if relevant. + * @return the session id on success. + * @throws BlobException on failure. + */ + virtual std::uint16_t openBlob(const std::string& id, + std::uint16_t handlerFlags) = 0; + + /** + * Attempt to close the open session. + * + * @param[in] session - the session to close. + */ + virtual void closeBlob(std::uint16_t session) = 0; + + /** + * Read bytes from a blob. + * + * @param[in] session - the session id. + * @param[in] offset - the offset to which to write the bytes. + * @param[in] length - the number of bytes to read. + * @return the bytes read + * @throws BlobException on failure. + */ + virtual std::vector<std::uint8_t> readBytes(std::uint16_t session, + std::uint32_t offset, + std::uint32_t length) = 0; +}; + +} // namespace host_tool diff --git a/src/ipmiblob/crc.cpp b/src/ipmiblob/crc.cpp new file mode 100644 index 0000000..d6f59ef --- /dev/null +++ b/src/ipmiblob/crc.cpp @@ -0,0 +1,44 @@ +#include "crc.hpp" + +namespace host_tool +{ + +/* + * This implementation tracks the specification given at + * http://srecord.sourceforge.net/crc16-ccitt.html + * Code copied from internal portable sources. + */ +std::uint16_t generateCrc(const std::vector<std::uint8_t>& data) +{ + const std::uint16_t kPoly = 0x1021; + const std::uint16_t kLeftBit = 0x8000; + const int kExtraRounds = 2; + const std::uint8_t* bytes = data.data(); + std::uint16_t crc = 0xFFFF; + std::size_t i; + std::size_t j; + std::size_t size = data.size(); + + for (i = 0; i < size + kExtraRounds; ++i) + { + for (j = 0; j < 8; ++j) + { + bool xor_flag = (crc & kLeftBit) ? 1 : 0; + crc <<= 1; + // If this isn't an extra round and the current byte's j'th bit from + // the left is set, increment the CRC. + if (i < size && (bytes[i] & (1 << (7 - j)))) + { + crc++; + } + if (xor_flag) + { + crc ^= kPoly; + } + } + } + + return crc; +} + +} // namespace host_tool diff --git a/src/ipmiblob/crc.hpp b/src/ipmiblob/crc.hpp new file mode 100644 index 0000000..c335ed2 --- /dev/null +++ b/src/ipmiblob/crc.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include <cstdint> +#include <vector> + +namespace host_tool +{ + +/** + * Generate the CRC for a payload (really any bytes). + * + * This is meant to only be called on the payload and not the CRC or the OEM + * header, etc. + * + * @param[in] data - the bytes against to run the CRC + * @return the CRC value + */ +std::uint16_t generateCrc(const std::vector<std::uint8_t>& data); + +} // namespace host_tool diff --git a/src/ipmiblob/internal/sys.cpp b/src/ipmiblob/internal/sys.cpp new file mode 100644 index 0000000..46c6642 --- /dev/null +++ b/src/ipmiblob/internal/sys.cpp @@ -0,0 +1,70 @@ +/* + * 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 "sys.hpp" + +#include <fcntl.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <unistd.h> + +namespace internal +{ + +int SysImpl::open(const char* pathname, int flags) const +{ + return ::open(pathname, flags); +} + +int SysImpl::read(int fd, void* buf, std::size_t count) const +{ + return static_cast<int>(::read(fd, buf, count)); +} + +int SysImpl::close(int fd) const +{ + return ::close(fd); +} + +void* SysImpl::mmap(void* addr, std::size_t length, int prot, int flags, int fd, + off_t offset) const +{ + return ::mmap(addr, length, prot, flags, fd, offset); +} + +int SysImpl::munmap(void* addr, std::size_t length) const +{ + return ::munmap(addr, length); +} + +int SysImpl::getpagesize() const +{ + return ::getpagesize(); +} + +int SysImpl::ioctl(int fd, unsigned long request, void* param) const +{ + return ::ioctl(fd, request, param); +} + +int SysImpl::poll(struct pollfd* fds, nfds_t nfds, int timeout) const +{ + return ::poll(fds, nfds, timeout); +} + +SysImpl sys_impl; + +} // namespace internal diff --git a/src/ipmiblob/internal/sys.hpp b/src/ipmiblob/internal/sys.hpp new file mode 100644 index 0000000..2975b8c --- /dev/null +++ b/src/ipmiblob/internal/sys.hpp @@ -0,0 +1,61 @@ +#pragma once + +/* NOTE: IIRC, wak@ is working on exposing some of this in stdplus, so we can + * transition when that's ready. + * + * Copied some from gpioplus to enable unit-testing of lpc nuvoton and later + * other pieces. + */ + +#include <poll.h> +#include <sys/mman.h> + +#include <cinttypes> +#include <cstddef> + +namespace internal +{ + +/** + * @class Sys + * @brief Overridable direct syscall interface + */ +class Sys +{ + public: + virtual ~Sys() = default; + + virtual int open(const char* pathname, int flags) const = 0; + virtual int read(int fd, void* buf, std::size_t count) const = 0; + virtual int close(int fd) const = 0; + virtual void* mmap(void* addr, std::size_t length, int prot, int flags, + int fd, off_t offset) const = 0; + virtual int munmap(void* addr, std::size_t length) const = 0; + virtual int getpagesize() const = 0; + virtual int ioctl(int fd, unsigned long request, void* param) const = 0; + virtual int poll(struct pollfd* fds, nfds_t nfds, int timeout) const = 0; +}; + +/** + * @class SysImpl + * @brief syscall concrete implementation + * @details Passes through all calls to the normal linux syscalls + */ +class SysImpl : public Sys +{ + public: + int open(const char* pathname, int flags) const override; + int read(int fd, void* buf, std::size_t count) const override; + int close(int fd) const override; + void* mmap(void* addr, std::size_t length, int prot, int flags, int fd, + off_t offset) const override; + int munmap(void* addr, std::size_t length) const override; + int getpagesize() const override; + int ioctl(int fd, unsigned long request, void* param) const override; + int poll(struct pollfd* fds, nfds_t nfds, int timeout) const override; +}; + +/** @brief Default instantiation of sys */ +extern SysImpl sys_impl; + +} // namespace internal diff --git a/src/ipmiblob/ipmi_errors.hpp b/src/ipmiblob/ipmi_errors.hpp new file mode 100644 index 0000000..9f1a9f9 --- /dev/null +++ b/src/ipmiblob/ipmi_errors.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include <exception> +#include <map> +#include <sstream> +#include <string> + +namespace host_tool +{ + +class IpmiException : public std::exception +{ + public: + const std::map<int, std::string> commonFailures = { + {0xc0, "busy"}, + {0xc1, "invalid"}, + {0xc3, "timeout"}, + }; + + explicit IpmiException(int cc) + { + std::ostringstream smessage; + + auto search = commonFailures.find(cc); + if (search != commonFailures.end()) + { + smessage << "Received IPMI_CC: " << search->second; + } + else + { + smessage << "Received IPMI_CC: " << cc; + } + + message = smessage.str(); + } + explicit IpmiException(const std::string& message) : message(message){}; + + virtual const char* what() const noexcept override + { + return message.c_str(); + } + + private: + std::string message; +}; + +} // namespace host_tool diff --git a/src/ipmiblob/ipmi_handler.cpp b/src/ipmiblob/ipmi_handler.cpp new file mode 100644 index 0000000..9278338 --- /dev/null +++ b/src/ipmiblob/ipmi_handler.cpp @@ -0,0 +1,165 @@ +/* + * 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 "ipmi_handler.hpp" + +#include "ipmi_errors.hpp" + +#include <fcntl.h> +#include <linux/ipmi.h> +#include <linux/ipmi_msgdefs.h> +#include <sys/ioctl.h> + +#include <array> +#include <cstdint> +#include <cstring> +#include <sstream> +#include <string> +#include <vector> + +namespace host_tool +{ + +void IpmiHandler::open() +{ + const int device = 0; + const std::vector<std::string> formats = {"/dev/ipmi", "/dev/ipmi/", + "/dev/ipmidev/"}; + + for (const auto& format : formats) + { + std::ostringstream path; + path << format << device; + + fd = sys->open(path.str().c_str(), O_RDWR); + if (fd < 0) + { + continue; + } + break; + } + + if (fd < 0) + { + throw IpmiException("Unable to open any ipmi devices"); + } +} + +std::vector<std::uint8_t> + IpmiHandler::sendPacket(std::vector<std::uint8_t>& data) +{ + if (fd < 0) + { + open(); + } + + constexpr int ipmiOEMNetFn = 46; + constexpr int ipmiOEMLun = 0; + /* /openbmc/phosphor-host-ipmid/blob/master/host-ipmid/oemopenbmc.hpp */ + constexpr int ipmiOEMBlobCmd = 128; + constexpr int fifteenMs = 15 * 1000; + constexpr int ipmiReadTimeout = fifteenMs; + constexpr int ipmiResponseBufferLen = IPMI_MAX_MSG_LENGTH; + constexpr int ipmiOk = 0; + + /* We have a handle to the IPMI device. */ + std::array<std::uint8_t, ipmiResponseBufferLen> responseBuffer; + + /* Build address. */ + struct ipmi_system_interface_addr systemAddress; + systemAddress.addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE; + systemAddress.channel = IPMI_BMC_CHANNEL; + systemAddress.lun = ipmiOEMLun; + + /* Build request. */ + struct ipmi_req request; + std::memset(&request, 0, sizeof(request)); + request.addr = reinterpret_cast<unsigned char*>(&systemAddress); + request.addr_len = sizeof(systemAddress); + request.msgid = sequence++; + request.msg.data = reinterpret_cast<unsigned char*>(data.data()); + request.msg.data_len = data.size(); + request.msg.netfn = ipmiOEMNetFn; + request.msg.cmd = ipmiOEMBlobCmd; + + struct ipmi_recv reply; + reply.addr = reinterpret_cast<unsigned char*>(&systemAddress); + reply.addr_len = sizeof(systemAddress); + reply.msg.data = reinterpret_cast<unsigned char*>(responseBuffer.data()); + reply.msg.data_len = responseBuffer.size(); + + /* Try to send request. */ + int rc = sys->ioctl(fd, IPMICTL_SEND_COMMAND, &request); + if (rc < 0) + { + throw IpmiException("Unable to send IPMI request."); + } + + /* Could use sdeventplus, but for only one type of event is it worth it? */ + struct pollfd pfd; + pfd.fd = fd; + pfd.events = POLLIN; + + do + { + rc = sys->poll(&pfd, 1, ipmiReadTimeout); + if (rc < 0) + { + if (errno == EINTR) + { + continue; + } + throw IpmiException("Error occurred."); + } + else if (rc == 0) + { + throw IpmiException("Timeout waiting for reply."); + } + + /* Yay, happy case! */ + rc = sys->ioctl(fd, IPMICTL_RECEIVE_MSG_TRUNC, &reply); + if (rc < 0) + { + throw IpmiException("Unable to read reply."); + } + + if (request.msgid != reply.msgid) + { + std::fprintf(stderr, "Received wrong message, trying again.\n"); + } + } while (request.msgid != reply.msgid); + + if (responseBuffer[0] != ipmiOk) + { + throw IpmiException(static_cast<int>(responseBuffer[0])); + } + + std::vector<std::uint8_t> returning; + auto dataLen = reply.msg.data_len - 1; + + returning.insert(returning.begin(), responseBuffer.begin() + 1, + responseBuffer.begin() + dataLen + 1); + + for (const auto& byte : returning) + { + std::fprintf(stderr, "0x%02x ", byte); + } + std::fprintf(stderr, "\n"); + + return returning; +} + +} // namespace host_tool diff --git a/src/ipmiblob/ipmi_handler.hpp b/src/ipmiblob/ipmi_handler.hpp new file mode 100644 index 0000000..1c91bff --- /dev/null +++ b/src/ipmiblob/ipmi_handler.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "internal/sys.hpp" +#include "ipmi_interface.hpp" + +#include <vector> + +namespace host_tool +{ + +class IpmiHandler : public IpmiInterface +{ + public: + explicit IpmiHandler(const internal::Sys* sys = &internal::sys_impl) : + sys(sys){}; + + ~IpmiHandler() = default; + IpmiHandler(const IpmiHandler&) = delete; + IpmiHandler& operator=(const IpmiHandler&) = delete; + IpmiHandler(IpmiHandler&&) = default; + IpmiHandler& operator=(IpmiHandler&&) = default; + + /** + * Attempt to open the device node. + * + * @throws IpmiException on failure. + */ + void open(); + + /** + * @throws IpmiException on failure. + */ + std::vector<std::uint8_t> + sendPacket(std::vector<std::uint8_t>& data) override; + + private: + const internal::Sys* sys; + /** TODO: Use a smart file descriptor when it's ready. Until then only + * allow moving this object. + */ + int fd = -1; + /* The last IPMI sequence number we used. */ + int sequence = 0; +}; + +} // namespace host_tool diff --git a/src/ipmiblob/ipmi_interface.hpp b/src/ipmiblob/ipmi_interface.hpp new file mode 100644 index 0000000..6bad7db --- /dev/null +++ b/src/ipmiblob/ipmi_interface.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include <cstdint> +#include <vector> + +namespace host_tool +{ + +class IpmiInterface +{ + public: + virtual ~IpmiInterface() = default; + + /** + * Send an IPMI packet to the BMC. + * + * @param[in] data - a vector of the IPMI packet contents. + * @return the bytes returned. + * @throws IpmiException on failure. + */ + virtual std::vector<std::uint8_t> + sendPacket(std::vector<std::uint8_t>& data) = 0; +}; + +} // namespace host_tool diff --git a/src/ipmiblob/test/ipmi_interface_mock.hpp b/src/ipmiblob/test/ipmi_interface_mock.hpp new file mode 100644 index 0000000..c3e187e --- /dev/null +++ b/src/ipmiblob/test/ipmi_interface_mock.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include <ipmiblob/ipmi_interface.hpp> + +#include <gmock/gmock.h> + +namespace host_tool +{ + +class IpmiInterfaceMock : public IpmiInterface +{ + public: + virtual ~IpmiInterfaceMock() = default; + MOCK_METHOD1(sendPacket, + std::vector<std::uint8_t>(std::vector<std::uint8_t>&)); +}; + +} // namespace host_tool diff --git a/test/Makefile.am b/test/Makefile.am new file mode 100644 index 0000000..05d9b1d --- /dev/null +++ b/test/Makefile.am @@ -0,0 +1,25 @@ +@VALGRIND_CHECK_RULES@ +@CODE_COVERAGE_RULES@ + +gtest_cppflags = $(AM_CPPFLAGS) $(GTEST_CFLAGS) $(GMOCK_CFLAGS) +gtest_ldadd = $(GTEST_LIBS) $(GMOCK_LIBS) -lgmock_main + +check_PROGRAMS = +TESTS = $(check_PROGRAMS) + +check_PROGRAMS += tools_blob_unittest +tools_blob_unittest_SOURCES = tools_blob_unittest.cpp +tools_blob_unittest_CPPFLAGS = $(gtest_cppflags) +tools_blob_unittest_LDADD = $(gtest_ldadd) +tools_blob_unittest_LDADD += $(top_builddir)/src/ipmiblob/blob_handler.o + +check_PROGRAMS += tools_ipmi_unittest +tools_ipmi_unittest_SOURCES = tools_ipmi_unittest.cpp +tools_ipmi_unittest_CPPFLAGS = $(gtest_cppflags) +tools_ipmi_unittest_LDADD = $(gtest_ldadd) +tools_ipmi_unittest_LDADD += $(top_builddir)/src/ipmiblob/ipmi_handler.o + +check_PROGRAMS += tools_ipmi_error_unittest +tools_ipmi_error_unittest_SOURCES = tools_ipmi_error_unittest.cpp +tools_ipmi_error_unittest_CPPFLAGS = $(gtest_cppflags) +tools_ipmi_error_unittest_LDADD = $(gtest_ldadd) diff --git a/test/crc_mock.hpp b/test/crc_mock.hpp new file mode 100644 index 0000000..293ec24 --- /dev/null +++ b/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/test/internal_sys_mock.hpp b/test/internal_sys_mock.hpp new file mode 100644 index 0000000..b4ba4b1 --- /dev/null +++ b/test/internal_sys_mock.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <unistd.h> + +#include <ipmiblob/internal/sys.hpp> + +#include <gmock/gmock.h> + +namespace internal +{ + +class InternalSysMock : public Sys +{ + public: + virtual ~InternalSysMock() = default; + + MOCK_CONST_METHOD2(open, int(const char*, int)); + MOCK_CONST_METHOD3(read, int(int, void*, std::size_t)); + MOCK_CONST_METHOD1(close, int(int)); + MOCK_CONST_METHOD6(mmap, void*(void*, std::size_t, int, int, int, off_t)); + MOCK_CONST_METHOD2(munmap, int(void*, std::size_t)); + MOCK_CONST_METHOD0(getpagesize, int()); + MOCK_CONST_METHOD3(ioctl, int(int, unsigned long, void*)); + MOCK_CONST_METHOD3(poll, int(struct pollfd*, nfds_t, int)); +}; + +} // namespace internal diff --git a/test/tools_blob_unittest.cpp b/test/tools_blob_unittest.cpp new file mode 100644 index 0000000..f7f58c4 --- /dev/null +++ b/test/tools_blob_unittest.cpp @@ -0,0 +1,289 @@ +#include "crc_mock.hpp" + +#include <ipmiblob/blob_handler.hpp> +#include <ipmiblob/test/ipmi_interface_mock.hpp> + +#include <gtest/gtest.h> + +namespace host_tool +{ +CrcInterface* crcIntf = nullptr; + +std::uint16_t generateCrc(const std::vector<std::uint8_t>& data) +{ + return (crcIntf) ? crcIntf->generateCrc(data) : 0x00; +} + +using ::testing::Eq; +using ::testing::Return; + +class BlobHandlerTest : public ::testing::Test +{ + protected: + void SetUp() override + { + crcIntf = &crcMock; + } + + CrcMock crcMock; +}; + +TEST_F(BlobHandlerTest, getCountIpmiHappy) +{ + /* Verify returns the value specified by the IPMI response. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobGetCount}; + + /* return 1 blob count. */ + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00}; + + std::vector<std::uint8_t> bytes = {0x01, 0x00, 0x00, 0x00}; + EXPECT_CALL(crcMock, generateCrc(Eq(bytes))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + EXPECT_EQ(1, blob.getBlobCount()); +} + +TEST_F(BlobHandlerTest, enumerateBlobIpmiHappy) +{ + /* Verify returns the name specified by the IPMI response. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobEnumerate, + 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00}; + + /* return value. */ + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00, 0x00, 0x00, + 'a', 'b', 'c', 'd', 0x00}; + + std::vector<std::uint8_t> bytes = {'a', 'b', 'c', 'd', 0x00}; + std::vector<std::uint8_t> reqCrc = {0x01, 0x00, 0x00, 0x00}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + EXPECT_CALL(crcMock, generateCrc(Eq(bytes))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + EXPECT_STREQ("abcd", blob.enumerateBlob(1).c_str()); +} + +TEST_F(BlobHandlerTest, enumerateBlobIpmiNoBytes) +{ + /* Simulate a case where the IPMI command returns no data. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobEnumerate, + 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00}; + + /* return value. */ + std::vector<std::uint8_t> resp = {}; + + std::vector<std::uint8_t> reqCrc = {0x01, 0x00, 0x00, 0x00}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + EXPECT_STREQ("", blob.enumerateBlob(1).c_str()); +} + +TEST_F(BlobHandlerTest, getBlobListIpmiHappy) +{ + /* Verify returns the list built via the above two commands. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + + std::vector<std::uint8_t> request1 = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobGetCount}; + + /* return 1 blob count. */ + std::vector<std::uint8_t> resp1 = {0xcf, 0xc2, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00}; + + std::vector<std::uint8_t> bytes1 = {0x01, 0x00, 0x00, 0x00}; + EXPECT_CALL(crcMock, generateCrc(Eq(bytes1))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request1))).WillOnce(Return(resp1)); + + std::vector<std::uint8_t> request2 = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobEnumerate, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; + + /* return value. */ + std::vector<std::uint8_t> resp2 = {0xcf, 0xc2, 0x00, 0x00, 0x00, + 'a', 'b', 'c', 'd', 0x00}; + + std::vector<std::uint8_t> reqCrc = {0x00, 0x00, 0x00, 0x00}; + std::vector<std::uint8_t> bytes2 = {'a', 'b', 'c', 'd', 0x00}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + EXPECT_CALL(crcMock, generateCrc(Eq(bytes2))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request2))).WillOnce(Return(resp2)); + + /* A std::string is not nul-terminated by default. */ + std::vector<std::string> expectedList = {std::string{"abcd"}}; + + EXPECT_EQ(expectedList, blob.getBlobList()); +} + +TEST_F(BlobHandlerTest, getStatWithMetadata) +{ + /* Stat received metadata. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobStat, + 0x00, 0x00, 'a', 'b', + 'c', 'd', 0x00}; + + /* return blob_state: 0xffff, size: 0x00, metadata 0x3445 */ + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x34, 0x45}; + + std::vector<std::uint8_t> reqCrc = {'a', 'b', 'c', 'd', 0x00}; + std::vector<std::uint8_t> respCrc = {0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x34, 0x45}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + EXPECT_CALL(crcMock, generateCrc(Eq(respCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + + auto meta = blob.getStat("abcd"); + EXPECT_EQ(meta.blob_state, 0xffff); + EXPECT_EQ(meta.size, 0x00); + std::vector<std::uint8_t> metadata = {0x34, 0x45}; + EXPECT_EQ(metadata, meta.metadata); +} + +TEST_F(BlobHandlerTest, getStatNoMetadata) +{ + /* Stat received no metadata. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobStat, + 0x00, 0x00, 'a', 'b', + 'c', 'd', 0x00}; + + /* return blob_state: 0xffff, size: 0x00, metadata 0x3445 */ + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00}; + + std::vector<std::uint8_t> reqCrc = {'a', 'b', 'c', 'd', 0x00}; + std::vector<std::uint8_t> respCrc = {0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00}; + + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + EXPECT_CALL(crcMock, generateCrc(Eq(respCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + + auto meta = blob.getStat("abcd"); + EXPECT_EQ(meta.blob_state, 0xffff); + EXPECT_EQ(meta.size, 0x00); + std::vector<std::uint8_t> metadata = {}; + EXPECT_EQ(metadata, meta.metadata); +} + +TEST_F(BlobHandlerTest, openBlobSucceeds) +{ + /* The open blob succeeds. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobOpen, + 0x00, 0x00, 0x02, 0x04, + 'a', 'b', 'c', 'd', + 0x00}; + + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00, 0x00, 0x00, 0xfe, 0xed}; + + std::vector<std::uint8_t> reqCrc = {0x02, 0x04, 'a', 'b', 'c', 'd', 0x00}; + std::vector<std::uint8_t> respCrc = {0xfe, 0xed}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + EXPECT_CALL(crcMock, generateCrc(Eq(respCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + + const int writeBit = (1 << 1); + const int lpcBit = (1 << 10); + + auto session = blob.openBlob("abcd", writeBit | lpcBit); + EXPECT_EQ(0xedfe, session); +} + +TEST_F(BlobHandlerTest, closeBlobSucceeds) +{ + /* The close succeeds. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobClose, + 0x00, 0x00, 0x01, 0x00}; + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00}; + std::vector<std::uint8_t> reqCrc = {0x01, 0x00}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + + blob.closeBlob(0x0001); +} + +TEST_F(BlobHandlerTest, writeBytesSucceeds) +{ + /* The write bytes succeeds. */ + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobWrite, + 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, + 'a', 'b', 'c', 'd'}; + + std::vector<std::uint8_t> bytes = {'a', 'b', 'c', 'd'}; + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00}; + std::vector<std::uint8_t> reqCrc = {0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 'a', 'b', 'c', 'd'}; + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + + blob.writeBytes(0x0001, 0, bytes); +} + +TEST_F(BlobHandlerTest, readBytesSucceeds) +{ + /* The reading of bytes succeeds. */ + + IpmiInterfaceMock ipmiMock; + BlobHandler blob(&ipmiMock); + + std::vector<std::uint8_t> request = { + 0xcf, 0xc2, 0x00, BlobHandler::BlobOEMCommands::bmcBlobRead, + 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00}; + + std::vector<std::uint8_t> expectedBytes = {'a', 'b', 'c', 'd'}; + std::vector<std::uint8_t> resp = {0xcf, 0xc2, 0x00, 0x00, 0x00, + 'a', 'b', 'c', 'd'}; + std::vector<std::uint8_t> reqCrc = {0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00}; + std::vector<std::uint8_t> respCrc = {'a', 'b', 'c', 'd'}; + + EXPECT_CALL(crcMock, generateCrc(Eq(reqCrc))).WillOnce(Return(0x00)); + EXPECT_CALL(crcMock, generateCrc(Eq(respCrc))).WillOnce(Return(0x00)); + + EXPECT_CALL(ipmiMock, sendPacket(Eq(request))).WillOnce(Return(resp)); + + EXPECT_EQ(blob.readBytes(0x0001, 0, 4), expectedBytes); +} + +} // namespace host_tool diff --git a/test/tools_ipmi_error_unittest.cpp b/test/tools_ipmi_error_unittest.cpp new file mode 100644 index 0000000..05048e2 --- /dev/null +++ b/test/tools_ipmi_error_unittest.cpp @@ -0,0 +1,28 @@ +#include <ipmiblob/ipmi_errors.hpp> + +#include <gtest/gtest.h> + +namespace host_tool +{ + +TEST(IpmiExceptionTest, VerifyTimedOutIsString) +{ + /* Verify that throwing the exception with the cc code for timed out gets + * converted to the human readable string. + */ + bool verified = false; + + try + { + throw IpmiException(0xc3); + } + catch (const IpmiException& i) + { + EXPECT_STREQ("Received IPMI_CC: timeout", i.what()); + verified = true; + } + + EXPECT_TRUE(verified); +} + +} // namespace host_tool diff --git a/test/tools_ipmi_unittest.cpp b/test/tools_ipmi_unittest.cpp new file mode 100644 index 0000000..9516d46 --- /dev/null +++ b/test/tools_ipmi_unittest.cpp @@ -0,0 +1,22 @@ +#include "internal_sys_mock.hpp" + +#include <ipmiblob/ipmi_errors.hpp> +#include <ipmiblob/ipmi_handler.hpp> + +namespace host_tool +{ + +using ::testing::_; +using ::testing::Return; + +TEST(IpmiHandlerTest, OpenAllFails) +{ + /* Open against all device files fail. */ + internal::InternalSysMock sysMock; + IpmiHandler ipmi(&sysMock); + + EXPECT_CALL(sysMock, open(_, _)).WillRepeatedly(Return(-1)); + EXPECT_THROW(ipmi.open(), IpmiException); +} + +} // namespace host_tool |