| // Copyright 2021 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Implementation of the Reporter interface that gathers instance status from |
| // the update-engine and the /etc/lsb-release file. |
| package imgstatus |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "policy_manager/dbus" |
| "policy_manager/sysapi" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "policy_manager/policyenforcer" |
| pmpb "policy_manager/policymanagerproto" |
| |
| "github.com/golang/protobuf/proto" |
| ) |
| |
| // Constants related to lsb-release file. |
| const ( |
| lsbFile = "/etc/lsb-release" |
| lsbChromeOSReleaseKey = "CHROMEOS_RELEASE_VERSION" |
| lsbChromeOSReleaseChannelKey = "CHROMEOS_RELEASE_TRACK" |
| lsbChromeOSReleaseMilestoneKey = "CHROMEOS_RELEASE_CHROME_MILESTONE" |
| ) |
| |
| // reporterImpl implements the Reporter interface. It talks to the underlying |
| // system through the sysapi handler so it can be run on both COS and non-COS |
| // machines. |
| type reporterImpl struct { |
| // currentOSVersion is a protobuf containing informatin of the current |
| // OS version being run. |
| currentOSVersion *pmpb.OSVersion |
| |
| // api is the APIHandler to use for making calls that depend on the |
| // system we are running on. |
| api sysapi.APIHandler |
| |
| // ueClient is the UpdateEngineClient for interacting with the update |
| // engine. |
| ueClient dbus.UpdateEngineClient |
| |
| // policyEnforcer is the PolicyEnforcer for interacting with |
| // enforcement around COS device policy, including health monitor logging |
| // and monitoring. |
| policyEnforcer policyenforcer.PolicyEnforcer |
| |
| // instanceID is the instance ID of the GCE instance. |
| instanceID uint64 |
| } |
| |
| // parseKeyValuePairs parses text with the form |
| // key1=value1 |
| // key2=value2 |
| // ... |
| // into a map. |
| // All keys must have the format: "[a-zA-Z_]*". |
| // Any non-conforming lines are ignored. |
| func parseKeyValuePairs(reader io.Reader) (map[string]string, error) { |
| content, err := ioutil.ReadAll(reader) |
| if err != nil { |
| return nil, err |
| } |
| |
| pairs := make(map[string]string) |
| |
| // Regexp for matching key-value pairs. |
| matchPair := regexp.MustCompile(`[a-zA-Z_]*=.*$`) |
| |
| // Extract release version string. |
| lines := strings.Split(string(content), "\n") |
| for _, line := range lines { |
| line = strings.TrimSpace(line) |
| if matchPair.MatchString(line) { |
| pair := strings.Split(line, "=") |
| key := strings.TrimSpace(pair[0]) |
| val := strings.TrimSpace(pair[1]) |
| pairs[key] = val |
| } |
| } |
| |
| return pairs, nil |
| } |
| |
| // convertOSReleaseChannel takes in a release channel string from the |
| // lsb-release file and converts it into a ReleaseChannel type. |
| // If the release channel string cannot be parsed, |
| // RELEASE_CHANNEL_UNSPECIFIED will be returned. |
| func convertOSReleaseChannel(channelString string) pmpb.ReleaseChannel { |
| // Regexp for matching release channel strings. |
| // Release channel strings have the format "<name>-channel". For |
| // example, "dev-channel". |
| pattern := `^([a-z]{1,}-channel)$` |
| matchVersionString := regexp.MustCompile(pattern) |
| |
| // Check we have a valid release channel string. |
| if matchVersionString.MatchString(channelString) { |
| ch := strings.ToUpper(strings.Split(channelString, "-")[0]) |
| if c, ok := pmpb.ReleaseChannel_value[ch]; ok { |
| return pmpb.ReleaseChannel(c) |
| } |
| } |
| |
| // Unable to parse release channel. |
| return pmpb.ReleaseChannel_RELEASE_CHANNEL_UNSPECIFIED |
| } |
| |
| // parseOSInfoFromLSB returns the current OS version and release channel as |
| // provided by the lsb-release file. An error is returned only if the lsb file |
| // cannot be parsed. |
| func parseOSInfoFromLSB(apiHandler sysapi.APIHandler) (*pmpb.OSVersion, error) { |
| content, err := apiHandler.ReadFile(lsbFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Parse lsb-release file as key value pairs. |
| pairs, err := parseKeyValuePairs(bytes.NewReader(content)) |
| if err != nil { |
| return nil, fmt.Errorf("unable to parse '%s': %s", lsbFile, err) |
| } |
| |
| osVersion := new(pmpb.OSVersion) |
| |
| // Get release version string. |
| if version, ok := pairs[lsbChromeOSReleaseKey]; ok { |
| osVersion.VersionString = proto.String(version) |
| } |
| |
| // Get the release channel. |
| if channel, ok := pairs[lsbChromeOSReleaseChannelKey]; ok { |
| ch := convertOSReleaseChannel(channel) |
| osVersion.Channel = &ch |
| } |
| |
| // Get release milestone. |
| if milestone, ok := pairs[lsbChromeOSReleaseMilestoneKey]; ok { |
| if n, err := strconv.ParseUint(milestone, 10, 32); err == nil { |
| osVersion.Milestone = proto.Uint32(uint32(n)) |
| } |
| } |
| |
| return osVersion, nil |
| } |
| |
| // getStatusFromUpdateEngine returns the pmpb.InstanceStatus protobuf |
| // with the update status filled in. |
| func getStatusFromUpdateEngine(ueClient dbus.UpdateEngineClient) (*pmpb.InstanceStatus, error) { |
| response, err := ueClient.GetStatus() |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| status := new(pmpb.InstanceStatus) |
| |
| // Set update status. |
| status.UpdateStatus = response.UpdateStatus |
| if status.UpdateStatus == nil { |
| return nil, fmt.Errorf("failed to fetch update status: It is possible that update_engine has not fetched the status yet.") |
| } |
| |
| // Set new OS version. |
| status.NewOsVersion = new(pmpb.OSVersion) |
| status.NewOsVersion.VersionString = proto.String(*response.NewVersion) |
| |
| // Set last update check timestamp. |
| status.UpdateCheckTimestamp = proto.Int64(*response.LastCheckedTime) |
| |
| return status, nil |
| } |
| |
| // NewReporter creates a new Reporter. |
| func NewReporter(api sysapi.APIHandler, ueClient dbus.UpdateEngineClient, |
| policyEnforcer policyenforcer.PolicyEnforcer, instanceID uint64) Reporter { |
| return &reporterImpl{nil, api, ueClient, policyEnforcer, instanceID} |
| } |
| |
| // GetStatus() returns the current status of the instance as given by |
| // /etc/lsb-release file and update-engine. |
| // If an error occured, the error will be returned along with a status protobuf |
| // where the error field is set to the message describing the error. If the |
| // error field is set, all other fields other than the InstanceId are invalid. |
| // Note that this function must always return a status with InstanceId field |
| // populated regardless of whether an error occurred or not. |
| func (rep *reporterImpl) GetStatus() (*pmpb.InstanceStatus, error) { |
| status := new(pmpb.InstanceStatus) |
| |
| status.InstanceId = proto.Uint64(rep.instanceID) |
| |
| // Get OS version. |
| // Check if we already know the current OS version. |
| if rep.currentOSVersion == nil { |
| // Get OS version from lsb-release file. |
| osVersion, err := parseOSInfoFromLSB(rep.api) |
| if err != nil { |
| status.Error = proto.String(err.Error()) |
| return status, err |
| } |
| |
| rep.currentOSVersion = osVersion |
| } |
| |
| status.OsVersion = rep.currentOSVersion |
| |
| // Get update status from update-engine via dbus. |
| updateStatus, err := getStatusFromUpdateEngine(rep.ueClient) |
| if err != nil { |
| status.UpdateStatus = nil |
| status.Error = proto.String(err.Error()) |
| return status, err |
| } |
| |
| proto.Merge(status, updateStatus) |
| |
| // Get healthmonitor logging and monitoring status health monitor Client. |
| healthMonitorStatus, err := rep.policyEnforcer.GetHealthMonitorStatus() |
| status.HealthMonitorStatus = healthMonitorStatus |
| if err != nil { |
| status.Error = proto.String(err.Error()) |
| return status, err |
| } |
| |
| return status, nil |
| } |
| |
| // GetOSVersion parses and returns the OS Version from /etc/lsb-release file. |
| // It returns error if unable to parse /etc/lsb-release file. |
| func (rep *reporterImpl) GetOSVersion() (*pmpb.OSVersion, error) { |
| return parseOSInfoFromLSB(rep.api) |
| } |