Commit 449095c3 authored by jan.koester's avatar jan.koester
Browse files

test

parent 89734508
Loading
Loading
Loading
Loading
+7 −4
Changes for include/storaged/api.h: 7 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -110,16 +110,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   Activate / Deactivate {vg, lv?}     without lv: every plain LV
 *
 * SMART (ATA drives over SG_IO -- SATA, SAS and USB bridges that pass ATA
 * through --, NVMe drives with admin commands; no smartctl). An ATA drive
 * in standby isn't spun up: its last reading is shown (read_at), power
 * through --, SCSI/SAS drives with LOG SENSE, NVMe drives with admin
 * commands; no smartctl). An ATA or SCSI drive in standby isn't spun up: its last reading is shown (read_at), power
 * says standby. A drive that doesn't answer keeps its last reading too,
 * with problem set.
 *   ListSmart -> per disk: disk (name), size, model, serial?, firmware?,
 *       transport (ata|nvme|none), smart (available), enabled, rotation?
 *       transport (ata|scsi|nvme|none), smart (available), enabled, rotation?
 *       (ssd|rpm), power (active|idle|standby|unknown), health
 *       (passed|failing|unknown), temperature? (°C), power_on_hours?,
 *       power_cycles?, ATA: reallocated?, pending?, uncorrectable?,
 *       crc_errors?; NVMe: critical_warning (bits), available_spare,
 *       crc_errors?; SCSI: reallocated? (grown defects), uncorrectable?,
 *       start_stop_cycles?, load_cycles?, non_medium_errors?, data_read?,
 *       data_written? (bytes), exception? (ASC/ASCQ of an informational
 *       exception); NVMe: critical_warning (bits), available_spare,
 *       spare_threshold, percentage_used (%), data_read, data_written
 *       (bytes), unsafe_shutdowns, media_errors, error_log_entries;
 *       self_test_supported, self_test? (passed|failed|aborted|
+238 −2
Changes for src/smart.cpp: 238 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -401,6 +401,228 @@ void ataSummary(Fields &f) {
    f.insert(first, s.begin(), s.end());
}

// ─── SCSI (SAS drives: LOG SENSE, MODE SENSE, SEND DIAGNOSTIC) ──────────────

uint64_t be(const uint8_t *p, int n) {
    uint64_t v = 0;
    for (int i = 0; i < n; ++i) v = (v << 8) | p[i];
    return v;
}

// a SCSI command; data: len bytes in (nullptr: none). sense: the sense
// key, ASC, ASCQ of a CHECK CONDITION (else 0)
struct Sense {
    int key = 0, asc = 0, ascq = 0;
};
bool scsi(int fd, const uint8_t *cdb, int cdbLen, uint8_t *data, unsigned len, std::string &err, Sense *out = nullptr) {
    uint8_t sense[32] = {};
    SgIoHdr io{};
    io.interface_id = 'S';
    io.dxfer_direction = data ? SgDxferFromDev : SgDxferNone;
    io.cmd_len = cdbLen;
    io.cmdp = const_cast<uint8_t *>(cdb);
    io.mx_sb_len = sizeof(sense);
    io.sbp = sense;
    io.dxfer_len = data ? len : 0;
    io.dxferp = data;
    io.timeout = 15000;
    if (ioctl(fd, SgIo, &io) < 0) {
        err = blk::sysErr("SG_IO", errno);
        return false;
    }
    if (io.host_status != 0) {
        err = "host status " + std::to_string(io.host_status);
        return false;
    }
    Sense s;
    if (io.sb_len_wr > 0) {
        int code = sense[0] & 0x7f;
        if (code == 0x72 || code == 0x73) s = {sense[1] & 0x0f, sense[2], sense[3]};
        else if (code == 0x70 || code == 0x71) s = {sense[2] & 0x0f, sense[12], sense[13]};
    }
    if (out) *out = s;
    if (s.key != 0 && s.key != 1) {
        err = s.key == 5 ? "not supported" : "sense key " + std::to_string(s.key);
        return false;
    }
    if (io.status != 0 && s.key == 0) {
        err = "SCSI status " + std::to_string(io.status);
        return false;
    }
    return true;
}

// LOG SENSE, current cumulative values; the page's length (0: none)
unsigned logSense(int fd, uint8_t page, uint8_t *buf, unsigned len) {
    uint8_t cdb[10] = {0x4D, 0, (uint8_t)(0x40 | page), 0, 0, 0, 0, (uint8_t)(len >> 8), (uint8_t)len, 0};
    std::string err;
    std::memset(buf, 0, len);
    if (!scsi(fd, cdb, sizeof(cdb), buf, len, err) || (buf[0] & 0x3f) != page) return 0;
    return std::min<unsigned>(len, 4 + (unsigned)be(buf + 2, 2));
}

// the parameters of a log page: code -> its data
std::map<int, std::pair<const uint8_t *, int>> logParams(const uint8_t *buf, unsigned len) {
    std::map<int, std::pair<const uint8_t *, int>> p;
    for (unsigned i = 4; i + 4 <= len;) {
        int n = buf[i + 3];
        if (i + 4 + n > len) break;
        p[(int)be(buf + i, 2)] = {buf + i + 4, n};
        i += 4 + n;
    }
    return p;
}

// MODE SENSE (10) of one page, current values; the page itself (nullptr: none)
const uint8_t *modeSense(int fd, uint8_t page, uint8_t *buf, unsigned len) {
    uint8_t cdb[10] = {0x5A, 0x08, page, 0, 0, 0, 0, (uint8_t)(len >> 8), (uint8_t)len, 0};  // DBD
    std::string err;
    std::memset(buf, 0, len);
    if (!scsi(fd, cdb, sizeof(cdb), buf, len, err)) return nullptr;
    unsigned off = 8 + (unsigned)be(buf + 6, 2);  // header, block descriptors
    if (off + 2 > len || (buf[off] & 0x3f) != page) return nullptr;
    return buf + off;
}

// a VPD page of INQUIRY; its length (0: none)
unsigned vpd(int fd, uint8_t page, uint8_t *buf, unsigned len) {
    uint8_t cdb[6] = {0x12, 0x01, page, (uint8_t)(len >> 8), (uint8_t)len, 0};
    std::string err;
    std::memset(buf, 0, len);
    if (!scsi(fd, cdb, sizeof(cdb), buf, len, err) || buf[1] != page) return 0;
    return std::min<unsigned>(len, 4 + (unsigned)be(buf + 2, 2));
}

Fields readScsi(int fd, bool wake, const std::string &ataProblem) {
    Fields f;
    std::string err;
    uint8_t inq[96] = {};
    uint8_t cdb[6] = {0x12, 0, 0, 0, sizeof(inq), 0};
    if (!scsi(fd, cdb, sizeof(cdb), inq, sizeof(inq), err) || (inq[0] & 0x1f) != 0x00) {
        f.emplace_back("transport", "none");
        f.emplace_back("smart", "no");
        f.emplace_back("problem", ataProblem);
        return f;
    }
    f.emplace_back("transport", "scsi");
    std::string vendor = trimmed(std::string((char *)inq + 8, 8)), product = trimmed(std::string((char *)inq + 16, 16));
    f.emplace_back("model", vendor.empty() ? product : vendor + " " + product);
    uint8_t buf[1024];
    if (unsigned n = vpd(fd, 0x80, buf, 255)) f.emplace_back("serial", trimmed(std::string((char *)buf + 4, n - 4)));
    f.emplace_back("firmware", trimmed(std::string((char *)inq + 32, 4)));
    // Block Device Characteristics: medium rotation rate
    if (vpd(fd, 0xB1, buf, 64) >= 6) {
        unsigned rpm = (unsigned)be(buf + 4, 2);
        if (rpm == 1) f.emplace_back("rotation", "ssd");
        else if (rpm >= 0x401 && rpm < 0xFFFF) f.emplace_back("rotation", std::to_string(rpm));
    }

    std::set<int> pages;
    if (unsigned n = logSense(fd, 0x00, buf, 255))
        for (unsigned i = 4; i < n; ++i) pages.insert(buf[i] & 0x3f);
    // Informational Exceptions: the drive's failure prediction
    const uint8_t *iec = modeSense(fd, 0x1C, buf, 64);
    bool supported = pages.count(0x2F) || iec, enabled = iec ? !(iec[2] & 0x08) : supported;  // DEXCPT
    f.emplace_back("smart", supported ? "yes" : "no");
    f.emplace_back("enabled", enabled ? "yes" : "no");
    if (!supported) {
        f.emplace_back("problem", "the drive has no SMART");
        return f;
    }

    // a drive in standby isn't spun up: REQUEST SENSE doesn't wake it
    uint8_t rs[18] = {};
    uint8_t rsCdb[6] = {0x03, 0, 0, 0, sizeof(rs), 0};
    if (scsi(fd, rsCdb, sizeof(rsCdb), rs, sizeof(rs), err)) {
        int asc = rs[12], ascq = rs[13];
        bool stopped = (rs[2] & 0x0f) == 2 && asc == 0x04 && ascq == 0x02;  // needs START UNIT
        bool standby = stopped || (asc == 0x5E && (ascq == 0x02 || ascq == 0x04 || ascq == 0x09 || ascq == 0x0A));
        bool idle = asc == 0x5E && (ascq == 0x01 || ascq == 0x03 || (ascq >= 0x05 && ascq <= 0x08));
        f.emplace_back("power", standby ? "standby" : idle ? "idle" : "active");
        if (standby && !wake) {
            f.emplace_back("asleep", "yes");
            return f;
        }
    } else {
        f.emplace_back("power", "unknown");
    }

    int temperature = -1;
    if (pages.count(0x2F)) {
        unsigned n = logSense(fd, 0x2F, buf, sizeof(buf));
        auto p = logParams(buf, n);
        if (p.count(0) && p[0].second >= 2) {
            const uint8_t *d = p[0].first;
            // ASC 5Dh: a failure prediction threshold exceeded
            f.emplace_back("health", d[0] == 0x5D ? "failing" : "passed");
            if (d[0]) f.emplace_back("exception", std::to_string(d[0]) + "/" + std::to_string(d[1]));
            if (p[0].second >= 3 && d[2] && d[2] != 0xFF) temperature = d[2];
        } else {
            f.emplace_back("health", "unknown");
        }
    } else {
        f.emplace_back("health", "unknown");
    }
    if (pages.count(0x0D)) {
        auto p = logParams(buf, logSense(fd, 0x0D, buf, sizeof(buf)));
        if (p.count(0) && p[0].second >= 2 && p[0].first[1] != 0xFF) temperature = p[0].first[1];
    }
    if (temperature >= 0) f.emplace_back("temperature", std::to_string(temperature));
    // Background Scan Results: accumulated power-on minutes
    if (pages.count(0x15)) {
        auto p = logParams(buf, logSense(fd, 0x15, buf, sizeof(buf)));
        if (p.count(0) && p[0].second >= 4) f.emplace_back("power_on_hours", std::to_string(be(p[0].first, 4) / 60));
    }
    if (pages.count(0x0E)) {
        auto p = logParams(buf, logSense(fd, 0x0E, buf, sizeof(buf)));
        if (p.count(4) && p[4].second >= 4) f.emplace_back("start_stop_cycles", std::to_string(be(p[4].first, 4)));
        if (p.count(6) && p[6].second >= 4) f.emplace_back("load_cycles", std::to_string(be(p[6].first, 4)));
    }

    // the grown defect list: sectors the drive replaced (READ DEFECT DATA (10))
    uint8_t dd[8] = {};
    uint8_t ddCdb[10] = {0x37, 0, 0x08 | 0x05, 0, 0, 0, 0, 0, sizeof(dd), 0};  // GLIST, physical sector format
    if (scsi(fd, ddCdb, sizeof(ddCdb), dd, sizeof(dd), err) && (dd[1] & 0x08))
        f.emplace_back("reallocated", std::to_string(be(dd + 2, 2) / 8));

    // Error counters: uncorrected errors (parameter 6), bytes processed (5)
    long long uncorrected = -1;
    for (uint8_t page : {0x03, 0x02, 0x05}) {  // read, write, verify
        if (!pages.count(page)) continue;
        auto p = logParams(buf, logSense(fd, page, buf, sizeof(buf)));
        if (p.count(6) && p[6].second > 0) uncorrected = std::max(uncorrected, 0LL) + (long long)be(p[6].first, std::min(p[6].second, 8));
        if (p.count(5) && p[5].second > 0 && page != 0x05)
            f.emplace_back(page == 0x03 ? "data_read" : "data_written", std::to_string(be(p[5].first, std::min(p[5].second, 8))));
    }
    if (uncorrected >= 0) f.emplace_back("uncorrectable", std::to_string(uncorrected));
    if (pages.count(0x06)) {
        auto p = logParams(buf, logSense(fd, 0x06, buf, sizeof(buf)));
        if (p.count(0) && p[0].second > 0) f.emplace_back("non_medium_errors", std::to_string(be(p[0].first, std::min(p[0].second, 8))));
    }

    // Self-Test Results: parameter 1 is the newest
    bool selfTest = pages.count(0x10);
    f.emplace_back("self_test_supported", selfTest ? "yes" : "no");
    if (selfTest) {
        auto p = logParams(buf, logSense(fd, 0x10, buf, sizeof(buf)));
        if (p.count(1) && p[1].second >= 1) {
            int code = p[1].first[0] >> 5, result = p[1].first[0] & 0x0f;
            f.emplace_back("self_test", code == 0 && result == 0 ? "none"
                                        : result == 0 ? "passed"
                                        : result == 1 ? "aborted"
                                        : result == 2 ? "interrupted"
                                        : result == 15 ? "running" : "failed");
        }
        // Control mode page: extended self-test completion time (s)
        if (const uint8_t *ctl = modeSense(fd, 0x0A, buf, 64))
            if (ctl[1] >= 10) {
                unsigned s = (unsigned)be(ctl + 10, 2);
                if (s && s != 0xFFFF) f.emplace_back("long_test_minutes", std::to_string((s + 59) / 60));
            }
    }
    return f;
}

// ─── NVMe (admin commands) ──────────────────────────────────────────────────

int nvmeAdmin(int fd, uint8_t opcode, uint32_t nsid, uint32_t cdw10, void *buf, uint32_t len) {
@@ -510,8 +732,10 @@ std::string readDrive(const std::string &name, bool wake) {
    int fd = open(("/dev/" + name).c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC);
    if (fd < 0) return pack({{"transport", "none"}, {"smart", "no"}, {"problem", blk::sysErr("/dev/" + name, errno)}});
    Fields f = k == Kind::Nvme ? readNvme(fd) : readAta(fd, wake);
    // no ATA, not even through a SAT layer: a SCSI drive (SAS)
    if (k == Kind::Ata && get(f, "transport") == "none") f = readScsi(fd, wake, get(f, "problem"));
    close(fd);
    if (k == Kind::Ata) ataSummary(f);
    if (get(f, "transport") == "ata") ataSummary(f);
    return pack(f);
}

@@ -525,8 +749,20 @@ std::string runTest(const std::string &name, const std::string &type) {
        int rc = nvmeAdmin(fd, 0x14, 0xFFFFFFFF, code, nullptr, 0);
        if (rc != 0) err = nvmeErr(rc);
    } else {
        uint8_t id[512];
        AtaCmd identify;
        identify.command = 0xEC;
        identify.count = 1;
        if (ata(fd, identify, id, nullptr, err)) {
            uint8_t sub = type == "short" ? 1 : type == "long" ? 2 : 127;  // offline immediate: 127 aborts
            if (!ata(fd, smartCmd(0xD4, sub), nullptr, nullptr, err) && err.empty()) err = "failed";
        } else {
            // SCSI: SEND DIAGNOSTIC, background short/extended, or abort
            err.clear();
            uint8_t code = type == "short" ? 1 : type == "long" ? 2 : 4;
            uint8_t cdb[6] = {0x1D, (uint8_t)(code << 5), 0, 0, 0, 0};
            if (!scsi(fd, cdb, sizeof(cdb), nullptr, 0, err) && err.empty()) err = "failed";
        }
    }
    close(fd);
    return err.empty() ? "ok" : err;
@@ -585,7 +821,7 @@ std::map<std::string, std::pair<Fields, std::string>> readAll(const std::vector<
// what is wrong with a drive, in the language of the call
std::vector<std::string> warnings(const Fields &f) {
    std::vector<std::string> w;
    if (get(f, "health") == "failing" && get(f, "transport") == "ata")
    if (get(f, "health") == "failing" && get(f, "transport") != "nvme")
        w.push_back(tr("the drive predicts its own failure (SMART status)", "das Laufwerk sagt seinen Ausfall voraus (SMART-Status)"));
    std::string attr, name;
    for (auto &kv : f) {
+2 −1
Changes for src/smart.h: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -26,7 +26,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* SMART, without smartctl: ATA drives (SATA, and SAS/USB bridges that
 * translate ATA PASS-THROUGH) over SG_IO, NVMe drives with the admin
 * translate ATA PASS-THROUGH) over SG_IO, SCSI drives (SAS) with LOG
 * SENSE / SEND DIAGNOSTIC over SG_IO, NVMe drives with the admin
 * commands of the nvme driver's ioctl. The methods are in
 * include/storaged/api.h.
 *