fwupd/plugins/redfish/fu-ipmi-device.c

594 lines
14 KiB
C

/*
* Copyright (C) 2021 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <fcntl.h>
#include <glib/gstdio.h>
#include <linux/ipmi.h>
#include <linux/ipmi_msgdefs.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "fu-ipmi-device.h"
#define FU_IPMI_DEVICE_TIMEOUT 1500 /* ms */
/* not defined in linux/ipmi_msgdefs.h */
#define IPMI_SET_USER_ACCESS 0x43
#define IPMI_SET_USER_NAME 0x45
#define IPMI_GET_USER_NAME 0x46
#define IPMI_SET_USER_PASSWORD 0x47
#define IPMI_PASSWORD_DISABLE_USER 0x00
#define IPMI_PASSWORD_ENABLE_USER 0x01
#define IPMI_PASSWORD_SET_PASSWORD 0x02
#define IPMI_PASSWORD_TEST_PASSWORD 0x03
struct _FuIpmiDevice {
FuUdevDevice parent_instance;
glong seq;
guint8 device_id;
guint8 device_rev;
guint8 version_ipmi;
};
G_DEFINE_TYPE(FuIpmiDevice, fu_ipmi_device, FU_TYPE_UDEV_DEVICE)
static void
fu_ipmi_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuIpmiDevice *self = FU_IPMI_DEVICE(device);
fu_common_string_append_kx(str, idt, "DeviceId", self->device_id);
fu_common_string_append_kx(str, idt, "DeviceRev", self->device_rev);
fu_common_string_append_kx(str, idt, "VersionIpmi", self->version_ipmi);
}
static gboolean
fu_ipmi_device_send(FuIpmiDevice *self,
guint8 netfn,
guint8 cmd,
const guint8 *buf,
gsize bufsz,
GError **error)
{
g_autofree guint8 *buf2 = fu_memdup_safe(buf, bufsz, NULL);
struct ipmi_system_interface_addr addr = {.addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE,
.channel = IPMI_BMC_CHANNEL};
struct ipmi_req req = {
.addr = (guint8 *)&addr,
.addr_len = sizeof(addr),
.msgid = self->seq++,
.msg.data = buf2,
.msg.data_len = (guint16)bufsz,
.msg.netfn = netfn,
.msg.cmd = cmd,
};
if (g_getenv("FWUPD_REDFISH_VERBOSE") != NULL && buf2 != NULL)
fu_common_dump_raw(G_LOG_DOMAIN, "ipmi-send", buf2, bufsz);
return fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
IPMICTL_SEND_COMMAND,
(guint8 *)&req,
NULL,
error);
}
static gboolean
fu_ipmi_device_recv(FuIpmiDevice *self,
guint8 *netfn,
guint8 *cmd,
glong *seq,
guint8 *buf,
gsize bufsz,
gsize *len, /* optional, out */
GError **error)
{
struct ipmi_addr addr = {0};
struct ipmi_recv recv = {
.addr = (guint8 *)&addr,
.addr_len = sizeof(addr),
.msg.data = buf,
.msg.data_len = bufsz,
};
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
IPMICTL_RECEIVE_MSG_TRUNC,
(guint8 *)&recv,
NULL,
error))
return FALSE;
if (g_getenv("FWUPD_REDFISH_VERBOSE") != NULL && buf != NULL)
fu_common_dump_raw(G_LOG_DOMAIN, "ipmi-recv", buf, bufsz);
if (netfn != NULL)
*netfn = recv.msg.netfn;
if (cmd != NULL)
*cmd = recv.msg.cmd;
if (seq != NULL)
*seq = recv.msgid;
if (len != NULL)
*len = (gsize)recv.msg.data_len;
return TRUE;
}
static gboolean
fu_ipmi_device_lock(GObject *device, GError **error)
{
FuIpmiDevice *self = FU_IPMI_DEVICE(device);
struct flock lock = {.l_type = F_WRLCK, .l_whence = SEEK_SET};
if (fcntl(fu_udev_device_get_fd(FU_UDEV_DEVICE(self)), F_SETLKW, &lock) == -1) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "error locking IPMI device: %m");
return FALSE;
}
return TRUE;
}
static gboolean
fu_ipmi_device_unlock(GObject *device, GError **error)
{
FuIpmiDevice *self = FU_IPMI_DEVICE(device);
struct flock lock = {.l_type = F_UNLCK};
if (fcntl(fu_udev_device_get_fd(FU_UDEV_DEVICE(self)), F_SETLKW, &lock) == -1) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"error unlocking IPMI device: %m");
return FALSE;
}
return TRUE;
}
static const gchar *
fu_ipmi_device_errcode_to_string(guint8 errcode)
{
if (errcode == IPMI_CC_NO_ERROR)
return "no-error";
if (errcode == IPMI_NODE_BUSY_ERR)
return "node-busy";
if (errcode == IPMI_INVALID_COMMAND_ERR)
return "invalid-command";
if (errcode == IPMI_TIMEOUT_ERR)
return "timeout";
if (errcode == IPMI_ERR_MSG_TRUNCATED)
return "msg-truncated";
if (errcode == IPMI_REQ_LEN_INVALID_ERR)
return "req-len-invalid";
if (errcode == IPMI_REQ_LEN_EXCEEDED_ERR)
return "req-len-exceeded";
#ifdef HAVE_IPMI_DEVICE_IN_FW_UPDATE_ERR
if (errcode == IPMI_DEVICE_IN_FW_UPDATE_ERR)
return "device-in-fw-update";
#endif
#ifdef HAVE_IPMI_DEVICE_IN_INIT_ERR
if (errcode == IPMI_DEVICE_IN_INIT_ERR)
return "device-in-init";
#endif
if (errcode == IPMI_NOT_IN_MY_STATE_ERR)
return "not-in-my-state";
if (errcode == IPMI_LOST_ARBITRATION_ERR)
return "lost-arbitration";
if (errcode == IPMI_BUS_ERR)
return "bus-error";
if (errcode == IPMI_NAK_ON_WRITE_ERR)
return "nak-on-write";
if (errcode == IPMI_ERR_UNSPECIFIED)
return "unspecified";
return "unspecified";
}
static gboolean
fu_ipmi_device_transaction(FuIpmiDevice *self,
guint8 netfn,
guint8 cmd,
const guint8 *req_buf,
gsize req_bufsz,
guint8 *resp_buf, /* optional */
gsize resp_bufsz,
gsize *resp_len, /* optional, out */
gint timeout_ms,
GError **error)
{
GPollFD pollfds[1];
gsize resp_buf2sz = resp_bufsz + 1;
gsize resp_len2 = 0;
g_autoptr(GTimer) timer = g_timer_new();
g_autoptr(FuDeviceLocker) lock = NULL;
g_autofree guint8 *resp_buf2 = g_malloc0(resp_buf2sz);
lock = fu_device_locker_new_full(self, fu_ipmi_device_lock, fu_ipmi_device_unlock, error);
if (lock == NULL)
return FALSE;
if (!fu_ipmi_device_send(self, netfn, cmd, req_buf, req_bufsz, error))
return FALSE;
pollfds[0].fd = fu_udev_device_get_fd(FU_UDEV_DEVICE(self));
pollfds[0].events = POLLIN;
for (;;) {
guint8 resp_netfn = 0;
guint8 resp_cmd = 0;
glong seq = 0;
gint rc;
rc = g_poll(pollfds, 1, timeout_ms - (g_timer_elapsed(timer, NULL) * 1000.f));
if (rc < 0) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "poll() error %m");
return FALSE;
}
if (rc == 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"timeout waiting for response "
"(netfn %d, cmd %d)",
netfn,
cmd);
return FALSE;
}
if (!(pollfds[0].revents & POLLIN)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"unexpected status");
return FALSE;
}
if (!fu_ipmi_device_recv(self,
&resp_netfn,
&resp_cmd,
&seq,
resp_buf2,
resp_buf2sz,
&resp_len2,
error))
return FALSE;
if (seq != self->seq - 1) {
g_debug("out-of-sequence reply: "
"expected %ld, got %ld",
self->seq,
seq);
if (g_timer_elapsed(timer, NULL) * 1000.f >= timeout_ms) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"timed out");
return FALSE;
}
} else {
if (resp_buf2[0] != IPMI_CC_NO_ERROR) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"CC error: %s [%02x]",
fu_ipmi_device_errcode_to_string(resp_buf2[0]),
resp_buf2[0]);
return FALSE;
}
if (resp_buf != NULL) {
if (!fu_memcpy_safe(resp_buf,
resp_bufsz,
0x0, /* dst */
resp_buf2,
resp_buf2sz,
0x01, /* src */
resp_bufsz,
error))
return FALSE;
}
if (resp_len != NULL)
*resp_len = resp_len2 - 1;
if (g_getenv("FWUPD_REDFISH_VERBOSE") != NULL) {
g_debug("IPMI netfn: %02x->%02x, cmd: %02x->%02x",
netfn,
resp_netfn,
cmd,
resp_cmd);
}
break;
}
}
return TRUE;
}
static gboolean
fu_ipmi_device_probe(FuDevice *device, GError **error)
{
FuIpmiDevice *self = FU_IPMI_DEVICE(device);
const gchar *physical_ids[] = {"/dev/ipmi0", "/dev/ipmi/0", "/dev/ipmidev/0", NULL};
/* look for the IPMI device */
for (guint i = 0; physical_ids[i] != NULL; i++) {
if (g_file_test(physical_ids[i], G_FILE_TEST_EXISTS)) {
fu_device_set_physical_id(FU_DEVICE(self), physical_ids[i]);
return TRUE;
}
}
/* cannot continue */
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "no BMC device found");
return FALSE;
}
static gboolean
fu_ipmi_device_setup(FuDevice *device, GError **error)
{
FuIpmiDevice *self = FU_IPMI_DEVICE(device);
gsize resp_len = 0;
guint8 resp[16] = {0};
/* get IPMI versions */
if (!fu_ipmi_device_transaction(self,
IPMI_NETFN_APP_REQUEST,
IPMI_GET_DEVICE_ID_CMD,
NULL,
0,
resp,
sizeof(resp),
&resp_len,
FU_IPMI_DEVICE_TIMEOUT,
error))
return FALSE;
if (resp_len == 11 || resp_len == 15) {
guint8 bcd;
g_autoptr(GString) str = g_string_new(NULL);
self->device_id = resp[0];
self->device_rev = resp[1];
bcd = resp[3] & 0x0f;
bcd += 10 * (resp[4] >> 3);
/* rev1.rev2.aux_revision */
g_string_append_printf(str, "%u.%02u", resp[2], bcd);
if (resp_len == 15) {
g_string_append_printf(str,
".%02x%02x%02x%02x",
resp[11],
resp[12],
resp[13],
resp[14]);
}
fu_device_set_version(device, str->str);
bcd = resp[4] & 0x0f;
bcd += 10 * (resp[4] >> 4);
self->version_ipmi = bcd;
} else {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"failed to parse DEVICE_ID_CMD response (sz: %" G_GSIZE_FORMAT ")",
resp_len);
return FALSE;
}
/* success */
return TRUE;
}
gchar *
fu_ipmi_device_get_user_password(FuIpmiDevice *self, guint8 user_id, GError **error)
{
const guint8 req[1] = {user_id};
guint8 resp[0x10] = {0};
gsize resp_len = 0;
g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), NULL);
g_return_val_if_fail(user_id != 0x0, NULL);
/* run transaction */
if (!fu_ipmi_device_transaction(self,
IPMI_NETFN_APP_REQUEST,
IPMI_GET_USER_NAME,
req,
sizeof(req),
resp,
sizeof(resp),
&resp_len,
FU_IPMI_DEVICE_TIMEOUT,
error)) {
g_prefix_error(error, "failed to get username: ");
return NULL;
}
if (resp_len != sizeof(resp)) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"failed to retrieve username from IPMI, got 0x%x bytes",
(guint)resp_len);
return NULL;
}
/* success */
return fu_common_strsafe((const gchar *)resp, resp_len);
}
gboolean
fu_ipmi_device_set_user_name(FuIpmiDevice *self,
guint8 user_id,
const gchar *username,
GError **error)
{
guint8 req[0x11] = {user_id};
gsize username_sz;
g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
g_return_val_if_fail(user_id != 0x0, FALSE);
g_return_val_if_fail(username != NULL, FALSE);
/* copy into buffer */
username_sz = strlen(username);
if (!fu_memcpy_safe(req,
sizeof(req),
0x1, /* dst */
(guint8 *)username,
username_sz,
0x0, /* src */
username_sz,
error)) {
g_prefix_error(error, "username invalid: ");
return FALSE;
}
/* run transaction */
if (!fu_ipmi_device_transaction(self,
IPMI_NETFN_APP_REQUEST,
IPMI_SET_USER_NAME,
req,
sizeof(req),
NULL, /* resp */
0,
NULL,
FU_IPMI_DEVICE_TIMEOUT,
error)) {
g_prefix_error(error, "failed to set user %02x name: ", user_id);
return FALSE;
}
/* success */
return TRUE;
}
gboolean
fu_ipmi_device_set_user_enable(FuIpmiDevice *self, guint8 user_id, gboolean value, GError **error)
{
guint8 op = value ? IPMI_PASSWORD_ENABLE_USER : IPMI_PASSWORD_DISABLE_USER;
const guint8 req[] = {user_id, op};
g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
g_return_val_if_fail(user_id != 0x0, FALSE);
/* run transaction */
if (!fu_ipmi_device_transaction(self,
IPMI_NETFN_APP_REQUEST,
IPMI_SET_USER_PASSWORD,
req,
sizeof(req),
NULL, /* resp */
0,
NULL,
FU_IPMI_DEVICE_TIMEOUT,
error)) {
g_prefix_error(error, "failed to set user %02x enable: ", user_id);
return FALSE;
}
/* success */
return TRUE;
}
gboolean
fu_ipmi_device_set_user_password(FuIpmiDevice *self,
guint8 user_id,
const gchar *password,
GError **error)
{
guint8 req[0x12] = {user_id, IPMI_PASSWORD_SET_PASSWORD};
gsize password_sz;
g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
g_return_val_if_fail(user_id != 0x0, FALSE);
g_return_val_if_fail(password != NULL, FALSE);
/* copy into buffer */
password_sz = strlen(password);
if (!fu_memcpy_safe(req,
sizeof(req),
0x2, /* dst */
(guint8 *)password,
password_sz,
0x0, /* src */
password_sz,
error)) {
g_prefix_error(error, "password invalid: ");
return FALSE;
}
/* run transaction */
if (!fu_ipmi_device_transaction(self,
IPMI_NETFN_APP_REQUEST,
IPMI_SET_USER_PASSWORD,
req,
sizeof(req),
NULL, /* resp */
0,
NULL,
FU_IPMI_DEVICE_TIMEOUT,
error)) {
g_prefix_error(error, "failed to set user %02x password: ", user_id);
return FALSE;
}
/* success */
return TRUE;
}
gboolean
fu_ipmi_device_set_user_priv(FuIpmiDevice *self,
guint8 user_id,
guint8 priv_limit,
guint8 channel,
GError **error)
{
const guint8 req[] = {channel, user_id, priv_limit, 0x0};
g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE);
g_return_val_if_fail(user_id != 0x0, FALSE);
g_return_val_if_fail(channel <= 0x0F, FALSE);
g_return_val_if_fail(priv_limit <= 0x0F, FALSE);
/* run transaction */
if (!fu_ipmi_device_transaction(self,
IPMI_NETFN_APP_REQUEST,
IPMI_SET_USER_ACCESS,
req,
sizeof(req),
NULL, /* resp */
0,
NULL,
FU_IPMI_DEVICE_TIMEOUT,
error)) {
g_prefix_error(error,
"failed to set user %02x privs of 0x%02x, 0x%02x: ",
user_id,
priv_limit,
channel);
return FALSE;
}
/* success */
return TRUE;
}
static void
fu_ipmi_device_init(FuIpmiDevice *self)
{
fu_device_set_name(FU_DEVICE(self), "IPMI");
fu_device_set_summary(FU_DEVICE(self), "Intelligent Platform Management Interface");
fu_device_add_icon(FU_DEVICE(self), "computer");
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL);
}
static void
fu_ipmi_device_class_init(FuIpmiDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
klass_device->probe = fu_ipmi_device_probe;
klass_device->setup = fu_ipmi_device_setup;
klass_device->to_string = fu_ipmi_device_to_string;
}
FuIpmiDevice *
fu_ipmi_device_new(FuContext *ctx)
{
FuIpmiDevice *self;
self = g_object_new(FU_TYPE_IPMI_DEVICE, "context", ctx, "device-file", "/dev/ipmi0", NULL);
return self;
}