diff --git a/go.mod b/go.mod index 3a44773..4bd1c40 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/traPtitech/go-traq v0.0.0-20240725071454-97c7b85dc879 github.com/traPtitech/traq-ws-bot v1.2.1 go.uber.org/zap v1.27.0 + golang.org/x/sync v0.7.0 ) require ( diff --git a/go.sum b/go.sum index c2cdfeb..e71319e 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2 golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= diff --git a/pkg/server/hosts.go b/pkg/server/hosts.go new file mode 100644 index 0000000..6080abc --- /dev/null +++ b/pkg/server/hosts.go @@ -0,0 +1,145 @@ +package server + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sort" + "sync" + + "github.com/dghubble/sling" + "github.com/traPtitech/DevOpsBot/pkg/config" + "golang.org/x/sync/errgroup" +) + +type hostsCommand struct { +} + +type serversResponse struct { + Servers []struct { + ID string `json:"id"` + Links []struct { + Href string `json:"href"` + Rel string `json:"rel"` + } `json:"links"` + Name string `json:"name"` + } `json:"servers"` +} + +type serverResponse struct { + Server struct { + ID string `json:"id"` + Status string `json:"status"` + OSEXTSRVATTRHost string `json:"OS-EXT-SRV-ATTR:host"` + Metadata struct { + InstanceNameTag string `json:"instance_name_tag"` + } `json:"metadata"` + } `json:"server"` +} + +type resultData struct { + ID string + Host string + Name string +} + +func (sc *hostsCommand) Execute(args []string) error { + token, err := getConohaAPIToken() + if err != nil { + return fmt.Errorf("failed to get conoha api token: %w", err) + } + + req, err := sling.New(). + Base(config.C.Servers.Conoha.Origin.Compute). + Get(fmt.Sprintf("v2/%s/servers", config.C.Servers.Conoha.TenantID)). + Set("Accept", "application/json"). + Set("X-Auth-Token", token). + Request() + if err != nil { + return fmt.Errorf("failed to create restart request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to post restart request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("invalid status code: %s (expected: 200)", resp.Status) + } + + var response serversResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + + servers := []resultData{} + + eg := errgroup.Group{} + mu := sync.Mutex{} + for _, server := range response.Servers { + eg.Go(func() error { + req, err := sling.New(). + Base(config.C.Servers.Conoha.Origin.Compute). + Get(fmt.Sprintf("v2/%s/servers/%s", config.C.Servers.Conoha.TenantID, server.ID)). + Set("Accept", "application/json"). + Set("X-Auth-Token", token). + Request() + if err != nil { + return fmt.Errorf("failed to create restart request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to post restart request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("invalid status code: %s (expected: 200)", resp.Status) + } + + var response serverResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + + mu.Lock() + servers = append(servers, resultData{ + ID: response.Server.ID, + Host: response.Server.OSEXTSRVATTRHost, + Name: response.Server.Metadata.InstanceNameTag, + }) + mu.Unlock() + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return fmt.Errorf("error in goroutines: %w", err) + } + + sort.Slice(servers, func(i, j int) bool { + return servers[i].Name < servers[j].Name + }) + + for _, server := range servers { + log.Printf("%s: %s\n", server.Name, server.Host) + } + + return nil +} diff --git a/pkg/server/restart.go b/pkg/server/restart.go new file mode 100644 index 0000000..fa2cd22 --- /dev/null +++ b/pkg/server/restart.go @@ -0,0 +1,82 @@ +package server + +import ( + "fmt" + "io" + "log" + "net/http" + + "github.com/dghubble/sling" + "github.com/samber/lo" + "github.com/traPtitech/DevOpsBot/pkg/config" +) + +type restartCommand struct { +} + +type m map[string]any + +func (sc *restartCommand) Execute(args []string) error { + if len(args) < 1 { + return fmt.Errorf("invalid arguments, expected server id") + } + + serverID := args[0] + args = args[1:] + + if len(args) < 1 { + return fmt.Errorf("invalid arguments, expected restart type (SOFT or HARD)") + } + + // args == [SOFT|HARD] + restartType := args[0] + + if !lo.Contains([]string{"SOFT", "HARD"}, restartType) { + return fmt.Errorf("unknown restart type: %s", restartType) + } + + token, err := getConohaAPIToken() + if err != nil { + return fmt.Errorf("failed to get conoha api token: %w", err) + } + + req, err := sling.New(). + Base(config.C.Servers.Conoha.Origin.Compute). + Post(fmt.Sprintf("v2/%s/servers/%s/action", config.C.Servers.Conoha.TenantID, serverID)). + BodyJSON(m{"reboot": m{"type": args[0]}}). + Set("Accept", "application/json"). + Set("X-Auth-Token", token). + Request() + if err != nil { + return fmt.Errorf("failed to create restart request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + return fmt.Errorf("failed to post restart request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + logStr := fmt.Sprintf(`Request +- URL: %s +- RestartType: %s + +Response +- Header: %+v +- Body: %s +- Status: %s (Expected: 202) +`, req.URL.String(), restartType, resp.Header, string(respBody), resp.Status) + log.Println(logStr) + + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("incorrect status code: %s", resp.Status) + } + + return nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 7ed2891..66d448d 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,187 +2,31 @@ package server import ( - "encoding/json" - "errors" "fmt" - "github.com/dghubble/sling" - "github.com/samber/lo" - "io" - "log" - "net/http" - - "github.com/traPtitech/DevOpsBot/pkg/config" ) type ServersCommand struct { - sub *subCommand + Commands map[string]command } func Compile() (*ServersCommand, error) { cmd := &ServersCommand{} - s := &subCommand{ - Commands: make(map[string]command), - } - s.Commands["restart"] = &restartCommand{s} - cmd.sub = s + cmd.Commands["restart"] = &restartCommand{} + cmd.Commands["hosts"] = &hostsCommand{} return cmd, nil } func (sc *ServersCommand) Execute(args []string) error { - if len(args) < 1 { - return fmt.Errorf("invalid arguments, expected server id") - } - - // args == [server id] restart [SOFT|HARD] - serverID := args[0] - return sc.sub.Execute(serverID, args[1:]) -} - -type subCommand struct { - Commands map[string]command -} - -func (i *subCommand) Execute(serverID string, args []string) error { - if len(args) < 1 { - return errors.New("invalid arguments, expected server action verb (supported: restart)") - } - - // args == restart [SOFT|HARD] verb := args[0] - c, ok := i.Commands[verb] + c, ok := sc.Commands[verb] if !ok { return fmt.Errorf("unknown command: `%s`", verb) } - return c.Execute(serverID, args[1:]) + return c.Execute(args[1:]) } type command interface { - Execute(serverID string, args []string) error -} - -type restartCommand struct { - server *subCommand -} - -type m map[string]any - -func (sc *restartCommand) Execute(serverID string, args []string) error { - if len(args) < 1 { - return fmt.Errorf("invalid arguments, expected restart type (SOFT or HARD)") - } - - // args == [SOFT|HARD] - restartType := args[0] - - if !lo.Contains([]string{"SOFT", "HARD"}, restartType) { - return fmt.Errorf("unknown restart type: %s", restartType) - } - - token, err := getConohaAPIToken() - if err != nil { - return fmt.Errorf("failed to get conoha api token: %w", err) - } - - req, err := sling.New(). - Base(config.C.Servers.Conoha.Origin.Compute). - Post(fmt.Sprintf("v2/%s/servers/%s/action", config.C.Servers.Conoha.TenantID, serverID)). - BodyJSON(m{"reboot": m{"type": args[0]}}). - Set("Accept", "application/json"). - Set("X-Auth-Token", token). - Request() - if err != nil { - return fmt.Errorf("failed to create restart request: %w", err) - } - - resp, err := http.DefaultClient.Do(req) - - if err != nil { - return fmt.Errorf("failed to post restart request: %w", err) - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w", err) - } - - logStr := fmt.Sprintf(`Request -- URL: %s -- RestartType: %s - -Response -- Header: %+v -- Body: %s -- Status: %s (Expected: 202) -`, req.URL.String(), restartType, resp.Header, string(respBody), resp.Status) - log.Println(logStr) - - if resp.StatusCode != http.StatusAccepted { - return fmt.Errorf("incorrect status code: %s", resp.Status) - } - - return nil -} - -func getConohaAPIToken() (string, error) { - type passwordCredentials struct { - Username string `json:"username"` - Password string `json:"password"` - } - type auth struct { - PasswordCredentials passwordCredentials `json:"passwordCredentials"` - TenantId string `json:"tenantId"` - } - requestJson := struct { - Auth auth `json:"auth"` - }{ - Auth: auth{ - PasswordCredentials: passwordCredentials{ - Username: config.C.Servers.Conoha.Username, - Password: config.C.Servers.Conoha.Password, - }, - TenantId: config.C.Servers.Conoha.TenantID, - }, - } - - req, err := sling.New(). - Base(config.C.Servers.Conoha.Origin.Identity). - Post("v2.0/tokens"). - BodyJSON(requestJson). - Set("Accept", "application/json"). - Request() - if err != nil { - return "", fmt.Errorf("failed to create authentication request: %w", err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to post authentication request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("invalid status code: %s (expected: 200)", resp.Status) - } - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response body: %w", err) - } - - var responseJson struct { - Access struct { - Token struct { - Id string `json:"id"` - } `json:"token"` - } `json:"access"` - } - err = json.Unmarshal(respBody, &responseJson) - if err != nil { - return "", fmt.Errorf("failed to unmarshal response body: %w", err) - } - - return responseJson.Access.Token.Id, nil + Execute(args []string) error } diff --git a/pkg/server/token.go b/pkg/server/token.go new file mode 100644 index 0000000..d365b56 --- /dev/null +++ b/pkg/server/token.go @@ -0,0 +1,72 @@ +package server + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/dghubble/sling" + "github.com/traPtitech/DevOpsBot/pkg/config" +) + +func getConohaAPIToken() (string, error) { + type passwordCredentials struct { + Username string `json:"username"` + Password string `json:"password"` + } + type auth struct { + PasswordCredentials passwordCredentials `json:"passwordCredentials"` + TenantId string `json:"tenantId"` + } + requestJson := struct { + Auth auth `json:"auth"` + }{ + Auth: auth{ + PasswordCredentials: passwordCredentials{ + Username: config.C.Servers.Conoha.Username, + Password: config.C.Servers.Conoha.Password, + }, + TenantId: config.C.Servers.Conoha.TenantID, + }, + } + + req, err := sling.New(). + Base(config.C.Servers.Conoha.Origin.Identity). + Post("v2.0/tokens"). + BodyJSON(requestJson). + Set("Accept", "application/json"). + Request() + if err != nil { + return "", fmt.Errorf("failed to create authentication request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to post authentication request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("invalid status code: %s (expected: 200)", resp.Status) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + var responseJson struct { + Access struct { + Token struct { + Id string `json:"id"` + } `json:"token"` + } `json:"access"` + } + err = json.Unmarshal(respBody, &responseJson) + if err != nil { + return "", fmt.Errorf("failed to unmarshal response body: %w", err) + } + + return responseJson.Access.Token.Id, nil +}