github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/logtransfer_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "io" 8 "net/http" 9 "time" 10 11 "github.com/gorilla/websocket" 12 "github.com/juju/errors" 13 jujuhttp "github.com/juju/http/v2" 14 "github.com/juju/loggo" 15 "github.com/juju/mgo/v3/bson" 16 "github.com/juju/names/v5" 17 jc "github.com/juju/testing/checkers" 18 "github.com/juju/utils/v3" 19 gc "gopkg.in/check.v1" 20 21 "github.com/juju/juju/apiserver/websocket/websockettest" 22 "github.com/juju/juju/core/permission" 23 "github.com/juju/juju/rpc/params" 24 "github.com/juju/juju/state" 25 coretesting "github.com/juju/juju/testing" 26 "github.com/juju/juju/testing/factory" 27 "github.com/juju/juju/version" 28 ) 29 30 type logtransferSuite struct { 31 apiserverBaseSuite 32 userTag names.UserTag 33 password string 34 machineTag names.MachineTag 35 machinePassword string 36 logs loggo.TestWriter 37 url string 38 } 39 40 var _ = gc.Suite(&logtransferSuite{}) 41 42 func (s *logtransferSuite) SetUpTest(c *gc.C) { 43 s.apiserverBaseSuite.SetUpTest(c) 44 s.password = "jabberwocky" 45 u := s.Factory.MakeUser(c, &factory.UserParams{Password: s.password}) 46 s.userTag = u.Tag().(names.UserTag) 47 m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 48 Nonce: "nonce", 49 }) 50 s.machineTag = m.Tag().(names.MachineTag) 51 s.machinePassword = password 52 s.setUserAccess(c, permission.SuperuserAccess) 53 54 url := s.URL("/migrate/logtransfer", nil) 55 url.Scheme = "wss" 56 s.url = url.String() 57 58 s.logs.Clear() 59 writer := loggo.NewMinimumLevelWriter(&s.logs, loggo.INFO) 60 c.Assert(loggo.RegisterWriter("logsink-tests", writer), jc.ErrorIsNil) 61 } 62 63 func (s *logtransferSuite) makeAuthHeader() http.Header { 64 header := jujuhttp.BasicAuthHeader(s.userTag.String(), s.password) 65 header.Add(params.MigrationModelHTTPHeader, s.State.ModelUUID()) 66 header.Add(params.JujuClientVersion, version.Current.String()) 67 return header 68 } 69 70 func (s *logtransferSuite) dialWebsocket(c *gc.C) *websocket.Conn { 71 return s.dialWebsocketInternal(c, s.makeAuthHeader()) 72 } 73 74 func (s *logtransferSuite) dialWebsocketInternal(c *gc.C, header http.Header) *websocket.Conn { 75 conn, _, err := dialWebsocketFromURL(c, s.url, header) 76 c.Assert(err, jc.ErrorIsNil) 77 s.AddCleanup(func(_ *gc.C) { conn.Close() }) 78 return conn 79 } 80 81 func (s *logtransferSuite) checkAuthFails(c *gc.C, header http.Header, code int, message string) { 82 _, resp, err := dialWebsocketFromURL(c, s.url, header) 83 c.Assert(err, gc.Equals, websocket.ErrBadHandshake) 84 defer resp.Body.Close() 85 86 c.Assert(resp.StatusCode, gc.Equals, code) 87 body, err := io.ReadAll(resp.Body) 88 c.Assert(err, jc.ErrorIsNil) 89 c.Assert(string(body), gc.Matches, message+"\n") 90 } 91 92 func (s *logtransferSuite) TestRejectsMissingModelHeader(c *gc.C) { 93 header := jujuhttp.BasicAuthHeader(s.userTag.String(), s.password) 94 ws := s.dialWebsocketInternal(c, header) 95 websockettest.AssertJSONError(c, ws, `initialising migration logsink session: unknown model: ""`) 96 websockettest.AssertWebsocketClosed(c, ws) 97 } 98 99 func (s *logtransferSuite) TestRejectsBadMigratingModelUUID(c *gc.C) { 100 header := jujuhttp.BasicAuthHeader(s.userTag.String(), s.password) 101 header.Add(params.MigrationModelHTTPHeader, "does-not-exist") 102 ws := s.dialWebsocketInternal(c, header) 103 websockettest.AssertJSONError(c, ws, `initialising migration logsink session: unknown model: "does-not-exist"`) 104 websockettest.AssertWebsocketClosed(c, ws) 105 } 106 107 func (s *logtransferSuite) TestRejectsInvalidVersion(c *gc.C) { 108 url := s.URL("/migrate/logtransfer", nil) 109 url.Scheme = "wss" 110 hdr := s.makeAuthHeader() 111 hdr.Set("X-Juju-ClientVersion", "blah") 112 conn, _, err := dialWebsocketFromURL(c, url.String(), hdr) 113 c.Assert(err, jc.ErrorIsNil) 114 defer conn.Close() 115 websockettest.AssertJSONError(c, conn, `^initialising migration logsink session: invalid X-Juju-ClientVersion "blah".*`) 116 websockettest.AssertWebsocketClosed(c, conn) 117 } 118 119 func (s *logtransferSuite) TestRejectsMachineLogins(c *gc.C) { 120 header := jujuhttp.BasicAuthHeader(s.machineTag.String(), s.machinePassword) 121 header.Add(params.MachineNonceHeader, "nonce") 122 s.checkAuthFails(c, header, http.StatusForbidden, "authorization failed: machine 0 is not a user") 123 } 124 125 func (s *logtransferSuite) TestRejectsBadPasword(c *gc.C) { 126 header := jujuhttp.BasicAuthHeader(s.userTag.String(), "wrong") 127 header.Add(params.MigrationModelHTTPHeader, s.State.ModelUUID()) 128 s.checkAuthFails(c, header, http.StatusUnauthorized, "authentication failed: invalid entity name or password") 129 } 130 131 func (s *logtransferSuite) TestRequiresSuperUser(c *gc.C) { 132 s.setUserAccess(c, permission.LoginAccess) 133 s.checkAuthFails(c, s.makeAuthHeader(), http.StatusForbidden, "authorization failed: user .* is not a controller admin") 134 } 135 136 func (s *logtransferSuite) TestRequiresMigrationModeNone(c *gc.C) { 137 s.setMigrationMode(c, state.MigrationModeImporting) 138 ws := s.dialWebsocket(c) 139 websockettest.AssertJSONError(c, ws, `initialising migration logsink session: model migration mode is "importing" instead of ""`) 140 websockettest.AssertWebsocketClosed(c, ws) 141 } 142 143 func (s *logtransferSuite) TestLogging(c *gc.C) { 144 conn := s.dialWebsocket(c) 145 146 // Read back the nil error, indicating that all is well. 147 websockettest.AssertJSONInitialErrorNil(c, conn) 148 149 t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC) 150 err := conn.WriteJSON(¶ms.LogRecord{ 151 Entity: "machine-23", 152 Time: t0, 153 Module: "some.where", 154 Location: "foo.go:42", 155 Level: loggo.INFO.String(), 156 Message: "all is well", 157 }) 158 c.Assert(err, jc.ErrorIsNil) 159 160 t1 := time.Date(2015, time.June, 1, 23, 2, 2, 0, time.UTC) 161 err = conn.WriteJSON(¶ms.LogRecord{ 162 Entity: "machine-101", 163 Time: t1, 164 Module: "else.where", 165 Location: "bar.go:99", 166 Level: loggo.ERROR.String(), 167 Message: "oh noes", 168 }) 169 c.Assert(err, jc.ErrorIsNil) 170 171 // Wait for the log documents to be written to the DB. 172 logsColl := s.State.MongoSession().DB("logs").C("logs." + s.State.ModelUUID()) 173 var docs []bson.M 174 for a := coretesting.LongAttempt.Start(); a.Next(); { 175 err := logsColl.Find(nil).Sort("t").All(&docs) 176 c.Assert(err, jc.ErrorIsNil) 177 if len(docs) == 2 { 178 break 179 } 180 if len(docs) >= 2 { 181 c.Fatalf("saw more log documents than expected") 182 } 183 if !a.HasNext() { 184 c.Fatalf("timed out waiting for log writes") 185 } 186 } 187 188 // Check the recorded logs are correct. 189 c.Assert(docs[0]["t"], gc.Equals, t0.UnixNano()) 190 c.Assert(docs[0]["n"], gc.Equals, "machine-23") 191 c.Assert(docs[0]["m"], gc.Equals, "some.where") 192 c.Assert(docs[0]["l"], gc.Equals, "foo.go:42") 193 c.Assert(docs[0]["v"], gc.Equals, int(loggo.INFO)) 194 c.Assert(docs[0]["x"], gc.Equals, "all is well") 195 196 c.Assert(docs[1]["t"], gc.Equals, t1.UnixNano()) 197 c.Assert(docs[1]["n"], gc.Equals, "machine-101") 198 c.Assert(docs[1]["m"], gc.Equals, "else.where") 199 c.Assert(docs[1]["l"], gc.Equals, "bar.go:99") 200 c.Assert(docs[1]["v"], gc.Equals, int(loggo.ERROR)) 201 c.Assert(docs[1]["x"], gc.Equals, "oh noes") 202 203 // Close connection. 204 err = conn.Close() 205 c.Assert(err, jc.ErrorIsNil) 206 207 // Ensure that no error is logged when the connection is closed 208 // normally. 209 shortAttempt := &utils.AttemptStrategy{ 210 Total: coretesting.ShortWait, 211 Delay: 2 * time.Millisecond, 212 } 213 for a := shortAttempt.Start(); a.Next(); { 214 for _, log := range s.logs.Log() { 215 c.Assert(log.Level, jc.LessThan, loggo.ERROR, gc.Commentf("log: %#v", log)) 216 } 217 } 218 } 219 220 func (s *logtransferSuite) TestTracksLastSentLogTime(c *gc.C) { 221 conn := s.dialWebsocket(c) 222 223 // Read back the nil error, indicating that all is well. 224 websockettest.AssertJSONInitialErrorNil(c, conn) 225 226 tracker := state.NewLastSentLogTracker(s.State, s.State.ModelUUID(), "migration-logtransfer") 227 defer tracker.Close() 228 229 t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC) 230 err := conn.WriteJSON(¶ms.LogRecord{ 231 Entity: "machine-23", 232 Time: t0, 233 Module: "some.where", 234 Location: "foo.go:42", 235 Level: loggo.INFO.String(), 236 Message: "all is well", 237 }) 238 c.Assert(err, jc.ErrorIsNil) 239 240 // First message time is tracked. 241 assertTrackerTime(c, tracker, t0) 242 243 // Doesn't track anything more until a log message 2 mins later. 244 t1 := t0.Add(2*time.Minute - 1*time.Nanosecond) 245 err = conn.WriteJSON(¶ms.LogRecord{ 246 Entity: "machine-23", 247 Time: t1, 248 Module: "some.where", 249 Location: "foo.go:42", 250 Level: loggo.INFO.String(), 251 Message: "still good", 252 }) 253 c.Assert(err, jc.ErrorIsNil) 254 255 // No change 256 assertTrackerTime(c, tracker, t0) 257 258 t2 := t1.Add(1 * time.Nanosecond) 259 err = conn.WriteJSON(¶ms.LogRecord{ 260 Entity: "machine-23", 261 Time: t2, 262 Module: "some.where", 263 Location: "foo.go:42", 264 Level: loggo.INFO.String(), 265 Message: "nae bather", 266 }) 267 c.Assert(err, jc.ErrorIsNil) 268 269 // Updated 270 assertTrackerTime(c, tracker, t2) 271 272 t3 := t2.Add(1 * time.Nanosecond) 273 err = conn.WriteJSON(¶ms.LogRecord{ 274 Entity: "machine-23", 275 Time: t3, 276 Module: "some.where", 277 Location: "foo.go:42", 278 Level: loggo.INFO.String(), 279 Message: "sweet as", 280 }) 281 c.Assert(err, jc.ErrorIsNil) 282 283 // No change, 284 assertTrackerTime(c, tracker, t2) 285 286 err = conn.Close() 287 c.Assert(err, jc.ErrorIsNil) 288 289 // Latest is saved when connection is closed. 290 assertTrackerTime(c, tracker, t3) 291 } 292 293 func assertTrackerTime(c *gc.C, tracker *state.LastSentLogTracker, expected time.Time) { 294 var timestamp int64 295 var err error 296 for a := coretesting.LongAttempt.Start(); a.Next(); { 297 _, timestamp, err = tracker.Get() 298 if err != nil && errors.Cause(err) != state.ErrNeverForwarded { 299 c.Assert(err, jc.ErrorIsNil) 300 } 301 if err == nil && timestamp == expected.UnixNano() { 302 return 303 } 304 } 305 c.Fatalf("tracker never set to %d - last seen was %d (err: %v)", expected.UnixNano(), timestamp, err) 306 } 307 308 func (s *logtransferSuite) setUserAccess(c *gc.C, level permission.Access) { 309 _, err := s.State.SetUserAccess(s.userTag, s.State.ControllerTag(), level) 310 c.Assert(err, jc.ErrorIsNil) 311 } 312 313 func (s *logtransferSuite) setMigrationMode(c *gc.C, mode state.MigrationMode) { 314 model, err := s.State.Model() 315 c.Assert(err, jc.ErrorIsNil) 316 err = model.SetMigrationMode(mode) 317 c.Assert(err, jc.ErrorIsNil) 318 }