想来想去2021年了,什么都不写也说不过去,可是写吧,又没什么能写的。放假在家技术上没什么可写的,总不能把昨天用冲击钻打瓷砖装浴霸的过程写上来;生活上,每天在学日语、看数学、吹笛子、看Youtube和B站、肝Switch这几件事儿中循环往复。偶尔干点别的事情,无外乎就是跟着别人的教程操作,没什么可以写的。
百无聊赖,最终决定把之前用Rust写的服务器远程开机的工具用Golang重写了一遍。
之前说过有尝试入门Rust,虽然Rust标榜最安全的系统级语言,但我认为在操作系统方面,C/C++的地位是不可动摇的:他们能够直接触及底层,灵活的操作各种硬件。而Rust,在我看来为了安全性(利用所有权机制从源头避免内存泄漏)牺牲了太多的灵活性。早年间试图用Rust写数据结构的实验,结果一个双向链表就给我劝退了。所以我觉得我的Rust之路就可以到此为止了。但是之前已然都用Rust写了一个工具,想来想去还是应该用熟悉的语言重写一遍比较好。
这个工具是之前用来做远程开机的,当时也有发文章:「【代码札记】命令行控制 ASMB8-ikvm 远程管理卡开启服务器」。目前这个服务器跑的是Unraid,以前的文章也说了,床底下的服务器性能虽然强劲,可动静也是真大,因此不能全天候运转(所以想卖了换今年新出的HP MicroServer Gen10 plus,真香啊)。今天想到的办法就是利用挂接在路由器上的树莓派做定时任务,每天早上8点开机,然后在Unraid里配置定时任务,在晚上自动关机。同时为了避免脚本因为系统更新而失效,又在树莓派上部署远程关机的脚本(登录unraid的页面服务器,然后调用网页API关机)。
上一次编译是在树莓派2B上安装RUST工具链,然后上传代码进行编译,当时花了40分钟。今天尝试使用GO的交叉编译。
由于代码量不多,因此就不使用Github了。
package commons
import (
"fmt"
"strings"
)
// ASMB8 auth tokens
type Auth struct {
SessionCookie string
CSRFToken string
}
type HostConfig struct {
Host string `yaml:"host"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type AppConfig struct {
ASMB8 HostConfig `yaml:"asmb8"`
Unraid HostConfig `yaml:"unraid"`
}
var (
Config AppConfig
Action string
Verbose bool
)
// Print only verbose is enabled
func VerbosePrintf(format string, a ...interface{}) {
if Verbose {
_, err := fmt.Printf(format, a...)
if err != nil {
panic("Error when print message: " + err.Error())
}
}
}
// Print only verbose is enabled
func VerbosePrintln(a ...interface{}) {
if Verbose {
_, err := fmt.Println(a...)
if err != nil {
panic("Error when print message: " + err.Error())
}
}
}
// Concat host and url into a url.
// Examples:
// host = 192.168.1.1, url = foo/bar, return = http://192.168.1.1/foo/bar
// host = https://192.168.1.1, url = foo/bar, return = https://192.168.1.1/foo/bar
// host = http://192.168.1.1, url = /foo/bar, return = http://192.168.1.1/foo/bar
// host = http://192.168.1.1/, url = /foo/bar, return = http://192.168.1.1/foo/bar
func ConcatURL(host, url string) string {
result := ""
if !strings.HasPrefix(host, "http") {
result += "http://" + host
} else {
result += host
}
if !strings.HasSuffix(result, "/") {
result += "/"
}
result += strings.TrimPrefix(url, "/")
VerbosePrintf("Concat url: %s\n", result)
return result
}
如上是公用代码,例如全局变量、共享的工具方法之类的。
下面是操作ASMB8面板相关的代码。与Rust版本功能一样,在代码上增加了一些错误处理。
package asmb8
import (
"CocoAdmin/commons"
"errors"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// Do a post request in asmb8 manner
func DoASMB8PostRequest(client *http.Client, auth *commons.Auth, url string, data url.Values) (resp *http.Response, err error) {
// construct post request
req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
// only pass the tokens if exist
if auth != nil {
req.Header.Add("Cookie", "SessionCookie="+auth.SessionCookie)
req.Header.Add("CSRFTOKEN", auth.CSRFToken)
}
// do request
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err = client.Do(req)
return
}
// Do a get request in asmb8 manner
func DoASMB8GetRequest(client *http.Client, auth *commons.Auth, url string) (resp *http.Response, err error) {
// construct get request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// only pass the tokens if exist
if auth != nil {
req.Header.Add("Cookie", "SessionCookie="+auth.SessionCookie)
req.Header.Add("CSRFTOKEN", auth.CSRFToken)
}
// do request
resp, err = client.Do(req)
return
}
// check if the power button is enabled in asmb 8 panel
func CheckPowerButtonEnabled(httpClient *http.Client, auth *commons.Auth) bool {
resp, _ := DoASMB8GetRequest(httpClient, auth, commons.ConcatURL(commons.Config.ASMB8.Host, "/rpc/PWRbutton.asp"))
body, _ := ioutil.ReadAll(resp.Body)
commons.VerbosePrintf("Repsonse: \n%s\n", string(body))
if !strings.Contains(string(body), "PB_STATE") {
panic("Cannot parse server response.")
}
return strings.Contains(strings.Split(strings.Split(string(body), "PB_STATE")[1], "}")[0], "1")
}
// check if the host is powered off
func CheckPowerOff(httpClient *http.Client, auth *commons.Auth) bool {
resp, _ := DoASMB8GetRequest(httpClient, auth, commons.ConcatURL(commons.Config.ASMB8.Host, "/rpc/hoststatus.asp"))
body, _ := ioutil.ReadAll(resp.Body)
commons.VerbosePrintf("Repsonse: \n%s\n", string(body))
if !strings.Contains(string(body), "JF_STATE") {
panic("Cannot parse server response.")
}
return strings.Contains(strings.Split(strings.Split(string(body), "JF_STATE")[1], "}")[0], "0")
}
// check if the host is powered on
func CheckPowerOn(httpClient *http.Client, auth *commons.Auth) bool {
resp, _ := DoASMB8GetRequest(httpClient, auth, commons.ConcatURL(commons.Config.ASMB8.Host, "/rpc/hoststatus.asp"))
body, _ := ioutil.ReadAll(resp.Body)
commons.VerbosePrintf("Repsonse: \n%s\n", string(body))
if !strings.Contains(string(body), "JF_STATE") {
panic("Cannot parse server response.")
}
return strings.Contains(strings.Split(strings.Split(string(body), "JF_STATE")[1], "}")[0], "1")
}
// Do login to asmb 8 panel
func DoLogin(httpClient *http.Client, username, password string) (*commons.Auth, error) {
urlValues := url.Values{}
urlValues.Add("WEBVAR_USERNAME", username)
urlValues.Add("WEBVAR_PASSWORD", password)
resp, err := DoASMB8PostRequest(httpClient, nil, commons.ConcatURL(commons.Config.ASMB8.Host, "/rpc/WEBSES/create.asp"), urlValues)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
commons.VerbosePrintf("Repsonse: \n%s\n", string(body))
if strings.Contains(string(body), "Failure_Login_IPMI_Then_LDAP_then_Active_Directory_Radius") {
return nil, errors.New("login failed")
}
raws := strings.Split(string(body), "'")
if len(raws) <= 11 {
panic("Cannot parse server response.")
}
sessionCookie := raws[3]
csrfToken := raws[11]
return &commons.Auth{
SessionCookie: sessionCookie,
CSRFToken: csrfToken,
}, nil
}
// perform a power on action, no matter whatever condition.
//
// Conditions must be check before invoke this method: PowerBtnEnable and ServerPoweredOff.
//
// Should check is the server started. Whatever by ping os or check from panel.
func DoPowerOn(httpClient *http.Client, auth *commons.Auth) error {
urlValues := url.Values{}
urlValues.Add("WEBVAR_POWER_CMD", "1")
resp, err := DoASMB8PostRequest(httpClient, auth, commons.ConcatURL(commons.Config.ASMB8.Host, "/rpc/hostctl.asp"), urlValues)
if err != nil {
return err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
commons.VerbosePrintf("Repsonse: \n%s\n", string(body))
return nil
}
如下是操作Unraid面板的相关代码,其中为了调用简单,直接将Unraid关机封装成了一个方法(因为关机之前先要从ASMB8面板确认当前是开机状态)。
package unraid
import (
"CocoAdmin/commons"
"errors"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
func DoLoginAndPerformShutdown(client *http.Client) {
csrfToken, err := DoLogin(client, commons.Config.Unraid.Username, commons.Config.Unraid.Password)
if err != nil {
panic("Error when login unraid panel: " + err.Error())
}
commons.VerbosePrintln("CSRF_TOKEN: " + csrfToken)
err = PerformShutdown(client, csrfToken)
if err != nil {
panic("Error when performing shutdown action: " + err.Error())
}
}
// client should have a cookie jar to save cookies. Otherwise you cannot login the unraid panel.
func DoLogin(client *http.Client, username, password string) (string, error) {
if client.Jar == nil {
panic("Http Client has to have a cookie jar to be able to log in.")
}
urlValues := url.Values{}
urlValues.Add("username", username)
urlValues.Add("password", password)
req, err := http.NewRequest("POST", commons.ConcatURL(commons.Config.Unraid.Host, "/login"), strings.NewReader(urlValues.Encode()))
if err != nil {
return "", err
}
// do request
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return "", err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
commons.VerbosePrintf("Repsonse: \n%s\n", string(body))
if !strings.Contains(string(body), "csrf_token=") {
return "", errors.New("login failed: cannot find csrf token")
}
return strings.Split(strings.Split(string(body), "csrf_token=")[1], "';")[0], nil
}
// Perform the shutdown action
func PerformShutdown(client *http.Client, csrfToken string) error {
urlValues := url.Values{}
urlValues.Add("csrf_token", csrfToken)
urlValues.Add("cmd", "shutdown")
req, err := http.NewRequest("POST", commons.ConcatURL(commons.Config.Unraid.Host, "/webGui/include/Boot.php"), strings.NewReader(urlValues.Encode()))
if err != nil {
return err
}
// do request
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
return err
}
commons.VerbosePrintf("Repsonse status code: %d\n", resp.StatusCode)
if resp.StatusCode != 200 {
return errors.New("unraid response not 200")
}
return nil
}
主函数相比Rust版本复杂了一些。Rust版本中将服务器地址、用户名和密码硬编码到了程序里面。这次重写加入了命令行的解析、配置文件的读取和调试开关。
相关依赖如下(go.mod
):
module CocoAdmin
go 1.15
require gopkg.in/yaml.v2 v2.4.0
主函数如下:
package main
import (
"CocoAdmin/asmb8"
"CocoAdmin/commons"
"CocoAdmin/unraid"
"flag"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"strings"
"time"
)
var (
configFilePath string
help bool
initConfig bool
)
func init() {
flag.StringVar(&configFilePath, "config", "./config.yaml", "`path` to config file")
flag.BoolVar(&initConfig, "init", false, "write the initial content to config file")
flag.StringVar(&commons.Action, "action", "", "specific a action to perform: startup or shutdown")
flag.BoolVar(&commons.Verbose, "verbose", false, "enable verbose output")
flag.BoolVar(&help, "help", false, "show this help")
}
func main() {
// parse cli
flag.Parse()
if help {
flag.Usage()
return
}
if initConfig {
data, err := yaml.Marshal(commons.AppConfig{})
if err != nil {
panic("Error when marshal initial data: " + err.Error())
}
if err := ioutil.WriteFile(
configFilePath,
data,
0644,
); err != nil {
panic("Failed writing into config file: " + err.Error())
}
return
}
// reading config file
configFile, err := ioutil.ReadFile(configFilePath)
if err != nil {
panic("Error when reading config file: " + err.Error())
}
if err := yaml.Unmarshal(configFile, &commons.Config); err != nil {
panic("Error when unmarshal config file: " + err.Error())
}
commons.VerbosePrintf("Read config file: %v\n", commons.Config)
// construct http client
jar, err := cookiejar.New(nil)
if err != nil {
panic("Failed initializing cookie jar")
}
httpClient := &http.Client{
Jar: jar,
}
switch strings.ToLower(commons.Action) {
case "startup":
auth, err := asmb8.DoLogin(httpClient, commons.Config.ASMB8.Username, commons.Config.ASMB8.Password)
if err != nil {
panic("Error when doing login: " + err.Error())
}
commons.VerbosePrintf("Fetching asmb8 SESSION_COOKIE: %s\n", auth.SessionCookie)
commons.VerbosePrintf("Fetching asmb8 CSRFTOKEN: %s\n", auth.CSRFToken)
serverPoweredOff := asmb8.CheckPowerOff(httpClient, auth)
commons.VerbosePrintf("Currently ServePoweredOff: %t\n", serverPoweredOff)
if serverPoweredOff {
// check power button status
powerBtnEnabled := asmb8.CheckPowerButtonEnabled(httpClient, auth)
commons.VerbosePrintf("PowerButtonEnable: %t\n", powerBtnEnabled)
if !powerBtnEnabled {
panic("Power Button Control is disabled in ASMB8 panel. Cannot remoter control server's power.")
}
// power on
fmt.Println("Trying to power on server...")
err = asmb8.DoPowerOn(httpClient, auth)
if err != nil {
panic("Error when performing power on: " + err.Error())
}
// checking power status
fmt.Println("Checking power status...")
i := 1
for ; i <= 5; i++ {
status := asmb8.CheckPowerOn(httpClient, auth)
fmt.Printf("Attemp %d. IsPoweredOn: %t\n", i, status)
if status {
break
}
time.Sleep(10 * time.Second)
}
if i > 5 {
fmt.Println("ASMB8 response ok, but still report power off.")
} else {
fmt.Println("Server should be powered on.")
}
} else {
panic("Server has already powered on.")
}
case "shutdown":
auth, err := asmb8.DoLogin(httpClient, commons.Config.Unraid.Username, commons.Config.Unraid.Password)
if err != nil {
panic("Error when doing login: " + err.Error())
}
commons.VerbosePrintf("Fetching asmb8 SESSION_COOKIE: %s\n", auth.SessionCookie)
commons.VerbosePrintf("Fetching asmb8 CSRFTOKEN: %s\n", auth.CSRFToken)
serverPoweredOff := asmb8.CheckPowerOff(httpClient, auth)
commons.VerbosePrintf("Currently ServePoweredOff: %t\n", serverPoweredOff)
if !serverPoweredOff {
fmt.Println("Trying to shutdown server through unraid panel...")
unraid.DoLoginAndPerformShutdown(httpClient)
fmt.Println("Server should be shutdown.")
} else {
panic("Server has been already shutdown.")
}
default:
commons.VerbosePrintln("Unknown action: " + commons.Action)
flag.Usage()
}
}
最后交叉编译。我这里是Windows环境,因此需要先设置交叉编译选项的环境变量:
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=arm
对于Arm架构的交叉编译,需要注意的是树莓派现行的操作系统都是32位的,所以GOARCH
都是arm
,如果是树莓派3B和4B运行的64位系统,那么应该使用arm64
并且忽略下面的GOARM
选项。
对于GOARCH-arm
,还有个GOARM
的选项,对于树莓派来说,设置GOARM=7
即可。
最后编译:
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=arm
SET GOARM=7
go build
部署在树莓派上:
*/20 8,9 * * * /usr/local/bin/cocoAdmin -config /usr/local/etc/cocoAdmin/config.json -action startup
*/20 22,23 * * * /usr/local/bin/cocoAdmin -config /usr/local/etc/cocoAdmin/config.json -action shutdown
事先需要生成好配置文件:
/usr/local/bin/cocoAdmin -config /usr/local/etc/cocoAdmin/config.json -init
-全文完-
【代码札记】Golang项目实践 由 天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。
冲击钻打瓷砖装浴霸可以的,是自己手操的嘛?我还没见过,你挂个B站视频,我马上给你+1s,啊不,+1播放量。
倒是我自己操作的,不过家用的那种两用钻,标称570W,看着和手电钻差不多,没啥新鲜新的。就是冲击钻有点震手