mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
514 lines
13 KiB
Go
514 lines
13 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2017 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package errtracker
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/snapcore/bolt"
|
|
"gopkg.in/mgo.v2/bson"
|
|
|
|
"github.com/snapcore/snapd/arch"
|
|
"github.com/snapcore/snapd/dirs"
|
|
"github.com/snapcore/snapd/logger"
|
|
"github.com/snapcore/snapd/osutil"
|
|
"github.com/snapcore/snapd/release"
|
|
"github.com/snapcore/snapd/snapdenv"
|
|
"github.com/snapcore/snapd/snapdtool"
|
|
)
|
|
|
|
var (
|
|
CrashDbURLBase string
|
|
SnapdVersion string
|
|
|
|
// The machine-id file is at different locations depending on how the system
|
|
// is setup. On Fedora for example /var/lib/dbus/machine-id doesn't exist
|
|
// but we have /etc/machine-id. See
|
|
// https://www.freedesktop.org/software/systemd/man/machine-id.html for a
|
|
// few more details.
|
|
machineIDs = []string{"/etc/machine-id", "/var/lib/dbus/machine-id"}
|
|
|
|
mockedHostSnapd = ""
|
|
mockedCoreSnapd = ""
|
|
|
|
snapConfineProfile = "/etc/apparmor.d/usr.lib.snapd.snap-confine"
|
|
|
|
procCpuinfo = "/proc/cpuinfo"
|
|
procSelfExe = "/proc/self/exe"
|
|
procSelfCwd = "/proc/self/cwd"
|
|
procSelfCmdline = "/proc/self/cmdline"
|
|
|
|
osGetenv = os.Getenv
|
|
timeNow = time.Now
|
|
)
|
|
|
|
type reportsDB struct {
|
|
db *bolt.DB
|
|
|
|
// time until an error report is cleaned from the database,
|
|
// usually 7 days
|
|
cleanupTime time.Duration
|
|
}
|
|
|
|
func hashString(s string) string {
|
|
h := sha512.New()
|
|
io.WriteString(h, s)
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
func newReportsDB(fname string) (*reportsDB, error) {
|
|
if err := os.MkdirAll(filepath.Dir(fname), 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
bdb, err := bolt.Open(fname, 0600, &bolt.Options{
|
|
Timeout: 10 * time.Second,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bdb.Update(func(tx *bolt.Tx) error {
|
|
_, err := tx.CreateBucketIfNotExists([]byte("reported"))
|
|
if err != nil {
|
|
return fmt.Errorf("create bucket: %s", err)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
db := &reportsDB{
|
|
db: bdb,
|
|
cleanupTime: time.Duration(7 * 24 * time.Hour),
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
func (db *reportsDB) Close() error {
|
|
return db.db.Close()
|
|
}
|
|
|
|
// AlreadyReported returns true if an identical report has been sent recently
|
|
func (db *reportsDB) AlreadyReported(dupSig string) bool {
|
|
// robustness
|
|
if db == nil {
|
|
return false
|
|
}
|
|
var reported []byte
|
|
db.db.View(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket([]byte("reported"))
|
|
reported = b.Get([]byte(hashString(dupSig)))
|
|
return nil
|
|
})
|
|
return len(reported) > 0
|
|
}
|
|
|
|
func (db *reportsDB) cleanupOldRecords() {
|
|
db.db.Update(func(tx *bolt.Tx) error {
|
|
now := time.Now()
|
|
|
|
b := tx.Bucket([]byte("reported"))
|
|
b.ForEach(func(dupSigHash, reportTime []byte) error {
|
|
var t time.Time
|
|
t.UnmarshalBinary(reportTime)
|
|
|
|
if now.After(t.Add(db.cleanupTime)) {
|
|
if err := b.Delete(dupSigHash); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// MarkReported marks an error report as reported to the error tracker
|
|
func (db *reportsDB) MarkReported(dupSig string) error {
|
|
// robustness
|
|
if db == nil {
|
|
return fmt.Errorf("cannot mark error report as reported with an uninitialized reports database")
|
|
}
|
|
db.cleanupOldRecords()
|
|
|
|
return db.db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket([]byte("reported"))
|
|
tb, err := time.Now().MarshalBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.Put([]byte(hashString(dupSig)), tb)
|
|
})
|
|
}
|
|
|
|
func whoopsieEnabled() bool {
|
|
cmd := exec.Command("systemctl", "is-enabled", "whoopsie.service")
|
|
output, _ := cmd.CombinedOutput()
|
|
switch string(output) {
|
|
case "enabled\n":
|
|
return true
|
|
case "disabled\n":
|
|
return false
|
|
default:
|
|
logger.Debugf("unexpected output when checking for whoopsie.service (not installed?): %s", output)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// distroRelease returns a distro release as it is expected by daisy.ubuntu.com
|
|
func distroRelease() string {
|
|
ID := release.ReleaseInfo.ID
|
|
if ID == "ubuntu" {
|
|
ID = "Ubuntu"
|
|
}
|
|
|
|
return fmt.Sprintf("%s %s", ID, release.ReleaseInfo.VersionID)
|
|
}
|
|
|
|
func readMachineID() ([]byte, error) {
|
|
for _, id := range machineIDs {
|
|
machineID, err := ioutil.ReadFile(id)
|
|
if err == nil {
|
|
return bytes.TrimSpace(machineID), nil
|
|
} else if !os.IsNotExist(err) {
|
|
logger.Noticef("cannot read %s: %s", id, err)
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("cannot report: no suitable machine id file found")
|
|
}
|
|
|
|
func snapConfineProfileDigest(suffix string) string {
|
|
profileText, err := ioutil.ReadFile(filepath.Join(dirs.GlobalRootDir, snapConfineProfile+suffix))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
// NOTE: uses md5sum for easier comparison against dpkg meta-data
|
|
return fmt.Sprintf("%x", md5.Sum(profileText))
|
|
}
|
|
|
|
var didSnapdReExec = func() string {
|
|
didReexec, err := snapdtool.IsReexecd()
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
if didReexec {
|
|
return "yes"
|
|
}
|
|
return "no"
|
|
}
|
|
|
|
// Report reports an error with the given snap to the error tracker
|
|
func Report(snap, errMsg, dupSig string, extra map[string]string) (string, error) {
|
|
if extra == nil {
|
|
extra = make(map[string]string)
|
|
}
|
|
extra["ProblemType"] = "Snap"
|
|
extra["Snap"] = snap
|
|
|
|
// check if we haven't already reported this error
|
|
db, err := newReportsDB(dirs.ErrtrackerDbDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot open error reports database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if db.AlreadyReported(dupSig) {
|
|
return "already-reported", nil
|
|
}
|
|
|
|
// do the actual report
|
|
oopsID, err := report(errMsg, dupSig, extra)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := db.MarkReported(dupSig); err != nil {
|
|
logger.Noticef("cannot mark %s as reported: %s", oopsID, err)
|
|
}
|
|
|
|
return oopsID, nil
|
|
}
|
|
|
|
// ReportRepair reports an error with the given repair assertion script
|
|
// to the error tracker
|
|
func ReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) {
|
|
if extra == nil {
|
|
extra = make(map[string]string)
|
|
}
|
|
extra["ProblemType"] = "Repair"
|
|
extra["Repair"] = repair
|
|
|
|
return report(errMsg, dupSig, extra)
|
|
}
|
|
|
|
func detectVirt() string {
|
|
cmd := exec.Command("systemd-detect-virt")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
|
|
func journalError() string {
|
|
// TODO: look into using systemd package (needs refactor)
|
|
|
|
// Before changing this line to be more consistent or nicer or anything
|
|
// else, remember it needs to run a lot of different systemd's: today,
|
|
// anything from 238 (on arch) to 204 (on ubuntu 14.04); this is why
|
|
// doing the refactor to the systemd package to only worry about this in
|
|
// there might be worth it.
|
|
output, err := exec.Command("journalctl", "-b", "--priority=warning..err", "--lines=1000").CombinedOutput()
|
|
if err != nil {
|
|
if len(output) == 0 {
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
output = append(output, fmt.Sprintf("\nerror: %v", err)...)
|
|
}
|
|
return string(output)
|
|
}
|
|
|
|
func procCpuinfoMinimal() string {
|
|
buf, err := ioutil.ReadFile(procCpuinfo)
|
|
if err != nil {
|
|
// if we can't read cpuinfo, we want to know _why_
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
idx := bytes.LastIndex(buf, []byte("\nprocessor\t:"))
|
|
|
|
// if not found (which will happen on non-x86 architectures, which is ok
|
|
// because they'd typically not have the same info over and over again),
|
|
// return whole buffer; otherwise, return from just after the \n
|
|
return string(buf[idx+1:])
|
|
}
|
|
|
|
func procExe() string {
|
|
out, err := os.Readlink(procSelfExe)
|
|
if err != nil {
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func procCwd() string {
|
|
out, err := os.Readlink(procSelfCwd)
|
|
if err != nil {
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func procCmdline() string {
|
|
out, err := ioutil.ReadFile(procSelfCmdline)
|
|
if err != nil {
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func environ() string {
|
|
safeVars := []string{
|
|
"SHELL", "TERM", "LANGUAGE", "LANG", "LC_CTYPE",
|
|
"LC_COLLATE", "LC_TIME", "LC_NUMERIC",
|
|
"LC_MONETARY", "LC_MESSAGES", "LC_PAPER",
|
|
"LC_NAME", "LC_ADDRESS", "LC_TELEPHONE",
|
|
"LC_MEASUREMENT", "LC_IDENTIFICATION", "LOCPATH",
|
|
}
|
|
unsafeVars := []string{"XDG_RUNTIME_DIR", "LD_PRELOAD", "LD_LIBRARY_PATH"}
|
|
knownPaths := map[string]bool{
|
|
"/snap/bin": true,
|
|
"/var/lib/snapd/snap/bin": true,
|
|
"/sbin": true,
|
|
"/bin": true,
|
|
"/usr/sbin": true,
|
|
"/usr/bin": true,
|
|
"/usr/local/sbin": true,
|
|
"/usr/local/bin": true,
|
|
"/usr/local/games": true,
|
|
"/usr/games": true,
|
|
}
|
|
|
|
// + 1 for PATH
|
|
out := make([]string, 0, len(safeVars)+len(unsafeVars)+1)
|
|
|
|
for _, k := range safeVars {
|
|
if v := osGetenv(k); v != "" {
|
|
out = append(out, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
}
|
|
|
|
for _, k := range unsafeVars {
|
|
if v := osGetenv(k); v != "" {
|
|
out = append(out, k+"=<set>")
|
|
}
|
|
}
|
|
|
|
if paths := filepath.SplitList(osGetenv("PATH")); len(paths) > 0 {
|
|
for i, p := range paths {
|
|
p = filepath.Clean(p)
|
|
if !knownPaths[p] {
|
|
if strings.Contains(p, "/home") || strings.Contains(p, "/tmp") {
|
|
p = "(user)"
|
|
} else {
|
|
p = "(custom)"
|
|
}
|
|
}
|
|
paths[i] = p
|
|
}
|
|
out = append(out, fmt.Sprintf("PATH=%s", strings.Join(paths, string(filepath.ListSeparator))))
|
|
}
|
|
|
|
return strings.Join(out, "\n")
|
|
}
|
|
|
|
func report(errMsg, dupSig string, extra map[string]string) (string, error) {
|
|
if CrashDbURLBase == "" {
|
|
return "", nil
|
|
}
|
|
if extra == nil || extra["ProblemType"] == "" {
|
|
return "", fmt.Errorf(`key "ProblemType" not set in %v`, extra)
|
|
}
|
|
|
|
if !whoopsieEnabled() {
|
|
return "", nil
|
|
}
|
|
|
|
machineID, err := readMachineID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
identifier := fmt.Sprintf("%x", sha512.Sum512(machineID))
|
|
|
|
crashDbUrl := fmt.Sprintf("%s/%s", CrashDbURLBase, identifier)
|
|
|
|
hostSnapdPath := filepath.Join(dirs.DistroLibExecDir, "snapd")
|
|
coreSnapdPath := filepath.Join(dirs.SnapMountDir, "core/current/usr/lib/snapd/snapd")
|
|
if mockedHostSnapd != "" {
|
|
hostSnapdPath = mockedHostSnapd
|
|
}
|
|
if mockedCoreSnapd != "" {
|
|
coreSnapdPath = mockedCoreSnapd
|
|
}
|
|
hostBuildID, _ := osutil.ReadBuildID(hostSnapdPath)
|
|
coreBuildID, _ := osutil.ReadBuildID(coreSnapdPath)
|
|
if hostBuildID == "" {
|
|
hostBuildID = "unknown"
|
|
}
|
|
if coreBuildID == "" {
|
|
coreBuildID = "unknown"
|
|
}
|
|
|
|
report := map[string]string{
|
|
"Architecture": arch.DpkgArchitecture(),
|
|
"SnapdVersion": SnapdVersion,
|
|
"DistroRelease": distroRelease(),
|
|
"HostSnapdBuildID": hostBuildID,
|
|
"CoreSnapdBuildID": coreBuildID,
|
|
"Date": timeNow().Format(time.ANSIC),
|
|
"KernelVersion": osutil.KernelVersion(),
|
|
"ErrorMessage": errMsg,
|
|
"DuplicateSignature": dupSig,
|
|
|
|
"JournalError": journalError(),
|
|
"ExecutablePath": procExe(),
|
|
"ProcCmdline": procCmdline(),
|
|
"ProcCpuinfoMinimal": procCpuinfoMinimal(),
|
|
"ProcCwd": procCwd(),
|
|
"ProcEnviron": environ(),
|
|
"DetectedVirt": detectVirt(),
|
|
"SourcePackage": "snapd",
|
|
|
|
"DidSnapdReExec": didSnapdReExec(),
|
|
}
|
|
|
|
if desktop := osGetenv("XDG_CURRENT_DESKTOP"); desktop != "" {
|
|
report["CurrentDesktop"] = desktop
|
|
}
|
|
|
|
for k, v := range extra {
|
|
// only set if empty
|
|
if _, ok := report[k]; !ok {
|
|
report[k] = v
|
|
}
|
|
}
|
|
|
|
// include md5 hashes of the apparmor conffile for easier debbuging
|
|
// of not-updated snap-confine apparmor profiles
|
|
for _, sp := range []struct {
|
|
suffix string
|
|
key string
|
|
}{
|
|
{"", "MD5SumSnapConfineAppArmorProfile"},
|
|
{".dpkg-new", "MD5SumSnapConfineAppArmorProfileDpkgNew"},
|
|
{".real", "MD5SumSnapConfineAppArmorProfileReal"},
|
|
{".real.dpkg-new", "MD5SumSnapConfineAppArmorProfileRealDpkgNew"},
|
|
} {
|
|
digest := snapConfineProfileDigest(sp.suffix)
|
|
if digest != "" {
|
|
report[sp.key] = digest
|
|
}
|
|
|
|
}
|
|
|
|
// see if we run in testing mode
|
|
if snapdenv.Testing() {
|
|
logger.Noticef("errtracker.Report is *not* sent because SNAPPY_TESTING is set")
|
|
logger.Noticef("report: %v", report)
|
|
return "oops-not-sent", nil
|
|
}
|
|
|
|
// send it for real
|
|
reportBson, err := bson.Marshal(report)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
client := &http.Client{}
|
|
req, err := http.NewRequest("POST", crashDbUrl, bytes.NewBuffer(reportBson))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Add("Content-Type", "application/octet-stream")
|
|
req.Header.Add("X-Whoopsie-Version", snapdenv.UserAgent())
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf("cannot upload error report, return code: %d", resp.StatusCode)
|
|
}
|
|
oopsID, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(oopsID), nil
|
|
}
|