Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cluster-sample-topology.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ servers:
- address: cache2:11211
- address: cache3:11211
config: # optional - per-node override
options: "-l 192.168.1.3" # optional - override global options
options: "-l 192.168.1.3" # optional - override global options (-p, -P, -z, -d are not allowed)

global_config:
options: "-t 4 -c 1024 -b 1024 -B auto -m 64" # memcached command-line arguments
Expand Down
15 changes: 13 additions & 2 deletions cmd/cluster/delete.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
package cluster

import "github.com/spf13/cobra"
import (
"github.com/jam2in/arcusctl/internal/cluster"
"github.com/spf13/cobra"
)

var deleteCmd = &cobra.Command{
Use: "delete <servicecode>",
Short: "Delete an Arcus cluster",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// TODO: delete ๊ตฌํ˜„
serviceCode := args[0]
purge, _ := cmd.Flags().GetBool("purge")
if err := cluster.Delete(serviceCode, purge); err != nil {
panic(err)
}
},
}

func init() {
deleteCmd.Flags().Bool("purge", false, "also remove the installation directory on each server")
}
9 changes: 7 additions & 2 deletions cmd/cluster/list.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package cluster

import "github.com/spf13/cobra"
import (
"github.com/jam2in/arcusctl/internal/cluster"
"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
Use: "list",
Short: "List managed Arcus clusters",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
// TODO: list ๊ตฌํ˜„
if err := cluster.List(); err != nil {
panic(err)
}
},
}
18 changes: 14 additions & 4 deletions cmd/cluster/start.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package cluster

import "github.com/spf13/cobra"
import (
"github.com/jam2in/arcusctl/internal/cluster"
"github.com/spf13/cobra"
)

var startCmd = &cobra.Command{
Use: "start <servicecode> [--node <address>]",
Use: "start <servicecode> [--node <address>] [--group <group-name>]",
Short: "Start an Arcus cluster",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// TODO: start ๊ตฌํ˜„
serviceCode := args[0]
nodeAddress, _ := cmd.Flags().GetString("node")
groupName, _ := cmd.Flags().GetString("group")
if err := cluster.Start(serviceCode, nodeAddress, groupName); err != nil {
panic(err)
}
},
}

func init() {
startCmd.Flags().String("node", "", "address of the specific node to start")
startCmd.Flags().String("node", "", "address of the specific node to start (community edition only)")
startCmd.Flags().String("group", "", "name of the specific group to start (enterprise edition only)")
startCmd.MarkFlagsMutuallyExclusive("node", "group")
}
10 changes: 8 additions & 2 deletions cmd/cluster/status.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package cluster

import "github.com/spf13/cobra"
import (
"github.com/jam2in/arcusctl/internal/cluster"
"github.com/spf13/cobra"
)

var statusCmd = &cobra.Command{
Use: "status <servicecode>",
Short: "Show status of an Arcus cluster",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// TODO: status ๊ตฌํ˜„
serviceCode := args[0]
if err := cluster.Status(serviceCode); err != nil {
panic(err)
}
},
}
18 changes: 14 additions & 4 deletions cmd/cluster/stop.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package cluster

import "github.com/spf13/cobra"
import (
"github.com/jam2in/arcusctl/internal/cluster"
"github.com/spf13/cobra"
)

var stopCmd = &cobra.Command{
Use: "stop <servicecode> [--node <address>]",
Use: "stop <servicecode> [--node <address>] [--group <group-name>]",
Short: "Stop an Arcus cluster",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// TODO: stop ๊ตฌํ˜„
serviceCode := args[0]
nodeAddress, _ := cmd.Flags().GetString("node")
groupName, _ := cmd.Flags().GetString("group")
if err := cluster.Stop(serviceCode, nodeAddress, groupName); err != nil {
panic(err)
}
},
}

func init() {
stopCmd.Flags().String("node", "", "address of the specific node to stop")
stopCmd.Flags().String("node", "", "address of the specific node to stop (community edition only)")
stopCmd.Flags().String("group", "", "name of the specific group to stop (enterprise edition only)")
stopCmd.MarkFlagsMutuallyExclusive("node", "group")
}
80 changes: 80 additions & 0 deletions internal/cluster/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cluster

import (
"github.com/jam2in/arcusctl/internal/store"
"github.com/jam2in/arcusctl/internal/topology"
)

func loadCluster(serviceCode string) (
*store.ClusterMeta,
*topology.ClusterTopology,
topology.ClusterEdition,
error,
) {
meta, err := store.LoadClusterMeta(serviceCode)
if err != nil {
return nil, nil, "", err
}

topo, err := store.LoadClusterTopology(serviceCode)
if err != nil {
return nil, nil, "", err
}

edition, err := topo.Edition()
if err != nil {
return nil, nil, "", err
}

return meta, topo, edition, nil
}

