PDA

View Full Version : getting model an serial information from SAS harddisk



thomas23
02-15-2014, 11:32 AM
platform: linux

I am writing a little program which requires to get the serial and model information out of harddisks. (SAS, FC, SATA etc)

for SATA it is very easy with an ioctl command (HDIO_GET_IDENTITY) which is defined in <linux/hdreg.h>.

However this doesnt work on SAS and FC drives. So I thought to myself they are SCSI based and thus a SCSI inquiry would work. So I write a neat little function which sends and inquiry for me and retrieves the output.

Now I get gibberish as the output from the inquiry(it doesnt 'fail' though). the drives are attached to a pci-e SAS card.
I suspect the SAS card does something odd as I have tried to use my program on a virtual machine with virtual SAS drives and that seems to work...

Also the SATA drives are hooked up to the same pci-e card (SAS offers backwards compatibility with SATA)

is there another way to get serial and model information out of a harddisk besides the ways I have done?

regards

c99tutorial
02-15-2014, 03:43 PM
This question is kind of Linux specific not exactly "C Programming" I would say. Have you tried hdparm? There are blog posts about it describing it in detail.

Linux: Find out serial / model number and vendor information for SATA and IDE hard disk (http://www.cyberciti.biz/faq/linux-getting-scsi-ide-harddisk-information/)

If you want to implement the same thing that such a program can do, the standard answer in this situation would be "use the source".

thomas23
02-15-2014, 09:10 PM
This question is kind of Linux specific not exactly "C Programming" I would say. Have you tried hdparm? There are blog posts about it describing it in detail.

Linux: Find out serial / model number and vendor information for SATA and IDE hard disk (http://www.cyberciti.biz/faq/linux-getting-scsi-ide-harddisk-information/)

If you want to implement the same thing that such a program can do, the standard answer in this situation would be "use the source".

hdparm uses ioctl. it doesnt work on scsi disks. There is a similair program called sdparm for scsi disks and that use a SCSI inquiry. so same result...

Codeplug
02-16-2014, 07:42 AM
Do you know of any existing tool that works? I would look at that source code...

gg

Nominal Animal
02-16-2014, 05:10 PM
If you use udev and have /sys mounted, then use bash and udevadm:


for DEV in /sys/block/* ; do
udevadm info --query=property --path=$DEV
echo
done

to output all the features of known block devices.

Note that most distributions have udev rules that generate symlinks in /dev/disk/by-id/ and /dev/disk/by/path/.

For an abbreviated list of what you asked, you can use udevadm, bash, and awk. (Note that I'll use the device name instead of device path, just for illustration, in this case):

for DEV in /sys/block/* ; do
udevadm info --query=property --name=${DEV##*/} | \
awk '{ n=v=$0; sub(/=.*$/,"",n); sub(/^[^=]*=/, "", v); p[n]=v }
END {
if ("ID_MODEL" in p) {
if ("ID_SERIAL_SHORT" in p)
s=p["ID_SERIAL_SHORT"];
else
s=p["ID_SERIAL"];
printf "%s\t%s\t%s\n", p["DEVNAME"], p["ID_MODEL"], s
}
}' ;
done
The /sys pseudo-filesystem does contain information on each block device, including size and vendor and model strings, e.g.

for DEV in /sys/block/* ; do
printf "%s\t%s\t%s\t%s MiB\n" "${DEV##*/}" "$(cat $DEV/device/vendor 2>/dev/null)" "$(cat $DEV/device/model 2>/dev/null)" $[($(cat $DEV/size)-0)/2048];
done
The /sys/block/DEVICE/size pseudofile contains the size of the block device in 512 byte units, therefore dividing its value by 2048 yields the block device size in units of 1048576 bytes.

In case you're wondering, the extra semicolons at the end of above lines are not needed if copy-pasted as it, but they allow you to paste the entire snippet as a single line command. That is, you can safely paste the above commands into a terminal, and run them as a non-root-user. The information does not require any special privileges to obtain.

In an application written in C, I'd personally recommend using two helper scripts, executed externally by your application. First, /usr/lib/yourapp/blockdev-list would output the list of known block devices, separated by ASCII NULs:

