github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/pkg/sysmetrics/run.go (about)

     1  package sysmetrics
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/pkg/errors"
    15  	log "github.com/sirupsen/logrus"
    16  	"github.com/ubuntu/ubuntu-report/internal/metrics"
    17  	"github.com/ubuntu/ubuntu-report/internal/sender"
    18  	"github.com/ubuntu/ubuntu-report/internal/utils"
    19  )
    20  
    21  // optOutJSON is the data sent in case of Opt-Out choice
    22  const optOutJSON = `{"OptOut": true}`
    23  
    24  var (
    25  	initialReportTimeoutDuration = 30 * time.Second
    26  )
    27  
    28  func metricsCollect(m metrics.Metrics) ([]byte, error) {
    29  	data, err := m.Collect()
    30  	if err != nil {
    31  		return nil, errors.Wrapf(err, "couldn't collect system minimal info")
    32  	}
    33  
    34  	log.Debug("pretty print format the collected data to the user")
    35  	h := json.RawMessage(data)
    36  	return json.MarshalIndent(&h, "", "  ")
    37  }
    38  
    39  func metricsSend(m metrics.Metrics, data []byte, acknowledgement, alwaysReport bool, baseURL string, reportBasePath string, in io.Reader, out io.Writer) error {
    40  	distro, version, err := m.GetIDS()
    41  	if err != nil {
    42  		return errors.Wrapf(err, "couldn't get mandatory information")
    43  	}
    44  
    45  	reportP, err := checkPreviousReport(distro, version, reportBasePath, alwaysReport)
    46  	if err != nil {
    47  		return err
    48  	}
    49  
    50  	// erase potential collected data
    51  	if !acknowledgement {
    52  		data = []byte(optOutJSON)
    53  	}
    54  
    55  	if baseURL == "" {
    56  		baseURL = sender.BaseURL
    57  	}
    58  	u, err := sender.GetURL(baseURL, distro, version)
    59  	if err != nil {
    60  		return errors.Wrapf(err, "report destination url is invalid")
    61  	}
    62  	if err := sender.Send(u, data); err != nil {
    63  		returnErr := errors.Wrapf(err, "data were not delivered successfully to metrics server, saving for a later automated report")
    64  		p, err := utils.PendingReportPath(reportBasePath)
    65  		if err != nil {
    66  			return errors.Wrapf(err, "couldn't get where pending reported metrics should be stored on disk: %v", returnErr)
    67  		}
    68  		if err := saveMetrics(p, data); err != nil {
    69  			return errors.Wrapf(err, "couldn't save pending reported are on disk: %v", returnErr)
    70  		}
    71  		return returnErr
    72  	}
    73  
    74  	return saveMetrics(reportP, data)
    75  }
    76  
    77  func metricsCollectAndSend(m metrics.Metrics, r ReportType, alwaysReport bool, baseURL string, reportBasePath string, in io.Reader, out io.Writer) error {
    78  	distro, version, err := m.GetIDS()
    79  	if err != nil {
    80  		return errors.Wrapf(err, "couldn't get mandatory information")
    81  	}
    82  
    83  	if _, err := checkPreviousReport(distro, version, reportBasePath, alwaysReport); err != nil {
    84  		return err
    85  	}
    86  
    87  	var data []byte
    88  	if r != ReportOptOut {
    89  		if data, err = metricsCollect(m); err != nil {
    90  			return errors.Wrapf(err, "couldn't collect system minimal info and format it")
    91  		}
    92  	}
    93  
    94  	sendMetrics := true
    95  	if r == ReportInteractive {
    96  		fmt.Fprintln(out, "This is the result of hardware and optional installer/upgrader that we collected:")
    97  		fmt.Fprintln(out, string(data))
    98  
    99  		validAnswer := false
   100  		scanner := bufio.NewScanner(in)
   101  		for validAnswer != true {
   102  			fmt.Fprintf(out, "Do you agree to report this? [y (send metrics)/n (send opt out message)/Q (quit)] ")
   103  			if !scanner.Scan() {
   104  				log.Info("programm interrupted")
   105  				return nil
   106  			}
   107  			text := strings.ToLower(strings.TrimSpace(scanner.Text()))
   108  			if text == "n" || text == "no" {
   109  				log.Debug("sending report was denied")
   110  				sendMetrics = false
   111  				validAnswer = true
   112  			} else if text == "y" || text == "yes" {
   113  				log.Debug("sending report was accepted")
   114  				sendMetrics = true
   115  				validAnswer = true
   116  			} else if text == "q" || text == "quit" || text == "" {
   117  				return nil
   118  			}
   119  			if validAnswer != true {
   120  				log.Error("we didn't understand your answer")
   121  			}
   122  		}
   123  	} else if r == ReportAuto {
   124  		log.Debug("auto report requested")
   125  		sendMetrics = true
   126  	} else {
   127  		log.Debug("opt-out report requested")
   128  		sendMetrics = false
   129  	}
   130  
   131  	return metricsSend(m, data, sendMetrics, alwaysReport, baseURL, reportBasePath, in, out)
   132  }
   133  
   134  func metricsCollectAndSendOnUpgrade(m metrics.Metrics, alwaysReport bool, baseURL string, reportBasePath string, in io.Reader, out io.Writer) error {
   135  	distro, version, err := m.GetIDS()
   136  	if err != nil {
   137  		return errors.Wrapf(err, "couldn't get mandatory information")
   138  	}
   139  
   140  	if _, err := checkPreviousReport(distro, version, reportBasePath, alwaysReport); err != nil {
   141  		return err
   142  	}
   143  
   144  	latestReportFile, err := getLastReport(distro, reportBasePath, alwaysReport)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	if latestReportFile == "" {
   149  		log.Debug("no previous report found, no upgrade report to generate then")
   150  		return nil
   151  	}
   152  
   153  	r := ReportOptOut
   154  	b, err := ioutil.ReadFile(latestReportFile)
   155  	if err != nil {
   156  		return errors.Wrapf(err, "not able to read latest report content")
   157  	}
   158  	if strings.TrimSpace(string(b)) != optOutJSON {
   159  		r = ReportAuto
   160  	}
   161  
   162  	return metricsCollectAndSend(m, r, alwaysReport, baseURL, reportBasePath, in, out)
   163  }
   164  
   165  func saveMetrics(p string, data []byte) error {
   166  	log.Debugf("save sent metrics to %s", p)
   167  
   168  	d := filepath.Dir(p)
   169  	if err := os.MkdirAll(d, 0700); err != nil {
   170  		return errors.Wrap(err, "couldn't create parent directory to save reported metrics")
   171  	}
   172  
   173  	if err := ioutil.WriteFile(p, data, 0666); err != nil {
   174  		return errors.Wrap(err, "couldn't save reported or pending metrics on disk")
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  func checkPreviousReport(distro, version, reportBasePath string, alwaysReport bool) (string, error) {
   181  	p, err := utils.ReportPath(distro, version, reportBasePath)
   182  	if err != nil {
   183  		return "", errors.Wrapf(err, "couldn't get where to save reported metrics on disk")
   184  	}
   185  	if _, err := os.Stat(p); !os.IsNotExist(err) {
   186  		log.Infof("previous report found in %s", p)
   187  		if !alwaysReport {
   188  			return "", errors.Errorf("metrics from this machine have already been reported and can be found in: %s", p)
   189  		}
   190  		log.Debug("ignore previous report requested")
   191  	}
   192  	return p, nil
   193  }
   194  
   195  func getLastReport(distro, reportBasePath string, alwaysReport bool) (string, error) {
   196  	p, err := utils.ReportPath(distro, "*", reportBasePath)
   197  	if err != nil {
   198  		return "", errors.Wrapf(err, "couldn't get path where metrics are reported on disk")
   199  	}
   200  
   201  	files, err := filepath.Glob(p)
   202  	if err != nil {
   203  		return "", errors.Wrapf(err, "incorrect pattern: %s", p)
   204  	}
   205  	newestReport := ""
   206  	for _, f := range files {
   207  		if f > newestReport {
   208  			newestReport = f
   209  		}
   210  	}
   211  	return newestReport, nil
   212  }
   213  
   214  func metricsSendPendingReport(m metrics.Metrics, baseURL, reportBasePath string, in io.Reader, out io.Writer) error {
   215  	distro, version, err := m.GetIDS()
   216  	if err != nil {
   217  		return errors.Wrapf(err, "couldn't get mandatory information")
   218  	}
   219  
   220  	reportP, err := utils.ReportPath(distro, version, reportBasePath)
   221  	if err != nil {
   222  		return errors.Wrapf(err, "couldn't get where to save reported metrics on disk")
   223  	}
   224  
   225  	pending, err := utils.PendingReportPath(reportBasePath)
   226  	if err != nil {
   227  		return errors.Wrapf(err, "couldn't get where to previous reported metrics are on disk")
   228  	}
   229  	data, err := ioutil.ReadFile(pending)
   230  	if err != nil {
   231  		return errors.Wrapf(err, "no pending report found")
   232  	}
   233  
   234  	if baseURL == "" {
   235  		baseURL = sender.BaseURL
   236  	}
   237  	u, err := sender.GetURL(baseURL, distro, version)
   238  	if err != nil {
   239  		return errors.Wrapf(err, "report destination url is invalid")
   240  	}
   241  
   242  	wait := time.Duration(initialReportTimeoutDuration)
   243  	for {
   244  		if err := sender.Send(u, data); err != nil {
   245  			log.Errorf("data were not delivered successfully to metrics server, retrying in %ds", wait/(1000*1000*1000))
   246  			time.Sleep(wait)
   247  			wait = wait * 2
   248  			if wait > time.Duration(30*time.Minute) {
   249  				wait = time.Duration(30 * time.Minute)
   250  			}
   251  			continue
   252  		}
   253  		break
   254  	}
   255  
   256  	if err := os.Remove(pending); err != nil {
   257  		return errors.Wrapf(err, "couldn't remove pending report after a successful report")
   258  	}
   259  	return saveMetrics(reportP, data)
   260  }