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  }