// 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 ipmitool import ( "context" "fmt" "net" "strconv" "strings" "time" "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/tristate" "yunion.io/x/pkg/util/stringutils" "yunion.io/x/pkg/utils" "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/types" "yunion.io/x/onecloud/pkg/util/procutils" "yunion.io/x/onecloud/pkg/util/ssh" "yunion.io/x/onecloud/pkg/util/stringutils2" "yunion.io/x/onecloud/pkg/util/sysutils" ) type IPMIParser struct{} func (parser *IPMIParser) GetDefaultTimeout() time.Duration { return 20 * time.Second } var ( BOOTDEVS = []string{"pxe", "disk", "safe", "diag", "cdrom", "bios"} SOLOPTS = []string{"default", "skip", "enable"} ) type Args []string func newArgs(args ...interface{}) Args { ret := make([]string, len(args)) for i, arg := range args { ret[i] = fmt.Sprintf("%v", arg) } return ret } type IPMIExecutor interface { GetMode() string ExecuteCommand(args ...string) ([]string, error) } type SSHIPMI struct { IPMIParser sshClient *ssh.Client } func NewSSHIPMI(cli *ssh.Client) *SSHIPMI { return &SSHIPMI{ sshClient: cli, } } func (ipmi *SSHIPMI) GetMode() string { return "ssh" } func (ipmi *SSHIPMI) GetCommand(args ...string) *procutils.Command { nArgs := []string{"-I", "open"} nArgs = append(nArgs, args...) return procutils.NewCommand("/usr/bin/ipmitool", nArgs...) } func (ipmi *SSHIPMI) ExecuteCommand(args ...string) ([]string, error) { cmd := ipmi.GetCommand(args...) log.Debugf("[SSHIPMI] execute command: %s", cmd) return ipmi.sshClient.Run(cmd.String()) } type LanPlusIPMI struct { IPMIParser host string user string password string port int } func NewLanPlusIPMI(host, user, password string) *LanPlusIPMI { return NewLanPlusIPMIWithPort(host, user, password, 623) } func NewLanPlusIPMIWithPort(host, user, password string, port int) *LanPlusIPMI { return &LanPlusIPMI{ host: host, user: user, password: password, port: port, } } func (ipmi *LanPlusIPMI) GetMode() string { return "rmcp" } func (ipmi *LanPlusIPMI) GetCommand(args ...string) (*procutils.Command, context.CancelFunc) { nArgs := []string{ "-I", "lanplus", "-H", ipmi.host, "-p", fmt.Sprintf("%d", ipmi.port), "-U", ipmi.user, "-P", ipmi.password, } nArgs = append(nArgs, args...) ctx, cancel := context.WithTimeout(context.Background(), ipmi.GetDefaultTimeout()) return procutils.NewCommandContext(ctx, "ipmitool", nArgs...), cancel } func (ipmi *LanPlusIPMI) ExecuteCommand(args ...string) ([]string, error) { cmd, cancel := ipmi.GetCommand(args...) defer cancel() log.Debugf("[LanPlusIPMI] execute command: %s", cmd.String()) out, err := cmd.Output() if err != nil { return nil, err } return ssh.ParseOutput(out), nil } func GetSysGuid(exector IPMIExecutor) string { args := []string{"mc", "guid"} // args := []string{"raw", "0x06", "0x37"} lines, err := exector.ExecuteCommand(args...) if err != nil { // ignore error log.Errorf("mc guid error: %s", err) } for _, line := range lines { key, val := stringutils.SplitKeyValue(line) if key == "System GUID" { return sysutils.NormalizeUuid(val) } } return "" } func GetSysInfo(exector IPMIExecutor) (*types.SSystemInfo, error) { // TODO: do cache args := []string{"fru", "print", "0"} lines, err := exector.ExecuteCommand(args...) if err != nil { // ignore error log.Errorf("fru print 0 error: %s", err) } ret := make(map[string]string) keys := map[string]string{ "manufacture": "Product Manufacturer", "model": "Product Name", "bmodel": "Board Product", "version": "Product Version", "sn": "Product Serial", "bsn": "Board Serial", } for _, line := range lines { key, val := stringutils.SplitKeyValue(line) if key != "" { for n, v := range keys { if _, ok := ret[n]; v == key && !ok { ret[n] = val } } } } _, snOk := ret["sn"] bsn, bsnOk := ret["bsn"] if !snOk && bsnOk { // no product serial ret["sn"] = bsn } info := types.SSystemInfo{} err = sysutils.DumpMapToObject(ret, &info) info.OemName = types.ManufactureOemName(info.Manufacture) return &info, err } /*func GetLanChannels(sysinfo *types.SSystemInfo) []int { return profiles.GetLanChannel(sysinfo) } func GetDefaultLanChannel(sysinfo *types.SSystemInfo) int { return GetLanChannels(sysinfo)[0] } func GetRootId(sysinfo *types.SSystemInfo) int { return profiles.GetRootId(sysinfo) }*/ func GetLanConfig(exector IPMIExecutor, channel uint8) (*types.SIPMILanConfig, error) { args := newArgs("lan", "print", channel) lines, err := ExecuteCommands(exector, args) if err != nil { return nil, err } ret := new(types.SIPMILanConfig) for _, line := range lines { key, val := stringutils.SplitKeyValue(line) if key == "" { continue } switch key { case "IP Address Source": if val == "Static Address" { ret.IPSrc = "static" } case "IP Address": ret.IPAddr = val case "Subnet Mask": ret.Netmask = val case "MAC Address": ret.Mac, _ = net.ParseMAC(val) case "Default Gateway IP": ret.Gateway = val case "802.1q VLAN ID": vlanId, _ := strconv.ParseInt(val, 10, 64) ret.VlanId = int(vlanId) } } return ret, nil } func tryExecuteCommand(exector IPMIExecutor, args ...string) ([]string, error) { var err error var ret []string maxTries := 3 for tried := 0; tried < maxTries; tried++ { ret, err = exector.ExecuteCommand(args...) if err == nil { return ret, nil } sleepTime := time.Second * (1 << uint(tried)) log.Errorf("Execute args %v error: %v, sleep %s then try again", args, err, sleepTime) time.Sleep(sleepTime) } return ret, err } func ExecuteCommands(exector IPMIExecutor, args ...Args) ([]string, error) { results := make([]string, 0) for _, arg := range args { ret, err := tryExecuteCommand(exector, arg...) if err != nil { return nil, err } results = append(results, ret...) } return results, nil } func doActions(exector IPMIExecutor, actionName string, args ...Args) error { _, err := ExecuteCommands(exector, args...) if err != nil { return fmt.Errorf("Do %s action error: %v", actionName, err) } return nil } func SetLanDHCP(exector IPMIExecutor, lanChannel uint8) error { args := newArgs("lan", "set", lanChannel, "ipsrc", "dhcp") return doActions(exector, "set_lan_dhcp", args) } func SetLanStatic( exector IPMIExecutor, channel uint8, ip string, mask string, gateway string, ) error { // config, err := GetLanConfig(exector, channel) // if err != nil { // return err // } // var argss []Args // if config.IPAddr == ip && config.Netmask == mask && config.Gateway == gateway { argss := []Args{ newArgs("lan", "set", channel, "ipsrc", "static"), newArgs("lan", "set", channel, "ipaddr", ip), newArgs("lan", "set", channel, "netmask", mask), newArgs("lan", "set", channel, "defgw", "ipaddr", gateway), } // } else { // argss = []Args{ // newArgs("lan", "set", channel, "ipsrc", "static"), // newArgs("lan", "set", channel, "ipaddr", ip), // newArgs("lan", "set", channel, "netmask", mask), // newArgs("lan", "set", channel, "defgw", "ipaddr", gateway), // } // } return doActions(exector, "set_lan_static", argss...) } func SetLanStaticIP(exector IPMIExecutor, channel uint8, ip string) error { args := newArgs("lan", "set", channel, "ipaddr", ip) return doActions(exector, "set_lan_static_ip", args) } func setLanAccess(exector IPMIExecutor, channel uint8, access string) error { args := []Args{ newArgs("lan", "set", channel, "access", access), // newArgs("lan", "set", channel, "auth", "ADMIN", "MD5"), } return doActions(exector, "set_lan_access", args...) } func EnableLanAccess(exector IPMIExecutor, channel uint8) error { return setLanAccess(exector, channel, "on") } func ListLanUsers(exector IPMIExecutor, channel uint8) ([]compute.IPMIUser, error) { args := newArgs("user", "list", channel) ret, err := ExecuteCommands(exector, args) if err != nil { return nil, errors.Wrapf(err, "list user at channel %d", channel) } return sysutils.ParseIPMIUser(ret), nil } func CreateOrSetAdminUser(exector IPMIExecutor, channel uint8, rootId int, username string, password string) error { users, err := ListLanUsers(exector, channel) if err != nil { return errors.Wrap(err, "List users") } if len(users) == 0 { return errors.Errorf("Empty users at channel %d", channel) } var foundUser *compute.IPMIUser = nil for _, user := range users { tmp := user if user.Name == username { foundUser = &tmp break } } if foundUser == nil { minEmptyUserId := -1 for _, user := range users { if user.Name == "" { minEmptyUserId = user.Id break } } if minEmptyUserId == -1 { log.Warningf("Not found min empty user id, use root id %d to set", rootId) minEmptyUserId = rootId } return SetIdUserPasswd(exector, channel, minEmptyUserId, username, password) } return SetLanUserAdminPasswd(exector, channel, foundUser.Id, password) } func SetLanUserAdminPasswd(exector IPMIExecutor, channel uint8, id int, password string) error { var err error password, err = stringutils2.EscapeEchoString(password) if err != nil { return fmt.Errorf("EscapeEchoString for password: %s, error: %v", password, err) } args := []Args{ newArgs("user", "enable", id), newArgs("user", "set", "password", id, fmt.Sprintf("\"%s\"", password)), newArgs("user", "priv", id, 4, channel), } err = doActions(exector, "set_lan_user_password", args...) if err != nil { return err } args = []Args{newArgs( "raw", "0x06", "0x43", fmt.Sprintf("0x%02x", 0xb0+channel), fmt.Sprintf("0x%02x", id), "0x04", "0x00")} err = doActions(exector, "set_lan_user_password2", args...) if err == nil { return nil } args = []Args{newArgs( "channel", "setaccess", channel, id, "link=on", "ipmi=on", "callin=on", "privilege=4", )} return doActions(exector, "set_lan_user_password3", args...) } func SetIdUserPasswd(exector IPMIExecutor, channel uint8, id int, user string, password string) error { args := newArgs("user", "set", "name", id, user) if err := doActions(exector, fmt.Sprintf("set_id%d_name", id), args); err != nil { return errors.Wrapf(err, "change root id %d to name %s", id, user) } return SetLanUserAdminPasswd(exector, channel, id, password) } func SetLanPasswd(exector IPMIExecutor, rootId int, password string) error { password, err := stringutils2.EscapeEchoString(password) if err != nil { return fmt.Errorf("EscapeEchoString for password: %v", err) } args := newArgs("user", "set", "password", rootId, fmt.Sprintf("\"%s\"", password)) return doActions(exector, "set_lan_passwd", args) } func GetChassisPowerStatus(exector IPMIExecutor) (string, error) { args := newArgs("chassis", "power", "status") ret, err := ExecuteCommands(exector, args) if err != nil { return "", err } for _, line := range ret { if strings.Contains(line, "Chassis Power is") { data := strings.Split(line, " ") status := strings.ToLower(strings.TrimSpace(data[len(data)-1])) return status, nil } } return "", fmt.Errorf("Unknown chassis status") } func GetBootFlags(exector IPMIExecutor) (*types.SIPMIBootFlags, error) { args := newArgs("raw", "0x00", "0x09", "0x05", "0x00", "0x00") ret, err := ExecuteCommands(exector, args) if err != nil { return nil, err } bytes, err := HexStr2Bytes(ret[0]) if err != nil { return nil, err } bootdevIdx := ((bytes[3] >> 2) & 0x0f) - 1 bootdev := "" if bootdevIdx >= 0 && int(bootdevIdx) < len(BOOTDEVS) { bootdev = BOOTDEVS[bootdevIdx] } flags := &types.SIPMIBootFlags{ Dev: bootdev, } solIdx := (bytes[4] & 0x03) if solIdx == 1 { sol := false flags.Sol = &sol } else if solIdx == 2 { sol := true flags.Sol = &sol } return flags, nil } func HexStr2Bytes(hs string) ([]int64, error) { b := []int64{} for _, x := range strings.Split(hs, " ") { intV, err := strconv.ParseInt(x, 16, 64) if err != nil { return nil, err } b = append(b, intV) } return b, nil } func GetACPIPowerStatus(exector IPMIExecutor) ([]int64, error) { args := newArgs("raw", "0x06", "0x07") ret, err := ExecuteCommands(exector, args) if err != nil { return nil, err } return HexStr2Bytes(ret[0]) } func DoSoftShutdown(exector IPMIExecutor) error { args := newArgs("chassis", "power", "soft") return doActions(exector, "do_soft_shutdown", args) } func DoHardShutdown(exector IPMIExecutor) error { args := newArgs("chassis", "power", "off") return doActions(exector, "do_hard_shutdown", args) } func DoPowerOn(exector IPMIExecutor) error { args := newArgs("chassis", "power", "on") return doActions(exector, "do_power_on", args) } func DoPowerReset(exector IPMIExecutor) error { args := newArgs("chassis", "power", "reset") return doActions(exector, "do_power_reset", args) } func DoPowerCycle(exector IPMIExecutor) error { args := newArgs("chassis", "power", "cycle") return doActions(exector, "do_power_cycle", args) } func DoReboot(exector IPMIExecutor) error { maxTries := 10 var status string var err error status, err = GetChassisPowerStatus(exector) if err != nil { log.Errorf("DoReboot get power status 1st: %v", err) } isValidStatus := func(s string) bool { return utils.IsInStringArray(s, []string{string(types.POWER_STATUS_ON), string(types.POWER_STATUS_OFF)}) } for tried := 0; !isValidStatus(status) && tried <= maxTries; tried++ { time.Sleep(1 * time.Second) status, err = GetChassisPowerStatus(exector) if err != nil { log.Errorf("DoReboot %d tries to get power status: %v", tried, err) } } if !isValidStatus(status) { return fmt.Errorf("Unexpected power status: %q", status) } // do shutdown if status == string(types.POWER_STATUS_ON) { if err := DoHardShutdown(exector); err != nil { log.Errorf("DoHardShutdown: %v", err) } time.Sleep(1 * time.Second) for tried := 0; tried < maxTries; tried++ { status, err = GetChassisPowerStatus(exector) if err != nil { log.Errorf("DoReboot %d tries to get power status: %v", tried, err) } if status == string(types.POWER_STATUS_OFF) { break } time.Sleep(10 * time.Second) } } // do power on status, _ = GetChassisPowerStatus(exector) for tried := 0; status != string(types.POWER_STATUS_ON) && tried < maxTries; tried++ { if err := DoPowerOn(exector); err != nil { log.Errorf("DoReboot %d tries to power on: %v", tried, err) } time.Sleep(10 * time.Second) status, err = GetChassisPowerStatus(exector) if err != nil { log.Errorf("DoReboot %d tries to get power status: %v", tried, err) } } status, err = GetChassisPowerStatus(exector) if err != nil { return errors.Wrap(err, "Get power status after power on") } if status != string(types.POWER_STATUS_ON) { return errors.Errorf("do reboot fail to poweron, current status: %s", status) } return nil } func doRebootToFlag(exector IPMIExecutor, setFunc func(IPMIExecutor) error) error { err := setFunc(exector) if err != nil { return err } return DoReboot(exector) } func SetRebootToDisk(exector IPMIExecutor) error { return SetBootFlags(exector, "disk", tristate.True, true) } func DoRebootToDisk(exector IPMIExecutor) error { return doRebootToFlag(exector, SetRebootToDisk) } func SetRebootToPXE(exector IPMIExecutor) error { return SetBootFlagPXE(exector) } func DoRebootToPXE(exector IPMIExecutor) error { return doRebootToFlag(exector, SetRebootToPXE) } func SetRebootToBIOS(exector IPMIExecutor) error { return SetBootFlags(exector, "bios", tristate.True, false) } func DoRebootToBIOS(exector IPMIExecutor) error { return doRebootToFlag(exector, SetRebootToBIOS) } func SetBootFlagPXE(exector IPMIExecutor) error { return setBootFlagsV2(exector, "pxe") } func SetBootFlags( exector IPMIExecutor, bootdev string, sol tristate.TriState, enablePersistent bool, ) error { err := setBootFlagsV1(exector, bootdev, sol, enablePersistent) if err == nil { return nil } return setBootFlagsV2(exector, bootdev) } func setBootFlagsV1( exector IPMIExecutor, bootdev string, sol tristate.TriState, enablePersistent bool, ) error { cmd := []interface{}{"raw", "0x00", "0x08", "0x05"} bootdevIdx := 0 if ok, idx := utils.InStringArray(bootdev, BOOTDEVS); ok { bootdevIdx = idx + 1 } else { return fmt.Errorf("Illegal bootdev %s", bootdev) } valid := 0x80 if enablePersistent { valid = valid + 0x40 } solIdx := 0 if !sol.IsNone() { if sol.IsTrue() { solIdx = 2 } else { solIdx = 1 } } for _, x := range []int{valid, bootdevIdx << 2, solIdx, 0, 0} { cmd = append(cmd, fmt.Sprintf("0x%02x", x)) } return doActions(exector, "set_boot_flags_v1", newArgs(cmd...)) } func setBootFlagsV2(exector IPMIExecutor, bootdev string) error { return doActions( exector, fmt.Sprintf("set_boot_flag_%s", bootdev), newArgs("chassis", "bootdev", bootdev), ) } func GetIPMILanPort(exector IPMIExecutor) (string, error) { ret, err := ExecuteCommands(exector, newArgs("delloem", "lan", "get")) if err != nil { return "", err } return ret[1], nil } func SetDellIPMILanPortShared(exector IPMIExecutor) error { args1 := newArgs("delloem", "lan", "set", "shared") args2 := newArgs("delloem", "lan", "set", "shared", "with", "lom1") err2 := doActions(exector, "_dell_set_ipmi_lan_port_shared_02", args2) if err2 != nil { return doActions(exector, "_dell_set_ipmi_lan_port_shared_01", args1) } return nil } func SetHuaweiIPMILanPortShared(exector IPMIExecutor) error { args := []Args{ newArgs( "raw", "0xc", "0x1", "0x1", "0xd7", "0xdb", "0x07", "0x00", "0x2", ), newArgs( "raw", "0x30", "0x93", "0xdb", "0x07", "0x00", "0x05", "0x0d", "0x0", "0x0", "0x1", "0x0", ), } return doActions(exector, "_huawei_set_ipmi_lan_port_shared", args...) } func SetIPMILanPortDedicated(exector IPMIExecutor) error { return doActions( exector, "set_ipmi_lan_port_dedicated", newArgs("delloem", "lan", "set", "dedicated"), ) } func DoBMCReset(exector IPMIExecutor) error { return doActions(exector, "do_bmc_reset", newArgs("mc", "reset", "cold")) }