mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-30 16:14:52 +00:00

This allows the Redfish plugin to "just work" when there is no username or password in the SMBIOS data. Using KCS we can create an admin account from the host OS and then automatically enumerate devices.
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)
|
|
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)
|
|
fu_common_dump_raw(G_LOG_DOMAIN, "ipmi-recv", buf, *len);
|
|
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;
|
|
}
|