github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/pkg/cli/volume_list.go (about) 1 package cli 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "strings" 8 "text/tabwriter" 9 10 "github.com/apprenda/kismatic/pkg/data" 11 "github.com/apprenda/kismatic/pkg/install" 12 "github.com/spf13/cobra" 13 ) 14 15 type volumeListOptions struct { 16 outputFormat string 17 } 18 19 // NewCmdVolumeList returns the command for listgin storage volumes 20 func NewCmdVolumeList(out io.Writer, planFile *string) *cobra.Command { 21 opts := volumeListOptions{} 22 cmd := &cobra.Command{ 23 Use: "list", 24 Short: "list storage volumes to the Kubernetes cluster", 25 Long: `List storage volumes to the Kubernetes cluster. 26 This function requires a target cluster that has storage nodes.`, 27 RunE: func(cmd *cobra.Command, args []string) error { 28 return doVolumeList(out, opts, *planFile, args) 29 }, 30 } 31 32 cmd.Flags().StringVarP(&opts.outputFormat, "output", "o", "simple", `output format (options "simple"|"json")`) 33 return cmd 34 } 35 36 func doVolumeList(out io.Writer, opts volumeListOptions, planFile string, args []string) error { 37 // verify command 38 if opts.outputFormat != "simple" && opts.outputFormat != "json" { 39 return fmt.Errorf("output format %q is not supported", opts.outputFormat) 40 } 41 42 // Setup ansible 43 planner := &install.FilePlanner{File: planFile} 44 if !planner.PlanExists() { 45 return planFileNotFoundErr{filename: planFile} 46 } 47 48 plan, err := planner.Read() 49 if err != nil { 50 return fmt.Errorf("error reading plan file: %v", err) 51 } 52 53 // find storage node 54 clientStorage, err := plan.GetSSHClient("storage") 55 if err != nil { 56 return err 57 } 58 glusterClient := data.RemoteGlusterCLI{SSHClient: clientStorage} 59 60 // find master node 61 clientMaster, err := plan.GetSSHClient("master") 62 if err != nil { 63 return err 64 } 65 kubernetesClient := data.RemoteKubectl{SSHClient: clientMaster} 66 67 resp, err := buildResponse(glusterClient, kubernetesClient) 68 if err != nil { 69 return err 70 } 71 if resp == nil { 72 fmt.Fprintln(out, "No volumes were found on the cluster. You may use `kismatic volume add` to create new volumes.") 73 return nil 74 } 75 76 return print(out, resp, opts.outputFormat) 77 } 78 79 func buildResponse(glusterClient data.GlusterClient, kubernetesClient data.KubernetesClient) (*ListResponse, error) { 80 // get gluster volume data 81 glusterVolumeInfo, err := glusterClient.ListVolumes() 82 if err != nil { 83 return nil, err 84 } 85 if glusterVolumeInfo == nil { 86 return nil, nil 87 } 88 // get persistent volumes data 89 pvs, err := kubernetesClient.ListPersistentVolumes() 90 if err != nil { 91 return nil, err 92 } 93 // get pods data 94 pods, err := kubernetesClient.ListPods() 95 if err != nil { 96 return nil, err 97 } 98 99 // build a map of pods that have PersistentVolumeClaim 100 podsMap := make(map[string][]Pod) 101 // iterate through all the pods 102 // since the api doesnt have a pv -> pod data, need to search through all the pods 103 // this will get PV -> PVC - > pod(s) -> container(s) 104 if pods != nil { // no pods running 105 for _, pod := range pods.Items { 106 for _, v := range pod.Spec.Volumes { 107 if v.PersistentVolumeClaim != nil { 108 var containers []Container 109 for _, container := range pod.Spec.Containers { 110 for _, volumeMount := range container.VolumeMounts { 111 if volumeMount.Name == v.Name { 112 containers = append(containers, Container{Name: container.Name, MountName: volumeMount.Name, MountPath: volumeMount.MountPath}) 113 } 114 } 115 } 116 // pods that have the same PVC are in one list 117 key := strings.Join([]string{pod.Namespace, v.PersistentVolumeClaim.ClaimName}, ":") 118 p := Pod{Namespace: pod.Namespace, Name: pod.Name, Containers: containers} 119 podsMap[key] = append(podsMap[key], p) 120 } 121 } 122 } 123 } 124 125 // iterate through PVs once and build a map 126 pvsMap := make(map[string]data.PersistentVolume) 127 if pvs != nil { 128 for _, pv := range pvs.Items { 129 pvsMap[pv.Name] = pv 130 } 131 } 132 133 // build response object 134 resp := ListResponse{} 135 // loop through all the gluster volumes 136 for _, gv := range glusterVolumeInfo.VolumeInfo.Volumes.Volume { 137 v := Volume{ 138 Name: gv.Name, 139 DistributionCount: gv.BrickCount / gv.ReplicaCount, //gv.DistCount doesn't actually return the correct number when ReplicaCount > 1 140 ReplicaCount: gv.ReplicaCount, 141 Capacity: "Unknown", 142 Available: "Unknown", 143 Status: "Unknown", 144 } 145 146 if gv.BrickCount > 0 { 147 v.Bricks = make([]Brick, gv.BrickCount) 148 for n, gbrick := range gv.Bricks.Brick { 149 brickArr := strings.Split(gbrick.Text, ":") 150 v.Bricks[n] = Brick{Host: brickArr[0], Path: brickArr[1]} 151 } 152 } 153 // get gluster volume quota 154 glusterVolumeQuota, err := glusterClient.GetQuota(gv.Name) 155 if err != nil { 156 return nil, err 157 } 158 if glusterVolumeQuota != nil && glusterVolumeQuota.VolumeQuota != nil && glusterVolumeQuota.VolumeQuota.Limit != nil { 159 v.Capacity = HumanFormat(glusterVolumeQuota.VolumeQuota.Limit.HardLimit) 160 } 161 if glusterVolumeQuota != nil && glusterVolumeQuota.VolumeQuota != nil && glusterVolumeQuota.VolumeQuota.Limit != nil { 162 v.Available = HumanFormat(glusterVolumeQuota.VolumeQuota.Limit.AvailSpace) 163 } 164 // it is possible that all PVs were delete in kubernetes 165 // set status of gluster volume to "Unknown" 166 foundPVInfo, ok := pvsMap[gv.Name] 167 // this PV does not exist, maybe it was deleted? 168 // set status of gluster volume to "Unknown" 169 if ok { 170 if class, ok := foundPVInfo.ObjectMeta.Annotations["volume.beta.kubernetes.io/storage-class"]; ok { 171 v.StorageClass = class 172 } 173 v.Labels = foundPVInfo.Labels 174 v.Status = string(foundPVInfo.Status.Phase) 175 if foundPVInfo.Spec.ClaimRef != nil { 176 // populate claim info 177 v.Claim = &Claim{Namespace: foundPVInfo.Spec.ClaimRef.Namespace, Name: foundPVInfo.Spec.ClaimRef.Name} 178 // populate pod info 179 key := strings.Join([]string{foundPVInfo.Spec.ClaimRef.Namespace, foundPVInfo.Spec.ClaimRef.Name}, ":") 180 if pod, ok := podsMap[key]; ok && pod != nil { 181 v.Pods = pod 182 } 183 } 184 } 185 186 resp.Volumes = append(resp.Volumes, v) 187 } 188 // return nil if there are no volumes 189 if len(resp.Volumes) == 0 { 190 return nil, nil 191 } 192 return &resp, nil 193 } 194 195 const ( 196 _ = iota // ignore first value by assigning to blank identifier 197 kb float64 = 1 << (10 * iota) 198 mb 199 gb 200 tb 201 ) 202 203 // HumanFormat converts bytes to human readable KB,MB,GB,TB formats 204 func HumanFormat(bytes float64) string { 205 switch { 206 case bytes >= tb: 207 return fmt.Sprintf("%.2fTB", bytes/tb) 208 case bytes >= gb: 209 return fmt.Sprintf("%.2fGB", bytes/gb) 210 case bytes >= mb: 211 return fmt.Sprintf("%.2fMB", bytes/mb) 212 case bytes >= kb: 213 return fmt.Sprintf("%.2fKB", bytes/kb) 214 } 215 return fmt.Sprintf("%.2fB", bytes) 216 } 217 218 // Print prints the volume list response 219 func print(out io.Writer, resp *ListResponse, format string) error { 220 if format == "simple" { 221 separator := "" 222 w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) 223 for _, v := range resp.Volumes { 224 fmt.Fprint(w, separator) 225 fmt.Fprintf(w, "Name:\t%s\t\n", v.Name) 226 fmt.Fprintf(w, "StorageClass:\t%s\t\n", v.StorageClass) 227 if len(v.Labels) > 0 { 228 fmt.Fprintf(w, "Labels:\t\t\n") 229 for k, v := range v.Labels { 230 fmt.Fprintf(w, " %s:\t%s\t\n", k, v) 231 } 232 } 233 fmt.Fprintf(w, "Capacity:\t%s\t\n", v.Capacity) 234 fmt.Fprintf(w, "Available:\t%s\t\n", v.Available) 235 fmt.Fprintf(w, "Replica:\t%d\t\n", v.ReplicaCount) 236 fmt.Fprintf(w, "Distribution:\t%d\t\n", v.DistributionCount) 237 fmt.Fprintf(w, "Bricks:\t%s\t\n", VolumeBrickToString(v.Bricks)) 238 fmt.Fprintf(w, "Status:\t%s\t\n", v.Status) 239 fmt.Fprintf(w, "Claim:\t%s\t\n", v.Claim.Readable()) 240 fmt.Fprintf(w, "Pods:\t\t\n") 241 for _, pod := range v.Pods { 242 fmt.Fprintf(w, " %s\t\n", pod.Readable()) 243 fmt.Fprintf(w, " Containers:\t\t\n") 244 for _, container := range pod.Containers { 245 fmt.Fprintf(w, " %s\t\t\n", container.Name) 246 fmt.Fprintf(w, " MountName:\t%s\t\n", container.MountName) 247 fmt.Fprintf(w, " MountPath:\t%s\t\n", container.MountPath) 248 } 249 } 250 separator = "\n\n" 251 } 252 w.Flush() 253 } else if format == "json" { 254 // pretty prtin JSON 255 prettyResp, err := json.MarshalIndent(resp, "", " ") 256 if err != nil { 257 return fmt.Errorf("marshal error: %v", err) 258 } 259 fmt.Fprintln(out, string(prettyResp)) 260 } 261 262 return nil 263 }