feat(region,host): server cpuset supported (#13527)

* feat(region,host): server cpuset supported

* feat(region,host): cpuset remove
This commit is contained in:
Zexi Li
2022-02-23 16:07:01 +08:00
committed by GitHub
parent 06e3c9cc81
commit b5d8522f37
60 changed files with 3787 additions and 18 deletions

View File

@@ -17,6 +17,7 @@ package compute
import (
"context"
"fmt"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
@@ -117,6 +118,33 @@ func init() {
return nil
})
type HostSysInfoOpt struct {
options.BaseIdOptions
Key string `help:"The key for extract, e.g. 'cpu_info.processors'"`
Format string `help:"Output format" choices:"yaml|json" default:"yaml"`
}
R(&HostSysInfoOpt{}, "host-sysinfo", "Get host system info", func(s *mcclient.ClientSession, args *HostSysInfoOpt) error {
obj, err := modules.Hosts.Get(s, args.GetId(), nil)
if err != nil {
return err
}
keys := []string{"sys_info"}
if args.Key != "" {
keys = append(keys, strings.Split(args.Key, ".")...)
}
sysInfo, err := obj.Get(keys...)
if err != nil {
return errors.Wrap(err, "Get sys_info")
}
if args.Format == "yaml" {
fmt.Print(sysInfo.YAMLString())
} else {
fmt.Print(sysInfo.PrettyString())
}
return nil
})
type HostUpdateOptions struct {
ID string `help:"ID or Name of Host"`
Name string `help:"New name of the host"`

View File

@@ -100,6 +100,8 @@ func init() {
cmd.Perform("user-metadata", &baseoptions.ResourceMetadataOptions{})
cmd.Perform("set-user-metadata", &baseoptions.ResourceMetadataOptions{})
cmd.Perform("probe-isolated-devices", &options.ServerIdOptions{})
cmd.Perform("cpuset", &options.ServerCPUSetOptions{})
cmd.Perform("cpuset-remove", &options.ServerIdOptions{})
cmd.Get("vnc", new(options.ServerIdOptions))
cmd.Get("desc", new(options.ServerIdOptions))

3
go.mod
View File

@@ -20,7 +20,6 @@ require (
github.com/RoaringBitmap/roaring v0.4.16 // indirect
github.com/Shopify/sarama v1.20.0 // indirect
github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.684
github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible
github.com/anacrolix/dht v0.0.0-20181129074040-b09db78595aa // indirect
@@ -58,7 +57,6 @@ require (
github.com/gin-gonic/gin v1.7.0
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect
github.com/go-logfmt/logfmt v0.4.0 // indirect
github.com/go-ole/go-ole v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/gofrs/uuid v4.1.0+incompatible // indirect
@@ -77,6 +75,7 @@ require (
github.com/hako/durafmt v0.0.0-20180520121703-7b7ae1e72ead
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jaypipes/ghw v0.8.0
github.com/jdcloud-api/jdcloud-sdk-go v1.55.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c

14
go.sum
View File

@@ -84,8 +84,8 @@ github.com/Shopify/sarama v1.20.0 h1:wAMHhl1lGRlobeoV/xOKpbqD2OQsOvY4A/vIOGroIe8
github.com/Shopify/sarama v1.20.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845/go.mod h1:c8Mh99Cw82nrsAnPgxQSZHkswVOJF7/MqZb1ZdvriLM=
@@ -255,8 +255,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-ole/go-ole v1.2.2 h1:QNWhweRd9D5Py2rRVboZ2L4SEoW/dyraWJCc8bgS8kE=
github.com/go-ole/go-ole v1.2.2/go.mod h1:pnvuG7BrDMZ8ifMurTQmxwhQM/odqm9sSqNe5BUI7v4=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
@@ -379,6 +379,9 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jaypipes/ghw v0.8.0 h1:02q1pTm9CD83vuhBsEZZhOCS128pq87uyaQeJZkp3sQ=
github.com/jaypipes/ghw v0.8.0/go.mod h1:+gR9bjm3W/HnFi90liF+Fj9GpCe/Dsibl9Im8KmC7c4=
github.com/jaypipes/pcidb v0.6.0/go.mod h1:L2RGk04sfRhp5wvHO0gfRAMoLY/F3PKv/nwJeVoho0o=
github.com/jdcloud-api/jdcloud-sdk-go v1.55.0 h1:mzVj8r6fluEwjn8ogqtGfYW2qSIVUaEq0JAsvjCav3A=
github.com/jdcloud-api/jdcloud-sdk-go v1.55.0/go.mod h1:UrKjuULIWLjHFlG6aSPunArE5QX57LftMmStAZJBEX8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -488,6 +491,7 @@ github.com/minio/minio-go/v6 v6.0.33 h1:CNThMAZ9bN6OEIP8DulqlYpaXKJ18rgG/Cqm14Q4
github.com/minio/minio-go/v6 v6.0.33/go.mod h1:vaNT59cWULS37E+E9zkuN/BVnKHyXtVGS+b04Boc66Y=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
@@ -598,6 +602,7 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -923,6 +928,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
k8s.io/api v0.19.3 h1:GN6ntFnv44Vptj/b+OnMW7FmzkpDoIDLZRvKX3XH9aU=
k8s.io/api v0.19.3/go.mod h1:VF+5FT1B74Pw3KxMdKyinLo+zynBaMBiAfGMuldcNDs=
k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc=

View File

@@ -330,6 +330,7 @@ const (
VM_METADATA_OS_DISTRO = "os_distribution"
VM_METADATA_OS_NAME = "os_name"
VM_METADATA_OS_VERSION = "os_version"
VM_METADATA_CGROUP_CPUSET = "cgroup_cpuset"
)
func Hypervisors2HostTypes(hypervisors []string) []string {

View File

@@ -797,3 +797,17 @@ type ServerCreateSnapshotParams struct {
Name string `json:"name"`
GenerateName string `json:"generate_name"`
}
type ServerCPUSetInput struct {
// Specifies the CPUs that tasks in this cgroup are permitted to access.
CPUS []int `json:"cpus"`
}
type ServerCPUSetResp struct{}
type ServerCPUSetRemoveInput struct{}
type ServerCPUSetRemoveResp struct {
Done bool `json:"done"`
Error string `json:"error"`
}

View File

@@ -14,7 +14,20 @@
package host
import (
"github.com/jaypipes/ghw/pkg/cpu"
"github.com/jaypipes/ghw/pkg/topology"
)
type ServerCloneDiskFromStorageResponse struct {
TargetAccessPath string `json:"target_access_path"`
TargetFormat string `json:"target_format"`
}
type HostTopology struct {
*topology.Info
}
type HostCPUInfo struct {
*cpu.Info
}

View File

@@ -227,6 +227,11 @@ const (
ACT_GUEST_SRC_CHECK = "guest_src_check"
ACT_GUEST_CPUSET = "guest_cpuset"
ACT_GUEST_CPUSET_FAIL = "guest_cpuset_fail"
ACT_GUEST_CPUSET_REMOVE = "guest_cpuset_remove"
ACT_GUEST_CPUSET_REMOVE_FAIL = "guest_cpuset_remove_fail"
ACT_CHANGE_BANDWIDTH = "eip_change_bandwidth"
ACT_EIP_CONVERT_FAIL = "eip_convert_fail"

View File

@@ -453,3 +453,11 @@ func (self *SBaseGuestDriver) RequestSyncIsolatedDevice(ctx context.Context, gue
task.ScheduleRun(nil)
return nil
}
func (self *SBaseGuestDriver) RequestCPUSet(ctx context.Context, userCred mcclient.TokenCredential, host *models.SHost, guest *models.SGuest, input *api.ServerCPUSetInput) (*api.ServerCPUSetResp, error) {
return nil, httperrors.ErrNotImplemented
}
func (self *SBaseGuestDriver) RequestCPUSetRemove(ctx context.Context, userCred mcclient.TokenCredential, host *models.SHost, guest *models.SGuest, input *api.ServerCPUSetRemoveInput) error {
return httperrors.ErrNotImplemented
}

View File

@@ -925,3 +925,34 @@ func (self *SKVMGuestDriver) ValidateUpdateData(ctx context.Context, guest *mode
func (self *SKVMGuestDriver) RequestSyncIsolatedDevice(ctx context.Context, guest *models.SGuest, task taskman.ITask) error {
return guest.StartSyncTask(ctx, task.GetUserCred(), false, task.GetTaskId())
}
func (self *SKVMGuestDriver) RequestCPUSet(ctx context.Context, userCred mcclient.TokenCredential, host *models.SHost, guest *models.SGuest, input *api.ServerCPUSetInput) (*api.ServerCPUSetResp, error) {
url := fmt.Sprintf("%s/servers/%s/cpuset", host.ManagerUri, guest.Id)
httpClient := httputils.GetDefaultClient()
header := mcclient.GetTokenHeaders(userCred)
body := jsonutils.Marshal(input)
_, respBody, err := httputils.JSONRequest(httpClient, ctx, "POST", url, header, body, false)
if err != nil {
return nil, errors.Wrap(err, "host request")
}
resp := new(api.ServerCPUSetResp)
if respBody == nil {
return resp, nil
}
if err := respBody.Unmarshal(resp); err != nil {
return nil, errors.Wrap(err, "unmarshal response")
}
return resp, nil
}
func (self *SKVMGuestDriver) RequestCPUSetRemove(ctx context.Context, userCred mcclient.TokenCredential, host *models.SHost, guest *models.SGuest, input *api.ServerCPUSetRemoveInput) error {
url := fmt.Sprintf("%s/servers/%s/cpuset-remove", host.ManagerUri, guest.Id)
httpClient := httputils.GetDefaultClient()
header := mcclient.GetTokenHeaders(userCred)
body := jsonutils.Marshal(input)
_, _, err := httputils.JSONRequest(httpClient, ctx, "POST", url, header, body, false)
if err != nil {
return errors.Wrap(err, "host request")
}
return nil
}

View File

@@ -40,6 +40,7 @@ import (
"yunion.io/x/onecloud/pkg/apis"
billing_api "yunion.io/x/onecloud/pkg/apis/billing"
api "yunion.io/x/onecloud/pkg/apis/compute"
hostapi "yunion.io/x/onecloud/pkg/apis/host"
imageapi "yunion.io/x/onecloud/pkg/apis/image"
noapi "yunion.io/x/onecloud/pkg/apis/notify"
schedapi "yunion.io/x/onecloud/pkg/apis/scheduler"
@@ -5346,3 +5347,69 @@ func (self *SGuest) PerformProbeIsolatedDevices(ctx context.Context, userCred mc
}
return jsonutils.Marshal(devs), nil
}
func (self *SGuest) PerformCpuset(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *api.ServerCPUSetInput) (jsonutils.JSONObject, error) {
host, err := self.GetHost()
if err != nil {
return nil, errors.Wrap(err, "get host model")
}
topoObj, err := host.SysInfo.Get("topology")
if err != nil {
return nil, errors.Wrap(err, "get topology from host sys_info")
}
hostTopo := new(hostapi.HostTopology)
if err := topoObj.Unmarshal(hostTopo); err != nil {
return nil, errors.Wrap(err, "Unmarshal host topology struct")
}
// get host logical cores
allCores := []int{}
for _, node := range hostTopo.Nodes {
for _, cores := range node.Cores {
allCores = append(allCores, cores.LogicalProcessors...)
}
}
if !sets.NewInt(allCores...).HasAll(data.CPUS...) {
return nil, httperrors.NewInputParameterError("Host cores %v not contains input %v", allCores, data.CPUS)
}
if err := self.SetMetadata(ctx, api.VM_METADATA_CGROUP_CPUSET, data, userCred); err != nil {
return nil, errors.Wrap(err, "set metadata")
}
return nil, self.StartGuestCPUSetTask(ctx, userCred, data)
}
func (self *SGuest) StartGuestCPUSetTask(ctx context.Context, userCred mcclient.TokenCredential, input *api.ServerCPUSetInput) error {
task, err := taskman.TaskManager.NewTask(ctx, "GuestCPUSetTask", self, userCred, jsonutils.Marshal(input).(*jsonutils.JSONDict), "", "")
if err != nil {
return errors.Wrap(err, "New GuestCPUSetTask")
}
return task.ScheduleRun(nil)
}
func (self *SGuest) PerformCpusetRemove(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *api.ServerCPUSetRemoveInput) (*api.ServerCPUSetRemoveResp, error) {
if err := self.RemoveMetadata(ctx, api.VM_METADATA_CGROUP_CPUSET, userCred); err != nil {
return nil, errors.Wrapf(err, "remove metadata %q", api.VM_METADATA_CGROUP_CPUSET)
}
host, err := self.GetHost()
if err != nil {
return nil, errors.Wrap(err, "get host model")
}
// TODO: maybe change to async task
db.OpsLog.LogEvent(self, db.ACT_GUEST_CPUSET_REMOVE, nil, userCred)
resp := new(api.ServerCPUSetRemoveResp)
if err := self.GetDriver().RequestCPUSetRemove(ctx, userCred, host, self, data); err != nil {
db.OpsLog.LogEvent(self, db.ACT_GUEST_CPUSET_REMOVE_FAIL, err, userCred)
logclient.AddActionLogWithContext(ctx, self, logclient.ACT_VM_CPUSET_REMOVE, data, userCred, false)
resp.Error = err.Error()
} else {
db.OpsLog.LogEvent(self, db.ACT_GUEST_CPUSET_REMOVE, nil, userCred)
logclient.AddActionLogWithContext(ctx, self, logclient.ACT_VM_CPUSET_REMOVE, data, userCred, true)
resp.Done = true
}
return resp, nil
}

View File

@@ -217,6 +217,9 @@ type IGuestDriver interface {
RequestChangeDiskStorage(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, input *api.ServerChangeDiskStorageInternalInput, task taskman.ITask) error
RequestSyncIsolatedDevice(ctx context.Context, guest *SGuest, task taskman.ITask) error
RequestCPUSet(ctx context.Context, userCred mcclient.TokenCredential, host *SHost, guest *SGuest, input *api.ServerCPUSetInput) (*api.ServerCPUSetResp, error)
RequestCPUSetRemove(ctx context.Context, userCred mcclient.TokenCredential, host *SHost, guest *SGuest, input *api.ServerCPUSetRemoveInput) error
}
var guestDrivers map[string]IGuestDriver

View File

@@ -0,0 +1,67 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tasks
import (
"context"
"yunion.io/x/jsonutils"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/compute/models"
"yunion.io/x/onecloud/pkg/util/logclient"
)
type GuestCPUSetTask struct {
SGuestBaseTask
}
func init() {
taskman.RegisterTask(GuestCPUSetTask{})
}
func (self *GuestCPUSetTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
guest := obj.(*models.SGuest)
self.SetStage("OnSyncComplete", nil)
db.OpsLog.LogEvent(guest, db.ACT_GUEST_CPUSET, self.GetParams(), self.UserCred)
if err := guest.StartSyncTask(ctx, self.GetUserCred(), true, self.GetTaskId()); err != nil {
self.setStageFailed(ctx, guest, data)
}
}
func (self *GuestCPUSetTask) setStageFailed(ctx context.Context, obj *models.SGuest, data jsonutils.JSONObject) {
logclient.AddActionLogWithStartable(self, obj, logclient.ACT_VM_CPUSET, data, self.UserCred, false)
db.OpsLog.LogEvent(obj, db.ACT_GUEST_CPUSET_FAIL, data, self.UserCred)
self.SetStageFailed(ctx, data)
}
func (self *GuestCPUSetTask) OnSyncComplete(ctx context.Context, obj *models.SGuest, data jsonutils.JSONObject) {
host, _ := obj.GetHost()
input := new(api.ServerCPUSetInput)
self.GetParams().Unmarshal(input)
_, err := obj.GetDriver().RequestCPUSet(ctx, self.GetUserCred(), host, obj, input)
if err != nil {
self.setStageFailed(ctx, obj, jsonutils.NewString(err.Error()))
return
}
logclient.AddActionLogWithStartable(self, obj, logclient.ACT_VM_CPUSET, input, self.UserCred, true)
self.SetStageComplete(ctx, nil)
}
func (self *GuestCPUSetTask) OnSyncCompleteFailed(ctx context.Context, obj *models.SGuest, data jsonutils.JSONObject) {
self.setStageFailed(ctx, obj, data)
}

View File

@@ -84,6 +84,8 @@ func AddGuestTaskHandler(prefix string, app *appsrv.Application) {
"list-forward": guestListForward,
"close-forward": guestCloseForward,
"storage-clone-disk": guestStorageCloneDisk,
"cpuset": guestCPUSet,
"cpuset-remove": guestCPUSetRemove,
} {
app.AddHandler("POST",
fmt.Sprintf("%s/%s/<sid>/%s", prefix, keyWord, action),
@@ -625,3 +627,20 @@ func guestStorageCloneDisk(ctx context.Context, sid string, body jsonutils.JSONO
hostutils.DelayTaskWithoutReqctx(ctx, guestman.GetGuestManager().StorageCloneDisk, params)
return nil, nil
}
func guestCPUSet(ctx context.Context, sid string, body jsonutils.JSONObject) (interface{}, error) {
input := new(computeapi.ServerCPUSetInput)
if err := body.Unmarshal(input); err != nil {
return nil, err
}
gm := guestman.GetGuestManager()
return gm.CPUSet(ctx, sid, input)
}
func guestCPUSetRemove(ctx context.Context, sid string, body jsonutils.JSONObject) (interface{}, error) {
gm := guestman.GetGuestManager()
if err := gm.CPUSetRemove(ctx, sid); err != nil {
return nil, err
}
return nil, nil
}

View File

@@ -254,6 +254,22 @@ func (m *SGuestManager) cpusetBalance() {
}
}
func (m *SGuestManager) CPUSet(ctx context.Context, sid string, req *compute.ServerCPUSetInput) (*compute.ServerCPUSetResp, error) {
guest, ok := m.GetServer(sid)
if !ok {
return nil, httperrors.NewNotFoundError("Not found")
}
return guest.CPUSet(ctx, req)
}
func (m *SGuestManager) CPUSetRemove(ctx context.Context, sid string) error {
guest, ok := m.GetServer(sid)
if !ok {
return httperrors.NewNotFoundError("Not found")
}
return guest.CPUSetRemove(ctx)
}
func (m *SGuestManager) IsGuestDir(f os.FileInfo) bool {
if !regutils.MatchUUID(f.Name()) {
return false

View File

@@ -1446,6 +1446,7 @@ func (s *SKVMGuestInstance) SetCgroup() {
s.cgroupPid = s.GetPid()
s.setCgroupIo()
s.setCgroupCpu()
s.setCgroupCPUSet()
}
func (s *SKVMGuestInstance) setCgroupIo() {
@@ -1475,6 +1476,31 @@ func (s *SKVMGuestInstance) setCgroupCpu() {
cgrouputils.CgroupSet(strconv.Itoa(s.cgroupPid), int(cpu)*cpuWeight)
}
func (s *SKVMGuestInstance) setCgroupCPUSet() {
meta, _ := s.Desc.Get("metadata")
if meta == nil {
return
}
cpusetStr, _ := meta.GetString(api.VM_METADATA_CGROUP_CPUSET)
if len(cpusetStr) == 0 {
return
}
obj, err := jsonutils.ParseString(cpusetStr)
if err != nil {
log.Errorf("Parse cpusetStr %q error: %v", cpusetStr, err)
return
}
input := new(api.ServerCPUSetInput)
if err := obj.Unmarshal(input); err != nil {
log.Errorf("Unmarshal %q to ServerCPUSetInput: %v", obj, err)
return
}
if _, err := s.CPUSet(context.Background(), input); err != nil {
log.Errorf("Do CPUSet error: %v", err)
return
}
}
func (s *SKVMGuestInstance) CreateFromDesc(desc jsonutils.JSONObject) error {
if err := s.PrepareDir(); err != nil {
uuid, _ := desc.GetString("uuid")
@@ -1839,3 +1865,38 @@ func (s *SKVMGuestInstance) getQemuCmdlineFromContent(content string) (string, e
}
return cmdStr, nil
}
func (s *SKVMGuestInstance) CPUSet(ctx context.Context, input *api.ServerCPUSetInput) (*api.ServerCPUSetResp, error) {
if !s.IsRunning() {
return nil, nil
}
cpus := []string{}
for _, id := range input.CPUS {
cpus = append(cpus, fmt.Sprintf("%d", id))
}
task := cgrouputils.NewCGroupCPUSetTask(strconv.Itoa(s.GetPid()), 0, strings.Join(cpus, ","))
if !task.SetTask() {
return nil, errors.Errorf("Cgroup cpuset task failed")
}
return new(api.ServerCPUSetResp), nil
}
func (s *SKVMGuestInstance) CPUSetRemove(ctx context.Context) error {
metadata, err := s.Desc.Get("metadata")
if err != nil {
return errors.Wrap(err, "get metadata from desc")
}
metadata.(*jsonutils.JSONDict).Remove(api.VM_METADATA_CGROUP_CPUSET)
s.Desc.Set("metadata", metadata)
if err := s.SaveDesc(s.Desc); err != nil {
return errors.Wrap(err, "save desc after update metadata")
}
if !s.IsRunning() {
return nil
}
task := cgrouputils.NewCGroupCPUSetTask(strconv.Itoa(s.GetPid()), 0, "")
if !task.RemoveTask() {
return errors.Errorf("Remove task error happened, please lookup host log")
}
return nil
}

View File

@@ -44,6 +44,7 @@ import (
"yunion.io/x/onecloud/pkg/hostman/hostinfo/hostbridge"
"yunion.io/x/onecloud/pkg/hostman/hostinfo/hostconsts"
"yunion.io/x/onecloud/pkg/hostman/hostutils"
"yunion.io/x/onecloud/pkg/hostman/hostutils/hardware"
"yunion.io/x/onecloud/pkg/hostman/hostutils/kubelet"
"yunion.io/x/onecloud/pkg/hostman/isolated_device"
"yunion.io/x/onecloud/pkg/hostman/options"
@@ -458,6 +459,12 @@ func (h *SHostInfo) detectHostInfo() error {
h.detectStorageSystem()
topoInfo, err := hardware.GetTopology()
if err != nil {
return errors.Wrap(err, "Get hardware topology")
}
h.sysinfo.Topology = topoInfo
system_service.Init()
if options.HostOptions.CheckSystemServices {
if err := h.checkSystemServices(); err != nil {

View File

@@ -32,6 +32,7 @@ import (
"yunion.io/x/pkg/util/netutils"
"yunion.io/x/pkg/util/regutils"
hostapi "yunion.io/x/onecloud/pkg/apis/host"
"yunion.io/x/onecloud/pkg/cloudcommon/types"
"yunion.io/x/onecloud/pkg/hostman/hostinfo/hostbridge"
"yunion.io/x/onecloud/pkg/hostman/hostinfo/hostdhcp"
@@ -324,6 +325,8 @@ type SSysInfo struct {
HugepagesOption string `json:"hugepages_option"`
HugepageSizeKb int `json:"hugepage_size_kb"`
Topology *hostapi.HostTopology `json:"topology"`
}
func StartDetachStorages(hs []jsonutils.JSONObject) {

View File

@@ -0,0 +1,31 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hardware
import (
"github.com/jaypipes/ghw/pkg/cpu"
"yunion.io/x/onecloud/pkg/apis/host"
)
func GetCPU() (*host.HostCPUInfo, error) {
info, err := cpu.New()
if err != nil {
return nil, err
}
return &host.HostCPUInfo{
Info: info,
}, nil
}

View File

@@ -0,0 +1 @@
package hardware // import "yunion.io/x/onecloud/pkg/hostman/hostutils/hardware"

View File

@@ -0,0 +1,31 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hardware
import (
"github.com/jaypipes/ghw/pkg/topology"
"yunion.io/x/onecloud/pkg/apis/host"
)
func GetTopology() (*host.HostTopology, error) {
info, err := topology.New()
if err != nil {
return nil, err
}
return &host.HostTopology{
Info: info,
}, nil
}

View File

@@ -29,6 +29,7 @@ import (
schedapi "yunion.io/x/onecloud/pkg/apis/scheduler"
"yunion.io/x/onecloud/pkg/cloudcommon/cmdline"
"yunion.io/x/onecloud/pkg/mcclient/options"
"yunion.io/x/onecloud/pkg/util/cgrouputils"
)
var ErrEmtptyUpdate = errors.New("No valid update data")
@@ -1176,3 +1177,27 @@ type ServerChangeDiskStorageOptions struct {
func (o *ServerChangeDiskStorageOptions) Params() (jsonutils.JSONObject, error) {
return jsonutils.Marshal(o), nil
}
type ServerCPUSetOptions struct {
options.BaseIdOptions
SETS string `help:"Cgroup cpusets CPUs spec string, e.g. '0-2,16'"`
}
func (o *ServerCPUSetOptions) Params() (jsonutils.JSONObject, error) {
sets := cgrouputils.ParseCpusetStr(o.SETS)
parts := strings.Split(sets, ",")
if len(parts) == 0 {
return nil, errors.New(fmt.Sprintf("Invalid cpu sets %q", o.SETS))
}
input := &computeapi.ServerCPUSetInput{
CPUS: make([]int, 0),
}
for _, s := range parts {
sd, err := strconv.Atoi(s)
if err != nil {
return nil, errors.New(fmt.Sprintf("Not digit part %q", s))
}
input.CPUS = append(input.CPUS, sd)
}
return jsonutils.Marshal(input), nil
}

View File

@@ -67,6 +67,8 @@ const (
ACT_VM_ASSIGNSECGROUP = "vm_assignsecgroup"
ACT_VM_REVOKESECGROUP = "vm_revokesecgroup"
ACT_VM_SETSECGROUP = "vm_setsecgroup"
ACT_VM_CPUSET = "vm_cpuset"
ACT_VM_CPUSET_REMOVE = "vm_cpuset_remove"
ACT_RESET_DISK = "reset_disk"
ACT_SYNC_STATUS = "sync_status"
ACT_SYNC_CONF = "sync_conf"

View File

@@ -77,7 +77,7 @@ func (s *SWbemServices) process(initError chan error) {
//fmt.Println("process: starting background thread initialization")
//All OLE/WMI calls must happen on the same initialized thead, so lock this goroutine
runtime.LockOSThread()
defer runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
if err != nil {

View File

@@ -285,6 +285,10 @@ func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismat
}
defer prop.Clear()
if prop.VT == 0x1 { //VT_NULL
continue
}
switch val := prop.Value().(type) {
case int8, int16, int32, int64, int:
v := reflect.ValueOf(val).Int()
@@ -383,7 +387,7 @@ func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismat
}
f.Set(fArr)
}
case reflect.Uint8:
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
safeArray := prop.ToArray()
if safeArray != nil {
arr := safeArray.ToValueArray()
@@ -394,6 +398,17 @@ func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismat
}
f.Set(fArr)
}
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
safeArray := prop.ToArray()
if safeArray != nil {
arr := safeArray.ToValueArray()
fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr))
for i, v := range arr {
s := fArr.Index(i)
s.SetInt(reflect.ValueOf(v).Int())
}
f.Set(fArr)
}
default:
return &ErrFieldMismatch{
StructType: of.Type(),

View File

@@ -2,8 +2,7 @@ language: go
sudo: false
go:
- 1.1
- 1.2
- 1.3
- 1.4
- 1.9.x
- 1.10.x
- 1.11.x
- tip

View File

@@ -1,5 +1,3 @@
module github.com/go-ole/go-ole
go 1.12
require github.com/go-ole/go-ole v1.2.2

View File

@@ -1,2 +0,0 @@
github.com/go-ole/go-ole v1.2.2 h1:HXmymm3IQ8iAfpqlbbUGLHd+SZrnmI4y1pv+WL/3R7c=
github.com/go-ole/go-ole v1.2.2/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=

176
vendor/github.com/jaypipes/ghw/COPYING generated vendored Normal file
View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

128
vendor/github.com/jaypipes/ghw/pkg/context/context.go generated vendored Normal file
View File

@@ -0,0 +1,128 @@
//
// 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 (
"github.com/jaypipes/ghw/pkg/option"
"github.com/jaypipes/ghw/pkg/snapshot"
)
// Concrete 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
snapshotUnpackedPath string
alert option.Alerter
}
// New returns a Context struct pointer that has had various options set on it
func New(opts ...*option.Option) *Context {
merged := option.Merge(opts...)
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
}
return ctx
}
// FromEnv returns an Option 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 ctx.Teardown()
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.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...)
}

169
vendor/github.com/jaypipes/ghw/pkg/cpu/cpu.go generated vendored Normal file
View File

@@ -0,0 +1,169 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package cpu
import (
"fmt"
"github.com/jaypipes/ghw/pkg/context"
"github.com/jaypipes/ghw/pkg/marshal"
"github.com/jaypipes/ghw/pkg/option"
)
// ProcessorCore describes a physical host processor core. A processor core is
// a separate processing unit within some types of central processing units
// (CPU).
type ProcessorCore struct {
// ID is the `uint32` identifier that the host gave this core. Note that
// this does *not* necessarily equate to a zero-based index of the core
// within a physical package. For example, the core IDs for an Intel Core
// i7 are 0, 1, 2, 8, 9, and 10
ID int `json:"id"`
// Index is the zero-based index of the core on the physical processor
// package
Index int `json:"index"`
// NumThreads is the number of hardware threads associated with the core
NumThreads uint32 `json:"total_threads"`
// LogicalProcessors is a slice of ints representing the logical processor
// IDs assigned to any processing unit for the core
LogicalProcessors []int `json:"logical_processors"`
}
// String returns a short string indicating important information about the
// processor core
func (c *ProcessorCore) String() string {
return fmt.Sprintf(
"processor core #%d (%d threads), logical processors %v",
c.Index,
c.NumThreads,
c.LogicalProcessors,
)
}
// Processor describes a physical host central processing unit (CPU).
type Processor struct {
// ID is the physical processor `uint32` ID according to the system
ID int `json:"id"`
// NumCores is the number of physical cores in the processor package
NumCores uint32 `json:"total_cores"`
// NumThreads is the number of hardware threads in the processor package
NumThreads uint32 `json:"total_threads"`
// Vendor is a string containing the vendor name
Vendor string `json:"vendor"`
// Model` is a string containing the vendor's model name
Model string `json:"model"`
// Capabilities is a slice of strings indicating the features the processor
// has enabled
Capabilities []string `json:"capabilities"`
// Cores is a slice of ProcessorCore` struct pointers that are packed onto
// this physical processor
Cores []*ProcessorCore `json:"cores"`
}
// HasCapability returns true if the Processor has the supplied cpuid
// capability, false otherwise. Example of cpuid capabilities would be 'vmx' or
// 'sse4_2'. To see a list of potential cpuid capabilitiies, see the section on
// CPUID feature bits in the following article:
//
// https://en.wikipedia.org/wiki/CPUID
func (p *Processor) HasCapability(find string) bool {
for _, c := range p.Capabilities {
if c == find {
return true
}
}
return false
}
// String returns a short string describing the Processor
func (p *Processor) String() string {
ncs := "cores"
if p.NumCores == 1 {
ncs = "core"
}
nts := "threads"
if p.NumThreads == 1 {
nts = "thread"
}
return fmt.Sprintf(
"physical package #%d (%d %s, %d hardware %s)",
p.ID,
p.NumCores,
ncs,
p.NumThreads,
nts,
)
}
// 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"`
// TotalThreads is the total number of hardware threads the host system
// contains
TotalThreads uint32 `json:"total_threads"`
// Processors is a slice of Processor struct pointers, one for each
// physical processor package contained in the host
Processors []*Processor `json:"processors"`
}
// 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 {
return nil, err
}
return info, nil
}
// String returns a short string indicating a summary of CPU information
func (i *Info) String() string {
nps := "packages"
if len(i.Processors) == 1 {
nps = "package"
}
ncs := "cores"
if i.TotalCores == 1 {
ncs = "core"
}
nts := "threads"
if i.TotalThreads == 1 {
nts = "thread"
}
return fmt.Sprintf(
"cpu (%d physical %s, %d %s, %d hardware %s)",
len(i.Processors),
nps,
i.TotalCores,
ncs,
i.TotalThreads,
nts,
)
}
// simple private struct used to encapsulate cpu information in a top-level
// "cpu" YAML/JSON map/object key
type cpuPrinter struct {
Info *Info `json:"cpu"`
}
// 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})
}
// 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)
}

220
vendor/github.com/jaypipes/ghw/pkg/cpu/cpu_linux.go generated vendored Normal file
View File

@@ -0,0 +1,220 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package cpu
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/jaypipes/ghw/pkg/context"
"github.com/jaypipes/ghw/pkg/linuxpath"
"github.com/jaypipes/ghw/pkg/util"
)
func (i *Info) load() error {
i.Processors = processorsGet(i.ctx)
var totCores uint32
var totThreads uint32
for _, p := range i.Processors {
totCores += p.NumCores
totThreads += p.NumThreads
}
i.TotalCores = totCores
i.TotalThreads = totThreads
return nil
}
func processorsGet(ctx *context.Context) []*Processor {
procs := make([]*Processor, 0)
paths := linuxpath.New(ctx)
r, err := os.Open(paths.ProcCpuinfo)
if err != nil {
return nil
}
defer util.SafeClose(r)
// An array of maps of attributes describing the logical processor
procAttrs := make([]map[string]string, 0)
curProcAttrs := make(map[string]string)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
// Output of /proc/cpuinfo has a blank newline to separate logical
// processors, so here we collect up all the attributes we've
// collected for this logical processor block
procAttrs = append(procAttrs, curProcAttrs)
// Reset the current set of processor attributes...
curProcAttrs = make(map[string]string)
continue
}
parts := strings.Split(line, ":")
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
curProcAttrs[key] = value
}
// Build a set of physical processor IDs which represent the physical
// package of the CPU
setPhysicalIDs := make(map[int]bool)
for _, attrs := range procAttrs {
pid, err := strconv.Atoi(attrs["physical id"])
if err != nil {
continue
}
setPhysicalIDs[pid] = true
}
for pid := range setPhysicalIDs {
p := &Processor{
ID: pid,
}
// The indexes into the array of attribute maps for each logical
// processor within the physical processor
lps := make([]int, 0)
for x := range procAttrs {
lppid, err := strconv.Atoi(procAttrs[x]["physical id"])
if err != nil {
continue
}
if pid == lppid {
lps = append(lps, x)
}
}
first := procAttrs[lps[0]]
p.Model = first["model name"]
p.Vendor = first["vendor_id"]
numCores, err := strconv.Atoi(first["cpu cores"])
if err != nil {
continue
}
p.NumCores = uint32(numCores)
numThreads, err := strconv.Atoi(first["siblings"])
if err != nil {
continue
}
p.NumThreads = uint32(numThreads)
// The flags field is a space-separated list of CPU capabilities
p.Capabilities = strings.Split(first["flags"], " ")
cores := make([]*ProcessorCore, 0)
for _, lpidx := range lps {
lpid, err := strconv.Atoi(procAttrs[lpidx]["processor"])
if err != nil {
continue
}
coreID, err := strconv.Atoi(procAttrs[lpidx]["core id"])
if err != nil {
continue
}
var core *ProcessorCore
for _, c := range cores {
if c.ID == coreID {
c.LogicalProcessors = append(
c.LogicalProcessors,
lpid,
)
c.NumThreads = uint32(len(c.LogicalProcessors))
core = c
}
}
if core == nil {
coreLps := make([]int, 1)
coreLps[0] = lpid
core = &ProcessorCore{
ID: coreID,
Index: len(cores),
NumThreads: 1,
LogicalProcessors: coreLps,
}
cores = append(cores, core)
}
}
p.Cores = cores
procs = append(procs, p)
}
return procs
}
func CoresForNode(ctx *context.Context, 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)
path := filepath.Join(
paths.SysDevicesSystemNode,
fmt.Sprintf("node%d", nodeID),
)
cores := make([]*ProcessorCore, 0)
findCoreByID := func(coreID int) *ProcessorCore {
for _, c := range cores {
if c.ID == coreID {
return c
}
}
c := &ProcessorCore{
ID: coreID,
Index: len(cores),
LogicalProcessors: make([]int, 0),
}
cores = append(cores, c)
return c
}
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
for _, file := range files {
filename := file.Name()
if !strings.HasPrefix(filename, "cpu") {
continue
}
if filename == "cpumap" || filename == "cpulist" {
// There are two files in the node directory that start with 'cpu'
// but are not subdirectories ('cpulist' and 'cpumap'). Ignore
// these files.
continue
}
// Grab the logical processor ID by cutting the integer from the
// /sys/devices/system/node/nodeX/cpuX filename
cpuPath := filepath.Join(path, filename)
procID, err := strconv.Atoi(filename[3:])
if err != nil {
_, _ = fmt.Fprintf(
os.Stderr,
"failed to determine procID from %s. Expected integer after 3rd char.",
filename,
)
continue
}
coreIDPath := filepath.Join(cpuPath, "topology", "core_id")
coreID := util.SafeIntFromFile(ctx, coreIDPath)
core := findCoreByID(coreID)
core.LogicalProcessors = append(
core.LogicalProcessors,
procID,
)
}
for _, c := range cores {
c.NumThreads = uint32(len(c.LogicalProcessors))
}
return cores, nil
}

17
vendor/github.com/jaypipes/ghw/pkg/cpu/cpu_stub.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
// +build !linux,!windows
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package cpu
import (
"runtime"
"github.com/pkg/errors"
)
func (i *Info) load() error {
return errors.New("cpu.Info.load not implemented on " + runtime.GOOS)
}

55
vendor/github.com/jaypipes/ghw/pkg/cpu/cpu_windows.go generated vendored Normal file
View File

@@ -0,0 +1,55 @@
// +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 cpu
import (
"github.com/StackExchange/wmi"
)
const wmqlProcessor = "SELECT Manufacturer, Name, NumberOfLogicalProcessors, NumberOfCores FROM Win32_Processor"
type win32Processor struct {
Manufacturer *string
Name *string
NumberOfLogicalProcessors uint32
NumberOfCores uint32
}
func (i *Info) load() error {
// Getting info from WMI
var win32descriptions []win32Processor
if err := wmi.Query(wmqlProcessor, &win32descriptions); err != nil {
return err
}
// Converting into standard structures
i.Processors = processorsGet(win32descriptions)
var totCores uint32
var totThreads uint32
for _, p := range i.Processors {
totCores += p.NumCores
totThreads += p.NumThreads
}
i.TotalCores = totCores
i.TotalThreads = totThreads
return nil
}
func processorsGet(win32descriptions []win32Processor) []*Processor {
var procs []*Processor
// Converting into standard structures
for index, description := range win32descriptions {
p := &Processor{
ID: index,
Model: *description.Name,
Vendor: *description.Manufacturer,
NumCores: description.NumberOfCores,
NumThreads: description.NumberOfLogicalProcessors,
}
procs = append(procs, p)
}
return procs
}

View File

@@ -0,0 +1,71 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package linuxpath
import (
"fmt"
"path/filepath"
"github.com/jaypipes/ghw/pkg/context"
)
type Paths struct {
VarLog string
ProcMeminfo string
ProcCpuinfo string
SysKernelMMHugepages string
EtcMtab string
SysBlock string
SysDevicesSystemNode string
SysDevicesSystemMemory string
SysBusPciDevices string
SysClassDRM string
SysClassDMI string
SysClassNet string
RunUdevData string
}
// New returns a new Paths struct containing filepath fields relative to the
// supplied Context
func New(ctx *context.Context) *Paths {
return &Paths{
VarLog: filepath.Join(ctx.Chroot, "var", "log"),
ProcMeminfo: filepath.Join(ctx.Chroot, "proc", "meminfo"),
ProcCpuinfo: filepath.Join(ctx.Chroot, "proc", "cpuinfo"),
SysKernelMMHugepages: filepath.Join(ctx.Chroot, "sys", "kernel", "mm", "hugepages"),
EtcMtab: filepath.Join(ctx.Chroot, "etc", "mtab"),
SysBlock: filepath.Join(ctx.Chroot, "sys", "block"),
SysDevicesSystemNode: filepath.Join(ctx.Chroot, "sys", "devices", "system", "node"),
SysDevicesSystemMemory: filepath.Join(ctx.Chroot, "sys", "devices", "system", "memory"),
SysBusPciDevices: filepath.Join(ctx.Chroot, "sys", "bus", "pci", "devices"),
SysClassDRM: filepath.Join(ctx.Chroot, "sys", "class", "drm"),
SysClassDMI: filepath.Join(ctx.Chroot, "sys", "class", "dmi"),
SysClassNet: filepath.Join(ctx.Chroot, "sys", "class", "net"),
RunUdevData: filepath.Join(ctx.Chroot, "run", "udev", "data"),
}
}
func (p *Paths) NodeCPU(nodeID int, lpID int) string {
return filepath.Join(
p.SysDevicesSystemNode,
fmt.Sprintf("node%d", nodeID),
fmt.Sprintf("cpu%d", lpID),
)
}
func (p *Paths) NodeCPUCache(nodeID int, lpID int) string {
return filepath.Join(
p.NodeCPU(nodeID, lpID),
"cache",
)
}
func (p *Paths) NodeCPUCacheIndex(nodeID int, lpID int, cacheIndex int) string {
return filepath.Join(
p.NodeCPUCache(nodeID, lpID),
fmt.Sprintf("index%d", cacheIndex),
)
}

47
vendor/github.com/jaypipes/ghw/pkg/marshal/marshal.go generated vendored Normal file
View File

@@ -0,0 +1,47 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package marshal
import (
"encoding/json"
"github.com/ghodss/yaml"
"github.com/jaypipes/ghw/pkg/context"
)
// safeYAML returns a string after marshalling the supplied parameter into YAML
func SafeYAML(ctx *context.Context, p interface{}) string {
b, err := json.Marshal(p)
if err != nil {
ctx.Warn("error marshalling JSON: %s", err)
return ""
}
yb, err := yaml.JSONToYAML(b)
if err != nil {
ctx.Warn("error converting JSON to YAML: %s", err)
return ""
}
return string(yb)
}
// 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 {
var b []byte
var err error
if !indent {
b, err = json.Marshal(p)
} else {
b, err = json.MarshalIndent(&p, "", " ")
}
if err != nil {
ctx.Warn("error marshalling JSON: %s", err)
return ""
}
return string(b)
}

80
vendor/github.com/jaypipes/ghw/pkg/memory/memory.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package memory
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"
"github.com/jaypipes/ghw/pkg/util"
)
type Module struct {
Label string `json:"label"`
Location string `json:"location"`
SerialNumber string `json:"serial_number"`
SizeBytes int64 `json:"size_bytes"`
Vendor string `json:"vendor"`
}
type Info struct {
ctx *context.Context
TotalPhysicalBytes int64 `json:"total_physical_bytes"`
TotalUsableBytes int64 `json:"total_usable_bytes"`
// An array of sizes, in bytes, of memory pages supported by the host
SupportedPageSizes []uint64 `json:"supported_page_sizes"`
Modules []*Module `json:"modules"`
}
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
}
return info, nil
}
func (i *Info) String() string {
tpbs := util.UNKNOWN
if i.TotalPhysicalBytes > 0 {
tpb := i.TotalPhysicalBytes
unit, unitStr := unitutil.AmountString(tpb)
tpb = int64(math.Ceil(float64(i.TotalPhysicalBytes) / float64(unit)))
tpbs = fmt.Sprintf("%d%s", tpb, unitStr)
}
tubs := util.UNKNOWN
if i.TotalUsableBytes > 0 {
tub := i.TotalUsableBytes
unit, unitStr := unitutil.AmountString(tub)
tub = int64(math.Ceil(float64(i.TotalUsableBytes) / float64(unit)))
tubs = fmt.Sprintf("%d%s", tub, unitStr)
}
return fmt.Sprintf("memory (%s physical, %s usable)", tpbs, tubs)
}
// simple private struct used to encapsulate memory information in a top-level
// "memory" YAML/JSON map/object key
type memoryPrinter struct {
Info *Info `json:"memory"`
}
// 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})
}
// 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)
}

