Files
clr-installer/telemetry/telemetry.go
T
puneetse a4bf9679fa Update TelemetryAboutURL
Documentation has moved to https://docs.01.org/clearlinux making the current URL invalid.
2020-02-19 11:08:17 -08:00

417 lines
11 KiB
Go

// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only
package telemetry
import (
"bytes"
"crypto/md5"
"crypto/rand"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/clearlinux/clr-installer/cmd"
"github.com/clearlinux/clr-installer/errors"
"github.com/clearlinux/clr-installer/log"
"github.com/clearlinux/clr-installer/network"
"github.com/clearlinux/clr-installer/utils"
)
const (
// RequiredBundle the bundle needed to use telemetry on the target
RequiredBundle = "telemetrics"
// Default Telemetry configuration file
customTelemetryConf = "/etc/telemetrics/telemetrics.conf"
telemetrySpoolDir = "/var/spool/telemetry"
// Title is a predefined text to display on the Telemetry
Title = `Enable Telemetry`
// Help is a predefined text to display on the Telemetry
// screen for interactive installations
Help = `Allow Clear Linux* OS to collect anonymized system data and usage
statistics for continuous improvement? These reports only relate to
operating system details - no personally identifiable information
is collected.
`
// TelemetryAboutURL is the URL to reference for telemetry details
TelemetryAboutURL = `https://docs.01.org/clearlinux/latest/guides/clear/telemetrics.html`
// telemetry controlling interface command
telemctlCmd = "telemctl"
// RequestNotice is a common text string to be displayed when enabling
// telemetry by default on local networks
RequestNotice = `NOTICE: Enabling Telemetry preferred by default on internal networks.`
// Default Telemetry server
defaultTelemtryServer = "clr.telemetry.intel.com"
maxPayload = 8 * 1024
baseClass = "org.clearlinux/clr-installer"
// Detect hypervisor if running in VM
envCmd = "/usr/bin/systemd-detect-virt"
// recordGen command
recordGenCmd = "telem-record-gen"
// recordGenFullPath command
recordGenCmdPath = "/usr/bin/" + recordGenCmd
// Configuration template
configTemplate = `[settings]
server=%s
tidheader=X-Telemetry-TID:\s%s
`
)
var (
// Policy is the default Telemetry policy to be displayed
// during interactive installations. Overridden by command line or
// configuration options
Policy = "Intel's privacy policy can be found at: http://www.intel.com/privacy."
// ProgVersion is the version of this Clear Installer set from the Model
// since telemetry is a component of the model, can directly include the model here
ProgVersion string
eventID string
)
// Telemetry represents the target system telemetry enabling flag
type Telemetry struct {
Enabled bool
Defined bool
URL string
TID string
requested bool
server string
userDefined bool
}
// randomString generates hex string
func randomString() (string, error) {
randData := make([]byte, 256)
_, err := rand.Read(randData)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", md5.Sum(randData)), nil
}
func init() {
// Initialize the event record ID
eventID, _ = randomString()
}
// SetUserDefined set the user defined flag
func (tl *Telemetry) SetUserDefined(userDefined bool) {
tl.userDefined = userDefined
}
// IsUserDefined returns true if the configuration was interactively
// defined by the user
func (tl *Telemetry) IsUserDefined() bool {
return tl.userDefined
}
// SetRequested set the Requested flag
func (tl *Telemetry) SetRequested(requested bool) {
tl.requested = requested
}
// IsRequested returns true if we are requested telemetry be enabled
func (tl *Telemetry) IsRequested() bool {
return tl.requested
}
// MarshalYAML marshals Telemetry into YAML format
func (tl *Telemetry) MarshalYAML() (interface{}, error) {
if tl.userDefined {
return tl.Enabled, nil
}
return nil, nil
}
// UnmarshalYAML unmarshals Telemetry from YAML format
func (tl *Telemetry) UnmarshalYAML(unmarshal func(interface{}) error) error {
var enabled bool
if err := unmarshal(&enabled); err != nil {
return err
}
// Including telemetry in the YAML will set the default
// as if the user had selected it in the UI
tl.Enabled = enabled
tl.userDefined = true
return nil
}
// SetEnable sets the enabled flag and sets this is an user defined configuration
func (tl *Telemetry) SetEnable(enable bool) {
tl.Enabled = enable
if tl.server == "" {
tl.server = defaultTelemtryServer
}
}
// SetTelemetryServer set new defaults for the Telemetry server
// to override the built-in defaults
func (tl *Telemetry) SetTelemetryServer(telmURL string, telmID string, telmPolicy string) error {
u, err := url.Parse(telmURL)
if err != nil {
return fmt.Errorf("Could not determine provided telemetry server name from URL (%s): %v", telmURL, err)
}
if urlErr := network.CheckURL(telmURL); urlErr != nil {
return fmt.Errorf("Server not responding")
}
log.Debug("Using Telemetry URL: %q with TID: %q", telmURL, telmID)
tl.URL = telmURL
tl.TID = telmID
tl.server = u.Hostname()
// Set the policy
Policy = telmPolicy
return nil
}
// IsUsingPrivateIP return true if the current image is resolving
// the Telemetry server to a Private network IP address
func (tl *Telemetry) IsUsingPrivateIP() bool {
inside := false
if ips, err := net.LookupIP(tl.server); err == nil {
// Create networks for all known Private Networks
_, ipNetPriv10, _ := net.ParseCIDR("10.0.0.0/8")
_, ipNetPriv172, _ := net.ParseCIDR("172.16.0.0/12")
_, ipNetPriv192, _ := net.ParseCIDR("192.168.0.0/16")
// Bounds checking to ensure we do not get stuck
if len(ips) > 256 {
ips = ips[:256]
}
for _, ip := range ips {
if ip.DefaultMask() == nil {
log.Warning("PrivateIP: Ignoring non-IPv4 IP address: %s", ip)
continue
}
in := ipNetPriv10.Contains(ip) ||
ipNetPriv172.Contains(ip) ||
ipNetPriv192.Contains(ip)
log.Debug("PrivateIP: Found IP: %s, Private IP?: %s", ip, strconv.FormatBool(in))
if in {
inside = true
}
}
} else {
log.Warning("PrivateIP: Could not determine network location: %v", err)
}
return inside
}
// CreateTelemetryConf create a custom Telemetry configuration file
// using the customer server and ID
func (tl *Telemetry) CreateTelemetryConf(rootDir string) error {
// Ensure the customer configuration file directory exists
targetConfFile := filepath.Join(rootDir, customTelemetryConf)
targetConfDir := filepath.Dir(targetConfFile)
if err := utils.MkdirAll(targetConfDir, 0755); err != nil {
return err
}
var confFile = fmt.Sprintf(configTemplate, tl.URL, tl.TID)
// Write the new file
writeErr := ioutil.WriteFile(targetConfFile, []byte(confFile), 0644)
if writeErr != nil {
return writeErr
}
log.Debug("Created Telemetry server configuration file with URL %q and tag %q", tl.URL, tl.TID)
return nil
}
// CopyTelemetryRecords copies the local spooled telemetry records
// to the target system to be uploaded when the target system is
// booted and telemetry is enabled.
func (tl *Telemetry) CopyTelemetryRecords(rootDir string) error {
// Get directory ownership
spoolDirInfo, err := os.Stat(telemetrySpoolDir)
if err != nil {
return fmt.Errorf("Unable to stat %q, %s", filepath.Join(telemetrySpoolDir), err)
}
sys := spoolDirInfo.Sys()
stat, ok := sys.(*syscall.Stat_t)
if !ok {
return fmt.Errorf("Could not stat telemetry %q", telemetrySpoolDir)
}
err = filepath.Walk(telemetrySpoolDir,
func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Warning("Failure accessing a path %q: %v\n", path, err)
return nil
}
target := filepath.Join(rootDir, path)
if info.IsDir() {
// Telemetry spool directory is flat
return nil
}
if err := utils.CopyFile(path, target); err != nil {
log.Warning("Failed to copy telemetry %q", target)
}
// Ensure all contents is owned correctly
if err := os.Chown(target, int(stat.Uid), int(stat.Gid)); err != nil {
log.Warning("Failed to change ownership of %q to UID:%d, GID:%d",
target, stat.Uid, stat.Gid)
}
return nil
})
if err != nil {
return fmt.Errorf("Failed to archive telemetry records")
}
return nil
}
// Installed returns true if telemetry tooling is present, false otherwise
func (tl *Telemetry) Installed() bool {
if _, err := os.Stat(recordGenCmdPath); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// LogRecord generates and saves a Telemetry record
func (tl *Telemetry) LogRecord(class string, severity int, payload string) error {
// Skip record generation if telemetry tooling is not present, and do not
// return an error since telemetry bundle is not a requirement.
if tl.Installed() == false {
log.Warning("Telemetry is not present in the installer, skip record generation")
return nil
}
w := bytes.NewBuffer(nil)
if severity < 1 {
log.Warning("Telemetry severity (%d) less than 1, defaulting to 1", severity)
severity = 1
} else if severity > 4 {
log.Warning("Telemetry severity (%d) greater than 4, defaulting to 4", severity)
severity = 4
}
payload = "version=" + ProgVersion + "\n" + payload
paySize := len(payload)
if paySize > maxPayload {
drop := payload[maxPayload:]
payload = payload[0:(maxPayload - 1)]
log.Warning("Telemetry payload greater than %d bytes, truncating: %q", maxPayload, drop)
}
args := []string{
recordGenCmd,
"--severity",
fmt.Sprintf("%d", severity),
"--class",
fmt.Sprintf("%s/%s", baseClass, class),
"--no-post",
"--echo",
}
if eventID != "" {
args = append(args,
[]string{
"--event-id", eventID,
}...)
}
args = append(args,
[]string{
"--payload", payload,
}...)
err := cmd.Run(w, args...)
if err != nil {
return errors.Wrap(err)
}
recordName, err := randomString()
if err != nil {
return errors.Wrap(err)
}
recordName = recordName[:6]
telemetryFilename := filepath.Join(telemetrySpoolDir, recordName)
if err := ioutil.WriteFile(telemetryFilename, w.Bytes(), 0644); err != nil {
log.Info(err.Error())
return errors.Wrap(err)
}
return nil
}
// opt is a helper function
func opt(rootDir string, optCmd string) error {
args := []string{
"chroot",
rootDir,
telemctlCmd,
optCmd,
}
err := cmd.RunAndLog(args...)
if err != nil {
return errors.Wrap(err)
}
return nil
}
// OptIn runs the command to opt-in to telemetry when users accept to
// provide telemetry from crashes
func (tl *Telemetry) OptIn(rootDir string) error {
return opt(rootDir, "opt-in")
}
// OptOut runs the command to opt-out to telemetry when users do not
// agree to provide telemetry from crashes
func (tl *Telemetry) OptOut(rootDir string) error {
return opt(rootDir, "opt-out")
}
// RunningEnvironment returns the name of the hypervisor if running in a
// virtual machine, otherwise none
func (tl *Telemetry) RunningEnvironment() string {
out, err := exec.Command(envCmd).Output()
if err == nil {
return strings.TrimRight(string(out), "\n")
}
return "none"
}