diff --git a/cmd/memcached/list.go b/cmd/memcached/list.go new file mode 100644 index 0000000..8086662 --- /dev/null +++ b/cmd/memcached/list.go @@ -0,0 +1,109 @@ +package memcached + +import ( + "fmt" + "os" + "path" + "sort" + "strings" + + "github.com/go-zookeeper/zk" + "github.com/jam2in/arcus-cli/internal" + "github.com/spf13/cobra" +) + +type serviceStatus struct { + serviceCode string + total int + online int + offline int +} + +var listCmd = &cobra.Command{ + Use: "list [serviceCode]", + Short: "list all servers in arcus cache cloud", + Run: func(cmd *cobra.Command, args []string) { + + zkConn := cmd.Context().Value(internal.CtxZkConnKey{}).(*zk.Conn) + + serviceCodeMap, err := buildServiceCodeMap(zkConn) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + var serviceCodes []string + if len(args) == 0 { + for sm := range serviceCodeMap { + serviceCodes = append(serviceCodes, sm) + } + sort.Strings(serviceCodes) + } else { + serviceCodes = []string{args[0]} + } + + liveServerMaps := make(map[string]map[string]struct{}) + var statuses []serviceStatus + for _, sc := range serviceCodes { + liveServers, _ := getLiveServers(zkConn, sc) + liveServerMaps[sc] = liveServers + + onlineCnt := 0 + for _, s := range serviceCodeMap[sc] { + if _, ok := liveServers[s]; ok { + onlineCnt++ + } + } + + statuses = append(statuses, serviceStatus{ + serviceCode: sc, + total: len(serviceCodeMap[sc]), + online: onlineCnt, + offline: len(serviceCodeMap[sc]) - onlineCnt, + }) + } + + if len(args) == 1 { + serviceCode := args[0] + servers := serviceCodeMap[serviceCode] + liveServers := liveServerMaps[serviceCode] + + fmt.Printf("Servers in service code '%s':\n", serviceCode) + for _, server := range servers { + status := "offline" + if _, isLive := liveServers[server]; isLive { + status = "online" + } + fmt.Printf(" - %-21s %s\n", server, status) + } + fmt.Println() + } + + fmt.Printf("%-25s %-8s %-8s %-8s\n", "SERVICE CODE", "TOTAL", "ONLINE", "OFFLINE") + fmt.Println(strings.Repeat("-", 60)) + for _, s := range statuses { + fmt.Printf("%-25s %-8d %-8d %-8d\n", s.serviceCode, s.total, s.online, s.offline) + } + + }, +} + +func buildServiceCodeMap(zkConn *zk.Conn) (map[string][]string, error) { + serviceCodeMap := make(map[string][]string) + allServers, _, err := zkConn.Children(path.Join(internal.ArcusCacheServerMappingPath)) + if err != nil { + return nil, err + } + + for _, s := range allServers { + serviceCodeTags, _, err := zkConn.Children(path.Join(internal.ArcusCacheServerMappingPath, s)) + if err != nil { + continue + } + for _, sc := range serviceCodeTags { + serviceCodeMap[sc] = append(serviceCodeMap[sc], s) + } + } + + return serviceCodeMap, nil +} diff --git a/cmd/memcached/memcached.go b/cmd/memcached/memcached.go index 4414e8b..29d0f9a 100644 --- a/cmd/memcached/memcached.go +++ b/cmd/memcached/memcached.go @@ -1,11 +1,18 @@ package memcached import ( + "path" + "strings" + "github.com/go-zookeeper/zk" "github.com/jam2in/arcus-cli/internal" "github.com/spf13/cobra" ) +const ( + memcachedStartCommandTemplate = "%s/bin/memcached -E %s/lib/default_engine.so -X %s/lib/syslog_logger.so -X %s/lib/ascii_scrub.so -P %s/memcached-%s.pid -d -v -r -R5 -U 0 -D: -b 8192 %s -z %s" +) + var MemcachedCmd = &cobra.Command{ Use: "memcached", Short: "Memcached command", @@ -33,4 +40,58 @@ func init() { MemcachedCmd.AddCommand(removeCmd) MemcachedCmd.AddCommand(configCmd) MemcachedCmd.AddCommand(connectCmd) + MemcachedCmd.AddCommand(listCmd) + MemcachedCmd.AddCommand(startCmd) + MemcachedCmd.AddCommand(stopCmd) +} + +func getServiceCodeServers(zkConn *zk.Conn, serviceCode string) ([]string, error) { + servers, _, err := zkConn.Children(internal.ArcusCacheServerMappingPath) + if err != nil { + return nil, err + } + + var result []string + for _, server := range servers { + children, _, err := zkConn.Children(path.Join(internal.ArcusCacheServerMappingPath, server)) + if err != nil { + continue + } + for _, child := range children { + if child == serviceCode { + result = append(result, server) + break + } + } + } + return result, nil +} + +func getLiveServers(zkConn *zk.Conn, serviceCode string) (map[string]struct{}, error) { + liveNodes, _, err := zkConn.Children(path.Join(internal.ArcusCacheListPath, serviceCode)) + if err != nil { + return nil, err + } + + liveServers := make(map[string]struct{}) + for _, liveNode := range liveNodes { + addr, _, _ := strings.Cut(liveNode, "-") + liveServers[addr] = struct{}{} + } + return liveServers, nil +} + +func filterServers(allServers []string, targets []string) []string { + targetSet := make(map[string]struct{}) + for _, t := range targets { + targetSet[t] = struct{}{} + } + + result := make([]string, 0) + for _, s := range allServers { + if _, ok := targetSet[s]; ok { + result = append(result, s) + } + } + return result } diff --git a/cmd/memcached/start.go b/cmd/memcached/start.go new file mode 100644 index 0000000..939114c --- /dev/null +++ b/cmd/memcached/start.go @@ -0,0 +1,92 @@ +package memcached + +import ( + "context" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/go-zookeeper/zk" + "github.com/jam2in/arcus-cli/internal" + "github.com/spf13/cobra" +) + +var startCmd = &cobra.Command{ + Use: "start [ip:port...]", + Short: "start all servers or specific servers in service code", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + serviceCode := args[0] + targetServers := args[1:] + zkConn := cmd.Context().Value(internal.CtxZkConnKey{}).(*zk.Conn) + + globalConfig, _, err := zkConn.Get(path.Join(internal.ArcusCacheListPath, serviceCode)) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + serviceCodeServers, err := getServiceCodeServers(zkConn, serviceCode) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + var serversToStart []string + if len(targetServers) > 0 { + serversToStart = filterServers(serviceCodeServers, targetServers) + if len(serversToStart) == 0 { + fmt.Fprintln(os.Stderr, "No servers found in service code") + os.Exit(1) + } + } else { + serversToStart = serviceCodeServers + } + + for _, serverAddress := range serversToStart { + ip, port, flag := strings.Cut(serverAddress, ":") + if !flag { + fmt.Fprintln(os.Stderr, "Invalid server address:", serverAddress) + os.Exit(1) + } + + client, err := internal.NewSSHClient(ip) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer session.Close() + + memcachedPath := os.Getenv("ARCUS_PATH") + command := fmt.Sprintf(memcachedStartCommandTemplate, + memcachedPath, memcachedPath, memcachedPath, memcachedPath, memcachedPath, + port, string(globalConfig), os.Getenv("ZK_ADDR")) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + errChan := make(chan error, 1) + go func() { + errChan <- session.Run(command) + }() + select { + case err := <-errChan: + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + case <-ctx.Done(): + continue + } + fmt.Printf(" - Start command sent to %s successfully.\n", serverAddress) + } + }, +} diff --git a/cmd/memcached/stop.go b/cmd/memcached/stop.go new file mode 100644 index 0000000..316ea95 --- /dev/null +++ b/cmd/memcached/stop.go @@ -0,0 +1,68 @@ +package memcached + +import ( + "fmt" + "os" + "strings" + + "github.com/go-zookeeper/zk" + "github.com/jam2in/arcus-cli/internal" + "github.com/spf13/cobra" +) + +var stopCmd = &cobra.Command{ + Use: "stop [ip:port...]", + Short: "stop all servers or specific servers in service code", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + serviceCode := args[0] + targetServers := args[1:] + zkConn := cmd.Context().Value(internal.CtxZkConnKey{}).(*zk.Conn) + + serviceCodeServers, err := getServiceCodeServers(zkConn, serviceCode) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + var serversToStop []string + if len(targetServers) > 0 { + serversToStop = filterServers(serviceCodeServers, targetServers) + if len(serversToStop) == 0 { + fmt.Fprintln(os.Stderr, "No servers found in service code") + os.Exit(1) + } + } else { + serversToStop = serviceCodeServers + } + + for _, serverAddress := range serversToStop { + ip, port, flag := strings.Cut(serverAddress, ":") + if !flag { + fmt.Fprintln(os.Stderr, "Invalid server address:", serverAddress) + os.Exit(1) + } + + client, err := internal.NewSSHClient(ip) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer session.Close() + + pidFilePath := fmt.Sprintf("%s/memcached-%s.pid", os.Getenv("ARCUS_PATH"), port) + command := fmt.Sprintf("kill -INT $(cat %s)", pidFilePath) + if err := session.Run(command); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Printf(" - Stop command sent to %s successfully.\n", serverAddress) + } + }, +} diff --git a/go.mod b/go.mod index 25eaefe..daaf3d6 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,13 @@ require ( github.com/cybergarage/go-sasl v1.2.6 github.com/go-zookeeper/zk v1.0.4 github.com/spf13/cobra v1.9.1 - golang.org/x/term v0.34.0 + golang.org/x/term v0.35.0 ) require ( github.com/cybergarage/go-safecast v1.3.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.6 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/sys v0.36.0 // indirect ) diff --git a/go.sum b/go.sum index 52aba46..59b255c 100644 --- a/go.sum +++ b/go.sum @@ -12,9 +12,15 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/ssh.go b/internal/ssh.go new file mode 100644 index 0000000..c2b1633 --- /dev/null +++ b/internal/ssh.go @@ -0,0 +1,36 @@ +package internal + +import ( + "os" + "os/user" + + "golang.org/x/crypto/ssh" +) + +func NewSSHClient(ip string) (*ssh.Client, error) { + current, err := user.Current() + if err != nil { + return nil, err + } + username := current.Username + + keyPath := os.Getenv("HOME") + "/.ssh/id_rsa" + key, err := os.ReadFile(keyPath) + if err != nil { + return nil, err + } + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, err + } + + sshConfig := &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + return ssh.Dial("tcp", ip+":22", sshConfig) +}