github.com/Mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/tools/diag.go (about)

     1  /*
     2  Copyright 2018 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tools
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"os"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	v1 "k8s.io/api/core/v1"
    30  
    31  	"github.com/Mirantis/virtlet/pkg/diag"
    32  	"github.com/Mirantis/virtlet/pkg/version"
    33  )
    34  
    35  const (
    36  	maxVirtletPodLogLines         = 20000
    37  	sonobuoyPluginsConfigMapName  = "sonobuoy-plugins-cm"
    38  	virtletSonobuoyPluginFileName = "virtlet.yaml"
    39  	sonobuoyPluginYaml            = `sonobuoy-config:
    40    driver: Job
    41    plugin-name: virtlet
    42    result-type: virtlet
    43  spec:
    44    command:
    45    - /bin/bash
    46    - -c
    47    - /sonobuoy.sh && sleep 3600
    48    env:
    49    - name: RESULTS_DIR
    50      value: /tmp/results
    51    image: mirantis/virtlet$TAG
    52    name: sonobuoy-virtlet
    53    volumeMounts:
    54    - mountPath: /tmp/results
    55      name: results
    56      readOnly: false
    57  `
    58  )
    59  
    60  type diagDumpCommand struct {
    61  	client  KubeClient
    62  	out     io.Writer
    63  	outDir  string
    64  	useJSON bool
    65  }
    66  
    67  // NewDiagDumpCommand returns a new cobra.Command that dumps
    68  // diagnostic information
    69  func NewDiagDumpCommand(client KubeClient, out io.Writer) *cobra.Command {
    70  	d := &diagDumpCommand{
    71  		client: client,
    72  		out:    out,
    73  	}
    74  	cmd := &cobra.Command{
    75  		Use:   "dump output_dir",
    76  		Short: "Dump Virtlet diagnostics information",
    77  		Long:  "Pull Virtlet diagnostics information from the nodes and dump it as a directory tree or JSON",
    78  		RunE: func(cmd *cobra.Command, args []string) error {
    79  			switch {
    80  			case !d.useJSON && len(args) != 1:
    81  				return errors.New("Must specify output directory or --json")
    82  			case !d.useJSON:
    83  				d.outDir = args[0]
    84  			case len(args) != 0:
    85  				return errors.New("This command does not accept arguments")
    86  			}
    87  			return d.Run()
    88  		},
    89  	}
    90  	cmd.Flags().BoolVar(&d.useJSON, "json", false, "Use JSON output")
    91  	return cmd
    92  }
    93  
    94  func (d *diagDumpCommand) diagResult() (diag.Result, error) {
    95  	dr := diag.Result{
    96  		IsDir:    true,
    97  		Name:     "nodes",
    98  		Children: make(map[string]diag.Result),
    99  	}
   100  	podNames, nodeNames, err := d.client.GetVirtletPodAndNodeNames()
   101  	if err != nil {
   102  		return diag.Result{}, err
   103  	}
   104  	for n, podName := range podNames {
   105  		nodeName := nodeNames[n]
   106  		var buf bytes.Buffer
   107  		exitCode, err := d.client.ExecInContainer(
   108  			podName, "virtlet", "kube-system", nil,
   109  			&buf, os.Stderr, []string{"virtlet", "--diag"})
   110  		var cur diag.Result
   111  		switch {
   112  		case err != nil:
   113  			cur = diag.Result{
   114  				Ext:   "err",
   115  				Error: fmt.Sprintf("node %q: error getting version from Virtlet pod %q: %v", nodeName, podName, err),
   116  			}
   117  		case exitCode != 0:
   118  			cur = diag.Result{
   119  				Ext:   "err",
   120  				Error: fmt.Sprintf("node %q: error getting version from Virtlet pod %q: exit code %d", nodeName, podName, exitCode),
   121  			}
   122  		default:
   123  			cur, err = diag.DecodeDiagnostics(buf.Bytes())
   124  			if err != nil {
   125  				cur = diag.Result{
   126  					Ext:   "err",
   127  					Error: fmt.Sprintf("error unmarshalling the diagnostics: %v", err),
   128  				}
   129  			}
   130  		}
   131  		if cur.IsDir {
   132  			if sub, found := cur.Children["diagnostics"]; found && len(dr.Children) == 1 {
   133  				cur = sub
   134  			}
   135  		}
   136  
   137  		if cur.IsDir {
   138  			d.dumpLogs(&cur, podName, "virtlet")
   139  			d.dumpLogs(&cur, podName, "libvirt")
   140  		}
   141  
   142  		cur.Name = nodeName
   143  		dr.Children[nodeName] = cur
   144  	}
   145  	return dr, nil
   146  }
   147  
   148  func (d *diagDumpCommand) dumpLogs(dr *diag.Result, podName, containerName string) {
   149  	cur := diag.Result{
   150  		Name: "virtlet-pod-" + containerName,
   151  		Ext:  "log",
   152  	}
   153  	logs, err := d.client.PodLogs(podName, containerName, "kube-system", maxVirtletPodLogLines)
   154  	if err != nil {
   155  		cur.Error = err.Error()
   156  	} else {
   157  		cur.Data = string(logs)
   158  	}
   159  	dr.Children[cur.Name] = cur
   160  }
   161  
   162  func (d *diagDumpCommand) Run() error {
   163  	dr, err := d.diagResult()
   164  	if err != nil {
   165  		return err
   166  	}
   167  	if d.useJSON {
   168  		d.out.Write(dr.ToJSON())
   169  		return nil
   170  	}
   171  	return dr.Unpack(d.outDir)
   172  }
   173  
   174  type diagUnpackCommand struct {
   175  	in     io.Reader
   176  	outDir string
   177  }
   178  
   179  // NewDiagUnpackCommand returns a new cobra.Command that unpacks
   180  // diagnostic information
   181  func NewDiagUnpackCommand(in io.Reader) *cobra.Command {
   182  	d := &diagUnpackCommand{in: in}
   183  	return &cobra.Command{
   184  		Use:   "unpack output_dir",
   185  		Short: "Unpack Virtlet diagnostics information",
   186  		Long:  "Read Virtlet diagnostics information as JSON from stdin and unpacks into a directory tree",
   187  		RunE: func(cmd *cobra.Command, args []string) error {
   188  			if len(args) != 1 {
   189  				return errors.New("Must specify output directory")
   190  			}
   191  			d.outDir = args[0]
   192  			return d.Run()
   193  		},
   194  	}
   195  }
   196  
   197  func (d *diagUnpackCommand) Run() error {
   198  	data, err := ioutil.ReadAll(d.in)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	dr, err := diag.DecodeDiagnostics(data)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	return dr.Unpack(d.outDir)
   207  }
   208  
   209  type diagSonobuoyCommand struct {
   210  	in  io.Reader
   211  	out io.Writer
   212  	tag string
   213  }
   214  
   215  // NewDiagSonobuoyCommand returns a new cobra.Command that adds
   216  // Virtlet plugin to sonobuoy-generated yaml
   217  func NewDiagSonobuoyCommand(in io.Reader, out io.Writer) *cobra.Command {
   218  	d := &diagSonobuoyCommand{in: in, out: out}
   219  	cmd := &cobra.Command{
   220  		Use:   "sonobuoy",
   221  		Short: "Add Virtlet sonobuoy plugin to the sonobuoy output",
   222  		Long:  "Find and patch sonobuoy configmap in the yaml that's read from stdin to include Virtlet sonobuoy plugin",
   223  		RunE: func(cmd *cobra.Command, args []string) error {
   224  			if len(args) != 0 {
   225  				return errors.New("This command does not accept arguments")
   226  			}
   227  			return d.Run()
   228  		},
   229  	}
   230  	cmd.Flags().StringVar(&d.tag, "tag", version.Get().ImageTag, "Set virtlet image tag for the plugin")
   231  	return cmd
   232  }
   233  
   234  func (d *diagSonobuoyCommand) getYaml() ([]byte, error) {
   235  	bs, err := ioutil.ReadAll(d.in)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	objs, err := LoadYaml(bs)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	if len(objs) == 0 {
   244  		return nil, errors.New("source yaml is empty")
   245  	}
   246  	found := false
   247  	for _, o := range objs {
   248  		cfgMap, ok := o.(*v1.ConfigMap)
   249  		if !ok {
   250  			continue
   251  		}
   252  		if cfgMap.Name != sonobuoyPluginsConfigMapName {
   253  			continue
   254  		}
   255  		found = true
   256  		tagStr := ""
   257  		if d.tag != "" {
   258  			tagStr = ":" + d.tag
   259  		}
   260  		yaml := strings.Replace(sonobuoyPluginYaml, "$TAG", tagStr, -1)
   261  		cfgMap.Data[virtletSonobuoyPluginFileName] = yaml
   262  	}
   263  	if !found {
   264  		return nil, fmt.Errorf("ConfigMap not found: %q", sonobuoyPluginsConfigMapName)
   265  	}
   266  	return ToYaml(objs)
   267  }
   268  
   269  func (d *diagSonobuoyCommand) Run() error {
   270  	bs, err := d.getYaml()
   271  	if err != nil {
   272  		return err
   273  	}
   274  	if _, err := d.out.Write(bs); err != nil {
   275  		return err
   276  	}
   277  	return nil
   278  }
   279  
   280  // NewDiagCommand returns a new cobra.Command that handles Virtlet
   281  // diagnostics.
   282  func NewDiagCommand(client KubeClient, in io.Reader, out io.Writer) *cobra.Command {
   283  	cmd := &cobra.Command{
   284  		Use:   "diag",
   285  		Short: "Virtlet diagnostics",
   286  		Long:  "Retrieve and unpack Virtlet diagnostics information",
   287  	}
   288  	cmd.AddCommand(NewDiagDumpCommand(client, out))
   289  	cmd.AddCommand(NewDiagUnpackCommand(in))
   290  	cmd.AddCommand(NewDiagSonobuoyCommand(in, out))
   291  	return cmd
   292  }