View File

@@ -0,0 +1,101 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package memory
import (
"fmt"
"strconv"
"strings"
"github.com/jaypipes/ghw/pkg/unitutil"
)
type CacheType int
const (
CACHE_TYPE_UNIFIED CacheType = iota
CACHE_TYPE_INSTRUCTION
CACHE_TYPE_DATA
)
var (
memoryCacheTypeString = map[CacheType]string{
CACHE_TYPE_UNIFIED: "Unified",
CACHE_TYPE_INSTRUCTION: "Instruction",
CACHE_TYPE_DATA: "Data",
}
)
func (a CacheType) String() string {
return memoryCacheTypeString[a]
}
// NOTE(jaypipes): since serialized output is as "official" as we're going to
// get, let's lowercase the string output when serializing, in order to
// "normalize" the expected serialized output
func (a CacheType) MarshalJSON() ([]byte, error) {
return []byte("\"" + strings.ToLower(a.String()) + "\""), nil
}
type SortByCacheLevelTypeFirstProcessor []*Cache
func (a SortByCacheLevelTypeFirstProcessor) Len() int { return len(a) }
func (a SortByCacheLevelTypeFirstProcessor) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortByCacheLevelTypeFirstProcessor) Less(i, j int) bool {
if a[i].Level < a[j].Level {
return true
} else if a[i].Level == a[j].Level {
if a[i].Type < a[j].Type {
return true
} else if a[i].Type == a[j].Type {
// NOTE(jaypipes): len(LogicalProcessors) is always >0 and is always
// sorted lowest LP ID to highest LP ID
return a[i].LogicalProcessors[0] < a[j].LogicalProcessors[0]
}
}
return false
}
type SortByLogicalProcessorId []uint32
func (a SortByLogicalProcessorId) Len() int { return len(a) }
func (a SortByLogicalProcessorId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortByLogicalProcessorId) Less(i, j int) bool { return a[i] < a[j] }
type Cache struct {
Level uint8 `json:"level"`
Type CacheType `json:"type"`
SizeBytes uint64 `json:"size_bytes"`
// The set of logical processors (hardware threads) that have access to the
// cache
LogicalProcessors []uint32 `json:"logical_processors"`
}
func (c *Cache) String() string {
sizeKb := c.SizeBytes / uint64(unitutil.KB)
typeStr := ""
if c.Type == CACHE_TYPE_INSTRUCTION {
typeStr = "i"
} else if c.Type == CACHE_TYPE_DATA {
typeStr = "d"
}
cacheIDStr := fmt.Sprintf("L%d%s", c.Level, typeStr)
processorMapStr := ""
if c.LogicalProcessors != nil {
lpStrings := make([]string, len(c.LogicalProcessors))
for x, lpid := range c.LogicalProcessors {
lpStrings[x] = strconv.Itoa(int(lpid))
}
processorMapStr = " shared with logical processors: " + strings.Join(lpStrings, ",")
}
return fmt.Sprintf(
"%s cache (%d KB)%s",
cacheIDStr,
sizeKb,
processorMapStr,
)
}

