You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
155 lines
5.2 KiB
155 lines
5.2 KiB
// Copyright 2020 PingCAP, Inc.
|
|
//
|
|
// 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,
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package telemetry
|
|
|
|
import (
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/iancoleman/strcase"
|
|
"github.com/pingcap/errors"
|
|
"github.com/pingcap/tidb/sessionctx"
|
|
"github.com/pingcap/tidb/util/sqlexec"
|
|
)
|
|
|
|
var (
|
|
// Explicitly list all allowed field names to avoid potential leaking sensitive info when new fields are added later.
|
|
sortedCPUAllowedFieldNames = []string{"cpu-logical-cores", "cpu-physical-cores", "cpu-frequency", "cache", "cpu-vendor-id", "l1-cache-size", "l1-cache-line-size", "l2-cache-size", "l2-cache-line-size", "l3-cache-size", "l3-cache-line-size"}
|
|
sortedDiskAllowedFieldNames = []string{"fstype", "opts", "total", "free", "used", "free-percent", "used-percent"} // path is not included
|
|
regexDiskAllowedNames = regexp.MustCompile(`^(disk\d+s\d+|rootfs|devtmpfs|sd[a-z]\d*|vd[a-z]\d*|hd[a-z]\d*|nvme\d+(n\d+(p\d)?)?|md[\da-z]+)$`)
|
|
sortedDiskAllowedPaths = []string{"/", "/boot", "/dev", "/private/var/vm", "/System/Volumes/Data"}
|
|
sortedMemoryAllowedFieldNames = []string{"capacity"}
|
|
)
|
|
|
|
func init() {
|
|
sort.Strings(sortedCPUAllowedFieldNames)
|
|
sort.Strings(sortedDiskAllowedFieldNames)
|
|
sort.Strings(sortedDiskAllowedPaths)
|
|
sort.Strings(sortedMemoryAllowedFieldNames)
|
|
}
|
|
|
|
type clusterHardwareItem struct {
|
|
InstanceType string `json:"instanceType"`
|
|
ListenHostHash string `json:"listenHostHash"`
|
|
ListenPort string `json:"listenPort"`
|
|
CPU map[string]string `json:"cpu,omitempty"`
|
|
Memory map[string]string `json:"memory,omitempty"`
|
|
Disk map[string]map[string]string `json:"disk,omitempty"`
|
|
}
|
|
|
|
func normalizeDiskName(name string) string {
|
|
const prefix = "/dev/"
|
|
if strings.HasPrefix(name, prefix) {
|
|
return name[len(prefix):]
|
|
}
|
|
return name
|
|
}
|
|
|
|
func isNormalizedDiskNameAllowed(name string) bool {
|
|
return regexDiskAllowedNames.MatchString(name)
|
|
}
|
|
|
|
func normalizeFieldName(name string) string {
|
|
return strcase.ToLowerCamel(name)
|
|
}
|
|
|
|
func getClusterHardware(ctx sessionctx.Context) ([]*clusterHardwareItem, error) {
|
|
rows, _, err := ctx.(sqlexec.RestrictedSQLExecutor).ExecRestrictedSQL(`SELECT TYPE, INSTANCE, DEVICE_TYPE, DEVICE_NAME, NAME, VALUE FROM information_schema.cluster_hardware`)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
// WARNING: The key of this variable is not hashed, it must not be returned directly.
|
|
itemsByInstance := make(map[string]*clusterHardwareItem)
|
|
L:
|
|
for _, row := range rows {
|
|
if row.Len() < 6 {
|
|
continue L
|
|
}
|
|
instance := row.GetString(1)
|
|
activeItem, ok := itemsByInstance[instance]
|
|
if !ok {
|
|
hostHash, port := parseAddressAndHash(instance)
|
|
activeItem = &clusterHardwareItem{
|
|
InstanceType: row.GetString(0),
|
|
ListenHostHash: hostHash,
|
|
ListenPort: port,
|
|
CPU: make(map[string]string),
|
|
Memory: make(map[string]string),
|
|
Disk: make(map[string]map[string]string),
|
|
}
|
|
itemsByInstance[instance] = activeItem
|
|
}
|
|
|
|
deviceType := row.GetString(2)
|
|
deviceName := row.GetString(3)
|
|
fieldName := row.GetString(4)
|
|
fieldValue := row.GetString(5)
|
|
|
|
switch deviceType {
|
|
case "cpu":
|
|
if deviceName != "cpu" {
|
|
continue L
|
|
}
|
|
if !sortedStringContains(sortedCPUAllowedFieldNames, fieldName) {
|
|
continue L
|
|
}
|
|
activeItem.CPU[normalizeFieldName(fieldName)] = fieldValue
|
|
case "memory":
|
|
if deviceName != "memory" {
|
|
continue L
|
|
}
|
|
if !sortedStringContains(sortedMemoryAllowedFieldNames, fieldName) {
|
|
continue L
|
|
}
|
|
activeItem.Memory[normalizeFieldName(fieldName)] = fieldValue
|
|
case "disk":
|
|
var hashedDeviceName string
|
|
normalizedDiskName := normalizeDiskName(deviceName)
|
|
if isNormalizedDiskNameAllowed(normalizedDiskName) {
|
|
// Use plain text only when it is in a list that we know safe.
|
|
hashedDeviceName = normalizedDiskName
|
|
} else {
|
|
hashedDeviceName = hashString(normalizedDiskName)
|
|
}
|
|
activeDiskItem, ok := activeItem.Disk[hashedDeviceName]
|
|
if !ok {
|
|
activeDiskItem = make(map[string]string)
|
|
activeDiskItem[normalizeFieldName("deviceName")] = hashedDeviceName
|
|
activeItem.Disk[hashedDeviceName] = activeDiskItem
|
|
}
|
|
if fieldName == "path" {
|
|
var path string
|
|
if sortedStringContains(sortedDiskAllowedPaths, fieldValue) {
|
|
// Use plain text only when it is in a list that we know safe.
|
|
path = fieldValue
|
|
} else {
|
|
path = hashString(fieldValue)
|
|
}
|
|
activeDiskItem[normalizeFieldName("path")] = path
|
|
} else if sortedStringContains(sortedDiskAllowedFieldNames, fieldName) {
|
|
activeDiskItem[normalizeFieldName(fieldName)] = fieldValue
|
|
}
|
|
}
|
|
}
|
|
|
|
r := make([]*clusterHardwareItem, 0, len(itemsByInstance))
|
|
for _, item := range itemsByInstance {
|
|
r = append(r, item)
|
|
}
|
|
|
|
// TODO: Go's map has random order. Better to generate a fixed-order result.
|
|
|
|
return r, nil
|
|
}
|
|
|