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 }