View File

@@ -0,0 +1,188 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package memory
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/jaypipes/ghw/pkg/context"
"github.com/jaypipes/ghw/pkg/linuxpath"
"github.com/jaypipes/ghw/pkg/unitutil"
)
func CachesForNode(ctx *context.Context, 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
// of subdirectories beginning with 'index' and ending in the cache's
// 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)
path := filepath.Join(
paths.SysDevicesSystemNode,
fmt.Sprintf("node%d", nodeID),
)
caches := make(map[string]*Cache)
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
for _, file := range files {
filename := file.Name()
if !strings.HasPrefix(filename, "cpu") {
continue
}
if filename == "cpumap" || filename == "cpulist" {
// There are two files in the node directory that start with 'cpu'
// but are not subdirectories ('cpulist' and 'cpumap'). Ignore
// these files.
continue
}
// Grab the logical processor ID by cutting the integer from the
// /sys/devices/system/node/nodeX/cpuX filename
cpuPath := filepath.Join(path, filename)
lpID, _ := strconv.Atoi(filename[3:])
// Inspect the caches for each logical processor. There will be a
// /sys/devices/system/node/nodeX/cpuX/cache directory containing a
// number of directories beginning with the prefix "index" followed by
// a number. The number indicates the level of the cache, which
// indicates the "distance" from the processor. Each of these
// directories contains information about the size of that level of
// cache and the processors mapped to it.
cachePath := filepath.Join(cpuPath, "cache")
if _, err = os.Stat(cachePath); errors.Is(err, os.ErrNotExist) {
continue
}
cacheDirFiles, err := ioutil.ReadDir(cachePath)
if err != nil {
return nil, err
}
for _, cacheDirFile := range cacheDirFiles {
cacheDirFileName := cacheDirFile.Name()
if !strings.HasPrefix(cacheDirFileName, "index") {
continue
}
cacheIndex, _ := strconv.Atoi(cacheDirFileName[5:])
// 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(paths, nodeID, lpID, cacheIndex)
cacheType := memoryCacheType(paths, nodeID, lpID, cacheIndex)
sharedCpuMap := memoryCacheSharedCPUMap(paths, nodeID, lpID, cacheIndex)
cacheKey := fmt.Sprintf("%d-%d-%s", level, cacheType, sharedCpuMap)
cache, exists := caches[cacheKey]
if !exists {
size := memoryCacheSize(paths, nodeID, lpID, level)
cache = &Cache{
Level: uint8(level),
Type: cacheType,
SizeBytes: uint64(size) * uint64(unitutil.KB),
LogicalProcessors: make([]uint32, 0),
}
caches[cacheKey] = cache
}
cache.LogicalProcessors = append(
cache.LogicalProcessors,
uint32(lpID),
)
}
}
cacheVals := make([]*Cache, len(caches))
x := 0
for _, c := range caches {
// ensure the cache's processor set is sorted by logical process ID
sort.Sort(SortByLogicalProcessorId(c.LogicalProcessors))
cacheVals[x] = c
x++
}
return cacheVals, nil
}
func memoryCacheLevel(paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) int {
levelPath := filepath.Join(
paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex),
"level",
)
levelContents, err := ioutil.ReadFile(levelPath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", 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 {
_, _ = fmt.Fprintf(os.Stderr, "Unable to parse int from %s\n", levelContents)
return -1
}
return level
}
func memoryCacheSize(paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) int {
sizePath := filepath.Join(
paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex),
"size",
)
sizeContents, err := ioutil.ReadFile(sizePath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", 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 {
_, _ = fmt.Fprintf(os.Stderr, "Unable to parse int from %s\n", sizeContents)
return -1
}
return size
}
func memoryCacheType(paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) CacheType {
typePath := filepath.Join(
paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex),
"type",
)
cacheTypeContents, err := ioutil.ReadFile(typePath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err)
return CACHE_TYPE_UNIFIED
}
switch string(cacheTypeContents[:len(cacheTypeContents)-1]) {
case "Data":
return CACHE_TYPE_DATA
case "Instruction":
return CACHE_TYPE_INSTRUCTION
default:
return CACHE_TYPE_UNIFIED
}
}
func memoryCacheSharedCPUMap(paths *linuxpath.Paths, nodeID int, lpID int, cacheIndex int) string {
scpuPath := filepath.Join(
paths.NodeCPUCacheIndex(nodeID, lpID, cacheIndex),
"shared_cpu_map",
)
sharedCpuMap, err := ioutil.ReadFile(scpuPath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err)
return ""
}
return string(sharedCpuMap[:len(sharedCpuMap)-1])
}

