Skip to content

Commit 7d59037

Browse files
authored
Merge pull request #49 from traPtitech/server-hosts-command
hosts コマンドを実装
2 parents b3376e3 + 2c36383 commit 7d59037

6 files changed

Lines changed: 308 additions & 162 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/traPtitech/go-traq v0.0.0-20240725071454-97c7b85dc879
1515
github.com/traPtitech/traq-ws-bot v1.2.1
1616
go.uber.org/zap v1.27.0
17+
golang.org/x/sync v0.7.0
1718
)
1819

1920
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2
9090
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
9191
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
9292
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
93+
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
94+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
9395
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
9496
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
9597
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=

pkg/server/hosts.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package server
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"sort"
10+
"sync"
11+
12+
"github.com/dghubble/sling"
13+
"github.com/traPtitech/DevOpsBot/pkg/config"
14+
"golang.org/x/sync/errgroup"
15+
)
16+
17+
type hostsCommand struct {
18+
}
19+
20+
type serversResponse struct {
21+
Servers []struct {
22+
ID string `json:"id"`
23+
Links []struct {
24+
Href string `json:"href"`
25+
Rel string `json:"rel"`
26+
} `json:"links"`
27+
Name string `json:"name"`
28+
} `json:"servers"`
29+
}
30+
31+
type serverResponse struct {
32+
Server struct {
33+
ID string `json:"id"`
34+
Status string `json:"status"`
35+
OSEXTSRVATTRHost string `json:"OS-EXT-SRV-ATTR:host"`
36+
Metadata struct {
37+
InstanceNameTag string `json:"instance_name_tag"`
38+
} `json:"metadata"`
39+
} `json:"server"`
40+
}
41+
42+
type resultData struct {
43+
ID string
44+
Host string
45+
Name string
46+
}
47+
48+
func (sc *hostsCommand) Execute(args []string) error {
49+
token, err := getConohaAPIToken()
50+
if err != nil {
51+
return fmt.Errorf("failed to get conoha api token: %w", err)
52+
}
53+
54+
req, err := sling.New().
55+
Base(config.C.Servers.Conoha.Origin.Compute).
56+
Get(fmt.Sprintf("v2/%s/servers", config.C.Servers.Conoha.TenantID)).
57+
Set("Accept", "application/json").
58+
Set("X-Auth-Token", token).
59+
Request()
60+
if err != nil {
61+
return fmt.Errorf("failed to create restart request: %w", err)
62+
}
63+
64+
resp, err := http.DefaultClient.Do(req)
65+
if err != nil {
66+
return fmt.Errorf("failed to post restart request: %w", err)
67+
}
68+
defer resp.Body.Close()
69+
70+
respBody, err := io.ReadAll(resp.Body)
71+
if err != nil {
72+
return fmt.Errorf("failed to read response body: %w", err)
73+
}
74+
75+
if resp.StatusCode != http.StatusOK {
76+
return fmt.Errorf("invalid status code: %s (expected: 200)", resp.Status)
77+
}
78+
79+
var response serversResponse
80+
if err := json.Unmarshal(respBody, &response); err != nil {
81+
return fmt.Errorf("failed to unmarshal response body: %w", err)
82+
}
83+
84+
servers := []resultData{}
85+
86+
eg := errgroup.Group{}
87+
mu := sync.Mutex{}
88+
for _, server := range response.Servers {
89+
eg.Go(func() error {
90+
req, err := sling.New().
91+
Base(config.C.Servers.Conoha.Origin.Compute).
92+
Get(fmt.Sprintf("v2/%s/servers/%s", config.C.Servers.Conoha.TenantID, server.ID)).
93+
Set("Accept", "application/json").
94+
Set("X-Auth-Token", token).
95+
Request()
96+
if err != nil {
97+
return fmt.Errorf("failed to create restart request: %w", err)
98+
}
99+
100+
resp, err := http.DefaultClient.Do(req)
101+
if err != nil {
102+
return fmt.Errorf("failed to post restart request: %w", err)
103+
}
104+
defer resp.Body.Close()
105+
106+
respBody, err := io.ReadAll(resp.Body)
107+
if err != nil {
108+
return fmt.Errorf("failed to read response body: %w", err)
109+
}
110+
111+
if resp.StatusCode != http.StatusOK {
112+
return fmt.Errorf("invalid status code: %s (expected: 200)", resp.Status)
113+
}
114+
115+
var response serverResponse
116+
if err := json.Unmarshal(respBody, &response); err != nil {
117+
return fmt.Errorf("failed to unmarshal response body: %w", err)
118+
}
119+
120+
mu.Lock()
121+
servers = append(servers, resultData{
122+
ID: response.Server.ID,
123+
Host: response.Server.OSEXTSRVATTRHost,
124+
Name: response.Server.Metadata.InstanceNameTag,
125+
})
126+
mu.Unlock()
127+
128+
return nil
129+
})
130+
}
131+
132+
if err := eg.Wait(); err != nil {
133+
return fmt.Errorf("error in goroutines: %w", err)
134+
}
135+
136+
sort.Slice(servers, func(i, j int) bool {
137+
return servers[i].Name < servers[j].Name
138+
})
139+
140+
for _, server := range servers {
141+
log.Printf("%s: %s\n", server.Name, server.Host)
142+
}
143+
144+
return nil
145+
}

