blob: 01d5c7e4a8f7a79df7f1a5ccf1a664221efcb297 [file] [log] [blame]
// Copyright 2022 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 imgstatus which returns the update status of the instance
package imgstatus
import (
"bytes"
"fmt"
"github.com/golang/protobuf/proto"
"io"
"io/ioutil"
"policy-manager/pkg/configfetcher"
"policy-manager/pkg/devicepolicy"
"policy-manager/pkg/updateengine"
"policy-manager/protos"
"regexp"
"strconv"
"strings"
)
// Constants related to lsb-release file.
const (
lsbChromeOSReleaseKey = "CHROMEOS_RELEASE_VERSION"
lsbChromeOSReleaseChannelKey = "CHROMEOS_RELEASE_TRACK"
lsbChromeOSReleaseMilestoneKey = "CHROMEOS_RELEASE_CHROME_MILESTONE"
updateEngineCmd = "update_engine_client"
)
// 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) protos.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 := protos.ReleaseChannel_value[ch]; ok {
return protos.ReleaseChannel(c)
}
}
// Unable to parse release channel.
return protos.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(lsbFile string) (*protos.OSVersion, error) {
content, err := ioutil.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(protos.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 protos.InstanceStatus protobuf
// with the update status filled in.
func getStatusFromUpdateEngine(instanceConfigFile string, updateEngineCmd string) (*protos.InstanceStatus, error) {
status := new(protos.InstanceStatus)
// Check if update engine is disabled.
config, err := devicepolicy.GetInstanceConfig(instanceConfigFile)
if err != nil {
return nil, fmt.Errorf("error fetching instance config: %v", err)
}
// Do not check update engine status if it is disabled.
if *config.UpdateStrategy == "update_disabled" {
status.UpdateStatus = proto.String("UPDATE_ENGINE_DISABLED")
return status, nil
}
// Get response from update engine.
response, err := updateengine.UpdateEngineStatus(updateEngineCmd)
if err != nil {
return nil, err
}
pairs, err := parseKeyValuePairs(bytes.NewReader(response))
if err != nil {
return nil, fmt.Errorf("unable to parse: %s", err)
}
// Set update status.
if pairs["CURRENT_OP"] == "" {
return nil, fmt.Errorf("failed to fetch update status. It is possible that update_engine has not fetched the status yet.")
}
status.UpdateStatus = proto.String(pairs["CURRENT_OP"])
// Set new OS version.
status.NewOsVersion = new(protos.OSVersion)
status.NewOsVersion.VersionString = proto.String(pairs["NEW_VERSION"])
// Set last update check timestamp.
lastTime, err := strconv.ParseInt(pairs["LAST_CHECKED_TIME"], 10, 64)
if err != nil {
return nil, fmt.Errorf("unable to parse LAST_CHECKED_TIME: %s", err)
}
status.UpdateCheckTimestamp = proto.Int64(lastTime)
return status, nil
}
// 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 GetStatus(lsbFile string, instanceConfigFile string) (*protos.InstanceStatus, error) {
status := new(protos.InstanceStatus)
var instanceID uint64
id, err := configfetcher.GetInstanceID()
if err == nil {
instanceID = id
}
status.InstanceId = proto.Uint64(instanceID)
// Get OS version from lsb-release file.
osVersion, err := parseOSInfoFromLSB(lsbFile)
if err != nil {
status.Error = proto.String(err.Error())
return status, err
}
status.OsVersion = osVersion
// Get update status from update-engine via update_engine_client.
updateStatus, err := getStatusFromUpdateEngine(instanceConfigFile, updateEngineCmd)
if err != nil {
status.UpdateStatus = nil
status.Error = proto.String(err.Error())
return status, err
}
proto.Merge(status, updateStatus)
return status, nil
}