// sharingCluster return, for each host, the service code of another cluster
// installed there from same path and version.
func sharingCluster(
serviceCode string,
topoPath string,
version string,
host string,
) (string, error) {
registered, err := store.ListCluster()
if err != nil {
return "", err
}

for _, other := range registered {
if other == serviceCode {
continue
}

meta, err := store.LoadClusterMeta(other)
if err != nil {
continue
}

topo, err := store.LoadClusterTopology(other)
if err != nil {
continue
}

if topo.Path == topoPath &&
meta.Version == version &&
hasServerOn(topo.Servers, host) {
return other, nil
}
}

return "", nil
}

func hasServerOn(
topoServers []topology.CacheServer,
host string,
) bool {
for _, server := range topoServers {
if server.Host() == host {
return true
}
}
return false
}
96 changes: 96 additions & 0 deletions internal/cluster/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package cluster

import (
"fmt"

"github.com/jam2in/arcusctl/internal"
"github.com/jam2in/arcusctl/internal/ssh"
"github.com/jam2in/arcusctl/internal/store"
"github.com/jam2in/arcusctl/internal/topology"
)

const removeCommandTemplate = "rm -rf %s"

func Delete(serviceCode string, purge bool) error {
meta, topo, edition, err := loadCluster(serviceCode)
if err != nil {
return err
}

if err := verifyAllStopped(topo, meta.Version); err != nil {
return err
}

fmt.Printf("This will remove Arcus cluster %q from all servers.\n", serviceCode)
if !internal.Confirm("Are you sure you want to proceed? (y/N): ") {
fmt.Println("Aborted.")
return nil
}

if err := unregisterZNodes(topo, edition); err != nil {
return err
}

// If user specified --purge, remove the installation directories on each server.
// If the installation directory is shared with another cluster, it will not be removed.
if purge {
if err := removeInstallationDirs(serviceCode, topo, meta.Version); err != nil {
return err
}
}

if err := store.DeleteCluster(serviceCode); err != nil {
return fmt.Errorf("delete cluster metadata: %w", err)
}

fmt.Printf("Arcus cluster %q deleted.\n", serviceCode)
return nil
}

func removeInstallationDirs(
serviceCode string,
topo *topology.ClusterTopology,
version string,
) error {
installPath := memcachedInstallPath(topo.Path, version)

for _, host := range distinctHosts(topo.Servers) {
other, err := sharingCluster(serviceCode, topo.Path, version, host)
if err != nil {
return err
}

if other != "" {
fmt.Printf(
"Skip removing directory on %s: install path is shared with cluster %q\n",
host, other,
)
continue
}

fmt.Printf("Removing files on %s...\n", host)
if err := ssh.Run(host, fmt.Sprintf(removeCommandTemplate, installPath)); err != nil {
return fmt.Errorf("remove files on %s: %w", host, err)
}
}

return nil
}

func verifyAllStopped(
topo *topology.ClusterTopology,
version string,
) error {
for _, server := range topo.Servers {
pidFile := pidFilePath(server.Address, topo.Path, version)
cmd := fmt.Sprintf("pgrep -f %q > /dev/null 2>&1", pidFile)
if err := ssh.Run(server.Host(), cmd); err == nil {
return fmt.Errorf(
"cache server %q is still running; stop the cluster before delete",
server.Address,
)
}
}

return nil
}
4 changes: 0 additions & 4 deletions internal/cluster/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,3 @@ func buildCommand(
strings.Join(options, " "),
)
}

func memcachedInstallPath(basePath string, version string) string {
return path.Join(basePath, version)
}
48 changes: 48 additions & 0 deletions internal/cluster/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cluster

import (
"fmt"
"os"
"text/tabwriter"

"github.com/jam2in/arcusctl/internal/store"
)

func List() error {
serviceCodes, err := store.ListCluster()
if err != nil {
return fmt.Errorf("list Arcus clusters: %w", err)
}

if len(serviceCodes) == 0 {
fmt.Println("No Arcus cluster found.")
return nil
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "SERVICECODE\tVERSION\tEDITION\tNODES\tDEPLOYED_AT")

for _, serviceCode := range serviceCodes {
meta, err := store.LoadClusterMeta(serviceCode)
if err != nil {
fmt.Fprintf(w, "%s\t<error>\t\t\t%v\n", serviceCode, err)
continue
}

edition := "<unknown>"
servers := 0
if topo, err := store.LoadClusterTopology(serviceCode); err == nil {
servers = len(topo.Servers)
if e, err := topo.Edition(); err == nil {
edition = string(e)
}
}

fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\n",
serviceCode, meta.Version, edition, servers,
meta.DeployedAt.Format("2006-01-02 15:04:05"),
)
}

return w.Flush()
}
Loading
Loading