From 6ea402b576d03840fc59cd5740b5865fe829d85f Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Thu, 3 Sep 2020 02:43:54 +0800 Subject: [PATCH] fix: compatiblity fixes with lenovo RD620 and huawei 2288 --- cmd/raidcli/main.go | 167 ++++++++++++++++++++ cmd/raidcli/shell/raid.go | 37 +++++ cmd/redfishcli/main.go | 55 ++++++- pkg/baremetal/manager.go | 2 + pkg/baremetal/utils/raid/megactl/megactl.go | 47 ++++-- pkg/baremetal/utils/raid/raid.go | 4 + pkg/compute/tasks/baremetal_delete_task.go | 7 +- pkg/util/redfish/bmconsole/lenovo.go | 90 +++++++++++ pkg/util/redfish/bmconsole/supermicro.go | 13 +- 9 files changed, 396 insertions(+), 26 deletions(-) create mode 100644 cmd/raidcli/main.go create mode 100644 cmd/raidcli/shell/raid.go create mode 100644 pkg/util/redfish/bmconsole/lenovo.go diff --git a/cmd/raidcli/main.go b/cmd/raidcli/main.go new file mode 100644 index 0000000000..105d8d2284 --- /dev/null +++ b/cmd/raidcli/main.go @@ -0,0 +1,167 @@ +// 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 main + +import ( + "fmt" + "os" + + "yunion.io/x/structarg" + + _ "yunion.io/x/onecloud/cmd/raidcli/shell" + "yunion.io/x/onecloud/pkg/baremetal/utils/raid" + "yunion.io/x/onecloud/pkg/baremetal/utils/raid/drivers" + "yunion.io/x/onecloud/pkg/util/shellutils" + "yunion.io/x/onecloud/pkg/util/ssh" +) + +type BaseOptions struct { + Debug bool `help:"debug mode"` + Help bool `help:"Show help"` + Host string `help:"SSH Host IP" default:"$RAID_HOST" metavar:"RAID_HOST"` + Username string `help:"Username, usually root" default:"$RAID_USERNAME" metavar:"RAID_USERNAME"` + Password string `help:"Password" default:"$RAID_PASSWORD" metavar:"RAID_PASSWORD"` + Driver string `help:"Password" default:"$RAID_DRIVER" metavar:"RAID_DRIVER" choices:"MegaRaid|HPSARaid|Mpt2SAS|MarvelRaid"` + SUBCOMMAND string `help:"s3cli subcommand" subcommand:"true"` +} + +var ( + options = &BaseOptions{} +) + +func getSubcommandParser() (*structarg.ArgumentParser, error) { + parse, e := structarg.NewArgumentParser(options, + "raidcli", + "Command-line interface to test RAID drivers.", + `See "raidcli help COMMAND" for help on a specific command.`) + + if e != nil { + return nil, e + } + + subcmd := parse.GetSubcommand() + if subcmd == nil { + return nil, fmt.Errorf("No subcommand argument.") + } + type HelpOptions struct { + SUBCOMMAND string `help:"sub-command name"` + } + shellutils.R(&HelpOptions{}, "help", "Show help of a subcommand", func(args *HelpOptions) error { + helpstr, e := subcmd.SubHelpString(args.SUBCOMMAND) + if e != nil { + return e + } else { + fmt.Print(helpstr) + return nil + } + }) + for _, v := range shellutils.CommandTable { + _, e := subcmd.AddSubParser(v.Options, v.Command, v.Desc, v.Callback) + if e != nil { + return nil, e + } + } + return parse, nil +} + +func showErrorAndExit(e error) { + fmt.Fprintf(os.Stderr, "%s", e) + fmt.Fprintln(os.Stderr) + os.Exit(1) +} + +func newClient() (raid.IRaidDriver, error) { + if len(options.Host) == 0 { + return nil, fmt.Errorf("Missing host") + } + + if len(options.Username) == 0 { + return nil, fmt.Errorf("Missing username") + } + + if len(options.Password) == 0 { + return nil, fmt.Errorf("Missing password") + } + + if len(options.Driver) == 0 { + return nil, fmt.Errorf("Missing driver") + } + + if options.Debug { + raid.Debug = true + } + + sshClient, err := ssh.NewClient( + options.Host, + 22, + options.Username, + options.Password, + "", + ) + if err != nil { + return nil, fmt.Errorf("ssh client init fail: %s", err) + } + + drv := drivers.GetDriver(options.Driver, sshClient) + if drv == nil { + return nil, fmt.Errorf("not supported driver %s", options.Driver) + } + + err = drv.ParsePhyDevs() + if err != nil { + return nil, fmt.Errorf("parse phyical devices error %s", err) + } + + return drv, nil +} + +func main() { + parser, e := getSubcommandParser() + if e != nil { + showErrorAndExit(e) + } + e = parser.ParseArgs(os.Args[1:], false) + // options := parser.Options().(*BaseOptions) + + if options.Help { + fmt.Print(parser.HelpString()) + } else { + subcmd := parser.GetSubcommand() + subparser := subcmd.GetSubParser() + if e != nil { + if subparser != nil { + fmt.Print(subparser.Usage()) + } else { + fmt.Print(parser.Usage()) + } + showErrorAndExit(e) + } else { + suboptions := subparser.Options() + if options.SUBCOMMAND == "help" { + e = subcmd.Invoke(suboptions) + } else { + var client raid.IRaidDriver + client, e = newClient() + if e != nil { + showErrorAndExit(e) + } + e = subcmd.Invoke(client, suboptions) + } + if e != nil { + showErrorAndExit(e) + } + } + } +} diff --git a/cmd/raidcli/shell/raid.go b/cmd/raidcli/shell/raid.go new file mode 100644 index 0000000000..7cf7266f32 --- /dev/null +++ b/cmd/raidcli/shell/raid.go @@ -0,0 +1,37 @@ +// 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 shell + +import ( + "fmt" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/baremetal/utils/raid" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type ShowOptions struct { + } + shellutils.R(&ShowOptions{}, "show", "show raid info", func(drv raid.IRaidDriver, args *ShowOptions) error { + adpts := drv.GetAdapters() + for i := range adpts { + fmt.Println("Index:", i) + fmt.Println(jsonutils.Marshal(adpts[i].GetDevices()).PrettyString()) + } + return nil + }) +} diff --git a/cmd/redfishcli/main.go b/cmd/redfishcli/main.go index 15922a915b..d7cce7a29b 100644 --- a/cmd/redfishcli/main.go +++ b/cmd/redfishcli/main.go @@ -17,12 +17,16 @@ package main import ( "context" "fmt" + "net/url" "os" + "strings" "yunion.io/x/structarg" _ "yunion.io/x/onecloud/cmd/redfishcli/shell" + "yunion.io/x/onecloud/pkg/util/fileutils2" "yunion.io/x/onecloud/pkg/util/redfish" + "yunion.io/x/onecloud/pkg/util/redfish/bmconsole" _ "yunion.io/x/onecloud/pkg/util/redfish/loader" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -36,8 +40,12 @@ type BaseOptions struct { SUBCOMMAND string `help:"s3cli subcommand" subcommand:"true"` } +var ( + options = &BaseOptions{} +) + func getSubcommandParser() (*structarg.ArgumentParser, error) { - parse, e := structarg.NewArgumentParser(&BaseOptions{}, + parse, e := structarg.NewArgumentParser(options, "redfishcli", "Command-line interface to redfish API.", `See "redfishcli help COMMAND" for help on a specific command.`) @@ -62,6 +70,7 @@ func getSubcommandParser() (*structarg.ArgumentParser, error) { return nil } }) + bmcJnlp() for _, v := range shellutils.CommandTable { _, e := subcmd.AddSubParser(v.Options, v.Command, v.Desc, v.Callback) if e != nil { @@ -71,13 +80,49 @@ func getSubcommandParser() (*structarg.ArgumentParser, error) { return parse, nil } +func bmcJnlp() { + type BmcGetOptions struct { + BRAND string `help:"brand of baremetal" choices:"Lenovo|Huawei|HPE|Dell|Supermicro"` + Save string `help:"save to file"` + Debug bool `help:"turn on debug mode"` + } + shellutils.R(&BmcGetOptions{}, "bmc-jnlp", "Get Java Console JNLP file", func(args *BmcGetOptions) error { + ctx := context.Background() + parts, err := url.Parse(options.Endpoint) + if err != nil { + return err + } + bmc := bmconsole.NewBMCConsole(parts.Hostname(), options.Username, options.Password, args.Debug) + var jnlp string + switch strings.ToLower(args.BRAND) { + case "hp", "hpe": + jnlp, err = bmc.GetIloConsoleJNLP(ctx) + case "dell", "dell inc.": + jnlp, err = bmc.GetIdracConsoleJNLP(ctx, "", "") + case "supermicro": + jnlp, err = bmc.GetSupermicroConsoleJNLP(ctx) + case "lenovo": + jnlp, err = bmc.GetLenovoConsoleJNLP(ctx) + } + if err != nil { + return err + } + if len(args.Save) > 0 { + return fileutils2.FilePutContents(args.Save, jnlp, false) + } else { + fmt.Println(jnlp) + return nil + } + }) +} + func showErrorAndExit(e error) { fmt.Fprintf(os.Stderr, "%s", e) fmt.Fprintln(os.Stderr) os.Exit(1) } -func newClient(options *BaseOptions) (redfish.IRedfishDriver, error) { +func newClient() (redfish.IRedfishDriver, error) { if len(options.Endpoint) == 0 { return nil, fmt.Errorf("Missing endpoint") } @@ -104,7 +149,7 @@ func main() { showErrorAndExit(e) } e = parser.ParseArgs(os.Args[1:], false) - options := parser.Options().(*BaseOptions) + // options := parser.Options().(*BaseOptions) if options.Help { fmt.Print(parser.HelpString()) @@ -122,9 +167,11 @@ func main() { suboptions := subparser.Options() if options.SUBCOMMAND == "help" { e = subcmd.Invoke(suboptions) + } else if options.SUBCOMMAND == "bmc-jnlp" { + e = subcmd.Invoke(suboptions) } else { var client redfish.IRedfishDriver - client, e = newClient(options) + client, e = newClient() if e != nil { showErrorAndExit(e) } diff --git a/pkg/baremetal/manager.go b/pkg/baremetal/manager.go index 6a64af2d6d..a179347146 100644 --- a/pkg/baremetal/manager.go +++ b/pkg/baremetal/manager.go @@ -1953,6 +1953,8 @@ func (b *SBaremetalInstance) GetConsoleJNLP(ctx context.Context) (string, error) return bmc.GetIdracConsoleJNLP(ctx, "", "") case "supermicro": return bmc.GetSupermicroConsoleJNLP(ctx) + case "lenovo": + return bmc.GetLenovoConsoleJNLP(ctx) } return "", httperrors.NewNotImplementedError("Unsupported manufacture %s", manufacture) } diff --git a/pkg/baremetal/utils/raid/megactl/megactl.go b/pkg/baremetal/utils/raid/megactl/megactl.go index 9c12aba35b..96be6374ef 100644 --- a/pkg/baremetal/utils/raid/megactl/megactl.go +++ b/pkg/baremetal/utils/raid/megactl/megactl.go @@ -20,9 +20,8 @@ import ( "strconv" "strings" - "github.com/pkg/errors" - "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" @@ -75,7 +74,7 @@ func (dev *MegaRaidPhyDev) ToBaremetalStorage(index int) *baremetal.BaremetalSto } func (dev *MegaRaidPhyDev) GetSize() int64 { - return dev.sector * int64(dev.block) / 1024 / 1024 // MB + return dev.sector * dev.block / 1024 / 1024 // MB } func (dev *MegaRaidPhyDev) parseLine(line string) bool { @@ -124,7 +123,7 @@ func (dev *MegaRaidPhyDev) parseLine(line string) bool { if err != nil { log.Errorf("parse logical sector size error: %v", err) dev.block = 512 - } else { + } else if block > 0 { dev.block = int64(block) } default: @@ -189,6 +188,7 @@ type MegaRaidAdaptor struct { raid *MegaRaid devs []*MegaRaidPhyDev sn string + name string busNumber string deviceNumber string funcNumber string @@ -209,6 +209,10 @@ func NewMegaRaidAdaptor(index int, raid *MegaRaid) (*MegaRaidAdaptor, error) { return adapter, nil } +func (adapter MegaRaidAdaptor) key() string { + return adapter.name + adapter.sn +} + func (adapter *MegaRaidAdaptor) fillInfo() error { cmd := GetCommand("-AdpAllInfo", fmt.Sprintf("-a%d", adapter.index)) ret, err := adapter.remoteRun(cmd) @@ -223,10 +227,12 @@ func (adapter *MegaRaidAdaptor) fillInfo() error { switch key { case "Serial No": adapter.sn = val + case "Product Name": + adapter.name = val } } - if len(adapter.sn) == 0 { - return errors.New("Not found Serial No") + if len(adapter.key()) == 0 { + return errors.Error("Not found Serial No and Product Name") } return adapter.fillPCIInfo() } @@ -267,7 +273,7 @@ func (adapter *MegaRaidAdaptor) fillPCIInfo() error { } } if len(adapter.busNumber) == 0 || len(adapter.deviceNumber) == 0 || len(adapter.funcNumber) == 0 { - return errors.New("Not found bus number") + return errors.Error("Not found bus number") } pciDir := fmt.Sprintf("/sys/bus/pci/devices/0000:%s:%s.%s/", adapter.busNumber, adapter.deviceNumber, adapter.funcNumber) cmd = raiddrivers.GetCommand("ls", pciDir, "|", "grep", "host") @@ -601,18 +607,24 @@ func (adapter *MegaRaidAdaptor) BuildNoneRaid(devs []*baremetal.BaremetalStorage type StorcliAdaptor struct { Controller int - SN string + sn string + name string } func newStorcliAdaptor() *StorcliAdaptor { return &StorcliAdaptor{ Controller: -1, - SN: "", + sn: "", + name: "", } } +func (a StorcliAdaptor) key() string { + return a.name + a.sn +} + func (a *StorcliAdaptor) isComplete() bool { - return a.Controller >= 0 && a.SN != "" + return a.Controller >= 0 && a.key() != "" } func (a *StorcliAdaptor) parseLine(l string) { @@ -626,13 +638,15 @@ func (a *StorcliAdaptor) parseLine(l string) { case "Controller": a.Controller, _ = strconv.Atoi(val) case "Serial Number": - a.SN = val + a.sn = val + case "Product Name": + a.name = val } } func (raid *MegaRaid) GetStorcliAdaptor() (map[string]*StorcliAdaptor, error) { ret := make(map[string]*StorcliAdaptor) - cmd := GetCommand2("/call", "show", "|", "grep", "-iE", `'^(Controller|Serial Number)\s='`) + cmd := GetCommand2("/call", "show", "|", "grep", "-iE", `'^(Controller|Product Name|Serial Number)\s='`) lines, err := raid.term.Run(cmd) if err != nil { return nil, errors.Wrap(err, "Get storcli adapter") @@ -641,7 +655,7 @@ func (raid *MegaRaid) GetStorcliAdaptor() (map[string]*StorcliAdaptor, error) { for _, l := range lines { adapter.parseLine(l) if adapter.isComplete() { - ret[adapter.SN] = adapter + ret[adapter.key()] = adapter adapter = newStorcliAdaptor() } } @@ -656,9 +670,9 @@ func (adapter *MegaRaidAdaptor) storcliCtrlIndex() (int, error) { if err != nil { return -1, errors.Wrap(err, "Get all Storcli adaptor") } - storAdap, ok := storcliAdaps[adapter.sn] + storAdap, ok := storcliAdaps[adapter.key()] if !ok { - return -1, errors.Errorf("Not found storcli adaptor by SN %q", adapter.sn) + return -1, errors.Errorf("Not found storcli adaptor by SN %q", adapter.key()) } return storAdap.Controller, nil } @@ -914,6 +928,9 @@ func (raid *MegaRaid) ParsePhyDevs() error { if err != nil { return fmt.Errorf("List raid disk error: %v", err) } + if raiddrivers.Debug { + log.Debugf("-PDList -aALL: %s", ret) + } err = raid.parsePhyDevs(ret) if err != nil { return fmt.Errorf("parse physical disk device error: %v", err) diff --git a/pkg/baremetal/utils/raid/raid.go b/pkg/baremetal/utils/raid/raid.go index 2a7bc00a8c..06d7f3842b 100644 --- a/pkg/baremetal/utils/raid/raid.go +++ b/pkg/baremetal/utils/raid/raid.go @@ -28,6 +28,10 @@ import ( "yunion.io/x/onecloud/pkg/util/sysutils" ) +var ( + Debug bool +) + const ( MODULE_MEGARAID = "megaraid_sas" MODULE_HPSA = "hpsa" diff --git a/pkg/compute/tasks/baremetal_delete_task.go b/pkg/compute/tasks/baremetal_delete_task.go index 1411954f68..a597a8f1d2 100644 --- a/pkg/compute/tasks/baremetal_delete_task.go +++ b/pkg/compute/tasks/baremetal_delete_task.go @@ -58,7 +58,12 @@ func (self *BaremetalDeleteTask) OnInit(ctx context.Context, obj db.IStandaloneM } func (self *BaremetalDeleteTask) OnDeleteBaremetalComplete(ctx context.Context, baremetal *models.SHost, body jsonutils.JSONObject) { - baremetal.RealDelete(ctx, self.UserCred) + err := baremetal.RealDelete(ctx, self.UserCred) + if err != nil { + log.Errorln("RealDelete fail %s", err) + self.OnFailure(ctx, baremetal, jsonutils.Marshal(err)) + return + } self.SetStageComplete(ctx, nil) } diff --git a/pkg/util/redfish/bmconsole/lenovo.go b/pkg/util/redfish/bmconsole/lenovo.go new file mode 100644 index 0000000000..974f7b8920 --- /dev/null +++ b/pkg/util/redfish/bmconsole/lenovo.go @@ -0,0 +1,90 @@ +// 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" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/httputils" +) + +func (r *SBMCConsole) GetLenovoConsoleJNLP(ctx context.Context) (string, error) { + loginData := strings.Join([]string{ + "user=" + url.QueryEscape(r.username), + "password=" + url.QueryEscape(r.password), + }, "&") + + // cookie: + // _appwebSessionId_=09eb9a178d520d2c9fa1430dd355dc27; path=/; httponly; secure + + cookies := make(map[string]string) + cookies["_appwebSessionId_"] = "" + + // first do html login + postHdr := http.Header{} + postHdr.Set("Content-Type", "application/x-www-form-urlencoded") + postHdr.Set("Referer", fmt.Sprintf("https://%s/login.html", r.host)) + setCookieHeader(postHdr, cookies) + hdr, _, err := r.RawRequest(ctx, httputils.POST, "/data/login", 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{} + getHdr.Set("Referer", fmt.Sprintf("https://%s/bmctree.html", r.host)) + setCookieHeader(getHdr, cookies) + _, launchResp, err := r.RawRequest(ctx, httputils.GET, "/vkvmLaunch.html", getHdr, nil) + if err != nil { + return "", errors.Wrap(err, "Get vkvmLauch.html") + } + + var token string + st3Pattern := regexp.MustCompile(``) + matched := st3Pattern.FindAllStringSubmatch(string(launchResp), -1) + if len(matched) > 0 && len(matched[0]) > 1 { + token = matched[0][1] + } + + if len(token) == 0 { + return "", errors.Wrap(httperrors.ErrBadRequest, "no valid ST3 token") + } + + path := fmt.Sprintf("viewer.jnlp(%s@0@%d)", r.host, time.Now().UnixNano()/1000000) + body := "ST3=" + url.QueryEscape(token) + getHdr.Set("Referer", fmt.Sprintf("https://%s/vkvmLaunch.html", r.host)) + _, rspBody, err := r.RawRequest(ctx, httputils.POST, path, getHdr, []byte(body)) + if err != nil { + return "", errors.Wrapf(err, "r.RawGet %s", path) + } + return string(rspBody), nil +} diff --git a/pkg/util/redfish/bmconsole/supermicro.go b/pkg/util/redfish/bmconsole/supermicro.go index d7b9b33495..4c64acdd2f 100644 --- a/pkg/util/redfish/bmconsole/supermicro.go +++ b/pkg/util/redfish/bmconsole/supermicro.go @@ -16,14 +16,12 @@ package bmconsole import ( "context" + "fmt" "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" ) @@ -44,6 +42,7 @@ func (r *SBMCConsole) GetSupermicroConsoleJNLP(ctx context.Context) (string, err // first do html login postHdr := http.Header{} postHdr.Set("Content-Type", "application/x-www-form-urlencoded") + postHdr.Set("Referer", fmt.Sprintf("http://%s/", r.host)) setCookieHeader(postHdr, cookies) hdr, _, err := r.RawRequest(ctx, httputils.POST, "/cgi/login.cgi", postHdr, []byte(loginData)) if err != nil { @@ -60,11 +59,12 @@ func (r *SBMCConsole) GetSupermicroConsoleJNLP(ctx context.Context) (string, err } getHdr := http.Header{} + getHdr.Set("Referer", fmt.Sprintf("https://%s/cgi/url_redirect.cgi?url_name=man_ikvm", r.host)) setCookieHeader(getHdr, cookies) - now := time.Now() + // 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{ + /*loginData = strings.Join([]string{ "fwtype=255", "time_stamp=" + url.QueryEscape(timeutils.RFC2882Time(now)), "_=", @@ -78,8 +78,9 @@ func (r *SBMCConsole) GetSupermicroConsoleJNLP(ctx context.Context) (string, err 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) + _, 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") }