/*
* AMD IPMI LED control
* Copyright (C) 2019, Advanced Micro Devices, Inc.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libgen.h>
#include <inttypes.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/sysinfo.h>
#include <sys/file.h>
#if _HAVE_DMALLOC_H
#include <dmalloc.h>
#endif
#include "config.h"
#include "ibpi.h"
#include "list.h"
#include "utils.h"
#include "amd.h"
#include "ipmi.h"
static uint8_t amd_ibpi_ipmi_register[] = {
[IBPI_PATTERN_PFA] = 0x41,
[IBPI_PATTERN_LOCATE] = 0x42,
[IBPI_PATTERN_FAILED_DRIVE] = 0x44,
[IBPI_PATTERN_FAILED_ARRAY] = 0x45,
[IBPI_PATTERN_REBUILD] = 0x46,
[IBPI_PATTERN_HOTSPARE] = 0x47,
};
/* The path we are given should be similar to
* /sys/devices/pci0000:e0/0000:e0:03.3/0000:e3:00.0
* ^^^^^^^^^^
* We need to retrieve the address from the path (indicated above)
* then use it to find the corresponding address for a slot in
* /sys/bus/pci_slots to determine the icorrect port for the NVMe device.
*/
static int _get_ipmi_nvme_port(char *path)
{
int rc;
char *p, *f;
struct list dir;
const char *dir_path;
char *port_name;
int port = -1;
p = strrchr(path, '/');
if (!p) {
log_error("Couldn't parse NVMe path to determine port\n");
return -1;
}
p++;
/* p now points to the address, remove the bits after the '.' */
f = strchr(p, '.');
if (!f) {
log_error("Couldn't parse NVMe port address\n");
return -1;
}
*f = '\0';
rc = scan_dir("/sys/bus/pci/slots", &dir);
if (rc)
return -1;
list_for_each(&dir, dir_path) {
port_name = get_text(dir_path, "address");
if (port_name && !strcmp(port_name, p)) {
char *dname = strrchr(dir_path, '/');
dname++;
port = strtol(dname, NULL, 0);
break;
}
}
list_erase(&dir);
/* Some platfroms require an adjustment to the port value based
* on how they are numbered by the BIOS.
*/
switch (amd_platform) {
case AMD_PLATFORM_DAYTONA_X:
port -= 2;
break;
case AMD_PLATFORM_ETHANOL_X:
port -= 7;
break;
default:
break;
}
/* Validate port. Some BIOSes provide port values that are
* not valid.
*/
if ((port < 0) || (port > 24)) {
log_error("Invalid NVMe physical port %d\n", port);
port = -1;
}
return port;
}
static int _get_ipmi_sata_port(const char *start_path)
{
int port;
char *p, *t;
char path[PATH_MAX];
strncpy(path, start_path, PATH_MAX);
path[PATH_MAX - 1] = 0;
t = p = strstr(path, "ata");
if (!p)
return -1;
/* terminate the path after the ataXX/ part */
p = strchr(p, '/');
if (!p)
return -1;
*p = '\0';
/* skip past 'ata' to get the ata port number */
t += 3;
port = strtoul(t, NULL, 10);
return port;
}
static int _get_amd_ipmi_drive(const char *start_path,
struct amd_drive *drive)
{
int found;
char path[PATH_MAX];
found = _find_file_path(start_path, "nvme", path, PATH_MAX);
if (found) {
drive->port = _get_ipmi_nvme_port(path);
if (drive->port < 0) {
log_error("Could not retrieve port number\n");
return -1;
}
drive->drive_bay = 1 << (drive->port - 1);
drive->dev = AMD_NVME_DEVICE;
} else {
int shift;
drive->port = _get_ipmi_sata_port(start_path);
if (drive->port < 0) {
log_error("Could not retrieve port number\n");
return -1;
}
/* IPMI control is handled through the MG9098 chips on
* the platform, where each MG9098 chip can control up
* to 8 drives. Since we can have multiple MG9098 chips,
* we need the drive bay relative to the set of 8 controlled
* by the MG9098 chip.
*/
shift = drive->port - 1;
if (shift >= 8)
shift %= 8;
drive->drive_bay = 1 << shift;
drive->dev = AMD_SATA_DEVICE;
}
log_debug("AMD Drive: port: %d, bay %x\n", drive->port,
drive->drive_bay);
return 0;
}
static int _ipmi_platform_channel(struct amd_drive *drive)
{
int rc = 0;
switch (amd_platform) {
case AMD_PLATFORM_ETHANOL_X:
drive->channel = 0xd;
break;
case AMD_PLATFORM_DAYTONA_X:
drive->channel = 0x17;
break;
default:
rc = -1;
log_error("AMD Platform does not have a defined IPMI channel\n");
break;
}
return rc;
}
static int _ipmi_platform_slave_address(struct amd_drive *drive)
{
int rc = 0;
switch (amd_platform) {
case AMD_PLATFORM_ETHANOL_X:
drive->slave_addr = 0xc0;
break;
case AMD_PLATFORM_DAYTONA_X:
if (drive->dev == AMD_NO_DEVICE) {
/* Assume base slave address, we may not be able
* to retrieve a valid amd_drive yet.
*/
drive->slave_addr = 0xc0;
} else if (drive->dev == AMD_NVME_DEVICE) {
/* On DaytonaX systems only drive bays 19 - 24
* support NVMe devices so use the slave address
* for the corresponding MG9098 chip.
*/
drive->slave_addr = 0xc4;
} else {
if (drive->port <= 8)
drive->slave_addr = 0xc0;
else if (drive->port > 8 && drive->port < 17)
drive->slave_addr = 0xc2;
else
drive->slave_addr = 0xc4;
}
break;
default:
rc = -1;
log_error("AMD Platform does not have a defined IPMI slave address\n");
break;
}
return rc;
}
static int _set_ipmi_register(int enable, uint8_t reg,
struct amd_drive *drive)
{
int rc;
int status, data_sz;
uint8_t drives_status;
uint8_t new_drives_status;
uint8_t cmd_data[5];
memset(cmd_data, 0, sizeof(cmd_data));
rc = _ipmi_platform_channel(drive);
rc |= _ipmi_platform_slave_address(drive);
if (rc)
return -1;
cmd_data[0] = drive->channel;
cmd_data[1] = drive->slave_addr;
cmd_data[2] = 0x1;
cmd_data[3] = reg;
/* Find current register setting */
status = 0;
log_debug("Retrieving current register status\n");
log_debug(REG_FMT_2, "channel", cmd_data[0], "slave addr", cmd_data[1]);
log_debug(REG_FMT_2, "len", cmd_data[2], "register", cmd_data[3]);
rc = ipmicmd(BMC_SA, 0x0, 0x6, 0x52, 4, &cmd_data, 1, &data_sz,
&status);
if (rc) {
log_error("Could not determine current register %x setting\n",
reg);
return rc;
}
drives_status = status;
if (enable)
new_drives_status = drives_status | drive->drive_bay;
else
new_drives_status = drives_status & ~drive->drive_bay;
/* Set the appropriate status */
status = 0;
cmd_data[4] = new_drives_status;
log_debug("Updating register status: %x -> %x\n", drives_status,
new_drives_status);
log_debug(REG_FMT_2, "channel", cmd_data[0], "slave addr", cmd_data[1]);
log_debug(REG_FMT_2, "len", cmd_data[2], "register", cmd_data[3]);
log_debug(REG_FMT_1, "status", cmd_data[4]);
rc = ipmicmd(BMC_SA, 0x0, 0x6, 0x52, 5, &cmd_data, 1, &data_sz,
&status);
if (rc) {
log_error("Could not enable register %x\n", reg);
return rc;
}
return 0;
}
static int _enable_smbus_control(struct amd_drive *drive)
{
log_debug("Enabling SMBUS Control\n");
return _set_ipmi_register(1, 0x3c, drive);
}
static int _enable_ibpi_state(struct amd_drive *drive, enum ibpi_pattern ibpi)
{
log_debug("Enabling %s LED\n", ibpi2str(ibpi));
return _set_ipmi_register(1, amd_ibpi_ipmi_register[ibpi], drive);
}
static int _disable_ibpi_state(struct amd_drive *drive, enum ibpi_pattern ibpi)
{
log_debug("Disabling %s LED\n", ibpi2str(ibpi));
return _set_ipmi_register(0, amd_ibpi_ipmi_register[ibpi], drive);
}
static int _disable_all_ibpi_states(struct amd_drive *drive)
{
int rc;
rc = _disable_ibpi_state(drive, IBPI_PATTERN_PFA);
rc |= _disable_ibpi_state(drive, IBPI_PATTERN_LOCATE);
rc |= _disable_ibpi_state(drive, IBPI_PATTERN_FAILED_DRIVE);
rc |= _disable_ibpi_state(drive, IBPI_PATTERN_FAILED_ARRAY);
rc |= _disable_ibpi_state(drive, IBPI_PATTERN_REBUILD);
return rc;
}
int _amd_ipmi_em_enabled(const char *path)
{
int rc;
int status, data_sz;
uint8_t cmd_data[4];
struct amd_drive drive;
memset(&drive, 0, sizeof(struct amd_drive));
rc = _ipmi_platform_channel(&drive);
rc |= _ipmi_platform_slave_address(&drive);
if (rc)
return -1;
cmd_data[0] = drive.channel;
cmd_data[1] = drive.slave_addr;
cmd_data[2] = 0x1;
cmd_data[3] = 0x63;
status = 0;
rc = ipmicmd(BMC_SA, 0x0, 0x6, 0x52, 4, &cmd_data, 1,
&data_sz, &status);
if (rc) {
log_error("Can't determine MG9098 Status\n");
return 0;
}
if (status != 98) {
log_error("Not a MG9098\n");
return 0;
}
return 1;
}
int _amd_ipmi_write(struct block_device *device, enum ibpi_pattern ibpi)
{
int rc;
struct amd_drive drive;
log_info("\n");
log_info("Setting %s...", ibpi2str(ibpi));
rc = _get_amd_ipmi_drive(device->cntrl_path, &drive);
if (rc)
return rc;
if ((ibpi == IBPI_PATTERN_NORMAL) ||
(ibpi == IBPI_PATTERN_ONESHOT_NORMAL)) {
rc = _disable_all_ibpi_states(&drive);
return rc;
}
if (ibpi == IBPI_PATTERN_LOCATE_OFF) {
rc = _disable_ibpi_state(&drive, IBPI_PATTERN_LOCATE);
return rc;
}
rc = _enable_smbus_control(&drive);
if (rc)
return rc;
rc = _enable_ibpi_state(&drive, ibpi);
if (rc)
return rc;
return 0;
}
char *_amd_ipmi_get_path(const char *cntrl_path, const char *sysfs_path)
{
char *p, *t;
/* For NVMe devices we can just dup the path sysfs path */
p = strstr(cntrl_path, "nvme");
if (p)
return strdup(sysfs_path);
/* For SATA devices we need everything up to 'ataXX/' in the path */
p = strdup(cntrl_path);
if (!p)
return NULL;
/* Find the beginning of the ataXX piece of the path */
t = strstr(p, "ata");
if (!t)
return NULL;
/* Move to the '/' after the ataXX piece of the path and terminate the
* string there.
*/
t = strchr(t, '/');
if (!t)
return NULL;
*++t = '\0';
return p;
}