View File

@@ -0,0 +1,237 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package memory
import (
"bufio"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/jaypipes/ghw/pkg/linuxpath"
"github.com/jaypipes/ghw/pkg/unitutil"
"github.com/jaypipes/ghw/pkg/util"
)
const (
_WARN_CANNOT_DETERMINE_PHYSICAL_MEMORY = `
Could not determine total physical bytes of memory. This may
be due to the host being a virtual machine or container with no
/var/log/syslog file or /sys/devices/system/memory directory, or
the current user may not have necessary privileges to read the syslog.
We are falling back to setting the total physical amount of memory to
the total usable amount of memory
`
)
var (
// System log lines will look similar to the following:
// ... kernel: [0.000000] Memory: 24633272K/25155024K ...
_REGEX_SYSLOG_MEMLINE = regexp.MustCompile(`Memory:\s+\d+K\/(\d+)K`)
)
func (i *Info) load() error {
paths := linuxpath.New(i.ctx)
tub := memTotalUsableBytes(paths)
if tub < 1 {
return fmt.Errorf("Could not determine total usable bytes of memory")
}
i.TotalUsableBytes = tub
tpb := memTotalPhysicalBytes(paths)
i.TotalPhysicalBytes = tpb
if tpb < 1 {
i.ctx.Warn(_WARN_CANNOT_DETERMINE_PHYSICAL_MEMORY)
i.TotalPhysicalBytes = tub
}
i.SupportedPageSizes = memSupportedPageSizes(paths)
return nil
}
func memTotalPhysicalBytes(paths *linuxpath.Paths) (total int64) {
defer func() {
// fallback to the syslog file approach in case of error
if total < 0 {
total = memTotalPhysicalBytesFromSyslog(paths)
}
}()
// detect physical memory from /sys/devices/system/memory
dir := paths.SysDevicesSystemMemory
// get the memory block size in byte in hexadecimal notation
blockSize := filepath.Join(dir, "block_size_bytes")
d, err := ioutil.ReadFile(blockSize)
if err != nil {
return -1
}
blockSizeBytes, err := strconv.ParseUint(strings.TrimSpace(string(d)), 16, 64)
if err != nil {
return -1
}
// iterate over memory's block /sys/devices/system/memory/memory*,
// if the memory block state is 'online' we increment the total
// with the memory block size to determine the amount of physical
// memory available on this system
sysMemory, err := filepath.Glob(filepath.Join(dir, "memory*"))
if err != nil {
return -1
} else if sysMemory == nil {
return -1
}
for _, path := range sysMemory {
s, err := ioutil.ReadFile(filepath.Join(path, "state"))
if err != nil {
return -1
}
if strings.TrimSpace(string(s)) != "online" {
continue
}
total += int64(blockSizeBytes)
}
return total
}
func memTotalPhysicalBytesFromSyslog(paths *linuxpath.Paths) int64 {
// In Linux, the total physical memory can be determined by looking at the
// output of dmidecode, however dmidecode requires root privileges to run,
// so instead we examine the system logs for startup information containing
// total physical memory and cache the results of this.
findPhysicalKb := func(line string) int64 {
matches := _REGEX_SYSLOG_MEMLINE.FindStringSubmatch(line)
if len(matches) == 2 {
i, err := strconv.Atoi(matches[1])
if err != nil {
return -1
}
return int64(i * 1024)
}
return -1
}
// /var/log will contain a file called syslog and 0 or more files called
// syslog.$NUMBER or syslog.$NUMBER.gz containing system log records. We
// search each, stopping when we match a system log record line that
// contains physical memory information.
logDir := paths.VarLog
logFiles, err := ioutil.ReadDir(logDir)
if err != nil {
return -1
}
for _, file := range logFiles {
if strings.HasPrefix(file.Name(), "syslog") {
fullPath := filepath.Join(logDir, file.Name())
unzip := strings.HasSuffix(file.Name(), ".gz")
var r io.ReadCloser
r, err = os.Open(fullPath)
if err != nil {
return -1
}
defer util.SafeClose(r)
if unzip {
r, err = gzip.NewReader(r)
if err != nil {
return -1
}
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
size := findPhysicalKb(line)
if size > 0 {
return size
}
}
}
}
return -1
}
func memTotalUsableBytes(paths *linuxpath.Paths) int64 {
// In Linux, /proc/meminfo contains a set of memory-related amounts, with
// lines looking like the following:
//
// $ cat /proc/meminfo
// MemTotal: 24677596 kB
// MemFree: 21244356 kB
// MemAvailable: 22085432 kB
// ...
// HugePages_Total: 0
// HugePages_Free: 0
// HugePages_Rsvd: 0
// HugePages_Surp: 0
// ...
//
// It's worth noting that /proc/meminfo returns exact information, not
// "theoretical" information. For instance, on the above system, I have
// 24GB of RAM but MemTotal is indicating only around 23GB. This is because
// MemTotal contains the exact amount of *usable* memory after accounting
// for the kernel's resident memory size and a few reserved bits. For more
// information, see:
//
// https://www.kernel.org/doc/Documentation/filesystems/proc.txt
filePath := paths.ProcMeminfo
r, err := os.Open(filePath)
if err != nil {
return -1
}
defer util.SafeClose(r)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
key := strings.Trim(parts[0], ": \t")
if key != "MemTotal" {
continue
}
value, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return -1
}
inKb := (len(parts) == 3 && strings.TrimSpace(parts[2]) == "kB")
if inKb {
value = value * int(unitutil.KB)
}
return int64(value)
}
return -1
}
func memSupportedPageSizes(paths *linuxpath.Paths) []uint64 {
// In Linux, /sys/kernel/mm/hugepages contains a directory per page size
// supported by the kernel. The directory name corresponds to the pattern
// 'hugepages-{pagesize}kb'
dir := paths.SysKernelMMHugepages
out := make([]uint64, 0)
files, err := ioutil.ReadDir(dir)
if err != nil {
return out
}
for _, file := range files {
parts := strings.Split(file.Name(), "-")
sizeStr := parts[1]
// Cut off the 'kb'
sizeStr = sizeStr[0 : len(sizeStr)-2]
size, err := strconv.Atoi(sizeStr)
if err != nil {
return out
}
out = append(out, uint64(size*int(unitutil.KB)))
}
return out
}

