├── .github └── workflows │ ├── fmtcheck.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING ├── Dockerfile ├── Makefile ├── README.md ├── SECURITY.md ├── SNAPSHOT.md ├── alias.go ├── cmd ├── ghw-snapshot │ ├── command │ │ ├── create.go │ │ ├── read.go │ │ └── root.go │ └── main.go └── ghwc │ ├── commands │ ├── accelerator.go │ ├── baseboard.go │ ├── bios.go │ ├── block.go │ ├── chassis.go │ ├── cpu.go │ ├── gpu.go │ ├── memory.go │ ├── net.go │ ├── pci.go │ ├── print_util.go │ ├── product.go │ ├── root.go │ ├── topology.go │ └── version.go │ └── main.go ├── doc.go ├── go.mod ├── go.sum ├── hack └── run-against-snapshot.sh ├── host.go ├── host_test.go ├── images └── ghw-gopher.png ├── pkg ├── accelerator │ ├── accelerator.go │ ├── accelerator_linux.go │ ├── accelerator_linux_test.go │ └── accelerator_stub.go ├── baseboard │ ├── baseboard.go │ ├── baseboard_linux.go │ ├── baseboard_stub.go │ └── baseboard_windows.go ├── bios │ ├── bios.go │ ├── bios_linux.go │ ├── bios_stub.go │ └── bios_windows.go ├── block │ ├── block.go │ ├── block_darwin.go │ ├── block_linux.go │ ├── block_linux_test.go │ ├── block_stub.go │ ├── block_test.go │ └── block_windows.go ├── chassis │ ├── chassis.go │ ├── chassis_linux.go │ ├── chassis_stub.go │ └── chassis_windows.go ├── context │ ├── context.go │ └── context_test.go ├── cpu │ ├── cpu.go │ ├── cpu_darwin.go │ ├── cpu_linux.go │ ├── cpu_linux_test.go │ ├── cpu_stub.go │ ├── cpu_test.go │ └── cpu_windows.go ├── gpu │ ├── gpu.go │ ├── gpu_linux.go │ ├── gpu_linux_test.go │ ├── gpu_stub.go │ ├── gpu_test.go │ └── gpu_windows.go ├── linuxdmi │ └── dmi_linux.go ├── linuxpath │ ├── path_linux.go │ └── path_linux_test.go ├── marshal │ └── marshal.go ├── memory │ ├── memory.go │ ├── memory_cache.go │ ├── memory_cache_linux.go │ ├── memory_linux.go │ ├── memory_linux_test.go │ ├── memory_stub.go │ ├── memory_test.go │ └── memory_windows.go ├── net │ ├── net.go │ ├── net_linux.go │ ├── net_linux_test.go │ ├── net_stub.go │ ├── net_test.go │ └── net_windows.go ├── option │ ├── option.go │ └── option_test.go ├── pci │ ├── address │ │ ├── address.go │ │ └── address_test.go │ ├── pci.go │ ├── pci_linux.go │ ├── pci_linux_test.go │ ├── pci_stub.go │ └── pci_test.go ├── product │ ├── product.go │ ├── product_linux.go │ ├── product_stub.go │ └── product_windows.go ├── snapshot │ ├── clonetree.go │ ├── clonetree_block_linux.go │ ├── clonetree_gpu_linux.go │ ├── clonetree_linux.go │ ├── clonetree_linux_test.go │ ├── clonetree_net_linux.go │ ├── clonetree_pci_linux.go │ ├── clonetree_stub.go │ ├── pack.go │ ├── pack_test.go │ ├── testdata.tar.gz │ ├── trace.go │ ├── unpack.go │ └── unpack_test.go ├── topology │ ├── topology.go │ ├── topology_linux.go │ ├── topology_linux_test.go │ ├── topology_stub.go │ ├── topology_test.go │ └── topology_windows.go ├── unitutil │ └── unit.go └── util │ ├── util.go │ └── util_test.go └── testdata ├── samples └── dell-r610-block.json ├── snapshots ├── linux-amd64-61797caf7c0b6bca1725ad7975ed4773.tar.gz ├── linux-amd64-8581cf3a529e5d8b97ea876eade2f60d.tar.gz ├── linux-amd64-accel-nvidia.tar.gz ├── linux-amd64-accel.tar.gz ├── linux-amd64-amd-ryzen-1600.tar.gz ├── linux-amd64-intel-xeon-L5640.tar.gz ├── linux-amd64-offlineCPUs.tar.gz └── linux-arm64-c288e0776090cd558ef793b2a4e61939.tar.gz ├── testdata.go └── usr └── share └── hwdata └── pci.ids /.github/workflows/fmtcheck.yml: -------------------------------------------------------------------------------- 1 | name: fmtcheck 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | fmtcheck: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: harden runner 17 | uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 18 | with: 19 | egress-policy: block 20 | disable-sudo: true 21 | allowed-endpoints: > 22 | github.com:443 23 | api.github.com:443 24 | proxy.github.com:443 25 | proxy.golang.org:443 26 | raw.githubusercontent.com:443 27 | objects.githubusercontent.com:443 28 | proxy.golang.org:443 29 | - name: checkout code 30 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 31 | - name: setup go 32 | uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 33 | with: 34 | go-version: 1.23 35 | - name: check fmt 36 | run: 'bash -c "diff -u <(echo -n) <(gofmt -d .)"' 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: read # needed for only-new-issues option below 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: harden runner 18 | uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 19 | with: 20 | egress-policy: block 21 | disable-sudo: true 22 | allowed-endpoints: > 23 | github.com:443 24 | api.github.com:443 25 | proxy.github.com:443 26 | proxy.golang.org:443 27 | raw.githubusercontent.com:443 28 | objects.githubusercontent.com:443 29 | proxy.golang.org:443 30 | - name: checkout code 31 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 32 | - name: setup go 33 | uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 34 | with: 35 | go-version: 1.23 36 | - name: lint 37 | uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 38 | with: 39 | version: v1.61.0 40 | args: --timeout=5m0s --verbose 41 | only-new-issues: true 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | coverage*.* 3 | *~ 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We welcome any and all contributions to `ghw`! Filing [bug reports][gh-issues], 4 | asking questions and submitting patches are all encouraged. 5 | 6 | [gh-issues]: https://github.com/jaypipes/ghw/issues 7 | 8 | ## Submitting patches via pull requests 9 | 10 | We use GitHub pull requests to review code submissions. 11 | 12 | Consult [GitHub Help][pr-help] for more information on using pull requests. 13 | 14 | [pr-help]: https://help.github.com/articles/about-pull-requests/ 15 | 16 | We ask that contributors submitting a pull request sign their commits and 17 | attest to the Developer Certificate of Origin (DCO). 18 | 19 | ## Developer Certificate of Origin 20 | 21 | The DCO is a lightweight way for contributors to certify that they wrote or 22 | otherwise have the right to submit the code they are contributing to the 23 | project. Here is the [full text of the DCO][dco], reformatted for readability: 24 | 25 | > By making a contribution to this project, I certify that: 26 | > 27 | > a. The contribution was created in whole or in part by me and I have the 28 | > right to submit it under the open source license indicated in the file; or 29 | > 30 | > b. The contribution is based upon previous work that, to the best of my 31 | > knowledge, is covered under an appropriate open source license and I have the 32 | > right under that license to submit that work with modifications, whether 33 | > created in whole or in part by me, under the same open source license (unless 34 | > I am permitted to submit under a different license), as indicated in the 35 | > file; or 36 | > 37 | > c. The contribution was provided directly to me by some other person who 38 | > certified (a), (b) or (c) and I have not modified it. 39 | > 40 | > d. I understand and agree that this project and the contribution are public 41 | > and that a record of the contribution (including all personal information I 42 | > submit with it, including my sign-off) is maintained indefinitely and may be 43 | > redistributed consistent with this project or the open source license(s) 44 | > involved. 45 | 46 | [dco]: https://developercertificate.org/ 47 | 48 | You can sign your commits using `git commit -s` before pushing commits to 49 | Github and creating a pull request. 50 | 51 | ## Community Guidelines 52 | 53 | 1. Be kind. 54 | 2. Seriously, that's it. 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-buster as builder 2 | WORKDIR /go/src/github.com/jaypipes/ghw 3 | 4 | ENV GOPROXY=direct 5 | 6 | # go.mod and go.sum go into their own layers. 7 | COPY go.mod . 8 | COPY go.sum . 9 | 10 | # This ensures `go mod download` happens only when go.mod and go.sum change. 11 | RUN go mod download 12 | 13 | COPY . . 14 | 15 | RUN CGO_ENABLED=0 go build -o ghwc ./cmd/ghwc/ 16 | 17 | FROM alpine:3.7@sha256:8421d9a84432575381bfabd248f1eb56f3aa21d9d7cd2511583c68c9b7511d10 18 | RUN apk add --no-cache ethtool 19 | 20 | WORKDIR /bin 21 | 22 | COPY --from=builder /go/src/github.com/jaypipes/ghw/ghwc /bin 23 | 24 | CMD ghwc 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: vet 3 | go test -v ./... 4 | 5 | .PHONY: fmt 6 | fmt: 7 | @echo "Running gofmt on all sources..." 8 | @gofmt -s -l -w . 9 | 10 | .PHONY: fmtcheck 11 | fmtcheck: 12 | @bash -c "diff -u <(echo -n) <(gofmt -d .)" 13 | 14 | .PHONY: vet 15 | vet: 16 | go vet ./... 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We take security vulnerabilities seriously (and so should you!) 4 | 5 | Our policy on reported vulnerabilities (see below on how to report) is that we will 6 | respond to the reporter of a vulnerability within two (2) business days of receiving 7 | the report and notify the reporter whether and when a remediation will be committed. 8 | 9 | When a remediation for a security vulnerability is committed, we will cut a tagged 10 | release of `ghw` and include in the release notes for that tagged release a description 11 | of the vulnerability and a discussion of how it was remediated, along with a note 12 | urging users to update to that fixed version. 13 | 14 | ## Reporting a Vulnerability 15 | 16 | While `ghw` does have automated Github Dependabot alerts about security vulnerabilities 17 | in `ghw`'s dependencies, there is always a chance that a vulnerability in a dependency 18 | goes undetected by Dependabot. If you are aware of a vulnerability either in `ghw` or 19 | one of its dependencies, please do not hesitate to reach out to `ghw` maintainers via 20 | email or Slack. **Do not discuss vulnerabilities in a public forum**. 21 | 22 | `ghw`'s primary maintainer is Jay Pipes, who can be found on the Kubernetes Slack 23 | community as `@jaypipes` and reached via email at jaypipes at gmail dot com. 24 | -------------------------------------------------------------------------------- /SNAPSHOT.md: -------------------------------------------------------------------------------- 1 | # ghw snapshots 2 | 3 | For ghw, snapshots are partial clones of the `/proc`, `/sys` (et. al.) subtrees copied from arbitrary 4 | machines, which ghw can consume later. "partial" is because the snapshot doesn't need to contain a 5 | complete copy of all the filesystem subtree (that is doable but inpractical). It only needs to contain 6 | the paths ghw cares about. The snapshot concept was introduced [to make ghw easier to test](https://github.com/jaypipes/ghw/issues/66). 7 | 8 | ## Create and consume snapshot 9 | 10 | The recommended way to create snapshots for ghw is to use the `ghw-snapshot` tool. 11 | This tool is maintained by the ghw authors, and snapshots created with this tool are guaranteed to work. 12 | 13 | To consume the ghw snapshots, please check the `README.md` document. 14 | 15 | ## Snapshot design and definitions 16 | 17 | The remainder of this document will describe how a snapshot looks like and provides rationale for all the major design decisions. 18 | Even though this document aims to provide all the necessary information to understand how ghw creates snapshots and what you should 19 | expect, we recommend to check also the [project issues](https://github.com/jaypipes/ghw/issues) and the `git` history to have the full picture. 20 | 21 | ### Scope 22 | 23 | ghw supports snapshots only on linux platforms. This restriction may be lifted in future releases. 24 | Snapshots must be consumable in the following supported ways: 25 | 26 | 1. (way 1) from docker (or podman), mounting them as volumes. See `hack/run-against-snapshot.sh` 27 | 2. (way 2) using the environment variables `GHW_SNAPSHOT_*`. See `README.md` for the full documentation. 28 | 29 | Other combinations are possible, but are unsupported and may stop working any time. 30 | You should depend only on the supported ways to consume snapshots. 31 | 32 | ### Snapshot content constraints 33 | 34 | Stemming from the use cases, the snapshot content must have the following properties: 35 | 36 | 0. (constraint 0) MUST contain the same information as live system (obviously). Whatever you learn from a live system, you MUST be able to learn from a snapshot. 37 | 1. (constraint 1) MUST NOT require any post processing before it is consumable besides, obviously, unpacking the `.tar.gz` on the right directory - and pointing ghw to that directory. 38 | 2. (constraint 2) MUST NOT require any special handling nor special code path in ghw. From ghw perspective running against a live system or against a snapshot should be completely transparent. 39 | 3. (constraint 3) MUST contain only data - no executable code is allowed ever. This makes snapshots trivially safe to share and consume. 40 | 4. (constraint 4) MUST NOT contain any personally-identifiable data. Data gathered into a snapshot is for testing and troubleshooting purposes and should be safe to send to troubleshooters to analyze. 41 | 42 | It must be noted that trivially cloning subtrees from `/proc` and `/sys` and creating a tarball out of them doesn't work 43 | because both pseudo filesystems make use of symlinks, and [docker doesn't really play nice with symlinks](https://github.com/jaypipes/ghw/commit/f8ffd4d24e62eb9017511f072ccf51f13d4a3399). 44 | This conflcits with (way 1) above. 45 | 46 | -------------------------------------------------------------------------------- /cmd/ghw-snapshot/command/create.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package command 8 | 9 | import ( 10 | "crypto/md5" 11 | "fmt" 12 | "io" 13 | "os" 14 | "runtime" 15 | 16 | "github.com/spf13/cobra" 17 | 18 | "github.com/jaypipes/ghw/pkg/snapshot" 19 | ) 20 | 21 | var ( 22 | // output filepath to save snapshot to 23 | outPath string 24 | ) 25 | 26 | var createCmd = &cobra.Command{ 27 | Use: "create", 28 | Short: "Creates a new ghw snapshot", 29 | RunE: doCreate, 30 | } 31 | 32 | // doCreate creates a ghw snapshot 33 | func doCreate(cmd *cobra.Command, args []string) error { 34 | scratchDir, err := os.MkdirTemp("", "ghw-snapshot") 35 | if err != nil { 36 | return err 37 | } 38 | defer os.RemoveAll(scratchDir) 39 | 40 | snapshot.SetTraceFunction(trace) 41 | if err = snapshot.CloneTreeInto(scratchDir); err != nil { 42 | return err 43 | } 44 | 45 | if outPath == "" { 46 | outPath, err = defaultOutPath() 47 | if err != nil { 48 | return err 49 | } 50 | trace("using default output filepath %s\n", outPath) 51 | } 52 | 53 | return snapshot.PackFrom(outPath, scratchDir) 54 | } 55 | 56 | func systemFingerprint() (string, error) { 57 | hn, err := os.Hostname() 58 | if err != nil { 59 | return "unknown", err 60 | } 61 | m := md5.New() 62 | _, err = io.WriteString(m, hn) 63 | if err != nil { 64 | return "unknown", err 65 | } 66 | return fmt.Sprintf("%x", m.Sum(nil)), nil 67 | } 68 | 69 | func defaultOutPath() (string, error) { 70 | fp, err := systemFingerprint() 71 | if err != nil { 72 | return "unknown", err 73 | } 74 | return fmt.Sprintf("%s-%s-%s.tar.gz", runtime.GOOS, runtime.GOARCH, fp), nil 75 | } 76 | 77 | func init() { 78 | createCmd.PersistentFlags().StringVarP( 79 | &outPath, 80 | "out", "o", 81 | outPath, 82 | "Path to place snapshot. Defaults to file in current directory with name $OS-$ARCH-$HASHSYSTEMNAME.tar.gz", 83 | ) 84 | rootCmd.AddCommand(createCmd) 85 | } 86 | -------------------------------------------------------------------------------- /cmd/ghw-snapshot/command/read.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package command 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "os" 13 | 14 | "github.com/spf13/cobra" 15 | 16 | "github.com/jaypipes/ghw" 17 | ghwcontext "github.com/jaypipes/ghw/pkg/context" 18 | ) 19 | 20 | var readCmd = &cobra.Command{ 21 | Use: "read", 22 | Short: "Reads a new ghw snapshot", 23 | RunE: doRead, 24 | } 25 | 26 | // doRead reads a ghw snapshot from the input snapshot path argument 27 | func doRead(cmd *cobra.Command, args []string) error { 28 | if len(args) != 1 { 29 | return errors.New("supply a single argument with the filepath to the snapshot you wish to read") 30 | } 31 | inPath := args[0] 32 | if _, err := os.Stat(inPath); err != nil { 33 | return err 34 | } 35 | os.Setenv("GHW_SNAPSHOT_PATH", inPath) 36 | ctx := ghwcontext.New() 37 | 38 | return ctx.Do(func() error { 39 | info, err := ghw.Host() 40 | fmt.Println(info.String()) 41 | return err 42 | }) 43 | } 44 | 45 | func init() { 46 | rootCmd.AddCommand(readCmd) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/ghw-snapshot/command/root.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package command 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var ( 17 | debug bool 18 | ) 19 | 20 | // rootCmd represents the base command when called without any subcommands 21 | var rootCmd = &cobra.Command{ 22 | Use: "ghw-snapshot", 23 | Short: "ghw-snapshot - create and read ghw snapshots.", 24 | Long: ` 25 | __ __ __ 26 | .-----.| |--.--.--.--.______.-----.-----.---.-.-----.-----.| |--.-----.| |_ 27 | | _ || | | | |______|__ --| | _ | _ |__ --|| | _ || _| 28 | |___ ||__|__|________| |_____|__|__|___._| __|_____||__|__|_____||____| 29 | |_____| |__| 30 | 31 | Create and read ghw snapshots. 32 | 33 | https://github.com/jaypipes/ghw 34 | `, 35 | RunE: doCreate, 36 | } 37 | 38 | // Execute adds all child commands to the root command and sets flags 39 | // appropriately. This is called by main.main(). It only needs to happen once 40 | // to the rootCmd. 41 | func Execute() { 42 | if err := rootCmd.Execute(); err != nil { 43 | fmt.Println(err) 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func trace(msg string, args ...interface{}) { 49 | if !debug { 50 | return 51 | } 52 | fmt.Printf(msg, args...) 53 | } 54 | 55 | func init() { 56 | rootCmd.PersistentFlags().BoolVar( 57 | &debug, "debug", false, "Enable or disable debug mode", 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/ghw-snapshot/main.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | 8 | package main 9 | 10 | import ( 11 | "github.com/jaypipes/ghw/cmd/ghw-snapshot/command" 12 | ) 13 | 14 | func main() { 15 | command.Execute() 16 | } 17 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/accelerator.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // acceleratorCmd represents the install command 18 | var acceleratorCmd = &cobra.Command{ 19 | Use: "accelerator", 20 | Short: "Show processing accelerators information for the host system", 21 | RunE: showGPU, 22 | } 23 | 24 | // showAccelerator show processing accelerators information for the host system. 25 | func showAccelerator(cmd *cobra.Command, args []string) error { 26 | accel, err := ghw.Accelerator() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting Accelerator info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", accel) 34 | 35 | for _, card := range accel.Devices { 36 | fmt.Printf(" %v\n", card) 37 | } 38 | case outputFormatJSON: 39 | fmt.Printf("%s\n", accel.JSONString(pretty)) 40 | case outputFormatYAML: 41 | fmt.Printf("%s", accel.YAMLString()) 42 | } 43 | return nil 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(acceleratorCmd) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/baseboard.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // baseboardCmd represents the install command 18 | var baseboardCmd = &cobra.Command{ 19 | Use: "baseboard", 20 | Short: "Show baseboard information for the host system", 21 | RunE: showBaseboard, 22 | } 23 | 24 | // showBaseboard shows baseboard information for the host system. 25 | func showBaseboard(cmd *cobra.Command, args []string) error { 26 | baseboard, err := ghw.Baseboard() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting baseboard info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", baseboard) 34 | case outputFormatJSON: 35 | fmt.Printf("%s\n", baseboard.JSONString(pretty)) 36 | case outputFormatYAML: 37 | fmt.Printf("%s", baseboard.YAMLString()) 38 | } 39 | return nil 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(baseboardCmd) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/bios.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // biosCmd represents the install command 18 | var biosCmd = &cobra.Command{ 19 | Use: "bios", 20 | Short: "Show BIOS information for the host system", 21 | RunE: showBIOS, 22 | } 23 | 24 | // showBIOS shows BIOS host system. 25 | func showBIOS(cmd *cobra.Command, args []string) error { 26 | bios, err := ghw.BIOS() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting BIOS info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", bios) 34 | case outputFormatJSON: 35 | fmt.Printf("%s\n", bios.JSONString(pretty)) 36 | case outputFormatYAML: 37 | fmt.Printf("%s", bios.YAMLString()) 38 | } 39 | return nil 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(biosCmd) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/block.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // blockCmd represents the install command 18 | var blockCmd = &cobra.Command{ 19 | Use: "block", 20 | Short: "Show block storage information for the host system", 21 | RunE: showBlock, 22 | } 23 | 24 | // showBlock show block storage information for the host system. 25 | func showBlock(cmd *cobra.Command, args []string) error { 26 | block, err := ghw.Block() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting block device info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", block) 34 | 35 | for _, disk := range block.Disks { 36 | fmt.Printf(" %v\n", disk) 37 | for _, part := range disk.Partitions { 38 | fmt.Printf(" %v\n", part) 39 | } 40 | } 41 | case outputFormatJSON: 42 | fmt.Printf("%s\n", block.JSONString(pretty)) 43 | case outputFormatYAML: 44 | fmt.Printf("%s", block.YAMLString()) 45 | } 46 | return nil 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(blockCmd) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/chassis.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // chassisCmd represents the install command 18 | var chassisCmd = &cobra.Command{ 19 | Use: "chassis", 20 | Short: "Show chassis information for the host system", 21 | RunE: showChassis, 22 | } 23 | 24 | // showChassis shows chassis information for the host system. 25 | func showChassis(cmd *cobra.Command, args []string) error { 26 | chassis, err := ghw.Chassis() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting chassis info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", chassis) 34 | case outputFormatJSON: 35 | fmt.Printf("%s\n", chassis.JSONString(pretty)) 36 | case outputFormatYAML: 37 | fmt.Printf("%s", chassis.YAMLString()) 38 | } 39 | return nil 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(chassisCmd) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/cpu.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | "math" 12 | "strings" 13 | 14 | "github.com/jaypipes/ghw" 15 | "github.com/pkg/errors" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | // cpuCmd represents the install command 20 | var cpuCmd = &cobra.Command{ 21 | Use: "cpu", 22 | Short: "Show CPU information for the host system", 23 | RunE: showCPU, 24 | } 25 | 26 | // showCPU show CPU information for the host system. 27 | func showCPU(cmd *cobra.Command, args []string) error { 28 | cpu, err := ghw.CPU() 29 | if err != nil { 30 | return errors.Wrap(err, "error getting CPU info") 31 | } 32 | 33 | switch outputFormat { 34 | case outputFormatHuman: 35 | fmt.Printf("%v\n", cpu) 36 | 37 | for _, proc := range cpu.Processors { 38 | fmt.Printf(" %v\n", proc) 39 | for _, core := range proc.Cores { 40 | fmt.Printf(" %v\n", core) 41 | } 42 | if len(proc.Capabilities) > 0 { 43 | // pretty-print the (large) block of capability strings into rows 44 | // of 6 capability strings 45 | rows := int(math.Ceil(float64(len(proc.Capabilities)) / float64(6))) 46 | for row := 1; row < rows; row = row + 1 { 47 | rowStart := (row * 6) - 1 48 | rowEnd := int(math.Min(float64(rowStart+6), float64(len(proc.Capabilities)))) 49 | rowElems := proc.Capabilities[rowStart:rowEnd] 50 | capStr := strings.Join(rowElems, " ") 51 | if row == 1 { 52 | fmt.Printf(" capabilities: [%s\n", capStr) 53 | } else if rowEnd < len(proc.Capabilities) { 54 | fmt.Printf(" %s\n", capStr) 55 | } else { 56 | fmt.Printf(" %s]\n", capStr) 57 | } 58 | } 59 | } 60 | } 61 | case outputFormatJSON: 62 | fmt.Printf("%s\n", cpu.JSONString(pretty)) 63 | case outputFormatYAML: 64 | fmt.Printf("%s", cpu.YAMLString()) 65 | } 66 | return nil 67 | } 68 | 69 | func init() { 70 | rootCmd.AddCommand(cpuCmd) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/gpu.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // gpuCmd represents the install command 18 | var gpuCmd = &cobra.Command{ 19 | Use: "gpu", 20 | Short: "Show graphics/GPU information for the host system", 21 | RunE: showGPU, 22 | } 23 | 24 | // showGPU show graphics/GPU information for the host system. 25 | func showGPU(cmd *cobra.Command, args []string) error { 26 | gpu, err := ghw.GPU() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting GPU info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", gpu) 34 | 35 | for _, card := range gpu.GraphicsCards { 36 | fmt.Printf(" %v\n", card) 37 | } 38 | case outputFormatJSON: 39 | fmt.Printf("%s\n", gpu.JSONString(pretty)) 40 | case outputFormatYAML: 41 | fmt.Printf("%s", gpu.YAMLString()) 42 | } 43 | return nil 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(gpuCmd) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/memory.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "github.com/jaypipes/ghw" 11 | "github.com/pkg/errors" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // memoryCmd represents the install command 16 | var memoryCmd = &cobra.Command{ 17 | Use: "memory", 18 | Short: "Show memory information for the host system", 19 | RunE: showMemory, 20 | } 21 | 22 | // showMemory show memory information for the host system. 23 | func showMemory(cmd *cobra.Command, args []string) error { 24 | mem, err := ghw.Memory() 25 | if err != nil { 26 | return errors.Wrap(err, "error getting memory info") 27 | } 28 | 29 | printInfo(mem) 30 | return nil 31 | } 32 | 33 | func init() { 34 | rootCmd.AddCommand(memoryCmd) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/net.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // netCmd represents the install command 18 | var netCmd = &cobra.Command{ 19 | Use: "net", 20 | Short: "Show network information for the host system", 21 | RunE: showNetwork, 22 | } 23 | 24 | // showNetwork show network information for the host system. 25 | func showNetwork(cmd *cobra.Command, args []string) error { 26 | net, err := ghw.Network() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting network info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", net) 34 | 35 | for _, nic := range net.NICs { 36 | fmt.Printf(" %v\n", nic) 37 | 38 | enabledCaps := make([]int, 0) 39 | for x, cap := range nic.Capabilities { 40 | if cap.IsEnabled { 41 | enabledCaps = append(enabledCaps, x) 42 | } 43 | } 44 | if len(enabledCaps) > 0 { 45 | fmt.Printf(" enabled capabilities:\n") 46 | for _, x := range enabledCaps { 47 | fmt.Printf(" - %s\n", nic.Capabilities[x].Name) 48 | } 49 | } 50 | } 51 | case outputFormatJSON: 52 | fmt.Printf("%s\n", net.JSONString(pretty)) 53 | case outputFormatYAML: 54 | fmt.Printf("%s", net.YAMLString()) 55 | } 56 | return nil 57 | } 58 | 59 | func init() { 60 | rootCmd.AddCommand(netCmd) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/pci.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "github.com/jaypipes/ghw" 11 | "github.com/pkg/errors" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // pciCmd represents the install command 16 | var pciCmd = &cobra.Command{ 17 | Use: "pci", 18 | Short: "Show information about PCI devices on the host system", 19 | RunE: showPCI, 20 | } 21 | 22 | // showPCI shows information for PCI devices on the host system. 23 | func showPCI(cmd *cobra.Command, args []string) error { 24 | pci, err := ghw.PCI() 25 | if err != nil { 26 | return errors.Wrap(err, "error getting PCI info") 27 | } 28 | 29 | printInfo(pci) 30 | return nil 31 | } 32 | 33 | func init() { 34 | rootCmd.AddCommand(pciCmd) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/print_util.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import "fmt" 10 | 11 | type formattable interface { 12 | String() string 13 | JSONString(bool) string 14 | YAMLString() string 15 | } 16 | 17 | func printInfo(f formattable) { 18 | switch outputFormat { 19 | case outputFormatHuman: 20 | fmt.Printf("%s\n", f) 21 | case outputFormatJSON: 22 | fmt.Printf("%s\n", f.JSONString(pretty)) 23 | case outputFormatYAML: 24 | fmt.Printf("%s", f.YAMLString()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/product.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // productCmd represents the install command 18 | var productCmd = &cobra.Command{ 19 | Use: "product", 20 | Short: "Show product information for the host system", 21 | RunE: showProduct, 22 | } 23 | 24 | // showProduct shows product information for the host system. 25 | func showProduct(cmd *cobra.Command, args []string) error { 26 | product, err := ghw.Product() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting product info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", product) 34 | case outputFormatJSON: 35 | fmt.Printf("%s\n", product.JSONString(pretty)) 36 | case outputFormatYAML: 37 | fmt.Printf("%s", product.YAMLString()) 38 | } 39 | return nil 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(productCmd) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/root.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | "github.com/jaypipes/ghw" 14 | "github.com/pkg/errors" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | const ( 19 | outputFormatHuman = "human" 20 | outputFormatJSON = "json" 21 | outputFormatYAML = "yaml" 22 | usageOutputFormat = `Output format. 23 | Choices are 'json','yaml', and 'human'.` 24 | ) 25 | 26 | var ( 27 | version string 28 | buildHash string 29 | buildDate string 30 | debug bool 31 | outputFormat string 32 | outputFormats = []string{ 33 | outputFormatHuman, 34 | outputFormatJSON, 35 | outputFormatYAML, 36 | } 37 | pretty bool 38 | ) 39 | 40 | // rootCmd represents the base command when called without any subcommands 41 | var rootCmd = &cobra.Command{ 42 | Use: "ghwc", 43 | Short: "ghwc - Discover hardware information.", 44 | Args: validateRootCommand, 45 | Long: ` 46 | __ 47 | .-----. | |--. .--.--.--. 48 | | _ | | | | | | | 49 | |___ | |__|__| |________| 50 | |_____| 51 | 52 | Discover hardware information. 53 | 54 | https://github.com/jaypipes/ghw 55 | `, 56 | RunE: showAll, 57 | } 58 | 59 | func showAll(cmd *cobra.Command, args []string) error { 60 | 61 | switch outputFormat { 62 | case outputFormatHuman: 63 | if err := showBlock(cmd, args); err != nil { 64 | return err 65 | } 66 | if err := showCPU(cmd, args); err != nil { 67 | return err 68 | } 69 | if err := showGPU(cmd, args); err != nil { 70 | return err 71 | } 72 | if err := showMemory(cmd, args); err != nil { 73 | return err 74 | } 75 | if err := showNetwork(cmd, args); err != nil { 76 | return err 77 | } 78 | if err := showTopology(cmd, args); err != nil { 79 | return err 80 | } 81 | if err := showChassis(cmd, args); err != nil { 82 | return err 83 | } 84 | if err := showBIOS(cmd, args); err != nil { 85 | return err 86 | } 87 | if err := showBaseboard(cmd, args); err != nil { 88 | return err 89 | } 90 | if err := showProduct(cmd, args); err != nil { 91 | return err 92 | } 93 | if err := showAccelerator(cmd, args); err != nil { 94 | return err 95 | } 96 | case outputFormatJSON: 97 | host, err := ghw.Host() 98 | if err != nil { 99 | return errors.Wrap(err, "error getting host info") 100 | } 101 | fmt.Printf("%s\n", host.JSONString(pretty)) 102 | case outputFormatYAML: 103 | host, err := ghw.Host() 104 | if err != nil { 105 | return errors.Wrap(err, "error getting host info") 106 | } 107 | fmt.Printf("%s", host.YAMLString()) 108 | } 109 | return nil 110 | } 111 | 112 | // Execute adds all child commands to the root command and sets flags 113 | // appropriately. This is called by main.main(). It only needs to happen once 114 | // to the rootCmd. 115 | func Execute(v string, bh string, bd string) { 116 | version = v 117 | buildHash = bh 118 | buildDate = bd 119 | 120 | if err := rootCmd.Execute(); err != nil { 121 | fmt.Println(err) 122 | os.Exit(1) 123 | } 124 | } 125 | 126 | func haveValidOutputFormat() bool { 127 | for _, choice := range outputFormats { 128 | if choice == outputFormat { 129 | return true 130 | } 131 | } 132 | return false 133 | } 134 | 135 | // validateRootCommand ensures any CLI options or arguments are valid, 136 | // returning an error if not 137 | func validateRootCommand(rootCmd *cobra.Command, args []string) error { 138 | if !haveValidOutputFormat() { 139 | return fmt.Errorf("invalid output format %q", outputFormat) 140 | } 141 | return nil 142 | } 143 | 144 | func init() { 145 | rootCmd.PersistentFlags().BoolVar( 146 | &debug, "debug", false, "Enable or disable debug mode", 147 | ) 148 | rootCmd.PersistentFlags().StringVarP( 149 | &outputFormat, 150 | "format", "f", 151 | outputFormatHuman, 152 | usageOutputFormat, 153 | ) 154 | rootCmd.PersistentFlags().BoolVar( 155 | &pretty, "pretty", false, "When outputting JSON, use indentation", 156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/topology.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw" 13 | "github.com/pkg/errors" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // topologyCmd represents the install command 18 | var topologyCmd = &cobra.Command{ 19 | Use: "topology", 20 | Short: "Show topology information for the host system", 21 | RunE: showTopology, 22 | } 23 | 24 | // showTopology show topology information for the host system. 25 | func showTopology(cmd *cobra.Command, args []string) error { 26 | topology, err := ghw.Topology() 27 | if err != nil { 28 | return errors.Wrap(err, "error getting topology info") 29 | } 30 | 31 | switch outputFormat { 32 | case outputFormatHuman: 33 | fmt.Printf("%v\n", topology) 34 | 35 | for _, node := range topology.Nodes { 36 | fmt.Printf(" %v\n", node) 37 | for _, cache := range node.Caches { 38 | fmt.Printf(" %v\n", cache) 39 | } 40 | fmt.Printf(" %v\n", node.Memory) 41 | fmt.Printf(" distances\n") 42 | for nodeID, dist := range node.Distances { 43 | fmt.Printf(" to node #%d %v\n", nodeID, dist) 44 | } 45 | } 46 | case outputFormatJSON: 47 | fmt.Printf("%s\n", topology.JSONString(pretty)) 48 | case outputFormatYAML: 49 | fmt.Printf("%s", topology.YAMLString()) 50 | } 51 | return nil 52 | } 53 | 54 | func init() { 55 | rootCmd.AddCommand(topologyCmd) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/ghwc/commands/version.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package commands 8 | 9 | import ( 10 | "fmt" 11 | "runtime" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | const debugHeader = ` 17 | Date: %s 18 | Build: %s 19 | Version: %s 20 | Git Hash: %s 21 | ` 22 | 23 | // versionCmd represents the version command 24 | var versionCmd = &cobra.Command{ 25 | Use: "version", 26 | Short: "Display the version of gofile", 27 | Run: func(cmd *cobra.Command, args []string) { 28 | goVersion := fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) 29 | fmt.Printf(debugHeader, buildDate, goVersion, version, buildHash) 30 | }, 31 | } 32 | 33 | func init() { 34 | rootCmd.AddCommand(versionCmd) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/ghwc/main.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "github.com/jaypipes/ghw/cmd/ghwc/commands" 11 | ) 12 | 13 | var ( 14 | // version of application at compile time (-X 'main.version=$(VERSION)'). 15 | version = "(Unknown Version)" 16 | // buildHash GIT hash of application at compile time (-X 'main.buildHash=$(GITCOMMIT)'). 17 | buildHash = "No Git-hash Provided." 18 | // buildDate of application at compile time (-X 'main.buildDate=$(BUILDDATE)'). 19 | buildDate = "No Build Date Provided." 20 | ) 21 | 22 | func main() { 23 | commands.Execute(version, buildHash, buildDate) 24 | } 25 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | /* 8 | package ghw discovers hardware-related information about the host computer, 9 | including CPU, memory, block storage, NUMA topology, network devices, PCI, GPU, 10 | and baseboard/BIOS/chassis/product information. 11 | 12 | Please see the extensive README.md document for examples of usage. 13 | */ 14 | package ghw 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jaypipes/ghw 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/StackExchange/wmi v1.2.1 7 | github.com/jaypipes/pcidb v1.0.1 8 | github.com/pkg/errors v0.9.1 9 | github.com/spf13/cobra v1.8.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | howett.net/plist v1.0.0 12 | ) 13 | 14 | require ( 15 | github.com/go-ole/go-ole v1.2.6 // indirect 16 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 17 | github.com/kr/pretty v0.1.0 // indirect 18 | github.com/mitchellh/go-homedir v1.1.0 // indirect 19 | github.com/spf13/pflag v1.0.5 // indirect 20 | golang.org/x/sys v0.1.0 // indirect 21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 2 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 5 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 6 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 7 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 8 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 9 | github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic= 10 | github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4= 11 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 18 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 22 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 23 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 24 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 25 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 26 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 28 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 31 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 36 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 37 | -------------------------------------------------------------------------------- /hack/run-against-snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CONTAINER_RUNTIME=${CONTAINER_RUNTIME:-docker} 4 | SNAPSHOT_FILEPATH=${SNAPSHOT_FILEPATH:-$1} 5 | 6 | if [[ ! -f $SNAPSHOT_FILEPATH ]]; then 7 | echo "Cannot find snapshot file. Please call $0 with path to snapshot or set SNAPSHOT_FILEPATH envvar." 8 | exit 1 9 | fi 10 | 11 | root_dir=$(cd "$(dirname "$0")/.."; pwd) 12 | ghwc_image_name="ghwc" 13 | local_git_version=$(git describe --tags --always --dirty) 14 | IMAGE_VERSION=${IMAGE_VERSION:-$local_git_version} 15 | 16 | snap_tmp_dir=$(mktemp -d -t ghw-snap-test-XXX) 17 | # needed to enabled PRESERVE and EXCLUSIVE (see README.md) 18 | mkdir -p "$snap_tmp_dir/root" 19 | 20 | echo "copying snapshot $SNAPSHOT_FILEPATH to $snap_tmp_dir ..." 21 | cp -L $SNAPSHOT_FILEPATH $snap_tmp_dir 22 | 23 | echo "building Docker image with ghwc ..." 24 | 25 | ${CONTAINER_RUNTIME} build -f $root_dir/Dockerfile -t $ghwc_image_name:$IMAGE_VERSION $root_dir 26 | 27 | echo "running ghwc Docker image with volume mount to snapshot dir ..." 28 | 29 | # note the trailing ":z" on the "-v" option. We use :z on the host volume mount below to ensure 30 | # the container runtime has the ability to read the contents contained in the host volume. 31 | # podman is especially picky about this. 32 | ${CONTAINER_RUNTIME} run -it -v $snap_tmp_dir:/host:z \ 33 | -e GHW_SNAPSHOT_PATH="/host/$( basename $SNAPSHOT_FILEPATH )" \ 34 | -e GHW_SNAPSHOT_PRESERVE=1 \ 35 | -e GHW_SNAPSHOT_EXCLUSIVE=1 \ 36 | -e GHW_SNAPSHOT_ROOT="/host/root" \ 37 | $ghwc_image_name:$IMAGE_VERSION 38 | -------------------------------------------------------------------------------- /host.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package ghw 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | 14 | "github.com/jaypipes/ghw/pkg/accelerator" 15 | "github.com/jaypipes/ghw/pkg/baseboard" 16 | "github.com/jaypipes/ghw/pkg/bios" 17 | "github.com/jaypipes/ghw/pkg/block" 18 | "github.com/jaypipes/ghw/pkg/chassis" 19 | "github.com/jaypipes/ghw/pkg/cpu" 20 | "github.com/jaypipes/ghw/pkg/gpu" 21 | "github.com/jaypipes/ghw/pkg/marshal" 22 | "github.com/jaypipes/ghw/pkg/memory" 23 | "github.com/jaypipes/ghw/pkg/net" 24 | "github.com/jaypipes/ghw/pkg/pci" 25 | "github.com/jaypipes/ghw/pkg/product" 26 | "github.com/jaypipes/ghw/pkg/topology" 27 | ) 28 | 29 | // HostInfo is a wrapper struct containing information about the host system's 30 | // memory, block storage, CPU, etc 31 | type HostInfo struct { 32 | ctx *context.Context 33 | Memory *memory.Info `json:"memory"` 34 | Block *block.Info `json:"block"` 35 | CPU *cpu.Info `json:"cpu"` 36 | Topology *topology.Info `json:"topology"` 37 | Network *net.Info `json:"network"` 38 | GPU *gpu.Info `json:"gpu"` 39 | Accelerator *accelerator.Info `json:"accelerator"` 40 | Chassis *chassis.Info `json:"chassis"` 41 | BIOS *bios.Info `json:"bios"` 42 | Baseboard *baseboard.Info `json:"baseboard"` 43 | Product *product.Info `json:"product"` 44 | PCI *pci.Info `json:"pci"` 45 | } 46 | 47 | // Host returns a pointer to a HostInfo struct that contains fields with 48 | // information about the host system's CPU, memory, network devices, etc 49 | func Host(opts ...*WithOption) (*HostInfo, error) { 50 | ctx := context.New(opts...) 51 | 52 | memInfo, err := memory.New(opts...) 53 | if err != nil { 54 | return nil, err 55 | } 56 | blockInfo, err := block.New(opts...) 57 | if err != nil { 58 | return nil, err 59 | } 60 | cpuInfo, err := cpu.New(opts...) 61 | if err != nil { 62 | return nil, err 63 | } 64 | topologyInfo, err := topology.New(opts...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | netInfo, err := net.New(opts...) 69 | if err != nil { 70 | return nil, err 71 | } 72 | gpuInfo, err := gpu.New(opts...) 73 | if err != nil { 74 | return nil, err 75 | } 76 | acceleratorInfo, err := accelerator.New(opts...) 77 | if err != nil { 78 | return nil, err 79 | } 80 | chassisInfo, err := chassis.New(opts...) 81 | if err != nil { 82 | return nil, err 83 | } 84 | biosInfo, err := bios.New(opts...) 85 | if err != nil { 86 | return nil, err 87 | } 88 | baseboardInfo, err := baseboard.New(opts...) 89 | if err != nil { 90 | return nil, err 91 | } 92 | productInfo, err := product.New(opts...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | pciInfo, err := pci.New(opts...) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return &HostInfo{ 101 | ctx: ctx, 102 | CPU: cpuInfo, 103 | Memory: memInfo, 104 | Block: blockInfo, 105 | Topology: topologyInfo, 106 | Network: netInfo, 107 | GPU: gpuInfo, 108 | Accelerator: acceleratorInfo, 109 | Chassis: chassisInfo, 110 | BIOS: biosInfo, 111 | Baseboard: baseboardInfo, 112 | Product: productInfo, 113 | PCI: pciInfo, 114 | }, nil 115 | } 116 | 117 | // String returns a newline-separated output of the HostInfo's component 118 | // structs' String-ified output 119 | func (info *HostInfo) String() string { 120 | return fmt.Sprintf( 121 | "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", 122 | info.Block.String(), 123 | info.CPU.String(), 124 | info.GPU.String(), 125 | info.Accelerator.String(), 126 | info.Memory.String(), 127 | info.Network.String(), 128 | info.Topology.String(), 129 | info.Chassis.String(), 130 | info.BIOS.String(), 131 | info.Baseboard.String(), 132 | info.Product.String(), 133 | info.PCI.String(), 134 | ) 135 | } 136 | 137 | // YAMLString returns a string with the host information formatted as YAML 138 | // under a top-level "host:" key 139 | func (i *HostInfo) YAMLString() string { 140 | return marshal.SafeYAML(i.ctx, i) 141 | } 142 | 143 | // JSONString returns a string with the host information formatted as JSON 144 | // under a top-level "host:" key 145 | func (i *HostInfo) JSONString(indent bool) string { 146 | return marshal.SafeJSON(i.ctx, i, indent) 147 | } 148 | -------------------------------------------------------------------------------- /host_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package ghw 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | ) 13 | 14 | // nolint: gocyclo 15 | func TestHost(t *testing.T) { 16 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_HOST"); ok { 17 | t.Skip("Skipping host tests.") 18 | } 19 | 20 | host, err := Host() 21 | 22 | if err != nil { 23 | t.Fatalf("Expected nil error but got %v", err) 24 | } 25 | if host == nil { 26 | t.Fatalf("Expected non-nil host but got nil.") 27 | } 28 | 29 | mem := host.Memory 30 | if mem == nil { 31 | t.Fatalf("Expected non-nil Memory but got nil.") 32 | } 33 | 34 | tpb := mem.TotalPhysicalBytes 35 | if tpb < 1 { 36 | t.Fatalf("Expected >0 total physical memory, but got %d", tpb) 37 | } 38 | 39 | tub := mem.TotalUsableBytes 40 | if tub < 1 { 41 | t.Fatalf("Expected >0 total usable memory, but got %d", tub) 42 | } 43 | 44 | cpu := host.CPU 45 | if cpu == nil { 46 | t.Fatalf("Expected non-nil CPU, but got nil") 47 | } 48 | 49 | cores := cpu.TotalCores 50 | if cores < 1 { 51 | t.Fatalf("Expected >0 total cores, but got %d", cores) 52 | } 53 | 54 | threads := cpu.TotalThreads 55 | if threads < 1 { 56 | t.Fatalf("Expected >0 total threads, but got %d", threads) 57 | } 58 | 59 | block := host.Block 60 | if block == nil { 61 | t.Fatalf("Expected non-nil Block but got nil.") 62 | } 63 | 64 | blockTpb := block.TotalPhysicalBytes 65 | if blockTpb < 1 { 66 | t.Fatalf("Expected >0 total physical block bytes, but got %d", blockTpb) 67 | } 68 | 69 | topology := host.Topology 70 | if topology == nil { 71 | t.Fatalf("Expected non-nil Topology but got nil.") 72 | } 73 | 74 | if len(topology.Nodes) < 1 { 75 | t.Fatalf("Expected >0 nodes , but got %d", len(topology.Nodes)) 76 | } 77 | 78 | gpu := host.GPU 79 | if gpu == nil { 80 | t.Fatalf("Expected non-nil GPU but got nil.") 81 | } 82 | 83 | // Processing accelerator cards are not common nowadays. 84 | // You may not have one in your machine, so this check displays a message but does not interrupt the test. 85 | accel := host.Accelerator 86 | if accel == nil { 87 | t.Logf("WARNING: Processing accelerator cards not detected.") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /images/ghw-gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/images/ghw-gopher.png -------------------------------------------------------------------------------- /pkg/accelerator/accelerator.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package accelerator 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | "github.com/jaypipes/ghw/pkg/marshal" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | "github.com/jaypipes/ghw/pkg/pci" 16 | ) 17 | 18 | type AcceleratorDevice struct { 19 | // the PCI address where the accelerator device can be found 20 | Address string `json:"address"` 21 | // pointer to a PCIDevice struct that describes the vendor and product 22 | // model, etc 23 | PCIDevice *pci.Device `json:"pci_device"` 24 | } 25 | 26 | func (dev *AcceleratorDevice) String() string { 27 | deviceStr := dev.Address 28 | if dev.PCIDevice != nil { 29 | deviceStr = dev.PCIDevice.String() 30 | } 31 | nodeStr := "" 32 | return fmt.Sprintf( 33 | "device %s@%s", 34 | nodeStr, 35 | deviceStr, 36 | ) 37 | } 38 | 39 | type Info struct { 40 | ctx *context.Context 41 | Devices []*AcceleratorDevice `json:"devices"` 42 | } 43 | 44 | // New returns a pointer to an Info struct that contains information about the 45 | // accelerator devices on the host system 46 | func New(opts ...*option.Option) (*Info, error) { 47 | ctx := context.New(opts...) 48 | info := &Info{ctx: ctx} 49 | 50 | if err := ctx.Do(info.load); err != nil { 51 | return nil, err 52 | } 53 | return info, nil 54 | } 55 | 56 | func (i *Info) String() string { 57 | numDevsStr := "devices" 58 | if len(i.Devices) == 1 { 59 | numDevsStr = "device" 60 | } 61 | return fmt.Sprintf( 62 | "processing accelerators (%d %s)", 63 | len(i.Devices), 64 | numDevsStr, 65 | ) 66 | } 67 | 68 | // simple private struct used to encapsulate processing accelerators information in a top-level 69 | // "accelerator" YAML/JSON map/object key 70 | type acceleratorPrinter struct { 71 | Info *Info `json:"accelerator"` 72 | } 73 | 74 | // YAMLString returns a string with the processing accelerators information formatted as YAML 75 | // under a top-level "accelerator:" key 76 | func (i *Info) YAMLString() string { 77 | return marshal.SafeYAML(i.ctx, acceleratorPrinter{i}) 78 | } 79 | 80 | // JSONString returns a string with the processing accelerators information formatted as JSON 81 | // under a top-level "accelerator:" key 82 | func (i *Info) JSONString(indent bool) string { 83 | return marshal.SafeJSON(i.ctx, acceleratorPrinter{i}, indent) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/accelerator/accelerator_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package accelerator 7 | 8 | import ( 9 | "github.com/jaypipes/ghw/pkg/context" 10 | "github.com/jaypipes/ghw/pkg/pci" 11 | ) 12 | 13 | // PCI IDs list available at https://admin.pci-ids.ucw.cz/read/PD 14 | const ( 15 | pciClassProcessingAccelerator = "12" 16 | pciSubclassProcessingAccelerator = "00" 17 | pciClassController = "03" 18 | pciSubclass3DController = "02" 19 | pciSubclassDisplayController = "80" 20 | ) 21 | 22 | var ( 23 | acceleratorPCIClasses = map[string][]string{ 24 | pciClassProcessingAccelerator: []string{ 25 | pciSubclassProcessingAccelerator, 26 | }, 27 | pciClassController: []string{ 28 | pciSubclass3DController, 29 | pciSubclassDisplayController, 30 | }, 31 | } 32 | ) 33 | 34 | func (i *Info) load() error { 35 | accelDevices := make([]*AcceleratorDevice, 0) 36 | 37 | // get PCI devices 38 | pciInfo, err := pci.New(context.WithContext(i.ctx)) 39 | if err != nil { 40 | i.ctx.Warn("error loading PCI information: %s", err) 41 | return nil 42 | } 43 | 44 | // Prepare hardware filter based in the PCI Class + Subclass 45 | isAccelerator := func(dev *pci.Device) bool { 46 | class := dev.Class.ID 47 | subclass := dev.Subclass.ID 48 | if subclasses, ok := acceleratorPCIClasses[class]; ok { 49 | if slicesContains(subclasses, subclass) { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | // This loop iterates over the list of PCI devices and filters them based on discovery criteria 57 | for _, device := range pciInfo.Devices { 58 | if !isAccelerator(device) { 59 | continue 60 | } 61 | accelDev := &AcceleratorDevice{ 62 | Address: device.Address, 63 | PCIDevice: device, 64 | } 65 | accelDevices = append(accelDevices, accelDev) 66 | } 67 | 68 | i.Devices = accelDevices 69 | return nil 70 | } 71 | 72 | // TODO: delete and just use slices.Contains when the minimal golang version we support is 1.21 73 | func slicesContains(s []string, v string) bool { 74 | for i := range s { 75 | if v == s[i] { 76 | return true 77 | } 78 | } 79 | return false 80 | } 81 | -------------------------------------------------------------------------------- /pkg/accelerator/accelerator_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | 6 | package accelerator_test 7 | 8 | import ( 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/accelerator" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | "github.com/jaypipes/ghw/pkg/snapshot" 16 | 17 | "github.com/jaypipes/ghw/testdata" 18 | ) 19 | 20 | func testScenario(t *testing.T, filename string, expectedDevs int) { 21 | testdataPath, err := testdata.SnapshotsDirectory() 22 | if err != nil { 23 | t.Fatalf("Expected nil err, but got %v", err) 24 | } 25 | 26 | t.Setenv("PCIDB_PATH", testdata.PCIDBChroot()) 27 | 28 | workstationSnapshot := filepath.Join(testdataPath, filename) 29 | 30 | tmpRoot, err := os.MkdirTemp("", "ghw-accelerator-testing-*") 31 | if err != nil { 32 | t.Fatalf("Unable to create temporary directory: %v", err) 33 | } 34 | 35 | _, err = snapshot.UnpackInto(workstationSnapshot, tmpRoot, 0) 36 | if err != nil { 37 | t.Fatalf("Unable to unpack %q into %q: %v", workstationSnapshot, tmpRoot, err) 38 | } 39 | 40 | defer func() { 41 | _ = snapshot.Cleanup(tmpRoot) 42 | }() 43 | 44 | info, err := accelerator.New(option.WithChroot(tmpRoot)) 45 | if err != nil { 46 | t.Fatalf("Expected nil err, but got %v", err) 47 | } 48 | if info == nil { 49 | t.Fatalf("Expected non-nil AcceleratorInfo, but got nil") 50 | } 51 | if len(info.Devices) != expectedDevs { 52 | t.Fatalf("Expected %d processing accelerator devices, but found %d.", expectedDevs, len(info.Devices)) 53 | } 54 | } 55 | 56 | func TestAcceleratorDefault(t *testing.T) { 57 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_ACCELERATOR"); ok { 58 | t.Skip("Skipping PCI tests.") 59 | } 60 | 61 | // In this scenario we have 1 processing accelerator device 62 | testScenario(t, "linux-amd64-accel.tar.gz", 1) 63 | 64 | } 65 | 66 | func TestAcceleratorNvidia(t *testing.T) { 67 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_ACCELERATOR"); ok { 68 | t.Skip("Skipping PCI tests.") 69 | } 70 | 71 | // In this scenario we have 1 Nvidia 3D controller device 72 | testScenario(t, "linux-amd64-accel-nvidia.tar.gz", 1) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/accelerator/accelerator_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package accelerator 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("accelerator.Info.load not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/baseboard/baseboard.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package baseboard 8 | 9 | import ( 10 | "github.com/jaypipes/ghw/pkg/context" 11 | "github.com/jaypipes/ghw/pkg/marshal" 12 | "github.com/jaypipes/ghw/pkg/option" 13 | "github.com/jaypipes/ghw/pkg/util" 14 | ) 15 | 16 | // Info defines baseboard release information 17 | type Info struct { 18 | ctx *context.Context 19 | AssetTag string `json:"asset_tag"` 20 | SerialNumber string `json:"serial_number"` 21 | Vendor string `json:"vendor"` 22 | Version string `json:"version"` 23 | Product string `json:"product"` 24 | } 25 | 26 | func (i *Info) String() string { 27 | vendorStr := "" 28 | if i.Vendor != "" { 29 | vendorStr = " vendor=" + i.Vendor 30 | } 31 | serialStr := "" 32 | if i.SerialNumber != "" && i.SerialNumber != util.UNKNOWN { 33 | serialStr = " serial=" + i.SerialNumber 34 | } 35 | versionStr := "" 36 | if i.Version != "" { 37 | versionStr = " version=" + i.Version 38 | } 39 | 40 | productStr := "" 41 | if i.Product != "" { 42 | productStr = " product=" + i.Product 43 | } 44 | 45 | return "baseboard" + util.ConcatStrings( 46 | vendorStr, 47 | serialStr, 48 | versionStr, 49 | productStr, 50 | ) 51 | } 52 | 53 | // New returns a pointer to an Info struct containing information about the 54 | // host's baseboard 55 | func New(opts ...*option.Option) (*Info, error) { 56 | ctx := context.New(opts...) 57 | info := &Info{ctx: ctx} 58 | if err := ctx.Do(info.load); err != nil { 59 | return nil, err 60 | } 61 | return info, nil 62 | } 63 | 64 | // simple private struct used to encapsulate baseboard information in a top-level 65 | // "baseboard" YAML/JSON map/object key 66 | type baseboardPrinter struct { 67 | Info *Info `json:"baseboard"` 68 | } 69 | 70 | // YAMLString returns a string with the baseboard information formatted as YAML 71 | // under a top-level "dmi:" key 72 | func (info *Info) YAMLString() string { 73 | return marshal.SafeYAML(info.ctx, baseboardPrinter{info}) 74 | } 75 | 76 | // JSONString returns a string with the baseboard information formatted as JSON 77 | // under a top-level "baseboard:" key 78 | func (info *Info) JSONString(indent bool) string { 79 | return marshal.SafeJSON(info.ctx, baseboardPrinter{info}, indent) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/baseboard/baseboard_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package baseboard 7 | 8 | import ( 9 | "github.com/jaypipes/ghw/pkg/linuxdmi" 10 | ) 11 | 12 | func (i *Info) load() error { 13 | i.AssetTag = linuxdmi.Item(i.ctx, "board_asset_tag") 14 | i.SerialNumber = linuxdmi.Item(i.ctx, "board_serial") 15 | i.Vendor = linuxdmi.Item(i.ctx, "board_vendor") 16 | i.Version = linuxdmi.Item(i.ctx, "board_version") 17 | i.Product = linuxdmi.Item(i.ctx, "board_name") 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/baseboard/baseboard_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package baseboard 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("baseboardFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/baseboard/baseboard_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package baseboard 7 | 8 | import ( 9 | "github.com/StackExchange/wmi" 10 | ) 11 | 12 | const wqlBaseboard = "SELECT Manufacturer, SerialNumber, Tag, Version, Product FROM Win32_BaseBoard" 13 | 14 | type win32Baseboard struct { 15 | Manufacturer *string 16 | SerialNumber *string 17 | Tag *string 18 | Version *string 19 | Product *string 20 | } 21 | 22 | func (i *Info) load() error { 23 | // Getting data from WMI 24 | var win32BaseboardDescriptions []win32Baseboard 25 | if err := wmi.Query(wqlBaseboard, &win32BaseboardDescriptions); err != nil { 26 | return err 27 | } 28 | if len(win32BaseboardDescriptions) > 0 { 29 | i.AssetTag = *win32BaseboardDescriptions[0].Tag 30 | i.SerialNumber = *win32BaseboardDescriptions[0].SerialNumber 31 | i.Vendor = *win32BaseboardDescriptions[0].Manufacturer 32 | i.Version = *win32BaseboardDescriptions[0].Version 33 | i.Product = *win32BaseboardDescriptions[0].Product 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/bios/bios.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package bios 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | "github.com/jaypipes/ghw/pkg/marshal" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | "github.com/jaypipes/ghw/pkg/util" 16 | ) 17 | 18 | // Info defines BIOS release information 19 | type Info struct { 20 | ctx *context.Context 21 | Vendor string `json:"vendor"` 22 | Version string `json:"version"` 23 | Date string `json:"date"` 24 | } 25 | 26 | func (i *Info) String() string { 27 | 28 | vendorStr := "" 29 | if i.Vendor != "" { 30 | vendorStr = " vendor=" + i.Vendor 31 | } 32 | versionStr := "" 33 | if i.Version != "" { 34 | versionStr = " version=" + i.Version 35 | } 36 | dateStr := "" 37 | if i.Date != "" && i.Date != util.UNKNOWN { 38 | dateStr = " date=" + i.Date 39 | } 40 | 41 | res := fmt.Sprintf( 42 | "bios%s%s%s", 43 | vendorStr, 44 | versionStr, 45 | dateStr, 46 | ) 47 | return res 48 | } 49 | 50 | // New returns a pointer to a Info struct containing information 51 | // about the host's BIOS 52 | func New(opts ...*option.Option) (*Info, error) { 53 | ctx := context.New(opts...) 54 | info := &Info{ctx: ctx} 55 | if err := ctx.Do(info.load); err != nil { 56 | return nil, err 57 | } 58 | return info, nil 59 | } 60 | 61 | // simple private struct used to encapsulate BIOS information in a top-level 62 | // "bios" YAML/JSON map/object key 63 | type biosPrinter struct { 64 | Info *Info `json:"bios"` 65 | } 66 | 67 | // YAMLString returns a string with the BIOS information formatted as YAML 68 | // under a top-level "dmi:" key 69 | func (info *Info) YAMLString() string { 70 | return marshal.SafeYAML(info.ctx, biosPrinter{info}) 71 | } 72 | 73 | // JSONString returns a string with the BIOS information formatted as JSON 74 | // under a top-level "bios:" key 75 | func (info *Info) JSONString(indent bool) string { 76 | return marshal.SafeJSON(info.ctx, biosPrinter{info}, indent) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/bios/bios_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package bios 7 | 8 | import "github.com/jaypipes/ghw/pkg/linuxdmi" 9 | 10 | func (i *Info) load() error { 11 | i.Vendor = linuxdmi.Item(i.ctx, "bios_vendor") 12 | i.Version = linuxdmi.Item(i.ctx, "bios_version") 13 | i.Date = linuxdmi.Item(i.ctx, "bios_date") 14 | 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/bios/bios_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package bios 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("biosFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/bios/bios_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package bios 7 | 8 | import ( 9 | "github.com/StackExchange/wmi" 10 | ) 11 | 12 | const wqlBIOS = "SELECT InstallDate, Manufacturer, Version FROM CIM_BIOSElement" 13 | 14 | type win32BIOS struct { 15 | InstallDate *string 16 | Manufacturer *string 17 | Version *string 18 | } 19 | 20 | func (i *Info) load() error { 21 | // Getting data from WMI 22 | var win32BIOSDescriptions []win32BIOS 23 | if err := wmi.Query(wqlBIOS, &win32BIOSDescriptions); err != nil { 24 | return err 25 | } 26 | if len(win32BIOSDescriptions) > 0 { 27 | i.Vendor = *win32BIOSDescriptions[0].Manufacturer 28 | i.Version = *win32BIOSDescriptions[0].Version 29 | i.Date = *win32BIOSDescriptions[0].InstallDate 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/block/block_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin && !windows 2 | // +build !linux,!darwin,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package block 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("blockFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/block/block_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package block_test 8 | 9 | import ( 10 | "encoding/json" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/jaypipes/ghw/pkg/block" 17 | 18 | "github.com/jaypipes/ghw/testdata" 19 | ) 20 | 21 | // nolint: gocyclo 22 | func TestBlock(t *testing.T) { 23 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_BLOCK"); ok { 24 | t.Skip("Skipping block tests.") 25 | } 26 | 27 | info, err := block.New() 28 | 29 | if err != nil { 30 | t.Fatalf("Expected no error creating block.Info, but got %v", err) 31 | } 32 | tpb := info.TotalPhysicalBytes 33 | 34 | if tpb < 1 { 35 | t.Fatalf("Expected >0 total physical bytes, got %d", tpb) 36 | } 37 | 38 | disks := info.Disks 39 | if len(disks) == 0 { 40 | t.Fatalf("Expected >0 disks. Got %d", len(disks)) 41 | } 42 | 43 | var d0 *block.Disk 44 | // Skip loop devices on generic tests as we don't know what the underlying system is going to have 45 | // And loop devices don't have Serial Numbers for example. 46 | for _, d := range disks { 47 | if d.StorageController != block.STORAGE_CONTROLLER_LOOP { 48 | d0 = d 49 | break 50 | } 51 | } 52 | if d0.Name == "" { 53 | t.Fatalf("Expected disk name, but got \"\"") 54 | } 55 | if d0.SizeBytes <= 0 { 56 | t.Fatalf("Expected >0 disk size, but got %d", d0.SizeBytes) 57 | } 58 | if d0.Partitions == nil { 59 | t.Fatalf("Expected non-nil partitions, but got nil.") 60 | } 61 | if d0.PhysicalBlockSizeBytes <= 0 { 62 | t.Fatalf("Expected >0 sector size, but got %d", d0.PhysicalBlockSizeBytes) 63 | } 64 | 65 | if len(d0.Partitions) > 0 { 66 | p0 := d0.Partitions[0] 67 | if p0 == nil { 68 | t.Fatalf("Expected non-nil partition, but got nil.") 69 | } 70 | if !strings.HasPrefix(p0.Name, d0.Name) { 71 | t.Fatalf("Expected partition name to begin with disk name but "+ 72 | "got %s does not begin with %s", p0.Name, d0.Name) 73 | } 74 | } 75 | 76 | for _, p := range d0.Partitions { 77 | if p.SizeBytes <= 0 { 78 | t.Fatalf("Expected >0 partition size, but got %d", p.SizeBytes) 79 | } 80 | if p.Disk != d0 { 81 | t.Fatalf("Expected disk to be the same as d0 but got %v", p.Disk) 82 | } 83 | } 84 | } 85 | 86 | func TestBlockMarshalUnmarshal(t *testing.T) { 87 | blocks, err := block.New() 88 | if err != nil { 89 | t.Fatalf("Expected no error creating block.Info, but got %v", err) 90 | } 91 | 92 | data, err := json.Marshal(blocks) 93 | if err != nil { 94 | t.Fatalf("Expected no error marshaling block.Info, but got %v", err) 95 | } 96 | 97 | var bi *block.Info 98 | err = json.Unmarshal(data, &bi) 99 | if err != nil { 100 | t.Fatalf("Expected no error unmarshaling block.Info, but got %v", err) 101 | } 102 | } 103 | 104 | type blockData struct { 105 | Block block.Info `json:"block"` 106 | } 107 | 108 | func TestBlockUnmarshal(t *testing.T) { 109 | testdataPath, err := testdata.SamplesDirectory() 110 | if err != nil { 111 | t.Fatalf("Expected nil err when detecting the samples directory, but got %v", err) 112 | } 113 | 114 | data, err := os.ReadFile(filepath.Join(testdataPath, "dell-r610-block.json")) 115 | if err != nil { 116 | t.Fatalf("Expected nil err when reading the sample data, but got %v", err) 117 | } 118 | 119 | var bd blockData 120 | err = json.Unmarshal(data, &bd) 121 | if err != nil { 122 | t.Fatalf("Expected no error unmarshaling block.Info, but got %v", err) 123 | } 124 | 125 | // to learn why we check these values, please review the "dell-r610-block.json" sample 126 | sda := findDiskByName(bd.Block.Disks, "sda") 127 | if sda == nil { 128 | t.Fatalf("unexpected error: can't find 'sda' in the test data") 129 | } 130 | if sda.DriveType != block.DRIVE_TYPE_HDD || sda.StorageController != block.STORAGE_CONTROLLER_SCSI { 131 | t.Fatalf("inconsistent data for sda: %s", sda) 132 | } 133 | 134 | zram0 := findDiskByName(bd.Block.Disks, "zram0") 135 | if zram0 == nil { 136 | t.Fatalf("unexpected error: can't find 'zram0' in the test data") 137 | } 138 | if zram0.DriveType != block.DRIVE_TYPE_SSD { 139 | t.Fatalf("inconsistent data for zram0: %s", zram0) 140 | } 141 | } 142 | 143 | func findDiskByName(disks []*block.Disk, name string) *block.Disk { 144 | for _, disk := range disks { 145 | if disk.Name == name { 146 | return disk 147 | } 148 | } 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /pkg/chassis/chassis.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package chassis 8 | 9 | import ( 10 | "github.com/jaypipes/ghw/pkg/context" 11 | "github.com/jaypipes/ghw/pkg/marshal" 12 | "github.com/jaypipes/ghw/pkg/option" 13 | "github.com/jaypipes/ghw/pkg/util" 14 | ) 15 | 16 | var ( 17 | chassisTypeDescriptions = map[string]string{ 18 | "1": "Other", 19 | "2": "Unknown", 20 | "3": "Desktop", 21 | "4": "Low profile desktop", 22 | "5": "Pizza box", 23 | "6": "Mini tower", 24 | "7": "Tower", 25 | "8": "Portable", 26 | "9": "Laptop", 27 | "10": "Notebook", 28 | "11": "Hand held", 29 | "12": "Docking station", 30 | "13": "All in one", 31 | "14": "Sub notebook", 32 | "15": "Space-saving", 33 | "16": "Lunch box", 34 | "17": "Main server chassis", 35 | "18": "Expansion chassis", 36 | "19": "SubChassis", 37 | "20": "Bus Expansion chassis", 38 | "21": "Peripheral chassis", 39 | "22": "RAID chassis", 40 | "23": "Rack mount chassis", 41 | "24": "Sealed-case PC", 42 | "25": "Multi-system chassis", 43 | "26": "Compact PCI", 44 | "27": "Advanced TCA", 45 | "28": "Blade", 46 | "29": "Blade enclosure", 47 | "30": "Tablet", 48 | "31": "Convertible", 49 | "32": "Detachable", 50 | "33": "IoT gateway", 51 | "34": "Embedded PC", 52 | "35": "Mini PC", 53 | "36": "Stick PC", 54 | } 55 | ) 56 | 57 | // Info defines chassis release information 58 | type Info struct { 59 | ctx *context.Context 60 | AssetTag string `json:"asset_tag"` 61 | SerialNumber string `json:"serial_number"` 62 | Type string `json:"type"` 63 | TypeDescription string `json:"type_description"` 64 | Vendor string `json:"vendor"` 65 | Version string `json:"version"` 66 | } 67 | 68 | func (i *Info) String() string { 69 | vendorStr := "" 70 | if i.Vendor != "" { 71 | vendorStr = " vendor=" + i.Vendor 72 | } 73 | serialStr := "" 74 | if i.SerialNumber != "" && i.SerialNumber != util.UNKNOWN { 75 | serialStr = " serial=" + i.SerialNumber 76 | } 77 | versionStr := "" 78 | if i.Version != "" { 79 | versionStr = " version=" + i.Version 80 | } 81 | 82 | return "chassis type=" + util.ConcatStrings( 83 | i.TypeDescription, 84 | vendorStr, 85 | serialStr, 86 | versionStr, 87 | ) 88 | } 89 | 90 | // New returns a pointer to a Info struct containing information 91 | // about the host's chassis 92 | func New(opts ...*option.Option) (*Info, error) { 93 | ctx := context.New(opts...) 94 | info := &Info{ctx: ctx} 95 | if err := ctx.Do(info.load); err != nil { 96 | return nil, err 97 | } 98 | return info, nil 99 | } 100 | 101 | // simple private struct used to encapsulate chassis information in a top-level 102 | // "chassis" YAML/JSON map/object key 103 | type chassisPrinter struct { 104 | Info *Info `json:"chassis"` 105 | } 106 | 107 | // YAMLString returns a string with the chassis information formatted as YAML 108 | // under a top-level "dmi:" key 109 | func (info *Info) YAMLString() string { 110 | return marshal.SafeYAML(info.ctx, chassisPrinter{info}) 111 | } 112 | 113 | // JSONString returns a string with the chassis information formatted as JSON 114 | // under a top-level "chassis:" key 115 | func (info *Info) JSONString(indent bool) string { 116 | return marshal.SafeJSON(info.ctx, chassisPrinter{info}, indent) 117 | } 118 | -------------------------------------------------------------------------------- /pkg/chassis/chassis_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package chassis 7 | 8 | import ( 9 | "github.com/jaypipes/ghw/pkg/linuxdmi" 10 | "github.com/jaypipes/ghw/pkg/util" 11 | ) 12 | 13 | func (i *Info) load() error { 14 | i.AssetTag = linuxdmi.Item(i.ctx, "chassis_asset_tag") 15 | i.SerialNumber = linuxdmi.Item(i.ctx, "chassis_serial") 16 | i.Type = linuxdmi.Item(i.ctx, "chassis_type") 17 | typeDesc, found := chassisTypeDescriptions[i.Type] 18 | if !found { 19 | typeDesc = util.UNKNOWN 20 | } 21 | i.TypeDescription = typeDesc 22 | i.Vendor = linuxdmi.Item(i.ctx, "chassis_vendor") 23 | i.Version = linuxdmi.Item(i.ctx, "chassis_version") 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/chassis/chassis_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package chassis 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("chassisFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/chassis/chassis_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package chassis 7 | 8 | import ( 9 | "github.com/StackExchange/wmi" 10 | 11 | "github.com/jaypipes/ghw/pkg/util" 12 | ) 13 | 14 | const wqlChassis = "SELECT Caption, Description, Name, Manufacturer, Model, SerialNumber, Tag, TypeDescriptions, Version FROM CIM_Chassis" 15 | 16 | type win32Chassis struct { 17 | Caption *string 18 | Description *string 19 | Name *string 20 | Manufacturer *string 21 | Model *string 22 | SerialNumber *string 23 | Tag *string 24 | TypeDescriptions []string 25 | Version *string 26 | } 27 | 28 | func (i *Info) load() error { 29 | // Getting data from WMI 30 | var win32ChassisDescriptions []win32Chassis 31 | if err := wmi.Query(wqlChassis, &win32ChassisDescriptions); err != nil { 32 | return err 33 | } 34 | if len(win32ChassisDescriptions) > 0 { 35 | i.AssetTag = *win32ChassisDescriptions[0].Tag 36 | i.SerialNumber = *win32ChassisDescriptions[0].SerialNumber 37 | i.Type = util.UNKNOWN // TODO: 38 | i.TypeDescription = *win32ChassisDescriptions[0].Model 39 | i.Vendor = *win32ChassisDescriptions[0].Manufacturer 40 | i.Version = *win32ChassisDescriptions[0].Version 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/context/context_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package context_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/context" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | ) 16 | 17 | const ( 18 | testDataSnapshot = "../snapshot/testdata.tar.gz" 19 | ) 20 | 21 | // nolint: gocyclo 22 | func TestSnapshotContext(t *testing.T) { 23 | ctx := context.New(option.WithSnapshot(option.SnapshotOptions{ 24 | Path: testDataSnapshot, 25 | })) 26 | 27 | var uncompressedDir string 28 | err := ctx.Do(func() error { 29 | uncompressedDir = ctx.Chroot 30 | return nil 31 | }) 32 | 33 | if uncompressedDir == "" { 34 | t.Fatalf("Expected the uncompressed dir path to not be empty") 35 | } 36 | if err != nil { 37 | t.Fatalf("Expected nil err, but got %v", err) 38 | } 39 | if _, err = os.Stat(uncompressedDir); !os.IsNotExist(err) { 40 | t.Fatalf("Expected the uncompressed dir to be deleted: %s", uncompressedDir) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/cpu/cpu_darwin.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "os/exec" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | hasARMArchitecture = false // determine if ARM 13 | sysctlOutput = make(map[string]string) // store all the sysctl output 14 | ) 15 | 16 | func (i *Info) load() error { 17 | err := populateSysctlOutput() 18 | if err != nil { 19 | return errors.Wrap(err, "unable to populate sysctl map") 20 | } 21 | 22 | i.TotalCores = getTotalCores() 23 | i.TotalThreads = getTotalThreads() 24 | i.Processors = getProcessors() 25 | 26 | return nil 27 | } 28 | 29 | // getProcessors some more info https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_system_capabilities 30 | func getProcessors() []*Processor { 31 | p := make([]*Processor, getProcTopCount()) 32 | for i, _ := range p { 33 | p[i] = new(Processor) 34 | p[i].Vendor = sysctlOutput[fmt.Sprintf("hw.perflevel%s.name", strconv.Itoa(i))] 35 | p[i].Model = getVendor() 36 | p[i].NumCores = getNumberCoresFromPerfLevel(i) 37 | p[i].Capabilities = getCapabilities() 38 | p[i].Cores = make([]*ProcessorCore, getTotalCores()) 39 | } 40 | return p 41 | } 42 | 43 | // getCapabilities valid for ARM, see https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics 44 | func getCapabilities() []string { 45 | var caps []string 46 | 47 | // add ARM capabilities 48 | if hasARMArchitecture { 49 | for cap, isEnabled := range sysctlOutput { 50 | if isEnabled == "1" { 51 | // capabilities with keys with a common prefix 52 | commonPrefix := "hw.optional.arm." 53 | if strings.HasPrefix(cap, commonPrefix) { 54 | caps = append(caps, strings.TrimPrefix(cap, commonPrefix)) 55 | } 56 | // not following prefix convention but are important 57 | if cap == "hw.optional.AdvSIMD_HPFPCvt" { 58 | caps = append(caps, "AdvSIMD_HPFPCvt") 59 | } 60 | if cap == "hw.optional.armv8_crc32" { 61 | caps = append(caps, "armv8_crc32") 62 | } 63 | } 64 | } 65 | 66 | // hw.optional.AdvSIMD and hw.optional.floatingpoint are always enabled (see linked doc) 67 | caps = append(caps, "AdvSIMD") 68 | caps = append(caps, "floatingpoint") 69 | } 70 | 71 | return caps 72 | } 73 | 74 | // populateSysctlOutput to populate a map to quickly retrieve values later 75 | func populateSysctlOutput() error { 76 | // get sysctl output 77 | o, err := exec.Command("sysctl", "-a").CombinedOutput() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // clean up and store sysctl output 83 | oS := strings.Split(string(o), "\n") 84 | for _, l := range oS { 85 | if l != "" { 86 | s := strings.SplitN(l, ":", 2) 87 | if len(s) < 2 { 88 | continue 89 | } 90 | k, v := strings.TrimSpace(s[0]), strings.TrimSpace(s[1]) 91 | sysctlOutput[k] = v 92 | 93 | // see if it's possible to determine if ARM 94 | if k == "hw.optional.arm64" && v == "1" { 95 | hasARMArchitecture = true 96 | } 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func getNumberCoresFromPerfLevel(i int) uint32 { 104 | key := fmt.Sprintf("hw.perflevel%s.physicalcpu_max", strconv.Itoa(i)) 105 | nCores := sysctlOutput[key] 106 | return stringToUint32(nCores) 107 | } 108 | 109 | func getVendor() string { 110 | v := sysctlOutput["machdep.cpu.brand_string"] 111 | return v 112 | } 113 | 114 | func getProcTopCount() int { 115 | pC, ok := sysctlOutput["hw.nperflevels"] 116 | if !ok { 117 | // most likely intel so no performance/efficiency core seperation 118 | return 1 119 | } 120 | i, _ := strconv.Atoi(pC) 121 | return i 122 | } 123 | 124 | // num of physical cores 125 | func getTotalCores() uint32 { 126 | nCores := sysctlOutput["hw.physicalcpu_max"] 127 | return stringToUint32(nCores) 128 | } 129 | 130 | func getTotalThreads() uint32 { 131 | nThreads := sysctlOutput["machdep.cpu.thread_count"] 132 | return stringToUint32(nThreads) 133 | } 134 | 135 | func stringToUint32(s string) uint32 { 136 | o, _ := strconv.ParseUint(s, 10, 0) 137 | return uint32(o) 138 | } 139 | -------------------------------------------------------------------------------- /pkg/cpu/cpu_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package cpu_test 8 | 9 | import ( 10 | "bytes" 11 | "io" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/jaypipes/ghw/pkg/cpu" 18 | "github.com/jaypipes/ghw/pkg/option" 19 | "github.com/jaypipes/ghw/pkg/topology" 20 | "github.com/jaypipes/ghw/testdata" 21 | ) 22 | 23 | // nolint: gocyclo 24 | func TestArmCPU(t *testing.T) { 25 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_CPU"); ok { 26 | t.Skip("Skipping CPU tests.") 27 | } 28 | 29 | testdataPath, err := testdata.SnapshotsDirectory() 30 | if err != nil { 31 | t.Fatalf("Expected nil err, but got %v", err) 32 | } 33 | 34 | multiNumaSnapshot := filepath.Join(testdataPath, "linux-arm64-c288e0776090cd558ef793b2a4e61939.tar.gz") 35 | 36 | info, err := cpu.New(option.WithSnapshot(option.SnapshotOptions{ 37 | Path: multiNumaSnapshot, 38 | })) 39 | 40 | if err != nil { 41 | t.Fatalf("Expected nil err, but got %v", err) 42 | } 43 | if info == nil { 44 | t.Fatalf("Expected non-nil CPUInfo, but got nil") 45 | } 46 | 47 | if len(info.Processors) == 0 { 48 | t.Fatalf("Expected >0 processors but got 0.") 49 | } 50 | 51 | for _, p := range info.Processors { 52 | if p.Vendor == "" { 53 | t.Fatalf("Expected not empty vendor field.") 54 | } 55 | if p.TotalCores == 0 { 56 | t.Fatalf("Expected >0 cores but got 0.") 57 | } 58 | if p.TotalHardwareThreads == 0 { 59 | t.Fatalf("Expected >0 threads but got 0.") 60 | } 61 | if len(p.Capabilities) == 0 { 62 | t.Fatalf("Expected >0 capabilities but got 0.") 63 | } 64 | if !p.HasCapability(p.Capabilities[0]) { 65 | t.Fatalf("Expected p to have capability %s, but did not.", 66 | p.Capabilities[0]) 67 | } 68 | if len(p.Cores) == 0 { 69 | t.Fatalf("Expected >0 cores in processor, but got 0.") 70 | } 71 | for _, c := range p.Cores { 72 | if c.TotalHardwareThreads == 0 { 73 | t.Fatalf("Expected >0 threads but got 0.") 74 | } 75 | if len(c.LogicalProcessors) == 0 { 76 | t.Fatalf("Expected >0 logical processors but got 0.") 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestCheckCPUTopologyFilesForOfflineCPU(t *testing.T) { 83 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_CPU"); ok { 84 | t.Skip("Skipping CPU tests.") 85 | } 86 | 87 | testdataPath, err := testdata.SnapshotsDirectory() 88 | if err != nil { 89 | t.Fatalf("Expected nil err, but got %v", err) 90 | } 91 | 92 | offlineCPUSnapshot := filepath.Join(testdataPath, "linux-amd64-offlineCPUs.tar.gz") 93 | 94 | // Capture stderr 95 | rErr, wErr, err := os.Pipe() 96 | if err != nil { 97 | t.Fatalf("Cannot pipe StdErr. %v", err) 98 | } 99 | os.Stderr = wErr 100 | 101 | info, err := cpu.New(option.WithSnapshot(option.SnapshotOptions{ 102 | Path: offlineCPUSnapshot, 103 | })) 104 | if err != nil { 105 | t.Fatalf("Expected nil err, but got %v", err) 106 | } 107 | if info == nil { 108 | t.Fatalf("Expected non-nil CPUInfo, but got nil") 109 | } 110 | 111 | if len(info.Processors) == 0 { 112 | t.Fatalf("Expected >0 processors but got 0.") 113 | } 114 | wErr.Close() 115 | var bufErr bytes.Buffer 116 | if _, err := io.Copy(&bufErr, rErr); err != nil { 117 | t.Fatalf("Failed to copy data to buffer: %v", err) 118 | } 119 | errorOutput := bufErr.String() 120 | if strings.Contains(errorOutput, "WARNING: failed to read int from file:") { 121 | t.Fatalf("Unexpected warning related to missing files under topology directory was reported") 122 | } 123 | } 124 | 125 | func TestNumCoresAmongOfflineCPUs(t *testing.T) { 126 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_CPU"); ok { 127 | t.Skip("Skipping CPU tests.") 128 | } 129 | 130 | testdataPath, err := testdata.SnapshotsDirectory() 131 | if err != nil { 132 | t.Fatalf("Expected nil err, but got %v", err) 133 | } 134 | 135 | offlineCPUSnapshot := filepath.Join(testdataPath, "linux-amd64-offlineCPUs.tar.gz") 136 | 137 | // Capture stderr 138 | rErr, wErr, err := os.Pipe() 139 | if err != nil { 140 | t.Fatalf("Cannot pipe the StdErr. %v", err) 141 | } 142 | info, err := topology.New(option.WithSnapshot(option.SnapshotOptions{ 143 | Path: offlineCPUSnapshot, 144 | })) 145 | if err != nil { 146 | t.Fatalf("Error determining node topology. %v", err) 147 | } 148 | 149 | if len(info.Nodes) < 1 { 150 | t.Fatal("No nodes found. Must contain one or more nodes") 151 | } 152 | for _, node := range info.Nodes { 153 | if len(node.Cores) < 1 { 154 | t.Fatal("No cores found. Must contain one or more cores") 155 | } 156 | } 157 | wErr.Close() 158 | var bufErr bytes.Buffer 159 | if _, err := io.Copy(&bufErr, rErr); err != nil { 160 | t.Fatalf("Failed to copy data to buffer: %v", err) 161 | } 162 | errorOutput := bufErr.String() 163 | if strings.Contains(errorOutput, "WARNING: failed to read int from file:") { 164 | t.Fatalf("Unexpected warnings related to missing files under topology directory was raised") 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /pkg/cpu/cpu_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows && !darwin 2 | // +build !linux,!windows,!darwin 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package cpu 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("cpu.Info.load not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/cpu/cpu_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package cpu_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/cpu" 14 | ) 15 | 16 | // nolint: gocyclo 17 | func TestCPU(t *testing.T) { 18 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_CPU"); ok { 19 | t.Skip("Skipping CPU tests.") 20 | } 21 | 22 | info, err := cpu.New() 23 | 24 | if err != nil { 25 | t.Fatalf("Expected nil err, but got %v", err) 26 | } 27 | if info == nil { 28 | t.Fatalf("Expected non-nil CPUInfo, but got nil") 29 | } 30 | 31 | if len(info.Processors) == 0 { 32 | t.Fatalf("Expected >0 processors but got 0.") 33 | } 34 | 35 | for _, p := range info.Processors { 36 | if p.TotalCores == 0 { 37 | t.Fatalf("Expected >0 cores but got 0.") 38 | } 39 | if p.TotalHardwareThreads == 0 { 40 | t.Fatalf("Expected >0 threads but got 0.") 41 | } 42 | if len(p.Capabilities) == 0 { 43 | t.Fatalf("Expected >0 capabilities but got 0.") 44 | } 45 | if !p.HasCapability(p.Capabilities[0]) { 46 | t.Fatalf("Expected p to have capability %s, but did not.", 47 | p.Capabilities[0]) 48 | } 49 | if len(p.Cores) == 0 { 50 | t.Fatalf("Expected >0 cores in processor, but got 0.") 51 | } 52 | for _, c := range p.Cores { 53 | if c.TotalHardwareThreads == 0 { 54 | t.Fatalf("Expected >0 threads but got 0.") 55 | } 56 | if len(c.LogicalProcessors) == 0 { 57 | t.Fatalf("Expected >0 logical processors but got 0.") 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/cpu/cpu_windows.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package cpu 10 | 11 | import ( 12 | "github.com/StackExchange/wmi" 13 | ) 14 | 15 | const wmqlProcessor = "SELECT Manufacturer, Name, NumberOfLogicalProcessors, NumberOfCores FROM Win32_Processor" 16 | 17 | type win32Processor struct { 18 | Manufacturer *string 19 | Name *string 20 | NumberOfLogicalProcessors uint32 21 | NumberOfCores uint32 22 | } 23 | 24 | func (i *Info) load() error { 25 | // Getting info from WMI 26 | var win32descriptions []win32Processor 27 | if err := wmi.Query(wmqlProcessor, &win32descriptions); err != nil { 28 | return err 29 | } 30 | // Converting into standard structures 31 | i.Processors = processorsGet(win32descriptions) 32 | var totCores uint32 33 | var totThreads uint32 34 | for _, p := range i.Processors { 35 | totCores += p.TotalCores 36 | totThreads += p.TotalHardwareThreads 37 | } 38 | i.TotalCores = totCores 39 | i.TotalHardwareThreads = totThreads 40 | // TODO(jaypipes): Remove TotalThreads by v1.0 41 | i.TotalThreads = totThreads 42 | return nil 43 | } 44 | 45 | func processorsGet(win32descriptions []win32Processor) []*Processor { 46 | var procs []*Processor 47 | // Converting into standard structures 48 | for index, description := range win32descriptions { 49 | p := &Processor{ 50 | ID: index, 51 | Model: *description.Name, 52 | Vendor: *description.Manufacturer, 53 | TotalCores: description.NumberOfCores, 54 | // TODO(jaypipes): Remove NumCores before v1.0 55 | NumCores: description.NumberOfCores, 56 | TotalHardwareThreads: description.NumberOfLogicalProcessors, 57 | // TODO(jaypipes): Remove NumThreads before v1.0 58 | NumThreads: description.NumberOfLogicalProcessors, 59 | } 60 | procs = append(procs, p) 61 | } 62 | return procs 63 | } 64 | -------------------------------------------------------------------------------- /pkg/gpu/gpu.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package gpu 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | "github.com/jaypipes/ghw/pkg/marshal" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | "github.com/jaypipes/ghw/pkg/pci" 16 | "github.com/jaypipes/ghw/pkg/topology" 17 | ) 18 | 19 | type GraphicsCard struct { 20 | // the PCI address where the graphics card can be found 21 | Address string `json:"address"` 22 | // The "index" of the card on the bus (generally not useful information, 23 | // but might as well include it) 24 | Index int `json:"index"` 25 | // pointer to a PCIDevice struct that describes the vendor and product 26 | // model, etc 27 | // TODO(jaypipes): Rename this field to PCI, instead of DeviceInfo 28 | DeviceInfo *pci.Device `json:"pci"` 29 | // Topology node that the graphics card is affined to. Will be nil if the 30 | // architecture is not NUMA. 31 | Node *topology.Node `json:"node,omitempty"` 32 | } 33 | 34 | func (card *GraphicsCard) String() string { 35 | deviceStr := card.Address 36 | if card.DeviceInfo != nil { 37 | deviceStr = card.DeviceInfo.String() 38 | } 39 | nodeStr := "" 40 | if card.Node != nil { 41 | nodeStr = fmt.Sprintf(" [affined to NUMA node %d]", card.Node.ID) 42 | } 43 | return fmt.Sprintf( 44 | "card #%d %s@%s", 45 | card.Index, 46 | nodeStr, 47 | deviceStr, 48 | ) 49 | } 50 | 51 | type Info struct { 52 | ctx *context.Context 53 | GraphicsCards []*GraphicsCard `json:"cards"` 54 | } 55 | 56 | // New returns a pointer to an Info struct that contains information about the 57 | // graphics cards on the host system 58 | func New(opts ...*option.Option) (*Info, error) { 59 | ctx := context.New(opts...) 60 | info := &Info{ctx: ctx} 61 | if err := ctx.Do(info.load); err != nil { 62 | return nil, err 63 | } 64 | return info, nil 65 | } 66 | 67 | func (i *Info) String() string { 68 | numCardsStr := "cards" 69 | if len(i.GraphicsCards) == 1 { 70 | numCardsStr = "card" 71 | } 72 | return fmt.Sprintf( 73 | "gpu (%d graphics %s)", 74 | len(i.GraphicsCards), 75 | numCardsStr, 76 | ) 77 | } 78 | 79 | // simple private struct used to encapsulate gpu information in a top-level 80 | // "gpu" YAML/JSON map/object key 81 | type gpuPrinter struct { 82 | Info *Info `json:"gpu"` 83 | } 84 | 85 | // YAMLString returns a string with the gpu information formatted as YAML 86 | // under a top-level "gpu:" key 87 | func (i *Info) YAMLString() string { 88 | return marshal.SafeYAML(i.ctx, gpuPrinter{i}) 89 | } 90 | 91 | // JSONString returns a string with the gpu information formatted as JSON 92 | // under a top-level "gpu:" key 93 | func (i *Info) JSONString(indent bool) string { 94 | return marshal.SafeJSON(i.ctx, gpuPrinter{i}, indent) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/gpu/gpu_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package gpu_test 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/jaypipes/ghw/pkg/gpu" 16 | "github.com/jaypipes/ghw/pkg/option" 17 | "github.com/jaypipes/ghw/pkg/snapshot" 18 | 19 | "github.com/jaypipes/ghw/testdata" 20 | ) 21 | 22 | // testcase for https://github.com/jaypipes/ghw/issues/234 23 | // if nothing else: demonstrate how to consume snapshots from tests; 24 | // test a boundary condition actually happened in the wild, even though on a VM environment. 25 | func TestGPUWithoutNUMANodeInfo(t *testing.T) { 26 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_GPU"); ok { 27 | t.Skip("Skipping PCI tests.") 28 | } 29 | 30 | testdataPath, err := testdata.SnapshotsDirectory() 31 | if err != nil { 32 | t.Fatalf("Expected nil err, but got %v", err) 33 | } 34 | 35 | t.Setenv("PCIDB_PATH", testdata.PCIDBChroot()) 36 | 37 | workstationSnapshot := filepath.Join(testdataPath, "linux-amd64-amd-ryzen-1600.tar.gz") 38 | // from now on we use constants reflecting the content of the snapshot we requested, 39 | // which we reviewed beforehand. IOW, you need to know the content of the 40 | // snapshot to fully understand this test. Inspect it using 41 | // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-amd-ryzen-1600.tar.gz" ghwc gpu 42 | 43 | tmpRoot, err := os.MkdirTemp("", "ghw-gpu-testing-*") 44 | if err != nil { 45 | t.Fatalf("Unable to create temporary directory: %v", err) 46 | } 47 | 48 | _, err = snapshot.UnpackInto(workstationSnapshot, tmpRoot, 0) 49 | if err != nil { 50 | t.Fatalf("Unable to unpack %q into %q: %v", workstationSnapshot, tmpRoot, err) 51 | } 52 | defer func() { 53 | _ = snapshot.Cleanup(tmpRoot) 54 | }() 55 | 56 | err = os.Remove(filepath.Join(tmpRoot, "/sys/class/drm/card0/device/numa_node")) 57 | if err != nil && !errors.Is(err, os.ErrNotExist) { 58 | t.Fatalf("Cannot remove the NUMA node info: %v", err) 59 | } 60 | 61 | info, err := gpu.New(option.WithChroot(tmpRoot)) 62 | if err != nil { 63 | t.Fatalf("Expected nil err, but got %v", err) 64 | } 65 | if info == nil { 66 | t.Fatalf("Expected non-nil GPUInfo, but got nil") 67 | } 68 | if len(info.GraphicsCards) == 0 { 69 | t.Fatalf("Expected >0 GPU cards, but found 0.") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/gpu/gpu_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package gpu 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("gpuFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/gpu/gpu_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package gpu_test 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "testing" 13 | 14 | "github.com/jaypipes/ghw/pkg/gpu" 15 | "github.com/jaypipes/ghw/testdata" 16 | ) 17 | 18 | func TestGPU(t *testing.T) { 19 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_GPU"); ok { 20 | t.Skip("Skipping GPU tests.") 21 | } 22 | if _, err := os.Stat("/sys/class/drm"); errors.Is(err, os.ErrNotExist) { 23 | t.Skip("Skipping GPU tests. The environment has no /sys/class/drm directory.") 24 | } 25 | 26 | t.Setenv("PCIDB_PATH", testdata.PCIDBChroot()) 27 | 28 | info, err := gpu.New() 29 | if err != nil { 30 | t.Fatalf("Expected no error creating GPUInfo, but got %v", err) 31 | } 32 | 33 | if len(info.GraphicsCards) == 0 { 34 | t.Fatalf("Expected >0 GPU cards, but found 0.") 35 | } 36 | 37 | for _, card := range info.GraphicsCards { 38 | if card.Address != "" { 39 | di := card.DeviceInfo 40 | if di == nil { 41 | t.Fatalf("Expected card with address %s to have non-nil DeviceInfo.", card.Address) 42 | } 43 | } 44 | // TODO(jaypipes): Add Card.Node test when using injected sysfs for testing 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/gpu/gpu_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package gpu 7 | 8 | import ( 9 | "strings" 10 | 11 | "github.com/StackExchange/wmi" 12 | "github.com/jaypipes/pcidb" 13 | 14 | "github.com/jaypipes/ghw/pkg/pci" 15 | "github.com/jaypipes/ghw/pkg/util" 16 | ) 17 | 18 | const wqlVideoController = "SELECT Caption, CreationClassName, Description, DeviceID, DriverVersion, Name, PNPDeviceID, SystemCreationClassName, SystemName, VideoArchitecture, VideoMemoryType, VideoModeDescription, VideoProcessor FROM Win32_VideoController" 19 | 20 | type win32VideoController struct { 21 | Caption string 22 | CreationClassName string 23 | Description string 24 | DeviceID string 25 | DriverVersion string 26 | Name string 27 | PNPDeviceID string 28 | SystemCreationClassName string 29 | SystemName string 30 | VideoArchitecture uint16 31 | VideoMemoryType uint16 32 | VideoModeDescription string 33 | VideoProcessor string 34 | } 35 | 36 | const wqlPnPEntity = "SELECT Caption, CreationClassName, Description, DeviceID, Manufacturer, Name, PNPClass, PNPDeviceID FROM Win32_PnPEntity" 37 | 38 | type win32PnPEntity struct { 39 | Caption string 40 | CreationClassName string 41 | Description string 42 | DeviceID string 43 | Manufacturer string 44 | Name string 45 | PNPClass string 46 | PNPDeviceID string 47 | } 48 | 49 | func (i *Info) load() error { 50 | // Getting data from WMI 51 | var win32VideoControllerDescriptions []win32VideoController 52 | if err := wmi.Query(wqlVideoController, &win32VideoControllerDescriptions); err != nil { 53 | return err 54 | } 55 | 56 | // Building dynamic WHERE clause with addresses to create a single query collecting all desired data 57 | queryAddresses := []string{} 58 | for _, description := range win32VideoControllerDescriptions { 59 | var queryAddres = strings.Replace(description.PNPDeviceID, "\\", `\\`, -1) 60 | queryAddresses = append(queryAddresses, "PNPDeviceID='"+queryAddres+"'") 61 | } 62 | whereClause := strings.Join(queryAddresses[:], " OR ") 63 | 64 | // Getting data from WMI 65 | var win32PnPDescriptions []win32PnPEntity 66 | var wqlPnPDevice = wqlPnPEntity + " WHERE " + whereClause 67 | if err := wmi.Query(wqlPnPDevice, &win32PnPDescriptions); err != nil { 68 | return err 69 | } 70 | 71 | // Converting into standard structures 72 | cards := make([]*GraphicsCard, 0) 73 | for _, description := range win32VideoControllerDescriptions { 74 | card := &GraphicsCard{ 75 | Address: description.DeviceID, // https://stackoverflow.com/questions/32073667/how-do-i-discover-the-pcie-bus-topology-and-slot-numbers-on-the-board 76 | Index: 0, 77 | DeviceInfo: GetDevice(description.PNPDeviceID, win32PnPDescriptions), 78 | } 79 | card.DeviceInfo.Driver = description.DriverVersion 80 | cards = append(cards, card) 81 | } 82 | i.GraphicsCards = cards 83 | return nil 84 | } 85 | 86 | func GetDevice(id string, entities []win32PnPEntity) *pci.Device { 87 | // Backslashing PnP address ID as requested by JSON and VMI query: https://docs.microsoft.com/en-us/windows/win32/wmisdk/where-clause 88 | var queryAddress = strings.Replace(id, "\\", `\\`, -1) 89 | // Preparing default structure 90 | var device = &pci.Device{ 91 | Address: queryAddress, 92 | Vendor: &pcidb.Vendor{ 93 | ID: util.UNKNOWN, 94 | Name: util.UNKNOWN, 95 | Products: []*pcidb.Product{}, 96 | }, 97 | Subsystem: &pcidb.Product{ 98 | ID: util.UNKNOWN, 99 | Name: util.UNKNOWN, 100 | Subsystems: []*pcidb.Product{}, 101 | }, 102 | Product: &pcidb.Product{ 103 | ID: util.UNKNOWN, 104 | Name: util.UNKNOWN, 105 | Subsystems: []*pcidb.Product{}, 106 | }, 107 | Class: &pcidb.Class{ 108 | ID: util.UNKNOWN, 109 | Name: util.UNKNOWN, 110 | Subclasses: []*pcidb.Subclass{}, 111 | }, 112 | Subclass: &pcidb.Subclass{ 113 | ID: util.UNKNOWN, 114 | Name: util.UNKNOWN, 115 | ProgrammingInterfaces: []*pcidb.ProgrammingInterface{}, 116 | }, 117 | ProgrammingInterface: &pcidb.ProgrammingInterface{ 118 | ID: util.UNKNOWN, 119 | Name: util.UNKNOWN, 120 | }, 121 | } 122 | // If an entity is found we get its data inside the standard structure 123 | for _, description := range entities { 124 | if id == description.PNPDeviceID { 125 | device.Vendor.ID = description.Manufacturer 126 | device.Vendor.Name = description.Manufacturer 127 | device.Product.ID = description.Name 128 | device.Product.Name = description.Description 129 | break 130 | } 131 | } 132 | return device 133 | } 134 | -------------------------------------------------------------------------------- /pkg/linuxdmi/dmi_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package linuxdmi 7 | 8 | import ( 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/jaypipes/ghw/pkg/context" 14 | "github.com/jaypipes/ghw/pkg/linuxpath" 15 | "github.com/jaypipes/ghw/pkg/util" 16 | ) 17 | 18 | func Item(ctx *context.Context, value string) string { 19 | paths := linuxpath.New(ctx) 20 | path := filepath.Join(paths.SysClassDMI, "id", value) 21 | 22 | b, err := os.ReadFile(path) 23 | if err != nil { 24 | ctx.Warn("Unable to read %s: %s\n", value, err) 25 | return util.UNKNOWN 26 | } 27 | 28 | return strings.TrimSpace(string(b)) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/linuxpath/path_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package linuxpath 7 | 8 | import ( 9 | "fmt" 10 | "path/filepath" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | ) 14 | 15 | // PathRoots holds the roots of all the filesystem subtrees 16 | // ghw wants to access. 17 | type PathRoots struct { 18 | Etc string 19 | Proc string 20 | Run string 21 | Sys string 22 | Var string 23 | } 24 | 25 | // DefaultPathRoots return the canonical default value for PathRoots 26 | func DefaultPathRoots() PathRoots { 27 | return PathRoots{ 28 | Etc: "/etc", 29 | Proc: "/proc", 30 | Run: "/run", 31 | Sys: "/sys", 32 | Var: "/var", 33 | } 34 | } 35 | 36 | // PathRootsFromContext initialize PathRoots from the given Context, 37 | // allowing overrides of the canonical default paths. 38 | func PathRootsFromContext(ctx *context.Context) PathRoots { 39 | roots := DefaultPathRoots() 40 | if pathEtc, ok := ctx.PathOverrides["/etc"]; ok { 41 | roots.Etc = pathEtc 42 | } 43 | if pathProc, ok := ctx.PathOverrides["/proc"]; ok { 44 | roots.Proc = pathProc 45 | } 46 | if pathRun, ok := ctx.PathOverrides["/run"]; ok { 47 | roots.Run = pathRun 48 | } 49 | if pathSys, ok := ctx.PathOverrides["/sys"]; ok { 50 | roots.Sys = pathSys 51 | } 52 | if pathVar, ok := ctx.PathOverrides["/var"]; ok { 53 | roots.Var = pathVar 54 | } 55 | return roots 56 | } 57 | 58 | type Paths struct { 59 | VarLog string 60 | ProcMeminfo string 61 | ProcCpuinfo string 62 | ProcMounts string 63 | SysKernelMMHugepages string 64 | SysBlock string 65 | SysDevicesSystemNode string 66 | SysDevicesSystemMemory string 67 | SysDevicesSystemCPU string 68 | SysBusPciDevices string 69 | SysClassDRM string 70 | SysClassDMI string 71 | SysClassNet string 72 | RunUdevData string 73 | } 74 | 75 | // New returns a new Paths struct containing filepath fields relative to the 76 | // supplied Context 77 | func New(ctx *context.Context) *Paths { 78 | roots := PathRootsFromContext(ctx) 79 | return &Paths{ 80 | VarLog: filepath.Join(ctx.Chroot, roots.Var, "log"), 81 | ProcMeminfo: filepath.Join(ctx.Chroot, roots.Proc, "meminfo"), 82 | ProcCpuinfo: filepath.Join(ctx.Chroot, roots.Proc, "cpuinfo"), 83 | ProcMounts: filepath.Join(ctx.Chroot, roots.Proc, "self", "mounts"), 84 | SysKernelMMHugepages: filepath.Join(ctx.Chroot, roots.Sys, "kernel", "mm", "hugepages"), 85 | SysBlock: filepath.Join(ctx.Chroot, roots.Sys, "block"), 86 | SysDevicesSystemNode: filepath.Join(ctx.Chroot, roots.Sys, "devices", "system", "node"), 87 | SysDevicesSystemMemory: filepath.Join(ctx.Chroot, roots.Sys, "devices", "system", "memory"), 88 | SysDevicesSystemCPU: filepath.Join(ctx.Chroot, roots.Sys, "devices", "system", "cpu"), 89 | SysBusPciDevices: filepath.Join(ctx.Chroot, roots.Sys, "bus", "pci", "devices"), 90 | SysClassDRM: filepath.Join(ctx.Chroot, roots.Sys, "class", "drm"), 91 | SysClassDMI: filepath.Join(ctx.Chroot, roots.Sys, "class", "dmi"), 92 | SysClassNet: filepath.Join(ctx.Chroot, roots.Sys, "class", "net"), 93 | RunUdevData: filepath.Join(ctx.Chroot, roots.Run, "udev", "data"), 94 | } 95 | } 96 | 97 | func (p *Paths) NodeCPU(nodeID int, lpID int) string { 98 | return filepath.Join( 99 | p.SysDevicesSystemNode, 100 | fmt.Sprintf("node%d", nodeID), 101 | fmt.Sprintf("cpu%d", lpID), 102 | ) 103 | } 104 | 105 | func (p *Paths) NodeCPUCache(nodeID int, lpID int) string { 106 | return filepath.Join( 107 | p.NodeCPU(nodeID, lpID), 108 | "cache", 109 | ) 110 | } 111 | 112 | func (p *Paths) NodeCPUCacheIndex(nodeID int, lpID int, cacheIndex int) string { 113 | return filepath.Join( 114 | p.NodeCPUCache(nodeID, lpID), 115 | fmt.Sprintf("index%d", cacheIndex), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /pkg/linuxpath/path_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | //go:build linux 8 | // +build linux 9 | 10 | package linuxpath_test 11 | 12 | import ( 13 | "os" 14 | "path/filepath" 15 | "slices" 16 | "sort" 17 | "testing" 18 | 19 | "github.com/jaypipes/ghw/pkg/context" 20 | "github.com/jaypipes/ghw/pkg/gpu" 21 | "github.com/jaypipes/ghw/pkg/linuxpath" 22 | "github.com/jaypipes/ghw/pkg/option" 23 | ) 24 | 25 | func TestPathRoot(t *testing.T) { 26 | orig, origExists := os.LookupEnv("GHW_CHROOT") 27 | if origExists { 28 | // For tests, save the original, test an override and then at the end 29 | // of the test, reset to the original 30 | defer os.Setenv("GHW_CHROOT", orig) 31 | os.Unsetenv("GHW_CHROOT") 32 | } else { 33 | defer os.Unsetenv("GHW_CHROOT") 34 | } 35 | 36 | ctx := context.FromEnv() 37 | paths := linuxpath.New(ctx) 38 | 39 | // No environment variable is set for GHW_CHROOT, so pathProcCpuinfo() should 40 | // return the default "/proc/cpuinfo" 41 | path := paths.ProcCpuinfo 42 | if path != "/proc/cpuinfo" { 43 | t.Fatalf("Expected pathProcCpuInfo() to return '/proc/cpuinfo' but got %s", path) 44 | } 45 | 46 | // Now set the GHW_CHROOT environ variable and verify that pathRoot() 47 | // returns that value 48 | os.Setenv("GHW_CHROOT", "/host") 49 | 50 | ctx = context.FromEnv() 51 | paths = linuxpath.New(ctx) 52 | 53 | path = paths.ProcCpuinfo 54 | if path != "/host/proc/cpuinfo" { 55 | t.Fatalf("Expected path.ProcCpuinfo to return '/host/proc/cpuinfo' but got %s", path) 56 | } 57 | } 58 | 59 | func TestPathSpecificRoots(t *testing.T) { 60 | ctx := context.New(option.WithPathOverrides(option.PathOverrides{ 61 | "/proc": "/host-proc", 62 | "/sys": "/host-sys", 63 | })) 64 | 65 | paths := linuxpath.New(ctx) 66 | 67 | path := paths.ProcCpuinfo 68 | expectedPath := "/host-proc/cpuinfo" 69 | if path != expectedPath { 70 | t.Fatalf("Expected path.ProcCpuInfo to return %q but got %q", expectedPath, path) 71 | } 72 | 73 | path = paths.SysBusPciDevices 74 | expectedPath = "/host-sys/bus/pci/devices" 75 | if path != expectedPath { 76 | t.Fatalf("Expected path.SysBusPciDevices to return %q but got %q", expectedPath, path) 77 | } 78 | } 79 | 80 | func TestPathChrootAndSpecifics(t *testing.T) { 81 | ctx := context.New( 82 | option.WithPathOverrides(option.PathOverrides{ 83 | "/proc": "/host2-proc", 84 | "/sys": "/host2-sys", 85 | }), 86 | option.WithChroot("/redirect"), 87 | ) 88 | 89 | paths := linuxpath.New(ctx) 90 | 91 | path := paths.ProcCpuinfo 92 | expectedPath := "/redirect/host2-proc/cpuinfo" 93 | if path != expectedPath { 94 | t.Fatalf("Expected path.ProcCpuInfo to return %q but got %q", expectedPath, path) 95 | } 96 | 97 | path = paths.SysBusPciDevices 98 | expectedPath = "/redirect/host2-sys/bus/pci/devices" 99 | if path != expectedPath { 100 | t.Fatalf("Expected path.SysBusPciDevices to return %q but got %q", expectedPath, path) 101 | } 102 | } 103 | 104 | func TestGpuPathRegexp(t *testing.T) { 105 | tmp := t.TempDir() 106 | 107 | // Make sure that last element of path is unique across other paths. 108 | var paths = []string{ 109 | "../../devices/pci0000:00/0000:00:03.1/0000:07:00.0/drm/card0", 110 | "../../devices/pci0000:00/0000:00:0d.0/0000:01:00.1/drm/card1", 111 | "../../devices/pci0000:25/0000:25:01.0/0000:26:00.0/drm/card2", 112 | "../../devices/pci0000:89/0000:89:01.0/0000:8a:00.0/drm/card3", 113 | } 114 | 115 | // Expecting third element from paths above. 116 | var expectedAddrs = []string{ 117 | "0000:07:00.0", "0000:01:00.1", "0000:26:00.0", "0000:8a:00.0", 118 | } 119 | 120 | drmPath := filepath.Join(tmp, "/sys/class/drm") 121 | err := os.MkdirAll(drmPath, 0755) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | for _, target := range paths { 126 | linkname := filepath.Join(drmPath, filepath.Base(target)) 127 | err := os.Symlink(target, linkname) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | } 132 | 133 | info, err := gpu.New(option.WithChroot(tmp)) 134 | if err != nil { 135 | t.Fatalf("Expected nil err, but got %v", err) 136 | } 137 | 138 | if len(info.GraphicsCards) != len(expectedAddrs) { 139 | t.Fatalf("Expected %d graphics cards, got %d", len(expectedAddrs), len(info.GraphicsCards)) 140 | } 141 | foundAddrs := make([]string, 0) 142 | for _, card := range info.GraphicsCards { 143 | foundAddrs = append(foundAddrs, card.Address) 144 | } 145 | 146 | sort.Strings(expectedAddrs) 147 | sort.Strings(foundAddrs) 148 | 149 | if !slices.Equal(expectedAddrs, foundAddrs) { 150 | t.Fatalf("Some cards not found") 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /pkg/marshal/marshal.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package marshal 8 | 9 | import ( 10 | "encoding/json" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | yaml "gopkg.in/yaml.v3" 14 | ) 15 | 16 | // SafeYAML returns a string after marshalling the supplied parameter into YAML. 17 | func SafeYAML(ctx *context.Context, p interface{}) string { 18 | b, err := json.Marshal(p) 19 | if err != nil { 20 | ctx.Warn("error marshalling JSON: %s", err) 21 | return "" 22 | } 23 | 24 | var jsonObj interface{} 25 | if err := yaml.Unmarshal(b, &jsonObj); err != nil { 26 | ctx.Warn("error converting JSON to YAML: %s", err) 27 | return "" 28 | } 29 | 30 | yb, err := yaml.Marshal(jsonObj) 31 | if err != nil { 32 | ctx.Warn("error marshalling YAML: %s", err) 33 | return "" 34 | } 35 | 36 | return string(yb) 37 | } 38 | 39 | // SafeJSON returns a string after marshalling the supplied parameter into 40 | // JSON. Accepts an optional argument to trigger pretty/indented formatting of 41 | // the JSON string. 42 | func SafeJSON(ctx *context.Context, p interface{}, indent bool) string { 43 | var b []byte 44 | var err error 45 | if !indent { 46 | b, err = json.Marshal(p) 47 | } else { 48 | b, err = json.MarshalIndent(&p, "", " ") 49 | } 50 | if err != nil { 51 | ctx.Warn("error marshalling JSON: %s", err) 52 | return "" 53 | } 54 | return string(b) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/memory/memory.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package memory 8 | 9 | import ( 10 | "fmt" 11 | "math" 12 | 13 | "github.com/jaypipes/ghw/pkg/context" 14 | "github.com/jaypipes/ghw/pkg/marshal" 15 | "github.com/jaypipes/ghw/pkg/option" 16 | "github.com/jaypipes/ghw/pkg/unitutil" 17 | "github.com/jaypipes/ghw/pkg/util" 18 | ) 19 | 20 | // Module describes a single physical memory module for a host system. Pretty 21 | // much all modern systems contain dual in-line memory modules (DIMMs). 22 | // 23 | // See https://en.wikipedia.org/wiki/DIMM 24 | type Module struct { 25 | Label string `json:"label"` 26 | Location string `json:"location"` 27 | SerialNumber string `json:"serial_number"` 28 | SizeBytes int64 `json:"size_bytes"` 29 | Vendor string `json:"vendor"` 30 | } 31 | 32 | // HugePageAmounts describes huge page info 33 | type HugePageAmounts struct { 34 | Total int64 `json:"total"` 35 | Free int64 `json:"free"` 36 | Surplus int64 `json:"surplus"` 37 | // Note: this field will not be populated for Topology call, since data not present in NUMA folder structure 38 | Reserved int64 `json:"reserved"` 39 | } 40 | 41 | // Area describes a set of physical memory on a host system. Non-NUMA systems 42 | // will almost always have a single memory area containing all memory the 43 | // system can use. NUMA systems will have multiple memory areas, one or more 44 | // for each NUMA node/cell in the system. 45 | type Area struct { 46 | TotalPhysicalBytes int64 `json:"total_physical_bytes"` 47 | TotalUsableBytes int64 `json:"total_usable_bytes"` 48 | // An array of sizes, in bytes, of memory pages supported in this area 49 | SupportedPageSizes []uint64 `json:"supported_page_sizes"` 50 | // Default system huge page size, in bytes 51 | DefaultHugePageSize uint64 `json:"default_huge_page_size"` 52 | // Amount of memory, in bytes, consumed by huge pages of all sizes 53 | TotalHugePageBytes int64 `json:"total_huge_page_bytes"` 54 | // Huge page info by size 55 | HugePageAmountsBySize map[uint64]*HugePageAmounts `json:"huge_page_amounts_by_size"` 56 | Modules []*Module `json:"modules"` 57 | } 58 | 59 | // String returns a short string with a summary of information for this memory 60 | // area 61 | func (a *Area) String() string { 62 | tpbs := util.UNKNOWN 63 | if a.TotalPhysicalBytes > 0 { 64 | tpb := a.TotalPhysicalBytes 65 | unit, unitStr := unitutil.AmountString(tpb) 66 | tpb = int64(math.Ceil(float64(a.TotalPhysicalBytes) / float64(unit))) 67 | tpbs = fmt.Sprintf("%d%s", tpb, unitStr) 68 | } 69 | tubs := util.UNKNOWN 70 | if a.TotalUsableBytes > 0 { 71 | tub := a.TotalUsableBytes 72 | unit, unitStr := unitutil.AmountString(tub) 73 | tub = int64(math.Ceil(float64(a.TotalUsableBytes) / float64(unit))) 74 | tubs = fmt.Sprintf("%d%s", tub, unitStr) 75 | } 76 | return fmt.Sprintf("memory (%s physical, %s usable)", tpbs, tubs) 77 | } 78 | 79 | // Info contains information about the memory on a host system. 80 | type Info struct { 81 | ctx *context.Context 82 | Area 83 | } 84 | 85 | // New returns an Info struct that describes the memory on a host system. 86 | func New(opts ...*option.Option) (*Info, error) { 87 | ctx := context.New(opts...) 88 | info := &Info{ctx: ctx} 89 | if err := ctx.Do(info.load); err != nil { 90 | return nil, err 91 | } 92 | return info, nil 93 | } 94 | 95 | // String returns a short string with a summary of memory information 96 | func (i *Info) String() string { 97 | return i.Area.String() 98 | } 99 | 100 | // simple private struct used to encapsulate memory information in a top-level 101 | // "memory" YAML/JSON map/object key 102 | type memoryPrinter struct { 103 | Info *Info `json:"memory"` 104 | } 105 | 106 | // YAMLString returns a string with the memory information formatted as YAML 107 | // under a top-level "memory:" key 108 | func (i *Info) YAMLString() string { 109 | return marshal.SafeYAML(i.ctx, memoryPrinter{i}) 110 | } 111 | 112 | // JSONString returns a string with the memory information formatted as JSON 113 | // under a top-level "memory:" key 114 | func (i *Info) JSONString(indent bool) string { 115 | return marshal.SafeJSON(i.ctx, memoryPrinter{i}, indent) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/memory/memory_cache.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package memory 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/jaypipes/ghw/pkg/unitutil" 16 | ) 17 | 18 | // CacheType indicates the type of memory stored in a memory cache. 19 | type CacheType int 20 | 21 | const ( 22 | // CacheTypeUnified indicates the memory cache stores both instructions and 23 | // data. 24 | CacheTypeUnified CacheType = iota 25 | // CacheTypeInstruction indicates the memory cache stores only instructions 26 | // (executable bytecode). 27 | CacheTypeInstruction 28 | // CacheTypeData indicates the memory cache stores only data 29 | // (non-executable bytecode). 30 | CacheTypeData 31 | ) 32 | 33 | const ( 34 | // DEPRECATED: Please use CacheTypeUnified 35 | CACHE_TYPE_UNIFIED = CacheTypeUnified 36 | // DEPRECATED: Please use CacheTypeUnified 37 | CACHE_TYPE_INSTRUCTION = CacheTypeInstruction 38 | // DEPRECATED: Please use CacheTypeUnified 39 | CACHE_TYPE_DATA = CacheTypeData 40 | ) 41 | 42 | var ( 43 | memoryCacheTypeString = map[CacheType]string{ 44 | CacheTypeUnified: "Unified", 45 | CacheTypeInstruction: "Instruction", 46 | CacheTypeData: "Data", 47 | } 48 | 49 | // NOTE(fromani): the keys are all lowercase and do not match 50 | // the keys in the opposite table `memoryCacheTypeString`. 51 | // This is done because of the choice we made in 52 | // CacheType:MarshalJSON. 53 | // We use this table only in UnmarshalJSON, so it should be OK. 54 | stringMemoryCacheType = map[string]CacheType{ 55 | "unified": CacheTypeUnified, 56 | "instruction": CacheTypeInstruction, 57 | "data": CacheTypeData, 58 | } 59 | ) 60 | 61 | func (a CacheType) String() string { 62 | return memoryCacheTypeString[a] 63 | } 64 | 65 | // NOTE(jaypipes): since serialized output is as "official" as we're going to 66 | // get, let's lowercase the string output when serializing, in order to 67 | // "normalize" the expected serialized output 68 | func (a CacheType) MarshalJSON() ([]byte, error) { 69 | return []byte(strconv.Quote(strings.ToLower(a.String()))), nil 70 | } 71 | 72 | func (a *CacheType) UnmarshalJSON(b []byte) error { 73 | var s string 74 | if err := json.Unmarshal(b, &s); err != nil { 75 | return err 76 | } 77 | key := strings.ToLower(s) 78 | val, ok := stringMemoryCacheType[key] 79 | if !ok { 80 | return fmt.Errorf("unknown memory cache type: %q", key) 81 | } 82 | *a = val 83 | return nil 84 | } 85 | 86 | type SortByCacheLevelTypeFirstProcessor []*Cache 87 | 88 | func (a SortByCacheLevelTypeFirstProcessor) Len() int { return len(a) } 89 | func (a SortByCacheLevelTypeFirstProcessor) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 90 | func (a SortByCacheLevelTypeFirstProcessor) Less(i, j int) bool { 91 | if a[i].Level < a[j].Level { 92 | return true 93 | } else if a[i].Level == a[j].Level { 94 | if a[i].Type < a[j].Type { 95 | return true 96 | } else if a[i].Type == a[j].Type { 97 | // NOTE(jaypipes): len(LogicalProcessors) is always >0 and is always 98 | // sorted lowest LP ID to highest LP ID 99 | return a[i].LogicalProcessors[0] < a[j].LogicalProcessors[0] 100 | } 101 | } 102 | return false 103 | } 104 | 105 | type SortByLogicalProcessorId []uint32 106 | 107 | func (a SortByLogicalProcessorId) Len() int { return len(a) } 108 | func (a SortByLogicalProcessorId) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 109 | func (a SortByLogicalProcessorId) Less(i, j int) bool { return a[i] < a[j] } 110 | 111 | // Cache contains information about a single memory cache on a physical CPU 112 | // package. Caches have a 1-based numeric level, with lower numbers indicating 113 | // the cache is "closer" to the processing cores and reading memory from the 114 | // cache will be faster relative to caches with higher levels. Note that this 115 | // has nothing to do with RAM or memory modules like DIMMs. 116 | type Cache struct { 117 | // Level is a 1-based numeric level that indicates the relative closeness 118 | // of this cache to processing cores on the physical package. Lower numbers 119 | // are "closer" to the processing cores and therefore have faster access 120 | // times. 121 | Level uint8 `json:"level"` 122 | // Type indicates what type of memory is stored in the cache. Can be 123 | // instruction (executable bytecodes), data or both. 124 | Type CacheType `json:"type"` 125 | // SizeBytes indicates the size of the cache in bytes. 126 | SizeBytes uint64 `json:"size_bytes"` 127 | // The set of logical processors (hardware threads) that have access to 128 | // this cache. 129 | LogicalProcessors []uint32 `json:"logical_processors"` 130 | } 131 | 132 | func (c *Cache) String() string { 133 | sizeKb := c.SizeBytes / uint64(unitutil.KB) 134 | typeStr := "" 135 | if c.Type == CacheTypeInstruction { 136 | typeStr = "i" 137 | } else if c.Type == CacheTypeData { 138 | typeStr = "d" 139 | } 140 | cacheIDStr := fmt.Sprintf("L%d%s", c.Level, typeStr) 141 | processorMapStr := "" 142 | if c.LogicalProcessors != nil { 143 | lpStrings := make([]string, len(c.LogicalProcessors)) 144 | for x, lpid := range c.LogicalProcessors { 145 | lpStrings[x] = strconv.Itoa(int(lpid)) 146 | } 147 | processorMapStr = " shared with logical processors: " + strings.Join(lpStrings, ",") 148 | } 149 | return fmt.Sprintf( 150 | "%s cache (%d KB)%s", 151 | cacheIDStr, 152 | sizeKb, 153 | processorMapStr, 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /pkg/memory/memory_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package memory_test 8 | 9 | import ( 10 | "encoding/json" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/memory" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | ) 16 | 17 | // we have this test in memory_linux_test.go (and not in memory_test.go) because `mem.load.Info` is implemented 18 | // only on linux; so having it in the platform-independent tests would lead to false negatives. 19 | func TestMemoryMarshalUnmarshal(t *testing.T) { 20 | data, err := memory.New(option.WithNullAlerter()) 21 | if err != nil { 22 | t.Fatalf("Expected no error creating memory.Info, but got %v", err) 23 | } 24 | 25 | jdata, err := json.Marshal(data) 26 | if err != nil { 27 | t.Fatalf("Expected no error marshaling memory.Info, but got %v", err) 28 | } 29 | 30 | var topo *memory.Info 31 | 32 | err = json.Unmarshal(jdata, &topo) 33 | if err != nil { 34 | t.Fatalf("Expected no error unmarshaling memory.Info, but got %v", err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/memory/memory_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package memory 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("mem.Info.load not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/memory/memory_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package memory_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/memory" 14 | ) 15 | 16 | // nolint: gocyclo 17 | func TestMemory(t *testing.T) { 18 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_MEMORY"); ok { 19 | t.Skip("Skipping MEMORY tests.") 20 | } 21 | 22 | mem, err := memory.New() 23 | if err != nil { 24 | t.Fatalf("Expected nil error, but got %v", err) 25 | } 26 | 27 | tpb := mem.TotalPhysicalBytes 28 | tub := mem.TotalUsableBytes 29 | if tpb == 0 { 30 | t.Fatalf("Total physical bytes reported zero") 31 | } 32 | if tub == 0 { 33 | t.Fatalf("Total usable bytes reported zero") 34 | } 35 | if tpb < tub { 36 | t.Fatalf("Total physical bytes < total usable bytes. %d < %d", 37 | tpb, tub) 38 | } 39 | 40 | sps := mem.SupportedPageSizes 41 | 42 | if sps == nil { 43 | t.Fatalf("Expected non-nil supported page sizes, but got nil") 44 | } 45 | if len(sps) == 0 { 46 | t.Fatalf("Expected >0 supported page sizes, but got 0.") 47 | } 48 | 49 | if mem.DefaultHugePageSize == 0 { 50 | t.Fatalf("Expected >0 default hugepagesize, but got 0") 51 | } 52 | 53 | if len(sps) != len(mem.HugePageAmountsBySize) { 54 | t.Fatalf("Expected %d hugepages, but got %d", len(sps), len(mem.HugePageAmountsBySize)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/memory/memory_windows.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package memory 8 | 9 | import ( 10 | "github.com/StackExchange/wmi" 11 | 12 | "github.com/jaypipes/ghw/pkg/unitutil" 13 | ) 14 | 15 | const wqlOperatingSystem = "SELECT TotalVisibleMemorySize FROM Win32_OperatingSystem" 16 | 17 | type win32OperatingSystem struct { 18 | TotalVisibleMemorySize *uint64 19 | } 20 | 21 | const wqlPhysicalMemory = "SELECT BankLabel, Capacity, DataWidth, Description, DeviceLocator, Manufacturer, Model, Name, PartNumber, PositionInRow, SerialNumber, Speed, Tag, TotalWidth FROM Win32_PhysicalMemory" 22 | 23 | type win32PhysicalMemory struct { 24 | BankLabel *string 25 | Capacity *uint64 26 | DataWidth *uint16 27 | Description *string 28 | DeviceLocator *string 29 | Manufacturer *string 30 | Model *string 31 | Name *string 32 | PartNumber *string 33 | PositionInRow *uint32 34 | SerialNumber *string 35 | Speed *uint32 36 | Tag *string 37 | TotalWidth *uint16 38 | } 39 | 40 | func (i *Info) load() error { 41 | // Getting info from WMI 42 | var win32OSDescriptions []win32OperatingSystem 43 | if err := wmi.Query(wqlOperatingSystem, &win32OSDescriptions); err != nil { 44 | return err 45 | } 46 | var win32MemDescriptions []win32PhysicalMemory 47 | if err := wmi.Query(wqlPhysicalMemory, &win32MemDescriptions); err != nil { 48 | return err 49 | } 50 | // We calculate total physical memory size by summing the DIMM sizes 51 | var totalPhysicalBytes uint64 52 | i.Modules = make([]*Module, 0, len(win32MemDescriptions)) 53 | for _, description := range win32MemDescriptions { 54 | totalPhysicalBytes += *description.Capacity 55 | i.Modules = append(i.Modules, &Module{ 56 | Label: *description.BankLabel, 57 | Location: *description.DeviceLocator, 58 | SerialNumber: *description.SerialNumber, 59 | SizeBytes: int64(*description.Capacity), 60 | Vendor: *description.Manufacturer, 61 | }) 62 | } 63 | var totalUsableBytes uint64 64 | for _, description := range win32OSDescriptions { 65 | // TotalVisibleMemorySize is the amount of memory available for us by 66 | // the operating system **in Kilobytes** 67 | totalUsableBytes += *description.TotalVisibleMemorySize * uint64(unitutil.KB) 68 | } 69 | i.TotalUsableBytes = int64(totalUsableBytes) 70 | i.TotalPhysicalBytes = int64(totalPhysicalBytes) 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/net/net.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package net 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/jaypipes/ghw/pkg/context" 13 | "github.com/jaypipes/ghw/pkg/marshal" 14 | "github.com/jaypipes/ghw/pkg/option" 15 | ) 16 | 17 | // NICCapability is a feature/capability of a Network Interface Controller 18 | // (NIC) 19 | type NICCapability struct { 20 | // Name is the string name for the capability, e.g. 21 | // "tcp-segmentation-offload" 22 | Name string `json:"name"` 23 | // IsEnabled is true if the capability is currently enabled on the NIC, 24 | // false otherwise. 25 | IsEnabled bool `json:"is_enabled"` 26 | // CanEnable is true if the capability can be enabled on the NIC, false 27 | // otherwise. 28 | CanEnable bool `json:"can_enable"` 29 | } 30 | 31 | // NIC contains information about a single Network Interface Controller (NIC). 32 | type NIC struct { 33 | // Name is the string identifier the system gave this NIC. 34 | Name string `json:"name"` 35 | // MACAddress is the Media Access Control (MAC) address of this NIC. 36 | MACAddress string `json:"mac_address"` 37 | // DEPRECATED: Please use MACAddress instead. 38 | MacAddress string `json:"-"` 39 | // IsVirtual is true if the NIC is entirely virtual/emulated, false 40 | // otherwise. 41 | IsVirtual bool `json:"is_virtual"` 42 | // Capabilities is a slice of pointers to `NICCapability` structs 43 | // describing a feature/capability of this NIC. 44 | Capabilities []*NICCapability `json:"capabilities"` 45 | // PCIAddress is a pointer to the PCI address for this NIC, or nil if there 46 | // is no PCI address for this NIC. 47 | PCIAddress *string `json:"pci_address,omitempty"` 48 | // Speed is a string describing the link speed of this NIC, e.g. "1000Mb/s" 49 | Speed string `json:"speed"` 50 | // Duplex is a string indicating the current duplex setting of this NIC, 51 | // e.g. "Full" 52 | Duplex string `json:"duplex"` 53 | // SupportedLinkModes is a slice of strings containing the supported link 54 | // modes of this NIC, e.g. "10baseT/Half", "1000baseT/Full", etc. 55 | SupportedLinkModes []string `json:"supported_link_modes,omitempty"` 56 | // SupportedPorts is a slice of strings containing the supported physical 57 | // ports on this NIC, e.g. "Twisted Pair" 58 | SupportedPorts []string `json:"supported_ports,omitempty"` 59 | // SupportedFECModes is a slice of strings containing the supported Forward 60 | // Error Correction (FEC) modes for this NIC. 61 | SupportedFECModes []string `json:"supported_fec_modes,omitempty"` 62 | // AdvertiseLinkModes is a slice of strings containing the advertised 63 | // (during auto-negotiation) link modes of this NIC, e.g. "10baseT/Half", 64 | // "1000baseT/Full", etc. 65 | AdvertisedLinkModes []string `json:"advertised_link_modes,omitempty"` 66 | // AvertisedFECModes is a slice of strings containing the advertised 67 | // (during auto-negotiation) Forward Error Correction (FEC) modes for this 68 | // NIC. 69 | AdvertisedFECModes []string `json:"advertised_fec_modes,omitempty"` 70 | // TODO(fromani): add other hw addresses (USB) when we support them 71 | } 72 | 73 | // String returns a short string with information about the NIC capability. 74 | func (nc *NICCapability) String() string { 75 | return fmt.Sprintf( 76 | "{Name:%s IsEnabled:%t CanEnable:%t}", 77 | nc.Name, 78 | nc.IsEnabled, 79 | nc.CanEnable, 80 | ) 81 | } 82 | 83 | // String returns a short string with information about the NIC. 84 | func (n *NIC) String() string { 85 | isVirtualStr := "" 86 | if n.IsVirtual { 87 | isVirtualStr = " (virtual)" 88 | } 89 | return fmt.Sprintf( 90 | "%s%s", 91 | n.Name, 92 | isVirtualStr, 93 | ) 94 | } 95 | 96 | // Info describes all network interface controllers (NICs) in the host system. 97 | type Info struct { 98 | ctx *context.Context 99 | // NICs is a slice of pointers to `NIC` structs describing the network 100 | // interface controllers (NICs) on the host system. 101 | NICs []*NIC `json:"nics"` 102 | } 103 | 104 | // New returns a pointer to an Info struct that contains information about the 105 | // network interface controllers (NICs) on the host system 106 | func New(opts ...*option.Option) (*Info, error) { 107 | ctx := context.New(opts...) 108 | info := &Info{ctx: ctx} 109 | if err := ctx.Do(info.load); err != nil { 110 | return nil, err 111 | } 112 | return info, nil 113 | } 114 | 115 | // String returns a short string with information about the networking on the 116 | // host system. 117 | func (i *Info) String() string { 118 | return fmt.Sprintf( 119 | "net (%d NICs)", 120 | len(i.NICs), 121 | ) 122 | } 123 | 124 | // simple private struct used to encapsulate net information in a 125 | // top-level "net" YAML/JSON map/object key 126 | type netPrinter struct { 127 | Info *Info `json:"network"` 128 | } 129 | 130 | // YAMLString returns a string with the net information formatted as YAML 131 | // under a top-level "net:" key 132 | func (i *Info) YAMLString() string { 133 | return marshal.SafeYAML(i.ctx, netPrinter{i}) 134 | } 135 | 136 | // JSONString returns a string with the net information formatted as JSON 137 | // under a top-level "net:" key 138 | func (i *Info) JSONString(indent bool) string { 139 | return marshal.SafeJSON(i.ctx, netPrinter{i}, indent) 140 | } 141 | -------------------------------------------------------------------------------- /pkg/net/net_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | //go:build linux 8 | // +build linux 9 | 10 | package net 11 | 12 | import ( 13 | "bytes" 14 | "os" 15 | "reflect" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | func TestParseEthtoolFeature(t *testing.T) { 21 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_NET"); ok { 22 | t.Skip("Skipping network tests.") 23 | } 24 | 25 | tests := []struct { 26 | line string 27 | expected *NICCapability 28 | }{ 29 | { 30 | line: "scatter-gather: off", 31 | expected: &NICCapability{ 32 | Name: "scatter-gather", 33 | IsEnabled: false, 34 | CanEnable: true, 35 | }, 36 | }, 37 | { 38 | line: "scatter-gather: on", 39 | expected: &NICCapability{ 40 | Name: "scatter-gather", 41 | IsEnabled: true, 42 | CanEnable: true, 43 | }, 44 | }, 45 | { 46 | line: "scatter-gather: off [fixed]", 47 | expected: &NICCapability{ 48 | Name: "scatter-gather", 49 | IsEnabled: false, 50 | CanEnable: false, 51 | }, 52 | }, 53 | } 54 | 55 | for x, test := range tests { 56 | actual := netParseEthtoolFeature(test.line) 57 | if !reflect.DeepEqual(test.expected, actual) { 58 | t.Fatalf("In test %d, expected %v == %v", x, test.expected, actual) 59 | } 60 | } 61 | } 62 | 63 | func TestParseNicAttrEthtool(t *testing.T) { 64 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_NET"); ok { 65 | t.Skip("Skipping network tests.") 66 | } 67 | 68 | tests := []struct { 69 | input string 70 | expected *NIC 71 | }{ 72 | { 73 | input: `Settings for eth0: 74 | Supported ports: [ TP ] 75 | Supported link modes: 10baseT/Half 10baseT/Full 76 | 100baseT/Half 100baseT/Full 77 | 1000baseT/Full 78 | Supported pause frame use: No 79 | Supports auto-negotiation: Yes 80 | Supported FEC modes: Not reported 81 | Advertised link modes: 10baseT/Half 10baseT/Full 82 | 100baseT/Half 100baseT/Full 83 | 1000baseT/Full 84 | Advertised pause frame use: No 85 | Advertised auto-negotiation: Yes 86 | Advertised FEC modes: Not reported 87 | Speed: 1000Mb/s 88 | Duplex: Full 89 | Auto-negotiation: on 90 | Port: Twisted Pair 91 | PHYAD: 1 92 | Transceiver: internal 93 | MDI-X: off (auto) 94 | Supports Wake-on: pumbg 95 | Wake-on: d 96 | Current message level: 0x00000007 (7) 97 | drv probe link 98 | Link detected: yes 99 | `, 100 | expected: &NIC{ 101 | Speed: "1000Mb/s", 102 | Duplex: "Full", 103 | SupportedPorts: []string{"TP"}, 104 | AdvertisedLinkModes: []string{ 105 | "10baseT/Half", 106 | "10baseT/Full", 107 | "100baseT/Half", 108 | "100baseT/Full", 109 | "1000baseT/Full", 110 | }, 111 | SupportedLinkModes: []string{ 112 | "10baseT/Half", 113 | "10baseT/Full", 114 | "100baseT/Half", 115 | "100baseT/Full", 116 | "1000baseT/Full", 117 | }, 118 | Capabilities: []*NICCapability{ 119 | { 120 | Name: "auto-negotiation", 121 | IsEnabled: true, 122 | CanEnable: true, 123 | }, 124 | { 125 | Name: "pause-frame-use", 126 | IsEnabled: false, 127 | CanEnable: false, 128 | }, 129 | }, 130 | }, 131 | }, 132 | } 133 | 134 | for x, test := range tests { 135 | m := parseNicAttrEthtool(bytes.NewBufferString(test.input)) 136 | actual := &NIC{} 137 | actual.Speed = strings.Join(m["Speed"], "") 138 | actual.Duplex = strings.Join(m["Duplex"], "") 139 | actual.SupportedLinkModes = m["Supported link modes"] 140 | actual.SupportedPorts = m["Supported ports"] 141 | actual.SupportedFECModes = m["Supported FEC modes"] 142 | actual.AdvertisedLinkModes = m["Advertised link modes"] 143 | actual.AdvertisedFECModes = m["Advertised FEC modes"] 144 | actual.Capabilities = append(actual.Capabilities, autoNegCap(m)) 145 | actual.Capabilities = append(actual.Capabilities, pauseFrameUseCap(m)) 146 | if !reflect.DeepEqual(test.expected, actual) { 147 | t.Fatalf("In test %d\nExpected:\n%+v\nActual:\n%+v\n", x, *test.expected, *actual) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /pkg/net/net_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package net 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("netFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/net/net_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package net_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/net" 14 | ) 15 | 16 | func TestNet(t *testing.T) { 17 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_NET"); ok { 18 | t.Skip("Skipping network tests.") 19 | } 20 | 21 | info, err := net.New() 22 | 23 | if err != nil { 24 | t.Fatalf("Expected nil err, but got %v", err) 25 | } 26 | if info == nil { 27 | t.Fatalf("Expected non-nil NetworkInfo, but got nil") 28 | } 29 | 30 | if len(info.NICs) > 0 { 31 | for _, n := range info.NICs { 32 | if n.Name == "" { 33 | t.Fatalf("Expected a NIC name but got \"\".") 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/net/net_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package net 7 | 8 | import ( 9 | "strings" 10 | 11 | "github.com/StackExchange/wmi" 12 | ) 13 | 14 | const wqlNetworkAdapter = "SELECT Description, DeviceID, Index, InterfaceIndex, MACAddress, Manufacturer, Name, NetConnectionID, ProductName, ServiceName, PhysicalAdapter FROM Win32_NetworkAdapter" 15 | 16 | type win32NetworkAdapter struct { 17 | Description *string 18 | DeviceID *string 19 | Index *uint32 20 | InterfaceIndex *uint32 21 | MACAddress *string 22 | Manufacturer *string 23 | Name *string 24 | NetConnectionID *string 25 | ProductName *string 26 | ServiceName *string 27 | PhysicalAdapter *bool 28 | } 29 | 30 | func (i *Info) load() error { 31 | // Getting info from WMI 32 | var win32NetDescriptions []win32NetworkAdapter 33 | if err := wmi.Query(wqlNetworkAdapter, &win32NetDescriptions); err != nil { 34 | return err 35 | } 36 | 37 | i.NICs = nics(win32NetDescriptions) 38 | return nil 39 | } 40 | 41 | func nics(win32NetDescriptions []win32NetworkAdapter) []*NIC { 42 | // Converting into standard structures 43 | nics := make([]*NIC, 0) 44 | for _, nicDescription := range win32NetDescriptions { 45 | nic := &NIC{ 46 | Name: netDeviceName(nicDescription), 47 | MacAddress: *nicDescription.MACAddress, 48 | MACAddress: *nicDescription.MACAddress, 49 | IsVirtual: netIsVirtual(nicDescription), 50 | Capabilities: []*NICCapability{}, 51 | } 52 | nics = append(nics, nic) 53 | } 54 | 55 | return nics 56 | } 57 | 58 | func netDeviceName(description win32NetworkAdapter) string { 59 | var name string 60 | if strings.TrimSpace(*description.NetConnectionID) != "" { 61 | name = *description.NetConnectionID + " - " + *description.Description 62 | } else { 63 | name = *description.Description 64 | } 65 | return name 66 | } 67 | 68 | func netIsVirtual(description win32NetworkAdapter) bool { 69 | if description.PhysicalAdapter == nil { 70 | return false 71 | } 72 | 73 | return !(*description.PhysicalAdapter) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/pci/address/address.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package address 8 | 9 | import ( 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | regexAddress *regexp.Regexp = regexp.MustCompile( 16 | `^((1?[0-9a-f]{0,4}):)?([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]{1})$`, 17 | ) 18 | ) 19 | 20 | // Address contains the components of a PCI Address 21 | type Address struct { 22 | Domain string 23 | Bus string 24 | Device string 25 | Function string 26 | } 27 | 28 | // String() returns the canonical [D]BDF representation of this Address 29 | func (addr *Address) String() string { 30 | return addr.Domain + ":" + addr.Bus + ":" + addr.Device + "." + addr.Function 31 | } 32 | 33 | // FromString returns [Address] from an address string in either 34 | // $BUS:$DEVICE.$FUNCTION (BDF) format or a full PCI address that 35 | // includes the domain: $DOMAIN:$BUS:$DEVICE.$FUNCTION. 36 | // 37 | // If the address string isn't a valid PCI address, then nil is returned. 38 | func FromString(address string) *Address { 39 | addrLowered := strings.ToLower(address) 40 | matches := regexAddress.FindStringSubmatch(addrLowered) 41 | if len(matches) == 6 { 42 | dom := "0000" 43 | if matches[1] != "" { 44 | dom = matches[2] 45 | } 46 | return &Address{ 47 | Domain: dom, 48 | Bus: matches[3], 49 | Device: matches[4], 50 | Function: matches[5], 51 | } 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/pci/address/address_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package address_test 8 | 9 | import ( 10 | "reflect" 11 | "strings" 12 | "testing" 13 | 14 | pciaddr "github.com/jaypipes/ghw/pkg/pci/address" 15 | ) 16 | 17 | func TestPCIAddressFromString(t *testing.T) { 18 | 19 | tests := []struct { 20 | addrStr string 21 | expected *pciaddr.Address 22 | // AddressFromString is more flexible than String() and wants 23 | // to accept addresses not in full canonical form, as long as 24 | // it can do the right thing - e.g. a sane default Domain exists. 25 | // Thus we need to sometimes skip the Address -> string check. 26 | skipStringTest bool 27 | }{ 28 | { 29 | addrStr: "00:00.0", 30 | expected: &pciaddr.Address{ 31 | Domain: "0000", 32 | Bus: "00", 33 | Device: "00", 34 | Function: "0", 35 | }, 36 | skipStringTest: true, 37 | }, 38 | { 39 | addrStr: "0000:00:00.0", 40 | expected: &pciaddr.Address{ 41 | Domain: "0000", 42 | Bus: "00", 43 | Device: "00", 44 | Function: "0", 45 | }, 46 | }, 47 | { 48 | addrStr: "0000:03:00.0", 49 | expected: &pciaddr.Address{ 50 | Domain: "0000", 51 | Bus: "03", 52 | Device: "00", 53 | Function: "0", 54 | }, 55 | }, 56 | { 57 | addrStr: "0000:03:00.A", 58 | expected: &pciaddr.Address{ 59 | Domain: "0000", 60 | Bus: "03", 61 | Device: "00", 62 | Function: "a", 63 | }, 64 | }, 65 | { 66 | // PCI-X / PCI Express extensions may use 5-digit domain 67 | addrStr: "10000:03:00.A", 68 | expected: &pciaddr.Address{ 69 | Domain: "10000", 70 | Bus: "03", 71 | Device: "00", 72 | Function: "a", 73 | }, 74 | }, 75 | } 76 | for x, test := range tests { 77 | got := pciaddr.FromString(test.addrStr) 78 | if !reflect.DeepEqual(got, test.expected) { 79 | t.Fatalf("Test #%d failed. Expected %v but got %v", x, test.expected, got) 80 | } 81 | 82 | if test.skipStringTest { 83 | continue 84 | } 85 | 86 | addrStr := got.String() 87 | // addresses are case insensitive 88 | if !strings.EqualFold(addrStr, test.addrStr) { 89 | t.Fatalf("Test #%d failed. Expected %q but got %q (case insensitive match)", x, test.addrStr, addrStr) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /pkg/pci/pci_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package pci 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("pciFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | 21 | // GetDevice returns a pointer to a Device struct that describes the PCI 22 | // device at the requested address. If no such device could be found, returns 23 | // nil 24 | func (info *Info) GetDevice(address string) *Device { 25 | return nil 26 | } 27 | 28 | // ListDevices returns a list of pointers to Device structs present on the 29 | // host system 30 | func (info *Info) ListDevices() []*Device { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/pci/pci_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package pci_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/pci" 14 | ) 15 | 16 | func TestPCI(t *testing.T) { 17 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_PCI"); ok { 18 | t.Skip("Skipping PCI tests.") 19 | } 20 | 21 | info, err := pci.New() 22 | if err != nil { 23 | t.Fatalf("Expected no error creating PciInfo, but got %v", err) 24 | } 25 | 26 | // Since we can't count on a specific device being present on the machine 27 | // being tested (and we haven't built in fixtures/mocks for things yet) 28 | // about all we can do is verify that the returned list of pointers to 29 | // PCIDevice structs is non-empty 30 | devs := info.Devices 31 | if len(devs) == 0 { 32 | t.Fatalf("Expected to find >0 PCI devices in PCIInfo.Devices but got 0.") 33 | } 34 | 35 | // Ensure that the data fields are at least populated, even if we don't yet 36 | // check for data accuracy 37 | for _, dev := range devs { 38 | if dev.Class == nil { 39 | t.Fatalf("Expected device class for %s to be non-nil", dev.Address) 40 | } 41 | if dev.Product == nil { 42 | t.Fatalf("Expected device product for %s to be non-nil", dev.Address) 43 | } 44 | if dev.Vendor == nil { 45 | t.Fatalf("Expected device vendor for %s to be non-nil", dev.Address) 46 | } 47 | if dev.Revision == "" { 48 | t.Fatalf("Expected device revision for %s to be non-empty", dev.Address) 49 | } 50 | if dev.Subclass == nil { 51 | t.Fatalf("Expected device subclass for %s to be non-nil", dev.Address) 52 | } 53 | if dev.Subsystem == nil { 54 | t.Fatalf("Expected device subsystem for %s to be non-nil", dev.Address) 55 | } 56 | if dev.ProgrammingInterface == nil { 57 | t.Fatalf("Expected device programming interface for %s to be non-nil", dev.Address) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/product/product.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package product 8 | 9 | import ( 10 | "github.com/jaypipes/ghw/pkg/context" 11 | "github.com/jaypipes/ghw/pkg/marshal" 12 | "github.com/jaypipes/ghw/pkg/option" 13 | "github.com/jaypipes/ghw/pkg/util" 14 | ) 15 | 16 | // Info defines product information 17 | type Info struct { 18 | ctx *context.Context 19 | Family string `json:"family"` 20 | Name string `json:"name"` 21 | Vendor string `json:"vendor"` 22 | SerialNumber string `json:"serial_number"` 23 | UUID string `json:"uuid"` 24 | SKU string `json:"sku"` 25 | Version string `json:"version"` 26 | } 27 | 28 | func (i *Info) String() string { 29 | familyStr := "" 30 | if i.Family != "" { 31 | familyStr = " family=" + i.Family 32 | } 33 | nameStr := "" 34 | if i.Name != "" { 35 | nameStr = " name=" + i.Name 36 | } 37 | vendorStr := "" 38 | if i.Vendor != "" { 39 | vendorStr = " vendor=" + i.Vendor 40 | } 41 | serialStr := "" 42 | if i.SerialNumber != "" && i.SerialNumber != util.UNKNOWN { 43 | serialStr = " serial=" + i.SerialNumber 44 | } 45 | uuidStr := "" 46 | if i.UUID != "" && i.UUID != util.UNKNOWN { 47 | uuidStr = " uuid=" + i.UUID 48 | } 49 | skuStr := "" 50 | if i.SKU != "" { 51 | skuStr = " sku=" + i.SKU 52 | } 53 | versionStr := "" 54 | if i.Version != "" { 55 | versionStr = " version=" + i.Version 56 | } 57 | 58 | return "product" + util.ConcatStrings( 59 | familyStr, 60 | nameStr, 61 | vendorStr, 62 | serialStr, 63 | uuidStr, 64 | skuStr, 65 | versionStr, 66 | ) 67 | } 68 | 69 | // New returns a pointer to a Info struct containing information 70 | // about the host's product 71 | func New(opts ...*option.Option) (*Info, error) { 72 | ctx := context.New(opts...) 73 | info := &Info{ctx: ctx} 74 | if err := ctx.Do(info.load); err != nil { 75 | return nil, err 76 | } 77 | return info, nil 78 | } 79 | 80 | // simple private struct used to encapsulate product information in a top-level 81 | // "product" YAML/JSON map/object key 82 | type productPrinter struct { 83 | Info *Info `json:"product"` 84 | } 85 | 86 | // YAMLString returns a string with the product information formatted as YAML 87 | // under a top-level "dmi:" key 88 | func (info *Info) YAMLString() string { 89 | return marshal.SafeYAML(info.ctx, productPrinter{info}) 90 | } 91 | 92 | // JSONString returns a string with the product information formatted as JSON 93 | // under a top-level "product:" key 94 | func (info *Info) JSONString(indent bool) string { 95 | return marshal.SafeJSON(info.ctx, productPrinter{info}, indent) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/product/product_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package product 7 | 8 | import ( 9 | "github.com/jaypipes/ghw/pkg/linuxdmi" 10 | ) 11 | 12 | func (i *Info) load() error { 13 | 14 | i.Family = linuxdmi.Item(i.ctx, "product_family") 15 | i.Name = linuxdmi.Item(i.ctx, "product_name") 16 | i.Vendor = linuxdmi.Item(i.ctx, "sys_vendor") 17 | i.SerialNumber = linuxdmi.Item(i.ctx, "product_serial") 18 | i.UUID = linuxdmi.Item(i.ctx, "product_uuid") 19 | i.SKU = linuxdmi.Item(i.ctx, "product_sku") 20 | i.Version = linuxdmi.Item(i.ctx, "product_version") 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/product/product_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package product 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("productFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/product/product_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package product 7 | 8 | import ( 9 | "github.com/StackExchange/wmi" 10 | 11 | "github.com/jaypipes/ghw/pkg/util" 12 | ) 13 | 14 | const wqlProduct = "SELECT Caption, Description, IdentifyingNumber, Name, SKUNumber, Vendor, Version, UUID FROM Win32_ComputerSystemProduct" 15 | 16 | type win32Product struct { 17 | Caption *string 18 | Description *string 19 | IdentifyingNumber *string 20 | Name *string 21 | SKUNumber *string 22 | Vendor *string 23 | Version *string 24 | UUID *string 25 | } 26 | 27 | func (i *Info) load() error { 28 | // Getting data from WMI 29 | var win32ProductDescriptions []win32Product 30 | // Assuming the first product is the host... 31 | if err := wmi.Query(wqlProduct, &win32ProductDescriptions); err != nil { 32 | return err 33 | } 34 | if len(win32ProductDescriptions) > 0 { 35 | i.Family = util.UNKNOWN 36 | i.Name = *win32ProductDescriptions[0].Name 37 | i.Vendor = *win32ProductDescriptions[0].Vendor 38 | i.SerialNumber = *win32ProductDescriptions[0].IdentifyingNumber 39 | i.UUID = *win32ProductDescriptions[0].UUID 40 | i.SKU = *win32ProductDescriptions[0].SKUNumber 41 | i.Version = *win32ProductDescriptions[0].Version 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/snapshot/clonetree_gpu_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | import ( 10 | "strings" 11 | ) 12 | 13 | // ExpectedCloneGPUContent returns a slice of strings pertaining to the GPU devices ghw 14 | // cares about. We cannot use a static list because we want to grab only the first cardX data 15 | // (see comment in pkg/gpu/gpu_linux.go) 16 | // Additionally, we want to make sure to clone the backing device data. 17 | func ExpectedCloneGPUContent() []string { 18 | cardEntries := []string{ 19 | "device", 20 | } 21 | 22 | filterName := func(cardName string) bool { 23 | if !strings.HasPrefix(cardName, "card") { 24 | return false 25 | } 26 | if strings.ContainsRune(cardName, '-') { 27 | return false 28 | } 29 | return true 30 | } 31 | 32 | return cloneContentByClass("drm", cardEntries, filterName, filterNone) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/snapshot/clonetree_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | import ( 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | func setupScratchDir(scratchDir string) error { 15 | var createPaths = []string{ 16 | "sys/block", 17 | } 18 | 19 | for _, path := range createPaths { 20 | if err := os.MkdirAll(filepath.Join(scratchDir, path), os.ModePerm); err != nil { 21 | return err 22 | } 23 | } 24 | 25 | return createBlockDevices(scratchDir) 26 | } 27 | 28 | // ExpectedCloneStaticContent return a slice of glob patterns which represent the pseudofiles 29 | // ghw cares about, and which are independent from host specific topology or configuration, 30 | // thus are safely represented by a static slice - e.g. they don't need to be discovered at runtime. 31 | func ExpectedCloneStaticContent() []string { 32 | return []string{ 33 | "/proc/cpuinfo", 34 | "/proc/meminfo", 35 | "/proc/self/mounts", 36 | "/sys/devices/system/cpu/cpu*/cache/index*/*", 37 | "/sys/devices/system/cpu/cpu*/topology/*", 38 | "/sys/devices/system/memory/block_size_bytes", 39 | "/sys/devices/system/memory/memory*/online", 40 | "/sys/devices/system/memory/memory*/state", 41 | "/sys/devices/system/node/has_*", 42 | "/sys/devices/system/node/online", 43 | "/sys/devices/system/node/possible", 44 | "/sys/devices/system/node/node*/cpu*", 45 | "/sys/devices/system/node/node*/cpu*/online", 46 | "/sys/devices/system/node/node*/distance", 47 | "/sys/devices/system/node/node*/meminfo", 48 | "/sys/devices/system/node/node*/memory*", 49 | "/sys/devices/system/node/node*/hugepages/hugepages-*/*", 50 | } 51 | } 52 | 53 | type filterFunc func(string) bool 54 | 55 | // cloneContentByClass copies all the content related to a given device class 56 | // (devClass), possibly filtering out devices whose name does NOT pass a 57 | // filter (filterName). Each entry in `/sys/class/$CLASS` is actually a 58 | // symbolic link. We can filter out entries depending on the link target. 59 | // Each filter is a simple function which takes the entry name or the link 60 | // target and must return true if the entry should be collected, false 61 | // otherwise. Last, explicitly collect a list of attributes for each entry, 62 | // given as list of glob patterns as `subEntries`. 63 | // Return the final list of glob patterns to be collected. 64 | func cloneContentByClass(devClass string, subEntries []string, filterName filterFunc, filterLink filterFunc) []string { 65 | var fileSpecs []string 66 | 67 | // warning: don't use the context package here, this means not even the linuxpath package. 68 | // TODO(fromani) remove the path duplication 69 | sysClass := filepath.Join("sys", "class", devClass) 70 | entries, err := os.ReadDir(sysClass) 71 | if err != nil { 72 | // we should not import context, hence we can't Warn() 73 | return fileSpecs 74 | } 75 | for _, entry := range entries { 76 | devName := entry.Name() 77 | 78 | if !filterName(devName) { 79 | continue 80 | } 81 | 82 | devPath := filepath.Join(sysClass, devName) 83 | dest, err := os.Readlink(devPath) 84 | if err != nil { 85 | continue 86 | } 87 | 88 | if !filterLink(dest) { 89 | continue 90 | } 91 | 92 | // so, first copy the symlink itself 93 | fileSpecs = append(fileSpecs, devPath) 94 | // now we have to clone the content of the actual entry 95 | // related (and found into a subdir of) the backing hardware 96 | // device 97 | devData := filepath.Clean(filepath.Join(sysClass, dest)) 98 | for _, subEntry := range subEntries { 99 | fileSpecs = append(fileSpecs, filepath.Join(devData, subEntry)) 100 | } 101 | } 102 | 103 | return fileSpecs 104 | } 105 | 106 | // filterNone allows all content, filtering out none of it 107 | func filterNone(_ string) bool { 108 | return true 109 | } 110 | -------------------------------------------------------------------------------- /pkg/snapshot/clonetree_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot_test 8 | 9 | import ( 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "sort" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/jaypipes/ghw/pkg/snapshot" 18 | ) 19 | 20 | // NOTE: we intentionally use `os.RemoveAll` - not `snapshot.Cleanup` because we 21 | // want to make sure we never leak directories. `snapshot.Cleanup` is used and 22 | // tested explicitly in `unpack_test.go`. 23 | 24 | // nolint: gocyclo 25 | func TestCloneTree(t *testing.T) { 26 | root, err := snapshot.Unpack(testDataSnapshot) 27 | if err != nil { 28 | t.Fatalf("Expected nil err, but got %v", err) 29 | } 30 | defer os.RemoveAll(root) 31 | 32 | cloneRoot, err := os.MkdirTemp("", "ghw-test-clonetree-*") 33 | if err != nil { 34 | t.Fatalf("Expected nil err, but got %v", err) 35 | } 36 | defer os.RemoveAll(cloneRoot) 37 | 38 | fileSpecs := []string{ 39 | filepath.Join(root, "ghw-test-*"), 40 | filepath.Join(root, "different/subtree/ghw*"), 41 | filepath.Join(root, "nested/ghw-test*"), 42 | filepath.Join(root, "nested/tree/of/subdirectories/forming/deep/unbalanced/tree/ghw-test-3"), 43 | } 44 | err = snapshot.CopyFilesInto(fileSpecs, cloneRoot, nil) 45 | if err != nil { 46 | t.Fatalf("Expected nil err, but got %v", err) 47 | } 48 | 49 | origContent, err := scanTree(root, "", []string{""}) 50 | if err != nil { 51 | t.Fatalf("Expected nil err, but got %v", err) 52 | } 53 | sort.Strings(origContent) 54 | 55 | cloneContent, err := scanTree(cloneRoot, cloneRoot, []string{"", "/tmp"}) 56 | if err != nil { 57 | t.Fatalf("Expected nil err, but got %v", err) 58 | } 59 | sort.Strings(cloneContent) 60 | 61 | if len(origContent) != len(cloneContent) { 62 | t.Fatalf("Expected tree size %d got %d", len(origContent), len(cloneContent)) 63 | } 64 | if !reflect.DeepEqual(origContent, cloneContent) { 65 | t.Fatalf("subtree content different expected %#v got %#v", origContent, cloneContent) 66 | } 67 | } 68 | 69 | // nolint: gocyclo 70 | func TestCloneSystemTree(t *testing.T) { 71 | // ok, this is tricky. Validating a cloned tree is a complex business. 72 | // We do the bare minimum here to check that both the CloneTree and the ValidateClonedTree did something 73 | // sensible. To really do a meaningful test we need a more advanced functional test, starting with from 74 | // a ghw snapshot. 75 | 76 | cloneRoot, err := os.MkdirTemp("", "ghw-test-clonetree-*") 77 | if err != nil { 78 | t.Fatalf("Expected nil err, but got %v", err) 79 | } 80 | defer os.RemoveAll(cloneRoot) 81 | 82 | err = snapshot.CloneTreeInto(cloneRoot) 83 | if err != nil { 84 | t.Fatalf("Expected nil err, but got %v", err) 85 | } 86 | 87 | missing, err := snapshot.ValidateClonedTree(snapshot.ExpectedCloneContent(), cloneRoot) 88 | if err != nil { 89 | t.Fatalf("Expected nil err, but got %v", err) 90 | } 91 | 92 | if len(missing) > 0 && areEntriesOnSysfs(missing) { 93 | t.Fatalf("Expected content %#v missing into the cloned tree %q", missing, cloneRoot) 94 | } 95 | } 96 | 97 | func areEntriesOnSysfs(sysfsEntries []string) bool { 98 | // turns out some ISA bridges do not actually expose the driver entry. The reason is not clear. 99 | // So let's check if we actually have the entry we were looking for on sysfs. If so, we 100 | // actually failed to clone an entry, and we must fail the test. Otherwise we carry on. 101 | for _, sysfsEntry := range sysfsEntries { 102 | if _, err := os.Lstat(sysfsEntry); err == nil { 103 | return true 104 | } 105 | } 106 | return false 107 | } 108 | 109 | func scanTree(root, prefix string, excludeList []string) ([]string, error) { 110 | var contents []string 111 | return contents, filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 112 | if err != nil { 113 | return err 114 | } 115 | if fp := strings.TrimPrefix(path, prefix); !includedInto(fp, excludeList) { 116 | contents = append(contents, fp) 117 | } 118 | return nil 119 | }) 120 | } 121 | 122 | func includedInto(s string, items []string) bool { 123 | if items == nil { 124 | return false 125 | } 126 | for _, item := range items { 127 | if s == item { 128 | return true 129 | } 130 | } 131 | return false 132 | } 133 | -------------------------------------------------------------------------------- /pkg/snapshot/clonetree_net_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | import ( 10 | "strings" 11 | ) 12 | 13 | // ExpectedCloneNetContent returns a slice of strings pertaning to the network interfaces ghw 14 | // cares about. We cannot use a static list because we want to filter away the virtual devices, 15 | // which ghw doesn't concern itself about. So we need to do some runtime discovery. 16 | // Additionally, we want to make sure to clone the backing device data. 17 | func ExpectedCloneNetContent() []string { 18 | ifaceEntries := []string{ 19 | "addr_assign_type", 20 | // intentionally avoid to clone "address" to avoid to leak any host-idenfifiable data. 21 | } 22 | 23 | filterLink := func(linkDest string) bool { 24 | return !strings.Contains(linkDest, "devices/virtual/net") 25 | } 26 | 27 | return cloneContentByClass("net", ifaceEntries, filterNone, filterLink) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/snapshot/clonetree_pci_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | 14 | pciaddr "github.com/jaypipes/ghw/pkg/pci/address" 15 | ) 16 | 17 | const ( 18 | // root directory: entry point to start scanning the PCI forest 19 | // warning: don't use the context package here, this means not even the linuxpath package. 20 | // TODO(fromani) remove the path duplication 21 | sysBusPCIDir = "/sys/bus/pci/devices" 22 | ) 23 | 24 | // ExpectedClonePCIContent return a slice of glob patterns which represent the pseudofiles 25 | // ghw cares about, pertaining to PCI devices only. 26 | // Beware: the content is host-specific, because the PCI topology is host-dependent and unpredictable. 27 | func ExpectedClonePCIContent() []string { 28 | fileSpecs := []string{ 29 | "/sys/bus/pci/drivers/*", 30 | } 31 | pciRoots := []string{ 32 | sysBusPCIDir, 33 | } 34 | for { 35 | if len(pciRoots) == 0 { 36 | break 37 | } 38 | pciRoot := pciRoots[0] 39 | pciRoots = pciRoots[1:] 40 | specs, roots := scanPCIDeviceRoot(pciRoot) 41 | pciRoots = append(pciRoots, roots...) 42 | fileSpecs = append(fileSpecs, specs...) 43 | } 44 | return fileSpecs 45 | } 46 | 47 | // scanPCIDeviceRoot reports a slice of glob patterns which represent the pseudofiles 48 | // ghw cares about pertaining to all the PCI devices connected to the bus connected from the 49 | // given root; usually (but not always) a CPU packages has 1+ PCI(e) roots, forming the first 50 | // level; more PCI bridges are (usually) attached to this level, creating deep nested trees. 51 | // hence we need to scan all possible roots, to make sure not to miss important devices. 52 | // 53 | // note about notifying errors. This function and its helper functions do use trace() everywhere 54 | // to report recoverable errors, even though it would have been appropriate to use Warn(). 55 | // This is unfortunate, and again a byproduct of the fact we cannot use context.Context to avoid 56 | // circular dependencies. 57 | // TODO(fromani): switch to Warn() as soon as we figure out how to break this circular dep. 58 | func scanPCIDeviceRoot(root string) (fileSpecs []string, pciRoots []string) { 59 | trace("scanning PCI device root %q\n", root) 60 | 61 | perDevEntries := []string{ 62 | "class", 63 | "device", 64 | "driver", 65 | "irq", 66 | "local_cpulist", 67 | "modalias", 68 | "numa_node", 69 | "revision", 70 | "vendor", 71 | } 72 | entries, err := os.ReadDir(root) 73 | if err != nil { 74 | return []string{}, []string{} 75 | } 76 | for _, entry := range entries { 77 | entryName := entry.Name() 78 | if addr := pciaddr.FromString(entryName); addr == nil { 79 | // doesn't look like a entry we care about 80 | // This is by far and large the most likely path 81 | // hence we should NOT trace/warn here. 82 | continue 83 | } 84 | 85 | entryPath := filepath.Join(root, entryName) 86 | pciEntry, err := findPCIEntryFromPath(root, entryName) 87 | if err != nil { 88 | trace("error scanning %q: %v", entryName, err) 89 | continue 90 | } 91 | 92 | trace("PCI entry is %q\n", pciEntry) 93 | fileSpecs = append(fileSpecs, entryPath) 94 | for _, perNetEntry := range perDevEntries { 95 | fileSpecs = append(fileSpecs, filepath.Join(pciEntry, perNetEntry)) 96 | } 97 | 98 | if isPCIBridge(entryPath) { 99 | trace("adding new PCI root %q\n", entryName) 100 | pciRoots = append(pciRoots, pciEntry) 101 | } 102 | } 103 | return fileSpecs, pciRoots 104 | } 105 | 106 | func findPCIEntryFromPath(root, entryName string) (string, error) { 107 | entryPath := filepath.Join(root, entryName) 108 | fi, err := os.Lstat(entryPath) 109 | if err != nil { 110 | return "", fmt.Errorf("stat(%s) failed: %v\n", entryPath, err) 111 | } 112 | if fi.Mode()&os.ModeSymlink == 0 { 113 | // regular file, nothing to resolve 114 | return entryPath, nil 115 | } 116 | // resolve symlink 117 | target, err := os.Readlink(entryPath) 118 | trace("entry %q is symlink resolved to %q\n", entryPath, target) 119 | if err != nil { 120 | return "", fmt.Errorf("readlink(%s) failed: %v - skipped\n", entryPath, err) 121 | } 122 | return filepath.Clean(filepath.Join(root, target)), nil 123 | } 124 | 125 | func isPCIBridge(entryPath string) bool { 126 | subNodes, err := os.ReadDir(entryPath) 127 | if err != nil { 128 | // this is so unlikely we don't even return error. But we trace just in case. 129 | trace("error scanning device entry path %q: %v", entryPath, err) 130 | return false 131 | } 132 | for _, subNode := range subNodes { 133 | if !subNode.IsDir() { 134 | continue 135 | } 136 | if addr := pciaddr.FromString(subNode.Name()); addr != nil { 137 | // we got an entry in the directory pertaining to this device 138 | // which is a directory itself and it is named like a PCI address. 139 | // Hence we infer the device we are considering is a PCI bridge of sorts. 140 | // This is is indeed a bit brutal, but the only possible alternative 141 | // (besides blindly copying everything in /sys/bus/pci/devices) is 142 | // to detect the type of the device and pick only the bridges. 143 | // This approach duplicates the logic within the `pci` subkpg 144 | // - or forces us into awkward dep cycles, and has poorer forward 145 | // compatibility. 146 | return true 147 | } 148 | } 149 | return false 150 | } 151 | -------------------------------------------------------------------------------- /pkg/snapshot/clonetree_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | // 5 | // Use and distribution licensed under the Apache license version 2. 6 | // 7 | // See the COPYING file in the root project directory for full text. 8 | // 9 | 10 | package snapshot 11 | 12 | func setupScratchDir(scratchDir string) error { 13 | return nil 14 | } 15 | 16 | func ExpectedCloneStaticContent() []string { 17 | return []string{} 18 | } 19 | 20 | func ExpectedCloneGPUContent() []string { 21 | return []string{} 22 | } 23 | 24 | func ExpectedCloneNetContent() []string { 25 | return []string{} 26 | } 27 | 28 | func ExpectedClonePCIContent() []string { 29 | return []string{} 30 | } 31 | -------------------------------------------------------------------------------- /pkg/snapshot/pack.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | import ( 10 | "archive/tar" 11 | "compress/gzip" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "os" 16 | "path/filepath" 17 | ) 18 | 19 | // PackFrom creates the snapshot named `snapshotName` from the 20 | // directory tree whose root is `sourceRoot`. 21 | func PackFrom(snapshotName, sourceRoot string) error { 22 | f, err := OpenDestination(snapshotName) 23 | if err != nil { 24 | return err 25 | } 26 | defer f.Close() 27 | 28 | return PackWithWriter(f, sourceRoot) 29 | } 30 | 31 | // OpenDestination opens the `snapshotName` file for writing, bailing out 32 | // if the file seems to exist and have existing content already. 33 | // This is done to avoid accidental overwrites. 34 | func OpenDestination(snapshotName string) (*os.File, error) { 35 | f, err := os.OpenFile(snapshotName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 36 | if err != nil { 37 | if !errors.Is(err, os.ErrExist) { 38 | return nil, err 39 | } 40 | fs, err := os.Stat(snapshotName) 41 | if err != nil { 42 | return nil, err 43 | } 44 | if fs.Size() > 0 { 45 | return nil, fmt.Errorf("file %s already exists and is of size > 0", snapshotName) 46 | } 47 | f, err = os.OpenFile(snapshotName, os.O_WRONLY, 0600) 48 | if err != nil { 49 | return nil, err 50 | } 51 | } 52 | return f, nil 53 | } 54 | 55 | // PakcWithWriter creates a snapshot sending all the binary data to the 56 | // given `fw` writer. The snapshot is made from the directory tree whose 57 | // root is `sourceRoot`. 58 | func PackWithWriter(fw io.Writer, sourceRoot string) error { 59 | gzw := gzip.NewWriter(fw) 60 | defer gzw.Close() 61 | 62 | tw := tar.NewWriter(gzw) 63 | defer tw.Close() 64 | 65 | return createSnapshot(tw, sourceRoot) 66 | } 67 | 68 | func createSnapshot(tw *tar.Writer, buildDir string) error { 69 | return filepath.Walk(buildDir, func(path string, fi os.FileInfo, _ error) error { 70 | if path == buildDir { 71 | return nil 72 | } 73 | var link string 74 | var err error 75 | 76 | if fi.Mode()&os.ModeSymlink != 0 { 77 | trace("processing symlink %s\n", path) 78 | link, err = os.Readlink(path) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | 84 | hdr, err := tar.FileInfoHeader(fi, link) 85 | if err != nil { 86 | return err 87 | } 88 | relPath, err := filepath.Rel(buildDir, path) 89 | if err != nil { 90 | return err 91 | } 92 | hdr.Name = relPath 93 | 94 | if err = tw.WriteHeader(hdr); err != nil { 95 | return err 96 | } 97 | 98 | switch hdr.Typeflag { 99 | case tar.TypeReg, tar.TypeRegA: 100 | f, err := os.Open(path) 101 | if err != nil { 102 | return err 103 | } 104 | defer f.Close() 105 | if _, err = io.Copy(tw, f); err != nil { 106 | return err 107 | } 108 | } 109 | return nil 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/snapshot/pack_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/snapshot" 14 | ) 15 | 16 | // NOTE: we intentionally use `os.RemoveAll` - not `snapshot.Cleanup` because we 17 | // want to make sure we never leak directories. `snapshot.Cleanup` is used and 18 | // tested explicitly in `unpack_test.go`. 19 | 20 | // nolint: gocyclo 21 | func TestPackUnpack(t *testing.T) { 22 | root, err := snapshot.Unpack(testDataSnapshot) 23 | if err != nil { 24 | t.Fatalf("Expected nil err, but got %v", err) 25 | } 26 | defer os.RemoveAll(root) 27 | 28 | tmpfile, err := os.CreateTemp("", "ght-test-snapshot-*.tgz") 29 | if err != nil { 30 | t.Fatalf("Expected nil err, but got %v", err) 31 | } 32 | defer func() { 33 | tmpfile.Close() 34 | os.Remove(tmpfile.Name()) 35 | }() 36 | 37 | err = snapshot.PackWithWriter(tmpfile, root) 38 | if err != nil { 39 | t.Fatalf("Expected nil err, but got %v", err) 40 | } 41 | err = tmpfile.Close() 42 | if err != nil { 43 | t.Fatalf("Expected nil err, but got %v", err) 44 | } 45 | 46 | cloneRoot, err := snapshot.Unpack(tmpfile.Name()) 47 | if err != nil { 48 | t.Fatalf("Expected nil err, but got %v", err) 49 | } 50 | defer os.RemoveAll(cloneRoot) 51 | 52 | verifyTestData(t, cloneRoot) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/snapshot/testdata.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/pkg/snapshot/testdata.tar.gz -------------------------------------------------------------------------------- /pkg/snapshot/trace.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | var trace func(msg string, args ...interface{}) 10 | 11 | func init() { 12 | trace = func(msg string, args ...interface{}) {} 13 | } 14 | 15 | func SetTraceFunction(fn func(msg string, args ...interface{})) { 16 | trace = fn 17 | } 18 | -------------------------------------------------------------------------------- /pkg/snapshot/unpack.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot 8 | 9 | import ( 10 | "archive/tar" 11 | "compress/gzip" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/jaypipes/ghw/pkg/option" 17 | ) 18 | 19 | const ( 20 | TargetRoot = "ghw-snapshot-*" 21 | ) 22 | 23 | const ( 24 | // If set, `ghw` will not unpack the snapshot in the user-supplied directory 25 | // unless the aforementioned directory is empty. 26 | OwnTargetDirectory = 1 << iota 27 | ) 28 | 29 | // Clanup removes the unpacket snapshot from the target root. 30 | // Please not that the environs variable `GHW_SNAPSHOT_PRESERVE`, if set, 31 | // will make this function silently skip. 32 | func Cleanup(targetRoot string) error { 33 | if option.EnvOrDefaultSnapshotPreserve() { 34 | return nil 35 | } 36 | return os.RemoveAll(targetRoot) 37 | } 38 | 39 | // Unpack expands the given snapshot in a temporary directory managed by `ghw`. Returns the path of that directory. 40 | func Unpack(snapshotName string) (string, error) { 41 | targetRoot, err := os.MkdirTemp("", TargetRoot) 42 | if err != nil { 43 | return "", err 44 | } 45 | _, err = UnpackInto(snapshotName, targetRoot, 0) 46 | return targetRoot, err 47 | } 48 | 49 | // UnpackInto expands the given snapshot in a client-supplied directory. 50 | // Returns true if the snapshot was actually unpacked, false otherwise 51 | func UnpackInto(snapshotName, targetRoot string, flags uint) (bool, error) { 52 | if (flags&OwnTargetDirectory) == OwnTargetDirectory && !isEmptyDir(targetRoot) { 53 | return false, nil 54 | } 55 | snap, err := os.Open(snapshotName) 56 | if err != nil { 57 | return false, err 58 | } 59 | defer snap.Close() 60 | return true, Untar(targetRoot, snap) 61 | } 62 | 63 | // Untar extracts data from the given reader (providing data in tar.gz format) and unpacks it in the given directory. 64 | func Untar(root string, r io.Reader) error { 65 | var err error 66 | gzr, err := gzip.NewReader(r) 67 | if err != nil { 68 | return err 69 | } 70 | defer gzr.Close() 71 | 72 | tr := tar.NewReader(gzr) 73 | for { 74 | header, err := tr.Next() 75 | if err == io.EOF { 76 | // we are done 77 | return nil 78 | } 79 | 80 | if err != nil { 81 | // bail out 82 | return err 83 | } 84 | 85 | if header == nil { 86 | // TODO: how come? 87 | continue 88 | } 89 | 90 | target := filepath.Join(root, header.Name) 91 | mode := os.FileMode(header.Mode) 92 | 93 | switch header.Typeflag { 94 | case tar.TypeDir: 95 | err = os.MkdirAll(target, mode) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | case tar.TypeReg: 101 | dst, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, mode) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | _, err = io.Copy(dst, tr) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | dst.Close() 112 | 113 | case tar.TypeSymlink: 114 | err = os.Symlink(header.Linkname, target) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | } 121 | 122 | func isEmptyDir(name string) bool { 123 | entries, err := os.ReadDir(name) 124 | if err != nil { 125 | return false 126 | } 127 | return len(entries) == 0 128 | } 129 | -------------------------------------------------------------------------------- /pkg/snapshot/unpack_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package snapshot_test 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/jaypipes/ghw/pkg/snapshot" 16 | ) 17 | 18 | const ( 19 | testDataSnapshot = "testdata.tar.gz" 20 | ) 21 | 22 | // nolint: gocyclo 23 | func TestUnpack(t *testing.T) { 24 | root, err := snapshot.Unpack(testDataSnapshot) 25 | if err != nil { 26 | t.Fatalf("Expected nil err, but got %v", err) 27 | } 28 | 29 | verifyTestData(t, root) 30 | 31 | err = snapshot.Cleanup(root) 32 | if err != nil { 33 | t.Fatalf("Expected nil err, but got %v", err) 34 | } 35 | 36 | if _, err := os.Stat(root); !errors.Is(err, os.ErrNotExist) { 37 | t.Fatalf("Expected %q to be gone, but still exists", root) 38 | } 39 | } 40 | 41 | // nolint: gocyclo 42 | func TestUnpackInto(t *testing.T) { 43 | testRoot, err := os.MkdirTemp("", "ghw-test-snapshot-*") 44 | if err != nil { 45 | t.Fatalf("Expected nil err, but got %v", err) 46 | } 47 | 48 | _, err = snapshot.UnpackInto(testDataSnapshot, testRoot, 0) 49 | if err != nil { 50 | t.Fatalf("Expected nil err, but got %v", err) 51 | } 52 | 53 | verifyTestData(t, testRoot) 54 | 55 | // note that in real production code the caller will likely manage its 56 | // snapshot root directory in a different way, here we call snapshot.Cleanup 57 | // to clean up after ourselves more than to test it 58 | err = snapshot.Cleanup(testRoot) 59 | if err != nil { 60 | t.Fatalf("Expected nil err, but got %v", err) 61 | } 62 | 63 | if _, err := os.Stat(testRoot); !errors.Is(err, os.ErrNotExist) { 64 | t.Fatalf("Expected %q to be gone, but still exists", testRoot) 65 | } 66 | } 67 | 68 | // nolint: gocyclo 69 | func TestUnpackIntoPresrving(t *testing.T) { 70 | testRoot, err := os.MkdirTemp("", "ghw-test-snapshot-*") 71 | if err != nil { 72 | t.Fatalf("Expected nil err, but got %v", err) 73 | } 74 | 75 | err = os.WriteFile(filepath.Join(testRoot, "canary"), []byte(""), 0644) 76 | if err != nil { 77 | t.Fatalf("Expected nil err, but got %v", err) 78 | } 79 | 80 | _, err = snapshot.UnpackInto(testDataSnapshot, testRoot, snapshot.OwnTargetDirectory) 81 | if err != nil { 82 | t.Fatalf("Expected nil err, but got %v", err) 83 | } 84 | 85 | entries, err := os.ReadDir(testRoot) 86 | if err != nil { 87 | t.Fatalf("Expected nil err, but got %v", err) 88 | } 89 | if len(entries) != 1 { 90 | t.Fatalf("Expected one entry in %q, but got %v", testRoot, entries) 91 | } 92 | 93 | canary := entries[0] 94 | if canary.Name() != "canary" { 95 | t.Fatalf("Expected entry %q, but got %q", "canary", canary.Name()) 96 | } 97 | 98 | // note that in real production code the caller will likely manage its 99 | // snapshot root directory in a different way, here we call snapshot.Cleanup 100 | // to clean up after ourselves more than to test it 101 | err = snapshot.Cleanup(testRoot) 102 | if err != nil { 103 | t.Fatalf("Expected nil err, but got %v", err) 104 | } 105 | 106 | if _, err := os.Stat(testRoot); !errors.Is(err, os.ErrNotExist) { 107 | t.Fatalf("Expected %q to be gone, but still exists", testRoot) 108 | } 109 | } 110 | 111 | func verifyTestData(t *testing.T, root string) { 112 | verifyFileContent(t, filepath.Join(root, "ghw-test-0"), "ghw-test-0\n") 113 | verifyFileContent(t, filepath.Join(root, "ghw-test-1"), "ghw-test-1\n") 114 | verifyFileContent(t, filepath.Join(root, "nested", "ghw-test-2"), "ghw-test-2\n") 115 | 116 | } 117 | 118 | func verifyFileContent(t *testing.T, path, expected string) { 119 | data, err := os.ReadFile(path) 120 | if err != nil { 121 | t.Fatalf("Expected nil err, but got %v", err) 122 | } 123 | content := string(data) 124 | if content != expected { 125 | t.Fatalf("Expected %q, but got %q", expected, content) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pkg/topology/topology.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package topology 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/jaypipes/ghw/pkg/context" 17 | "github.com/jaypipes/ghw/pkg/cpu" 18 | "github.com/jaypipes/ghw/pkg/marshal" 19 | "github.com/jaypipes/ghw/pkg/memory" 20 | "github.com/jaypipes/ghw/pkg/option" 21 | ) 22 | 23 | // Architecture describes the overall hardware architecture. It can be either 24 | // Symmetric Multi-Processor (SMP) or Non-Uniform Memory Access (NUMA) 25 | type Architecture int 26 | 27 | const ( 28 | // SMP is a Symmetric Multi-Processor system 29 | ArchitectureSMP Architecture = iota 30 | // NUMA is a Non-Uniform Memory Access system 31 | ArchitectureNUMA 32 | ) 33 | 34 | const ( 35 | // DEPRECATED: please use ArchitectureSMP. 36 | // TODO(jaypipes): Remove before v1.0 37 | ARCHITECTURE_SMP = ArchitectureSMP 38 | // DEPRECATED: please use ArchitectureNUMA. 39 | // TODO(jaypipes): Remove before v1.0 40 | ARCHITECTURE_NUMA = ArchitectureNUMA 41 | ) 42 | 43 | var ( 44 | architectureString = map[Architecture]string{ 45 | ArchitectureSMP: "SMP", 46 | ArchitectureNUMA: "NUMA", 47 | } 48 | 49 | // NOTE(fromani): the keys are all lowercase and do not match 50 | // the keys in the opposite table `architectureString`. 51 | // This is done because of the choice we made in 52 | // Architecture:MarshalJSON. 53 | // We use this table only in UnmarshalJSON, so it should be OK. 54 | stringArchitecture = map[string]Architecture{ 55 | "smp": ArchitectureSMP, 56 | "numa": ArchitectureNUMA, 57 | } 58 | ) 59 | 60 | func (a Architecture) String() string { 61 | return architectureString[a] 62 | } 63 | 64 | // NOTE(jaypipes): since serialized output is as "official" as we're going to 65 | // get, let's lowercase the string output when serializing, in order to 66 | // "normalize" the expected serialized output 67 | func (a Architecture) MarshalJSON() ([]byte, error) { 68 | return []byte(strconv.Quote(strings.ToLower(a.String()))), nil 69 | } 70 | 71 | func (a *Architecture) UnmarshalJSON(b []byte) error { 72 | var s string 73 | if err := json.Unmarshal(b, &s); err != nil { 74 | return err 75 | } 76 | key := strings.ToLower(s) 77 | val, ok := stringArchitecture[key] 78 | if !ok { 79 | return fmt.Errorf("unknown architecture: %q", key) 80 | } 81 | *a = val 82 | return nil 83 | } 84 | 85 | // Node is an abstract construct representing a collection of processors and 86 | // various levels of memory cache that those processors share. In a NUMA 87 | // architecture, there are multiple NUMA nodes, abstracted here as multiple 88 | // Node structs. In an SMP architecture, a single Node will be available in the 89 | // Info struct and this single struct can be used to describe the levels of 90 | // memory caching available to the single physical processor package's physical 91 | // processor cores 92 | type Node struct { 93 | ID int `json:"id"` 94 | Cores []*cpu.ProcessorCore `json:"cores"` 95 | Caches []*memory.Cache `json:"caches"` 96 | Distances []int `json:"distances"` 97 | Memory *memory.Area `json:"memory"` 98 | } 99 | 100 | func (n *Node) String() string { 101 | return fmt.Sprintf( 102 | "node #%d (%d cores)", 103 | n.ID, 104 | len(n.Cores), 105 | ) 106 | } 107 | 108 | // Info describes the system topology for the host hardware 109 | type Info struct { 110 | ctx *context.Context 111 | Architecture Architecture `json:"architecture"` 112 | Nodes []*Node `json:"nodes"` 113 | } 114 | 115 | // New returns a pointer to an Info struct that contains information about the 116 | // NUMA topology on the host system 117 | func New(opts ...*option.Option) (*Info, error) { 118 | merged := option.Merge(opts...) 119 | ctx := context.New(merged) 120 | info := &Info{ctx: ctx} 121 | var err error 122 | if context.Exists(merged) { 123 | err = info.load() 124 | } else { 125 | err = ctx.Do(info.load) 126 | } 127 | if err != nil { 128 | return nil, err 129 | } 130 | for _, node := range info.Nodes { 131 | sort.Sort(memory.SortByCacheLevelTypeFirstProcessor(node.Caches)) 132 | } 133 | return info, nil 134 | } 135 | 136 | func (i *Info) String() string { 137 | archStr := "SMP" 138 | if i.Architecture == ArchitectureNUMA { 139 | archStr = "NUMA" 140 | } 141 | res := fmt.Sprintf( 142 | "topology %s (%d nodes)", 143 | archStr, 144 | len(i.Nodes), 145 | ) 146 | return res 147 | } 148 | 149 | // simple private struct used to encapsulate topology information in a 150 | // top-level "topology" YAML/JSON map/object key 151 | type topologyPrinter struct { 152 | Info *Info `json:"topology"` 153 | } 154 | 155 | // YAMLString returns a string with the topology information formatted as YAML 156 | // under a top-level "topology:" key 157 | func (i *Info) YAMLString() string { 158 | return marshal.SafeYAML(i.ctx, topologyPrinter{i}) 159 | } 160 | 161 | // JSONString returns a string with the topology information formatted as JSON 162 | // under a top-level "topology:" key 163 | func (i *Info) JSONString(indent bool) string { 164 | return marshal.SafeJSON(i.ctx, topologyPrinter{i}, indent) 165 | } 166 | -------------------------------------------------------------------------------- /pkg/topology/topology_linux.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package topology 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/jaypipes/ghw/pkg/context" 16 | "github.com/jaypipes/ghw/pkg/cpu" 17 | "github.com/jaypipes/ghw/pkg/linuxpath" 18 | "github.com/jaypipes/ghw/pkg/memory" 19 | ) 20 | 21 | func (i *Info) load() error { 22 | i.Nodes = topologyNodes(i.ctx) 23 | if len(i.Nodes) == 1 { 24 | i.Architecture = ArchitectureSMP 25 | } else { 26 | i.Architecture = ArchitectureNUMA 27 | } 28 | return nil 29 | } 30 | 31 | func topologyNodes(ctx *context.Context) []*Node { 32 | paths := linuxpath.New(ctx) 33 | nodes := make([]*Node, 0) 34 | 35 | files, err := os.ReadDir(paths.SysDevicesSystemNode) 36 | if err != nil { 37 | ctx.Warn("failed to determine nodes: %s\n", err) 38 | return nodes 39 | } 40 | for _, file := range files { 41 | filename := file.Name() 42 | if !strings.HasPrefix(filename, "node") { 43 | continue 44 | } 45 | node := &Node{} 46 | nodeID, err := strconv.Atoi(filename[4:]) 47 | if err != nil { 48 | ctx.Warn("failed to determine node ID: %s\n", err) 49 | return nodes 50 | } 51 | node.ID = nodeID 52 | cores, err := cpu.CoresForNode(ctx, nodeID) 53 | if err != nil { 54 | ctx.Warn("failed to determine cores for node: %s\n", err) 55 | return nodes 56 | } 57 | node.Cores = cores 58 | caches, err := memory.CachesForNode(ctx, nodeID) 59 | if err != nil { 60 | ctx.Warn("failed to determine caches for node: %s\n", err) 61 | return nodes 62 | } 63 | node.Caches = caches 64 | 65 | distances, err := distancesForNode(ctx, nodeID) 66 | if err != nil { 67 | ctx.Warn("failed to determine node distances for node: %s\n", err) 68 | return nodes 69 | } 70 | node.Distances = distances 71 | 72 | area, err := memory.AreaForNode(ctx, nodeID) 73 | if err != nil { 74 | ctx.Warn("failed to determine memory area for node: %s\n", err) 75 | return nodes 76 | } 77 | node.Memory = area 78 | 79 | nodes = append(nodes, node) 80 | } 81 | return nodes 82 | } 83 | 84 | func distancesForNode(ctx *context.Context, nodeID int) ([]int, error) { 85 | paths := linuxpath.New(ctx) 86 | path := filepath.Join( 87 | paths.SysDevicesSystemNode, 88 | fmt.Sprintf("node%d", nodeID), 89 | "distance", 90 | ) 91 | 92 | data, err := os.ReadFile(path) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | items := strings.Fields(strings.TrimSpace(string(data))) 98 | dists := make([]int, len(items)) // TODO: can a NUMA cell be offlined? 99 | for idx, item := range items { 100 | dist, err := strconv.Atoi(item) 101 | if err != nil { 102 | return dists, err 103 | } 104 | dists[idx] = dist 105 | } 106 | return dists, nil 107 | } 108 | -------------------------------------------------------------------------------- /pkg/topology/topology_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package topology_test 8 | 9 | import ( 10 | "encoding/json" 11 | "path/filepath" 12 | "testing" 13 | 14 | "github.com/jaypipes/ghw/pkg/memory" 15 | "github.com/jaypipes/ghw/pkg/option" 16 | "github.com/jaypipes/ghw/pkg/topology" 17 | 18 | "github.com/jaypipes/ghw/testdata" 19 | ) 20 | 21 | // nolint: gocyclo 22 | func TestTopologyNUMADistances(t *testing.T) { 23 | testdataPath, err := testdata.SnapshotsDirectory() 24 | if err != nil { 25 | t.Fatalf("Expected nil err, but got %v", err) 26 | } 27 | 28 | multiNumaSnapshot := filepath.Join(testdataPath, "linux-amd64-intel-xeon-L5640.tar.gz") 29 | // from now on we use constants reflecting the content of the snapshot we requested, 30 | // which we reviewed beforehand. IOW, you need to know the content of the 31 | // snapshot to fully understand this test. Inspect it using 32 | // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-intel-xeon-L5640.tar.gz" ghwc topology 33 | 34 | info, err := topology.New(option.WithSnapshot(option.SnapshotOptions{ 35 | Path: multiNumaSnapshot, 36 | })) 37 | 38 | if err != nil { 39 | t.Fatalf("Expected nil err, but got %v", err) 40 | } 41 | if info == nil { 42 | t.Fatalf("Expected non-nil TopologyInfo, but got nil") 43 | } 44 | 45 | if len(info.Nodes) != 2 { 46 | t.Fatalf("Expected 2 nodes but got 0.") 47 | } 48 | 49 | for _, n := range info.Nodes { 50 | if len(n.Distances) != len(info.Nodes) { 51 | t.Fatalf("Expected distances to all known nodes") 52 | } 53 | } 54 | 55 | if info.Nodes[0].Distances[0] != info.Nodes[1].Distances[1] { 56 | t.Fatalf("Expected symmetric distance to self, got %v and %v", info.Nodes[0].Distances, info.Nodes[1].Distances) 57 | } 58 | 59 | if info.Nodes[0].Distances[1] != info.Nodes[1].Distances[0] { 60 | t.Fatalf("Expected symmetric distance to the other node, got %v and %v", info.Nodes[0].Distances, info.Nodes[1].Distances) 61 | } 62 | } 63 | 64 | // we have this test in topology_linux_test.go (and not in topology_test.go) because `topologyFillInfo` 65 | // is not implemented on darwin; so having it in the platform-independent tests would lead to false negatives. 66 | func TestTopologyMarshalUnmarshal(t *testing.T) { 67 | data, err := topology.New(option.WithNullAlerter()) 68 | if err != nil { 69 | t.Fatalf("Expected no error creating topology.Info, but got %v", err) 70 | } 71 | 72 | jdata, err := json.Marshal(data) 73 | if err != nil { 74 | t.Fatalf("Expected no error marshaling topology.Info, but got %v", err) 75 | } 76 | 77 | var topo *topology.Info 78 | 79 | err = json.Unmarshal(jdata, &topo) 80 | if err != nil { 81 | t.Fatalf("Expected no error unmarshaling topology.Info, but got %v", err) 82 | } 83 | } 84 | 85 | // nolint: gocyclo 86 | func TestTopologyPerNUMAMemory(t *testing.T) { 87 | testdataPath, err := testdata.SnapshotsDirectory() 88 | if err != nil { 89 | t.Fatalf("Expected nil err, but got %v", err) 90 | } 91 | 92 | multiNumaSnapshot := filepath.Join(testdataPath, "linux-amd64-intel-xeon-L5640.tar.gz") 93 | // from now on we use constants reflecting the content of the snapshot we requested, 94 | // which we reviewed beforehand. IOW, you need to know the content of the 95 | // snapshot to fully understand this test. Inspect it using 96 | // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-intel-xeon-L5640.tar.gz" ghwc topology 97 | 98 | memInfo, err := memory.New(option.WithSnapshot(option.SnapshotOptions{ 99 | Path: multiNumaSnapshot, 100 | })) 101 | 102 | if err != nil { 103 | t.Fatalf("Expected nil err, but got %v", err) 104 | } 105 | if memInfo == nil { 106 | t.Fatalf("Expected non-nil MemoryInfo, but got nil") 107 | } 108 | 109 | info, err := topology.New(option.WithSnapshot(option.SnapshotOptions{ 110 | Path: multiNumaSnapshot, 111 | })) 112 | 113 | if err != nil { 114 | t.Fatalf("Expected nil err, but got %v", err) 115 | } 116 | if info == nil { 117 | t.Fatalf("Expected non-nil TopologyInfo, but got nil") 118 | } 119 | 120 | if len(info.Nodes) != 2 { 121 | t.Fatalf("Expected 2 nodes but got 0.") 122 | } 123 | 124 | for _, node := range info.Nodes { 125 | if node.Memory == nil { 126 | t.Fatalf("missing memory information for node %d", node.ID) 127 | } 128 | 129 | if node.Memory.TotalPhysicalBytes <= 0 { 130 | t.Fatalf("negative physical size for node %d", node.ID) 131 | } 132 | if node.Memory.TotalPhysicalBytes > memInfo.TotalPhysicalBytes { 133 | t.Fatalf("physical size for node %d exceeds system's", node.ID) 134 | } 135 | if node.Memory.TotalUsableBytes <= 0 { 136 | t.Fatalf("negative usable size for node %d", node.ID) 137 | } 138 | if node.Memory.TotalUsableBytes > memInfo.TotalUsableBytes { 139 | t.Fatalf("usable size for node %d exceeds system's", node.ID) 140 | } 141 | if node.Memory.TotalUsableBytes > node.Memory.TotalPhysicalBytes { 142 | t.Fatalf("excessive usable size for node %d", node.ID) 143 | } 144 | if node.Memory.DefaultHugePageSize == 0 { 145 | t.Fatalf("unexpected default HP size for node %d", node.ID) 146 | } 147 | if len(node.Memory.HugePageAmountsBySize) != 2 { 148 | t.Fatalf("expected 2 huge page info records, but got '%d' for node %d", len(node.Memory.HugePageAmountsBySize), node.ID) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pkg/topology/topology_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | // Use and distribution licensed under the Apache license version 2. 5 | // 6 | // See the COPYING file in the root project directory for full text. 7 | // 8 | 9 | package topology 10 | 11 | import ( 12 | "runtime" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (i *Info) load() error { 18 | return errors.New("topologyFillInfo not implemented on " + runtime.GOOS) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/topology/topology_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package topology_test 8 | 9 | import ( 10 | "os" 11 | "testing" 12 | 13 | "github.com/jaypipes/ghw/pkg/topology" 14 | ) 15 | 16 | // nolint: gocyclo 17 | func TestTopology(t *testing.T) { 18 | if _, ok := os.LookupEnv("GHW_TESTING_SKIP_TOPOLOGY"); ok { 19 | t.Skip("Skipping topology tests.") 20 | } 21 | 22 | info, err := topology.New() 23 | 24 | if err != nil { 25 | t.Fatalf("Expected nil err, but got %v", err) 26 | } 27 | if info == nil { 28 | t.Fatalf("Expected non-nil TopologyInfo, but got nil") 29 | } 30 | 31 | if len(info.Nodes) == 0 { 32 | t.Fatalf("Expected >0 nodes but got 0.") 33 | } 34 | 35 | if info.Architecture == topology.ArchitectureNUMA && len(info.Nodes) == 1 { 36 | t.Fatalf("Got NUMA architecture but only 1 node.") 37 | } 38 | 39 | for _, n := range info.Nodes { 40 | if len(n.Cores) == 0 { 41 | t.Fatalf("Expected >0 cores but got 0.") 42 | } 43 | for _, c := range n.Cores { 44 | if len(c.LogicalProcessors) == 0 { 45 | t.Fatalf("Expected >0 logical processors but got 0.") 46 | } 47 | if uint32(len(c.LogicalProcessors)) != c.TotalHardwareThreads { 48 | t.Fatalf( 49 | "Expected TotalHardwareThreads == len(logical procs) but %d != %d", 50 | c.TotalHardwareThreads, 51 | len(c.LogicalProcessors), 52 | ) 53 | } 54 | } 55 | if len(n.Caches) == 0 { 56 | t.Fatalf("Expected >0 caches but got 0.") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/topology/topology_windows.go: -------------------------------------------------------------------------------- 1 | // Use and distribution licensed under the Apache license version 2. 2 | // 3 | // See the COPYING file in the root project directory for full text. 4 | // 5 | 6 | package topology 7 | 8 | import ( 9 | "encoding/binary" 10 | "fmt" 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | const ( 16 | rcFailure = 0 17 | sizeofLogicalProcessorInfo = 32 18 | errInsufficientBuffer syscall.Errno = 122 19 | 20 | relationProcessorCore = 0 21 | relationNUMANode = 1 22 | relationCache = 2 23 | relationProcessorPackage = 3 24 | relationGroup = 4 25 | ) 26 | 27 | func (i *Info) load() error { 28 | nodes, err := topologyNodes() 29 | if err != nil { 30 | return err 31 | } 32 | i.Nodes = nodes 33 | if len(nodes) == 1 { 34 | i.Architecture = ArchitectureSMP 35 | } else { 36 | i.Architecture = ArchitectureNUMA 37 | } 38 | return nil 39 | } 40 | 41 | func topologyNodes() ([]*Node, error) { 42 | nodes := make([]*Node, 0) 43 | lpis, err := getWin32LogicalProcessorInfos() 44 | if err != nil { 45 | return nil, err 46 | } 47 | for _, lpi := range lpis { 48 | switch lpi.relationship { 49 | case relationNUMANode: 50 | nodes = append(nodes, &Node{ 51 | ID: lpi.numaNodeID(), 52 | }) 53 | case relationProcessorCore: 54 | // TODO(jaypipes): associated LP to processor core 55 | case relationProcessorPackage: 56 | // ignore 57 | case relationCache: 58 | // TODO(jaypipes) handle cache layers 59 | default: 60 | return nil, fmt.Errorf("Unknown LOGICAL_PROCESSOR_RELATIONSHIP value: %d", lpi.relationship) 61 | 62 | } 63 | } 64 | return nodes, nil 65 | } 66 | 67 | // This is the CACHE_DESCRIPTOR struct in the Win32 API 68 | type cacheDescriptor struct { 69 | level uint8 70 | associativity uint8 71 | lineSize uint16 72 | size uint32 73 | cacheType uint32 74 | } 75 | 76 | // This is the SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct in the Win32 API 77 | type logicalProcessorInfo struct { 78 | processorMask uint64 79 | relationship uint64 80 | // The following dummyunion member is a representation of this part of 81 | // the SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct: 82 | // 83 | // union { 84 | // struct { 85 | // BYTE Flags; 86 | // } ProcessorCore; 87 | // struct { 88 | // DWORD NodeNumber; 89 | // } NumaNode; 90 | // CACHE_DESCRIPTOR Cache; 91 | // ULONGLONG Reserved[2]; 92 | // } DUMMYUNIONNAME; 93 | dummyunion [16]byte 94 | } 95 | 96 | // numaNodeID returns the NUMA node's identifier from the logical processor 97 | // information struct by grabbing the integer representation of the struct's 98 | // NumaNode unioned data element 99 | func (lpi *logicalProcessorInfo) numaNodeID() int { 100 | if lpi.relationship != relationNUMANode { 101 | return -1 102 | } 103 | return int(binary.LittleEndian.Uint16(lpi.dummyunion[0:])) 104 | } 105 | 106 | // ref: https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlogicalprocessorinformation 107 | func getWin32LogicalProcessorInfos() ( 108 | []*logicalProcessorInfo, 109 | error, 110 | ) { 111 | lpis := make([]*logicalProcessorInfo, 0) 112 | win32api := syscall.NewLazyDLL("kernel32.dll") 113 | glpi := win32api.NewProc("GetLogicalProcessorInformation") 114 | 115 | // The way the GetLogicalProcessorInformation (GLPI) Win32 API call 116 | // works is wonky, but consistent with the Win32 API calling structure. 117 | // Basically, you need to first call the GLPI API with a NUL pointerr 118 | // and a pointer to an integer. That first call to the API should 119 | // return ERROR_INSUFFICIENT_BUFFER, which is the indication that the 120 | // supplied buffer pointer is NUL and needs to have memory allocated to 121 | // it of an amount equal to the value of the integer pointer argument. 122 | // Once the buffer is allocated this amount of space, the GLPI API call 123 | // is again called. This time, the return value should be 0 and the 124 | // buffer will have been set to an array of 125 | // SYSTEM_LOGICAL_PROCESSOR_INFORMATION structs. 126 | toAllocate := uint32(0) 127 | // first, figure out how much we need 128 | rc, _, win32err := glpi.Call(uintptr(0), uintptr(unsafe.Pointer(&toAllocate))) 129 | if rc == rcFailure { 130 | if win32err != errInsufficientBuffer { 131 | return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API initial call failed to return ERROR_INSUFFICIENT_BUFFER") 132 | } 133 | } else { 134 | // This shouldn't happen because buffer hasn't yet been allocated... 135 | return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API initial call returned success instead of failure with ERROR_INSUFFICIENT_BUFFER") 136 | } 137 | 138 | // OK, now we actually allocate a raw buffer to fill with some number 139 | // of SYSTEM_LOGICAL_PROCESSOR_INFORMATION structs 140 | b := make([]byte, toAllocate) 141 | rc, _, win32err = glpi.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(unsafe.Pointer(&toAllocate))) 142 | if rc == rcFailure { 143 | return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API call failed to set supplied buffer. Win32 system error: %s", win32err) 144 | } 145 | 146 | for x := uint32(0); x < toAllocate; x += sizeofLogicalProcessorInfo { 147 | lpiraw := b[x : x+sizeofLogicalProcessorInfo] 148 | lpi := &logicalProcessorInfo{ 149 | processorMask: binary.LittleEndian.Uint64(lpiraw[0:]), 150 | relationship: binary.LittleEndian.Uint64(lpiraw[8:]), 151 | } 152 | copy(lpi.dummyunion[0:16], lpiraw[16:32]) 153 | lpis = append(lpis, lpi) 154 | } 155 | return lpis, nil 156 | } 157 | -------------------------------------------------------------------------------- /pkg/unitutil/unit.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package unitutil 8 | 9 | var ( 10 | KB int64 = 1024 11 | MB = KB * 1024 12 | GB = MB * 1024 13 | TB = GB * 1024 14 | PB = TB * 1024 15 | EB = PB * 1024 16 | ) 17 | 18 | // AmountString returns a string representation of the amount with an amount 19 | // suffix corresponding to the nearest kibibit. 20 | // 21 | // For example, AmountString(1022) == "1022). AmountString(1024) == "1KB", etc 22 | func AmountString(size int64) (int64, string) { 23 | switch { 24 | case size < MB: 25 | return KB, "KB" 26 | case size < GB: 27 | return MB, "MB" 28 | case size < TB: 29 | return GB, "GB" 30 | case size < PB: 31 | return TB, "TB" 32 | case size < EB: 33 | return PB, "PB" 34 | default: 35 | return EB, "EB" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package util 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/jaypipes/ghw/pkg/context" 16 | ) 17 | 18 | const ( 19 | UNKNOWN = "unknown" 20 | ) 21 | 22 | type closer interface { 23 | Close() error 24 | } 25 | 26 | func SafeClose(c closer) { 27 | err := c.Close() 28 | if err != nil { 29 | _, _ = fmt.Fprintf(os.Stderr, "failed to close: %s", err) 30 | } 31 | } 32 | 33 | // Reads a supplied filepath and converts the contents to an integer. Returns 34 | // -1 if there were file permissions or existence errors or if the contents 35 | // could not be successfully converted to an integer. In any error, a warning 36 | // message is printed to STDERR and -1 is returned. 37 | func SafeIntFromFile(ctx *context.Context, path string) int { 38 | msg := "failed to read int from file: %s\n" 39 | buf, err := os.ReadFile(path) 40 | if err != nil { 41 | ctx.Warn(msg, err) 42 | return -1 43 | } 44 | contents := strings.TrimSpace(string(buf)) 45 | res, err := strconv.Atoi(contents) 46 | if err != nil { 47 | ctx.Warn(msg, err) 48 | return -1 49 | } 50 | return res 51 | } 52 | 53 | // ConcatStrings concatenate strings in a larger one. This function 54 | // addresses a very specific ghw use case. For a more general approach, 55 | // just use strings.Join() 56 | func ConcatStrings(items ...string) string { 57 | return strings.Join(items, "") 58 | } 59 | 60 | // Convert strings to bool using strconv.ParseBool() when recognized, otherwise 61 | // use map lookup to convert strings like "Yes" "No" "On" "Off" to bool 62 | // `ethtool` uses on, off, yes, no (upper and lower case) rather than true and 63 | // false. 64 | func ParseBool(str string) (bool, error) { 65 | if b, err := strconv.ParseBool(str); err == nil { 66 | return b, err 67 | } else { 68 | ExtraBools := map[string]bool{ 69 | "on": true, 70 | "off": false, 71 | "yes": true, 72 | "no": false, 73 | // Return false instead of an error on empty strings 74 | // For example from empty files in SysClassNet/Device 75 | "": false, 76 | } 77 | if b, ok := ExtraBools[strings.ToLower(str)]; ok { 78 | return b, nil 79 | } else { 80 | // Return strconv.ParseBool's error here 81 | return b, err 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package util_test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/jaypipes/ghw/pkg/util" 13 | ) 14 | 15 | // nolint: gocyclo 16 | func TestConcatStrings(t *testing.T) { 17 | type testCase struct { 18 | items []string 19 | expected string 20 | } 21 | 22 | testCases := []testCase{ 23 | { 24 | items: []string{}, 25 | expected: "", 26 | }, 27 | { 28 | items: []string{"simple"}, 29 | expected: "simple", 30 | }, 31 | { 32 | items: []string{ 33 | "foo", 34 | "bar", 35 | "baz", 36 | }, 37 | expected: "foobarbaz", 38 | }, 39 | { 40 | items: []string{ 41 | "foo ", 42 | " bar ", 43 | " baz", 44 | }, 45 | expected: "foo bar baz", 46 | }, 47 | } 48 | 49 | for _, tCase := range testCases { 50 | t.Run(tCase.expected, func(t *testing.T) { 51 | got := util.ConcatStrings(tCase.items...) 52 | if got != tCase.expected { 53 | t.Errorf("expected %q got %q", tCase.expected, got) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func TestParseBool(t *testing.T) { 60 | type testCase struct { 61 | item string 62 | expected bool 63 | } 64 | 65 | testCases := []testCase{ 66 | { 67 | item: "False", 68 | expected: false, 69 | }, 70 | { 71 | item: "F", 72 | expected: false, 73 | }, 74 | { 75 | item: "1", 76 | expected: true, 77 | }, 78 | { 79 | item: "", 80 | expected: false, 81 | }, 82 | { 83 | item: "on", 84 | expected: true, 85 | }, 86 | { 87 | item: "Off", 88 | expected: false, 89 | }, 90 | { 91 | item: "Yes", 92 | expected: true, 93 | }, 94 | { 95 | item: "no", 96 | expected: false, 97 | }, 98 | } 99 | 100 | for _, tCase := range testCases { 101 | t.Run(tCase.item, func(t *testing.T) { 102 | got, err := util.ParseBool(tCase.item) 103 | if got != tCase.expected { 104 | t.Errorf("expected %t got %t", tCase.expected, got) 105 | } 106 | if err != nil { 107 | t.Errorf("util.ParseBool threw error %s", err) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /testdata/samples/dell-r610-block.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "total_size_bytes": 775510031872, 4 | "disks": [ 5 | { 6 | "name": "dm-0", 7 | "size_bytes": 34359738368, 8 | "physical_block_size_bytes": 512, 9 | "drive_type": "unknown", 10 | "removable": false, 11 | "storage_controller": "unknown", 12 | "bus_path": "unknown", 13 | "vendor": "unknown", 14 | "model": "unknown", 15 | "serial_number": "unknown", 16 | "wwn": "unknown", 17 | "partitions": [] 18 | }, 19 | { 20 | "name": "dm-1", 21 | "size_bytes": 8589934592, 22 | "physical_block_size_bytes": 512, 23 | "drive_type": "unknown", 24 | "removable": false, 25 | "storage_controller": "unknown", 26 | "bus_path": "unknown", 27 | "vendor": "unknown", 28 | "model": "unknown", 29 | "serial_number": "unknown", 30 | "wwn": "unknown", 31 | "partitions": [] 32 | }, 33 | { 34 | "name": "dm-2", 35 | "size_bytes": 12884901888, 36 | "physical_block_size_bytes": 512, 37 | "drive_type": "unknown", 38 | "removable": false, 39 | "storage_controller": "unknown", 40 | "bus_path": "unknown", 41 | "vendor": "unknown", 42 | "model": "unknown", 43 | "serial_number": "unknown", 44 | "wwn": "unknown", 45 | "partitions": [] 46 | }, 47 | { 48 | "name": "dm-3", 49 | "size_bytes": 133143986176, 50 | "physical_block_size_bytes": 512, 51 | "drive_type": "unknown", 52 | "removable": false, 53 | "storage_controller": "unknown", 54 | "bus_path": "unknown", 55 | "vendor": "unknown", 56 | "model": "unknown", 57 | "serial_number": "unknown", 58 | "wwn": "unknown", 59 | "partitions": [] 60 | }, 61 | { 62 | "name": "dm-4", 63 | "size_bytes": 85899345920, 64 | "physical_block_size_bytes": 512, 65 | "drive_type": "unknown", 66 | "removable": false, 67 | "storage_controller": "unknown", 68 | "bus_path": "unknown", 69 | "vendor": "unknown", 70 | "model": "unknown", 71 | "serial_number": "unknown", 72 | "wwn": "unknown", 73 | "partitions": [] 74 | }, 75 | { 76 | "name": "sda", 77 | "size_bytes": 499558383616, 78 | "physical_block_size_bytes": 512, 79 | "drive_type": "hdd", 80 | "removable": false, 81 | "storage_controller": "scsi", 82 | "bus_path": "pci-0000:03:00.0-scsi-0:2:0:0", 83 | "vendor": "DELL", 84 | "model": "PERC_6_i", 85 | "serial_number": "unknown", 86 | "wwn": "0xCAFE", 87 | "partitions": [ 88 | { 89 | "name": "sda1", 90 | "label": "", 91 | "mount_point": "/boot/efi", 92 | "size_bytes": 134217728, 93 | "type": "vfat", 94 | "read_only": false, 95 | "uuid": "00000000-0000-0000-0000-000000000000" 96 | }, 97 | { 98 | "name": "sda2", 99 | "label": "", 100 | "mount_point": "/boot", 101 | "size_bytes": 536870912, 102 | "type": "ext4", 103 | "read_only": false, 104 | "uuid": "00000000-0000-0000-0000-000000000000" 105 | }, 106 | { 107 | "name": "sda3", 108 | "label": "", 109 | "mount_point": "", 110 | "size_bytes": 498886229504, 111 | "type": "", 112 | "read_only": true, 113 | "uuid": "00000000-0000-0000-0000-000000000000" 114 | } 115 | ] 116 | }, 117 | { 118 | "name": "sr0", 119 | "size_bytes": 1073741312, 120 | "physical_block_size_bytes": 512, 121 | "drive_type": "odd", 122 | "removable": true, 123 | "storage_controller": "scsi", 124 | "bus_path": "pci-0000:00:1f.2-ata-1.0", 125 | "vendor": "PLDS", 126 | "model": "PLDS_DVD-ROM_DS-8D3SH", 127 | "serial_number": "unknown", 128 | "wwn": "unknown", 129 | "partitions": [] 130 | }, 131 | { 132 | "name": "zram0", 133 | "size_bytes": 0, 134 | "physical_block_size_bytes": 4096, 135 | "drive_type": "ssd", 136 | "removable": false, 137 | "storage_controller": "unknown", 138 | "bus_path": "unknown", 139 | "vendor": "unknown", 140 | "model": "unknown", 141 | "serial_number": "unknown", 142 | "wwn": "unknown", 143 | "partitions": [] 144 | } 145 | ] 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-61797caf7c0b6bca1725ad7975ed4773.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/testdata/snapshots/linux-amd64-61797caf7c0b6bca1725ad7975ed4773.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-8581cf3a529e5d8b97ea876eade2f60d.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/testdata/snapshots/linux-amd64-8581cf3a529e5d8b97ea876eade2f60d.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-accel-nvidia.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/testdata/snapshots/linux-amd64-accel-nvidia.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-accel.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/testdata/snapshots/linux-amd64-accel.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-amd-ryzen-1600.tar.gz: -------------------------------------------------------------------------------- 1 | linux-amd64-61797caf7c0b6bca1725ad7975ed4773.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-intel-xeon-L5640.tar.gz: -------------------------------------------------------------------------------- 1 | linux-amd64-8581cf3a529e5d8b97ea876eade2f60d.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-amd64-offlineCPUs.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/testdata/snapshots/linux-amd64-offlineCPUs.tar.gz -------------------------------------------------------------------------------- /testdata/snapshots/linux-arm64-c288e0776090cd558ef793b2a4e61939.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaypipes/ghw/e75a40d23a6cb2e7314ccedd4a20a2e92542332f/testdata/snapshots/linux-arm64-c288e0776090cd558ef793b2a4e61939.tar.gz -------------------------------------------------------------------------------- /testdata/testdata.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use and distribution licensed under the Apache license version 2. 3 | // 4 | // See the COPYING file in the root project directory for full text. 5 | // 6 | 7 | package testdata 8 | 9 | import ( 10 | "fmt" 11 | "path/filepath" 12 | "runtime" 13 | ) 14 | 15 | func SnapshotsDirectory() (string, error) { 16 | _, file, _, ok := runtime.Caller(0) 17 | if !ok { 18 | return "", fmt.Errorf("Cannot retrieve testdata directory") 19 | } 20 | basedir := filepath.Dir(file) 21 | return filepath.Join(basedir, "snapshots"), nil 22 | } 23 | 24 | func SamplesDirectory() (string, error) { 25 | _, file, _, ok := runtime.Caller(0) 26 | if !ok { 27 | return "", fmt.Errorf("Cannot retrieve testdata directory") 28 | } 29 | basedir := filepath.Dir(file) 30 | return filepath.Join(basedir, "samples"), nil 31 | } 32 | 33 | func PCIDBChroot() string { 34 | _, file, _, ok := runtime.Caller(0) 35 | if !ok { 36 | panic("cannot retrieve testdata directory") 37 | } 38 | basedir := filepath.Dir(file) 39 | return filepath.Join(basedir, "usr", "share", "hwdata", "pci.ids") 40 | } 41 | --------------------------------------------------------------------------------