#!/bin/bash
udevadm info --export-db | awk 'BEGIN { split("", prop) }
function show() {
if (prop["SUBSYSTEM"]=="block")
printf "%s\0", prop["DEVNAME"]
split("", prop)
}
/^[^\t ]*$/ { show(); next }
/^E:/ { s=$0; sub(/^E:[\t ]*/, "", s); n=v=s; sub(/=.*$/, "", n); sub(/^[^=]*=/, "", v); prop[n]=v; next }
END { show() }'

Note that this uses udev database for block devices, and therefore should include those devices that create their own subtrees instead of direct device nodes (making globbing their names harder than normal) -- I seem to recall most SAS drivers do this, but am too lazy to verify; in any case, this approach should list them all. I personally prefer this approach in all cases.

Second, /usr/lib/yourapp/blockdev-info would output the interesting details of specified block devices. The below example outputs specific "lines" per device, with the first character defining the value type (N for device node (path), T for device type, M for device model, S for device serial number, and R for device revision), and ASCII NUL as the newline character:

#!/bin/bash
for DEV in "$@" ; do
udevadm info --query=property --name="$DEV" | awk 'BEGIN { split("", prop) }
{ n=v=$0; sub(/=.*$/, "", n); sub(/^[^=]*=/, "", v); prop[n]=v }
END {
if ("ID_SERIAL_SHORT" in prop)
serial=prop["ID_SERIAL_SHORT"]
else
serial=prop["ID_SERIAL"]
printf "N%s\0T%s\0M%s\0S%s\0R%s\0", prop["DEVNAME"], prop["DEVTYPE"], prop["ID_MODEL"], serial, prop["ID_REVISION"]
}'
done
The exact output format from the above is obviously up to you, but using ASCII NULs as the separator is the most reliable option, as the Linux kernel uses it internally as the end-of-string mark. Using the first character as the identifier makes it trivial to parse the entries. The output from above scripts is easy to parse using the POSIX.1-2008 getdelim() (http://man7.org/linux/man-pages/man3/getdelim.3.html) function (assuming the scripts were executed using e.g. popen() (http://man7.org/linux/man-pages/man3/popen.3.html), and it is easily extended to include other properties you might need later, too.

The reason I would split the operation into two scripts (for an application written in C or C++), is simply one of maintenance and portability. The /usr/lib/yourapp/ directory is the preferred location for these. This way, if there is some specific architecture or Linux distribution -- perhaps even a custom Linux distribution --, I could easily install target-specific scripts or even setuid binaries if necessary, to obtain the required information. Also, debugging and simulation of new devices becomes much simpler.

thomas23
02-17-2014, 05:01 PM
thank you. you are a godsend.

I have chosen to implement it without bash scripting simply because it doesn't need to be portable. I use it on my own linux derivation. I wonder though how reliable it is. Here is some sample code I threw together quickly. no fancy checking etc yet. I don't use getdelim(); or getline(). because I think they are difficult do deal with and not really as flexible as just writing a short while loop.



#define DRIVEAMOUNT 20

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main() {
/* initialize various vars.*/
FILE *driveptr,
*sysstream;


int i,j;


char OSpointer[9],
sysstdout[256],
serial[256],
serial_short[256],
model[256],
syscmd[256];



for (i=0;i<DRIVEAMOUNT;i++) {
strcpy(OSpointer,"/dev/sdX");
OSpointer[7]='a'+i;


driveptr=fopen(OSpointer,"r");
if (driveptr==NULL) {
continue;
}
fclose(driveptr);


sprintf(syscmd,"udevadm info --query=property --name=%s",OSpointer);
sysstream=popen(syscmd,"r");


do {
j=0;do {
sysstdout[j]=fgetc(sysstream);
++j;
} while (sysstdout[j-1]!='\n'&&sysstdout[j-1]!=EOF);
sscanf(sysstdout,"ID_SERIAL_SHORT=%s",&serial_short);
sscanf(sysstdout,"ID_SERIAL=%s",&serial);
sscanf(sysstdout,"ID_MODEL=%s",&model);
} while(sysstdout[j-1]!=EOF);

pclose(sysstream);


printf("%s\n |%s|\n |%s|\n |%s|\n\n",syscmd,serial_short,serial,model);
}
}

Nominal Animal
02-17-2014, 09:43 PM
Just for completeness:

You could instead use the libudev (http://www.freedesktop.org/software/systemd/libudev/) interface (part of systemd (http://www.freedesktop.org/software/systemd/)).

example.c:

#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <libudev.h>

struct blockdev_stats {
unsigned long long read_ops; /* Number of read I/Os processed */
unsigned long long read_merged; /* Number of read I/Os merged with in-queue I/O */
unsigned long long read_512bytes; /* Number of sectors read */
unsigned long long read_waits_ms; /* Total wait time for read requests, milliseconds */
unsigned long long write_ops; /* Number of write I/Os processed */
unsigned long long write_merged; /* Number of write I/Os merged with in-queue I/O */
unsigned long long write_512bytes; /* Number of sectors written */
unsigned long long write_waits_ms; /* Total wait time for write requests, milliseconds */
unsigned long long in_flight; /* Number of I/Os currently in flight */
unsigned long long active_ms; /* Total active time, milliseconds */
unsigned long long waits_ms; /* Total wait time, milliseconds */
};

/* Returns nonzero if stats is filled with the above values.
*/
static inline int get_blockdev_stats(struct udev_device *const device, struct blockdev_stats *const stats)
{
if (device != NULL && stats != NULL) {
const char *const s = udev_device_get_sysattr_value(device, "stat");
if (s == NULL || *s == '\0')
return 0;
if (sscanf(s, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu",
&(stats->read_ops), &(stats->read_merged), &(stats->read_512bytes), &(stats->read_waits_ms),
&(stats->write_ops), &(stats->write_merged), &(stats->write_512bytes), &(stats->write_waits_ms),
&(stats->in_flight), &(stats->active_ms), &(stats->waits_ms)) >= 11)
return 11;
}
return 0;
}

/* NULL pointer safe string comparison.
* Returns 1 if neither pointer is NULL, and the strings they point to match.
*/
static inline int eq(const char *const a, const char *const b)
{
if (a == NULL || b == NULL)
return 0;
else
return !strcmp(a, b);
}

/* NULL pointer safe string to long long conversion.
*/
static inline long long stoll(const char *const string, const long long error)
{
if (!string || *string == '\0')
return error;
return strtoll(string, NULL, 10);
}

int main(void)
{
struct udev *udevhandle;
struct udev_enumerate *blockdevices;
struct udev_list_entry *entry;
const char *devicepath;
struct udev_device *device;

/* Get an udev handle. */
udevhandle = udev_new();
if (!udevhandle) {
fprintf(stderr, "Cannot get udev handle.\n");
return 1;
}

/* Enumerate block devices. */
blockdevices = udev_enumerate_new(udevhandle);
udev_enumerate_add_match_subsystem(blockdevices, "block");
udev_enumerate_scan_devices(blockdevices);

/* Iterate through the enumerated list. */
udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(blockdevices)) {
/* Use the devpath in the list to obtain a reference to the device. */
devicepath = udev_list_entry_get_name(entry);
device = udev_device_new_from_syspath(udevhandle, devicepath);

/* Do the following only for "disk" devtypes in "block" subsystem: */
if (eq("disk", udev_device_get_devtype(device))) {

const char *const dev = udev_device_get_devnode(device); /* In /dev */
const char *const bus = udev_device_get_property_value(device, "ID_BUS");
const char *const model = udev_device_get_property_value(device, "ID_MODEL");
const char *const serial = udev_device_get_property_value(device, "ID_SERIAL");
const char *const serial_short = udev_device_get_property_value(device, "ID_SERIAL_SHORT");
const char *const revision = udev_device_get_property_value(device, "ID_REVISION");
const char *const vendor = udev_device_get_sysattr_value(device, "device/vendor");
const char *const rev = udev_device_get_sysattr_value(device, "device/rev");
const long size = stoll(udev_device_get_sysattr_value(device, "size"), -2048LL) / 2048LL;
const long removable = stoll(udev_device_get_sysattr_value(device, "removable"), -1LL);
const long readonly = stoll(udev_device_get_sysattr_value(device, "ro"), -1LL);
struct blockdev_stats stats;

if (dev)
printf("%s:\n", dev);
else
printf("Unknown device:\n");

if (size > 0L)
printf("\tSize: %ld MiB\n", size);

if (removable == 0L)
printf("\tRemovable: No\n");
else
if (removable == 1L)
printf("\tRemovable: Yes\n");

if (readonly == 0L)
printf("\tRead-only: No\n");
else
if (readonly == 1L)
printf("\tRead-only: Yes\n");

if (bus)
printf("\tBus: '%s'\n", bus);

if (vendor)
printf("\tVendor: '%s'\n", vendor);

if (model)
printf("\tModel: '%s'\n", model);

if (serial)
printf("\tSerial: '%s'\n", serial);

if (serial_short)
printf("\tShort serial: '%s'\n", serial_short);

if (revision)
printf("\tRevision: '%s' (udev)\n", revision);

if (rev)
printf("\tRevision: '%s' (sys)\n", rev);

if (get_blockdev_stats(device, &stats)) {
printf("\tRead: %llu requests, %llu MiB\n", stats.read_ops, stats.read_512bytes / 2048ULL);
printf("\tWritten: %llu requests, %llu MiB\n", stats.write_ops, stats.write_512bytes / 2048ULL);
}
}

/* Drop the device reference. */
udev_device_unref(device);
}

/* Release the enumerator object, and the udev handle. */
udev_enumerate_unref(blockdevices);
udev_unref(udevhandle);

return 0;
}
Apologies for the very messy and inelegant code.

Compile and run with e.g.


gcc -W -Wall -O3 example.c -ludev -o example
./example


Here's what the output looks like on my machine with two Samsung HD103UJ drives and a Samsung DVD-RW drive:


/dev/sda:
Size: 953869 MiB
Removable: No
Read-only: No
Bus: 'ata'
Vendor: 'ATA '
Model: 'SAMSUNG_HD103UJ'
Serial: 'SAMSUNG_HD103UJ_S13PJ90QA14955'
Short serial: 'S13PJ90QA14955'
Revision: '1AA01131' (udev)
Revision: '1AA0' (sys)
Read: 18678 requests, 523 MiB
Written: 43437 requests, 1385 MiB
/dev/sdb:
Size: 953869 MiB
Removable: No
Read-only: No
Bus: 'ata'
Vendor: 'ATA '
Model: 'SAMSUNG_HD103UJ'
Serial: 'SAMSUNG_HD103UJ_S13PJ9AQA16637'
Short serial: 'S13PJ9AQA16637'
Revision: '1AA01131' (udev)
Revision: '1AA0' (sys)
Read: 14348 requests, 431 MiB
Written: 42356 requests, 1328 MiB
/dev/sr0:
Size: 1023 MiB
Removable: Yes
Read-only: No
Bus: 'ata'
Vendor: 'TSSTcorp'
Model: 'TSSTcorp_CDDVDW_SH-S202N'
Serial: 'TSSTcorp_CDDVDW_SH-S202N'
Revision: 'SB01' (udev)
Revision: 'SB01' (sys)
Read: 0 requests, 0 MiB
Written: 0 requests, 0 MiB

The output actually includes logical devices like /dev/loop0 and so on, but I left those out for brevity. The statistics are only since the last bootup, of course. (These HD103UJ's actually have almost three years of power on time according to SMART attributes.)

If portability were desired, I'd still recommend the two-script approach. With that, you could port the program to Busybox-based Linux distros (which have minimal udev support), and to even e.g. FreeBSD, just by using different scripts. As it is, the this program relies on both Linux kernel features (/sys hierarchy) and libudev.

(But, since the OP mentioned this is only going to run on a customized Linux distribution, and if systemd-based udev is used, this is probably the easiest to maintain in the long run. Any udev updates or changes are handled by libudev, and the kernel interfaces are extremely unlikely to change in a way that would break interoperability.)

thomas23
02-18-2014, 06:41 AM
is it possible to use it via the path of the device? my entire code is set-up to use the path as the identifier. I must admit I have 0 experience with udev. it seems to mee it uses a struct called 'device' as identification.

Elkvis
02-18-2014, 07:23 AM
keep in mind that if the drive is part of a RAID array, chances are you won't see the physical drive itself, but rather the array or logical drive.

Nominal Animal
02-18-2014, 04:49 PM
Elkvis had an excellent point. Some SAN (and hardware RAID) drivers are known to organize the storage in weird ways, which may complicate or hinder listing them correctly. However, using the device node (path to /dev/DEVICE or whatever it happens to be), should cut right through all that complexity. (Obviously you don't get access to any physical disks (unless it's a JBOD configured to do exactly that), but you certainly get access to exactly what the userspace uses, without any risk of confusion, and I think that's what's important here.) I think it is a good idea to use it as the identifier, thomas23.

(Side note: udev uses "device path" to refer to the exact path in /sys hierarchy, whereas "device node" refers to the path in /dev. I'll try to keep to that same naming here.)

If you have a device node, you can stat() (http://man7.org/linux/man-pages/man2/stat.2.html) it (to find out whether it is a block ('b' type) or character ('c' type) device, and the device ID (== packed major and minor numbers)). With those, you can use udev_device_new_from_devnum() (http://www.freedesktop.org/software/systemd/libudev/libudev-udev-device.html#udev-device-new-from-devnum) to get a reference to the actual udev device.

The below example defines udev_device_new_from_filename(), which does exactly that. It's not a perfect name, but I couldn't think of a better one just now. Also, it does require the device node to already exist. It works for all devices udev knows about, not just block devices.



#define _POSIX_C_SOURCE 200809L
#define _BSD_SOURCE
#include <stdlib.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <libudev.h>

/* Save this file as example.c,
* then compile using
* gcc -W -Wall -O3 example.c -ludev -o example
*
* Supply device file names as parameters to get their details.
*/

struct udev_device *udev_device_new_from_filename(struct udev *const udevhandle,
const char *const filename)
{
struct stat info;
struct udev_device *dev;
int saved_errno, result;
char type;
dev_t devnum;

saved_errno = errno;

if (!filename || !*filename) {
errno = EINVAL;
return NULL;
}

/* Obtain the information on the specified filename. */
result = stat(filename, &info);
if (result == -1)
return NULL; /* errno set by stat() call. */

/* Construct udev device id. */
if (S_ISBLK(info.st_mode)) {
/* Block device. */
type = 'b';
devnum = info.st_rdev;
} else
if (S_ISCHR(info.st_mode)) {
/* Character device. */
type = 'c';
devnum = info.st_rdev;
} else {
errno = ENODEV;
return NULL;
}

/* No udev handle specified?
* (Note: this way users can call (NULL, filename) to see
* if a filename might refer to a device.) */
if (!udevhandle) {
errno = EINVAL;
return NULL;
}

/* Obtain udev device handle. */
errno = ENOTSUP;
dev = udev_device_new_from_devnum(udevhandle, type, devnum);
if (!dev)
return NULL; /* Hopefully errno set by the above call. */

/* Success. */
errno = saved_errno;
return dev;
}

long long stoll(const char *const str, const long long invalid)
{
long long result;
int saved_errno;

if (!str || !*str)
return invalid;

saved_errno = errno;
errno = 0;

result = strtoll(str, NULL, 10);
if (errno) {
errno = saved_errno;
return invalid;
}

errno = saved_errno;
return result;
}

int main(int argc, char *argv[])
{
struct udev *udevhandle;
struct udev_device *dev;
const char *s;
long long v;
int arg;

if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s /dev/DEVICE ...\n", argv[0]);
fprintf(stderr, "\n");
return 1;
}

udevhandle = udev_new();

for (arg = 1; arg < argc; arg++) {

dev = udev_device_new_from_filename(udevhandle, argv[arg]);
if (!dev) {
const char *const errmsg = strerror(errno);
fflush(stdout);
fprintf(stderr, "%s: %s.\n", argv[arg], errmsg);
udev_unref(udevhandle);
return 1;
}

printf("%s:\n", argv[arg]);

if ((s = udev_device_get_subsystem(dev)))
printf("\tSubsystem: '%s'\n", s);
if ((s = udev_device_get_property_value(dev, "ID_BUS")))
printf("\tBus: '%s'\n", s);
if ((v = stoll(udev_device_get_sysattr_value(dev, "size"), -2048LL) / 2048LL) >= 0LL)
printf("\tSize: %lld MiB\n", v);
if ((s = udev_device_get_devtype(dev)))
printf("\tType: '%s'\n", s);
if ((s = udev_device_get_sysattr_value(dev, "device/vendor")))
printf("\tVendor: '%s'\n", s);
if ((s = udev_device_get_property_value(dev, "ID_MODEL")))
printf("\tModel: '%s'\n", s);
if ((s = udev_device_get_property_value(dev, "ID_SERIAL")))
printf("\tSerial: '%s'\n", s);
if ((s = udev_device_get_property_value(dev, "ID_SERIAL_SHORT")))
printf("\tShort serial: '%s'\n", s);
if ((s = udev_device_get_property_value(dev, "ID_REVISION")))
printf("\tRevision: '%s'\n", s);

udev_device_unref(dev);
}

udev_unref(udevhandle);

return 0;
}
Hope this helps.

thomas23
02-19-2014, 03:59 PM
thank you. I will have to buy you a beer sometime :)

I wonder though how udev accomplishes this though. I can't seem to find the source for the udev codebase

Nominal Animal
02-20-2014, 01:24 AM
I will have to buy you a beer sometime
Well, if you happen to come to Helsinki, Finland, I'm up for a beer or two, and exchanging some "war stories".. :)


I wonder though how udev accomplishes this though.
The Wikipedia udev (http://en.wikipedia.org/wiki/Udev) article contains a pretty good overview. The kernel provides most of the information in the uevent messages it provides for udev when it finds each device.

For ATA, SCSI, and CD-ROM devices, udev does contain code to probe the block devices deeper, to obtain for example the serial number (which is not included in the data provided by the kernel, since the kernel does not probe for it itself). These implementations are found in src/udev/ata_id/ata_id.c, src/udev/scsi_id/, and src/udev/cdrom_id/cdrom_id.c, respectively, in the systemd (http://www.freedesktop.org/software/systemd/) sources. Of these, you'll likely find the scsi_get_serial() and scsi_inquiry() functions in src/udev/scsi_id/scsi_serial.c most interesting, as it is the implementation of the actual SCSI probing via an SG_IO ioctl().

The information the abovementioned files obtain from the block devices is included in udev properties.

All of the SAN systems I've managed to get my mittens on have been based on SCSI or SATA disks, with controllers that support the above SCSI ioctl() for basic information at least. However, I don't currently manage any Linux HPC clusters, and do not even have any proper SAN hardware to test with, so my knowledge might be a year or two out of date. And besides, I find the simulation development side much more interesting than cluster management.


I can't seem to find the source for the udev codebase
It was merged into systemd (http://www.freedesktop.org/software/systemd/), so grab the latest systemd-###.tar.xz tar archive (systemd-209.tar.xz (http://www.freedesktop.org/software/systemd/systemd-209.tar.xz) as of 2014-02-19), see the src/libudev/ and src/udev/ directories in it.

thomas23
02-23-2014, 02:27 AM
Finland looks nice, but really cold :) especially this time of year

Simulations are cool. I study physics currently which is all about simulating the universe with mathematics.

Anyway it seems that on the OS I use, udev is kind of weird. It does not return an ID_MODEL,ID_SERIAL or ID_SERIAL_SHORT for ANY drive. be it SATA,SAS or even USB. It works perfectly on vms... its some hardware back from work so I can test at home since I usually am only a few hours a week onsite. I will try a few different OS`s and play a little with udevadm monitor and such. I will keep you guys posted I guess.

thomas23
02-23-2014, 04:59 PM
it seems my OS was the one who didn`t provide ID_MODEL,ID_SERIAL or ID_SERIAL_SHORT.

I switched to debian and it is fine...

thanks for the help