View File

@@ -0,0 +1,17 @@
// +build !linux,!windows
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package memory
import (
"runtime"
"github.com/pkg/errors"
)
func (i *Info) load() error {
return errors.New("mem.Info.load not implemented on " + runtime.GOOS)
}

View File

@@ -0,0 +1,72 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package memory
import (
"github.com/StackExchange/wmi"
"github.com/jaypipes/ghw/pkg/unitutil"
)
const wqlOperatingSystem = "SELECT TotalVisibleMemorySize FROM Win32_OperatingSystem"
type win32OperatingSystem struct {
TotalVisibleMemorySize *uint64
}
const wqlPhysicalMemory = "SELECT BankLabel, Capacity, DataWidth, Description, DeviceLocator, Manufacturer, Model, Name, PartNumber, PositionInRow, SerialNumber, Speed, Tag, TotalWidth FROM Win32_PhysicalMemory"
type win32PhysicalMemory struct {
BankLabel *string
Capacity *uint64
DataWidth *uint16
Description *string
DeviceLocator *string
Manufacturer *string
Model *string
Name *string
PartNumber *string
PositionInRow *uint32
SerialNumber *string
Speed *uint32
Tag *string
TotalWidth *uint16
}
func (i *Info) load() error {
// Getting info from WMI
var win32OSDescriptions []win32OperatingSystem
if err := wmi.Query(wqlOperatingSystem, &win32OSDescriptions); err != nil {
return err
}
var win32MemDescriptions []win32PhysicalMemory
if err := wmi.Query(wqlPhysicalMemory, &win32MemDescriptions); err != nil {
return err
}
// We calculate total physical memory size by summing the DIMM sizes
var totalPhysicalBytes uint64
i.Modules = make([]*Module, 0, len(win32MemDescriptions))
for _, description := range win32MemDescriptions {
totalPhysicalBytes += *description.Capacity
i.Modules = append(i.Modules, &Module{
Label: *description.BankLabel,
Location: *description.DeviceLocator,
SerialNumber: *description.SerialNumber,
SizeBytes: int64(*description.Capacity),
Vendor: *description.Manufacturer,
})
}
var totalUsableBytes uint64
for _, description := range win32OSDescriptions {
// TotalVisibleMemorySize is the amount of memory available for us by
// the operating system **in Kilobytes**
totalUsableBytes += *description.TotalVisibleMemorySize * uint64(unitutil.KB)
}
i.TotalUsableBytes = int64(totalUsableBytes)
i.TotalPhysicalBytes = int64(totalPhysicalBytes)
return nil
}

227
vendor/github.com/jaypipes/ghw/pkg/option/option.go generated vendored Normal file
View File

@@ -0,0 +1,227 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package option
import (
"io"
"io/ioutil"
"log"
"os"
)
const (
defaultChroot = "/"
envKeyChroot = "GHW_CHROOT"
envKeyDisableWarnings = "GHW_DISABLE_WARNINGS"
envKeyDisableTools = "GHW_DISABLE_TOOLS"
envKeySnapshotPath = "GHW_SNAPSHOT_PATH"
envKeySnapshotRoot = "GHW_SNAPSHOT_ROOT"
envKeySnapshotExclusive = "GHW_SNAPSHOT_EXCLUSIVE"
envKeySnapshotPreserve = "GHW_SNAPSHOT_PRESERVE"
)
// Alerter emits warnings about undesirable but recoverable errors.
// We use a subset of a logger interface only to emit warnings, and
// `Warninger` sounded ugly.
type Alerter interface {
Printf(format string, v ...interface{})
}
var (
NullAlerter = log.New(ioutil.Discard, "", 0)
)
// EnvOrDefaultAlerter returns the default instance ghw will use to emit
// its warnings. ghw will emit warnings to stderr by default unless the
// environs variable GHW_DISABLE_WARNINGS is specified; in the latter case
// all warning will be suppressed.
func EnvOrDefaultAlerter() Alerter {
var dest io.Writer
if _, exists := os.LookupEnv(envKeyDisableWarnings); exists {
dest = ioutil.Discard
} else {
// default
dest = os.Stderr
}
return log.New(dest, "", 0)
}
// EnvOrDefaultChroot returns the value of the GHW_CHROOT environs variable or
// the default value of "/" if not set
func EnvOrDefaultChroot() string {
// Grab options from the environs by default
if val, exists := os.LookupEnv(envKeyChroot); exists {
return val
}
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 {
if _, exists := os.LookupEnv(envKeyDisableTools); exists {
return false
}
return true
}
// Option 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 {
// 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
// vairable 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
// Alerter contains the target for ghw warnings
Alerter Alerter
// EnableTools 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
}
// 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 WithChroot(dir string) *Option {
return &Option{Chroot: &dir}
}
// WithSnapshot sets snapshot-processing options for a ghw run
func WithSnapshot(opts SnapshotOptions) *Option {
return &Option{
Snapshot: &opts,
}
}
// WithAlerter sets alerting options for ghw
func WithAlerter(alerter Alerter) *Option {
return &Option{
Alerter: alerter,
}
}
// WithNullAlerter sets No-op alerting options for ghw
func WithNullAlerter() *Option {
return &Option{
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_}
}
// 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.
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
}
}
// 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
}
return merged
}

View File

