diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..95d5bd78de01b48c1dac1bf08560a663b901105d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.iml +/.idea +/bin/ +/manifest.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..76dad46bbc7e776d92daa77d92451f8464adb605 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +## Usage + +Invoke binary (hetzner-robot-automation) with a config file as parameter. +That config file should contain the API keys, server id, and list of files to upload and/or execute. + +The binary then reboots the specified server into rescue mode, copies the specified files over, and executes them if +specified. + +The config should comply with the JSON schema and look similar to + +```json +{ + "api-key": "AzureDiamond", + "api-secret": "hunter2", + "server": 123456, + "files": [ + { + "source": "config/ignition.ign", + "target": "/root/ignition.ign", + "mode": 644, + "execute": false + }, + { + "source": "install.sh", + "target": "/root/install_rescue.sh", + "mode": 755, + "execute": true + } + ] +} +``` \ No newline at end of file diff --git a/cli/deploy.go b/cli/deploy.go new file mode 100644 index 0000000000000000000000000000000000000000..0cac6dc2824df5f9309e31e96bc4f3c8868f0ffe --- /dev/null +++ b/cli/deploy.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "golang.org/x/crypto/ssh" + "io" + "log" +) + +func isExitMissingError(err error) bool { + _, isExitMissingError := err.(*ssh.ExitMissingError) + return isExitMissingError +} + +func deploy(manifest Manifest) error { + connectionData, err := provideConnection(manifest) + if err != nil { + return fmt.Errorf("error while obtaining connection data: %w", err) + } + sshClient, err := openSshClient(connectionData) + if err != nil { + return fmt.Errorf("error while opening ssh client: %w", err) + } + defer sshClient.Close() + + for _, file := range manifest.Files { + sshSession, err := openSshSession(sshClient) + if err != nil { + return fmt.Errorf("error while opening ssh session: %w", err) + } + log.Printf("copying file %s to %s with mode 0%d \n", file.Source, file.Target, file.Mode) + if err := copyFile(sshSession, file); err != nil && !isExitMissingError(err) { + _ = sshSession.Close() + return fmt.Errorf("error while copying file %s: %w", file.Target, err) + } + if err := sshSession.Close(); err != nil && err != io.EOF { + return fmt.Errorf("error while closing ssh session: %w", err) + } + } + + for _, file := range manifest.Files { + if file.Execute { + sshSession, err := openSshSession(sshClient) + if err != nil { + return fmt.Errorf("error while opening ssh session: %w", err) + } + log.Printf("running command %s\n", file.Target) + if err := sshSession.Run(file.Target); err != nil && !isExitMissingError(err) { + _ = sshSession.Close() + return fmt.Errorf("error while running command: %w", err) + } + if err := sshSession.Close(); err != nil && err != io.EOF { + return fmt.Errorf("error while closing ssh session: %w", err) + } + } + } + + return nil +} diff --git a/cli/hetzner.go b/cli/hetzner.go new file mode 100644 index 0000000000000000000000000000000000000000..15bbcde75954da9867843b739516c6410d1dd974 --- /dev/null +++ b/cli/hetzner.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "hetzner-robot/client" + "hetzner-robot/models" + "log" +) + +func enableRescue(robotClient client.RobotClient, server models.ServerNumber) (*SshConnection, error) { + oldRescueInfo, err := robotClient.BootRescueGetLast(server) + if err != nil { + return nil, fmt.Errorf("error while retrieving rescue status: %w", err) + } + if oldRescueInfo.Active { + log.Println("rescue system already active, deactivating") + if err := robotClient.BootRescueDelete(server); err != nil { + return nil, fmt.Errorf("error while deactivating rescue system: %w", err) + } + } + + log.Println("activating rescue system") + rescueInfo, err := robotClient.BootRescueSet(server, &models.RescueSetInput{ + OS: "linux", + }) + if err != nil { + return nil, fmt.Errorf("error while setting server rescue properties: %w", err) + } + + return &SshConnection{ + Username: "root", + Password: rescueInfo.Password, + Hostname: rescueInfo.ServerIP, + Port: 22, + }, nil +} + +func provideConnection(manifest Manifest) (*SshConnection, error) { + robotClient := client.NewBasicAuthClient(manifest.ApiKey, manifest.ApiSecret) + + server, err := robotClient.ServerGet(models.ServerNumber(manifest.Server)) + if err != nil { + return nil, fmt.Errorf("error while retrieving server: %w", err) + } + connectionData, err := enableRescue(robotClient, server.ServerNumber) + if err != nil { + return nil, fmt.Errorf("error deploying configuration to server: %w", err) + } + log.Println("restarting server") + _, err = robotClient.ResetSet(server.ServerNumber, &models.ResetSetInput{ + Type: models.ResetTypeHardware, + }) + if err != nil { + return nil, fmt.Errorf("error restarting server: %w", err) + } + return connectionData, nil +} diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000000000000000000000000000000000000..09312526ff804dff58d92f671e5b9b5d1e90e3fc --- /dev/null +++ b/cli/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "path/filepath" +) + +type Manifest struct { + ApiKey string `json:"api-key"` + ApiSecret string `json:"api-secret"` + Server int `json:"server"` + Files []CopyFile `json:"files"` +} + +type CopyFile struct { + Source string `json:"source"` + Target string `json:"target"` + Mode uint `json:"mode"` + Execute bool `json:"execute"` +} + +func (manifest *Manifest) resolvePaths(root string) { + for i, file := range manifest.Files { + if !filepath.IsAbs(file.Source) { + newPath := filepath.Join(root, file.Source) + manifest.Files[i].Source = newPath + } + } +} + +func loadManifest(filename string) (Manifest, error) { + var manifest Manifest + manifestPath, err := filepath.Abs(filename) + if err != nil { + return manifest, fmt.Errorf("could not find absolute path for file") + } + manifestFile, err := os.Open(manifestPath) + if err != nil { + return manifest, fmt.Errorf("error opening manifest: %w", err) + } + if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil { + return manifest, fmt.Errorf("error loading manifest: %w", err) + } + manifest.resolvePaths(filepath.Dir(manifestPath)) + return manifest, nil +} + +func main() { + flag.Parse() + for _, arg := range flag.Args() { + log.Printf("loading manifest %s", arg) + manifest, err := loadManifest(arg) + if err != nil { + log.Fatalf(err.Error()) + } + err = deploy(manifest) + if err != nil { + log.Fatalf(err.Error()) + } + } +} diff --git a/cli/ssh.go b/cli/ssh.go new file mode 100644 index 0000000000000000000000000000000000000000..27c2138f56ca5c45e358b2a8cbdc5216709db004 --- /dev/null +++ b/cli/ssh.go @@ -0,0 +1,140 @@ +package main + +import ( + "bufio" + "fmt" + "golang.org/x/crypto/ssh" + "golang.org/x/sync/errgroup" + "io" + "log" + "os" + "path" + "time" +) + +type SshConnection struct { + Username string + Password string + Hostname string + Port int + Timeout time.Duration +} + +func openSshClient(connection *SshConnection) (*ssh.Client, error) { + conf := &ssh.ClientConfig{ + User: connection.Username, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Auth: []ssh.AuthMethod{ + ssh.Password(connection.Password), + }, + Timeout: connection.Timeout, + } + + log.Println("opening ssh connection") + var conn *ssh.Client + for attempt := 0; attempt < 20; attempt++ { + if conn != nil { + if err := conn.Close(); err != nil { + return nil, fmt.Errorf("could not connect to server via ssh %w", err) + } + } + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", connection.Hostname, connection.Port), conf) + if err == nil { + return conn, nil + } + log.Println("waiting on ssh session") + time.Sleep(5 * time.Second) + } + return nil, fmt.Errorf("could not connect to server via ssh") +} + +func openSshSession(conn *ssh.Client) (*ssh.Session, error) { + log.Println("opening ssh session") + var session *ssh.Session + session, err := conn.NewSession() + if err != nil { + return nil, fmt.Errorf("error opening ssh session %w", err) + } + + log.Println("connecting ssh outputs to stdout") + if err := connectShellOutputs(session); err != nil { + return nil, fmt.Errorf("error connecting ssh outputs to stdout %w", err) + } + + return session, nil +} + +func copyFile(session *ssh.Session, file CopyFile) error { + handle, err := os.Open(file.Source) + if err != nil { + return err + } + defer handle.Close() + + stat, err := handle.Stat() + if err != nil { + return err + } + + filedir, filename := path.Split(file.Target) + + group := errgroup.Group{} + group.Go(func() error { + stdin, _ := session.StdinPipe() + defer stdin.Close() + if _, err := fmt.Fprintf(stdin, "C0%d %d %s\n", file.Mode, stat.Size(), filename); err != nil { + return fmt.Errorf("error sending file: %w", err) + } + if _, err := io.Copy(stdin, handle); err != nil { + return fmt.Errorf("error sending file: %w", err) + } + if _, err := fmt.Fprint(stdin, "\x00"); err != nil { + return fmt.Errorf("error sending file: %w", err) + } + return nil + }) + group.Go(func() error { + if err := session.Run(fmt.Sprintf("/usr/bin/scp -t %s", filedir)); err != nil { + return fmt.Errorf("error receiving file: %w", err) + } + return nil + }) + return group.Wait() +} + +func connectReader(label string, reader io.Reader) { + scanner := bufio.NewScanner(reader) + for { + if tkn := scanner.Scan(); tkn { + rcv := scanner.Bytes() + raw := make([]byte, len(rcv)) + copy(raw, rcv) + log.Printf("%s: %s\n", label, string(raw)) + } else { + if scanner.Err() != nil { + log.Printf("%s error: %s\n", label, scanner.Err()) + } + return + } + } +} + +func connectShellOutputs(session *ssh.Session) error { + var stdout, stderr io.Reader + var err error + + stdout, err = session.StdoutPipe() + if err != nil { + return err + } + + stderr, err = session.StderrPipe() + if err != nil { + return err + } + + go connectReader("ssh stdout", stdout) + go connectReader("ssh stderr", stderr) + + return nil +} diff --git a/client/boot.go b/client/boot.go new file mode 100644 index 0000000000000000000000000000000000000000..c37f834acbe28a3d5446e75c167defeb588f16cb --- /dev/null +++ b/client/boot.go @@ -0,0 +1,82 @@ +package client + +import ( + "encoding/json" + "fmt" + "hetzner-robot/models" + "net/http" + neturl "net/url" + "strconv" +) + +func (c *Client) BootRescueGet(server models.ServerNumber) (*models.RescueInfo, error) { + url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue", int(server)) + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var rescueResp models.RescueInfoGetResponse + err = json.Unmarshal(bytes, &rescueResp) + if err != nil { + return nil, err + } + + return &rescueResp.Rescue, nil +} + +func (c *Client) BootRescueGetLast(server models.ServerNumber) (*models.Rescue, error) { + url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue/last", int(server)) + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var rescueResp models.RescueGetResponse + err = json.Unmarshal(bytes, &rescueResp) + if err != nil { + return nil, err + } + + return &rescueResp.Rescue, nil +} + +func (c *Client) BootRescueSet(server models.ServerNumber, input *models.RescueSetInput) (*models.Rescue, error) { + url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue", int(server)) + + formData := neturl.Values{} + formData.Set("os", input.OS) + if input.Arch > 0 { + formData.Set("arch", strconv.Itoa(input.Arch)) + } + if len(input.AuthorizedKeys) > 0 { + for _, authorizedKey := range input.AuthorizedKeys { + formData.Add("authorized_key[]", authorizedKey.Fingerprint) + } + } + + bytes, err := c.doPostFormRequest(url, formData) + if err != nil { + return nil, err + } + + var rescueResp models.RescueGetResponse + err = json.Unmarshal(bytes, &rescueResp) + if err != nil { + return nil, err + } + + return &rescueResp.Rescue, nil +} + +func (c *Client) BootRescueDelete(server models.ServerNumber) error { + url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue", int(server)) + + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + _, err = c.doRequest(req) + return err +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000000000000000000000000000000000000..334022a4a2b7d68dd5ca6e8f980f61302787cebe --- /dev/null +++ b/client/client.go @@ -0,0 +1,89 @@ +package client + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +const baseURL string = "https://robot-ws.your-server.de" +const version = "0.1.3" +const userAgent = "hrobot-client/" + version + +type Client struct { + Username string + Password string + baseURL string + userAgent string +} + +func NewBasicAuthClient(username, password string) RobotClient { + return &Client{ + Username: username, + Password: password, + baseURL: baseURL, + userAgent: userAgent, + } +} + +func (c *Client) SetBaseURL(baseURL string) { + c.baseURL = baseURL +} + +func (c *Client) SetUserAgent(userAgent string) { + c.userAgent = userAgent +} + +func (c *Client) GetVersion() string { + return version +} + +func (c *Client) doGetRequest(url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + bytes, err := c.doRequest(req) + if err != nil { + return nil, err + } + + return bytes, nil +} + +func (c *Client) doPostFormRequest(url string, formData url.Values) ([]byte, error) { + req, err := http.NewRequest("POST", url, strings.NewReader(formData.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + bytes, err := c.doRequest(req) + if err != nil { + return nil, err + } + + return bytes, nil +} + +func (c *Client) doRequest(req *http.Request) ([]byte, error) { + req.Header.Set("User-Agent", c.userAgent) + req.SetBasicAuth(c.Username, c.Password) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if 200 != resp.StatusCode { + return nil, fmt.Errorf("%s", body) + } + return body, nil +} diff --git a/client/failover.go b/client/failover.go new file mode 100644 index 0000000000000000000000000000000000000000..a362a7daf58ddb82287f093e12a094dd2e91a228 --- /dev/null +++ b/client/failover.go @@ -0,0 +1,44 @@ +package client + +import ( + "encoding/json" + "fmt" + "hetzner-robot/models" +) + +func (c *Client) FailoverGetList() ([]models.Failover, error) { + url := c.baseURL + "/failover" + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var failoverList []models.FailoverResponse + err = json.Unmarshal(bytes, &failoverList) + if err != nil { + return nil, err + } + + var data []models.Failover + for _, failover := range failoverList { + data = append(data, failover.Failover) + } + + return data, nil +} + +func (c *Client) FailoverGet(server models.ServerNumber) (*models.Failover, error) { + url := fmt.Sprintf(c.baseURL+"/failover/%d", int(server)) + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var failoverResp models.FailoverResponse + err = json.Unmarshal(bytes, &failoverResp) + if err != nil { + return nil, err + } + + return &failoverResp.Failover, nil +} diff --git a/client/interface.go b/client/interface.go new file mode 100644 index 0000000000000000000000000000000000000000..5d38c27bc796182cc210d1be6df4cf4d3950d4f2 --- /dev/null +++ b/client/interface.go @@ -0,0 +1,27 @@ +package client + +import ( + "hetzner-robot/models" +) + +type RobotClient interface { + SetBaseURL(baseURL string) + SetUserAgent(userAgent string) + GetVersion() string + ServerGetList() ([]models.Server, error) + ServerGet(server models.ServerNumber) (*models.Server, error) + ServerSetName(server models.ServerNumber, input *models.ServerSetNameInput) (*models.Server, error) + ServerReverse(server models.ServerNumber) (*models.Cancellation, error) + KeyGetList() ([]models.Key, error) + IPGetList() ([]models.IP, error) + RDnsGetList() ([]models.Rdns, error) + RDnsGet(server models.ServerNumber) (*models.Rdns, error) + BootRescueGet(server models.ServerNumber) (*models.RescueInfo, error) + BootRescueGetLast(server models.ServerNumber) (*models.Rescue, error) + BootRescueSet(server models.ServerNumber, input *models.RescueSetInput) (*models.Rescue, error) + BootRescueDelete(server models.ServerNumber) error + ResetGet(server models.ServerNumber) (*models.Reset, error) + ResetSet(server models.ServerNumber, input *models.ResetSetInput) (*models.ResetPost, error) + FailoverGetList() ([]models.Failover, error) + FailoverGet(server models.ServerNumber) (*models.Failover, error) +} diff --git a/client/ip.go b/client/ip.go new file mode 100644 index 0000000000000000000000000000000000000000..d130a0b6928dc809027811314e2a5d66986c0b18 --- /dev/null +++ b/client/ip.go @@ -0,0 +1,27 @@ +package client + +import ( + "encoding/json" + "hetzner-robot/models" +) + +func (c *Client) IPGetList() ([]models.IP, error) { + url := c.baseURL + "/ip" + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var ips []models.IPResponse + err = json.Unmarshal(bytes, &ips) + if err != nil { + return nil, err + } + + var data []models.IP + for _, ip := range ips { + data = append(data, ip.IP) + } + + return data, nil +} diff --git a/client/key.go b/client/key.go new file mode 100644 index 0000000000000000000000000000000000000000..33124b9af54c2edb2ac12a41f6b6656755bba0a5 --- /dev/null +++ b/client/key.go @@ -0,0 +1,27 @@ +package client + +import ( + "encoding/json" + "hetzner-robot/models" +) + +func (c *Client) KeyGetList() ([]models.Key, error) { + url := c.baseURL + "/key" + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var keys []models.KeyResponse + err = json.Unmarshal(bytes, &keys) + if err != nil { + return nil, err + } + + var data []models.Key + for _, key := range keys { + data = append(data, key.Key) + } + + return data, nil +} diff --git a/client/rdns.go b/client/rdns.go new file mode 100644 index 0000000000000000000000000000000000000000..1190c16fc6c65d8ebf0e7700f55fd051a166f14c --- /dev/null +++ b/client/rdns.go @@ -0,0 +1,44 @@ +package client + +import ( + "encoding/json" + "fmt" + "hetzner-robot/models" +) + +func (c *Client) RDnsGetList() ([]models.Rdns, error) { + url := c.baseURL + "/rdns" + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var rdnsList []models.RdnsResponse + err = json.Unmarshal(bytes, &rdnsList) + if err != nil { + return nil, err + } + + var data []models.Rdns + for _, rdns := range rdnsList { + data = append(data, rdns.Rdns) + } + + return data, nil +} + +func (c *Client) RDnsGet(server models.ServerNumber) (*models.Rdns, error) { + url := fmt.Sprintf(c.baseURL+"/rdns/%d", int(server)) + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var rDnsResp models.RdnsResponse + err = json.Unmarshal(bytes, &rDnsResp) + if err != nil { + return nil, err + } + + return &rDnsResp.Rdns, nil +} diff --git a/client/reset.go b/client/reset.go new file mode 100644 index 0000000000000000000000000000000000000000..9949a1cdaf88e8f5cb7b65bafc112a16cad9a019 --- /dev/null +++ b/client/reset.go @@ -0,0 +1,44 @@ +package client + +import ( + "encoding/json" + "fmt" + "hetzner-robot/models" + neturl "net/url" +) + +func (c *Client) ResetGet(server models.ServerNumber) (*models.Reset, error) { + url := fmt.Sprintf(c.baseURL+"/reset/%d", int(server)) + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var resetResp models.ResetResponse + err = json.Unmarshal(bytes, &resetResp) + if err != nil { + return nil, err + } + + return &resetResp.Reset, nil +} + +func (c *Client) ResetSet(server models.ServerNumber, input *models.ResetSetInput) (*models.ResetPost, error) { + url := fmt.Sprintf(c.baseURL+"/reset/%d", int(server)) + + formData := neturl.Values{} + formData.Set("type", input.Type) + + bytes, err := c.doPostFormRequest(url, formData) + if err != nil { + return nil, err + } + + var resetResp models.ResetPostResponse + err = json.Unmarshal(bytes, &resetResp) + if err != nil { + return nil, err + } + + return &resetResp.Reset, nil +} diff --git a/client/server.go b/client/server.go new file mode 100644 index 0000000000000000000000000000000000000000..a455ad3f13191356df908960010c9b238ce37b86 --- /dev/null +++ b/client/server.go @@ -0,0 +1,82 @@ +package client + +import ( + "encoding/json" + "fmt" + "hetzner-robot/models" + neturl "net/url" +) + +func (c *Client) ServerGetList() ([]models.Server, error) { + url := c.baseURL + "/server" + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var servers []models.ServerResponse + err = json.Unmarshal(bytes, &servers) + if err != nil { + return nil, err + } + + var data []models.Server + for _, server := range servers { + data = append(data, server.Server) + } + + return data, nil +} + +func (c *Client) ServerGet(server models.ServerNumber) (*models.Server, error) { + url := fmt.Sprintf(c.baseURL+"/server/%d", int(server)) + bytes, err := c.doGetRequest(url) + if err != nil { + return nil, err + } + + var serverResp models.ServerResponse + err = json.Unmarshal(bytes, &serverResp) + if err != nil { + return nil, err + } + + return &serverResp.Server, nil +} + +func (c *Client) ServerSetName(server models.ServerNumber, input *models.ServerSetNameInput) (*models.Server, error) { + url := fmt.Sprintf(c.baseURL+"/server/%d", int(server)) + + formData := neturl.Values{} + formData.Set("server_name", input.Name) + + bytes, err := c.doPostFormRequest(url, formData) + if err != nil { + return nil, err + } + + var serverResp models.ServerResponse + err = json.Unmarshal(bytes, &serverResp) + if err != nil { + return nil, err + } + + return &serverResp.Server, nil +} + +func (c *Client) ServerReverse(server models.ServerNumber) (*models.Cancellation, error) { + url := fmt.Sprintf(c.baseURL+"/server/%d/reversal", int(server)) + + bytes, err := c.doPostFormRequest(url, nil) + if err != nil { + return nil, err + } + + var cancelResp models.CancellationResponse + err = json.Unmarshal(bytes, &cancelResp) + if err != nil { + return nil, err + } + + return &cancelResp.Cancellation, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..27f9005f5dfdaa066cd6fbdf494984e3ba7e083c --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module hetzner-robot-automation + +go 1.18 + +require ( + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d + golang.org/x/sync v0.0.0-20190423024810-112230192c58 +) + +require golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..fc390d9561570e3b1cbc2e8c3a7561d94872f825 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= diff --git a/models/boot.go b/models/boot.go new file mode 100644 index 0000000000000000000000000000000000000000..be2bdd42339a03fb8ca0cdb1c3ac4cf2e047fb57 --- /dev/null +++ b/models/boot.go @@ -0,0 +1,41 @@ +package models + +type RescueInfoGetResponse struct { + Rescue RescueInfo `json:"rescue"` +} + +type RescueGetResponse struct { + Rescue Rescue `json:"rescue"` +} + +type AuthorizedKey struct { + Key Key `json:"key"` +} + +type RescueInfo struct { + ServerIP string `json:"server_ip"` + ServerNumber ServerNumber `json:"server_number"` + Os []string `json:"os"` + Arch []int `json:"arch"` + Active bool `json:"active"` + Password string `json:"password"` + AuthorizedKey []AuthorizedKey `json:"authorized_key"` + HostKey []interface{} `json:"host_key"` +} + +type Rescue struct { + ServerIP string `json:"server_ip"` + ServerNumber ServerNumber `json:"server_number"` + Os string `json:"os"` + Arch int `json:"arch"` + Active bool `json:"active"` + Password string `json:"password"` + AuthorizedKey []AuthorizedKey `json:"authorized_key"` + HostKey []interface{} `json:"host_key"` +} + +type RescueSetInput struct { + OS string + Arch int + AuthorizedKeys []Key +} diff --git a/models/cancellation.go b/models/cancellation.go new file mode 100644 index 0000000000000000000000000000000000000000..65d496b2009888a8686dd3c91e6d2067f75550a9 --- /dev/null +++ b/models/cancellation.go @@ -0,0 +1,14 @@ +package models + +type CancellationResponse struct { + Cancellation Cancellation `json:"cancellation"` +} +type Cancellation struct { + ServerIP string `json:"server_ip"` + ServerNumber ServerNumber `json:"server_number"` + ServerName string `json:"server_name"` + EarliestCancellationDate string `json:"earliest_cancellation_date"` + Cancelled bool `json:"cancelled"` + CancellationDate string `json:"cancellation_date"` + CancellationReason interface{} `json:"cancellation_reason"` +} diff --git a/models/failover.go b/models/failover.go new file mode 100644 index 0000000000000000000000000000000000000000..65ee63d869a423e2c8b49f485f514f750825e5f7 --- /dev/null +++ b/models/failover.go @@ -0,0 +1,13 @@ +package models + +type FailoverResponse struct { + Failover Failover `json:"failover"` +} + +type Failover struct { + IP string `json:"ip"` + Netmask string `json:"netmask"` + ServerIP string `json:"server_ip"` + ServerNumber ServerNumber `json:"server_number"` + ActiveServerIP string `json:"active_server_ip"` +} diff --git a/models/ip.go b/models/ip.go new file mode 100644 index 0000000000000000000000000000000000000000..b349f1f35238a6b6ce3a5f5125ecbdbd1c925245 --- /dev/null +++ b/models/ip.go @@ -0,0 +1,17 @@ +package models + +type IPResponse struct { + IP IP `json:"ip"` +} + +type IP struct { + IP string `json:"ip"` + ServerIP string `json:"server_ip"` + ServerNumber int `json:"server_number"` + Locked bool `json:"locked"` + SeparateMac string `json:"separate_mac"` + TrafficWarnings bool `json:"traffic_warnings"` + TrafficHourly int `json:"traffic_hourly"` + TrafficDaily int `json:"traffic_daily"` + TrafficMonthly int `json:"traffic_monthly"` +} diff --git a/models/key.go b/models/key.go new file mode 100644 index 0000000000000000000000000000000000000000..ef59a9006a39832370aa4c333ae6641f51701e6c --- /dev/null +++ b/models/key.go @@ -0,0 +1,13 @@ +package models + +type KeyResponse struct { + Key Key `json:"key"` +} + +type Key struct { + Name string `json:"name"` + Fingerprint string `json:"fingerprint"` + Type string `json:"type"` + Size int `json:"size"` + Data string `json:"data"` +} diff --git a/models/rdns.go b/models/rdns.go new file mode 100644 index 0000000000000000000000000000000000000000..6b191141c4d9ab40ced3983fdfcc18c3a6e475bb --- /dev/null +++ b/models/rdns.go @@ -0,0 +1,10 @@ +package models + +type RdnsResponse struct { + Rdns Rdns `json:"rdns"` +} + +type Rdns struct { + IP string `json:"ip"` + Ptr string `json:"ptr"` +} diff --git a/models/reset.go b/models/reset.go new file mode 100644 index 0000000000000000000000000000000000000000..6675e95e885d3db7f79b90f6b3c920f898bf4bac --- /dev/null +++ b/models/reset.go @@ -0,0 +1,30 @@ +package models + +const ResetTypePower = "power" +const ResetTypeHardware = "hw" +const ResetTypeSoftware = "sw" +const ResetTypeManual = "man" + +type ResetResponse struct { + Reset Reset `json:"reset"` +} + +type Reset struct { + OperatingStatus string `json:"operating_status"` + ServerIP string `json:"server_ip"` + ServerNumber ServerNumber `json:"server_number"` + Type []string `json:"type"` +} + +type ResetPostResponse struct { + Reset ResetPost `json:"reset"` +} + +type ResetPost struct { + ServerIP string `json:"server_ip"` + Type string `json:"type"` +} + +type ResetSetInput struct { + Type string +} diff --git a/models/server.go b/models/server.go new file mode 100644 index 0000000000000000000000000000000000000000..647653a259e0cc3a11e889284808b9f856121224 --- /dev/null +++ b/models/server.go @@ -0,0 +1,40 @@ +package models + +type ServerResponse struct { + Server Server `json:"server"` +} + +type Subnet struct { + IP string `json:"ip"` + Mask string `json:"mask"` +} + +type ServerNumber int + +type Server struct { + ServerIP string `json:"server_ip"` + ServerNumber ServerNumber `json:"server_number"` + ServerName string `json:"server_name"` + Product string `json:"product"` + Dc string `json:"dc"` + Traffic string `json:"traffic"` + Flatrate bool `json:"flatrate"` + Status string `json:"status"` + Throttled bool `json:"throttled"` + Cancelled bool `json:"cancelled"` + PaidUntil string `json:"paid_until"` + IP []string `json:"ip"` + Subnet []Subnet `json:"subnet"` + Reset bool `json:"reset"` + Rescue bool `json:"rescue"` + Vnc bool `json:"vnc"` + Windows bool `json:"windows"` + Plesk bool `json:"plesk"` + Cpanel bool `json:"cpanel"` + Wol bool `json:"wol"` + HotSwap bool `json:"hot_swap"` +} + +type ServerSetNameInput struct { + Name string +} diff --git a/schema.json b/schema.json new file mode 100644 index 0000000000000000000000000000000000000000..8f277d0dd2f66e8c899fa5129405e241e02c9c23 --- /dev/null +++ b/schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.kuschku.de/justjanne/hetzner-robot/schema.json", + "title": "hetzner robot manifest", + "description": "something here", + "type": "object", + "properties": { + "api-key": { + "description": "API Key for Hetzner Robot API (username)", + "type": "string" + }, + "api-secret": { + "description": "API Secret for Hetzner Robot API (password)", + "type": "string" + }, + "server": { + "description": "Hetzner Baremetal Server ID", + "type": "integer", + "exclusiveMinimum": 0 + }, + "files": { + "description": "Files to upload and execute", + "type": "array", + "items": { + "type": "object", + "properties": { + "source": { + "description": "Current local path for the file", + "type": "string" + }, + "target": { + "description": "Desired remote path for the file", + "type": "string" + }, + "mode": { + "description": "Desired access mode for the file after uploading", + "type": "integer" + }, + "execute": { + "description": "Whether the file should be executed after uploading", + "type": "boolean" + } + }, + "required": ["source", "target", "mode", "execute"] + }, + "uniqueItems": true + } + }, + "required": ["api-key", "api-secret", "server", "files"] +} \ No newline at end of file