-
Notifications
You must be signed in to change notification settings - Fork 0
/
update_engine.go
269 lines (222 loc) · 6.37 KB
/
update_engine.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/godbus/dbus/v5"
)
const (
// Implement the same DBus interface as update_engine
// - https://github.com/kinvolk/update_engine/blob/v0.4.10/src/update_engine/dbus_constants.h
dbusName = "com.coreos.update1"
dbusPath = "/com/coreos/update1"
dbusInterface = "com.coreos.update1.Manager"
// These interval values were taken from the original update_engine:
// - https://github.com/kinvolk/update_engine/blob/v0.4.10/src/update_engine/update_check_scheduler.cc#L14-L20
intervalInitial = 7 * time.Minute
intervalPeriodic = 45 * time.Minute
intervalFuzz = 10 * time.Minute
// This file should always exist on Flatcar
osReleasePath = "/etc/os-release"
)
var (
// Flag file location for kured:
// https://github.com/flatcar-linux/update_engine/commit/93f6cdddd46e9fba6c336c1db3baa6c89d85979b
// https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s13.html
kuredReleasePath = "/run/reboot-required"
)
// dbusConn is an interface for all the methods of *dbus.Conn that the
// updateEngine uses. This allows the connection to dbus to be mocked out in
// tests
type dbusConn interface {
Emit(path dbus.ObjectPath, name string, values ...interface{}) error
}
// newVersionFunc returns the latest version from the given URL. Can be mocked
// out in tests.
type newVersionFunc func(versionURL string) (string, error)
type updateEngine struct {
conn dbusConn
newVersion newVersionFunc
osVersion string
random *rand.Rand
status *status
updateCh chan bool
versionURL *url.URL
}
func newUpdateEngine(versionURL string) (*updateEngine, error) {
ov, err := osVersion()
if err != nil {
return nil, err
}
vu, err := url.Parse(versionURL)
if err != nil {
return nil, err
}
conn, err := dbus.SystemBus()
if err != nil {
return nil, err
}
reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue)
if err != nil {
return nil, err
}
if reply != dbus.RequestNameReplyPrimaryOwner {
return nil, fmt.Errorf("Name is already taken: %s", dbusName)
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
ch := make(chan bool)
ue := &updateEngine{
conn: conn,
newVersion: newVersion,
osVersion: ov,
random: r,
status: newStatus(),
updateCh: ch,
versionURL: vu,
}
conn.Export(ue, dbusPath, dbusInterface)
return ue, nil
}
// GetStatus implements com.coreos.update1.Manager.GetStatus
func (ue *updateEngine) GetStatus() (int64, float64, string, string, int64, *dbus.Error) {
return ue.status.lastCheckedTime, ue.status.progress, ue.status.currentOperation, ue.status.newVersion, ue.status.newSize, nil
}
// ResetStatus implements com.coreos.update1.Manager.ResetStatus
func (ue *updateEngine) ResetStatus() *dbus.Error {
ue.status = newStatus()
log.Printf("The update status was reset")
return nil
}
// AttemptUpdate implements com.coreos.update1.Manager.AttemptUpdate
func (ue *updateEngine) AttemptUpdate() *dbus.Error {
log.Printf("Update check requested")
ue.updateCh <- true
return nil
}
func (ue *updateEngine) checkForUpdate() error {
vu := ue.versionURL.String()
log.Printf("Checking for new version at %s", vu)
nv, err := ue.newVersion(vu)
if err != nil {
return err
}
ue.status.lastCheckedTime = time.Now().Unix()
// If the latest version differs from the current OS version
// then update the status and emit the new status to dbus
if nv != ue.osVersion && nv != ue.status.newVersion {
ue.status.newVersion = nv
ue.status.currentOperation = updateStatusUpdatedNeedReboot
if err := ue.conn.Emit(
dbusPath,
dbusInterface+".StatusUpdate",
ue.status.lastCheckedTime,
ue.status.progress,
ue.status.currentOperation,
ue.status.newVersion,
ue.status.newSize,
); err != nil {
return err
}
if err := touchFile(kuredReleasePath); err != nil {
return err
}
log.Printf("Updated status: %s\n", ue.status)
return nil
}
log.Printf("Didn't find a new version")
return nil
}
func (ue *updateEngine) run() {
// Wait for a short time before performing the first status check
id := fuzzDuration(ue.random, intervalInitial, intervalFuzz)
ticker := time.NewTicker(id)
defer ticker.Stop()
log.Printf("Waiting %s before the initial update check", id)
update := func() {
if err := ue.checkForUpdate(); err != nil {
log.Printf("Error checking for update: %s", err)
}
// Wait for a longer period between updates
d := fuzzDuration(ue.random, intervalPeriodic, intervalFuzz)
log.Printf("Waiting %s before next update check", d)
ticker.Reset(d)
}
for {
select {
case <-ue.updateCh:
update()
case <-ticker.C:
update()
}
}
}
func newVersion(versionURL string) (string, error) {
resp, err := http.Get(versionURL)
if err != nil {
return "", err
}
defer func() {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return getValue("FLATCAR_VERSION", string(b))
}
func osVersion() (string, error) {
b, err := ioutil.ReadFile(osReleasePath)
if err != nil {
return "", fmt.Errorf("reading file %q: %w", osReleasePath, err)
}
return getValue("VERSION", string(b))
}
func getValue(match, body string) (string, error) {
sc := bufio.NewScanner(strings.NewReader(body))
for sc.Scan() {
spl := strings.SplitN(sc.Text(), "=", 2)
// Just skip empty lines or lines without a value.
if len(spl) == 1 {
continue
}
if spl[0] == match {
return spl[1], nil
}
}
return "", fmt.Errorf("couldn't get value for %s", match)
}
func touchFile(fileName string) error {
_, err := os.Stat(fileName)
if os.IsNotExist(err) {
file, err := os.Create(fileName)
defer file.Close()
if err != nil {
return err
}
} else {
currentTime := time.Now().Local()
err = os.Chtimes(fileName, currentTime, currentTime)
if err != nil {
return err
}
}
return nil
}
// fuzzDuration adds a random jitter to a given duration. It's adapted from the
// equivalent method in the original update_engine:
// - https://github.com/kinvolk/update_engine/blob/v0.4.10/src/update_engine/utils.cc#L510-L515
func fuzzDuration(r *rand.Rand, value time.Duration, fuzz time.Duration) time.Duration {
min := int64(value.Nanoseconds() - (fuzz.Nanoseconds() / 2))
max := int64(value.Nanoseconds() + (fuzz.Nanoseconds() / 2))
d := r.Int63n(max-min+1) + min
return time.Duration(d)
}