@@ -0,0 +1,55 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package address
import (
"regexp"
"strings"
)
var (
regexAddress *regexp.Regexp = regexp.MustCompile(
`^(([0-9a-f]{0,4}):)?([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]{1})$`,
)
)
type Address struct {
Domain string
Bus string
Slot string
Function string
}
// String() returns the canonical [D]BSF representation of this Address
func (addr *Address) String() string {
return addr.Domain + ":" + addr.Bus + ":" + addr.Slot + "." + addr.Function
}
// Given a string address, returns a complete Address struct, filled in with
// domain, bus, slot and function components. The address string may either
// be in $BUS:$SLOT.$FUNCTION (BSF) format or it can be a full PCI address
// that includes the 4-digit $DOMAIN information as well:
// $DOMAIN:$BUS:$SLOT.$FUNCTION.
//
// Returns "" if the address string wasn't a valid PCI address.
func FromString(address string) *Address {
addrLowered := strings.ToLower(address)
matches := regexAddress.FindStringSubmatch(addrLowered)
if len(matches) == 6 {
dom := "0000"
if matches[1] != "" {
dom = matches[2]
}
return &Address{
Domain: dom,
Bus: matches[3],
Slot: matches[4],
Function: matches[5],
}
}
return nil
}

View File

@@ -0,0 +1,263 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"io/ioutil"
"os"
"path/filepath"
)
// Attempting to tar up pseudofiles like /proc/cpuinfo is an exercise in
// futility. Notably, the pseudofiles, when read by syscalls, do not return the
// number of bytes read. This causes the tar writer to write zero-length files.
//
// Instead, it is necessary to build a directory structure in a tmpdir and
// create actual files with copies of the pseudofile contents
// CloneTreeInto copies all the pseudofiles that ghw will consume into the root
// `scratchDir`, preserving the hieratchy.
func CloneTreeInto(scratchDir string) error {
var err error
var createPaths = []string{
"sys/block",
}
for _, path := range createPaths {
if err = os.MkdirAll(filepath.Join(scratchDir, path), os.ModePerm); err != nil {
return err
}
}
if err = createBlockDevices(scratchDir); err != nil {
return err
}
fileSpecs := ExpectedCloneContent()
return CopyFilesInto(fileSpecs, scratchDir, nil)
}
// ExpectedCloneContent return a slice of glob patterns which represent the pseudofiles
// ghw cares about.
// The intended usage of this function is to validate a clone tree, checking that the
// content matches the expectations.
// Beware: the content is host-specific, because the content pertaining some subsystems,
// most notably PCI, is host-specific and unpredictable.
func ExpectedCloneContent() []string {
fileSpecs := ExpectedCloneStaticContent()
fileSpecs = append(fileSpecs, ExpectedCloneNetContent()...)
fileSpecs = append(fileSpecs, ExpectedClonePCIContent()...)
fileSpecs = append(fileSpecs, ExpectedCloneGPUContent()...)
return fileSpecs
}
// ExpectedCloneStaticContent return a slice of glob patterns which represent the pseudofiles
// ghw cares about, and which are independent from host specific topology or configuration,
// thus are safely represented by a static slice - e.g. they don't need to be discovered at runtime.
func ExpectedCloneStaticContent() []string {
return []string{
"/etc/mtab",
"/proc/cpuinfo",
"/proc/meminfo",
"/sys/devices/system/cpu/cpu*/cache/index*/*",
"/sys/devices/system/cpu/cpu*/topology/*",
"/sys/devices/system/memory/block_size_bytes",
"/sys/devices/system/memory/memory*/online",
"/sys/devices/system/memory/memory*/state",
"/sys/devices/system/node/has_*",
"/sys/devices/system/node/online",
"/sys/devices/system/node/possible",
"/sys/devices/system/node/node*/cpu*",
"/sys/devices/system/node/node*/distance",
}
}
// ValidateClonedTree checks the content of a cloned tree, whose root is `clonedDir`,
// against a slice of glob specs which must be included in the cloned tree.
// Is not wrong, and this functions doesn't enforce this, that the cloned tree includes
// more files than the necessary; ghw will just ignore the files it doesn't care about.
// Returns a slice of glob patters expected (given) but not found in the cloned tree,
// and the error during the validation (if any).
func ValidateClonedTree(fileSpecs []string, clonedDir string) ([]string, error) {
missing := []string{}
for _, fileSpec := range fileSpecs {
matches, err := filepath.Glob(filepath.Join(clonedDir, fileSpec))
if err != nil {
return missing, err
}
if len(matches) == 0 {
missing = append(missing, fileSpec)
}
}
return missing, nil
}
func copyPseudoFile(path, targetPath string) error {
buf, err := ioutil.ReadFile(path)
if err != nil {
return err
}
trace("creating %s\n", targetPath)
f, err := os.Create(targetPath)
if err != nil {
return err
}
if _, err = f.Write(buf); err != nil {
return err
}
f.Close()
return nil
}
// CopyFileOptions allows to finetune the behaviour of the CopyFilesInto function
type CopyFileOptions struct {
// IsSymlinkFn allows to control the behaviour when handling a symlink.
// If this hook returns true, the source file is treated as symlink: the cloned
// tree will thus contain a symlink, with its path adjusted to match the relative
// path inside the cloned tree. If return false, the symlink will be deferred.
// The easiest use case of this hook is if you want to avoid symlinks in your cloned
// tree (having duplicated content). In this case you can just add a function
// which always return false.
IsSymlinkFn func(path string, info os.FileInfo) bool
}
// CopyFilesInto copies all the given glob files specs in the given `destDir` directory,
// preserving the directory structure. This means you can provide a deeply nested filespec
// like
// - /some/deeply/nested/file*
// and you DO NOT need to build the tree incrementally like
// - /some/
// - /some/deeply/
// ...
// all glob patterns supported in `filepath.Glob` are supported.
func CopyFilesInto(fileSpecs []string, destDir string, opts *CopyFileOptions) error {
if opts == nil {
opts = &CopyFileOptions{
IsSymlinkFn: isSymlink,
}
}
for _, fileSpec := range fileSpecs {
trace("copying spec: %q\n", fileSpec)
matches, err := filepath.Glob(fileSpec)
if err != nil {
return err
}
if err := copyFileTreeInto(matches, destDir, opts); err != nil {
return err
}
}
return nil
}
func copyFileTreeInto(paths []string, destDir string, opts *CopyFileOptions) error {
for _, path := range paths {
trace(" copying path: %q\n", path)
baseDir := filepath.Dir(path)
if err := os.MkdirAll(filepath.Join(destDir, baseDir), os.ModePerm); err != nil {
return err
}
fi, err := os.Lstat(path)
if err != nil {
return err
}
// directories must be listed explicitely and created separately.
// In the future we may want to expose this decision as hook point in
// CopyFileOptions, when clear use cases emerge.
if fi.IsDir() {
trace("expanded glob path %q is a directory - skipped", path)
continue
}
if opts.IsSymlinkFn(path, fi) {
trace(" copying link: %q\n", path)
if err := copyLink(path, filepath.Join(destDir, path)); err != nil {
return err
}
} else {
trace(" copying file: %q\n", path)
if err := copyPseudoFile(path, filepath.Join(destDir, path)); err != nil {
return err
}
}
}
return nil
}
func isSymlink(path string, fi os.FileInfo) bool {
return fi.Mode()&os.ModeSymlink != 0
}
func copyLink(path, targetPath string) error {
target, err := os.Readlink(path)
if err != nil {
return err
}
if err := os.Symlink(target, targetPath); err != nil {
return err
}
return nil
}
type filterFunc func(string) bool
// cloneContentByClass copies all the content related to a given device class
// (devClass), possibly filtering out devices whose name does NOT pass a
// filter (filterName). Each entry in `/sys/class/$CLASS` is actually a
// symbolic link. We can filter out entries depending on the link target.
// Each filter is a simple function which takes the entry name or the link
// target and must return true if the entry should be collected, false
// otherwise. Last, explicitely collect a list of attributes for each entry,
// given as list of glob patterns as `subEntries`.
// Return the final list of glob patterns to be collected.
func cloneContentByClass(devClass string, subEntries []string, filterName filterFunc, filterLink filterFunc) []string {
var fileSpecs []string
// warning: don't use the context package here, this means not even the linuxpath package.
// TODO(fromani) remove the path duplication
sysClass := filepath.Join("sys", "class", devClass)
entries, err := ioutil.ReadDir(sysClass)
if err != nil {
// we should not import context, hence we can't Warn()
return fileSpecs
}
for _, entry := range entries {
devName := entry.Name()
if !filterName(devName) {
continue
}
devPath := filepath.Join(sysClass, devName)
dest, err := os.Readlink(devPath)
if err != nil {
continue
}
if !filterLink(dest) {
continue
}
// so, first copy the symlink itself
fileSpecs = append(fileSpecs, devPath)
// now we have to clone the content of the actual entry
// related (and found into a subdir of) the backing hardware
// device
devData := filepath.Clean(filepath.Join(sysClass, dest))
for _, subEntry := range subEntries {
fileSpecs = append(fileSpecs, filepath.Join(devData, subEntry))
}
}
return fileSpecs
}
// filterNone allows all content, filtering out none of it
func filterNone(_ string) bool {
return true
}

View File

@@ -0,0 +1,221 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
func createBlockDevices(buildDir string) error {
// Grab all the block device pseudo-directories from /sys/block symlinks
// (excluding loopback devices) and inject them into our build filesystem
// with all but the circular symlink'd subsystem directories
devLinks, err := ioutil.ReadDir("/sys/block")
if err != nil {
return err
}
for _, devLink := range devLinks {
dname := devLink.Name()
if strings.HasPrefix(dname, "loop") {
continue
}
devPath := filepath.Join("/sys/block", dname)
trace("processing block device %q\n", devPath)
// from the sysfs layout, we know this is always a symlink
linkContentPath, err := os.Readlink(devPath)
if err != nil {
return err
}
trace("link target for block device %q is %q\n", devPath, linkContentPath)
// Create a symlink in our build filesystem that is a directory
// pointing to the actual device bus path where the block device's
// information directory resides
linkPath := filepath.Join(buildDir, "sys/block", dname)
linkTargetPath := filepath.Join(
buildDir,
"sys/block",
strings.TrimPrefix(linkContentPath, string(os.PathSeparator)),
)
trace("creating device directory %s\n", linkTargetPath)
if err = os.MkdirAll(linkTargetPath, os.ModePerm); err != nil {
return err
}
trace("linking device directory %s to %s\n", linkPath, linkContentPath)
// Make sure the link target is a relative path!
// if we use absolute path, the link target will be an absolute path starting
// with buildDir, hence the snapshot will contain broken link.
// Otherwise, the unpack directory will never have the same prefix of buildDir!
if err = os.Symlink(linkContentPath, linkPath); err != nil {
return err
}
// Now read the source block device directory and populate the
// newly-created target link in the build directory with the
// appropriate block device pseudofiles
srcDeviceDir := filepath.Join(
"/sys/block",
strings.TrimPrefix(linkContentPath, string(os.PathSeparator)),
)
trace("creating device directory %q from %q\n", linkTargetPath, srcDeviceDir)
if err = createBlockDeviceDir(linkTargetPath, srcDeviceDir); err != nil {
return err
}
}
return nil
}
func createBlockDeviceDir(buildDeviceDir string, srcDeviceDir string) error {
// Populate the supplied directory (in our build filesystem) with all the
// appropriate information pseudofile contents for the block device.
devName := filepath.Base(srcDeviceDir)
devFiles, err := ioutil.ReadDir(srcDeviceDir)
if err != nil {
return err
}
for _, f := range devFiles {
fname := f.Name()
fp := filepath.Join(srcDeviceDir, fname)
fi, err := os.Lstat(fp)
if err != nil {
return err
}
if fi.Mode()&os.ModeSymlink != 0 {
// Ignore any symlinks in the deviceDir since they simply point to
// either self-referential links or information we aren't
// interested in like "subsystem"
continue
} else if fi.IsDir() {
if strings.HasPrefix(fname, devName) {
// We're interested in are the directories that begin with the
// block device name. These are directories with information
// about the partitions on the device
buildPartitionDir := filepath.Join(
buildDeviceDir, fname,
)
srcPartitionDir := filepath.Join(
srcDeviceDir, fname,
)
trace("creating partition directory %s\n", buildPartitionDir)
err = os.MkdirAll(buildPartitionDir, os.ModePerm)
if err != nil {
return err
}
err = createPartitionDir(buildPartitionDir, srcPartitionDir)
if err != nil {
return err
}
}
} else if fi.Mode().IsRegular() {
// Regular files in the block device directory are both regular and
// pseudofiles containing information such as the size (in sectors)
// and whether the device is read-only
buf, err := ioutil.ReadFile(fp)
if err != nil {
if errors.Is(err, os.ErrPermission) {
// example: /sys/devices/virtual/block/zram0/compact is 0400
trace("permission denied reading %q - skipped\n", fp)
continue
}
return err
}
targetPath := filepath.Join(buildDeviceDir, fname)
trace("creating %s\n", targetPath)
f, err := os.Create(targetPath)
if err != nil {
return err
}
if _, err = f.Write(buf); err != nil {
return err
}
f.Close()
}
}
// There is a special file $DEVICE_DIR/queue/rotational that, for some hard
// drives, contains a 1 or 0 indicating whether the device is a spinning
// disk or not
srcQueueDir := filepath.Join(
srcDeviceDir,
"queue",
)
buildQueueDir := filepath.Join(
buildDeviceDir,
"queue",
)
err = os.MkdirAll(buildQueueDir, os.ModePerm)
if err != nil {
return err
}
fp := filepath.Join(srcQueueDir, "rotational")
buf, err := ioutil.ReadFile(fp)
if err != nil {
return err
}
targetPath := filepath.Join(buildQueueDir, "rotational")
trace("creating %s\n", targetPath)
f, err := os.Create(targetPath)
if err != nil {
return err
}
if _, err = f.Write(buf); err != nil {
return err
}
f.Close()
return nil
}
func createPartitionDir(buildPartitionDir string, srcPartitionDir string) error {
// Populate the supplied directory (in our build filesystem) with all the
// appropriate information pseudofile contents for the partition.
partFiles, err := ioutil.ReadDir(srcPartitionDir)
if err != nil {
return err
}
for _, f := range partFiles {
fname := f.Name()
fp := filepath.Join(srcPartitionDir, fname)
fi, err := os.Lstat(fp)
if err != nil {
return err
}
if fi.Mode()&os.ModeSymlink != 0 {
// Ignore any symlinks in the partition directory since they simply
// point to information we aren't interested in like "subsystem"
continue
} else if fi.IsDir() {
// The subdirectories in the partition directory are not
// interesting for us. They have information about power events and
// traces
continue
} else if fi.Mode().IsRegular() {
// Regular files in the block device directory are both regular and
// pseudofiles containing information such as the size (in sectors)
// and whether the device is read-only
buf, err := ioutil.ReadFile(fp)
if err != nil {
return err
}
targetPath := filepath.Join(buildPartitionDir, fname)
trace("creating %s\n", targetPath)
f, err := os.Create(targetPath)
if err != nil {
return err
}
if _, err = f.Write(buf); err != nil {
return err
}
f.Close()
}
}
return nil
}

