From fa2e55ce1469dbeb29f617feeb7a2c8eee039378 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Thu, 23 Apr 2020 02:48:40 +0800 Subject: [PATCH] supermicro redfish support --- pkg/baremetal/manager.go | 2 + pkg/util/redfish/bmconsole/supermicro.go | 88 ++++++++ pkg/util/redfish/loader/loader.go | 1 + pkg/util/redfish/redfish.go | 59 ++--- pkg/util/redfish/supermicro/doc.go | 15 ++ pkg/util/redfish/supermicro/supermicro.go | 252 ++++++++++++++++++++++ 6 files changed, 393 insertions(+), 24 deletions(-) create mode 100644 pkg/util/redfish/bmconsole/supermicro.go create mode 100644 pkg/util/redfish/supermicro/doc.go create mode 100644 pkg/util/redfish/supermicro/supermicro.go diff --git a/pkg/baremetal/manager.go b/pkg/baremetal/manager.go index f03f09b7bc..46f2a2327c 100644 --- a/pkg/baremetal/manager.go +++ b/pkg/baremetal/manager.go @@ -1949,6 +1949,8 @@ func (b *SBaremetalInstance) GetConsoleJNLP(ctx context.Context) (string, error) return bmc.GetIloConsoleJNLP(ctx) case "dell", "dell inc.": return bmc.GetIdracConsoleJNLP(ctx, "", "") + case "supermicro": + return bmc.GetSupermicroConsoleJNLP(ctx) } return "", httperrors.NewNotImplementedError("Unsupported manufacture %s", manufacture) } diff --git a/pkg/util/redfish/bmconsole/supermicro.go b/pkg/util/redfish/bmconsole/supermicro.go new file mode 100644 index 0000000000..d7b9b33495 --- /dev/null +++ b/pkg/util/redfish/bmconsole/supermicro.go @@ -0,0 +1,88 @@ +// 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 bmconsole + +import ( + "context" + "net/http" + "net/url" + "strings" + "time" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/timeutils" + + "yunion.io/x/onecloud/pkg/util/httputils" +) + +func (r *SBMCConsole) GetSupermicroConsoleJNLP(ctx context.Context) (string, error) { + loginData := strings.Join([]string{ + "name=" + url.QueryEscape(r.username), + "pwd=" + url.QueryEscape(r.password), + }, "&") + + // cookie: + // -http-session-=::http.session::0103fd02ceac2d642361b6fdcd4a5994; + // sysidledicon=ledIcon%20grayLed; + // tokenvalue=478be97abdaeb4d454c0418fcca9094d + + cookies := make(map[string]string) + + // first do html login + postHdr := http.Header{} + postHdr.Set("Content-Type", "application/x-www-form-urlencoded") + setCookieHeader(postHdr, cookies) + hdr, _, err := r.RawRequest(ctx, httputils.POST, "/cgi/login.cgi", postHdr, []byte(loginData)) + if err != nil { + return "", errors.Wrap(err, "r.FormPost Login") + } + for _, cookieHdr := range hdr["Set-Cookie"] { + parts := strings.Split(cookieHdr, ";") + if len(parts) > 0 { + pparts := strings.Split(parts[0], "=") + if len(pparts) > 1 { + cookies[pparts[0]] = pparts[1] + } + } + } + + getHdr := http.Header{} + setCookieHeader(getHdr, cookies) + + now := time.Now() + // fwtype=255&time_stamp=Thu%20Apr%2023%202020%2002%3A25%3A13%20GMT%2B0800%20(%E4%B8%AD%E5%9B%BD%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4)&_= + loginData = strings.Join([]string{ + "fwtype=255", + "time_stamp=" + url.QueryEscape(timeutils.RFC2882Time(now)), + "_=", + }, "&") + + _, rspBody, err := r.RawRequest(ctx, httputils.POST, "/cgi/upgrade_process.cgi", getHdr, []byte(loginData)) + if err != nil { + return "", errors.Wrapf(err, "r.RawPost %s", loginData) + } + + if r.isDebug { + log.Debugf("upgrade_process.cgi %s", rspBody) + } + + _, rspBody, err = r.RawRequest(ctx, httputils.GET, "/cgi/url_redirect.cgi?url_name=ikvm&url_type=jwsk", getHdr, nil) + if err != nil { + return "", errors.Wrap(err, "r.RawGet") + } + + return string(rspBody), nil +} diff --git a/pkg/util/redfish/loader/loader.go b/pkg/util/redfish/loader/loader.go index 775cdc11fd..7030b1ab76 100644 --- a/pkg/util/redfish/loader/loader.go +++ b/pkg/util/redfish/loader/loader.go @@ -21,6 +21,7 @@ import ( _ "yunion.io/x/onecloud/pkg/util/redfish/idrac" _ "yunion.io/x/onecloud/pkg/util/redfish/idrac9" _ "yunion.io/x/onecloud/pkg/util/redfish/ilo" + _ "yunion.io/x/onecloud/pkg/util/redfish/supermicro" ) func init() { diff --git a/pkg/util/redfish/redfish.go b/pkg/util/redfish/redfish.go index 77f2150e96..c7d4fe542c 100644 --- a/pkg/util/redfish/redfish.go +++ b/pkg/util/redfish/redfish.go @@ -78,7 +78,11 @@ func (r *SBaseRedfishClient) GetHost() string { log.Errorf("urlParse %s fail %s", r.endpoint, err) return "" } - return parts.Hostname() + ports := parts.Port() + if len(ports) == 0 { + return parts.Hostname() + } + return parts.Hostname() + ":" + parts.Port() } func (r *SBaseRedfishClient) GetUsername() string { @@ -421,32 +425,36 @@ func (r *SBaseRedfishClient) GetSystemInfo(ctx context.Context) (string, SSystem sysInfo.ResetTypeSupported[i] = resetTypeStr } - nicListPath, _ := resp.GetString("EthernetInterfaces", "@odata.id") - nicListInfo, err := r.Get(ctx, nicListPath) + nicListPath, err := resp.GetString("EthernetInterfaces", "@odata.id") if err != nil { - return path, sysInfo, errors.Wrap(err, "Get EthernetInterfaces fail") - } - nicList, _ := nicListInfo.GetArray("Members") - if len(nicList) > 0 { - sysInfo.EthernetNICs = make([]string, len(nicList)) - for i := range nicList { - nicPath, _ := nicList[i].GetString("@odata.id") - nicInfo, err := r.Get(ctx, nicPath) - if err != nil { - return path, sysInfo, errors.Wrapf(err, "Get EthernetInterface[%d] error", i) - } - var macAddr string - for _, key := range []string{ - "MacAddress", - "MACAddress", - "PermanentMACAddress", - } { - macAddr, _ = nicInfo.GetString(key) - if len(macAddr) > 0 { - break + log.Warningf("no EthernetInterfaces info") + } else { + nicListInfo, err := r.Get(ctx, nicListPath) + if err != nil { + return path, sysInfo, errors.Wrap(err, "Get EthernetInterfaces fail") + } + nicList, _ := nicListInfo.GetArray("Members") + if len(nicList) > 0 { + sysInfo.EthernetNICs = make([]string, len(nicList)) + for i := range nicList { + nicPath, _ := nicList[i].GetString("@odata.id") + nicInfo, err := r.Get(ctx, nicPath) + if err != nil { + return path, sysInfo, errors.Wrapf(err, "Get EthernetInterface[%d] error", i) } + var macAddr string + for _, key := range []string{ + "MacAddress", + "MACAddress", + "PermanentMACAddress", + } { + macAddr, _ = nicInfo.GetString(key) + if len(macAddr) > 0 { + break + } + } + sysInfo.EthernetNICs[i] = netutils.FormatMacAddr(macAddr) } - sysInfo.EthernetNICs[i] = netutils.FormatMacAddr(macAddr) } } @@ -542,6 +550,9 @@ func (r *SBaseRedfishClient) readLogs(ctx context.Context, path string, subsys s // } // return nil, errors.Wrap(err, path) } + if r.IsDebug { + log.Debugf("readLogs %s", resp.PrettyString()) + } tmpEvents := make([]SEvent, 0) err = resp.Unmarshal(&tmpEvents, r.IRedfishDriver().LogItemsKey()) if err != nil { diff --git a/pkg/util/redfish/supermicro/doc.go b/pkg/util/redfish/supermicro/doc.go new file mode 100644 index 0000000000..9d6eece0df --- /dev/null +++ b/pkg/util/redfish/supermicro/doc.go @@ -0,0 +1,15 @@ +// 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 supermicro // import "yunion.io/x/onecloud/pkg/util/redfish/supermicro" diff --git a/pkg/util/redfish/supermicro/supermicro.go b/pkg/util/redfish/supermicro/supermicro.go new file mode 100644 index 0000000000..f98d9852ce --- /dev/null +++ b/pkg/util/redfish/supermicro/supermicro.go @@ -0,0 +1,252 @@ +// 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 supermicro + +import ( + "context" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/httputils" + "yunion.io/x/onecloud/pkg/util/redfish" + "yunion.io/x/onecloud/pkg/util/redfish/bmconsole" + "yunion.io/x/onecloud/pkg/util/redfish/generic" +) + +type SSupermicroRedfishApiFactory struct { +} + +func (f *SSupermicroRedfishApiFactory) Name() string { + return "Supermicro" +} + +func (f *SSupermicroRedfishApiFactory) NewApi(endpoint, username, password string, debug bool) redfish.IRedfishDriver { + return NewSupermicroRedfishApi(endpoint, username, password, debug) +} + +func init() { + redfish.RegisterApiFactory(&SSupermicroRedfishApiFactory{}) +} + +type SSupermicroRefishApi struct { + generic.SGenericRefishApi +} + +func NewSupermicroRedfishApi(endpoint, username, password string, debug bool) redfish.IRedfishDriver { + api := &SSupermicroRefishApi{ + SGenericRefishApi: generic.SGenericRefishApi{ + SBaseRedfishClient: redfish.NewBaseRedfishClient(endpoint, username, password, debug), + }, + } + api.SetVirtualObject(api) + return api +} + +func (r *SSupermicroRefishApi) ParseRoot(root jsonutils.JSONObject) error { + if root.Contains("UUID") && root.Contains("UpdateService") && root.Contains("Oem") { + return nil + } + return errors.Error("not iDrac") +} + +func (r *SSupermicroRefishApi) GetVirtualCdromJSON(ctx context.Context) (string, jsonutils.JSONObject, error) { + _, resp, err := r.GetResource(ctx, "Managers", "0", "VirtualMedia") + if err != nil { + return "", nil, errors.Wrap(err, "r.GetResource") + } + log.Debugf("resp %s", resp) + path, err := resp.GetString("Oem", "Supermicro", "VirtualMediaConfig", "@odata.id") + if err != nil { + return "", nil, errors.Wrap(err, "GetString Oem Supermicro VirtualMediaConfig @odata.id") + } + cdResp, err := r.Get(ctx, path) + if err != nil { + return "", nil, errors.Wrap(err, "Get Cdrom info fail") + } + if cdResp != nil { + return path, cdResp, nil + } + return "", nil, httperrors.ErrNotFound +} + +func (r *SSupermicroRefishApi) GetVirtualCdromInfo(ctx context.Context) (string, redfish.SCdromInfo, error) { + cdInfo := redfish.SCdromInfo{} + path, jsonResp, err := r.GetVirtualCdromJSON(ctx) + if err != nil { + return "", cdInfo, errors.Wrap(err, "r.GetVirtualCdromJSON") + } + imgPath, _ := jsonResp.GetString("Image") + if imgPath == "null" { + imgPath = "" + } + cdInfo.Image = imgPath + if jsonResp.Contains("Actions") { + cdInfo.SupportAction = true + } + return path, cdInfo, nil +} + +func (r *SSupermicroRefishApi) MountVirtualCdrom(ctx context.Context, path string, cdromUrl string, boot bool) error { + info := jsonutils.NewDict() + info.Set("Image", jsonutils.NewString(cdromUrl)) + + path = httputils.JoinPath(path, "Actions/VirtualMedia.InsertMedia") + _, _, err := r.Post(ctx, path, info) + if err != nil { + return errors.Wrap(err, "r.Post") + } + // log.Debugf("%s", resp.PrettyString()) + if boot { + err = r.SetNextBootVirtualCdrom(ctx) + if err != nil { + return errors.Wrap(err, "r.SetNextBootVirtualCdrom") + } + } + return nil +} + +func (r *SSupermicroRefishApi) UmountVirtualCdrom(ctx context.Context, path string) error { + info := jsonutils.NewDict() + + path = httputils.JoinPath(path, "Actions/VirtualMedia.EjectMedia") + _, _, err := r.Post(ctx, path, info) + if err != nil { + return errors.Wrap(err, "r.Post") + } + // log.Debugf("%s", resp.PrettyString()) + return nil +} + +func (r *SSupermicroRefishApi) GetConsoleJNLP(ctx context.Context) (string, error) { + bmc := bmconsole.NewBMCConsole(r.GetHost(), r.GetUsername(), r.GetPassword(), r.IsDebug) + return bmc.GetSupermicroConsoleJNLP(ctx) +} + +func (r *SSupermicroRefishApi) ReadSystemLogs(ctx context.Context, since time.Time) ([]redfish.SEvent, error) { + _, resp, err := r.GetResource(ctx, "Managers", "0", "LogServices", "0", "Entries") + if err != nil { + return nil, errors.Wrap(err, "GetResource Managers 0 LogServices 0 Entries") + } + members, err := resp.GetArray("Members") + if err != nil { + return nil, errors.Wrap(err, "find Members") + } + events := make([]redfish.SEvent, 0) + for i := range members { + path, _ := members[i].GetString("@odata.id") + resp, err := r.Get(ctx, path) + if err != nil { + log.Errorf("Get url for event %s error %s", path, err) + continue + } + event := redfish.SEvent{} + err = resp.Unmarshal(&event) + if err == nil { + event.Type, _ = resp.GetString("EntryType") + event.Severity, _ = resp.GetString("EntryCode") + events = append(events, event) + } else { + log.Errorf("unmarshal event fail %s %s", resp, err) + } + } + return events, nil +} + +func (r *SSupermicroRefishApi) ReadManagerLogs(ctx context.Context, since time.Time) ([]redfish.SEvent, error) { + return nil, httperrors.ErrNotSupported +} + +func (r *SSupermicroRefishApi) GetClearSystemLogsPath() string { + return "/redfish/v1/Managers/1/LogServices/Log1/Actions/LogService.Reset" +} + +func (r *SSupermicroRefishApi) GetClearManagerLogsPath() string { + return "/redfish/v1/Managers/1/LogServices/Log1/Actions/LogService.Reset" +} + +func (r *SSupermicroRefishApi) GetPowerPath() string { + return "/redfish/v1/Chassis/1/Power" +} + +func (r *SSupermicroRefishApi) GetThermalPath() string { + return "/redfish/v1/Chassis/1/Thermal" +} + +type sNtpConf struct { + NTPEnable bool `json:"NTPEnable"` + PrimaryNTPServer string `json:"PrimaryNTPServer"` + SecondaryNTPServer string `json:"SecondaryNTPServer"` +} + +func (r *SSupermicroRefishApi) GetNTPConf(ctx context.Context) (redfish.SNTPConf, error) { + ntpConf := redfish.SNTPConf{} + _, resp, err := r.GetResource(ctx, "Managers", "0") + if err != nil { + return ntpConf, errors.Wrap(err, "GetResource") + } + path, err := resp.GetString("Oem", "Supermicro", "NTP", "@odata.id") + if err != nil { + return ntpConf, errors.Wrap(err, "GetString \"Oem\", \"Supermicro\", \"NTP\", \"@odata.id\"") + } + resp, err = r.Get(ctx, path) + if err != nil { + return ntpConf, errors.Wrapf(err, "Get %s fail %s", path, err) + } + conf := sNtpConf{} + err = resp.Unmarshal(&conf) + if err != nil { + return ntpConf, errors.Wrap(err, "resp.Unmarshal NTP") + } + ntpConf.ProtocolEnabled = conf.NTPEnable + if len(conf.PrimaryNTPServer) > 0 { + ntpConf.NTPServers = append(ntpConf.NTPServers, conf.PrimaryNTPServer) + } + if len(conf.SecondaryNTPServer) > 0 { + ntpConf.NTPServers = append(ntpConf.NTPServers, conf.SecondaryNTPServer) + } + return ntpConf, nil +} + +func (r *SSupermicroRefishApi) SetNTPConf(ctx context.Context, conf redfish.SNTPConf) error { + _, resp, err := r.GetResource(ctx, "Managers", "0") + if err != nil { + return errors.Wrap(err, "GetResource") + } + path, err := resp.GetString("Oem", "Supermicro", "NTP", "@odata.id") + if err != nil { + return errors.Wrap(err, "GetString \"Oem\", \"Supermicro\", \"NTP\", \"@odata.id\"") + } + nconf := sNtpConf{ + NTPEnable: conf.ProtocolEnabled, + } + if len(conf.NTPServers) > 0 { + nconf.PrimaryNTPServer = conf.NTPServers[0] + } + if len(conf.NTPServers) > 1 { + nconf.SecondaryNTPServer = conf.NTPServers[1] + } + resp, err = r.Patch(ctx, path, jsonutils.Marshal(nconf)) + if err != nil { + return errors.Wrap(err, "r.Patch") + } + if r.IsDebug { + log.Debugf("%s", resp.PrettyString()) + } + return nil +}