forked from jaypipes/ghw
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathblock_linux.go
More file actions
326 lines (296 loc) · 8.34 KB
/
block_linux.go
File metadata and controls
326 lines (296 loc) · 8.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package ghw
import (
"bufio"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
const (
LINUX_SECTOR_SIZE = 512
PathMtab = "/etc/mtab"
PathSysBlock = "/sys/block"
PathDevDiskById = "/dev/disk/by-id"
)
var RegexNVMeDev = regexp.MustCompile(`^nvme\d+n\d+$`)
var RegexNVMePart = regexp.MustCompile(`^(nvme\d+n\d+)p\d+$`)
func blockFillInfo(info *BlockInfo) error {
info.Disks = Disks()
var tpb uint64
for _, d := range info.Disks {
tpb += d.SizeBytes
}
info.TotalPhysicalBytes = tpb
return nil
}
func DiskPhysicalBlockSizeBytes(disk string) uint64 {
// We can find the sector size in Linux by looking at the
// /sys/block/$DEVICE/queue/physical_block_size file in sysfs
path := filepath.Join(PathSysBlock, disk, "queue", "physical_block_size")
contents, err := ioutil.ReadFile(path)
if err != nil {
return 0
}
i, err := strconv.Atoi(strings.TrimSpace(string(contents)))
if err != nil {
return 0
}
return uint64(i)
}
func DiskSizeBytes(disk string) uint64 {
// We can find the number of 512-byte sectors by examining the contents of
// /sys/block/$DEVICE/size and calculate the physical bytes accordingly.
path := filepath.Join(PathSysBlock, disk, "size")
contents, err := ioutil.ReadFile(path)
if err != nil {
return 0
}
i, err := strconv.Atoi(strings.TrimSpace(string(contents)))
if err != nil {
return 0
}
return uint64(i) * LINUX_SECTOR_SIZE
}
func DiskVendor(disk string) string {
// In Linux, the vendor for a disk device is found in the
// /sys/block/$DEVICE/device/vendor file in sysfs
path := filepath.Join(PathSysBlock, disk, "device", "vendor")
contents, err := ioutil.ReadFile(path)
if err != nil {
return "unknown"
}
return strings.TrimSpace(string(contents))
}
func DiskSerialNumber(disk string) string {
// Finding the serial number of a disk without root privileges in Linux is
// a little tricky. The /dev/disk/by-id directory contains a bunch of
// symbolic links to disk devices and partitions. The serial number is
// embedded as part of the symbolic link. For example, on my system, the
// primary SCSI disk (/dev/sda) is represented as a symbolic link named
// /dev/disk/by-id/scsi-3600508e000000000f8253aac9a1abd0c. The serial
// number is 3600508e000000000f8253aac9a1abd0c.
//
// Some SATA drives (or rather, disk drive vendors) use inconsistent ways
// of putting the serial numbers of the disks in this symbolic link name.
// For example, here are two SATA drive identifiers (examples come from
// @antylama on GH Issue #19):
//
// /dev/disk/by-id/ata-AXIOMTEK_Corp.-FSA032G300MW5T-H_BCA11704240020001
//
// in the above identifier, "BCA11704240020001" is the drive serial number.
// The vendor name along with what appears to be a vendor model name
// (FSA032G300MW5T-H) are also included in the symbolic link name.
//
// /dev/disk/by-id/ata-WDC_WD10JFCX-68N6GN0_WD-WX31A76R3KFS
//
// in the above identifier, the serial number of the disk is actually
// WD-WX31A76R3KFS, not WX31A76R3KFS. Go figure...
path := filepath.Join(PathDevDiskById)
links, err := ioutil.ReadDir(path)
if err != nil {
return "unknown"
}
for _, link := range links {
lname := link.Name()
lpath := filepath.Join(PathDevDiskById, lname)
dest, err := os.Readlink(lpath)
if err != nil {
continue
}
dest = filepath.Base(dest)
if dest != disk {
continue
}
pos := strings.LastIndex(lname, "_")
if pos < 0 {
pos = strings.Index(lname, "-")
}
if pos >= 0 {
return lname[pos+1:]
}
}
return "unknown"
}
func DiskPartitions(disk string) []*Partition {
out := make([]*Partition, 0)
path := filepath.Join(PathSysBlock, disk)
files, err := ioutil.ReadDir(path)
if err != nil {
return nil
}
for _, file := range files {
fname := file.Name()
if !strings.HasPrefix(fname, disk) {
continue
}
size := PartitionSizeBytes(fname)
mp, pt, ro := PartitionInfo(fname)
p := &Partition{
Name: fname,
SizeBytes: size,
MountPoint: mp,
Type: pt,
IsReadOnly: ro,
}
out = append(out, p)
}
return out
}
func Disks() []*Disk {
// In Linux, we could use the fdisk, lshw or blockdev commands to list disk
// information, however all of these utilities require root privileges to
// run. We can get all of this information by examining the /sys/block
// and /sys/class/block files
disks := make([]*Disk, 0)
files, err := ioutil.ReadDir(PathSysBlock)
if err != nil {
return nil
}
for _, file := range files {
dname := file.Name()
var busType string
if strings.HasPrefix(dname, "sd") {
busType = "SCSI"
} else if strings.HasPrefix(dname, "hd") {
busType = "IDE"
} else if RegexNVMeDev.MatchString(dname) {
busType = "NVMe"
}
if busType == "" {
continue
}
size := DiskSizeBytes(dname)
pbs := DiskPhysicalBlockSizeBytes(dname)
vendor := DiskVendor(dname)
serialNo := DiskSerialNumber(dname)
d := &Disk{
Name: dname,
SizeBytes: size,
PhysicalBlockSizeBytes: pbs,
BusType: busType,
Vendor: vendor,
SerialNumber: serialNo,
}
parts := DiskPartitions(dname)
// Map this Disk object into the Partition...
for _, part := range parts {
part.Disk = d
}
d.Partitions = parts
disks = append(disks, d)
}
return disks
}
func PartitionSizeBytes(part string) uint64 {
// Allow calling PartitionSize with either the full partition name
// "/dev/sda1" or just "sda1"
if strings.HasPrefix(part, "/dev") {
part = part[4:len(part)]
}
disk := part[0:3]
if m := RegexNVMePart.FindStringSubmatch(part); len(m) > 0 {
disk = m[1]
}
path := filepath.Join(PathSysBlock, disk, part, "size")
contents, err := ioutil.ReadFile(path)
if err != nil {
return 0
}
i, err := strconv.Atoi(strings.TrimSpace(string(contents)))
if err != nil {
return 0
}
return uint64(i) * LINUX_SECTOR_SIZE
}
// Given a full or short partition name, returns the mount point, the type of
// the partition and whether it's readonly
func PartitionInfo(part string) (string, string, bool) {
// Allow calling PartitionInfo with either the full partition name
// "/dev/sda1" or just "sda1"
if !strings.HasPrefix(part, "/dev") {
part = "/dev/" + part
}
// /etc/mtab entries for mounted partitions look like this:
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
var r io.ReadCloser
r, err := os.Open(PathMtab)
if err != nil {
return "", "", true
}
defer r.Close()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
entry := parseMtabEntry(line)
if entry == nil || entry.Partition != part {
continue
}
ro := true
for _, opt := range entry.Options {
if opt == "rw" {
ro = false
break
}
}
return entry.Mountpoint, entry.FilesystemType, ro
}
return "", "", true
}
type mtabEntry struct {
Partition string
Mountpoint string
FilesystemType string
Options []string
}
func parseMtabEntry(line string) *mtabEntry {
// /etc/mtab entries for mounted partitions look like this:
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
if line[0] != '/' {
return nil
}
fields := strings.Fields(line)
if len(fields) < 4 {
return nil
}
// We do some special parsing of the mountpoint, which may contain space,
// tab and newline characters, encoded into the mtab entry line using their
// octal-to-string representations. From the GNU mtab man pages:
//
// "Therefore these characters are encoded in the files and the getmntent
// function takes care of the decoding while reading the entries back in.
// '\040' is used to encode a space character, '\011' to encode a tab
// character, '\012' to encode a newline character, and '\\' to encode a
// backslash."
mp := fields[1]
r := strings.NewReplacer(
"\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\",
)
mp = r.Replace(mp)
res := &mtabEntry{
Partition: fields[0],
Mountpoint: mp,
FilesystemType: fields[2],
}
opts := strings.Split(fields[3], ",")
res.Options = opts
return res
}
func PartitionMountPoint(part string) string {
mp, _, _ := PartitionInfo(part)
return mp
}
func PartitionType(part string) string {
_, pt, _ := PartitionInfo(part)
return pt
}
func PartitionIsReadOnly(part string) bool {
_, _, ro := PartitionInfo(part)
return ro
}