View File

@@ -0,0 +1,33 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"strings"
)
// ExpectedCloneGPUContent returns a slice of strings pertaining to the GPU devices ghw
// cares about. We cannot use a static list because we want to grab only the first cardX data
// (see comment in pkg/gpu/gpu_linux.go)
// Additionally, we want to make sure to clone the backing device data.
func ExpectedCloneGPUContent() []string {
cardEntries := []string{
"device",
}
filterName := func(cardName string) bool {
if !strings.HasPrefix(cardName, "card") {
return false
}
if strings.ContainsRune(cardName, '-') {
return false
}
return true
}
return cloneContentByClass("drm", cardEntries, filterName, filterNone)
}

View File

@@ -0,0 +1,31 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"strings"
)
// ExpectedCloneNetContent returns a slice of strings pertaning to the network interfaces ghw
// cares about. We cannot use a static list because we want to filter away the virtual devices,
// which ghw doesn't concern itself about. So we need to do some runtime discovery.
// Additionally, we want to make sure to clone the backing device data.
func ExpectedCloneNetContent() []string {
ifaceEntries := []string{
"addr_assign_type",
// intentionally avoid to clone "address" to avoid to leak any host-idenfifiable data.
}
filterLink := func(linkDest string) bool {
if strings.Contains(linkDest, "devices/virtual/net") {
return false
}
return true
}
return cloneContentByClass("net", ifaceEntries, filterNone, filterLink)
}

View File

@@ -0,0 +1,148 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
pciaddr "github.com/jaypipes/ghw/pkg/pci/address"
)
const (
// root directory: entry point to start scanning the PCI forest
// warning: don't use the context package here, this means not even the linuxpath package.
// TODO(fromani) remove the path duplication
sysBusPCIDir = "/sys/bus/pci/devices"
)
// ExpectedClonePCIContent return a slice of glob patterns which represent the pseudofiles
// ghw cares about, pertaining to PCI devices only.
// Beware: the content is host-specific, because the PCI topology is host-dependent and unpredictable.
func ExpectedClonePCIContent() []string {
var fileSpecs []string
pciRoots := []string{
sysBusPCIDir,
}
for {
if len(pciRoots) == 0 {
break
}
pciRoot := pciRoots[0]
pciRoots = pciRoots[1:]
specs, roots := scanPCIDeviceRoot(pciRoot)
pciRoots = append(pciRoots, roots...)
fileSpecs = append(fileSpecs, specs...)
}
return fileSpecs
}
// scanPCIDeviceRoot reports a slice of glob patterns which represent the pseudofiles
// ghw cares about pertaining to all the PCI devices connected to the bus connected from the
// given root; usually (but not always) a CPU packages has 1+ PCI(e) roots, forming the first
// level; more PCI bridges are (usually) attached to this level, creating deep nested trees.
// hence we need to scan all possible roots, to make sure not to miss important devices.
//
// note about notifying errors. This function and its helper functions do use trace() everywhere
// to report recoverable errors, even though it would have been appropriate to use Warn().
// This is unfortunate, and again a byproduct of the fact we cannot use context.Context to avoid
// circular dependencies.
// TODO(fromani): switch to Warn() as soon as we figure out how to break this circular dep.
func scanPCIDeviceRoot(root string) (fileSpecs []string, pciRoots []string) {
trace("scanning PCI device root %q\n", root)
perDevEntries := []string{
"class",
"device",
"irq",
"local_cpulist",
"modalias",
"numa_node",
"revision",
"vendor",
}
entries, err := ioutil.ReadDir(root)
if err != nil {
return []string{}, []string{}
}
for _, entry := range entries {
entryName := entry.Name()
if addr := pciaddr.FromString(entryName); addr == nil {
// doesn't look like a entry we care about
// This is by far and large the most likely path
// hence we should NOT trace/warn here.
continue
}
entryPath := filepath.Join(root, entryName)
pciEntry, err := findPCIEntryFromPath(root, entryName)
if err != nil {
trace("error scanning %q: %v", entryName, err)
continue
}
trace("PCI entry is %q\n", pciEntry)
fileSpecs = append(fileSpecs, entryPath)
for _, perNetEntry := range perDevEntries {
fileSpecs = append(fileSpecs, filepath.Join(pciEntry, perNetEntry))
}
if isPCIBridge(entryPath) {
trace("adding new PCI root %q\n", entryName)
pciRoots = append(pciRoots, pciEntry)
}
}
return fileSpecs, pciRoots
}
func findPCIEntryFromPath(root, entryName string) (string, error) {
entryPath := filepath.Join(root, entryName)
fi, err := os.Lstat(entryPath)
if err != nil {
return "", fmt.Errorf("stat(%s) failed: %v\n", entryPath, err)
}
if fi.Mode()&os.ModeSymlink == 0 {
// regular file, nothing to resolve
return entryPath, nil
}
// resolve symlink
target, err := os.Readlink(entryPath)
trace("entry %q is symlink resolved to %q\n", entryPath, target)
if err != nil {
return "", fmt.Errorf("readlink(%s) failed: %v - skipped\n", entryPath, err)
}
return filepath.Clean(filepath.Join(root, target)), nil
}
func isPCIBridge(entryPath string) bool {
subNodes, err := ioutil.ReadDir(entryPath)
if err != nil {
// this is so unlikely we don't even return error. But we trace just in case.
trace("error scanning device entry path %q: %v", entryPath, err)
return false
}
for _, subNode := range subNodes {
if !subNode.IsDir() {
continue
}
if addr := pciaddr.FromString(subNode.Name()); addr != nil {
// we got an entry in the directory pertaining to this device
// which is a directory itself and it is named like a PCI address.
// Hence we infer the device we are considering is a PCI bridge of sorts.
// This is is indeed a bit brutal, but the only possible alternative
// (besides blindly copying everything in /sys/bus/pci/devices) is
// to detect the type of the device and pick only the bridges.
// This approach duplicates the logic within the `pci` subkpg
// - or forces us into awkward dep cycles, and has poorer forward
// compatibility.
return true
}
}
return false
}

112
vendor/github.com/jaypipes/ghw/pkg/snapshot/pack.go generated vendored Normal file
View File

@@ -0,0 +1,112 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// PackFrom creates the snapshot named `snapshotName` from the
// directory tree whose root is `sourceRoot`.
func PackFrom(snapshotName, sourceRoot string) error {
f, err := OpenDestination(snapshotName)
if err != nil {
return err
}
defer f.Close()
return PackWithWriter(f, sourceRoot)
}
// OpenDestination opens the `snapshotName` file for writing, bailing out
// if the file seems to exist and have existing content already.
// This is done to avoid accidental overwrites.
func OpenDestination(snapshotName string) (*os.File, error) {
var f *os.File
var err error
if _, err = os.Stat(snapshotName); errors.Is(err, os.ErrNotExist) {
if f, err = os.Create(snapshotName); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
f, err := os.OpenFile(snapshotName, os.O_WRONLY, 0600)
if err != nil {
return nil, err
}
fs, err := f.Stat()
if err != nil {
return nil, err
}
if fs.Size() > 0 {
return nil, fmt.Errorf("File %s already exists and is of size >0", snapshotName)
}
}
return f, nil
}
// PakcWithWriter creates a snapshot sending all the binary data to the
// given `fw` writer. The snapshot is made from the directory tree whose
// root is `sourceRoot`.
func PackWithWriter(fw io.Writer, sourceRoot string) error {
gzw := gzip.NewWriter(fw)
defer gzw.Close()
tw := tar.NewWriter(gzw)
defer tw.Close()
return createSnapshot(tw, sourceRoot)
}
func createSnapshot(tw *tar.Writer, buildDir string) error {
return filepath.Walk(buildDir, func(path string, fi os.FileInfo, err error) error {
if path == buildDir {
return nil
}
var link string
if fi.Mode()&os.ModeSymlink != 0 {
trace("processing symlink %s\n", path)
link, err = os.Readlink(path)
if err != nil {
return err
}
}
hdr, err := tar.FileInfoHeader(fi, link)
if err != nil {
return err
}
hdr.Name = strings.TrimPrefix(strings.TrimPrefix(path, buildDir), string(os.PathSeparator))
if err = tw.WriteHeader(hdr); err != nil {
return err
}
switch hdr.Typeflag {
case tar.TypeReg, tar.TypeRegA:
f, err := os.Open(path)
if err != nil {
return err
}
if _, err = io.Copy(tw, f); err != nil {
return err
}
f.Close()
}
return nil
})
}

Binary file not shown.

19
vendor/github.com/jaypipes/ghw/pkg/snapshot/trace.go generated vendored Normal file
View File

@@ -0,0 +1,19 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
var trace func(msg string, args ...interface{})
func init() {
trace = func(msg string, args ...interface{}) {
return
}
}
func SetTraceFunction(fn func(msg string, args ...interface{})) {
trace = fn
}

129
vendor/github.com/jaypipes/ghw/pkg/snapshot/unpack.go generated vendored Normal file
View File

@@ -0,0 +1,129 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package snapshot
import (
"archive/tar"
"compress/gzip"
"io"
"io/ioutil"
"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.
func Unpack(snapshotName string) (string, error) {
targetRoot, err := ioutil.TempDir("", TargetRoot)
if err != nil {
return "", err
}
_, err = UnpackInto(snapshotName, targetRoot, 0)
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
}
snap, err := os.Open(snapshotName)
if err != nil {
return false, err
}
defer snap.Close()
return true, Untar(targetRoot, snap)
}
// Untar extracts data from the given reader (providing data in tar.gz format) and unpacks it in the given directory.
func Untar(root string, r io.Reader) error {
var err error
gzr, err := gzip.NewReader(r)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
// we are done
return nil
}
if err != nil {
// bail out
return err
}
if header == nil {
// TODO: how come?
continue
}
target := filepath.Join(root, header.Name)
mode := os.FileMode(header.Mode)
switch header.Typeflag {
case tar.TypeDir:
err = os.MkdirAll(target, mode)
if err != nil {
return err
}
case tar.TypeReg:
dst, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, mode)
if err != nil {
return err
}
_, err = io.Copy(dst, tr)
if err != nil {
return err
}
dst.Close()
case tar.TypeSymlink:
err = os.Symlink(header.Linkname, target)
if err != nil {
return err
}
}
}
}
func isEmptyDir(name string) bool {
entries, err := ioutil.ReadDir(name)
if err != nil {
return false
}
return len(entries) == 0
}

128
vendor/github.com/jaypipes/ghw/pkg/topology/topology.go generated vendored Normal file
View File

