diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..863288e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +lto-info +lto-info.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b2ba4ef --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +EXE = lto-info +MAN = $(EXE).1 + +all: $(EXE) $(MAN) + +clean: + rm -f $(EXE) $(MAN) + +$(EXE): *.go + go fmt *.go + go build -o $(EXE) *.go + +$(MAN): $(EXE) + ./$(EXE) --man > $(MAN) + +.PHONY: run fmt + +run: $(EXE) + ./$(EXE) + +fmt: *.go + go fmt *.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..261718a --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +LTO Info Tool +============= + +This tool reads the internal memory of LTO/Ultrium cartridges from a tape drive + +More precisely, this tool can: +- Read, decode and display factory and usage information stored in the Cartridge Memory (`CM`) aka Medium Auxiliary Memory (`MAM`) +- View and modify the custom `User Medium Text Label` of a cartridge +- Display basic information about the tape drive device + +## How to build and use + +You need to have a go build environment [properly set up](https://golang.org/doc/install), then just type: + +``` +make +``` + +And try the tool by typing: + +``` +./lto-info +``` + +By default, the tool will look for a tape device in `/dev/nst0`, or what is pointed to by the `TAPE` environment variable. To specify another device, use the `-d` option. + +## Output example +``` +Drive information: + Vendor : HP + Model : Ultrium 2-SCSI + Firmware: F63D +Medium information: + Cartridge Type: 0x01 - Cleaning cartridge (50 cycles max) + Medium format : 0x42 - LTO-2 + Formatted as : 0x42 - LTO-2 + Assign. Org. : LTO-FAKE + Manufacturer : FAKMANUF + Serial No : 123456789 + Manuf. Date : 2019-12-31 (roughly 1.3 years ago) + Tape length : 999 meters + Tape width : 11.1 mm + MAM Capacity : 4096 bytes (850 bytes remaining) +Format specs: + Capacity : 200 GB native - 400 GB compressed with a 2:1 ratio + R/W Speed : 40 MB/s native - 80 MB/s compressed + Partitions: 1 max partitions supported + Phy. specs: 4 bands/tape, 16 wraps/band, 8 tracks/wrap, 512 total tracks + Duration : 1h23 to fill tape with 64 end-to-end passes (78 seconds/pass) +Usage information: + Partition space free : 98% (198423/200448 MiB, 193/195 GiB, 0.19/0.19 TiB) + Cartridge load count : 42 + Data written - alltime: 17476 MiB ( 17.07 GiB, 0.02 TiB, 0.09 FVE) + Data read - alltime: 15827 MiB ( 15.46 GiB, 0.02 TiB, 0.08 FVE) + Data written - session: 0 MiB ( 0.00 GiB, 0.00 TiB, 0.00 FVE) + Data read - session: 139 MiB ( 0.14 GiB, 0.00 TiB, 0.00 FVE) +Previous sessions: + Session N-0: Used in a device of vendor FAKEVEND (serial MODEL012345678901234567890123456) + Session N-1: Used in a device of vendor FAKEVEND (serial MODEL12345) + Session N-2: Used in device ACMEINC + Session N-3: Used in a device of vendor FAKEVEND (serial MODEL34567) +``` + +## Build-time dependencies + +- https://github.com/HewlettPackard/structex: encode/decode bitfields in SCSI structs in a readable way +- https://github.com/benmcclelland/mtio: go bindings for `mt` ioctls +- https://github.com/benmcclelland/sgio: go bindings for `sgio` ioctls +- https://github.com/jessevdk/go-flags: command-line options parser + +## Related + +Inspired from other tools written in C: +- https://github.com/arogge/maminfo +- https://github.com/scangeo/lto-cm/ + +Big up to them! diff --git a/cm.go b/cm.go new file mode 100644 index 0000000..b65d0db --- /dev/null +++ b/cm.go @@ -0,0 +1,488 @@ +package main + +// vim: ts=4:sts=4: + +import ( + "fmt" + "strings" + "time" +) + +const ( + TYPE_BINARY = 0x00 + TYPE_ASCII = 0x01 + + READ_ATT_REPLY_LEN = 512 + WRITE_ATT_CMD_LEN = 16 +) + +type CmAttr struct { + IsValid bool + Name string + Command int + Len int + DataType int + DataInt uint64 + DataStr string + NoTrim bool + MockInt uint64 + MockStr string +} + +type Cm struct { + PartCapRemain *CmAttr // + PartCapMax *CmAttr // + TapeAlertFlags *CmAttr + LoadCount *CmAttr // + MAMSpaceRemaining *CmAttr + AssigningOrganization *CmAttr // + FormattedDensityCode *CmAttr // + InitializationCount *CmAttr //err + Identifier *CmAttr //err + VolumeChangeReference *CmAttr //err + + DeviceAtLoadN0 *CmAttr // + DeviceAtLoadN1 *CmAttr // + DeviceAtLoadN2 *CmAttr // + DeviceAtLoadN3 *CmAttr // + TotalWritten *CmAttr // + TotalRead *CmAttr // + TotalWrittenSession *CmAttr // + TotalReadSession *CmAttr // + LogicalPosFirstEncrypted *CmAttr //err + LogicalPosFirstUnencrypted *CmAttr //err + + UsageHistory *CmAttr + PartUsageHistory *CmAttr + + Manufacturer *CmAttr // + SerialNo *CmAttr // + Length *CmAttr // + Width *CmAttr // + AssigningOrg *CmAttr // + MediumDensity *CmAttr // + ManufactureDate *CmAttr // + MAMCapacity *CmAttr // + Type *CmAttr // + TypeInformation *CmAttr // + UserText *CmAttr + DateTimeLastWritten *CmAttr //err + TextLocalizationId *CmAttr //err + Barcode *CmAttr //err + OwningHostTextualName *CmAttr //err + MediaPool *CmAttr //err + ApplicationFormatVersion *CmAttr //err + MediumGloballyUniqId *CmAttr //err + MediaPoolGloballyUniqId *CmAttr //err +} + +type SpecsType struct { + IsValid bool + NativeCap int + CompressedCap int + NativeSpeed int + CompressedSpeed int + FullTapeMinutes int + CompressFactor string + CanWORM bool + CanEncrypt bool + PartitionNumber int + BandsPerTape int + WrapsPerBand int + TracksPerWrap int +} + +func min2human(min int) string { + if min < 60 { + return fmt.Sprintf("%d min", min) + } + return fmt.Sprintf("%dh%d", min/60, min-60*(min/60)) +} + +// https://github.com/hreinecke/sg3_utils/issues/18 +func cmDensityFriendly(d int) (string, SpecsType) { + friendlyName := "Unknown" + var specs SpecsType + switch d { + case 0x40: + friendlyName = "LTO-1" + specs = SpecsType{true, 100, 200, 20, 40, 60 + 23, "2:1", false, false, 1, 4, 12, 8} + case 0x42: + friendlyName = "LTO-2" + specs = SpecsType{true, 200, 400, 40, 80, 60 + 23, "2:1", false, false, 1, 4, 16, 8} + case 0x44: + friendlyName = "LTO-3" + specs = SpecsType{true, 400, 800, 80, 160, 60 + 23, "2:1", true, false, 1, 4, 11, 16} + case 0x46: + friendlyName = "LTO-4" + specs = SpecsType{true, 800, 1600, 120, 240, 60 + 51, "2:1", true, true, 1, 4, 14, 16} + case 0x58: + friendlyName = "LTO-5" + specs = SpecsType{true, 1500, 3000, 140, 280, 60*3 + 10, "2:1", true, true, 2, 4, 20, 16} + case 0x5A: + friendlyName = "LTO-6" + specs = SpecsType{true, 2500, 6250, 160, 400, 60*4 + 20, "2.5:1", true, true, 4, 4, 34, 16} + case 0x5C: + friendlyName = "LTO-7" + specs = SpecsType{true, 6000, 15000, 300, 750, 60*5 + 33, "2.5:1", true, true, 4, 4, 28, 32} + case 0x5D: + friendlyName = "LTO-M8" + specs = SpecsType{true, 9000, 22500, 300, 750, 60*8 + 20, "2.5:1", false, true, 4, 4, 42, 32} + case 0x5E: + friendlyName = "LTO-8" + specs = SpecsType{true, 12000, 30000, 360, 900, 60*9 + 16, "2.5:1", true, true, 4, 4, 52, 32} + case 0x60: /* guessed, to check FIXME */ + friendlyName = "LTO-9" + specs = SpecsType{true, 18000, 45000, 400, 1000, 60*12 + 30, "2.5:1", true, true, 4, 0, 0, 0} /* FIXME */ + } + return friendlyName, specs +} + +func (cm *Cm) String() string { + s := "Medium information:\n" + + if cm.Type.IsValid { + friendlyName := "Unknown" + switch cm.Type.DataInt { + case 0x00: + friendlyName = "Data cartridge" + case 0x01: + friendlyName = "Cleaning cartridge" + if cm.TypeInformation.IsValid { + friendlyName = fmt.Sprintf("%s (%d cycles max)", friendlyName, cm.TypeInformation.DataInt) + } + case 0x80: + friendlyName = "WORM (Write-once) cartridge" + } + s += fmt.Sprintf(" Cartridge Type: 0x%02x - %s\n", cm.Type.DataInt, friendlyName) + } + + var specs SpecsType + specs.IsValid = false + if cm.MediumDensity.IsValid { + var s1 string + s1, specs = cmDensityFriendly(int(cm.MediumDensity.DataInt)) + s += fmt.Sprintf(" Medium format : 0x%02x - %s\n", cm.MediumDensity.DataInt, s1) + s2, _ := cmDensityFriendly(int(cm.FormattedDensityCode.DataInt)) + s += fmt.Sprintf(" Formatted as : 0x%02x - %s\n", cm.FormattedDensityCode.DataInt, s2) + } + if cm.AssigningOrg.IsValid { + s += fmt.Sprintf(" Assign. Org. : %s\n", cm.AssigningOrg.DataStr) + } + if cm.Manufacturer.IsValid { + s += fmt.Sprintf(" Manufacturer : %s\n", cm.Manufacturer.DataStr) + } + if cm.SerialNo.IsValid { + s += fmt.Sprintf(" Serial No : %s\n", cm.SerialNo.DataStr) + } + if cm.ManufactureDate.IsValid { + if len(cm.ManufactureDate.DataStr) == 8 { + // YYYYMMDD + if d, err := time.Parse("20060102", cm.ManufactureDate.DataStr); err == nil { + years := time.Since(d).Hours() / 24.0 / 365.0 + s += fmt.Sprintf(" Manuf. Date : %s-%s-%s (roughly %.1f years ago)\n", cm.ManufactureDate.DataStr[0:4], cm.ManufactureDate.DataStr[4:6], cm.ManufactureDate.DataStr[6:8], years) + } else { + s += fmt.Sprintf(" Manuf. Date : %s-%s-%s\n", cm.ManufactureDate.DataStr[0:4], cm.ManufactureDate.DataStr[4:6], cm.ManufactureDate.DataStr[6:8]) + } + } else { + s += fmt.Sprintf(" Manuf. Date : %s\n", cm.ManufactureDate.DataStr) + } + } + if cm.Length.IsValid { + s += fmt.Sprintf(" Tape length : %d meters\n", cm.Length.DataInt) + } + if cm.Width.IsValid { + s += fmt.Sprintf(" Tape width : %.1f mm\n", float32(cm.Width.DataInt)/10) + } + if cm.MAMCapacity.IsValid { + if cm.MAMSpaceRemaining.IsValid { + s += fmt.Sprintf(" MAM Capacity : %d bytes (%d bytes remaining)\n", cm.MAMCapacity.DataInt, cm.MAMSpaceRemaining.DataInt) + } else { + s += fmt.Sprintf(" MAM Capacity : %d bytes\n", cm.MAMCapacity.DataInt) + } + } + + if specs.IsValid { + s += fmt.Sprintf("Format specs:\n") + s += fmt.Sprintf(" Capacity : %5d GB native - %5d GB compressed with a %s ratio\n", specs.NativeCap, specs.CompressedCap, specs.CompressFactor) + s += fmt.Sprintf(" R/W Speed : %5d MB/s native - %5d MB/s compressed\n", specs.NativeSpeed, specs.CompressedSpeed) + s += fmt.Sprintf(" Partitions: %5d max partitions supported\n", specs.PartitionNumber) + s += fmt.Sprintf(" Phy. specs: %d bands/tape, %d wraps/band, %d tracks/wrap, %d total tracks\n", specs.BandsPerTape, specs.WrapsPerBand, specs.TracksPerWrap, specs.BandsPerTape*specs.WrapsPerBand*specs.TracksPerWrap) + s += fmt.Sprintf(" Duration : %s to fill tape with %d end-to-end passes (%.0f seconds/pass)\n", min2human(specs.FullTapeMinutes), specs.BandsPerTape*specs.WrapsPerBand, float64(specs.FullTapeMinutes)*60.0/float64(specs.BandsPerTape*specs.WrapsPerBand)) + } + + s += fmt.Sprintf("Usage information:\n") + if cm.PartCapRemain.IsValid && cm.PartCapMax.IsValid { + r := cm.PartCapRemain.DataInt + m := cm.PartCapMax.DataInt + if m > 0 { + s += fmt.Sprintf(" Partition space free : %d%% (%d/%d MiB, %d/%d GiB, %.2f/%.2f TiB)\n", 100*r/m, r, m, r/1024, m/1024, float32(r)/1024/1024, float32(m)/1024/1024) + } else { + s += fmt.Sprintf(" Partition space free : ?%% (%d/%d MiB, %d/%d GiB, %.2f/%.2f TiB)\n", r, m, r/1024, m/1024, float32(r)/1024/1024, float32(m)/1024/1024) + } + } + if cm.LoadCount.IsValid { + s += fmt.Sprintf(" Cartridge load count : %d\n", cm.LoadCount.DataInt) + } + if cm.TotalWritten.IsValid && cm.TotalRead.IsValid { + s += fmt.Sprintf(" Data written - alltime: %12d MiB (%9.2f GiB, %6.2f TiB", cm.TotalWritten.DataInt, float64(cm.TotalWritten.DataInt)/1024, float64(cm.TotalWritten.DataInt)/1024/1024) + if cm.PartCapMax.IsValid { + s += fmt.Sprintf(", %.2f FVE", float64(cm.TotalWritten.DataInt)/float64(cm.PartCapMax.DataInt)) + } + s += fmt.Sprintf(")\n") + + s += fmt.Sprintf(" Data read - alltime: %12d MiB (%9.2f GiB, %6.2f TiB", cm.TotalRead.DataInt, float64(cm.TotalRead.DataInt)/1024, float64(cm.TotalRead.DataInt)/1024/1024) + if cm.PartCapMax.IsValid { + s += fmt.Sprintf(", %.2f FVE", float64(cm.TotalRead.DataInt)/float64(cm.PartCapMax.DataInt)) + } + s += fmt.Sprintf(")\n") + } + if cm.TotalWrittenSession.IsValid && cm.TotalReadSession.IsValid { + s += fmt.Sprintf(" Data written - session: %12d MiB (%9.2f GiB, %6.2f TiB", cm.TotalWrittenSession.DataInt, float64(cm.TotalWrittenSession.DataInt)/1024, float64(cm.TotalWrittenSession.DataInt)/1024/1024) + if cm.PartCapMax.IsValid { + s += fmt.Sprintf(", %.2f FVE", float64(cm.TotalWrittenSession.DataInt)/float64(cm.PartCapMax.DataInt)) + } + s += fmt.Sprintf(")\n") + + s += fmt.Sprintf(" Data read - session: %12d MiB (%9.2f GiB, %6.2f TiB", cm.TotalReadSession.DataInt, float64(cm.TotalReadSession.DataInt)/1024, float64(cm.TotalReadSession.DataInt)/1024/1024) + if cm.PartCapMax.IsValid { + s += fmt.Sprintf(", %.2f FVE", float64(cm.TotalReadSession.DataInt)/float64(cm.PartCapMax.DataInt)) + } + s += fmt.Sprintf(")\n") + } + + s += fmt.Sprintf("Previous sessions:\n") + for i, load := range []*CmAttr{cm.DeviceAtLoadN0, cm.DeviceAtLoadN1, cm.DeviceAtLoadN2, cm.DeviceAtLoadN3} { + if load.IsValid { + var devname, serial string + if len(load.DataStr) > 8 { + devname = strings.Trim(load.DataStr[:8], " \u0000") + serial = strings.Trim(load.DataStr[8:], " \u0000") + } else { + devname = strings.Trim(load.DataStr, " \u0000") + } + if serial != "" { + s += fmt.Sprintf(" Session N-%d: Used in a device of vendor %s (serial %s)\n", i, devname, serial) + } else { + s += fmt.Sprintf(" Session N-%d: Used in device %s\n", i, devname) + } + } + } + + //s += fmt.Sprintf("Medium Usage History:\n") + + return s +} + +var attributes []*CmAttr + +func CmAttrNew(name string, command int, length int, datatype int, mock interface{}) *CmAttr { + cmAttr := &CmAttr{ + Name: name, + Command: command, + Len: length, + DataType: datatype, + } + switch mock.(type) { + case string: + cmAttr.MockStr = mock.(string) + default: + cmAttr.MockInt = uint64(mock.(int)) + } + attributes = append(attributes, cmAttr) + return cmAttr +} + +func CmNew() *Cm { + return &Cm{ + PartCapRemain: CmAttrNew( + "Remaining capacity in partition (MiB)", + 0x0000, 8, TYPE_BINARY, 198423, + ), + PartCapMax: CmAttrNew( + "Maximum capacity in partition (MiB)", + 0x0001, 8, TYPE_BINARY, 200448, + ), + TapeAlertFlags: CmAttrNew( + "Tape alert flags", + 0x0002, 8, TYPE_BINARY, 0, + ), + LoadCount: CmAttrNew( + "Load count", + 0x0003, 8, TYPE_BINARY, 42, + ), + MAMSpaceRemaining: CmAttrNew( + "MAM space remaining (bytes)", + 0x0004, 8, TYPE_BINARY, 850, + ), + AssigningOrganization: CmAttrNew( + "Assigning organization", + 0x0005, 8, TYPE_ASCII, "LTO-FAKE", + ), + FormattedDensityCode: CmAttrNew( + "Formatted density code", + 0x0006, 1, TYPE_BINARY, 66, + ), + InitializationCount: CmAttrNew( + "Initialization count", + 0x0007, 2, TYPE_BINARY, "err", + ), + Identifier: CmAttrNew( + "Identifier (deprecated)", + 0x0008, 32, TYPE_ASCII, "err", + ), + VolumeChangeReference: CmAttrNew( + "Volume change reference", + 0x0009, 4, TYPE_BINARY, "err", + ), + DeviceAtLoadN0: CmAttrNew( + "Device Vendor/Serial at current load", + 0x020A, 40, TYPE_ASCII, "FAKEVENDMODEL012345678901234567890123456", + ), + DeviceAtLoadN1: CmAttrNew( + "Device Vendor/Serial at load N-1", + 0x020B, 40, TYPE_ASCII, "FAKEVEND MODEL12345", + ), + DeviceAtLoadN2: CmAttrNew( + "Device Vendor/Serial at load N-2", + 0x020C, 40, TYPE_ASCII, "ACMEINC \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + ), + DeviceAtLoadN3: CmAttrNew( + "Device Vendor/Serial at load N-3", + 0x020D, 40, TYPE_ASCII, "FAKEVEND MODEL34567", + ), + + TotalWritten: CmAttrNew( + "Total MiB written", + 0x0220, 8, TYPE_BINARY, 17476, + ), + TotalRead: CmAttrNew( + "Total MiB read", + 0x0221, 8, TYPE_BINARY, 15827, + ), + TotalWrittenSession: CmAttrNew( + "Total MiB written in current load", + 0x0222, 8, TYPE_BINARY, 0, + ), + TotalReadSession: CmAttrNew( + "Total MiB Read in current load", + 0x0223, 8, TYPE_BINARY, 139, + ), + LogicalPosFirstEncrypted: CmAttrNew( + "Logical pos. of 1st encrypted block", + 0x0224, 8, TYPE_BINARY, "err", + ), + LogicalPosFirstUnencrypted: CmAttrNew( + "Logical pos. of 1st unencrypted block after 1st encrypted block", + 0x0225, 8, TYPE_BINARY, "err", + ), + + UsageHistory: CmAttrNew( + "Medium Usage History", + 0x0340, 90, TYPE_BINARY, "err", + ), + PartUsageHistory: CmAttrNew( + "Partition Usage History", + 0x0341, 90, TYPE_BINARY, "err", + ), + + Manufacturer: CmAttrNew( + "Manufacturer", + 0x0400, 8, TYPE_ASCII, "FAKMANUF", + ), + SerialNo: CmAttrNew( + "Serial No", + 0x0401, 32, TYPE_ASCII, "123456789", + ), + Length: CmAttrNew( + "Tape length", + 0x0402, 4, TYPE_BINARY, 999, + ), + Width: CmAttrNew( + "Tape width", + 0x0403, 4, TYPE_BINARY, 111, + ), + AssigningOrg: CmAttrNew( + "Assigning Organization", + 0x0404, 8, TYPE_ASCII, "LTO-FAKE", + ), + MediumDensity: CmAttrNew( + "Medium density code", + 0x0405, 1, TYPE_BINARY, 0x42, + ), + ManufactureDate: CmAttrNew( + "Manufacture Date", + 0x0406, 8, TYPE_ASCII, "20191231", + ), + MAMCapacity: CmAttrNew( + "MAM Capacity", + 0x0407, 8, TYPE_BINARY, 4096, + ), + Type: CmAttrNew( + "Type", + 0x0408, 1, TYPE_BINARY, 1, + ), + TypeInformation: CmAttrNew( + "Type Information", + 0x0409, 2, TYPE_BINARY, 50, + ), + /* + CmAttr{ + Name: "Application Vendor", + Command: 0x0800, + Len: 8, + DataType: TYPE_ASCII, + }, + CmAttr{ + Name: "Application Name", + Command: 0x0801, + Len: 32, + DataType: TYPE_ASCII, + }, + CmAttr{ + Name: "Application Version", + Command: 0x0802, + Len: 8, + DataType: TYPE_ASCII, + }, + */ + UserText: CmAttrNew( + "User Medium Text Label", + 0x0803, 160, TYPE_ASCII, "User Label", + //NoTrim: tr)e, + ), + + DateTimeLastWritten: CmAttrNew( + "Date and Time Last Written", + 0x0804, 12, TYPE_ASCII, "err", + ), + TextLocalizationId: CmAttrNew( + "Text Localization Identifier", + 0x0805, 1, TYPE_BINARY, "err", + ), + Barcode: CmAttrNew( + "Barcode", + 0x0806, 12, TYPE_ASCII, "err", + ), + OwningHostTextualName: CmAttrNew( + "Owning Host Textual Name", + 0x0807, 80, TYPE_ASCII, "err", + ), + MediaPool: CmAttrNew( + "Media Pool", + 0x0808, 160, TYPE_ASCII, "err", + ), + ApplicationFormatVersion: CmAttrNew( + "Application Format Version", + 0x080B, 16, TYPE_ASCII, "err", + ), + MediumGloballyUniqId: CmAttrNew( + "Medium Globally Unique Identifier", + 0x0820, 36, TYPE_ASCII, "err", + ), + MediaPoolGloballyUniqId: CmAttrNew( + "Media Pool Globally Unique Identifier", + 0x0821, 36, TYPE_ASCII, "err", + ), + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8b939c9 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module speed47.net/cminfo + +go 1.16 + +require ( + github.com/HewlettPackard/structex v1.0.2 + github.com/benmcclelland/mtio v0.0.0-20170506231306-f929531fb4fe + github.com/benmcclelland/sgio v0.0.0-20180629175614-f710aebf64c1 + github.com/jessevdk/go-flags v1.5.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..04420b0 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/HewlettPackard/structex v1.0.2 h1:p2EH/p6zvUd5fSa0onudAUvLWdKsvQSRP0jGKeblA5E= +github.com/HewlettPackard/structex v1.0.2/go.mod h1:3frC4RY/cPsP/4+N8rkxsNAGlQwHV+zDC7qvrN+N+rE= +github.com/benmcclelland/mtio v0.0.0-20170506231306-f929531fb4fe h1:f+PTGRJrCYSquf31olVAWIqyJwx42eBzVH4D3igzgSk= +github.com/benmcclelland/mtio v0.0.0-20170506231306-f929531fb4fe/go.mod h1:XyVqnMjuqI1qOvgei81EgX68tV7BjN9JlluJPsjArs0= +github.com/benmcclelland/sgio v0.0.0-20180629175614-f710aebf64c1 h1:f1AIRyf6d21xBd1DirrIa6fk41O3LB0WvVuVqhPN4co= +github.com/benmcclelland/sgio v0.0.0-20180629175614-f710aebf64c1/go.mod h1:WdrapyVn/Aduwwf/OMW6sEtk9+7BSoMst1kGrx4E4xE= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..44cb303 --- /dev/null +++ b/main.go @@ -0,0 +1,117 @@ +package main + +// vim: ts=4:sts=4 + +import ( + "encoding/json" + "errors" + "fmt" + flags "github.com/jessevdk/go-flags" + "os" + "time" +) + +type OptionsStruct struct { + Device string `short:"f" long:"device" value-name:"DEV" description:"Tape device (default: /dev/nst0, or TAPE envvar)"` + Mock bool `long:"mock" description:"Use a mocked tape drive (for tests only)"` + Debug bool `short:"d" long:"debug" description:"Print debug information"` + Dump string `long:"dump" value-name:"FILE" description:"Dump SCSI raw data to a file"` + + Man bool `hidden:"1" long:"man"` +} + +func main() { + var err error + + options := &OptionsStruct{} + parser := flags.NewParser(options, flags.Default) + _, err = parser.Parse() + if err != nil { + os.Exit(1) + } + + if options.Man { + fmt.Println("man") + parser.WriteManPage(os.Stdout) + return + } + + // err = attrSet0803(dev, "Cartouche de Test YaY! 3 spaces: ") + // fmt.Println("Set:",err) + + var drive *TapeDrive + + syncerr := make(chan error) + go func() { + if options.Mock { + drive, err = TapeDriveNewFake() + } else { + drive, err = TapeDriveNew(options.Device) + } + syncerr <- err + }() + + openerr := errors.New("timeout") + started := time.Now() + waitupto := started.Add(time.Second * 20) + lastprint := started +waitfor: + for waitupto.After(time.Now()) && openerr != nil { + select { + case openerr = <-syncerr: + if openerr != nil { + fmt.Println("Failed") + os.Exit(1) + } else { + // openerr + break waitfor + } + default: + if time.Since(lastprint) > time.Second { + fmt.Printf("Still trying to open the device, aborting in %s...\n", time.Until(waitupto).Round(time.Second)) + lastprint = time.Now() + } + } + time.Sleep(10 * time.Millisecond) + } + if openerr != nil { + fmt.Println("Timed out opening device, is a tape inserted?") + os.Exit(1) + } else { + fmt.Printf("Device %s opened\n", drive.DeviceName) + } + + if options.Dump != "" { + drive.SetDumpFile(options.Dump) + } + + /* + poh := &LogSenseType{0x3C, 0x0008} + err = scsiLogSense(dev, poh) + if err != nil { + fmt.Println("logtest:",err) + / } + */ + + //fmt.Println("Inquiry") + /*err =*/ + drive.ScsiInquiry() + //fmt.Println("Inquiry err:") + //fmt.Println(err) + + drive.GetStatus() + + for i, a := range attributes { + fmt.Printf("\rReading attribute %02d/%02d...", i+1, len(attributes)) + drive.GetAttribute(a) + if options.Debug { + j, _ := json.MarshalIndent(a, "", " ") + fmt.Println(string(j)) + } + } + fmt.Println("") + //fmt.Printf("\r \r") + + //drive.CmList.Print() + fmt.Println(drive) +} diff --git a/tapedrive.go b/tapedrive.go new file mode 100644 index 0000000..d63b504 --- /dev/null +++ b/tapedrive.go @@ -0,0 +1,466 @@ +package main + +// vim: ts=4:sts=4: + +import ( + "bytes" + //"encoding/json" + "errors" + "fmt" + "github.com/HewlettPackard/structex" + "github.com/benmcclelland/mtio" + "github.com/benmcclelland/sgio" + "os" + "strings" +) + +/* +type TapeDriveInterface interface { + Open() error + SetUserLabel(string) error + GetAttribute(*CmAttr) error +} +*/ + +type InquiryInfoType struct { + Vendor string + Model string + Firmware string +} + +type TapeDrive struct { + DeviceName string + Dev *os.File + CmList *Cm + InquiryInfo InquiryInfoType + dumpFd *os.File +} + +func TapeDriveNewDefault() (*TapeDrive, error) { + return TapeDriveNew("") +} + +func TapeDriveNewFake() (*TapeDrive, error) { + return TapeDriveNew("FAKE") +} + +func (drive TapeDrive) IsFake() bool { + return drive.DeviceName == "FAKE" +} + +func TapeDriveNew(devicename string) (*TapeDrive, error) { + if devicename == "" { + if os.Getenv("TAPE") != "" { + devicename = os.Getenv("TAPE") + } else { + devicename = "/dev/nst0" + } + } + + drive := &TapeDrive{DeviceName: devicename, CmList: CmNew()} + if drive.IsFake() { + fmt.Println("Will use a fake tape drive") + return drive, nil + } + + fmt.Printf("Opening device %s\n", devicename) + dev, err := sgio.OpenScsiDevice(devicename) + if err != nil { + fmt.Println("Failed to open:", err) + return nil, err + } + + fmt.Println("Checking whether device is ready") + err = sgio.TestUnitReady(dev) + if err != nil { + fmt.Println("Unit is not ready:", err) + return nil, err + } + + fmt.Println("Unit is ready") + drive.Dev = dev + return drive, nil +} + +func (drive *TapeDrive) GetStatus() error { + // http://manpages.ubuntu.com/manpages/focal/man4/st.4.html + mtget, _ := mtio.GetStatus(drive.Dev) + fmt.Println(mtget) + //blocksz := uint32(mtget.DsReg) & 0x00FFFFFF + //density := (uint32(mtget.DsReg) & 0xFF000000) >> 24 + //fmt.Printf("blocksz=%d density=%x\n", blocksz, density) + //fmt.Println(mtio.GetPos(drive.Dev)) + return nil +} + +func (drive *TapeDrive) SetDumpFile(file string) error { + if drive.dumpFd != nil { + drive.dumpFd.Close() + } + fo, err := os.Create(file) + if err != nil { + panic(err) + } + drive.dumpFd = fo + return nil +} + +func (drive *TapeDrive) SetUserLabel(str string) error { + senseBuf := make([]byte, sgio.SENSE_BUF_LEN) + inqCmdBlk := []uint8{0x8D, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 0, 0} + wrAtt := make([]byte, 169) + wrAtt[0] = 0 + wrAtt[1] = 0 + wrAtt[2] = 0 + wrAtt[3] = 165 + wrAtt[4] = 0x08 + wrAtt[5] = 0x03 + wrAtt[6] = 2 + wrAtt[7] = 0 + wrAtt[8] = 160 + + for i := 0; i < 160; i++ { + if i < len(str) { + wrAtt[9+i] = str[i] + } else { + wrAtt[9+i] = 0 + } + } + + ioHdr := &sgio.SgIoHdr{ + InterfaceID: int32('S'), + CmdLen: uint8(len(inqCmdBlk)), + MxSbLen: sgio.SENSE_BUF_LEN, + DxferDirection: sgio.SG_DXFER_TO_DEV, + DxferLen: uint32(len(wrAtt)), + Dxferp: &wrAtt[0], + Cmdp: &inqCmdBlk[0], + Sbp: &senseBuf[0], + Timeout: sgio.TIMEOUT_20_SECS, + } + + err := sgio.SgioSyscall(drive.Dev, ioHdr) + if err != nil { + return err + } + + err = sgio.CheckSense(ioHdr, &senseBuf) + if err != nil { + return err + } + + return nil +} + +func (drive *TapeDrive) GetAttribute(attr *CmAttr) error { + if drive == nil { + return errors.New("drive is nil") + } + + senseBuf := make([]byte, sgio.SENSE_BUF_LEN) + replyBuf := make([]byte, READ_ATT_REPLY_LEN) + + /* READ ATTRIBUTE (8Ch) + bits: 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 + byte0: --- OPERATION CODE (8Ch) ---- + byte1: reserved | SERVICE ACTION + byte2: obsolete + byte3: obsolete + byte4: obsolete + byte5: LOGICAL VOLUME NUMBER + byte6: reserved + byte7: PARTITION NUMBER + byte8: (MSB) <-- FIRST ATTRIBUTE + byte9: IDENTIFIER --> (LSB) + byte10: (MSB) <-- ALLOCATION + byte11: + byte12: + byte13: LENGTH --> (LSB) + byte14: reserved | CACHE + byte15: CONTROL BYTE (00h) + */ + inqCmdBlk := []uint8{0x8C, 0, 0, 0, 0, 0, 0, 0, 0x04, 0x00, 0, 0, 159, 0, 0, 0} + inqCmdBlk[8] = uint8(0xff & (attr.Command >> 8)) + inqCmdBlk[9] = uint8(0xff & attr.Command) + inqCmdBlk[12] = uint8(0xff & attr.Len) + + ioHdr := &sgio.SgIoHdr{ + InterfaceID: int32('S'), + CmdLen: uint8(len(inqCmdBlk)), + MxSbLen: sgio.SENSE_BUF_LEN, + DxferDirection: sgio.SG_DXFER_FROM_DEV, + DxferLen: READ_ATT_REPLY_LEN, + Dxferp: &replyBuf[0], + Cmdp: &inqCmdBlk[0], + Sbp: &senseBuf[0], + Timeout: sgio.TIMEOUT_20_SECS, + } + + if drive.IsFake() { + if attr.MockStr == "err" { + attr.IsValid = false + return errors.New("mocked error") + } + + if attr.DataType == TYPE_BINARY { + attr.DataInt = attr.MockInt + attr.IsValid = true + return nil + } + + if attr.DataType == TYPE_ASCII { + attr.DataStr = attr.MockStr + attr.IsValid = true + return nil + } + + return errors.New("Invalid type") + } + + attr.IsValid = false + + err := sgio.SgioSyscall(drive.Dev, ioHdr) + if drive.dumpFd != nil { + senserr := sgio.CheckSense(ioHdr, &senseBuf) + senstr := "" + if senserr != nil { + senstr = strings.Replace(senserr.Error(), "\n", " ", -1) + } + drive.dumpFd.Write([]byte(fmt.Sprintf("GetAttribute[%s]:\nsyscallerr: %v\nsenserr: %v\ncommand: 0x%04x\ninqCmdBlk: %v\nsenseBuf: %v\nreplyBuf: %v\n\n", attr.Name, err, senstr, attr.Command, inqCmdBlk, senseBuf, replyBuf))) + } + if err != nil { + return err + } + + err = sgio.CheckSense(ioHdr, &senseBuf) + if err != nil { + return err + } + + if attr.DataType == TYPE_BINARY { + attr.DataInt = 0 + for i := 0; i < attr.Len; i++ { + attr.DataInt *= 256 + attr.DataInt += uint64(replyBuf[9+i]) + } + attr.IsValid = true + return nil + } + + if attr.DataType == TYPE_ASCII { + attr.DataStr = string(replyBuf[9:(9 + attr.Len)]) + if !attr.NoTrim { + attr.DataStr = strings.TrimRight(attr.DataStr, " ") + } + attr.IsValid = true + return nil + } + + return errors.New("Invalid type") +} + +type SCSI_Inquiry_Cmd struct { + OpCode uint8 + EVPD uint8 `bitfield:"1"` + reserved0 uint8 `bitfield:"4,reserved"` + obsolete0 uint8 `bitfield:"3,reserved"` + PageCode uint8 + AllocationLength uint16 + ControlByte uint8 +} + +type SCSI_Drive_Serial_Numbers_Return struct { + PeripheralDeviceType uint8 `bitfield:"5"` // Byte 0 + PeripheralQualifier uint8 `bitfield:"3"` + PageCode uint8 // Byte 1 + reserved0 uint8 `bitfield:"8,reserved"` // Byte 2 + PageLength uint8 + ManufSN [12]byte + ReportedSN [12]byte +} + +type SCSI_Inquiry_Return struct { + PeripheralDeviceType uint8 `bitfield:"5"` // Byte 0 + PeripheralQualifier uint8 `bitfield:"3"` + Reserved0 uint8 + Version uint8 + ReponseDataFormat uint8 `bitfield:"4"` + HiSup uint8 `bitfield:"1"` + NACA uint8 `bitfield:"1"` + Obsolete0 uint8 `bitfield:"1"` + Obsolete1 uint8 `bitfield:"1"` + AdditionalLen uint8 + Protect uint8 `bitfield:"1"` + Reserved1 uint8 `bitfield:"2"` + ThreePC uint8 `bitfield:"1"` + TPGS uint8 `bitfield:"2"` + ACC uint8 `bitfield:"1"` + SCCS uint8 `bitfield:"1"` + Osef0 uint8 + Osef1 uint8 + VendorID [8]byte + ProductID [16]byte + ProductRevision [4]byte // YMDV(F63D), Y=15 M=6 D=3 V=D + Reserved2 uint8 + Obsolete2 uint8 + MaxSpeed uint8 `bitfield:"4"` + ProtocolID uint8 `bitfield:"4"` + FIPS uint8 `bitfield:"2"` + Reserved3 uint8 `bitfield:"5"` + Restricted uint8 `bitfield:"1"` + Reserved4 uint8 + OEMSpecific uint8 + OEMSpecificSubfield uint8 + Reserved5 uint8 + Reserved6 uint32 + PartNumber [8]byte + Reserved7 uint8 + Reserved8 uint8 + Truc1 uint16 + Truc2 uint16 + Truc3 uint16 + Truc4 uint16 + Truc5 uint16 + Truc6 uint16 +} + +func (drive *TapeDrive) ScsiInquiry() error { + senseBuf := make([]byte, sgio.SENSE_BUF_LEN) + replyBuf := make([]byte, 0xFF) + + inqCmdBlk := []uint8{0x12, 0, 0, 0, 0xFF, 0} + + ioHdr := &sgio.SgIoHdr{ + InterfaceID: int32('S'), + CmdLen: uint8(len(inqCmdBlk)), + MxSbLen: sgio.SENSE_BUF_LEN, + DxferDirection: sgio.SG_DXFER_FROM_DEV, + DxferLen: 0xFF, + Dxferp: &replyBuf[0], + Cmdp: &inqCmdBlk[0], + Sbp: &senseBuf[0], + Timeout: sgio.TIMEOUT_20_SECS, + } + + if !drive.IsFake() { + err := sgio.SgioSyscall(drive.Dev, ioHdr) + if drive.dumpFd != nil { + senserr := sgio.CheckSense(ioHdr, &senseBuf) + senstr := "" + if senserr != nil { + senstr = strings.Replace(senserr.Error(), "\n", " ", -1) + } + drive.dumpFd.Write([]byte(fmt.Sprintf("ScsiInquiry:\nsyscallerr: %v\nsenserr: %v\ninqCmdBlk: %v\nsenseBuf: %v\nreplyBuf: %v\n\n", err, senstr, inqCmdBlk, senseBuf, replyBuf))) + } + if err != nil { + return err + } + + err = sgio.CheckSense(ioHdr, &senseBuf) + if err != nil { + return err + } + } else { + replyBuf = []byte{1, 128, 3, 2, 91, 0, 1, 48, 72, 80, 32, 32, 32, 32, 32, 32, 85, 108, 116, 114, 105, 117, 109, 32, 50, 45, 83, 67, 83, 73, 32, 32, 70, 54, 51, 68, 0, 0, 0, 0, 0, 12, 0, 36, 68, 82, 45, 49, 48, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 84, 11, 28, 2, 119, 2, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + } + + //fmt.Println(replyBuf) + + var parsed = new(SCSI_Inquiry_Return) + if err := structex.Decode(bytes.NewReader(replyBuf), parsed); err != nil { + fmt.Println("structex failed:", err) + } + drive.InquiryInfo.Vendor = strings.Trim(string(parsed.VendorID[:]), " \u0000") + drive.InquiryInfo.Model = strings.Trim(string(parsed.ProductID[:]), " \u0000") + drive.InquiryInfo.Firmware = strings.Trim(string(parsed.ProductRevision[:]), " \u0000") + //fmt.Printf("MaxSpeed=%d ProtoID=%d OEMSpec=%d OEMSpecSub=%d PartNu=<%s>\n", parsed.MaxSpeed, parsed.ProtocolID, parsed.OEMSpecific, parsed.OEMSpecificSubfield, parsed.PartNumber) + //fmt.Printf("Truc1=%04x Truc2=%04x Truc3=%04x Truc4=%04x Truc5=%04x Truc6=%04x\n", parsed.Truc1, parsed.Truc2, parsed.Truc3, parsed.Truc4, parsed.Truc5, parsed.Truc6) + + return nil +} + +type LogSenseType struct { + PageCode uint8 + SubPageCode uint8 +} + +func (drive *TapeDrive) scsiLogSense(ls *LogSenseType) error { + senseBuf := make([]byte, sgio.SENSE_BUF_LEN) + replyBuf := make([]byte, READ_ATT_REPLY_LEN) + + /* LOG SENSE (4Dh) + bits: 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 + byte0: --- OPERATION CODE (4Dh) ---- + byte1: reserved |PPC|SP + byte2: PC | PAGE CODE + byte3: SUBPAGE CODE + byte4: reserved + byte5: <-- (MSB) PARAMETER.......... + byte6: ............POINTER (LSB) --> + byte7: <-- (MSB) ALLOCATION......... + byte8: .............LENGTH (LSB) --> + byte9: CONTROL BYTE (00h) + The log values returned are controlled by the Page Control ( PC ) field value as follows: + Value Description + 00b the maximum value for each log entry is returned. + 01b the current values are returned. + 10b the maximum value for each log entry is returned. + 11b the power-on values are returned. + NOTE 10 - For page 2Eh (TapeAlert) only, the PC field is ignored. Current values are always returned. + The Parameter Pointer Control ( PPC ) must be set to 0. Returning changed parameters is not supported. The + Save Page ( SP ) field must be set to 0. Saved pages are not supported. The Parameter Pointer will be 0. + */ + var opcode uint8 = 0x4D + var ppc uint8 = 0 + var sp uint8 = 0 + var pc uint8 = 0b01 + var parameterpointer uint16 = 0 + var alloclen uint16 = 0 + var controlbyte uint8 = 0 + inqCmdBlk := []uint8{ + opcode, + ((ppc & 0b1) << 1) | (sp & 0b1), + ((pc & 0b11) << 6) | (ls.PageCode & 0b111111), + ls.SubPageCode, + 0, + uint8((parameterpointer & 0xFF00) >> 8), + uint8((parameterpointer & 0x00FF) >> 0), + uint8((alloclen & 0xFF00) >> 8), + uint8((alloclen & 0x00FF) >> 0), + controlbyte} + + ioHdr := &sgio.SgIoHdr{ + InterfaceID: int32('S'), + CmdLen: uint8(len(inqCmdBlk)), + MxSbLen: sgio.SENSE_BUF_LEN, + DxferDirection: sgio.SG_DXFER_FROM_DEV, + DxferLen: READ_ATT_REPLY_LEN, + Dxferp: &replyBuf[0], + Cmdp: &inqCmdBlk[0], + Sbp: &senseBuf[0], + Timeout: sgio.TIMEOUT_20_SECS, + } + + err := sgio.SgioSyscall(drive.Dev, ioHdr) + if err != nil { + return err + } + + err = sgio.CheckSense(ioHdr, &senseBuf) + if err != nil { + return err + } + + fmt.Println(replyBuf) + + return nil +} + +func (drive *TapeDrive) String() string { + s := fmt.Sprintf("Drive information:\n") + s += fmt.Sprintf(" Vendor : %s\n", drive.InquiryInfo.Vendor) + s += fmt.Sprintf(" Model : %s\n", drive.InquiryInfo.Model) + s += fmt.Sprintf(" Firmware: %s\n", drive.InquiryInfo.Firmware) + s += drive.CmList.String() + return s +}