Added get_status api in policy manager BUG=b/222726350 TEST=presubmit RELEASE_NOTE=Added get_status api in device_policy_manager Change-Id: I5b4b3f4e27adce4b663bbe0fa74b6a07351a1a01
diff --git a/cmd/getstatus/getstatus.go b/cmd/getstatus/getstatus.go new file mode 100644 index 0000000..b933f8e --- /dev/null +++ b/cmd/getstatus/getstatus.go
@@ -0,0 +1,38 @@ +// 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. + +package getstatus + +import ( + "fmt" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "policy-manager/pkg/imgstatus" +) + +const ( + lsbFile = "/etc/lsb-release" + instanceConfigFile = "/var/lib/devicesettings/instance_config" +) + +// HandleGetStatusCmd prints the status of the isntance to stdout. +func HandleGetStatusCmd() { + status, err := imgstatus.GetStatus(lsbFile, instanceConfigFile) + if err != nil { + glog.Exitf("Error fetching instance status: %v", err) + } + + fmt.Println(proto.MarshalTextString(status)) +}
diff --git a/main.go b/main.go index 094b819..3c11bb9 100644 --- a/main.go +++ b/main.go
@@ -39,6 +39,7 @@ "flag" "fmt" + "policy-manager/cmd/getstatus" "policy-manager/cmd/monitor" "policy-manager/cmd/showconfig" @@ -51,10 +52,14 @@ // Command line flags for show_config mode. var showConfigModeFlags *flag.FlagSet +// Command line flags for get_status mode. +var getStatusModeFlags *flag.FlagSet + const ( // Command names. monitorModeCommand = "monitor" showConfigCommand = "show_config" + getStatusCommand = "get_status" ) func init() { @@ -70,6 +75,12 @@ fmt.Println("Prints the current device policy for this instance to stdout.") } + // Flags for get_status. + getStatusModeFlags = flag.NewFlagSet(getStatusCommand, flag.ExitOnError) + getStatusModeFlags.Usage = func() { + fmt.Println("Prints the status of updates available to stdout.") + } + // Log to stderr only by default flag.Set("logtostderr", "true") } @@ -83,8 +94,8 @@ flag.PrintDefaults() fmt.Println("") - fmt.Printf("Supported commands:\n%s,%s\n", - monitorModeCommand, showConfigCommand) + fmt.Printf("Supported commands:\n%s,%s,%s\n", + monitorModeCommand, showConfigCommand, getStatusCommand) } // Parse the glog flags. @@ -105,6 +116,8 @@ monitor.HandleMonitorCmd() case showConfigCommand: showconfig.HandleShowConfigCmd() + case getStatusCommand: + getstatus.HandleGetStatusCmd() default: glog.Errorf("Unrecognized command: %s\n", command) flag.Usage()
diff --git a/pkg/imgstatus/imgstatus.go b/pkg/imgstatus/imgstatus.go new file mode 100644 index 0000000..01d5c7e --- /dev/null +++ b/pkg/imgstatus/imgstatus.go
@@ -0,0 +1,218 @@ +// 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 +}
diff --git a/pkg/imgstatus/imgstatus_test.go b/pkg/imgstatus/imgstatus_test.go new file mode 100644 index 0000000..fe9bd70 --- /dev/null +++ b/pkg/imgstatus/imgstatus_test.go
@@ -0,0 +1,239 @@ +// 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. + +// Unit tests for the imgstatus implementation. +package imgstatus + +import ( + "os" + "policy-manager/pkg/sysapi" + "policy-manager/protos" + "reflect" + "strings" + "testing" + + "github.com/golang/protobuf/proto" +) + +const ( + // Test LSB file. + testLSB = `CHROMEOS_AUSERVER=https://tools.google.com/service/update2 + CHROMEOS_BOARD_APPID={76E245CF-C0D0-444D-BA50-36739C18EB00} + CHROMEOS_DEVSERVER= + CHROMEOS_RELEASE_APPID={90F229CE-83E2-4FAF-8479-E368A34938B1} + CHROMEOS_RELEASE_BOARD=lakitu-signed-prempkeys + CHROMEOS_RELEASE_BRANCH_NUMBER=0 + CHROMEOS_RELEASE_BUILD_NUMBER=16511 + CHROMEOS_RELEASE_BUILD_TYPE=Official Build + CHROMEOS_RELEASE_CHROME_MILESTONE=89 + CHROMEOS_RELEASE_DESCRIPTION=16511.0.0 (Official Build) dev-channel lakitu + CHROMEOS_RELEASE_NAME=Chrome OS + CHROMEOS_RELEASE_PATCH_NUMBER=0 + CHROMEOS_RELEASE_TRACK=dev-channel + CHROMEOS_RELEASE_VERSION=16511.0.0 + GOOGLE_RELEASE=16511.0.0 + HWID_OVERRIDE=LAKITU` + + testDevBuildLSB = ` + CHROMEOS_RELEASE_BOARD=lakitu + CHROMEOS_DEVSERVER=http://foo.google.com:8080 + GOOGLE_RELEASE=7520.3.2015_10_12_1443 + CHROMEOS_RELEASE_BUILD_NUMBER=7520 + CHROMEOS_RELEASE_BRANCH_NUMBER=3 + CHROMEOS_RELEASE_CHROME_MILESTONE=89 + CHROMEOS_RELEASE_PATCH_NUMBER=2015_10_12_1443 + CHROMEOS_RELEASE_TRACK=developer-build + CHROMEOS_RELEASE_DESCRIPTION=7520.3.2015_10_12_1443 (Developer Build) + CHROMEOS_RELEASE_NAME=Chromium OS + CHROMEOS_RELEASE_BUILD_TYPE=Developer Build + CHROMEOS_RELEASE_VERSION=7520.3.2015_10_12_1443 + CHROMEOS_AUSERVER=http://foo.google.com:8080/update` + + // Dummy instance ID. + testInstanceID = uint64(12031993) + tmpLsbFile = "/tmp/lsb-release" + tmpLsbFilePerm = os.FileMode(0644) +) + +// TestParseKeyValuePairs tests that we can correctly parse /etc/lsb-release +// into a map. +func TestParseKeyValuePairs(t *testing.T) { + tests := []struct { + name string + want map[string]string + input string + }{ + { + "ParseLSBFile", + map[string]string{ + "CHROMEOS_AUSERVER": "https://tools.google.com/service/update2", + "CHROMEOS_BOARD_APPID": "{76E245CF-C0D0-444D-BA50-36739C18EB00}", + "CHROMEOS_DEVSERVER": "", + "CHROMEOS_RELEASE_APPID": "{90F229CE-83E2-4FAF-8479-E368A34938B1}", + "CHROMEOS_RELEASE_BOARD": "lakitu-signed-prempkeys", + "CHROMEOS_RELEASE_BRANCH_NUMBER": "0", + "CHROMEOS_RELEASE_BUILD_NUMBER": "16511", + "CHROMEOS_RELEASE_BUILD_TYPE": "Official Build", + "CHROMEOS_RELEASE_CHROME_MILESTONE": "89", + "CHROMEOS_RELEASE_DESCRIPTION": "16511.0.0 (Official Build) dev-channel lakitu", + "CHROMEOS_RELEASE_NAME": "Chrome OS", + "CHROMEOS_RELEASE_PATCH_NUMBER": "0", + "CHROMEOS_RELEASE_TRACK": "dev-channel", + "CHROMEOS_RELEASE_VERSION": "16511.0.0", + "GOOGLE_RELEASE": "16511.0.0", + "HWID_OVERRIDE": "LAKITU", + }, + testLSB, + }, + { + "PoorlyFormattedPairs", + map[string]string{ + "apple_A": "a", + "BAnaNA_b_": "b", + "cantaloupe": "", + }, + ` + apple_A= a + BAnaNA_b_ =b + cantaloupe= + `, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mockReader := strings.NewReader(test.input) + pairs, err := parseKeyValuePairs(mockReader) + if err != nil { + t.Errorf("got unexpected error %v", err) + } else if !reflect.DeepEqual(pairs, test.want) { + t.Errorf("got %v, want %v", pairs, test.want) + } + }) + } +} + +// TestConvertOSReleaseChannel tests that we can correctly convert the string +// representation of release channel into the protobuf definition. +func TestConvertOSReleaseChannel(t *testing.T) { + tests := []struct { + name string + want protos.ReleaseChannel + input string + }{ + { + "DevChannelTest", + protos.ReleaseChannel_DEV, + "dev-channel", + }, + { + "BetaChannelTest", + protos.ReleaseChannel_BETA, + "beta-channel", + }, + { + "StableChannelTest", + protos.ReleaseChannel_STABLE, + "stable-channel", + }, + { + "DeveloperBuildTest", + protos.ReleaseChannel_RELEASE_CHANNEL_UNSPECIFIED, + "developer-build", + }, + { + "BadChannelTest", + protos.ReleaseChannel_RELEASE_CHANNEL_UNSPECIFIED, + "abc-chan", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := convertOSReleaseChannel(test.input) + if !reflect.DeepEqual(test.want, result) { + t.Errorf("got %s, want %s", + result.String(), + test.want.String()) + } + }) + } +} + +// TestParseOSInfoFromLSB tests that we can correctly get the OS version and +// release channel from a lsb-release file. +func TestParseOSInfoFromLSB(t *testing.T) { + devChannel := protos.ReleaseChannel_DEV + unknownChannel := protos.ReleaseChannel_RELEASE_CHANNEL_UNSPECIFIED + + tests := []struct { + name string + want *protos.OSVersion + lsbContent string + expectErr bool + }{ + { + "GoodLSBFile", + &protos.OSVersion{ + VersionString: proto.String("16511.0.0"), + Milestone: proto.Uint32(uint32(89)), + Channel: &devChannel, + }, + testLSB, + false, + }, + { + "OnlyReleaseVersion", + &protos.OSVersion{ + VersionString: proto.String("16511.0.0"), + }, + "CHROMEOS_RELEASE_VERSION=16511.0.0", + false, + }, + { + "BadLSBFile", + &protos.OSVersion{}, + "", + false, + }, + { + "DevBuildLSBFile", + &protos.OSVersion{ + VersionString: proto.String("7520.3.2015_10_12_1443"), + Milestone: proto.Uint32(uint32(89)), + Channel: &unknownChannel, + }, + testDevBuildLSB, + false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sysapi.AtomicWriteFile(tmpLsbFile, []byte(test.lsbContent), tmpLsbFilePerm) + defer os.RemoveAll(tmpLsbFile) + + result, err := parseOSInfoFromLSB(tmpLsbFile) + + if err == nil && test.expectErr { + t.Errorf("test passed, want error") + } else if err == nil && !proto.Equal(test.want, result) { + t.Errorf("got %s, want %s", + result.String(), + test.want.String()) + } else if err != nil && !test.expectErr { + t.Errorf("got unexpected error %v", err) + } + }) + } +}
diff --git a/pkg/policyenforcer/policy_enforcer.go b/pkg/policyenforcer/policy_enforcer.go index 51c03e1..4cfbc15 100644 --- a/pkg/policyenforcer/policy_enforcer.go +++ b/pkg/policyenforcer/policy_enforcer.go
@@ -115,11 +115,11 @@ } // GetServiceStatus checks whether the services are running and returns their -// status in a InstanceStatus proto. Failure to check one service will result +// status in a ServiceStatus proto. Failure to check one service will result // in a missing field in the returned proto, and will not affect checking the // other service's status. -func (client *PolicyEnforcer) GetServiceStatus(serviceMonitor map[string]string) (*protos.InstanceStatus, error) { - instanceStatus := new(protos.InstanceStatus) +func (client *PolicyEnforcer) GetServiceStatus(serviceMonitor map[string]string) (*protos.ServiceStatus, error) { + ServiceStatus := new(protos.ServiceStatus) statusErr := false isRunning, err := client.systemdClient.IsUnitActiveRunning(serviceMonitor["metricsService"]) @@ -127,7 +127,7 @@ glog.Error(err) statusErr = true } else { - instanceStatus.Metrics = proto.Bool(isRunning) + ServiceStatus.Metrics = proto.Bool(isRunning) } isRunning, err = client.systemdClient.IsUnitActiveRunning(serviceMonitor["updateService"]) @@ -137,9 +137,9 @@ } else { if isRunning { - instanceStatus.UpdateEngine = proto.String("") + ServiceStatus.UpdateEngine = proto.String("") } else { - instanceStatus.UpdateEngine = proto.String(updateDisabledStrategy) + ServiceStatus.UpdateEngine = proto.String(updateDisabledStrategy) } } @@ -148,7 +148,7 @@ glog.Error(err) statusErr = true } else { - instanceStatus.Logging = proto.Bool(isRunning) + ServiceStatus.Logging = proto.Bool(isRunning) } isRunning, err = client.systemdClient.IsUnitActiveRunning(serviceMonitor["monitoringService"]) @@ -157,11 +157,11 @@ statusErr = true } else { - instanceStatus.Monitoring = proto.Bool(isRunning) + ServiceStatus.Monitoring = proto.Bool(isRunning) } if statusErr { - return instanceStatus, errors.New("unable to get service status") + return ServiceStatus, errors.New("unable to get service status") } - return instanceStatus, nil + return ServiceStatus, nil }
diff --git a/pkg/policyenforcer/policy_enforcer_test.go b/pkg/policyenforcer/policy_enforcer_test.go index 15ca5f0..b5da283 100644 --- a/pkg/policyenforcer/policy_enforcer_test.go +++ b/pkg/policyenforcer/policy_enforcer_test.go
@@ -266,7 +266,7 @@ checkLoggingErr error isMonitoring bool checkMonitoringErr error - expectedStatus *protos.InstanceStatus + expectedStatus *protos.ServiceStatus expectErr bool }{ { @@ -279,7 +279,7 @@ checkLoggingErr: nil, isMonitoring: false, checkMonitoringErr: nil, - expectedStatus: &protos.InstanceStatus{ + expectedStatus: &protos.ServiceStatus{ UpdateEngine: proto.String("update_disabled"), Metrics: proto.Bool(false), Logging: proto.Bool(false), @@ -297,7 +297,7 @@ checkLoggingErr: nil, isMonitoring: true, checkMonitoringErr: nil, - expectedStatus: &protos.InstanceStatus{ + expectedStatus: &protos.ServiceStatus{ UpdateEngine: proto.String(""), Metrics: proto.Bool(true), Logging: proto.Bool(true), @@ -315,7 +315,7 @@ checkLoggingErr: nil, isMonitoring: false, checkMonitoringErr: nil, - expectedStatus: &protos.InstanceStatus{ + expectedStatus: &protos.ServiceStatus{ UpdateEngine: proto.String(""), Logging: proto.Bool(true), Monitoring: proto.Bool(false), @@ -332,7 +332,7 @@ checkLoggingErr: nil, isMonitoring: false, checkMonitoringErr: nil, - expectedStatus: &protos.InstanceStatus{ + expectedStatus: &protos.ServiceStatus{ Metrics: proto.Bool(true), Logging: proto.Bool(true), Monitoring: proto.Bool(false), @@ -349,7 +349,7 @@ checkLoggingErr: errors.New("error"), isMonitoring: true, checkMonitoringErr: nil, - expectedStatus: &protos.InstanceStatus{ + expectedStatus: &protos.ServiceStatus{ UpdateEngine: proto.String(""), Metrics: proto.Bool(true), Monitoring: proto.Bool(true), @@ -366,7 +366,7 @@ checkLoggingErr: nil, isMonitoring: false, checkMonitoringErr: errors.New("error"), - expectedStatus: &protos.InstanceStatus{ + expectedStatus: &protos.ServiceStatus{ UpdateEngine: proto.String(""), Metrics: proto.Bool(true), Logging: proto.Bool(true),
diff --git a/pkg/updateengine/testdata/update_engine.sh b/pkg/updateengine/testdata/update_engine.sh new file mode 100755 index 0000000..3363bc0 --- /dev/null +++ b/pkg/updateengine/testdata/update_engine.sh
@@ -0,0 +1,30 @@ +#!/bin/bash + +# 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. + +# This is a fake implementation of update-engine service + +main() { + # perform the following action when the command is + # update-engine --status + if [[ $1 == "--status" ]]; then + echo "CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT\ + \nLAST_CHECKED_TIME=1646430409\nNEW_VERSION=0.0.0.0" + else + exit 1 + fi +} + +main $@
diff --git a/pkg/updateengine/update_engine_client.go b/pkg/updateengine/update_engine_client.go new file mode 100644 index 0000000..9855d7b --- /dev/null +++ b/pkg/updateengine/update_engine_client.go
@@ -0,0 +1,41 @@ +// 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. + +package updateengine + +import ( + "bytes" + "fmt" + "github.com/golang/glog" + "os/exec" +) + +// UpdateEngineStatus is used to get the status of update engine if auto updates +// are enabled in the instance. +func UpdateEngineStatus(updateEngineCmd string) ([]byte, error) { + cmd := exec.Command(updateEngineCmd, "--status") + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("unable to get update-engine status: %s", err) + } + + if stderr.String() != "" { + glog.Errorf("error from update_engine_client: %v", stderr.String()) + } + + return stdout.Bytes(), nil +}
diff --git a/pkg/updateengine/update_engine_client_test.go b/pkg/updateengine/update_engine_client_test.go new file mode 100644 index 0000000..00fc1e4 --- /dev/null +++ b/pkg/updateengine/update_engine_client_test.go
@@ -0,0 +1,34 @@ +// 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. + +// Unit tests for the TestUpdateEngineStatus implementation. +package updateengine + +import ( + "reflect" + "testing" +) + +const tmpUpdateEngineCmd = "testdata/update_engine.sh" + +func TestUpdateEngineStatus(t *testing.T) { + want := []byte("CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT\nLAST_CHECKED_TIME=1646430409\nNEW_VERSION=0.0.0.0") + got, err := UpdateEngineStatus(tmpUpdateEngineCmd) + if err != nil { + t.Errorf("got unexpected error: %v", err) + } + if reflect.DeepEqual(got, want) { + t.Errorf("got: %s, want: %s", got, want) + } +}
diff --git a/protos/instance_status.proto b/protos/instance_status.proto index 72ca8be..f268b2c 100644 --- a/protos/instance_status.proto +++ b/protos/instance_status.proto
@@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// 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. @@ -16,7 +16,40 @@ syntax = "proto2"; package protos; -// option go_package = "policy_manager/protos"; + +// ReleaseChannel specifies the Chrome OS release channel that the image is on. +enum ReleaseChannel { + // Unknown release channel. + RELEASE_CHANNEL_UNSPECIFIED = 0; + + // Stable channel. + STABLE = 1; + + // Beta channel. + BETA = 2; + + // Dev channel. + DEV = 3; + + reserved 4; +} + +// OSVersion contains all the fields identifying a particular version of the +// Chrome OS. +message OSVersion { + // Release version string. For example: "16511.0.0" + // Chrome OS uses the following version format: + // <TIP_BUILD>.<BRANCH_BUILD>.<BRANCH_BRANCH_BUILD> + optional string version_string = 1; + + // Milestone number for Chrome. For example: 89 + optional uint32 milestone = 2; + + // Release channel of the OS. + optional ReleaseChannel channel = 3; + + // Next number: 4 +} // InstanceStatus contains all the fields related to the current status of the // instance. @@ -24,26 +57,25 @@ // GCE instance ID. optional uint64 instance_id = 1; - // update_engine is set to true if the update engine systemd - // service is active. - optional string update_engine = 2; + // Current version of the image. + optional OSVersion os_version = 2; - // metrics is set to true if the crash reporter systemd - // service is active. - optional bool metrics = 3; + // The current update status as reported by the update_engine. + optional string update_status = 3; - // logging is set to true if the logging systemd service - // is active and running. - optional bool logging = 4; + // New version delivered by the update. + optional OSVersion new_os_version = 4; - // monitoring is set to true if the monitoring systemd service - // is active and running. - optional bool monitoring = 5; + // Unix timestamp of last reboot. + optional int64 reboot_timestamp = 5; + + // Unix timestamp fo last update check. + optional int64 update_check_timestamp = 6; // If an error occurred while collecting the instance status, this field will // be set with the error message and all other fields except the instance_id // are undefined. - optional string error = 6; + optional string error = 7; - // Next number: 7 + // Next number: 8 }
diff --git a/protos/service_status.proto b/protos/service_status.proto new file mode 100644 index 0000000..8147ddd --- /dev/null +++ b/protos/service_status.proto
@@ -0,0 +1,49 @@ +// 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. + +// Protobuf definitions for reporting instance status. +syntax = "proto2"; + +package protos; +// option go_package = "policy_manager/protos"; + +// ServiceStatus contains all the fields related to the current status of the +// instance. +message ServiceStatus { + // GCE instance ID. + optional uint64 instance_id = 1; + + // update_engine is set to true if the update engine systemd + // service is active. + optional string update_engine = 2; + + // metrics is set to true if the crash reporter systemd + // service is active. + optional bool metrics = 3; + + // logging is set to true if the logging systemd service + // is active and running. + optional bool logging = 4; + + // monitoring is set to true if the monitoring systemd service + // is active and running. + optional bool monitoring = 5; + + // If an error occurred while collecting the instance status, this field will + // be set with the error message and all other fields except the instance_id + // are undefined. + optional string error = 6; + + // Next number: 7 +}