@@ -0,0 +1,128 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package topology
import (
"fmt"
"sort"
"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"
"github.com/jaypipes/ghw/pkg/option"
)
// Architecture describes the overall hardware architecture. It can be either
// Symmetric Multi-Processor (SMP) or Non-Uniform Memory Access (NUMA)
type Architecture int
const (
// SMP is a Symmetric Multi-Processor system
ARCHITECTURE_SMP Architecture = iota
// NUMA is a Non-Uniform Memory Access system
ARCHITECTURE_NUMA
)
var (
architectureString = map[Architecture]string{
ARCHITECTURE_SMP: "SMP",
ARCHITECTURE_NUMA: "NUMA",
}
)
func (a Architecture) String() string {
return architectureString[a]
}
// NOTE(jaypipes): since serialized output is as "official" as we're going to
// get, let's lowercase the string output when serializing, in order to
// "normalize" the expected serialized output
func (a Architecture) MarshalJSON() ([]byte, error) {
return []byte("\"" + strings.ToLower(a.String()) + "\""), nil
}
// Node is an abstract construct representing a collection of processors and
// various levels of memory cache that those processors share. In a NUMA
// architecture, there are multiple NUMA nodes, abstracted here as multiple
// Node structs. In an SMP architecture, a single Node will be available in the
// Info struct and this single struct can be used to describe the levels of
// memory caching available to the single physical processor package's physical
// processor cores
type Node struct {
ID int `json:"id"`
Cores []*cpu.ProcessorCore `json:"cores"`
Caches []*memory.Cache `json:"caches"`
Distances []int `json:"distances"`
}
func (n *Node) String() string {
return fmt.Sprintf(
"node #%d (%d cores)",
n.ID,
len(n.Cores),
)
}
// 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) {
return NewWithContext(context.New(opts...))
}
// NewWithContext returns a pointer to an Info struct that contains information about
// the NUMA topology on the host system. Use this function when you want to consume
// the topology package from another package (e.g. pci, gpu)
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
}
for _, node := range info.Nodes {
sort.Sort(memory.SortByCacheLevelTypeFirstProcessor(node.Caches))
}
return info, nil
}
func (i *Info) String() string {
archStr := "SMP"
if i.Architecture == ARCHITECTURE_NUMA {
archStr = "NUMA"
}
res := fmt.Sprintf(
"topology %s (%d nodes)",
archStr,
len(i.Nodes),
)
return res
}
// simple private struct used to encapsulate topology information in a
// top-level "topology" YAML/JSON map/object key
type topologyPrinter struct {
Info *Info `json:"topology"`
}
// 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})
}
// 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)
}

View File

@@ -0,0 +1,100 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package topology
import (
"fmt"
"io/ioutil"
"path/filepath"
"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"
)
func (i *Info) load() error {
i.Nodes = topologyNodes(i.ctx)
if len(i.Nodes) == 1 {
i.Architecture = ARCHITECTURE_SMP
} else {
i.Architecture = ARCHITECTURE_NUMA
}
return nil
}
func topologyNodes(ctx *context.Context) []*Node {
paths := linuxpath.New(ctx)
nodes := make([]*Node, 0)
files, err := ioutil.ReadDir(paths.SysDevicesSystemNode)
if err != nil {
ctx.Warn("failed to determine nodes: %s\n", err)
return nodes
}
for _, file := range files {
filename := file.Name()
if !strings.HasPrefix(filename, "node") {
continue
}
node := &Node{}
nodeID, err := strconv.Atoi(filename[4:])
if err != nil {
ctx.Warn("failed to determine node ID: %s\n", err)
return nodes
}
node.ID = nodeID
cores, err := cpu.CoresForNode(ctx, nodeID)
if err != nil {
ctx.Warn("failed to determine cores for node: %s\n", err)
return nodes
}
node.Cores = cores
caches, err := memory.CachesForNode(ctx, nodeID)
if err != nil {
ctx.Warn("failed to determine caches for node: %s\n", err)
return nodes
}
node.Caches = caches
distances, err := distancesForNode(ctx, nodeID)
if err != nil {
ctx.Warn("failed to determine node distances for node: %s\n", err)
return nodes
}
node.Distances = distances
nodes = append(nodes, node)
}
return nodes
}
func distancesForNode(ctx *context.Context, nodeID int) ([]int, error) {
paths := linuxpath.New(ctx)
path := filepath.Join(
paths.SysDevicesSystemNode,
fmt.Sprintf("node%d", nodeID),
"distance",
)
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
items := strings.Fields(strings.TrimSpace(string(data)))
dists := make([]int, len(items), len(items)) // TODO: can a NUMA cell be offlined?
for idx, item := range items {
dist, err := strconv.Atoi(item)
if err != nil {
return dists, err
}
dists[idx] = dist
}
return dists, nil
}

View File

@@ -0,0 +1,17 @@
// +build !linux,!windows
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package topology
import (
"runtime"
"github.com/pkg/errors"
)
func (i *Info) load() error {
return errors.New("topologyFillInfo not implemented on " + runtime.GOOS)
}

View File

@@ -0,0 +1,156 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package topology
import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
)
const (
rcFailure = 0
sizeofLogicalProcessorInfo = 32
errInsufficientBuffer syscall.Errno = 122
relationProcessorCore = 0
relationNUMANode = 1
relationCache = 2
relationProcessorPackage = 3
relationGroup = 4
)
func (i *Info) load() error {
nodes, err := topologyNodes()
if err != nil {
return err
}
i.Nodes = nodes
if len(nodes) == 1 {
i.Architecture = ARCHITECTURE_SMP
} else {
i.Architecture = ARCHITECTURE_NUMA
}
return nil
}
func topologyNodes() ([]*Node, error) {
nodes := make([]*Node, 0)
lpis, err := getWin32LogicalProcessorInfos()
if err != nil {
return nil, err
}
for _, lpi := range lpis {
switch lpi.relationship {
case relationNUMANode:
nodes = append(nodes, &Node{
ID: lpi.numaNodeID(),
})
case relationProcessorCore:
// TODO(jaypipes): associated LP to processor core
case relationProcessorPackage:
// ignore
case relationCache:
// TODO(jaypipes) handle cache layers
default:
return nil, fmt.Errorf("Unknown LOGICAL_PROCESSOR_RELATIONSHIP value: %d", lpi.relationship)
}
}
return nodes, nil
}
// This is the CACHE_DESCRIPTOR struct in the Win32 API
type cacheDescriptor struct {
level uint8
associativity uint8
lineSize uint16
size uint32
cacheType uint32
}
// This is the SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct in the Win32 API
type logicalProcessorInfo struct {
processorMask uint64
relationship uint64
// The following dummyunion member is a representation of this part of
// the SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct:
//
// union {
// struct {
// BYTE Flags;
// } ProcessorCore;
// struct {
// DWORD NodeNumber;
// } NumaNode;
// CACHE_DESCRIPTOR Cache;
// ULONGLONG Reserved[2];
// } DUMMYUNIONNAME;
dummyunion [16]byte
}
// numaNodeID returns the NUMA node's identifier from the logical processor
// information struct by grabbing the integer representation of the struct's
// NumaNode unioned data element
func (lpi *logicalProcessorInfo) numaNodeID() int {
if lpi.relationship != relationNUMANode {
return -1
}
return int(binary.LittleEndian.Uint16(lpi.dummyunion[0:]))
}
// ref: https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlogicalprocessorinformation
func getWin32LogicalProcessorInfos() (
[]*logicalProcessorInfo,
error,
) {
lpis := make([]*logicalProcessorInfo, 0)
win32api := syscall.NewLazyDLL("kernel32.dll")
glpi := win32api.NewProc("GetLogicalProcessorInformation")
// The way the GetLogicalProcessorInformation (GLPI) Win32 API call
// works is wonky, but consistent with the Win32 API calling structure.
// Basically, you need to first call the GLPI API with a NUL pointerr
// and a pointer to an integer. That first call to the API should
// return ERROR_INSUFFICIENT_BUFFER, which is the indication that the
// supplied buffer pointer is NUL and needs to have memory allocated to
// it of an amount equal to the value of the integer pointer argument.
// Once the buffer is allocated this amount of space, the GLPI API call
// is again called. This time, the return value should be 0 and the
// buffer will have been set to an array of
// SYSTEM_LOGICAL_PROCESSOR_INFORMATION structs.
toAllocate := uint32(0)
// first, figure out how much we need
rc, _, win32err := glpi.Call(uintptr(0), uintptr(unsafe.Pointer(&toAllocate)))
if rc == rcFailure {
if win32err != errInsufficientBuffer {
return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API initial call failed to return ERROR_INSUFFICIENT_BUFFER")
}
} else {
// This shouldn't happen because buffer hasn't yet been allocated...
return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API initial call returned success instead of failure with ERROR_INSUFFICIENT_BUFFER")
}
// OK, now we actually allocate a raw buffer to fill with some number
// of SYSTEM_LOGICAL_PROCESSOR_INFORMATION structs
b := make([]byte, toAllocate)
rc, _, win32err = glpi.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(unsafe.Pointer(&toAllocate)))
if rc == rcFailure {
return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API call failed to set supplied buffer. Win32 system error: %s", win32err)
}
for x := uint32(0); x < toAllocate; x += sizeofLogicalProcessorInfo {
lpiraw := b[x : x+sizeofLogicalProcessorInfo]
lpi := &logicalProcessorInfo{
processorMask: binary.LittleEndian.Uint64(lpiraw[0:]),
relationship: binary.LittleEndian.Uint64(lpiraw[8:]),
}
copy(lpi.dummyunion[0:16], lpiraw[16:32])
lpis = append(lpis, lpi)
}
return lpis, nil
}

37
vendor/github.com/jaypipes/ghw/pkg/unitutil/unit.go generated vendored Normal file
View File

@@ -0,0 +1,37 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package unitutil
var (
KB int64 = 1024
MB = KB * 1024
GB = MB * 1024
TB = GB * 1024
PB = TB * 1024
EB = PB * 1024
)
// AmountString returns a string representation of the amount with an amount
// suffix corresponding to the nearest kibibit.
//
// For example, AmountString(1022) == "1022). AmountString(1024) == "1KB", etc
func AmountString(size int64) (int64, string) {
switch {
case size < MB:
return KB, "KB"
case size < GB:
return MB, "MB"
case size < TB:
return GB, "GB"
case size < PB:
return TB, "TB"
case size < EB:
return PB, "PB"
default:
return EB, "EB"
}
}

53
vendor/github.com/jaypipes/ghw/pkg/util/util.go generated vendored Normal file
View File

@@ -0,0 +1,53 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package util
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/jaypipes/ghw/pkg/context"
)
const (
UNKNOWN = "unknown"
disableWarningsEnv = "GHW_DISABLE_WARNINGS"
)
type closer interface {
Close() error
}
func SafeClose(c closer) {
err := c.Close()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to close: %s", err)
}
}
// Reads a supplied filepath and converts the contents to an integer. Returns
// -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 {
msg := "failed to read int from file: %s\n"
buf, err := ioutil.ReadFile(path)
if err != nil {
ctx.Warn(msg, err)
return -1
}
contents := strings.TrimSpace(string(buf))
res, err := strconv.Atoi(contents)
if err != nil {
ctx.Warn(msg, err)
return -1
}
return res
}

16
vendor/modules.txt vendored
View File

@@ -73,7 +73,7 @@ github.com/Microsoft/azure-vhd-utils/vhdcore/writer
github.com/RoaringBitmap/roaring
# github.com/Shopify/sarama v1.20.0
github.com/Shopify/sarama
# github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6
# github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d
github.com/StackExchange/wmi
# github.com/VividCortex/ewma v1.1.1
github.com/VividCortex/ewma
@@ -329,7 +329,7 @@ github.com/glycerine/go-unsnap-stream
github.com/go-logfmt/logfmt
# github.com/go-logr/logr v0.2.0
github.com/go-logr/logr
# github.com/go-ole/go-ole v1.2.2
# github.com/go-ole/go-ole v1.2.4
github.com/go-ole/go-ole
github.com/go-ole/go-ole/oleutil
# github.com/go-playground/locales v0.13.0
@@ -423,6 +423,18 @@ github.com/hako/durafmt
github.com/huandu/xstrings
# github.com/imdario/mergo v0.3.6
github.com/imdario/mergo
# github.com/jaypipes/ghw v0.8.0
github.com/jaypipes/ghw/pkg/context
github.com/jaypipes/ghw/pkg/cpu
github.com/jaypipes/ghw/pkg/linuxpath
github.com/jaypipes/ghw/pkg/marshal
github.com/jaypipes/ghw/pkg/memory
github.com/jaypipes/ghw/pkg/option
github.com/jaypipes/ghw/pkg/pci/address
github.com/jaypipes/ghw/pkg/snapshot
github.com/jaypipes/ghw/pkg/topology
github.com/jaypipes/ghw/pkg/unitutil
github.com/jaypipes/ghw/pkg/util
# github.com/jdcloud-api/jdcloud-sdk-go v1.55.0
github.com/jdcloud-api/jdcloud-sdk-go/core
github.com/jdcloud-api/jdcloud-sdk-go/services/asset/apis