pkg/server/restart.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"log"
7+
"net/http"
8+
9+
"github.com/dghubble/sling"
10+
"github.com/samber/lo"
11+
"github.com/traPtitech/DevOpsBot/pkg/config"
12+
)
13+
14+
type restartCommand struct {
15+
}
16+
17+
type m map[string]any
18+
19+
func (sc *restartCommand) Execute(args []string) error {
20+
if len(args) < 1 {
21+
return fmt.Errorf("invalid arguments, expected server id")
22+
}
23+
24+
serverID := args[0]
25+
args = args[1:]
26+
27+
if len(args) < 1 {
28+
return fmt.Errorf("invalid arguments, expected restart type (SOFT or HARD)")
29+
}
30+
31+
// args == [SOFT|HARD]
32+
restartType := args[0]
33+
34+
if !lo.Contains([]string{"SOFT", "HARD"}, restartType) {
35+
return fmt.Errorf("unknown restart type: %s", restartType)
36+
}
37+
38+
token, err := getConohaAPIToken()
39+
if err != nil {
40+
return fmt.Errorf("failed to get conoha api token: %w", err)
41+
}
42+
43+
req, err := sling.New().
44+
Base(config.C.Servers.Conoha.Origin.Compute).
45+
Post(fmt.Sprintf("v2/%s/servers/%s/action", config.C.Servers.Conoha.TenantID, serverID)).
46+
BodyJSON(m{"reboot": m{"type": args[0]}}).
47+
Set("Accept", "application/json").
48+
Set("X-Auth-Token", token).
49+
Request()
50+
if err != nil {
51+
return fmt.Errorf("failed to create restart request: %w", err)
52+
}
53+
54+
resp, err := http.DefaultClient.Do(req)
55+
56+
if err != nil {
57+
return fmt.Errorf("failed to post restart request: %w", err)
58+
}
59+
defer resp.Body.Close()
60+
61+
respBody, err := io.ReadAll(resp.Body)
62+
if err != nil {
63+
return fmt.Errorf("failed to read response body: %w", err)
64+
}
65+
66+
logStr := fmt.Sprintf(`Request
67+
- URL: %s
68+
- RestartType: %s
69+
70+
Response
71+
- Header: %+v
72+
- Body: %s
73+
- Status: %s (Expected: 202)
74+
`, req.URL.String(), restartType, resp.Header, string(respBody), resp.Status)
75+
log.Println(logStr)
76+
77+
if resp.StatusCode != http.StatusAccepted {
78+
return fmt.Errorf("incorrect status code: %s", resp.Status)
79+
}
80+
81+
return nil
82+
}

0 commit comments

Comments
 (0)