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(&params.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(&params.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(&params.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(&params.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(&params.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(&params.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  }