diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8884c2b8..92383195 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,8 +75,8 @@ jobs: GHW_TESTING_SKIP_GPU: "1" run: go test -v ./... - windows-2022: - runs-on: windows-2022 + windows-latest: + runs-on: windows-latest strategy: matrix: go: ['1.23', '1.24', '1.24'] @@ -119,8 +119,8 @@ jobs: # the tests have block skipped because we cannot get meaningful information # about the block devices in the Github Actions Runner virtual machines. So # this is really just a test of whether the library builds on MacOS 13. - macos-13: - runs-on: macos-13 + macos-latest: + runs-on: macos-latest strategy: matrix: go: ['1.22', '1.23'] diff --git a/.gitignore b/.gitignore index 34d0d840..a155166d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ coverage*.* *~ +bin/ diff --git a/Makefile b/Makefile index 75d2bcc8..b55e9dfb 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,35 @@ -.PHONY: test +VERSION ?= $(shell git describe --tags --always --dirty) + +.PHONY: test clean vet fmt fmtcheck build run + +bin/ghwc: + @cd cmd/ghwc && go build -o ../../bin/ghwc main.go && cd ../../ + +# If the first argument is "run"... +ifeq (run,$(firstword $(MAKECMDGOALS))) + # use the rest as arguments for "run" + RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + # ...and turn them into do-nothing targets + $(eval $(RUN_ARGS):;@:) +endif + +build: clean bin/ghwc + +run: build + @bin/ghwc $(RUN_ARGS) + test: vet go test -v ./... -.PHONY: fmt fmt: @echo "Running gofmt on all sources..." @gofmt -s -l -w . -.PHONY: fmtcheck fmtcheck: @bash -c "diff -u <(echo -n) <(gofmt -d .)" -.PHONY: vet vet: go vet ./... + +clean: + @rm -f bin/ghwc diff --git a/README.md b/README.md index 48dedafe..ac4e7873 100644 --- a/README.md +++ b/README.md @@ -1332,59 +1332,75 @@ cpu, err := ghw.CPU(ghw.WithPathOverrides(ghw.PathOverrides{ ### Reading hardware information from a `ghw` snapshot (Linux only) -The `ghw-snapshot` tool can create a snapshot of a host's hardware information. - -Please read [`SNAPSHOT.md`](SNAPSHOT.md) to learn about creating snapshots with -the `ghw-snapshot` tool. - -You can make `ghw` read hardware information from a snapshot created with -`ghw-snapshot` using environment variables or programmatically. - -Use the `GHW_SNAPSHOT_PATH` environment variable to specify the filepath to a -snapshot that `ghw` will read to determine hardware information. All the needed -chroot changes will be automatically performed. By default, the snapshot is -unpacked into a temporary directory managed by `ghw`. This temporary directory -is automatically deleted when `ghw` is finished reading the snapshot. - -Three other environment variables are relevant if and only if `GHW_SNAPSHOT_PATH` -is not empty: - -* `GHW_SNAPSHOT_ROOT` let users specify the directory on which the snapshot - should be unpacked. This moves the ownership of that directory from `ghw` to - users. For this reason, `ghw` will *not* automatically clean up the content - unpacked into `GHW_SNAPSHOT_ROOT`. -* `GHW_SNAPSHOT_EXCLUSIVE` tells `ghw` that the directory is meant only to - contain the given snapshot, thus `ghw` will *not* attempt to unpack it unless - the directory is empty. You can use both `GHW_SNAPSHOT_ROOT` and - `GHW_SNAPSHOT_EXCLUSIVE` to make sure `ghw` unpacks the snapshot only once - regardless of how many `ghw` packages (e.g. cpu, memory) access it. Set the - value of this environment variable to any non-empty string. -* `GHW_SNAPSHOT_PRESERVE` tells `ghw` not to clean up the unpacked snapshot. - Set the value of this environment variable to any non-empty string. +The `ghwc snapshot` command creates a snapshot of a host's hardware information. -```go -cpu, err := ghw.CPU(ghw.WithSnapshot(ghw.SnapshotOptions{ - Path: "/path/to/linux-amd64-d4771ed3300339bc75f856be09fc6430.tar.gz", -})) - - -myRoot := "/my/safe/directory" -cpu, err := ghw.CPU(ghw.WithSnapshot(ghw.SnapshotOptions{ - Path: "/path/to/linux-amd64-d4771ed3300339bc75f856be09fc6430.tar.gz", - Root: &myRoot, -})) +Use the `ghwc -s` flag to supply a path to a snapshot to read with the `ghwc` +command-line program. -myOtherRoot := "/my/other/safe/directory" -cpu, err := ghw.CPU(ghw.WithSnapshot(ghw.SnapshotOptions{ - Path: "/path/to/linux-amd64-d4771ed3300339bc75f856be09fc6430.tar.gz", - Root: &myOtherRoot, - Exclusive: true, -})) +``` +$ ghwc -s testdata/snapshots/linux-amd64-amd-ryzen-1600.tar.gz +block storage (8 disks, 3TB physical storage) + dm-0 SSD (90GB) Unknown [@unknown (node #0)] vendor=unknown + dm-1 Unknown (16GB) Unknown [@unknown (node #0)] vendor=unknown + dm-2 SSD (60GB) Unknown [@unknown (node #0)] vendor=unknown + dm-3 SSD (13GB) Unknown [@unknown (node #0)] vendor=unknown + dm-4 SSD (436GB) Unknown [@unknown (node #0)] vendor=unknown + sda SSD (239GB) SCSI [@unknown (node #0)] vendor=unknown + sda1 (128MB) [unknown] + sda2 (384MB) [unknown] + sda3 (238GB) [unknown] + sdb HDD (932GB) SCSI [@unknown (node #0)] vendor=unknown + sdb1 (2GB) [unknown] + sdb2 (930GB) [unknown] + sdc SSD (466GB) SCSI [@unknown (node #0)] vendor=unknown + sdc1 (32GB) [unknown] + sdc2 (434GB) [unknown] +cpu (1 physical package, 6 cores, 12 hardware threads) + physical package #0 (6 cores, 12 hardware threads) + processor core #0 (2 threads), logical processors [0 6] + processor core #1 (2 threads), logical processors [1 7] + processor core #5 (2 threads), logical processors [4 10] + processor core #6 (2 threads), logical processors [5 11] + processor core #2 (2 threads), logical processors [2 8] + processor core #4 (2 threads), logical processors [3 9] + capabilities: [msr pae mce cx8 apic sep + mtrr pge mca cmov pat pse36 + clflush mmx fxsr sse sse2 ht + syscall nx mmxext fxsr_opt pdpe1gb rdtscp + lm constant_tsc art rep_good nopl nonstop_tsc + extd_apicid aperfmperf eagerfpu pni pclmulqdq monitor + ssse3 fma cx16 sse4_1 sse4_2 movbe + popcnt aes xsave avx f16c rdrand + lahf_lm cmp_legacy svm extapic cr8_legacy abm + sse4a misalignsse 3dnowprefetch osvw skinit wdt + tce topoext perfctr_core perfctr_nb bpext perfctr_l2 + hw_pstate sme retpoline_amd ssbd ibpb vmmcall + fsgsbase bmi1 avx2 smep bmi2 rdseed + adx smap clflushopt sha_ni xsaveopt xsavec + xgetbv1 clzero irperf xsaveerptr arat npt + lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid + decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif + overflow_recov succor smca] +gpu (1 graphics card) + card #0 @0000:0a:00.0 -> driver: '' class: 'Display controller' vendor: 'Advanced Micro Devices, Inc. [AMD/ATI]' product: 'Turks XT [Radeon HD 6670/7670]' +processing accelerators (0 devices) +memory (32GB physical, 32GB usable) +net (4 NICs) + enp3s0 + enp6s0f0 + enp6s0f1 + enp8s0 +topology NUMA (0 nodes) +chassis type=unknown vendor=unknown version=unknown +bios vendor=unknown version=unknown +baseboard vendor=unknown version=unknown product=unknown +product family=unknown name=unknown vendor=unknown sku=unknown version=unknown +PCI (43 devices) ``` ### Creating snapshots -You can create `ghw` snapshots using the `ghw-snapshot` tool or +You can create `ghw` snapshots using the `ghwc snapshot` command or programmatically using the `pkg/snapshot` package. Below is an example of creating a `ghw` snapshot using the `pkg/snapshot` diff --git a/SNAPSHOT.md b/SNAPSHOT.md index 696a3ea6..ddd81574 100644 --- a/SNAPSHOT.md +++ b/SNAPSHOT.md @@ -7,10 +7,11 @@ the paths ghw cares about. The snapshot concept was introduced [to make ghw easi ## Create and consume snapshot -The recommended way to create snapshots for ghw is to use the `ghw-snapshot` tool. +The recommended way to create snapshots for ghw is to use the `ghwc snapshot` command. + This tool is maintained by the ghw authors, and snapshots created with this tool are guaranteed to work. -To consume the ghw snapshots, please check the `README.md` document. +To display hardware information from a ghw snapshot, use the `ghwc -s` flag, passing the filepath to the snapshot to use. ## Snapshot design and definitions @@ -21,10 +22,6 @@ expect, we recommend to check also the [project issues](https://github.com/jaypi ### Scope ghw supports snapshots only on linux platforms. This restriction may be lifted in future releases. -Snapshots must be consumable in the following supported ways: - -1. (way 1) from docker (or podman), mounting them as volumes. See `hack/run-against-snapshot.sh` -2. (way 2) using the environment variables `GHW_SNAPSHOT_*`. See `README.md` for the full documentation. Other combinations are possible, but are unsupported and may stop working any time. You should depend only on the supported ways to consume snapshots. @@ -42,4 +39,3 @@ Stemming from the use cases, the snapshot content must have the following proper It must be noted that trivially cloning subtrees from `/proc` and `/sys` and creating a tarball out of them doesn't work because both pseudo filesystems make use of symlinks, and [docker doesn't really play nice with symlinks](https://github.com/jaypipes/ghw/commit/f8ffd4d24e62eb9017511f072ccf51f13d4a3399). This conflcits with (way 1) above. - diff --git a/alias.go b/alias.go index 2909bead..44895300 100644 --- a/alias.go +++ b/alias.go @@ -24,11 +24,12 @@ import ( "github.com/jaypipes/ghw/pkg/usb" ) +// DEPRECATED: Please use Option type WithOption = option.Option +type Option = option.Option var ( WithChroot = option.WithChroot - WithSnapshot = option.WithSnapshot WithAlerter = option.WithAlerter WithNullAlerter = option.WithNullAlerter // match the existing environ variable to minimize surprises @@ -37,8 +38,6 @@ var ( WithPathOverrides = option.WithPathOverrides ) -type SnapshotOptions = option.SnapshotOptions - type PathOverrides = option.PathOverrides type CPUInfo = cpu.Info diff --git a/cmd/ghw-snapshot/command/read.go b/cmd/ghw-snapshot/command/read.go deleted file mode 100644 index aea3ba6e..00000000 --- a/cmd/ghw-snapshot/command/read.go +++ /dev/null @@ -1,47 +0,0 @@ -// -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. -// - -package command - -import ( - "errors" - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/jaypipes/ghw" - ghwcontext "github.com/jaypipes/ghw/pkg/context" -) - -var readCmd = &cobra.Command{ - Use: "read", - Short: "Reads a new ghw snapshot", - RunE: doRead, -} - -// doRead reads a ghw snapshot from the input snapshot path argument -func doRead(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return errors.New("supply a single argument with the filepath to the snapshot you wish to read") - } - inPath := args[0] - if _, err := os.Stat(inPath); err != nil { - return err - } - os.Setenv("GHW_SNAPSHOT_PATH", inPath) - ctx := ghwcontext.New() - - return ctx.Do(func() error { - info, err := ghw.Host() - fmt.Println(info.String()) - return err - }) -} - -func init() { - rootCmd.AddCommand(readCmd) -} diff --git a/cmd/ghw-snapshot/command/root.go b/cmd/ghw-snapshot/command/root.go deleted file mode 100644 index 668a9875..00000000 --- a/cmd/ghw-snapshot/command/root.go +++ /dev/null @@ -1,59 +0,0 @@ -// -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. -// - -package command - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var ( - debug bool -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "ghw-snapshot", - Short: "ghw-snapshot - create and read ghw snapshots.", - Long: ` - __ __ __ -.-----.| |--.--.--.--.______.-----.-----.---.-.-----.-----.| |--.-----.| |_ -| _ || | | | |______|__ --| | _ | _ |__ --|| | _ || _| -|___ ||__|__|________| |_____|__|__|___._| __|_____||__|__|_____||____| -|_____| |__| - -Create and read ghw snapshots. - -https://github.com/jaypipes/ghw -`, - RunE: doCreate, -} - -// Execute adds all child commands to the root command and sets flags -// appropriately. This is called by main.main(). It only needs to happen once -// to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func trace(msg string, args ...interface{}) { - if !debug { - return - } - fmt.Printf(msg, args...) -} - -func init() { - rootCmd.PersistentFlags().BoolVar( - &debug, "debug", false, "Enable or disable debug mode", - ) -} diff --git a/cmd/ghw-snapshot/main.go b/cmd/ghw-snapshot/main.go deleted file mode 100644 index ed929342..00000000 --- a/cmd/ghw-snapshot/main.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build linux -// +build linux - -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. - -package main - -import ( - "github.com/jaypipes/ghw/cmd/ghw-snapshot/command" -) - -func main() { - command.Execute() -} diff --git a/cmd/ghwc/commands/accelerator.go b/cmd/ghwc/commands/accelerator.go index 24cf28dc..1eb32d1d 100644 --- a/cmd/ghwc/commands/accelerator.go +++ b/cmd/ghwc/commands/accelerator.go @@ -23,7 +23,8 @@ var acceleratorCmd = &cobra.Command{ // showAccelerator show processing accelerators information for the host system. func showAccelerator(cmd *cobra.Command, args []string) error { - accel, err := ghw.Accelerator() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + accel, err := ghw.Accelerator(opts...) if err != nil { return errors.Wrap(err, "error getting Accelerator info") } diff --git a/cmd/ghwc/commands/baseboard.go b/cmd/ghwc/commands/baseboard.go index a45da486..7961bf40 100644 --- a/cmd/ghwc/commands/baseboard.go +++ b/cmd/ghwc/commands/baseboard.go @@ -23,7 +23,8 @@ var baseboardCmd = &cobra.Command{ // showBaseboard shows baseboard information for the host system. func showBaseboard(cmd *cobra.Command, args []string) error { - baseboard, err := ghw.Baseboard() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + baseboard, err := ghw.Baseboard(opts...) if err != nil { return errors.Wrap(err, "error getting baseboard info") } diff --git a/cmd/ghwc/commands/bios.go b/cmd/ghwc/commands/bios.go index f877cb77..5d4f28b1 100644 --- a/cmd/ghwc/commands/bios.go +++ b/cmd/ghwc/commands/bios.go @@ -23,7 +23,8 @@ var biosCmd = &cobra.Command{ // showBIOS shows BIOS host system. func showBIOS(cmd *cobra.Command, args []string) error { - bios, err := ghw.BIOS() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + bios, err := ghw.BIOS(opts...) if err != nil { return errors.Wrap(err, "error getting BIOS info") } diff --git a/cmd/ghwc/commands/block.go b/cmd/ghwc/commands/block.go index cc407f6d..63a26503 100644 --- a/cmd/ghwc/commands/block.go +++ b/cmd/ghwc/commands/block.go @@ -23,7 +23,8 @@ var blockCmd = &cobra.Command{ // showBlock show block storage information for the host system. func showBlock(cmd *cobra.Command, args []string) error { - block, err := ghw.Block() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + block, err := ghw.Block(opts...) if err != nil { return errors.Wrap(err, "error getting block device info") } diff --git a/cmd/ghwc/commands/chassis.go b/cmd/ghwc/commands/chassis.go index 9a10e51d..eac56ed5 100644 --- a/cmd/ghwc/commands/chassis.go +++ b/cmd/ghwc/commands/chassis.go @@ -23,7 +23,8 @@ var chassisCmd = &cobra.Command{ // showChassis shows chassis information for the host system. func showChassis(cmd *cobra.Command, args []string) error { - chassis, err := ghw.Chassis() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + chassis, err := ghw.Chassis(opts...) if err != nil { return errors.Wrap(err, "error getting chassis info") } diff --git a/cmd/ghwc/commands/cpu.go b/cmd/ghwc/commands/cpu.go index 572ec975..3173bc70 100644 --- a/cmd/ghwc/commands/cpu.go +++ b/cmd/ghwc/commands/cpu.go @@ -25,7 +25,8 @@ var cpuCmd = &cobra.Command{ // showCPU show CPU information for the host system. func showCPU(cmd *cobra.Command, args []string) error { - cpu, err := ghw.CPU() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + cpu, err := ghw.CPU(opts...) if err != nil { return errors.Wrap(err, "error getting CPU info") } diff --git a/cmd/ghwc/commands/gpu.go b/cmd/ghwc/commands/gpu.go index 3ec7a96c..eadb9bea 100644 --- a/cmd/ghwc/commands/gpu.go +++ b/cmd/ghwc/commands/gpu.go @@ -23,7 +23,8 @@ var gpuCmd = &cobra.Command{ // showGPU show graphics/GPU information for the host system. func showGPU(cmd *cobra.Command, args []string) error { - gpu, err := ghw.GPU() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + gpu, err := ghw.GPU(opts...) if err != nil { return errors.Wrap(err, "error getting GPU info") } diff --git a/cmd/ghwc/commands/memory.go b/cmd/ghwc/commands/memory.go index 5de0bd9d..46d02b6d 100644 --- a/cmd/ghwc/commands/memory.go +++ b/cmd/ghwc/commands/memory.go @@ -21,7 +21,8 @@ var memoryCmd = &cobra.Command{ // showMemory show memory information for the host system. func showMemory(cmd *cobra.Command, args []string) error { - mem, err := ghw.Memory() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + mem, err := ghw.Memory(opts...) if err != nil { return errors.Wrap(err, "error getting memory info") } diff --git a/cmd/ghwc/commands/net.go b/cmd/ghwc/commands/net.go index 28c4454e..da34b6bf 100644 --- a/cmd/ghwc/commands/net.go +++ b/cmd/ghwc/commands/net.go @@ -23,7 +23,8 @@ var netCmd = &cobra.Command{ // showNetwork show network information for the host system. func showNetwork(cmd *cobra.Command, args []string) error { - net, err := ghw.Network() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + net, err := ghw.Network(opts...) if err != nil { return errors.Wrap(err, "error getting network info") } diff --git a/cmd/ghwc/commands/pci.go b/cmd/ghwc/commands/pci.go index 8e8dc792..7d261526 100644 --- a/cmd/ghwc/commands/pci.go +++ b/cmd/ghwc/commands/pci.go @@ -21,7 +21,8 @@ var pciCmd = &cobra.Command{ // showPCI shows information for PCI devices on the host system. func showPCI(cmd *cobra.Command, args []string) error { - pci, err := ghw.PCI() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + pci, err := ghw.PCI(opts...) if err != nil { return errors.Wrap(err, "error getting PCI info") } diff --git a/cmd/ghwc/commands/product.go b/cmd/ghwc/commands/product.go index c7376165..4f7511d0 100644 --- a/cmd/ghwc/commands/product.go +++ b/cmd/ghwc/commands/product.go @@ -23,7 +23,8 @@ var productCmd = &cobra.Command{ // showProduct shows product information for the host system. func showProduct(cmd *cobra.Command, args []string) error { - product, err := ghw.Product() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + product, err := ghw.Product(opts...) if err != nil { return errors.Wrap(err, "error getting product info") } diff --git a/cmd/ghwc/commands/root.go b/cmd/ghwc/commands/root.go index badb4e3b..4775f916 100644 --- a/cmd/ghwc/commands/root.go +++ b/cmd/ghwc/commands/root.go @@ -7,20 +7,30 @@ package commands import ( + "context" "fmt" "os" "github.com/jaypipes/ghw" + "github.com/jaypipes/ghw/pkg/option" + "github.com/jaypipes/ghw/pkg/snapshot" "github.com/pkg/errors" "github.com/spf13/cobra" ) +type optKey string + +const ( + optsKey optKey = "ghwc.opts" +) + const ( outputFormatHuman = "human" outputFormatJSON = "json" outputFormatYAML = "yaml" usageOutputFormat = `Output format. Choices are 'json','yaml', and 'human'.` + usageSnapshotPath = `Specify path to snapshot.` ) var ( @@ -34,7 +44,8 @@ var ( outputFormatJSON, outputFormatYAML, } - pretty bool + snapshotPath string + pretty bool ) // rootCmd represents the base command when called without any subcommands @@ -42,8 +53,7 @@ var rootCmd = &cobra.Command{ Use: "ghwc", Short: "ghwc - Discover hardware information.", Args: validateRootCommand, - Long: ` - __ + Long: ` __ .-----. | |--. .--.--.--. | _ | | | | | | | |___ | |__|__| |________| @@ -53,7 +63,30 @@ Discover hardware information. https://github.com/jaypipes/ghw `, - RunE: showAll, + PersistentPreRunE: doPreRun, + RunE: showAll, + SilenceUsage: true, +} + +func init() { + rootCmd.PersistentFlags().BoolVar( + &debug, "debug", false, "Enable or disable debug mode", + ) + rootCmd.PersistentFlags().StringVarP( + &outputFormat, + "format", "f", + outputFormatHuman, + usageOutputFormat, + ) + rootCmd.PersistentFlags().BoolVar( + &pretty, "pretty", false, "When outputting JSON, use indentation", + ) + rootCmd.PersistentFlags().StringVarP( + &snapshotPath, + "snapshot-path", "s", + "", + usageSnapshotPath, + ) } func showAll(cmd *cobra.Command, args []string) error { @@ -106,7 +139,6 @@ func Execute(v string, bh string, bd string) { buildDate = bd if err := rootCmd.Execute(); err != nil { - fmt.Println(err) os.Exit(1) } } @@ -120,26 +152,48 @@ func haveValidOutputFormat() bool { return false } +func validateSnapshotPath() error { + if _, err := os.Stat(snapshotPath); err != nil { + return fmt.Errorf("invalid snapshot path: %w", err) + } + return nil +} + // validateRootCommand ensures any CLI options or arguments are valid, // returning an error if not func validateRootCommand(rootCmd *cobra.Command, args []string) error { if !haveValidOutputFormat() { return fmt.Errorf("invalid output format %q", outputFormat) } + if snapshotPath != "" { + if err := validateSnapshotPath(); err != nil { + return err + } + } return nil } -func init() { - rootCmd.PersistentFlags().BoolVar( - &debug, "debug", false, "Enable or disable debug mode", - ) - rootCmd.PersistentFlags().StringVarP( - &outputFormat, - "format", "f", - outputFormatHuman, - usageOutputFormat, - ) - rootCmd.PersistentFlags().BoolVar( - &pretty, "pretty", false, "When outputting JSON, use indentation", - ) +func doPreRun(cmd *cobra.Command, args []string) error { + opts := []option.Option{} + if snapshotPath != "" { + // unpack the snapshot into a tempdir and clean up this tempdir after + // the run... + unpackDir, err := os.MkdirTemp("", "ghw-snap-*") + if err != nil { + return err + } + err = snapshot.UnpackInto(snapshotPath, unpackDir) + if err != nil { + return err + } + opts = append(opts, ghw.WithChroot(unpackDir)) + cmd.PersistentPostRunE = func(c *cobra.Command, args []string) error { + _ = os.RemoveAll(unpackDir) + return nil + } + } + ctx := context.TODO() + ctx = context.WithValue(ctx, optsKey, opts) + cmd.SetContext(ctx) + return nil } diff --git a/cmd/ghw-snapshot/command/create.go b/cmd/ghwc/commands/snapshot.go similarity index 75% rename from cmd/ghw-snapshot/command/create.go rename to cmd/ghwc/commands/snapshot.go index 091d3c90..7a31be00 100644 --- a/cmd/ghw-snapshot/command/create.go +++ b/cmd/ghwc/commands/snapshot.go @@ -4,7 +4,7 @@ // See the COPYING file in the root project directory for full text. // -package command +package commands import ( "crypto/md5" @@ -15,6 +15,7 @@ import ( "github.com/spf13/cobra" + "github.com/jaypipes/ghw" "github.com/jaypipes/ghw/pkg/snapshot" ) @@ -23,14 +24,23 @@ var ( outPath string ) -var createCmd = &cobra.Command{ - Use: "create", +var snapshotCmd = &cobra.Command{ + Use: "snapshot", Short: "Creates a new ghw snapshot", - RunE: doCreate, + RunE: doSnapshot, } -// doCreate creates a ghw snapshot -func doCreate(cmd *cobra.Command, args []string) error { +func trace(msg string, args ...interface{}) { + if !debug { + return + } + fmt.Printf(msg, args...) +} + +// doSnapshot creates a ghw snapshot +func doSnapshot(cmd *cobra.Command, args []string) error { + opts := cmd.Context().Value(optsKey).([]ghw.Option) + _ = opts scratchDir, err := os.MkdirTemp("", "ghw-snapshot") if err != nil { return err @@ -75,11 +85,11 @@ func defaultOutPath() (string, error) { } func init() { - createCmd.PersistentFlags().StringVarP( + snapshotCmd.PersistentFlags().StringVarP( &outPath, "out", "o", outPath, "Path to place snapshot. Defaults to file in current directory with name $OS-$ARCH-$HASHSYSTEMNAME.tar.gz", ) - rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(snapshotCmd) } diff --git a/cmd/ghwc/commands/topology.go b/cmd/ghwc/commands/topology.go index 6ab613d4..cc9618e3 100644 --- a/cmd/ghwc/commands/topology.go +++ b/cmd/ghwc/commands/topology.go @@ -23,7 +23,8 @@ var topologyCmd = &cobra.Command{ // showTopology show topology information for the host system. func showTopology(cmd *cobra.Command, args []string) error { - topology, err := ghw.Topology() + opts := cmd.Context().Value(optsKey).([]ghw.Option) + topology, err := ghw.Topology(opts...) if err != nil { return errors.Wrap(err, "error getting topology info") } diff --git a/host.go b/host.go index 034798d6..6116199c 100644 --- a/host.go +++ b/host.go @@ -9,9 +9,6 @@ package ghw import ( "fmt" - "github.com/jaypipes/ghw/pkg/context" - "github.com/jaypipes/ghw/pkg/usb" - "github.com/jaypipes/ghw/pkg/accelerator" "github.com/jaypipes/ghw/pkg/baseboard" "github.com/jaypipes/ghw/pkg/bios" @@ -25,12 +22,12 @@ import ( "github.com/jaypipes/ghw/pkg/pci" "github.com/jaypipes/ghw/pkg/product" "github.com/jaypipes/ghw/pkg/topology" + "github.com/jaypipes/ghw/pkg/usb" ) // HostInfo is a wrapper struct containing information about the host system's // memory, block storage, CPU, etc type HostInfo struct { - ctx *context.Context Memory *memory.Info `json:"memory"` Block *block.Info `json:"block"` CPU *cpu.Info `json:"cpu"` @@ -48,9 +45,7 @@ type HostInfo struct { // Host returns a pointer to a HostInfo struct that contains fields with // information about the host system's CPU, memory, network devices, etc -func Host(opts ...*WithOption) (*HostInfo, error) { - ctx := context.New(opts...) - +func Host(opts ...Option) (*HostInfo, error) { memInfo, err := memory.New(opts...) if err != nil { return nil, err @@ -105,7 +100,6 @@ func Host(opts ...*WithOption) (*HostInfo, error) { } return &HostInfo{ - ctx: ctx, CPU: cpuInfo, Memory: memInfo, Block: blockInfo, @@ -146,11 +140,11 @@ func (info *HostInfo) String() string { // YAMLString returns a string with the host information formatted as YAML // under a top-level "host:" key func (i *HostInfo) YAMLString() string { - return marshal.SafeYAML(i.ctx, i) + return marshal.SafeYAML(i) } // JSONString returns a string with the host information formatted as JSON // under a top-level "host:" key func (i *HostInfo) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, i, indent) + return marshal.SafeJSON(i, indent) } diff --git a/pkg/accelerator/accelerator.go b/pkg/accelerator/accelerator.go index b51ef2e2..b41f71a8 100644 --- a/pkg/accelerator/accelerator.go +++ b/pkg/accelerator/accelerator.go @@ -9,7 +9,6 @@ package accelerator import ( "fmt" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/pci" @@ -37,17 +36,14 @@ func (dev *AcceleratorDevice) String() string { } type Info struct { - ctx *context.Context Devices []*AcceleratorDevice `json:"devices"` } // New returns a pointer to an Info struct that contains information about the // accelerator devices on the host system -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + info := &Info{} + if err := info.load(opt...); err != nil { return nil, err } return info, nil @@ -74,11 +70,11 @@ type acceleratorPrinter struct { // YAMLString returns a string with the processing accelerators information formatted as YAML // under a top-level "accelerator:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, acceleratorPrinter{i}) + return marshal.SafeYAML(acceleratorPrinter{i}) } // JSONString returns a string with the processing accelerators information formatted as JSON // under a top-level "accelerator:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, acceleratorPrinter{i}, indent) + return marshal.SafeJSON(acceleratorPrinter{i}, indent) } diff --git a/pkg/accelerator/accelerator_linux.go b/pkg/accelerator/accelerator_linux.go index 39d7eecb..728ec31e 100644 --- a/pkg/accelerator/accelerator_linux.go +++ b/pkg/accelerator/accelerator_linux.go @@ -6,7 +6,7 @@ package accelerator import ( - "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/pci" ) @@ -31,13 +31,17 @@ var ( } ) -func (i *Info) load() error { +func (i *Info) load(opt ...option.Option) error { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } accelDevices := make([]*AcceleratorDevice, 0) // get PCI devices - pciInfo, err := pci.New(context.WithContext(i.ctx)) + pciInfo, err := pci.New(opt...) if err != nil { - i.ctx.Warn("error loading PCI information: %s", err) + opts.Warn("error loading PCI information: %s", err) return nil } diff --git a/pkg/accelerator/accelerator_linux_test.go b/pkg/accelerator/accelerator_linux_test.go index 6a42c487..c27456b6 100644 --- a/pkg/accelerator/accelerator_linux_test.go +++ b/pkg/accelerator/accelerator_linux_test.go @@ -27,20 +27,12 @@ func testScenario(t *testing.T, filename string, expectedDevs int) { workstationSnapshot := filepath.Join(testdataPath, filename) - tmpRoot, err := os.MkdirTemp("", "ghw-accelerator-testing-*") - if err != nil { - t.Fatalf("Unable to create temporary directory: %v", err) - } - - _, err = snapshot.UnpackInto(workstationSnapshot, tmpRoot, 0) + tmpRoot := t.TempDir() + err = snapshot.UnpackInto(workstationSnapshot, tmpRoot) if err != nil { t.Fatalf("Unable to unpack %q into %q: %v", workstationSnapshot, tmpRoot, err) } - defer func() { - _ = snapshot.Cleanup(tmpRoot) - }() - info, err := accelerator.New(option.WithChroot(tmpRoot)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) diff --git a/pkg/accelerator/accelerator_stub.go b/pkg/accelerator/accelerator_stub.go index 303f27d7..b8ebe3f4 100644 --- a/pkg/accelerator/accelerator_stub.go +++ b/pkg/accelerator/accelerator_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opt ...option.Option) error { return errors.New("accelerator.Info.load not implemented on " + runtime.GOOS) } diff --git a/pkg/accelerator/accelerator_windows.go b/pkg/accelerator/accelerator_windows.go index f8532ecb..155fd355 100644 --- a/pkg/accelerator/accelerator_windows.go +++ b/pkg/accelerator/accelerator_windows.go @@ -5,7 +5,9 @@ package accelerator -func (i *Info) load() error { +import "github.com/jaypipes/ghw/pkg/option" + +func (i *Info) load(opt ...option.Option) error { i.Devices = []*AcceleratorDevice{} return nil } diff --git a/pkg/baseboard/baseboard.go b/pkg/baseboard/baseboard.go index ac4bf41a..646b6bb4 100644 --- a/pkg/baseboard/baseboard.go +++ b/pkg/baseboard/baseboard.go @@ -7,7 +7,6 @@ package baseboard import ( - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" @@ -15,7 +14,6 @@ import ( // Info defines baseboard release information type Info struct { - ctx *context.Context AssetTag string `json:"asset_tag"` SerialNumber string `json:"serial_number"` Vendor string `json:"vendor"` @@ -52,10 +50,13 @@ func (i *Info) String() string { // New returns a pointer to an Info struct containing information about the // host's baseboard -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -70,11 +71,11 @@ type baseboardPrinter struct { // YAMLString returns a string with the baseboard information formatted as YAML // under a top-level "dmi:" key func (info *Info) YAMLString() string { - return marshal.SafeYAML(info.ctx, baseboardPrinter{info}) + return marshal.SafeYAML(baseboardPrinter{info}) } // JSONString returns a string with the baseboard information formatted as JSON // under a top-level "baseboard:" key func (info *Info) JSONString(indent bool) string { - return marshal.SafeJSON(info.ctx, baseboardPrinter{info}, indent) + return marshal.SafeJSON(baseboardPrinter{info}, indent) } diff --git a/pkg/baseboard/baseboard_linux.go b/pkg/baseboard/baseboard_linux.go index c8c598d4..39c9519f 100644 --- a/pkg/baseboard/baseboard_linux.go +++ b/pkg/baseboard/baseboard_linux.go @@ -7,14 +7,15 @@ package baseboard import ( "github.com/jaypipes/ghw/pkg/linuxdmi" + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { - i.AssetTag = linuxdmi.Item(i.ctx, "board_asset_tag") - i.SerialNumber = linuxdmi.Item(i.ctx, "board_serial") - i.Vendor = linuxdmi.Item(i.ctx, "board_vendor") - i.Version = linuxdmi.Item(i.ctx, "board_version") - i.Product = linuxdmi.Item(i.ctx, "board_name") +func (i *Info) load(opts *option.Options) error { + i.AssetTag = linuxdmi.Item(opts, "board_asset_tag") + i.SerialNumber = linuxdmi.Item(opts, "board_serial") + i.Vendor = linuxdmi.Item(opts, "board_vendor") + i.Version = linuxdmi.Item(opts, "board_version") + i.Product = linuxdmi.Item(opts, "board_name") return nil } diff --git a/pkg/baseboard/baseboard_stub.go b/pkg/baseboard/baseboard_stub.go index f5b14691..0d61ae51 100644 --- a/pkg/baseboard/baseboard_stub.go +++ b/pkg/baseboard/baseboard_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("baseboardFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/baseboard/baseboard_windows.go b/pkg/baseboard/baseboard_windows.go index cdb50b4d..428d5e68 100644 --- a/pkg/baseboard/baseboard_windows.go +++ b/pkg/baseboard/baseboard_windows.go @@ -7,6 +7,8 @@ package baseboard import ( "github.com/yusufpapurcu/wmi" + + "github.com/jaypipes/ghw/pkg/option" ) const wqlBaseboard = "SELECT Manufacturer, SerialNumber, Tag, Version, Product FROM Win32_BaseBoard" @@ -19,7 +21,7 @@ type win32Baseboard struct { Product *string } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting data from WMI var win32BaseboardDescriptions []win32Baseboard if err := wmi.Query(wqlBaseboard, &win32BaseboardDescriptions); err != nil { diff --git a/pkg/bios/bios.go b/pkg/bios/bios.go index 85a7c64b..4b36df8f 100644 --- a/pkg/bios/bios.go +++ b/pkg/bios/bios.go @@ -9,7 +9,6 @@ package bios import ( "fmt" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" @@ -17,7 +16,6 @@ import ( // Info defines BIOS release information type Info struct { - ctx *context.Context Vendor string `json:"vendor"` Version string `json:"version"` Date string `json:"date"` @@ -49,10 +47,13 @@ func (i *Info) String() string { // New returns a pointer to a Info struct containing information // about the host's BIOS -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -67,11 +68,11 @@ type biosPrinter struct { // YAMLString returns a string with the BIOS information formatted as YAML // under a top-level "dmi:" key func (info *Info) YAMLString() string { - return marshal.SafeYAML(info.ctx, biosPrinter{info}) + return marshal.SafeYAML(biosPrinter{info}) } // JSONString returns a string with the BIOS information formatted as JSON // under a top-level "bios:" key func (info *Info) JSONString(indent bool) string { - return marshal.SafeJSON(info.ctx, biosPrinter{info}, indent) + return marshal.SafeJSON(biosPrinter{info}, indent) } diff --git a/pkg/bios/bios_linux.go b/pkg/bios/bios_linux.go index 9788f4f7..716e5918 100644 --- a/pkg/bios/bios_linux.go +++ b/pkg/bios/bios_linux.go @@ -5,12 +5,15 @@ package bios -import "github.com/jaypipes/ghw/pkg/linuxdmi" +import ( + "github.com/jaypipes/ghw/pkg/linuxdmi" + "github.com/jaypipes/ghw/pkg/option" +) -func (i *Info) load() error { - i.Vendor = linuxdmi.Item(i.ctx, "bios_vendor") - i.Version = linuxdmi.Item(i.ctx, "bios_version") - i.Date = linuxdmi.Item(i.ctx, "bios_date") +func (i *Info) load(opts *option.Options) error { + i.Vendor = linuxdmi.Item(opts, "bios_vendor") + i.Version = linuxdmi.Item(opts, "bios_version") + i.Date = linuxdmi.Item(opts, "bios_date") return nil } diff --git a/pkg/bios/bios_stub.go b/pkg/bios/bios_stub.go index 5307b4a0..2b4fb6bb 100644 --- a/pkg/bios/bios_stub.go +++ b/pkg/bios/bios_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("biosFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/bios/bios_windows.go b/pkg/bios/bios_windows.go index cb8244e0..45b77ed7 100644 --- a/pkg/bios/bios_windows.go +++ b/pkg/bios/bios_windows.go @@ -7,6 +7,8 @@ package bios import ( "github.com/yusufpapurcu/wmi" + + "github.com/jaypipes/ghw/pkg/option" ) const wqlBIOS = "SELECT InstallDate, Manufacturer, Version FROM CIM_BIOSElement" @@ -17,7 +19,7 @@ type win32BIOS struct { Version *string } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting data from WMI var win32BIOSDescriptions []win32BIOS if err := wmi.Query(wqlBIOS, &win32BIOSDescriptions); err != nil { diff --git a/pkg/block/block.go b/pkg/block/block.go index 016b5c7d..ab11915d 100644 --- a/pkg/block/block.go +++ b/pkg/block/block.go @@ -13,7 +13,6 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/unitutil" @@ -276,7 +275,6 @@ type Partition struct { // Info describes all disk drives and partitions in the host system. type Info struct { - ctx *context.Context // TotalSizeBytes contains the total amount of storage, in bytes, on the // host system. TotalSizeBytes uint64 `json:"total_size_bytes"` @@ -292,10 +290,13 @@ type Info struct { // New returns a pointer to an Info struct that describes the block storage // resources of the host system. -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -407,11 +408,11 @@ type blockPrinter struct { // YAMLString returns a string with the block information formatted as YAML // under a top-level "block:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, blockPrinter{i}) + return marshal.SafeYAML(blockPrinter{i}) } // JSONString returns a string with the block information formatted as JSON // under a top-level "block:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, blockPrinter{i}, indent) + return marshal.SafeJSON(blockPrinter{i}, indent) } diff --git a/pkg/block/block_darwin.go b/pkg/block/block_darwin.go index c6b6c266..e9ab875a 100644 --- a/pkg/block/block_darwin.go +++ b/pkg/block/block_darwin.go @@ -12,6 +12,7 @@ import ( "path" "strings" + "github.com/jaypipes/ghw/pkg/option" "github.com/pkg/errors" "howett.net/plist" ) @@ -146,7 +147,11 @@ func getIoregPlist(ioDeviceTreePath string) (*ioregPlist, error) { return nil, errors.Wrapf(err, "ioreg unmarshal for %q failed", ioDeviceTreePath) } if len(data) != 1 { - err := errors.Errorf("ioreg unmarshal resulted in %d I/O device tree nodes (expected 1)", len(data)) + err := errors.Errorf( + "ioreg unmarshal resulted in %d I/O device tree nodes "+ + "for path %q (expected 1)", + len(data), ioDeviceTreePath, + ) return nil, err } @@ -206,9 +211,9 @@ func storageControllerFromPlist(infoPlist *diskUtilInfoPlist) StorageController return sc } -func (info *Info) load() error { - if !info.ctx.EnableTools { - return fmt.Errorf("EnableTools=false on darwin disables block support entirely.") +func (info *Info) load(opts *option.Options) error { + if opts.DisableTools { + return fmt.Errorf("DisableTools=true on darwin disables block support entirely.") } listPlist, err := getDiskUtilListPlist() diff --git a/pkg/block/block_linux.go b/pkg/block/block_linux.go index 6399317a..48a9dc21 100644 --- a/pkg/block/block_linux.go +++ b/pkg/block/block_linux.go @@ -13,8 +13,8 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) @@ -22,9 +22,8 @@ const ( sectorSize = 512 ) -func (i *Info) load() error { - paths := linuxpath.New(i.ctx) - i.Disks = disks(i.ctx, paths) +func (i *Info) load(opts *option.Options) error { + i.Disks = disks(opts) var tsb uint64 for _, d := range i.Disks { tsb += d.SizeBytes @@ -219,12 +218,16 @@ func diskWWN(paths *linuxpath.Paths, disk string) string { // but just the name. In other words, "sda", not "/dev/sda" and "nvme0n1" not // "/dev/nvme0n1") and returns a slice of pointers to Partition structs // representing the partitions in that disk -func diskPartitions(ctx *context.Context, paths *linuxpath.Paths, disk string) []*Partition { +func diskPartitions( + opts *option.Options, + paths *linuxpath.Paths, + disk string, +) []*Partition { out := make([]*Partition, 0) path := filepath.Join(paths.SysBlock, disk) files, err := os.ReadDir(path) if err != nil { - ctx.Warn("failed to read disk partitions: %s\n", err) + opts.Warn("failed to read disk partitions: %s\n", err) return out } for _, file := range files { @@ -315,7 +318,8 @@ func diskIsRemovable(paths *linuxpath.Paths, disk string) bool { return removable == "1" } -func disks(ctx *context.Context, paths *linuxpath.Paths) []*Disk { +func disks(opts *option.Options) []*Disk { + paths := linuxpath.New(opts) // In Linux, we could use the fdisk, lshw or blockdev commands to list disk // information, however all of these utilities require root privileges to // run. We can get all of this information by examining the /sys/block @@ -334,7 +338,7 @@ func disks(ctx *context.Context, paths *linuxpath.Paths) []*Disk { // Only reclassify HDD to SSD if non-rotational to avoid changing already correct types. // This addresses changed kernel behavior where rotational detection may be unreliable, // where some kernels report CD-ROM drives as non-rotational, incorrectly classifying them as SSD. - if !diskIsRotational(ctx, paths, dname) && driveType == DRIVE_TYPE_HDD { + if !diskIsRotational(opts, paths, dname) && driveType == DRIVE_TYPE_HDD { driveType = DRIVE_TYPE_SSD } size := diskSizeBytes(paths, dname) @@ -368,7 +372,7 @@ func disks(ctx *context.Context, paths *linuxpath.Paths) []*Disk { WWNNoExtension: wwnNoExtension, } - parts := diskPartitions(ctx, paths, dname) + parts := diskPartitions(opts, paths, dname) // Map this Disk object into the Partition... for _, part := range parts { part.Disk = d @@ -422,9 +426,9 @@ func diskTypes(dname string) ( return driveType, storageController } -func diskIsRotational(ctx *context.Context, paths *linuxpath.Paths, devName string) bool { +func diskIsRotational(opts *option.Options, paths *linuxpath.Paths, devName string) bool { path := filepath.Join(paths.SysBlock, devName, "queue", "rotational") - contents := util.SafeIntFromFile(ctx, path) + contents := util.SafeIntFromFile(opts, path) return contents == 1 } diff --git a/pkg/block/block_linux_test.go b/pkg/block/block_linux_test.go index f4d0a262..6301ef5c 100644 --- a/pkg/block/block_linux_test.go +++ b/pkg/block/block_linux_test.go @@ -16,7 +16,6 @@ import ( "reflect" "testing" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" @@ -200,9 +199,9 @@ func TestDiskPartLabel(t *testing.T) { } baseDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(baseDir) - ctx := context.New() - ctx.Chroot = baseDir - paths := linuxpath.New(ctx) + opts := &option.Options{} + opts.Chroot = baseDir + paths := linuxpath.New(opts) partLabel := "TEST_LABEL_GHW" _ = os.MkdirAll(paths.SysBlock, 0755) @@ -231,9 +230,9 @@ func TestDiskFSLabel(t *testing.T) { } baseDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(baseDir) - ctx := context.New() - ctx.Chroot = baseDir - paths := linuxpath.New(ctx) + opts := &option.Options{} + opts.Chroot = baseDir + paths := linuxpath.New(opts) fsLabel := "TEST_LABEL_GHW" _ = os.MkdirAll(paths.SysBlock, 0755) @@ -262,9 +261,9 @@ func TestDiskTypeUdev(t *testing.T) { } baseDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(baseDir) - ctx := context.New() - ctx.Chroot = baseDir - paths := linuxpath.New(ctx) + opts := &option.Options{} + opts.Chroot = baseDir + paths := linuxpath.New(opts) expectedPartType := "ext4" _ = os.MkdirAll(paths.SysBlock, 0755) @@ -293,9 +292,9 @@ func TestDiskPartUUID(t *testing.T) { } baseDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(baseDir) - ctx := context.New() - ctx.Chroot = baseDir - paths := linuxpath.New(ctx) + opts := &option.Options{} + opts.Chroot = baseDir + paths := linuxpath.New(opts) partUUID := "11111111-1111-1111-1111-111111111111" _ = os.MkdirAll(paths.SysBlock, 0755) @@ -325,9 +324,10 @@ func TestLoopDevicesWithOption(t *testing.T) { } baseDir, _ := os.MkdirTemp("", "test") defer os.RemoveAll(baseDir) - ctx := context.New(option.WithNullAlerter(), option.WithDisableTools()) - ctx.Chroot = baseDir - paths := linuxpath.New(ctx) + opts := &option.Options{} + opts.Chroot = baseDir + opts.DisableTools = true + paths := linuxpath.New(opts) fsType := "ext4" expectedLoopName := "loop0" loopNotUsed := "loop1" @@ -348,7 +348,7 @@ func TestLoopDevicesWithOption(t *testing.T) { _ = os.WriteFile(filepath.Join(paths.SysBlock, expectedLoopName, loopPartitionName, "dev"), []byte("259:0\n"), 0644) _ = os.WriteFile(filepath.Join(paths.SysBlock, expectedLoopName, loopPartitionName, "size"), []byte("102400\n"), 0644) _ = os.WriteFile(filepath.Join(paths.RunUdevData, "b259:0"), []byte(fmt.Sprintf("E:ID_FS_TYPE=%s\n", fsType)), 0644) - d := disks(ctx, paths) + d := disks(opts) // There should be one disk, the other should be ignored due to 0 size if len(d) != 1 { t.Fatalf("expected one disk device but the function reported %d", len(d)) diff --git a/pkg/block/block_test.go b/pkg/block/block_test.go index 9b9e2d2e..b97c3fbc 100644 --- a/pkg/block/block_test.go +++ b/pkg/block/block_test.go @@ -84,6 +84,9 @@ func TestBlock(t *testing.T) { } func TestBlockMarshalUnmarshal(t *testing.T) { + if _, ok := os.LookupEnv("GHW_TESTING_SKIP_BLOCK"); ok { + t.Skip("Skipping block tests.") + } blocks, err := block.New() if err != nil { t.Fatalf("Expected no error creating block.Info, but got %v", err) @@ -106,6 +109,9 @@ type blockData struct { } func TestBlockUnmarshal(t *testing.T) { + if _, ok := os.LookupEnv("GHW_TESTING_SKIP_BLOCK"); ok { + t.Skip("Skipping block tests.") + } testdataPath, err := testdata.SamplesDirectory() if err != nil { t.Fatalf("Expected nil err when detecting the samples directory, but got %v", err) diff --git a/pkg/block/block_windows.go b/pkg/block/block_windows.go index cbb052a8..18190cf5 100644 --- a/pkg/block/block_windows.go +++ b/pkg/block/block_windows.go @@ -11,6 +11,7 @@ import ( "github.com/yusufpapurcu/wmi" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) @@ -108,7 +109,7 @@ type win32PhysicalDisk struct { MediaType physicalDiskMediaType } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { win32DiskDriveDescriptions, err := getDiskDrives() if err != nil { return err diff --git a/pkg/chassis/chassis.go b/pkg/chassis/chassis.go index a7667bbc..cb67bb5d 100644 --- a/pkg/chassis/chassis.go +++ b/pkg/chassis/chassis.go @@ -7,7 +7,6 @@ package chassis import ( - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" @@ -56,7 +55,6 @@ var ( // Info defines chassis release information type Info struct { - ctx *context.Context AssetTag string `json:"asset_tag"` SerialNumber string `json:"serial_number"` Type string `json:"type"` @@ -89,10 +87,13 @@ func (i *Info) String() string { // New returns a pointer to a Info struct containing information // about the host's chassis -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -107,11 +108,11 @@ type chassisPrinter struct { // YAMLString returns a string with the chassis information formatted as YAML // under a top-level "dmi:" key func (info *Info) YAMLString() string { - return marshal.SafeYAML(info.ctx, chassisPrinter{info}) + return marshal.SafeYAML(chassisPrinter{info}) } // JSONString returns a string with the chassis information formatted as JSON // under a top-level "chassis:" key func (info *Info) JSONString(indent bool) string { - return marshal.SafeJSON(info.ctx, chassisPrinter{info}, indent) + return marshal.SafeJSON(chassisPrinter{info}, indent) } diff --git a/pkg/chassis/chassis_linux.go b/pkg/chassis/chassis_linux.go index 00f64de6..6834a74a 100644 --- a/pkg/chassis/chassis_linux.go +++ b/pkg/chassis/chassis_linux.go @@ -7,20 +7,21 @@ package chassis import ( "github.com/jaypipes/ghw/pkg/linuxdmi" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) -func (i *Info) load() error { - i.AssetTag = linuxdmi.Item(i.ctx, "chassis_asset_tag") - i.SerialNumber = linuxdmi.Item(i.ctx, "chassis_serial") - i.Type = linuxdmi.Item(i.ctx, "chassis_type") +func (i *Info) load(opts *option.Options) error { + i.AssetTag = linuxdmi.Item(opts, "chassis_asset_tag") + i.SerialNumber = linuxdmi.Item(opts, "chassis_serial") + i.Type = linuxdmi.Item(opts, "chassis_type") typeDesc, found := chassisTypeDescriptions[i.Type] if !found { typeDesc = util.UNKNOWN } i.TypeDescription = typeDesc - i.Vendor = linuxdmi.Item(i.ctx, "chassis_vendor") - i.Version = linuxdmi.Item(i.ctx, "chassis_version") + i.Vendor = linuxdmi.Item(opts, "chassis_vendor") + i.Version = linuxdmi.Item(opts, "chassis_version") return nil } diff --git a/pkg/chassis/chassis_stub.go b/pkg/chassis/chassis_stub.go index 0e3fd94b..819d6bf3 100644 --- a/pkg/chassis/chassis_stub.go +++ b/pkg/chassis/chassis_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("chassisFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/chassis/chassis_windows.go b/pkg/chassis/chassis_windows.go index 834fc74b..94e11b84 100644 --- a/pkg/chassis/chassis_windows.go +++ b/pkg/chassis/chassis_windows.go @@ -8,6 +8,7 @@ package chassis import ( "github.com/yusufpapurcu/wmi" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) @@ -25,7 +26,7 @@ type win32Chassis struct { Version *string } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting data from WMI var win32ChassisDescriptions []win32Chassis if err := wmi.Query(wqlChassis, &win32ChassisDescriptions); err != nil { diff --git a/pkg/context/context.go b/pkg/context/context.go deleted file mode 100644 index fb8de528..00000000 --- a/pkg/context/context.go +++ /dev/null @@ -1,178 +0,0 @@ -// -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. -// - -package context - -import ( - "fmt" - - "github.com/jaypipes/ghw/pkg/option" - "github.com/jaypipes/ghw/pkg/snapshot" -) - -// Context contains the merged set of configuration switches that act as an -// execution context when calling internal discovery methods -type Context struct { - Chroot string - EnableTools bool - SnapshotPath string - SnapshotRoot string - SnapshotExclusive bool - PathOverrides option.PathOverrides - snapshotUnpackedPath string - alert option.Alerter - err error -} - -// WithContext returns an option.Option that contains a pre-existing Context -// struct. This is useful for some internal code that sets up snapshots. -func WithContext(ctx *Context) *option.Option { - return &option.Option{ - Context: ctx, - } -} - -// Exists returns true if the supplied (merged) Option already contains -// a context. -// -// TODO(jaypipes): We can get rid of this when we combine the option and -// context packages, which will make it easier to detect the presence of a -// pre-setup Context. -func Exists(opt *option.Option) bool { - return opt != nil && opt.Context != nil -} - -// New returns a Context struct pointer that has had various options set on it -func New(opts ...*option.Option) *Context { - merged := option.Merge(opts...) - var ctx *Context - if merged.Context != nil { - var castOK bool - ctx, castOK = merged.Context.(*Context) - if !castOK { - panic("passed in a non-Context for the WithContext() function!") - } - return ctx - } - ctx = &Context{ - alert: option.EnvOrDefaultAlerter(), - Chroot: *merged.Chroot, - } - - if merged.Snapshot != nil { - ctx.SnapshotPath = merged.Snapshot.Path - // root is optional, so a extra check is warranted - if merged.Snapshot.Root != nil { - ctx.SnapshotRoot = *merged.Snapshot.Root - } - ctx.SnapshotExclusive = merged.Snapshot.Exclusive - } - - if merged.Alerter != nil { - ctx.alert = merged.Alerter - } - - if merged.EnableTools != nil { - ctx.EnableTools = *merged.EnableTools - } - - if merged.PathOverrides != nil { - ctx.PathOverrides = merged.PathOverrides - } - - // New is not allowed to return error - it would break the established API. - // so the only way out is to actually do the checks here and record the error, - // and return it later, at the earliest possible occasion, in Setup() - if ctx.SnapshotPath != "" && ctx.Chroot != option.DefaultChroot { - // The env/client code supplied a value, but we are will overwrite it when unpacking shapshots! - ctx.err = fmt.Errorf("Conflicting options: chroot %q and snapshot path %q", ctx.Chroot, ctx.SnapshotPath) - } - return ctx -} - -// FromEnv returns a Context that has been populated from the environs or -// default options values -func FromEnv() *Context { - chrootVal := option.EnvOrDefaultChroot() - enableTools := option.EnvOrDefaultTools() - snapPathVal := option.EnvOrDefaultSnapshotPath() - snapRootVal := option.EnvOrDefaultSnapshotRoot() - snapExclusiveVal := option.EnvOrDefaultSnapshotExclusive() - return &Context{ - Chroot: chrootVal, - EnableTools: enableTools, - SnapshotPath: snapPathVal, - SnapshotRoot: snapRootVal, - SnapshotExclusive: snapExclusiveVal, - } -} - -// Do wraps a Setup/Teardown pair around the given function -func (ctx *Context) Do(fn func() error) error { - err := ctx.Setup() - if err != nil { - return err - } - defer func() { - err := ctx.Teardown() - if err != nil { - ctx.Warn("teardown error: %v", err) - } - }() - return fn() -} - -// Setup prepares the extra optional data a Context may use. -// `Context`s are ready to use once returned by `New`. Optional features, -// like snapshot unpacking, may require extra steps. Run `Setup` to perform them. -// You should call `Setup` just once. It is safe to call `Setup` if you don't make -// use of optional extra features - `Setup` will do nothing. -func (ctx *Context) Setup() error { - if ctx.err != nil { - return ctx.err - } - if ctx.SnapshotPath == "" { - // nothing to do! - return nil - } - - var err error - root := ctx.SnapshotRoot - if root == "" { - root, err = snapshot.Unpack(ctx.SnapshotPath) - if err == nil { - ctx.snapshotUnpackedPath = root - } - } else { - var flags uint - if ctx.SnapshotExclusive { - flags |= snapshot.OwnTargetDirectory - } - _, err = snapshot.UnpackInto(ctx.SnapshotPath, root, flags) - } - if err != nil { - return err - } - - ctx.Chroot = root - return nil -} - -// Teardown releases any resource acquired by Setup. -// You should always call `Teardown` if you called `Setup` to free any resources -// acquired by `Setup`. Check `Do` for more automated management. -func (ctx *Context) Teardown() error { - if ctx.snapshotUnpackedPath == "" { - // if the client code provided the unpack directory, - // then it is also in charge of the cleanup. - return nil - } - return snapshot.Cleanup(ctx.snapshotUnpackedPath) -} - -func (ctx *Context) Warn(msg string, args ...interface{}) { - ctx.alert.Printf("WARNING: "+msg, args...) -} diff --git a/pkg/context/context_test.go b/pkg/context/context_test.go deleted file mode 100644 index e12d3f74..00000000 --- a/pkg/context/context_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. -// - -package context_test - -import ( - "os" - "testing" - - "github.com/jaypipes/ghw/pkg/context" - "github.com/jaypipes/ghw/pkg/option" -) - -const ( - testDataSnapshot = "../snapshot/testdata.tar.gz" -) - -// nolint: gocyclo -func TestSnapshotContext(t *testing.T) { - ctx := context.New(option.WithSnapshot(option.SnapshotOptions{ - Path: testDataSnapshot, - })) - - var uncompressedDir string - err := ctx.Do(func() error { - uncompressedDir = ctx.Chroot - return nil - }) - - if uncompressedDir == "" { - t.Fatalf("Expected the uncompressed dir path to not be empty") - } - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - if _, err = os.Stat(uncompressedDir); !os.IsNotExist(err) { - t.Fatalf("Expected the uncompressed dir to be deleted: %s", uncompressedDir) - } -} diff --git a/pkg/cpu/cpu.go b/pkg/cpu/cpu.go index 5c5e6bab..ce333442 100644 --- a/pkg/cpu/cpu.go +++ b/pkg/cpu/cpu.go @@ -9,7 +9,6 @@ package cpu import ( "fmt" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" ) @@ -122,7 +121,6 @@ func (p *Processor) String() string { // Info describes all central processing unit (CPU) functionality on a host. // Returned by the `ghw.CPU()` function. type Info struct { - ctx *context.Context // TotalCores is the total number of physical cores the host system // contains TotalCores uint32 `json:"total_cores"` @@ -140,10 +138,13 @@ type Info struct { // New returns a pointer to an Info struct that contains information about the // CPUs on the host system -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -183,11 +184,11 @@ type cpuPrinter struct { // YAMLString returns a string with the cpu information formatted as YAML // under a top-level "cpu:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, cpuPrinter{i}) + return marshal.SafeYAML(cpuPrinter{i}) } // JSONString returns a string with the cpu information formatted as JSON // under a top-level "cpu:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, cpuPrinter{i}, indent) + return marshal.SafeJSON(cpuPrinter{i}, indent) } diff --git a/pkg/cpu/cpu_darwin.go b/pkg/cpu/cpu_darwin.go index e4353e47..fca42834 100644 --- a/pkg/cpu/cpu_darwin.go +++ b/pkg/cpu/cpu_darwin.go @@ -2,10 +2,13 @@ package cpu import ( "fmt" - "github.com/pkg/errors" "os/exec" "strconv" "strings" + + "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) var ( @@ -13,7 +16,7 @@ var ( sysctlOutput = make(map[string]string) // store all the sysctl output ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { err := populateSysctlOutput() if err != nil { return errors.Wrap(err, "unable to populate sysctl map") diff --git a/pkg/cpu/cpu_linux.go b/pkg/cpu/cpu_linux.go index f403d29d..1b8b03c2 100644 --- a/pkg/cpu/cpu_linux.go +++ b/pkg/cpu/cpu_linux.go @@ -16,8 +16,8 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) @@ -26,8 +26,8 @@ var ( onlineFile = "online" ) -func (i *Info) load() error { - i.Processors = processorsGet(i.ctx) +func (i *Info) load(opts *option.Options) error { + i.Processors = processorsGet(opts) var totCores uint32 var totThreads uint32 for _, p := range i.Processors { @@ -41,10 +41,10 @@ func (i *Info) load() error { return nil } -func processorsGet(ctx *context.Context) []*Processor { - paths := linuxpath.New(ctx) +func processorsGet(opts *option.Options) []*Processor { + paths := linuxpath.New(opts) - lps := logicalProcessorsFromProcCPUInfo(ctx) + lps := logicalProcessorsFromProcCPUInfo(opts) // keyed by processor ID (physical_package_id) procs := map[int]*Processor{} @@ -53,7 +53,7 @@ func processorsGet(ctx *context.Context) []*Processor { // processor pseudodirs are of the pattern /sys/devices/system/cpu/cpu{N} fnames, err := os.ReadDir(paths.SysDevicesSystemCPU) if err != nil { - ctx.Warn("failed to read /sys/devices/system/cpu: %s", err) + opts.Warn("failed to read /sys/devices/system/cpu: %s", err) return []*Processor{} } for _, fname := range fnames { @@ -64,26 +64,26 @@ func processorsGet(ctx *context.Context) []*Processor { lpID, err := strconv.Atoi(matches[1]) if err != nil { - ctx.Warn("failed to find numeric logical processor ID: %s", err) + opts.Warn("failed to find numeric logical processor ID: %s", err) continue } onlineFilePath := filepath.Join(paths.SysDevicesSystemCPU, fmt.Sprintf("cpu%d", lpID), onlineFile) if _, err := os.Stat(onlineFilePath); err == nil { - if util.SafeIntFromFile(ctx, onlineFilePath) == 0 { + if util.SafeIntFromFile(opts, onlineFilePath) == 0 { continue } } else if errors.Is(err, os.ErrNotExist) { // Assume the CPU is online if the online state file doesn't exist // (as is the case with older snapshots) } - procID := processorIDFromLogicalProcessorID(ctx, lpID) + procID := processorIDFromLogicalProcessorID(opts, lpID) proc, found := procs[procID] if !found { proc = &Processor{ID: procID} lp, ok := lps[lpID] if !ok { - ctx.Warn( + opts.Warn( "failed to find attributes for logical processor %d", lpID, ) @@ -120,7 +120,7 @@ func processorsGet(ctx *context.Context) []*Processor { procs[procID] = proc } - coreID := coreIDFromLogicalProcessorID(ctx, lpID) + coreID := coreIDFromLogicalProcessorID(opts, lpID) core := proc.CoreByID(coreID) if core == nil { core = &ProcessorCore{ @@ -155,37 +155,37 @@ func processorsGet(ctx *context.Context) []*Processor { // processorIDFromLogicalProcessorID returns the processor physical package ID // for the supplied logical processor ID -func processorIDFromLogicalProcessorID(ctx *context.Context, lpID int) int { - paths := linuxpath.New(ctx) +func processorIDFromLogicalProcessorID(opts *option.Options, lpID int) int { + paths := linuxpath.New(opts) // Fetch CPU ID path := filepath.Join( paths.SysDevicesSystemCPU, fmt.Sprintf("cpu%d", lpID), "topology", "physical_package_id", ) - return util.SafeIntFromFile(ctx, path) + return util.SafeIntFromFile(opts, path) } // coreIDFromLogicalProcessorID returns the core ID for the supplied logical // processor ID -func coreIDFromLogicalProcessorID(ctx *context.Context, lpID int) int { - paths := linuxpath.New(ctx) +func coreIDFromLogicalProcessorID(opts *option.Options, lpID int) int { + paths := linuxpath.New(opts) // Fetch CPU ID path := filepath.Join( paths.SysDevicesSystemCPU, fmt.Sprintf("cpu%d", lpID), "topology", "core_id", ) - return util.SafeIntFromFile(ctx, path) + return util.SafeIntFromFile(opts, path) } -func CoresForNode(ctx *context.Context, nodeID int) ([]*ProcessorCore, error) { +func CoresForNode(opts *option.Options, nodeID int) ([]*ProcessorCore, error) { // The /sys/devices/system/node/nodeX directory contains a subdirectory // called 'cpuX' for each logical processor assigned to the node. Each of // those subdirectories contains a topology subdirectory which has a // core_id file that indicates the 0-based identifier of the physical core // the logical processor (hardware thread) is on. - paths := linuxpath.New(ctx) + paths := linuxpath.New(opts) path := filepath.Join( paths.SysDevicesSystemNode, fmt.Sprintf("node%d", nodeID), @@ -227,7 +227,7 @@ func CoresForNode(ctx *context.Context, nodeID int) ([]*ProcessorCore, error) { cpuPath := filepath.Join(path, filename) procID, err := strconv.Atoi(filename[3:]) if err != nil { - ctx.Warn( + opts.Warn( "failed to determine procID from %s. Expected integer after 3rd char.", filename, ) @@ -235,7 +235,7 @@ func CoresForNode(ctx *context.Context, nodeID int) ([]*ProcessorCore, error) { } onlineFilePath := filepath.Join(cpuPath, onlineFile) if _, err := os.Stat(onlineFilePath); err == nil { - if util.SafeIntFromFile(ctx, onlineFilePath) == 0 { + if util.SafeIntFromFile(opts, onlineFilePath) == 0 { continue } } else if errors.Is(err, os.ErrNotExist) { @@ -243,7 +243,7 @@ func CoresForNode(ctx *context.Context, nodeID int) ([]*ProcessorCore, error) { // (as is the case with older snapshots) } coreIDPath := filepath.Join(cpuPath, "topology", "core_id") - coreID := util.SafeIntFromFile(ctx, coreIDPath) + coreID := util.SafeIntFromFile(opts, coreIDPath) core := findCoreByID(coreID) core.LogicalProcessors = append( core.LogicalProcessors, @@ -339,9 +339,9 @@ type logicalProcessor struct { // with blank line-separated blocks of colon-delimited attribute name/value // pairs for a specific logical processor on the host. func logicalProcessorsFromProcCPUInfo( - ctx *context.Context, + opts *option.Options, ) map[int]*logicalProcessor { - paths := linuxpath.New(ctx) + paths := linuxpath.New(opts) r, err := os.Open(paths.ProcCpuinfo) if err != nil { return nil @@ -362,7 +362,7 @@ func logicalProcessorsFromProcCPUInfo( // collected for this logical processor block lpIDstr, ok := lpAttrs["processor"] if !ok { - ctx.Warn("expected to find 'processor' key in /proc/cpuinfo attributes") + opts.Warn("expected to find 'processor' key in /proc/cpuinfo attributes") continue } lpID, _ := strconv.Atoi(lpIDstr) diff --git a/pkg/cpu/cpu_linux_test.go b/pkg/cpu/cpu_linux_test.go index 6eb748fd..d6c2ffec 100644 --- a/pkg/cpu/cpu_linux_test.go +++ b/pkg/cpu/cpu_linux_test.go @@ -16,6 +16,7 @@ import ( "github.com/jaypipes/ghw/pkg/cpu" "github.com/jaypipes/ghw/pkg/option" + "github.com/jaypipes/ghw/pkg/snapshot" "github.com/jaypipes/ghw/pkg/topology" "github.com/jaypipes/ghw/testdata" ) @@ -33,9 +34,13 @@ func TestArmCPU(t *testing.T) { multiNumaSnapshot := filepath.Join(testdataPath, "linux-arm64-c288e0776090cd558ef793b2a4e61939.tar.gz") - info, err := cpu.New(option.WithSnapshot(option.SnapshotOptions{ - Path: multiNumaSnapshot, - })) + unpackDir := t.TempDir() + err = snapshot.UnpackInto(multiNumaSnapshot, unpackDir) + if err != nil { + t.Fatal(err) + } + + info, err := cpu.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) @@ -98,9 +103,13 @@ func TestCheckCPUTopologyFilesForOfflineCPU(t *testing.T) { } os.Stderr = wErr - info, err := cpu.New(option.WithSnapshot(option.SnapshotOptions{ - Path: offlineCPUSnapshot, - })) + unpackDir := t.TempDir() + err = snapshot.UnpackInto(offlineCPUSnapshot, unpackDir) + if err != nil { + t.Fatal(err) + } + + info, err := cpu.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) } @@ -139,9 +148,13 @@ func TestNumCoresAmongOfflineCPUs(t *testing.T) { if err != nil { t.Fatalf("Cannot pipe the StdErr. %v", err) } - info, err := topology.New(option.WithSnapshot(option.SnapshotOptions{ - Path: offlineCPUSnapshot, - })) + unpackDir := t.TempDir() + err = snapshot.UnpackInto(offlineCPUSnapshot, unpackDir) + if err != nil { + t.Fatal(err) + } + + info, err := topology.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Error determining node topology. %v", err) } diff --git a/pkg/cpu/cpu_stub.go b/pkg/cpu/cpu_stub.go index 85156069..e4b8a278 100644 --- a/pkg/cpu/cpu_stub.go +++ b/pkg/cpu/cpu_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("cpu.Info.load not implemented on " + runtime.GOOS) } diff --git a/pkg/cpu/cpu_windows.go b/pkg/cpu/cpu_windows.go index 012323c2..036e0293 100644 --- a/pkg/cpu/cpu_windows.go +++ b/pkg/cpu/cpu_windows.go @@ -10,6 +10,8 @@ package cpu import ( "github.com/yusufpapurcu/wmi" + + "github.com/jaypipes/ghw/pkg/option" ) const wmqlProcessor = "SELECT Manufacturer, Name, NumberOfLogicalProcessors, NumberOfCores FROM Win32_Processor" @@ -21,7 +23,7 @@ type win32Processor struct { NumberOfCores uint32 } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting info from WMI var win32descriptions []win32Processor if err := wmi.Query(wmqlProcessor, &win32descriptions); err != nil { diff --git a/pkg/gpu/gpu.go b/pkg/gpu/gpu.go index 65864c7e..329915b8 100644 --- a/pkg/gpu/gpu.go +++ b/pkg/gpu/gpu.go @@ -9,7 +9,6 @@ package gpu import ( "fmt" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/pci" @@ -49,16 +48,14 @@ func (card *GraphicsCard) String() string { } type Info struct { - ctx *context.Context GraphicsCards []*GraphicsCard `json:"cards"` } // New returns a pointer to an Info struct that contains information about the // graphics cards on the host system -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + info := &Info{} + if err := info.load(opt...); err != nil { return nil, err } return info, nil @@ -85,11 +82,11 @@ type gpuPrinter struct { // YAMLString returns a string with the gpu information formatted as YAML // under a top-level "gpu:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, gpuPrinter{i}) + return marshal.SafeYAML(gpuPrinter{i}) } // JSONString returns a string with the gpu information formatted as JSON // under a top-level "gpu:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, gpuPrinter{i}, indent) + return marshal.SafeJSON(gpuPrinter{i}, indent) } diff --git a/pkg/gpu/gpu_linux.go b/pkg/gpu/gpu_linux.go index f61e98bf..ff0bbcd3 100644 --- a/pkg/gpu/gpu_linux.go +++ b/pkg/gpu/gpu_linux.go @@ -6,14 +6,15 @@ package gpu import ( + "fmt" "os" "path/filepath" "regexp" "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/pci" "github.com/jaypipes/ghw/pkg/topology" "github.com/jaypipes/ghw/pkg/util" @@ -33,7 +34,7 @@ GPUInfo.GraphicsCards will be an empty array. ` ) -func (i *Info) load() error { +func (i *Info) load(opt ...option.Option) error { // In Linux, each graphics card is listed under the /sys/class/drm // directory as a symbolic link named "cardN", where N is a zero-based // index of the card in the system. "DRM" stands for Direct Rendering @@ -60,10 +61,18 @@ func (i *Info) load() error { // we follow to gather information about the actual device from the PCI // subsystem (we query the modalias file of the PCI device's sysfs // directory using the `ghw.PCIInfo.GetDevice()` function. - paths := linuxpath.New(i.ctx) + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + pci, err := pci.New(opt...) + if err != nil { + return fmt.Errorf("failed to PCI device database: %w", err) + } + paths := linuxpath.New(opts) links, err := os.ReadDir(paths.SysClassDRM) if err != nil { - i.ctx.Warn(_WARN_NO_SYS_CLASS_DRM) + opts.Warn(_WARN_NO_SYS_CLASS_DRM) return nil } cards := make([]*GraphicsCard, 0) @@ -109,20 +118,15 @@ func (i *Info) load() error { } cards = append(cards, card) } - gpuFillNUMANodes(i.ctx, cards) - gpuFillPCIDevice(i.ctx, cards) + gpuFillNUMANodes(opts, cards) + gpuFillPCIDevice(pci, cards) i.GraphicsCards = cards return nil } // Loops through each GraphicsCard struct and attempts to fill the DeviceInfo // attribute with PCI device information -func gpuFillPCIDevice(ctx *context.Context, cards []*GraphicsCard) { - pci, err := pci.New(context.WithContext(ctx)) - if err != nil { - ctx.Warn("failed to PCI device database: %s", err) - return - } +func gpuFillPCIDevice(pci *pci.Info, cards []*GraphicsCard) { for _, card := range cards { if card.DeviceInfo == nil { card.DeviceInfo = pci.GetDevice(card.Address) @@ -133,9 +137,9 @@ func gpuFillPCIDevice(ctx *context.Context, cards []*GraphicsCard) { // Loops through each GraphicsCard struct and find which NUMA node the card is // affined to, setting the GraphicsCard.Node field accordingly. If the host // system is not a NUMA system, the Node field will be set to nil. -func gpuFillNUMANodes(ctx *context.Context, cards []*GraphicsCard) { - paths := linuxpath.New(ctx) - topo, err := topology.New(context.WithContext(ctx)) +func gpuFillNUMANodes(opts *option.Options, cards []*GraphicsCard) { + paths := linuxpath.New(opts) + topo, err := topology.New() if err != nil { // Problem getting topology information so just set the graphics card's // node to nil @@ -157,7 +161,7 @@ func gpuFillNUMANodes(ctx *context.Context, cards []*GraphicsCard) { "device", "numa_node", ) - nodeIdx := util.SafeIntFromFile(ctx, fpath) + nodeIdx := util.SafeIntFromFile(opts, fpath) if nodeIdx == -1 { continue } diff --git a/pkg/gpu/gpu_linux_test.go b/pkg/gpu/gpu_linux_test.go index 9d4c92a0..73abeb9b 100644 --- a/pkg/gpu/gpu_linux_test.go +++ b/pkg/gpu/gpu_linux_test.go @@ -40,18 +40,12 @@ func TestGPUWithoutNUMANodeInfo(t *testing.T) { // snapshot to fully understand this test. Inspect it using // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-amd-ryzen-1600.tar.gz" ghwc gpu - tmpRoot, err := os.MkdirTemp("", "ghw-gpu-testing-*") - if err != nil { - t.Fatalf("Unable to create temporary directory: %v", err) - } + tmpRoot := t.TempDir() - _, err = snapshot.UnpackInto(workstationSnapshot, tmpRoot, 0) + err = snapshot.UnpackInto(workstationSnapshot, tmpRoot) if err != nil { t.Fatalf("Unable to unpack %q into %q: %v", workstationSnapshot, tmpRoot, err) } - defer func() { - _ = snapshot.Cleanup(tmpRoot) - }() err = os.Remove(filepath.Join(tmpRoot, "/sys/class/drm/card0/device/numa_node")) if err != nil && !errors.Is(err, os.ErrNotExist) { diff --git a/pkg/gpu/gpu_stub.go b/pkg/gpu/gpu_stub.go index 48991ec8..09493562 100644 --- a/pkg/gpu/gpu_stub.go +++ b/pkg/gpu/gpu_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opt ...option.Option) error { return errors.New("gpuFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/gpu/gpu_windows.go b/pkg/gpu/gpu_windows.go index 1a2f4d63..2e08775f 100644 --- a/pkg/gpu/gpu_windows.go +++ b/pkg/gpu/gpu_windows.go @@ -11,6 +11,7 @@ import ( "github.com/jaypipes/pcidb" "github.com/yusufpapurcu/wmi" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/pci" "github.com/jaypipes/ghw/pkg/util" ) @@ -46,7 +47,7 @@ type win32PnPEntity struct { PNPDeviceID string } -func (i *Info) load() error { +func (i *Info) load(opt ...option.Option) error { // Getting data from WMI var win32VideoControllerDescriptions []win32VideoController if err := wmi.Query(wqlVideoController, &win32VideoControllerDescriptions); err != nil { diff --git a/pkg/linuxdmi/dmi_linux.go b/pkg/linuxdmi/dmi_linux.go index 8e6d8302..15c21fb9 100644 --- a/pkg/linuxdmi/dmi_linux.go +++ b/pkg/linuxdmi/dmi_linux.go @@ -10,18 +10,18 @@ import ( "path/filepath" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) -func Item(ctx *context.Context, value string) string { - paths := linuxpath.New(ctx) +func Item(opts *option.Options, value string) string { + paths := linuxpath.New(opts) path := filepath.Join(paths.SysClassDMI, "id", value) b, err := os.ReadFile(path) if err != nil { - ctx.Warn("Unable to read %s: %s\n", value, err) + opts.Warn("Unable to read %s: %s\n", value, err) return util.UNKNOWN } diff --git a/pkg/linuxpath/path_linux.go b/pkg/linuxpath/path_linux.go index 17c0c147..629be51d 100644 --- a/pkg/linuxpath/path_linux.go +++ b/pkg/linuxpath/path_linux.go @@ -9,7 +9,7 @@ import ( "fmt" "path/filepath" - "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/option" ) // PathRoots holds the roots of all the filesystem subtrees @@ -35,21 +35,21 @@ func DefaultPathRoots() PathRoots { // PathRootsFromContext initialize PathRoots from the given Context, // allowing overrides of the canonical default paths. -func PathRootsFromContext(ctx *context.Context) PathRoots { +func PathRootsFromContext(opts *option.Options) PathRoots { roots := DefaultPathRoots() - if pathEtc, ok := ctx.PathOverrides["/etc"]; ok { + if pathEtc, ok := opts.PathOverrides["/etc"]; ok { roots.Etc = pathEtc } - if pathProc, ok := ctx.PathOverrides["/proc"]; ok { + if pathProc, ok := opts.PathOverrides["/proc"]; ok { roots.Proc = pathProc } - if pathRun, ok := ctx.PathOverrides["/run"]; ok { + if pathRun, ok := opts.PathOverrides["/run"]; ok { roots.Run = pathRun } - if pathSys, ok := ctx.PathOverrides["/sys"]; ok { + if pathSys, ok := opts.PathOverrides["/sys"]; ok { roots.Sys = pathSys } - if pathVar, ok := ctx.PathOverrides["/var"]; ok { + if pathVar, ok := opts.PathOverrides["/var"]; ok { roots.Var = pathVar } return roots @@ -76,25 +76,25 @@ type Paths struct { // New returns a new Paths struct containing filepath fields relative to the // supplied Context -func New(ctx *context.Context) *Paths { - roots := PathRootsFromContext(ctx) +func New(opts *option.Options) *Paths { + roots := PathRootsFromContext(opts) return &Paths{ - SysRoot: filepath.Join(ctx.Chroot, roots.Sys), - VarLog: filepath.Join(ctx.Chroot, roots.Var, "log"), - ProcMeminfo: filepath.Join(ctx.Chroot, roots.Proc, "meminfo"), - ProcCpuinfo: filepath.Join(ctx.Chroot, roots.Proc, "cpuinfo"), - ProcMounts: filepath.Join(ctx.Chroot, roots.Proc, "self", "mounts"), - SysKernelMMHugepages: filepath.Join(ctx.Chroot, roots.Sys, "kernel", "mm", "hugepages"), - SysBlock: filepath.Join(ctx.Chroot, roots.Sys, "block"), - SysDevicesSystemNode: filepath.Join(ctx.Chroot, roots.Sys, "devices", "system", "node"), - SysDevicesSystemMemory: filepath.Join(ctx.Chroot, roots.Sys, "devices", "system", "memory"), - SysDevicesSystemCPU: filepath.Join(ctx.Chroot, roots.Sys, "devices", "system", "cpu"), - SysBusPciDevices: filepath.Join(ctx.Chroot, roots.Sys, "bus", "pci", "devices"), - SysBusUsbDevices: filepath.Join(ctx.Chroot, roots.Sys, "bus", "usb", "devices"), - SysClassDRM: filepath.Join(ctx.Chroot, roots.Sys, "class", "drm"), - SysClassDMI: filepath.Join(ctx.Chroot, roots.Sys, "class", "dmi"), - SysClassNet: filepath.Join(ctx.Chroot, roots.Sys, "class", "net"), - RunUdevData: filepath.Join(ctx.Chroot, roots.Run, "udev", "data"), + SysRoot: filepath.Join(opts.Chroot, roots.Sys), + VarLog: filepath.Join(opts.Chroot, roots.Var, "log"), + ProcMeminfo: filepath.Join(opts.Chroot, roots.Proc, "meminfo"), + ProcCpuinfo: filepath.Join(opts.Chroot, roots.Proc, "cpuinfo"), + ProcMounts: filepath.Join(opts.Chroot, roots.Proc, "self", "mounts"), + SysKernelMMHugepages: filepath.Join(opts.Chroot, roots.Sys, "kernel", "mm", "hugepages"), + SysBlock: filepath.Join(opts.Chroot, roots.Sys, "block"), + SysDevicesSystemNode: filepath.Join(opts.Chroot, roots.Sys, "devices", "system", "node"), + SysDevicesSystemMemory: filepath.Join(opts.Chroot, roots.Sys, "devices", "system", "memory"), + SysDevicesSystemCPU: filepath.Join(opts.Chroot, roots.Sys, "devices", "system", "cpu"), + SysBusPciDevices: filepath.Join(opts.Chroot, roots.Sys, "bus", "pci", "devices"), + SysBusUsbDevices: filepath.Join(opts.Chroot, roots.Sys, "bus", "usb", "devices"), + SysClassDRM: filepath.Join(opts.Chroot, roots.Sys, "class", "drm"), + SysClassDMI: filepath.Join(opts.Chroot, roots.Sys, "class", "dmi"), + SysClassNet: filepath.Join(opts.Chroot, roots.Sys, "class", "net"), + RunUdevData: filepath.Join(opts.Chroot, roots.Run, "udev", "data"), } } diff --git a/pkg/linuxpath/path_linux_test.go b/pkg/linuxpath/path_linux_test.go index ca343f8c..83a6000c 100644 --- a/pkg/linuxpath/path_linux_test.go +++ b/pkg/linuxpath/path_linux_test.go @@ -16,7 +16,6 @@ import ( "sort" "testing" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/gpu" "github.com/jaypipes/ghw/pkg/linuxpath" "github.com/jaypipes/ghw/pkg/option" @@ -33,8 +32,8 @@ func TestPathRoot(t *testing.T) { defer os.Unsetenv("GHW_CHROOT") } - ctx := context.FromEnv() - paths := linuxpath.New(ctx) + opts := option.FromEnv() + paths := linuxpath.New(opts) // No environment variable is set for GHW_CHROOT, so pathProcCpuinfo() should // return the default "/proc/cpuinfo" @@ -47,8 +46,8 @@ func TestPathRoot(t *testing.T) { // returns that value os.Setenv("GHW_CHROOT", "/host") - ctx = context.FromEnv() - paths = linuxpath.New(ctx) + opts = option.FromEnv() + paths = linuxpath.New(opts) path = paths.ProcCpuinfo if path != "/host/proc/cpuinfo" { @@ -57,12 +56,12 @@ func TestPathRoot(t *testing.T) { } func TestPathSpecificRoots(t *testing.T) { - ctx := context.New(option.WithPathOverrides(option.PathOverrides{ + opts := option.FromEnv() + opts.PathOverrides = option.PathOverrides{ "/proc": "/host-proc", "/sys": "/host-sys", - })) - - paths := linuxpath.New(ctx) + } + paths := linuxpath.New(opts) path := paths.ProcCpuinfo expectedPath := "/host-proc/cpuinfo" @@ -78,15 +77,14 @@ func TestPathSpecificRoots(t *testing.T) { } func TestPathChrootAndSpecifics(t *testing.T) { - ctx := context.New( - option.WithPathOverrides(option.PathOverrides{ - "/proc": "/host2-proc", - "/sys": "/host2-sys", - }), - option.WithChroot("/redirect"), - ) - - paths := linuxpath.New(ctx) + opts := option.FromEnv() + opts.PathOverrides = option.PathOverrides{ + "/proc": "/host2-proc", + "/sys": "/host2-sys", + } + opts.Chroot = "/redirect" + + paths := linuxpath.New(opts) path := paths.ProcCpuinfo expectedPath := "/redirect/host2-proc/cpuinfo" diff --git a/pkg/marshal/marshal.go b/pkg/marshal/marshal.go index e442d6af..c8567ce7 100644 --- a/pkg/marshal/marshal.go +++ b/pkg/marshal/marshal.go @@ -9,27 +9,23 @@ package marshal import ( "encoding/json" - "github.com/jaypipes/ghw/pkg/context" yaml "gopkg.in/yaml.v3" ) // SafeYAML returns a string after marshalling the supplied parameter into YAML. -func SafeYAML(ctx *context.Context, p interface{}) string { +func SafeYAML(p interface{}) string { b, err := json.Marshal(p) if err != nil { - ctx.Warn("error marshalling JSON: %s", err) return "" } var jsonObj interface{} if err := yaml.Unmarshal(b, &jsonObj); err != nil { - ctx.Warn("error converting JSON to YAML: %s", err) return "" } yb, err := yaml.Marshal(jsonObj) if err != nil { - ctx.Warn("error marshalling YAML: %s", err) return "" } @@ -39,7 +35,7 @@ func SafeYAML(ctx *context.Context, p interface{}) string { // SafeJSON returns a string after marshalling the supplied parameter into // JSON. Accepts an optional argument to trigger pretty/indented formatting of // the JSON string. -func SafeJSON(ctx *context.Context, p interface{}, indent bool) string { +func SafeJSON(p interface{}, indent bool) string { var b []byte var err error if !indent { @@ -48,7 +44,6 @@ func SafeJSON(ctx *context.Context, p interface{}, indent bool) string { b, err = json.MarshalIndent(&p, "", " ") } if err != nil { - ctx.Warn("error marshalling JSON: %s", err) return "" } return string(b) diff --git a/pkg/memory/memory.go b/pkg/memory/memory.go index f58ba8b9..ee35cc9f 100644 --- a/pkg/memory/memory.go +++ b/pkg/memory/memory.go @@ -10,7 +10,6 @@ import ( "fmt" "math" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/unitutil" @@ -78,15 +77,17 @@ func (a *Area) String() string { // Info contains information about the memory on a host system. type Info struct { - ctx *context.Context Area } // New returns an Info struct that describes the memory on a host system. -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -106,11 +107,11 @@ type memoryPrinter struct { // YAMLString returns a string with the memory information formatted as YAML // under a top-level "memory:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, memoryPrinter{i}) + return marshal.SafeYAML(memoryPrinter{i}) } // JSONString returns a string with the memory information formatted as JSON // under a top-level "memory:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, memoryPrinter{i}, indent) + return marshal.SafeJSON(memoryPrinter{i}, indent) } diff --git a/pkg/memory/memory_cache_linux.go b/pkg/memory/memory_cache_linux.go index 12258ea4..802e681b 100644 --- a/pkg/memory/memory_cache_linux.go +++ b/pkg/memory/memory_cache_linux.go @@ -14,12 +14,12 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/unitutil" ) -func CachesForNode(ctx *context.Context, nodeID int) ([]*Cache, error) { +func CachesForNode(opts *option.Options, nodeID int) ([]*Cache, error) { // The /sys/devices/node/nodeX directory contains a subdirectory called // 'cpuX' for each logical processor assigned to the node. Each of those // subdirectories containers a 'cache' subdirectory which contains a number @@ -27,7 +27,7 @@ func CachesForNode(ctx *context.Context, nodeID int) ([]*Cache, error) { // internal 0-based identifier. Those subdirectories contain a number of // files, including 'shared_cpu_list', 'size', and 'type' which we use to // determine cache characteristics. - paths := linuxpath.New(ctx) + paths := linuxpath.New(opts) path := filepath.Join( paths.SysDevicesSystemNode, fmt.Sprintf("node%d", nodeID), @@ -79,14 +79,14 @@ func CachesForNode(ctx *context.Context, nodeID int) ([]*Cache, error) { // The cache information is repeated for each node, so here, we // just ensure that we only have a one Cache object for each // unique combination of level, type and processor map - level := memoryCacheLevel(ctx, paths, nodeID, lpID, cacheIndex) - cacheType := memoryCacheType(ctx, paths, nodeID, lpID, cacheIndex) - sharedCpuMap := memoryCacheSharedCPUMap(ctx, paths, nodeID, lpID, cacheIndex) + level := memoryCacheLevel(opts, paths, nodeID, lpID, cacheIndex) + cacheType := memoryCacheType(opts, paths, nodeID, lpID, cacheIndex) + sharedCpuMap := memoryCacheSharedCPUMap(opts, paths, nodeID, lpID, cacheIndex) cacheKey := fmt.Sprintf("%d-%d-%s", level, cacheType, sharedCpuMap) cache, exists := caches[cacheKey] if !exists { - size := memoryCacheSize(ctx, paths, nodeID, lpID, level) + size := memoryCacheSize(opts, paths, nodeID, lpID, level) cache = &Cache{ Level: uint8(level), Type: cacheType, @@ -114,53 +114,71 @@ func CachesForNode(ctx *context.Context, nodeID int) ([]*Cache, error) { return cacheVals, nil } -func memoryCacheLevel(ctx *context.Context, paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) int { +func memoryCacheLevel( + opts *option.Options, + paths *linuxpath.Paths, + nodeID int, + lpID int, + cacheIndex int, +) int { levelPath := filepath.Join( paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex), "level", ) levelContents, err := os.ReadFile(levelPath) if err != nil { - ctx.Warn("%s", err) + opts.Warn("%s", err) return -1 } // levelContents is now a []byte with the last byte being a newline // character. Trim that off and convert the contents to an integer. level, err := strconv.Atoi(string(levelContents[:len(levelContents)-1])) if err != nil { - ctx.Warn("Unable to parse int from %s", levelContents) + opts.Warn("Unable to parse int from %s", levelContents) return -1 } return level } -func memoryCacheSize(ctx *context.Context, paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) int { +func memoryCacheSize( + opts *option.Options, + paths *linuxpath.Paths, + nodeID int, + lpID int, + cacheIndex int, +) int { sizePath := filepath.Join( paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex), "size", ) sizeContents, err := os.ReadFile(sizePath) if err != nil { - ctx.Warn("%s", err) + opts.Warn("%s", err) return -1 } // size comes as XK\n, so we trim off the K and the newline. size, err := strconv.Atoi(string(sizeContents[:len(sizeContents)-2])) if err != nil { - ctx.Warn("Unable to parse int from %s", sizeContents) + opts.Warn("Unable to parse int from %s", sizeContents) return -1 } return size } -func memoryCacheType(ctx *context.Context, paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) CacheType { +func memoryCacheType( + opts *option.Options, + paths *linuxpath.Paths, + nodeID int, + lpID int, + cacheIndex int, +) CacheType { typePath := filepath.Join( paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex), "type", ) cacheTypeContents, err := os.ReadFile(typePath) if err != nil { - ctx.Warn("%s", err) + opts.Warn("%s", err) return CacheTypeUnified } switch string(cacheTypeContents[:len(cacheTypeContents)-1]) { @@ -173,14 +191,20 @@ func memoryCacheType(ctx *context.Context, paths *linuxpath.Paths, nodeID int, l } } -func memoryCacheSharedCPUMap(ctx *context.Context, paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) string { +func memoryCacheSharedCPUMap( + opts *option.Options, + paths *linuxpath.Paths, + nodeID int, + lpID int, + cacheIndex int, +) string { scpuPath := filepath.Join( paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex), "shared_cpu_map", ) sharedCpuMap, err := os.ReadFile(scpuPath) if err != nil { - ctx.Warn("%s", err) + opts.Warn("%s", err) return "" } return string(sharedCpuMap[:len(sharedCpuMap)-1]) diff --git a/pkg/memory/memory_linux.go b/pkg/memory/memory_linux.go index 031fcdef..b295810e 100644 --- a/pkg/memory/memory_linux.go +++ b/pkg/memory/memory_linux.go @@ -17,8 +17,8 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/unitutil" "github.com/jaypipes/ghw/pkg/util" ) @@ -44,8 +44,8 @@ var ( regexMemoryBlockDirname = regexp.MustCompile(`memory\d+$`) ) -func (i *Info) load() error { - paths := linuxpath.New(i.ctx) +func (i *Info) load(opts *option.Options) error { + paths := linuxpath.New(opts) tub := memTotalUsableBytes(paths) if tub < 1 { return fmt.Errorf("Could not determine total usable bytes of memory") @@ -54,7 +54,7 @@ func (i *Info) load() error { tpb := memTotalPhysicalBytes(paths) i.TotalPhysicalBytes = tpb if tpb < 1 { - i.ctx.Warn(warnCannotDeterminePhysicalMemory) + opts.Warn(warnCannotDeterminePhysicalMemory) i.TotalPhysicalBytes = tub } i.SupportedPageSizes, _ = memorySupportedPageSizes(paths.SysKernelMMHugepages) @@ -72,8 +72,7 @@ func (i *Info) load() error { return nil } -func AreaForNode(ctx *context.Context, nodeID int) (*Area, error) { - paths := linuxpath.New(ctx) +func AreaForNode(paths *linuxpath.Paths, nodeID int) (*Area, error) { path := filepath.Join( paths.SysDevicesSystemNode, fmt.Sprintf("node%d", nodeID), diff --git a/pkg/memory/memory_stub.go b/pkg/memory/memory_stub.go index 6ce99e00..acc0f0f1 100644 --- a/pkg/memory/memory_stub.go +++ b/pkg/memory/memory_stub.go @@ -11,9 +11,11 @@ package memory import ( "runtime" + "github.com/jaypipes/ghw/pkg/option" + "github.com/pkg/errors" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("mem.Info.load not implemented on " + runtime.GOOS) } diff --git a/pkg/memory/memory_windows.go b/pkg/memory/memory_windows.go index 60646552..eb3db45e 100644 --- a/pkg/memory/memory_windows.go +++ b/pkg/memory/memory_windows.go @@ -9,6 +9,7 @@ package memory import ( "github.com/yusufpapurcu/wmi" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/unitutil" ) @@ -37,7 +38,7 @@ type win32PhysicalMemory struct { TotalWidth *uint16 } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting info from WMI var win32OSDescriptions []win32OperatingSystem if err := wmi.Query(wqlOperatingSystem, &win32OSDescriptions); err != nil { diff --git a/pkg/net/net.go b/pkg/net/net.go index e26dab70..456b13cb 100644 --- a/pkg/net/net.go +++ b/pkg/net/net.go @@ -9,7 +9,6 @@ package net import ( "fmt" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" ) @@ -95,7 +94,6 @@ func (n *NIC) String() string { // Info describes all network interface controllers (NICs) in the host system. type Info struct { - ctx *context.Context // NICs is a slice of pointers to `NIC` structs describing the network // interface controllers (NICs) on the host system. NICs []*NIC `json:"nics"` @@ -103,10 +101,13 @@ type Info struct { // New returns a pointer to an Info struct that contains information about the // network interface controllers (NICs) on the host system -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -130,11 +131,11 @@ type netPrinter struct { // YAMLString returns a string with the net information formatted as YAML // under a top-level "net:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, netPrinter{i}) + return marshal.SafeYAML(netPrinter{i}) } // JSONString returns a string with the net information formatted as JSON // under a top-level "net:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, netPrinter{i}, indent) + return marshal.SafeJSON(netPrinter{i}, indent) } diff --git a/pkg/net/net_linux.go b/pkg/net/net_linux.go index 7f11e78b..3dc68883 100644 --- a/pkg/net/net_linux.go +++ b/pkg/net/net_linux.go @@ -14,8 +14,8 @@ import ( "path/filepath" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) @@ -23,24 +23,24 @@ const ( warnEthtoolNotInstalled = `ethtool not installed. Cannot grab NIC capabilities` ) -func (i *Info) load() error { - i.NICs = nics(i.ctx) +func (i *Info) load(opts *option.Options) error { + i.NICs = nics(opts) return nil } -func nics(ctx *context.Context) []*NIC { +func nics(opts *option.Options) []*NIC { nics := make([]*NIC, 0) - paths := linuxpath.New(ctx) + paths := linuxpath.New(opts) files, err := os.ReadDir(paths.SysClassNet) if err != nil { return nics } - etAvailable := ctx.EnableTools + etAvailable := !opts.DisableTools if etAvailable { if etInstalled := ethtoolInstalled(); !etInstalled { - ctx.Warn(warnEthtoolNotInstalled) + opts.Warn(warnEthtoolNotInstalled) etAvailable = false } } @@ -68,7 +68,7 @@ func nics(ctx *context.Context) []*NIC { nic.MacAddress = mac nic.MACAddress = mac if etAvailable { - nic.netDeviceParseEthtool(ctx, filename) + nic.netDeviceParseEthtool(opts, filename) } else { nic.Capabilities = []*NICCapability{} // Sets NIC struct fields from data in SysFs @@ -108,7 +108,7 @@ func ethtoolInstalled() bool { return err == nil } -func (n *NIC) netDeviceParseEthtool(ctx *context.Context, dev string) { +func (n *NIC) netDeviceParseEthtool(opts *option.Options, dev string) { var out bytes.Buffer path, _ := exec.LookPath("ethtool") @@ -133,7 +133,7 @@ func (n *NIC) netDeviceParseEthtool(ctx *context.Context, dev string) { n.AdvertisedFECModes = m["Advertised FEC modes"] } else { msg := fmt.Sprintf("could not grab NIC link info for %s: %s", dev, err) - ctx.Warn(msg) + opts.Warn(msg) } // Get all other capabilities from "ethtool -k" @@ -171,9 +171,8 @@ func (n *NIC) netDeviceParseEthtool(ctx *context.Context, dev string) { } else { msg := fmt.Sprintf("could not grab NIC capabilities for %s: %s", dev, err) - ctx.Warn(msg) + opts.Warn(msg) } - } // netParseEthtoolFeature parses a line from the ethtool -k output and returns diff --git a/pkg/net/net_stub.go b/pkg/net/net_stub.go index c8dfa090..b9ffcc2f 100644 --- a/pkg/net/net_stub.go +++ b/pkg/net/net_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("netFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/net/net_windows.go b/pkg/net/net_windows.go index 5936ff64..a716318e 100644 --- a/pkg/net/net_windows.go +++ b/pkg/net/net_windows.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/yusufpapurcu/wmi" + + "github.com/jaypipes/ghw/pkg/option" ) const wqlNetworkAdapter = "SELECT Description, DeviceID, Index, InterfaceIndex, MACAddress, Manufacturer, Name, NetConnectionID, ProductName, ServiceName, PhysicalAdapter FROM Win32_NetworkAdapter" @@ -27,7 +29,7 @@ type win32NetworkAdapter struct { PhysicalAdapter *bool } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting info from WMI var win32NetDescriptions []win32NetworkAdapter if err := wmi.Query(wqlNetworkAdapter, &win32NetDescriptions); err != nil { diff --git a/pkg/option/option.go b/pkg/option/option.go index 36b253b4..9e8b4b4a 100644 --- a/pkg/option/option.go +++ b/pkg/option/option.go @@ -64,214 +64,113 @@ func EnvOrDefaultChroot() string { return DefaultChroot } -// EnvOrDefaultSnapshotPath returns the value of the GHW_SNAPSHOT_PATH environs variable -// or the default value of "" (disable snapshot consumption) if not set -func EnvOrDefaultSnapshotPath() string { - if val, exists := os.LookupEnv(envKeySnapshotPath); exists { - return val - } - return "" // default is no snapshot -} - -// EnvOrDefaultSnapshotRoot returns the value of the the GHW_SNAPSHOT_ROOT environs variable -// or the default value of "" (self-manage the snapshot unpack directory, if relevant) if not set -func EnvOrDefaultSnapshotRoot() string { - if val, exists := os.LookupEnv(envKeySnapshotRoot); exists { - return val - } - return "" // default is to self-manage the snapshot directory -} - -// EnvOrDefaultSnapshotExclusive returns the value of the GHW_SNAPSHOT_EXCLUSIVE environs variable -// or the default value of false if not set -func EnvOrDefaultSnapshotExclusive() bool { - if _, exists := os.LookupEnv(envKeySnapshotExclusive); exists { - return true - } - return false -} - -// EnvOrDefaultSnapshotPreserve returns the value of the GHW_SNAPSHOT_PRESERVE environs variable -// or the default value of false if not set -func EnvOrDefaultSnapshotPreserve() bool { - if _, exists := os.LookupEnv(envKeySnapshotPreserve); exists { - return true - } - return false -} - -// EnvOrDefaultTools return true if ghw should use external tools to augment the data collected -// from sysfs. Most users want to do this most of time, so this is enabled by default. -// Users consuming snapshots may want to opt out, thus they can set the GHW_DISABLE_TOOLS -// environs variable to any value to make ghw skip calling external tools even if they are available. -func EnvOrDefaultTools() bool { +// EnvOrDefaultDisableTools return true if ghw should use external tools to +// augment the data collected from sysfs. Most users want to do this most of +// time, so this is enabled by default. Users consuming snapshots may want to +// opt out, thus they can set the GHW_DISABLE_TOOLS environs variable to any +// value to make ghw skip calling external tools even if they are available. +func EnvOrDefaultDisableTools() bool { if _, exists := os.LookupEnv(envKeyDisableTools); exists { return false } return true } -// Option is used to represent optionally-configured settings. Each field is a +// Options is used to represent optionally-configured settings. Each field is a // pointer to some concrete value so that we can tell when something has been // set or left unset. -type Option struct { +type Options struct { // To facilitate querying of sysfs filesystems that are bind-mounted to a - // non-default root mountpoint, we allow users to set the GHW_CHROOT environ - // variable to an alternate mountpoint. For instance, assume that the user of - // ghw is a Golang binary being executed from an application container that has - // certain host filesystems bind-mounted into the container at /host. The user - // would ensure the GHW_CHROOT environ variable is set to "/host" and ghw will - // build its paths from that location instead of / - Chroot *string - - // Snapshot contains options for handling ghw snapshots - Snapshot *SnapshotOptions + // non-default root mountpoint, we allow users to set the GHW_CHROOT + // environ variable to an alternate mountpoint. For instance, assume that + // the user of ghw is a Golang binary being executed from an application + // container that has certain host filesystems bind-mounted into the + // container at /host. The user would ensure the GHW_CHROOT environ + // variable is set to "/host" and ghw will build its paths from that + // location instead of / + Chroot string // Alerter contains the target for ghw warnings Alerter Alerter - // EnableTools optionally request ghw to not call any external program to learn + // DisableTools optionally request ghw to not call any external program to learn // about the hardware. The default is to use such tools if available. - EnableTools *bool + DisableTools bool - // PathOverrides optionally allows to override the default paths ghw uses internally - // to learn about the system resources. + // PathOverrides optionally allows to override the default paths ghw uses + // internally to learn about the system resources. PathOverrides PathOverrides - // Context may contain a pointer to a `Context` struct that is constructed - // during a call to the `context.WithContext` function. Only used internally. - // This is an interface to get around recursive package import issues. - Context interface{} - - // PCIDB allows users to provide a custom instance of the PCI database (pcidb.PCIDB) - // to be used by ghw. This can be useful for testing, supplying a preloaded database, - // or providing an instance created with custom pcidb.WithOption settings, instead of - // letting ghw load the PCI database automatically. + // PCIDB allows users to provide a custom instance of the PCI database + // (pcidb.PCIDB) to be used by ghw. This can be useful for testing, + // supplying a preloaded database, or providing an instance created with + // custom pcidb.WithOption settings, instead of letting ghw load the PCI + // database automatically. PCIDB *pcidb.PCIDB } -// SnapshotOptions contains options for handling of ghw snapshots -type SnapshotOptions struct { - // Path allows users to specify a snapshot (captured using ghw-snapshot) to be - // automatically consumed. Users need to supply the path of the snapshot, and - // ghw will take care of unpacking it on a temporary directory. - // Set the environment variable "GHW_SNAPSHOT_PRESERVE" to make ghw skip the cleanup - // stage and keep the unpacked snapshot in the temporary directory. - Path string - // Root is the directory on which the snapshot must be unpacked. This allows - // the users to manage their snapshot directory instead of ghw doing that on - // their behalf. Relevant only if SnapshotPath is given. - Root *string - // Exclusive tells ghw if the given directory should be considered of exclusive - // usage of ghw or not If the user provides a Root. If the flag is set, ghw will - // unpack the snapshot in the given SnapshotRoot iff the directory is empty; otherwise - // any existing content will be left untouched and the unpack stage will exit silently. - // As additional side effect, give both this option and SnapshotRoot to make each - // context try to unpack the snapshot only once. - Exclusive bool +func (o *Options) Warn(msg string, args ...interface{}) { + if o.Alerter != nil { + o.Alerter.Printf("WARNING: "+msg, args...) + } } -// WithChroot allows to override the root directory ghw uses. -func WithChroot(dir string) *Option { - return &Option{Chroot: &dir} -} +type Option func(opts *Options) -// WithSnapshot sets snapshot-processing options for a ghw run -func WithSnapshot(opts SnapshotOptions) *Option { - return &Option{ - Snapshot: &opts, +// WithChroot allows to override the root directory ghw uses. +func WithChroot(dir string) Option { + return func(opts *Options) { + opts.Chroot = dir } } // WithAlerter sets alerting options for ghw -func WithAlerter(alerter Alerter) *Option { - return &Option{ - Alerter: alerter, +func WithAlerter(alerter Alerter) Option { + return func(opts *Options) { + opts.Alerter = alerter } } // WithNullAlerter sets No-op alerting options for ghw -func WithNullAlerter() *Option { - return &Option{ - Alerter: NullAlerter, +func WithNullAlerter() Option { + return func(opts *Options) { + opts.Alerter = NullAlerter } } -// WithDisableTools sets enables or prohibts ghw to call external tools to discover hardware capabilities. -func WithDisableTools() *Option { - false_ := false - return &Option{EnableTools: &false_} +// WithDisableTools revents ghw from calling external tools to discover +// hardware capabilities. +func WithDisableTools() Option { + return func(opts *Options) { + opts.DisableTools = true + } } // WithPCIDB allows you to provide a custom instance of the PCI database (pcidb.PCIDB) // to ghw. This is useful if you want to use a preloaded or specially configured // PCI database, such as one created with custom pcidb.WithOption settings, instead // of letting ghw load the PCI database automatically. -func WithPCIDB(pcidb *pcidb.PCIDB) *Option { - return &Option{PCIDB: pcidb} +func WithPCIDB(pcidb *pcidb.PCIDB) Option { + return func(opts *Options) { + opts.PCIDB = pcidb + } } // PathOverrides is a map, keyed by the string name of a mount path, of override paths type PathOverrides map[string]string // WithPathOverrides supplies path-specific overrides for the context -func WithPathOverrides(overrides PathOverrides) *Option { - return &Option{ - PathOverrides: overrides, +func WithPathOverrides(overrides PathOverrides) Option { + return func(opts *Options) { + opts.PathOverrides = overrides } } -// There is intentionally no Option related to GHW_SNAPSHOT_PRESERVE because we see that as -// a debug/troubleshoot aid more something users wants to do regularly. -// Hence we allow that only via the environment variable for the time being. - -// Merge accepts one or more Options and merges them together, returning the -// merged Option -func Merge(opts ...*Option) *Option { - merged := &Option{} - for _, opt := range opts { - if opt.Chroot != nil { - merged.Chroot = opt.Chroot - } - if opt.Snapshot != nil { - merged.Snapshot = opt.Snapshot - } - if opt.Alerter != nil { - merged.Alerter = opt.Alerter - } - if opt.EnableTools != nil { - merged.EnableTools = opt.EnableTools - } - // intentionally only programmatically - if opt.PathOverrides != nil { - merged.PathOverrides = opt.PathOverrides - } - if opt.Context != nil { - merged.Context = opt.Context - } - if opt.PCIDB != nil { - merged.PCIDB = opt.PCIDB - } - } - // Set the default value if missing from mergeOpts - if merged.Chroot == nil { - chroot := EnvOrDefaultChroot() - merged.Chroot = &chroot - } - if merged.Alerter == nil { - merged.Alerter = EnvOrDefaultAlerter() - } - if merged.Snapshot == nil { - snapRoot := EnvOrDefaultSnapshotRoot() - merged.Snapshot = &SnapshotOptions{ - Path: EnvOrDefaultSnapshotPath(), - Root: &snapRoot, - Exclusive: EnvOrDefaultSnapshotExclusive(), - } - } - if merged.EnableTools == nil { - enabled := EnvOrDefaultTools() - merged.EnableTools = &enabled +// FromEnv returns an Options populated from the environs or default option +// values +func FromEnv() *Options { + return &Options{ + Chroot: EnvOrDefaultChroot(), + DisableTools: EnvOrDefaultDisableTools(), } - return merged } diff --git a/pkg/option/option_test.go b/pkg/option/option_test.go deleted file mode 100644 index e9047da8..00000000 --- a/pkg/option/option_test.go +++ /dev/null @@ -1,242 +0,0 @@ -// -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. -// - -package option_test - -import ( - "runtime" - "testing" - - "github.com/jaypipes/ghw/pkg/option" - "github.com/jaypipes/pcidb" -) - -type optTestCase struct { - name string - opts []*option.Option - merged *option.Option -} - -// nolint: gocyclo -func TestOption(t *testing.T) { - var pciTest *optTestCase - - if runtime.GOOS == "linux" { - pcidb, err := pcidb.New() - if err != nil { - t.Fatalf("error creating new pcidb: %v", err) - } - pciTest = &optTestCase{ - name: "pcidb", - opts: []*option.Option{ - option.WithPCIDB(pcidb), - option.WithChroot("/my/chroot/dir"), - }, - merged: &option.Option{ - Chroot: stringPtr("/my/chroot/dir"), - PCIDB: pcidb, - }, - } - } - - optTCases := []optTestCase{ - { - name: "multiple chroots", - opts: []*option.Option{ - option.WithChroot("/my/chroot/dir"), - option.WithChroot("/my/chroot/dir/2"), - }, - merged: &option.Option{ - Chroot: stringPtr("/my/chroot/dir/2"), - EnableTools: boolPtr(true), - }, - }, - { - name: "multiple chroots interleaved", - opts: []*option.Option{ - option.WithChroot("/my/chroot/dir"), - option.WithSnapshot(option.SnapshotOptions{ - Path: "/my/snapshot/dir", - }), - option.WithChroot("/my/chroot/dir/2"), - }, - merged: &option.Option{ - // latest seen takes precedence - Chroot: stringPtr("/my/chroot/dir/2"), - Snapshot: &option.SnapshotOptions{ - Path: "/my/snapshot/dir", - }, - }, - }, - { - name: "multiple snapshots overriding path", - opts: []*option.Option{ - option.WithSnapshot(option.SnapshotOptions{ - Path: "/my/snapshot/dir", - }), - option.WithSnapshot(option.SnapshotOptions{ - Exclusive: true, - }), - }, - merged: &option.Option{ - Chroot: stringPtr("/"), - Snapshot: &option.SnapshotOptions{ - // note Path is gone because the second instance - // has default (empty) path, and the latest always - // takes precedence. - Path: "", - Exclusive: true, - }, - }, - }, - { - name: "chroot and snapshot", - opts: []*option.Option{ - option.WithChroot("/my/chroot/dir"), - option.WithSnapshot(option.SnapshotOptions{ - Path: "/my/snapshot/dir", - Exclusive: true, - }), - }, - merged: &option.Option{ - Chroot: stringPtr("/my/chroot/dir"), - Snapshot: &option.SnapshotOptions{ - Path: "/my/snapshot/dir", - Exclusive: true, - }, - }, - }, - { - name: "chroot and snapshot with root", - opts: []*option.Option{ - option.WithChroot("/my/chroot/dir"), - option.WithSnapshot(option.SnapshotOptions{ - Path: "/my/snapshot/dir", - Root: stringPtr("/my/overridden/chroot/dir"), - Exclusive: true, - }), - }, - merged: &option.Option{ - // caveat! the option merge logic DOES NOT DO the override - Chroot: stringPtr("/my/chroot/dir"), - Snapshot: &option.SnapshotOptions{ - Path: "/my/snapshot/dir", - Root: stringPtr("/my/overridden/chroot/dir"), - Exclusive: true, - }, - }, - }, - { - name: "chroot and disabling tools", - opts: []*option.Option{ - option.WithChroot("/my/chroot/dir"), - option.WithDisableTools(), - }, - merged: &option.Option{ - Chroot: stringPtr("/my/chroot/dir"), - EnableTools: boolPtr(false), - }, - }, - { - name: "paths", - opts: []*option.Option{ - option.WithPathOverrides(option.PathOverrides{ - "/run": "/host-run", - "/var": "/host-var", - }), - }, - merged: &option.Option{ - PathOverrides: option.PathOverrides{ - "/run": "/host-run", - "/var": "/host-var", - }, - }, - }, - { - name: "chroot paths", - opts: []*option.Option{ - option.WithChroot("/my/chroot/dir"), - option.WithPathOverrides(option.PathOverrides{ - "/run": "/host-run", - "/var": "/host-var", - }), - }, - merged: &option.Option{ - Chroot: stringPtr("/my/chroot/dir"), - PathOverrides: option.PathOverrides{ - "/run": "/host-run", - "/var": "/host-var", - }, - }, - }, - } - if pciTest != nil { - optTCases = append(optTCases, *pciTest) - } - for _, optTCase := range optTCases { - t.Run(optTCase.name, func(t *testing.T) { - opt := option.Merge(optTCase.opts...) - if what, ok := optionEqual(optTCase.merged, opt); !ok { - t.Errorf("expected %#v got %#v - difference on %s", optTCase.merged, opt, what) - } - }) - } -} - -func stringPtr(s string) *string { - return &s -} - -func boolPtr(b bool) *bool { - return &b -} - -func optionEqual(a, b *option.Option) (string, bool) { - if a == nil || b == nil { - return "top-level", false - } - if a.Chroot != nil { - if b.Chroot == nil { - return "chroot ptr", false - } - if *a.Chroot != *b.Chroot { - return "chroot value", false - } - } - if a.Snapshot != nil { - if b.Snapshot == nil { - return "snapshot ptr", false - } - return optionSnapshotEqual(a.Snapshot, b.Snapshot) - } - if a.EnableTools != nil { - if b.EnableTools == nil { - return "enabletools ptr", false - } - if *a.EnableTools != *b.EnableTools { - return "enabletools value", false - } - } - return "", true -} - -func optionSnapshotEqual(a, b *option.SnapshotOptions) (string, bool) { - if a.Path != b.Path { - return "snapshot path", false - } - if a.Exclusive != b.Exclusive { - return "snapshot exclusive flag", false - } - if a.Root != nil { - if b.Root == nil { - return "snapshot root ptr", false - } - if *a.Root != *b.Root { - return "snapshot root value", false - } - } - return "", true -} diff --git a/pkg/pci/pci.go b/pkg/pci/pci.go index c27928ca..ca95adb6 100644 --- a/pkg/pci/pci.go +++ b/pkg/pci/pci.go @@ -12,7 +12,6 @@ import ( "github.com/jaypipes/pcidb" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/topology" @@ -127,7 +126,6 @@ func (d *Device) String() string { type Info struct { db *pcidb.PCIDB arch topology.Architecture - ctx *context.Context // All PCI devices on the host system Devices []*Device } @@ -138,39 +136,25 @@ func (i *Info) String() string { // New returns a pointer to an Info struct that contains information about the // PCI devices on the host system -func New(opts ...*option.Option) (*Info, error) { - merged := option.Merge(opts...) - ctx := context.New(merged) +func New(opt ...option.Option) (*Info, error) { + topo, err := topology.New(opt...) + if err != nil { + return nil, fmt.Errorf( + "failed to initialize PCI info dur to failure to initialize "+ + "Topology info: %w", + err, + ) + } + opts := &option.Options{} + for _, o := range opt { + o(opts) + } // by default we don't report NUMA information; // we will only if are sure we are running on NUMA architecture info := &Info{ - arch: topology.ArchitectureSMP, - ctx: ctx, - } - - // we do this trick because we need to make sure ctx.Setup() gets - // a chance to run before any subordinate package is created reusing - // our context. - loadDetectingTopology := func() error { - topo, err := topology.New(context.WithContext(ctx)) - if err == nil { - info.arch = topo.Architecture - } else { - ctx.Warn("error detecting system topology: %v", err) - } - if merged.PCIDB != nil { - info.db = merged.PCIDB - } - return info.load() + arch: topo.Architecture, } - - var err error - if context.Exists(merged) { - err = loadDetectingTopology() - } else { - err = ctx.Do(loadDetectingTopology) - } - if err != nil { + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -195,11 +179,11 @@ type pciPrinter struct { // YAMLString returns a string with the PCI information formatted as YAML // under a top-level "pci:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, pciPrinter{i}) + return marshal.SafeYAML(pciPrinter{i}) } // JSONString returns a string with the PCI information formatted as JSON // under a top-level "pci:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, pciPrinter{i}, indent) + return marshal.SafeJSON(pciPrinter{i}, indent) } diff --git a/pkg/pci/pci_linux.go b/pkg/pci/pci_linux.go index 9b00effd..15e4d741 100644 --- a/pkg/pci/pci_linux.go +++ b/pkg/pci/pci_linux.go @@ -12,7 +12,6 @@ import ( "github.com/jaypipes/pcidb" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" "github.com/jaypipes/ghw/pkg/option" pciaddr "github.com/jaypipes/ghw/pkg/pci/address" @@ -25,37 +24,23 @@ const ( modAliasExpectedLength = 54 ) -func (i *Info) load() error { - // when consuming snapshots - most notably, but not only, in tests, - // the context pkg forces the chroot value to the unpacked snapshot root. - // This is intentional, intentionally transparent and ghw is prepared to handle this case. - // However, `pcidb` is not. It doesn't know about ghw snaphots, nor it should. - // so we need to complicate things a bit. If the user explicitely supplied - // a chroot option, then we should honor it all across the stack, and passing down - // the chroot to pcidb is the right thing to do. If, however, the chroot was - // implcitely set by snapshot support, then this must be consumed by ghw only. - // In this case we should NOT pass it down to pcidb. - chroot := i.ctx.Chroot - if i.ctx.SnapshotPath != "" { - chroot = option.DefaultChroot - } - opt := pcidb.WithChroot(chroot) +func (i *Info) load(opts *option.Options) error { + pcidbOpt := &pcidb.WithOption{} if path := os.Getenv("PCIDB_PATH"); path != "" { - opt = pcidb.WithPath(path) + pcidbOpt = pcidb.WithPath(path) } if i.db == nil { - db, err := pcidb.New(opt) + db, err := pcidb.New(pcidbOpt) if err != nil { return err } i.db = db } - i.Devices = i.getDevices() + i.Devices = i.getDevices(opts) return nil } -func getDeviceModaliasPath(ctx *context.Context, pciAddr *pciaddr.Address) string { - paths := linuxpath.New(ctx) +func getDeviceModaliasPath(paths *linuxpath.Paths, pciAddr *pciaddr.Address) string { return filepath.Join( paths.SysBusPciDevices, pciAddr.String(), @@ -63,8 +48,7 @@ func getDeviceModaliasPath(ctx *context.Context, pciAddr *pciaddr.Address) strin ) } -func getDeviceRevision(ctx *context.Context, pciAddr *pciaddr.Address) string { - paths := linuxpath.New(ctx) +func getDeviceRevision(paths *linuxpath.Paths, pciAddr *pciaddr.Address) string { revisionPath := filepath.Join( paths.SysBusPciDevices, pciAddr.String(), @@ -81,15 +65,15 @@ func getDeviceRevision(ctx *context.Context, pciAddr *pciaddr.Address) string { return strings.TrimSpace(string(revision)) } -func getDeviceNUMANode(ctx *context.Context, pciAddr *pciaddr.Address) *topology.Node { - paths := linuxpath.New(ctx) +func getDeviceNUMANode(opts *option.Options, pciAddr *pciaddr.Address) *topology.Node { + paths := linuxpath.New(opts) numaNodePath := filepath.Join(paths.SysBusPciDevices, pciAddr.String(), "numa_node") if _, err := os.Stat(numaNodePath); err != nil { return nil } - nodeIdx := util.SafeIntFromFile(ctx, numaNodePath) + nodeIdx := util.SafeIntFromFile(opts, numaNodePath) if nodeIdx == -1 { return nil } @@ -99,8 +83,7 @@ func getDeviceNUMANode(ctx *context.Context, pciAddr *pciaddr.Address) *topology } } -func getDeviceIommuGroup(ctx *context.Context, pciAddr *pciaddr.Address) string { - paths := linuxpath.New(ctx) +func getDeviceIommuGroup(paths *linuxpath.Paths, pciAddr *pciaddr.Address) string { iommuGroupPath := filepath.Join(paths.SysBusPciDevices, pciAddr.String(), "iommu_group") dest, err := os.Readlink(iommuGroupPath) @@ -110,8 +93,7 @@ func getDeviceIommuGroup(ctx *context.Context, pciAddr *pciaddr.Address) string return filepath.Base(dest) } -func getDeviceParentAddress(ctx *context.Context, pciAddr *pciaddr.Address) string { - paths := linuxpath.New(ctx) +func getDeviceParentAddress(paths *linuxpath.Paths, pciAddr *pciaddr.Address) string { devPath := filepath.Join(paths.SysBusPciDevices, pciAddr.String()) dest, err := os.Readlink(devPath) @@ -128,8 +110,7 @@ func getDeviceParentAddress(ctx *context.Context, pciAddr *pciaddr.Address) stri return parentAddr } -func getDeviceDriver(ctx *context.Context, pciAddr *pciaddr.Address) string { - paths := linuxpath.New(ctx) +func getDeviceDriver(paths *linuxpath.Paths, pciAddr *pciaddr.Address) string { driverPath := filepath.Join(paths.SysBusPciDevices, pciAddr.String(), "driver") if _, err := os.Stat(driverPath); err != nil { @@ -334,39 +315,7 @@ func findPCIProgrammingInterface( // GetDevice returns a pointer to a Device struct that describes the PCI // device at the requested address. If no such device could be found, returns nil. func (info *Info) GetDevice(address string) *Device { - // check cached data first - if dev := info.lookupDevice(address); dev != nil { - return dev - } - - pciAddr := pciaddr.FromString(address) - if pciAddr == nil { - info.ctx.Warn("error parsing the pci address %q", address) - return nil - } - - // no cached data, let's get the information from system. - fp := getDeviceModaliasPath(info.ctx, pciAddr) - if fp == "" { - info.ctx.Warn("error finding modalias info for device %q", address) - return nil - } - - modaliasInfo := parseModaliasFile(fp) - if modaliasInfo == nil { - info.ctx.Warn("error parsing modalias info for device %q", address) - return nil - } - - device := info.getDeviceFromModaliasInfo(address, modaliasInfo) - device.Revision = getDeviceRevision(info.ctx, pciAddr) - if info.arch == topology.ArchitectureNUMA { - device.Node = getDeviceNUMANode(info.ctx, pciAddr) - } - device.Driver = getDeviceDriver(info.ctx, pciAddr) - device.ParentAddress = getDeviceParentAddress(info.ctx, pciAddr) - device.IOMMUGroup = getDeviceIommuGroup(info.ctx, pciAddr) - return device + return info.lookupDevice(address) } // ParseDevice returns a pointer to a Device given its describing data. @@ -423,8 +372,8 @@ func (info *Info) getDeviceFromModaliasInfo( // getDevices returns a list of pointers to Device structs present on the // host system -func (info *Info) getDevices() []*Device { - paths := linuxpath.New(info.ctx) +func (info *Info) getDevices(opts *option.Options) []*Device { + paths := linuxpath.New(opts) devs := make([]*Device, 0) // We scan the /sys/bus/pci/devices directory which contains a collection // of symlinks. The names of the symlinks are all the known PCI addresses @@ -432,18 +381,39 @@ func (info *Info) getDevices() []*Device { // address and append to the returned array. links, err := os.ReadDir(paths.SysBusPciDevices) if err != nil { - info.ctx.Warn("failed to read /sys/bus/pci/devices") + opts.Warn("failed to read /sys/bus/pci/devices") return nil } - var dev *Device for _, link := range links { - addr := link.Name() - dev = info.GetDevice(addr) - if dev == nil { - info.ctx.Warn("failed to get device information for PCI address %s", addr) - } else { - devs = append(devs, dev) + address := link.Name() + pciAddr := pciaddr.FromString(address) + if pciAddr == nil { + opts.Warn("error parsing the pci address %q", address) + return nil + } + + // no cached data, let's get the information from system. + fp := getDeviceModaliasPath(paths, pciAddr) + if fp == "" { + opts.Warn("error finding modalias info for device %q", address) + return nil + } + + modaliasInfo := parseModaliasFile(fp) + if modaliasInfo == nil { + opts.Warn("error parsing modalias info for device %q", address) + return nil + } + + device := info.getDeviceFromModaliasInfo(address, modaliasInfo) + device.Revision = getDeviceRevision(paths, pciAddr) + if info.arch == topology.ArchitectureNUMA { + device.Node = getDeviceNUMANode(opts, pciAddr) } + device.Driver = getDeviceDriver(paths, pciAddr) + device.ParentAddress = getDeviceParentAddress(paths, pciAddr) + device.IOMMUGroup = getDeviceIommuGroup(paths, pciAddr) + devs = append(devs, device) } return devs } diff --git a/pkg/pci/pci_linux_test.go b/pkg/pci/pci_linux_test.go index 8c8c9b2e..a4a37f71 100644 --- a/pkg/pci/pci_linux_test.go +++ b/pkg/pci/pci_linux_test.go @@ -13,10 +13,10 @@ import ( "path/filepath" "testing" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/pci" + "github.com/jaypipes/ghw/pkg/snapshot" "github.com/jaypipes/ghw/pkg/util" "github.com/jaypipes/ghw/testdata" @@ -58,6 +58,12 @@ func TestPCINUMANode(t *testing.T) { } if dev.Node == nil { if tCase.node != -1 { + addrs := []string{} + for _, d := range info.Devices { + addrs = append(addrs, d.Address) + } + msg := fmt.Sprintf("address: %q device addresses: %v", tCase.addr, addrs) + t.Error(msg) t.Fatalf("got nil numa NODE for address %q", tCase.addr) } } else { @@ -192,7 +198,7 @@ func TestPCIMarshalJSON(t *testing.T) { if dev == nil { t.Fatalf("Failed to parse valid modalias") } - s := marshal.SafeJSON(context.FromEnv(), dev, true) + s := marshal.SafeJSON(dev, true) if s == "" { t.Fatalf("Error marshalling device: %v", dev) } @@ -240,16 +246,19 @@ func pciTestSetup(t *testing.T, snapshotFilename string) *pci.Info { t.Fatalf("Expected nil err, but got %v", err) } - snapshot := filepath.Join(testdataPath, snapshotFilename) + snapshotPath := filepath.Join(testdataPath, snapshotFilename) + unpackDir := t.TempDir() + err = snapshot.UnpackInto(snapshotPath, unpackDir) + if err != nil { + t.Fatal(err) + } + // from now on we use constants reflecting the content of the snapshot we requested, // which we reviewed beforehand. IOW, you need to know the content of the // snapshot to fully understand this test. Inspect it using // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-intel-xeon-L5640.tar.gz" ghwc topology - info, err := pci.New(option.WithSnapshot(option.SnapshotOptions{ - Path: snapshot, - })) - + info, err := pci.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) } diff --git a/pkg/pci/pci_stub.go b/pkg/pci/pci_stub.go index 9ebb396d..84bb1fa9 100644 --- a/pkg/pci/pci_stub.go +++ b/pkg/pci/pci_stub.go @@ -12,9 +12,11 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("pciFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/product/product.go b/pkg/product/product.go index 83d6541d..5a8486c2 100644 --- a/pkg/product/product.go +++ b/pkg/product/product.go @@ -7,7 +7,6 @@ package product import ( - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" @@ -15,7 +14,6 @@ import ( // Info defines product information type Info struct { - ctx *context.Context Family string `json:"family"` Name string `json:"name"` Vendor string `json:"vendor"` @@ -68,10 +66,13 @@ func (i *Info) String() string { // New returns a pointer to a Info struct containing information // about the host's product -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } return info, nil @@ -86,11 +87,11 @@ type productPrinter struct { // YAMLString returns a string with the product information formatted as YAML // under a top-level "dmi:" key func (info *Info) YAMLString() string { - return marshal.SafeYAML(info.ctx, productPrinter{info}) + return marshal.SafeYAML(productPrinter{info}) } // JSONString returns a string with the product information formatted as JSON // under a top-level "product:" key func (info *Info) JSONString(indent bool) string { - return marshal.SafeJSON(info.ctx, productPrinter{info}, indent) + return marshal.SafeJSON(productPrinter{info}, indent) } diff --git a/pkg/product/product_linux.go b/pkg/product/product_linux.go index 36b6b447..214aa23a 100644 --- a/pkg/product/product_linux.go +++ b/pkg/product/product_linux.go @@ -7,17 +7,17 @@ package product import ( "github.com/jaypipes/ghw/pkg/linuxdmi" + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { - - i.Family = linuxdmi.Item(i.ctx, "product_family") - i.Name = linuxdmi.Item(i.ctx, "product_name") - i.Vendor = linuxdmi.Item(i.ctx, "sys_vendor") - i.SerialNumber = linuxdmi.Item(i.ctx, "product_serial") - i.UUID = linuxdmi.Item(i.ctx, "product_uuid") - i.SKU = linuxdmi.Item(i.ctx, "product_sku") - i.Version = linuxdmi.Item(i.ctx, "product_version") +func (i *Info) load(opts *option.Options) error { + i.Family = linuxdmi.Item(opts, "product_family") + i.Name = linuxdmi.Item(opts, "product_name") + i.Vendor = linuxdmi.Item(opts, "sys_vendor") + i.SerialNumber = linuxdmi.Item(opts, "product_serial") + i.UUID = linuxdmi.Item(opts, "product_uuid") + i.SKU = linuxdmi.Item(opts, "product_sku") + i.Version = linuxdmi.Item(opts, "product_version") return nil } diff --git a/pkg/product/product_stub.go b/pkg/product/product_stub.go index 8fc9724f..f97b02b0 100644 --- a/pkg/product/product_stub.go +++ b/pkg/product/product_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("productFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/product/product_windows.go b/pkg/product/product_windows.go index 36db6a8d..cf6ff17c 100644 --- a/pkg/product/product_windows.go +++ b/pkg/product/product_windows.go @@ -8,6 +8,7 @@ package product import ( "github.com/yusufpapurcu/wmi" + "github.com/jaypipes/ghw/pkg/option" "github.com/jaypipes/ghw/pkg/util" ) @@ -24,7 +25,7 @@ type win32Product struct { UUID *string } -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { // Getting data from WMI var win32ProductDescriptions []win32Product // Assuming the first product is the host... diff --git a/pkg/snapshot/unpack.go b/pkg/snapshot/unpack.go index f05f8f79..9eb1d550 100644 --- a/pkg/snapshot/unpack.go +++ b/pkg/snapshot/unpack.go @@ -12,52 +12,33 @@ import ( "io" "os" "path/filepath" - - "github.com/jaypipes/ghw/pkg/option" ) const ( TargetRoot = "ghw-snapshot-*" ) -const ( - // If set, `ghw` will not unpack the snapshot in the user-supplied directory - // unless the aforementioned directory is empty. - OwnTargetDirectory = 1 << iota -) - -// Clanup removes the unpacket snapshot from the target root. -// Please not that the environs variable `GHW_SNAPSHOT_PRESERVE`, if set, -// will make this function silently skip. -func Cleanup(targetRoot string) error { - if option.EnvOrDefaultSnapshotPreserve() { - return nil - } - return os.RemoveAll(targetRoot) -} - -// Unpack expands the given snapshot in a temporary directory managed by `ghw`. Returns the path of that directory. +// Unpack expands the given snapshot in a temporary directory managed by `ghw`. +// Returns the path of that directory. Callers are responsible for cleaning up +// the temporary directory. func Unpack(snapshotName string) (string, error) { targetRoot, err := os.MkdirTemp("", TargetRoot) if err != nil { return "", err } - _, err = UnpackInto(snapshotName, targetRoot, 0) + err = UnpackInto(snapshotName, targetRoot) return targetRoot, err } // UnpackInto expands the given snapshot in a client-supplied directory. // Returns true if the snapshot was actually unpacked, false otherwise -func UnpackInto(snapshotName, targetRoot string, flags uint) (bool, error) { - if (flags&OwnTargetDirectory) == OwnTargetDirectory && !isEmptyDir(targetRoot) { - return false, nil - } +func UnpackInto(snapshotName, targetRoot string) error { snap, err := os.Open(snapshotName) if err != nil { - return false, err + return err } defer snap.Close() - return true, Untar(targetRoot, snap) + return Untar(targetRoot, snap) } // Untar extracts data from the given reader (providing data in tar.gz format) and unpacks it in the given directory. diff --git a/pkg/snapshot/unpack_test.go b/pkg/snapshot/unpack_test.go index 9925e38d..1db41364 100644 --- a/pkg/snapshot/unpack_test.go +++ b/pkg/snapshot/unpack_test.go @@ -7,7 +7,6 @@ package snapshot_test import ( - "errors" "os" "path/filepath" "testing" @@ -25,17 +24,11 @@ func TestUnpack(t *testing.T) { if err != nil { t.Fatalf("Expected nil err, but got %v", err) } + defer func() { + _ = os.RemoveAll(root) + }() verifyTestData(t, root) - - err = snapshot.Cleanup(root) - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - - if _, err := os.Stat(root); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("Expected %q to be gone, but still exists", root) - } } // nolint: gocyclo @@ -44,68 +37,16 @@ func TestUnpackInto(t *testing.T) { if err != nil { t.Fatalf("Expected nil err, but got %v", err) } + defer func() { + _ = os.RemoveAll(testRoot) + }() - _, err = snapshot.UnpackInto(testDataSnapshot, testRoot, 0) + err = snapshot.UnpackInto(testDataSnapshot, testRoot) if err != nil { t.Fatalf("Expected nil err, but got %v", err) } verifyTestData(t, testRoot) - - // note that in real production code the caller will likely manage its - // snapshot root directory in a different way, here we call snapshot.Cleanup - // to clean up after ourselves more than to test it - err = snapshot.Cleanup(testRoot) - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - - if _, err := os.Stat(testRoot); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("Expected %q to be gone, but still exists", testRoot) - } -} - -// nolint: gocyclo -func TestUnpackIntoPresrving(t *testing.T) { - testRoot, err := os.MkdirTemp("", "ghw-test-snapshot-*") - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - - err = os.WriteFile(filepath.Join(testRoot, "canary"), []byte(""), 0644) - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - - _, err = snapshot.UnpackInto(testDataSnapshot, testRoot, snapshot.OwnTargetDirectory) - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - - entries, err := os.ReadDir(testRoot) - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - if len(entries) != 1 { - t.Fatalf("Expected one entry in %q, but got %v", testRoot, entries) - } - - canary := entries[0] - if canary.Name() != "canary" { - t.Fatalf("Expected entry %q, but got %q", "canary", canary.Name()) - } - - // note that in real production code the caller will likely manage its - // snapshot root directory in a different way, here we call snapshot.Cleanup - // to clean up after ourselves more than to test it - err = snapshot.Cleanup(testRoot) - if err != nil { - t.Fatalf("Expected nil err, but got %v", err) - } - - if _, err := os.Stat(testRoot); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("Expected %q to be gone, but still exists", testRoot) - } } func verifyTestData(t *testing.T, root string) { diff --git a/pkg/topology/topology.go b/pkg/topology/topology.go index 0210ab48..bc323040 100644 --- a/pkg/topology/topology.go +++ b/pkg/topology/topology.go @@ -13,7 +13,6 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/cpu" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/memory" @@ -107,24 +106,19 @@ func (n *Node) String() string { // Info describes the system topology for the host hardware type Info struct { - ctx *context.Context Architecture Architecture `json:"architecture"` Nodes []*Node `json:"nodes"` } // New returns a pointer to an Info struct that contains information about the // NUMA topology on the host system -func New(opts ...*option.Option) (*Info, error) { - merged := option.Merge(opts...) - ctx := context.New(merged) - info := &Info{ctx: ctx} - var err error - if context.Exists(merged) { - err = info.load() - } else { - err = ctx.Do(info.load) +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) } - if err != nil { + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } for _, node := range info.Nodes { @@ -155,11 +149,11 @@ type topologyPrinter struct { // YAMLString returns a string with the topology information formatted as YAML // under a top-level "topology:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, topologyPrinter{i}) + return marshal.SafeYAML(topologyPrinter{i}) } // JSONString returns a string with the topology information formatted as JSON // under a top-level "topology:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, topologyPrinter{i}, indent) + return marshal.SafeJSON(topologyPrinter{i}, indent) } diff --git a/pkg/topology/topology_linux.go b/pkg/topology/topology_linux.go index dbd0811e..f3c16d98 100644 --- a/pkg/topology/topology_linux.go +++ b/pkg/topology/topology_linux.go @@ -12,14 +12,14 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/cpu" "github.com/jaypipes/ghw/pkg/linuxpath" "github.com/jaypipes/ghw/pkg/memory" + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { - i.Nodes = topologyNodes(i.ctx) +func (i *Info) load(opts *option.Options) error { + i.Nodes = topologyNodes(opts) if len(i.Nodes) == 1 { i.Architecture = ArchitectureSMP } else { @@ -28,13 +28,13 @@ func (i *Info) load() error { return nil } -func topologyNodes(ctx *context.Context) []*Node { - paths := linuxpath.New(ctx) +func topologyNodes(opts *option.Options) []*Node { + paths := linuxpath.New(opts) nodes := make([]*Node, 0) files, err := os.ReadDir(paths.SysDevicesSystemNode) if err != nil { - ctx.Warn("failed to determine nodes: %s\n", err) + opts.Warn("failed to determine nodes: %s\n", err) return nodes } for _, file := range files { @@ -45,33 +45,33 @@ func topologyNodes(ctx *context.Context) []*Node { node := &Node{} nodeID, err := strconv.Atoi(filename[4:]) if err != nil { - ctx.Warn("failed to determine node ID: %s\n", err) + opts.Warn("failed to determine node ID: %s\n", err) return nodes } node.ID = nodeID - cores, err := cpu.CoresForNode(ctx, nodeID) + cores, err := cpu.CoresForNode(opts, nodeID) if err != nil { - ctx.Warn("failed to determine cores for node: %s\n", err) + opts.Warn("failed to determine cores for node: %s\n", err) return nodes } node.Cores = cores - caches, err := memory.CachesForNode(ctx, nodeID) + caches, err := memory.CachesForNode(opts, nodeID) if err != nil { - ctx.Warn("failed to determine caches for node: %s\n", err) + opts.Warn("failed to determine caches for node: %s\n", err) return nodes } node.Caches = caches - distances, err := distancesForNode(ctx, nodeID) + distances, err := distancesForNode(paths, nodeID) if err != nil { - ctx.Warn("failed to determine node distances for node: %s\n", err) + opts.Warn("failed to determine node distances for node: %s\n", err) return nodes } node.Distances = distances - area, err := memory.AreaForNode(ctx, nodeID) + area, err := memory.AreaForNode(paths, nodeID) if err != nil { - ctx.Warn("failed to determine memory area for node: %s\n", err) + opts.Warn("failed to determine memory area for node: %s\n", err) return nodes } node.Memory = area @@ -81,8 +81,7 @@ func topologyNodes(ctx *context.Context) []*Node { return nodes } -func distancesForNode(ctx *context.Context, nodeID int) ([]int, error) { - paths := linuxpath.New(ctx) +func distancesForNode(paths *linuxpath.Paths, nodeID int) ([]int, error) { path := filepath.Join( paths.SysDevicesSystemNode, fmt.Sprintf("node%d", nodeID), diff --git a/pkg/topology/topology_linux_test.go b/pkg/topology/topology_linux_test.go index 4c705e75..981ca366 100644 --- a/pkg/topology/topology_linux_test.go +++ b/pkg/topology/topology_linux_test.go @@ -13,6 +13,7 @@ import ( "github.com/jaypipes/ghw/pkg/memory" "github.com/jaypipes/ghw/pkg/option" + "github.com/jaypipes/ghw/pkg/snapshot" "github.com/jaypipes/ghw/pkg/topology" "github.com/jaypipes/ghw/testdata" @@ -26,14 +27,17 @@ func TestTopologyNUMADistances(t *testing.T) { } multiNumaSnapshot := filepath.Join(testdataPath, "linux-amd64-intel-xeon-L5640.tar.gz") + unpackDir := t.TempDir() + err = snapshot.UnpackInto(multiNumaSnapshot, unpackDir) + if err != nil { + t.Fatal(err) + } // from now on we use constants reflecting the content of the snapshot we requested, // which we reviewed beforehand. IOW, you need to know the content of the // snapshot to fully understand this test. Inspect it using // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-intel-xeon-L5640.tar.gz" ghwc topology - info, err := topology.New(option.WithSnapshot(option.SnapshotOptions{ - Path: multiNumaSnapshot, - })) + info, err := topology.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) @@ -90,15 +94,16 @@ func TestTopologyPerNUMAMemory(t *testing.T) { } multiNumaSnapshot := filepath.Join(testdataPath, "linux-amd64-intel-xeon-L5640.tar.gz") + unpackDir := t.TempDir() + err = snapshot.UnpackInto(multiNumaSnapshot, unpackDir) + if err != nil { + t.Fatal(err) + } // from now on we use constants reflecting the content of the snapshot we requested, // which we reviewed beforehand. IOW, you need to know the content of the // snapshot to fully understand this test. Inspect it using // GHW_SNAPSHOT_PATH="/path/to/linux-amd64-intel-xeon-L5640.tar.gz" ghwc topology - - memInfo, err := memory.New(option.WithSnapshot(option.SnapshotOptions{ - Path: multiNumaSnapshot, - })) - + memInfo, err := memory.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) } @@ -106,10 +111,7 @@ func TestTopologyPerNUMAMemory(t *testing.T) { t.Fatalf("Expected non-nil MemoryInfo, but got nil") } - info, err := topology.New(option.WithSnapshot(option.SnapshotOptions{ - Path: multiNumaSnapshot, - })) - + info, err := topology.New(option.WithChroot(unpackDir)) if err != nil { t.Fatalf("Expected nil err, but got %v", err) } diff --git a/pkg/topology/topology_stub.go b/pkg/topology/topology_stub.go index b5ee4354..154f9293 100644 --- a/pkg/topology/topology_stub.go +++ b/pkg/topology/topology_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "github.com/pkg/errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("topologyFillInfo not implemented on " + runtime.GOOS) } diff --git a/pkg/topology/topology_windows.go b/pkg/topology/topology_windows.go index 2991aaa9..7a01b8d4 100644 --- a/pkg/topology/topology_windows.go +++ b/pkg/topology/topology_windows.go @@ -10,6 +10,8 @@ import ( "fmt" "syscall" "unsafe" + + "github.com/jaypipes/ghw/pkg/option" ) const ( @@ -24,7 +26,7 @@ const ( relationGroup = 4 ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { nodes, err := topologyNodes() if err != nil { return err diff --git a/pkg/usb/usb.go b/pkg/usb/usb.go index 4418f0d3..381203a6 100644 --- a/pkg/usb/usb.go +++ b/pkg/usb/usb.go @@ -9,7 +9,6 @@ import ( "fmt" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/marshal" "github.com/jaypipes/ghw/pkg/option" ) @@ -71,7 +70,6 @@ func (d Device) String() string { // Info describes all network interface controllers (NICs) in the host system. type Info struct { - ctx *context.Context Devices []*Device `json:"devices"` } @@ -86,13 +84,15 @@ func (i *Info) String() string { // New returns a pointer to an Info struct that contains information about the // network interface controllers (NICs) on the host system -func New(opts ...*option.Option) (*Info, error) { - ctx := context.New(opts...) - info := &Info{ctx: ctx} - if err := ctx.Do(info.load); err != nil { +func New(opt ...option.Option) (*Info, error) { + opts := &option.Options{} + for _, o := range opt { + o(opts) + } + info := &Info{} + if err := info.load(opts); err != nil { return nil, err } - return info, nil } @@ -105,11 +105,11 @@ type usbPrinter struct { // YAMLString returns a string with the net information formatted as YAML // under a top-level "net:" key func (i *Info) YAMLString() string { - return marshal.SafeYAML(i.ctx, usbPrinter{i}) + return marshal.SafeYAML(usbPrinter{i}) } // JSONString returns a string with the net information formatted as JSON // under a top-level "net:" key func (i *Info) JSONString(indent bool) string { - return marshal.SafeJSON(i.ctx, usbPrinter{i}, indent) + return marshal.SafeJSON(usbPrinter{i}, indent) } diff --git a/pkg/usb/usb_linux.go b/pkg/usb/usb_linux.go index 14dd6ae4..c1228d44 100644 --- a/pkg/usb/usb_linux.go +++ b/pkg/usb/usb_linux.go @@ -13,14 +13,14 @@ import ( "path/filepath" "strings" - "github.com/jaypipes/ghw/pkg/context" "github.com/jaypipes/ghw/pkg/linuxpath" + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { var errs []error - i.Devices, errs = usbs(i.ctx) + i.Devices, errs = usbs(opts) if len(errs) == 0 { return nil @@ -76,18 +76,19 @@ func slurp(path string) string { return string(bytes.TrimSpace(bs)) } -func usbs(ctx *context.Context) ([]*Device, []error) { +func usbs(opts *option.Options) ([]*Device, []error) { + paths := linuxpath.New(opts) devs := make([]*Device, 0) errs := []error{} - paths := linuxpath.New(ctx) usbDevicesDirs, err := os.ReadDir(paths.SysBusUsbDevices) if err != nil { return devs, []error{err} } for _, dir := range usbDevicesDirs { - fullDir, err := os.Readlink(filepath.Join(paths.SysBusUsbDevices, dir.Name())) + linkPath := filepath.Join(paths.SysBusUsbDevices, dir.Name()) + fullDir, err := os.Readlink(linkPath) if err != nil { continue } diff --git a/pkg/usb/usb_stub.go b/pkg/usb/usb_stub.go index 3c1ccdee..9c3c3041 100644 --- a/pkg/usb/usb_stub.go +++ b/pkg/usb/usb_stub.go @@ -12,8 +12,10 @@ import ( "runtime" "errors" + + "github.com/jaypipes/ghw/pkg/option" ) -func (i *Info) load() error { +func (i *Info) load(opts *option.Options) error { return errors.New("usb load not implemented on " + runtime.GOOS) } diff --git a/pkg/util/util.go b/pkg/util/util.go index 816aeb1b..75401200 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -12,7 +12,7 @@ import ( "strconv" "strings" - "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/option" ) const ( @@ -34,17 +34,17 @@ func SafeClose(c closer) { // -1 if there were file permissions or existence errors or if the contents // could not be successfully converted to an integer. In any error, a warning // message is printed to STDERR and -1 is returned. -func SafeIntFromFile(ctx *context.Context, path string) int { +func SafeIntFromFile(opts *option.Options, path string) int { msg := "failed to read int from file: %s\n" buf, err := os.ReadFile(path) if err != nil { - ctx.Warn(msg, err) + opts.Warn(msg, err) return -1 } contents := strings.TrimSpace(string(buf)) res, err := strconv.Atoi(contents) if err != nil { - ctx.Warn(msg, err) + opts.Warn(msg, err) return -1 } return res