mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-25 22:40:50 +00:00
594 lines
14 KiB
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;
|
|
}
|