github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/state/apiserver/server_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "crypto/tls" 8 "crypto/x509" 9 "fmt" 10 "io" 11 "net" 12 stdtesting "testing" 13 "time" 14 15 "code.google.com/p/go.net/websocket" 16 jc "github.com/juju/testing/checkers" 17 "github.com/juju/utils" 18 gc "launchpad.net/gocheck" 19 20 "github.com/juju/juju/cert" 21 jujutesting "github.com/juju/juju/juju/testing" 22 "github.com/juju/juju/rpc" 23 "github.com/juju/juju/state" 24 "github.com/juju/juju/state/api" 25 "github.com/juju/juju/state/api/params" 26 "github.com/juju/juju/state/apiserver" 27 coretesting "github.com/juju/juju/testing" 28 ) 29 30 func TestAll(t *stdtesting.T) { 31 coretesting.MgoTestPackage(t) 32 } 33 34 var fastDialOpts = api.DialOpts{} 35 36 type serverSuite struct { 37 jujutesting.JujuConnSuite 38 } 39 40 var _ = gc.Suite(&serverSuite{}) 41 42 func (s *serverSuite) TestStop(c *gc.C) { 43 // Start our own instance of the server so we have 44 // a handle on it to stop it. 45 srv, err := apiserver.NewServer(s.State, apiserver.ServerConfig{ 46 Addr: "localhost:0", 47 Cert: []byte(coretesting.ServerCert), 48 Key: []byte(coretesting.ServerKey), 49 }) 50 c.Assert(err, gc.IsNil) 51 defer srv.Stop() 52 53 stm, err := s.State.AddMachine("quantal", state.JobHostUnits) 54 c.Assert(err, gc.IsNil) 55 err = stm.SetProvisioned("foo", "fake_nonce", nil) 56 c.Assert(err, gc.IsNil) 57 password, err := utils.RandomPassword() 58 c.Assert(err, gc.IsNil) 59 err = stm.SetPassword(password) 60 c.Assert(err, gc.IsNil) 61 62 // Note we can't use openAs because we're not connecting to 63 // s.APIConn. 64 apiInfo := &api.Info{ 65 Tag: stm.Tag(), 66 Password: password, 67 Nonce: "fake_nonce", 68 Addrs: []string{srv.Addr()}, 69 CACert: coretesting.CACert, 70 } 71 st, err := api.Open(apiInfo, fastDialOpts) 72 c.Assert(err, gc.IsNil) 73 defer st.Close() 74 75 _, err = st.Machiner().Machine(stm.Tag()) 76 c.Assert(err, gc.IsNil) 77 78 err = srv.Stop() 79 c.Assert(err, gc.IsNil) 80 81 _, err = st.Machiner().Machine(stm.Tag()) 82 // The client has not necessarily seen the server shutdown yet, 83 // so there are two possible errors. 84 if err != rpc.ErrShutdown && err != io.ErrUnexpectedEOF { 85 c.Fatalf("unexpected error from request: %v", err) 86 } 87 88 // Check it can be stopped twice. 89 err = srv.Stop() 90 c.Assert(err, gc.IsNil) 91 } 92 93 func (s *serverSuite) TestOpenAsMachineErrors(c *gc.C) { 94 assertNotProvisioned := func(err error) { 95 c.Assert(err, gc.NotNil) 96 c.Assert(err, jc.Satisfies, params.IsCodeNotProvisioned) 97 c.Assert(err, gc.ErrorMatches, `machine \d+ is not provisioned`) 98 } 99 stm, err := s.State.AddMachine("quantal", state.JobHostUnits) 100 c.Assert(err, gc.IsNil) 101 err = stm.SetProvisioned("foo", "fake_nonce", nil) 102 c.Assert(err, gc.IsNil) 103 password, err := utils.RandomPassword() 104 c.Assert(err, gc.IsNil) 105 err = stm.SetPassword(password) 106 c.Assert(err, gc.IsNil) 107 108 // This does almost exactly the same as OpenAPIAsMachine but checks 109 // for failures instead. 110 _, info, err := s.APIConn.Environ.StateInfo() 111 info.Tag = stm.Tag() 112 info.Password = password 113 info.Nonce = "invalid-nonce" 114 st, err := api.Open(info, fastDialOpts) 115 assertNotProvisioned(err) 116 c.Assert(st, gc.IsNil) 117 118 // Try with empty nonce as well. 119 info.Nonce = "" 120 st, err = api.Open(info, fastDialOpts) 121 assertNotProvisioned(err) 122 c.Assert(st, gc.IsNil) 123 124 // Finally, with the correct one succeeds. 125 info.Nonce = "fake_nonce" 126 st, err = api.Open(info, fastDialOpts) 127 c.Assert(err, gc.IsNil) 128 c.Assert(st, gc.NotNil) 129 st.Close() 130 131 // Now add another machine, intentionally unprovisioned. 132 stm1, err := s.State.AddMachine("quantal", state.JobHostUnits) 133 c.Assert(err, gc.IsNil) 134 err = stm1.SetPassword(password) 135 c.Assert(err, gc.IsNil) 136 137 // Try connecting, it will fail. 138 info.Tag = stm1.Tag() 139 info.Nonce = "" 140 st, err = api.Open(info, fastDialOpts) 141 assertNotProvisioned(err) 142 c.Assert(st, gc.IsNil) 143 } 144 145 func (s *serverSuite) TestMachineLoginStartsPinger(c *gc.C) { 146 // This is the same steps as OpenAPIAsNewMachine but we need to assert 147 // the agent is not alive before we actually open the API. 148 // Create a new machine to verify "agent alive" behavior. 149 machine, err := s.State.AddMachine("quantal", state.JobHostUnits) 150 c.Assert(err, gc.IsNil) 151 err = machine.SetProvisioned("foo", "fake_nonce", nil) 152 c.Assert(err, gc.IsNil) 153 password, err := utils.RandomPassword() 154 c.Assert(err, gc.IsNil) 155 err = machine.SetPassword(password) 156 c.Assert(err, gc.IsNil) 157 158 // Not alive yet. 159 s.assertAlive(c, machine, false) 160 161 // Login as the machine agent of the created machine. 162 st := s.OpenAPIAsMachine(c, machine.Tag(), password, "fake_nonce") 163 164 // Make sure the pinger has started. 165 s.assertAlive(c, machine, true) 166 167 // Now make sure it stops when connection is closed. 168 c.Assert(st.Close(), gc.IsNil) 169 170 // Sync, then wait for a bit to make sure the state is updated. 171 s.State.StartSync() 172 <-time.After(coretesting.ShortWait) 173 s.State.StartSync() 174 175 s.assertAlive(c, machine, false) 176 } 177 178 func (s *serverSuite) TestUnitLoginStartsPinger(c *gc.C) { 179 // Create a new service and unit to verify "agent alive" behavior. 180 service := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) 181 unit, err := service.AddUnit() 182 c.Assert(err, gc.IsNil) 183 password, err := utils.RandomPassword() 184 c.Assert(err, gc.IsNil) 185 err = unit.SetPassword(password) 186 c.Assert(err, gc.IsNil) 187 188 // Not alive yet. 189 s.assertAlive(c, unit, false) 190 191 // Login as the unit agent of the created unit. 192 st := s.OpenAPIAs(c, unit.Tag(), password) 193 194 // Make sure the pinger has started. 195 s.assertAlive(c, unit, true) 196 197 // Now make sure it stops when connection is closed. 198 c.Assert(st.Close(), gc.IsNil) 199 200 // Sync, then wait for a bit to make sure the state is updated. 201 s.State.StartSync() 202 <-time.After(coretesting.ShortWait) 203 s.State.StartSync() 204 205 s.assertAlive(c, unit, false) 206 } 207 208 type agentAliver interface { 209 AgentAlive() (bool, error) 210 } 211 212 func (s *serverSuite) assertAlive(c *gc.C, entity agentAliver, isAlive bool) { 213 s.State.StartSync() 214 alive, err := entity.AgentAlive() 215 c.Assert(err, gc.IsNil) 216 c.Assert(alive, gc.Equals, isAlive) 217 } 218 219 func dialWebsocket(c *gc.C, addr, path string) (*websocket.Conn, error) { 220 origin := "http://localhost/" 221 url := fmt.Sprintf("wss://%s%s", addr, path) 222 config, err := websocket.NewConfig(url, origin) 223 c.Assert(err, gc.IsNil) 224 pool := x509.NewCertPool() 225 xcert, err := cert.ParseCert(coretesting.CACert) 226 c.Assert(err, gc.IsNil) 227 pool.AddCert(xcert) 228 config.TlsConfig = &tls.Config{RootCAs: pool} 229 return websocket.DialConfig(config) 230 } 231 232 func (s *serverSuite) TestNonCompatiblePathsAre404(c *gc.C) { 233 // we expose the API at '/' for compatibility, and at '/ENVUUID/api' 234 // for the correct location, but other Paths should fail. 235 srv, err := apiserver.NewServer(s.State, apiserver.ServerConfig{ 236 Addr: "localhost:0", 237 Cert: []byte(coretesting.ServerCert), 238 Key: []byte(coretesting.ServerKey), 239 }) 240 c.Assert(err, gc.IsNil) 241 defer srv.Stop() 242 // We have to use 'localhost' because that is what the TLS cert says. 243 // So find just the Port for the server 244 _, portString, err := net.SplitHostPort(srv.Addr()) 245 c.Assert(err, gc.IsNil) 246 addr := "localhost:" + portString 247 // '/' should be fine 248 conn, err := dialWebsocket(c, addr, "/") 249 c.Assert(err, gc.IsNil) 250 conn.Close() 251 // '/environment/ENVIRONUUID/api' should be fine 252 conn, err = dialWebsocket(c, addr, "/environment/dead-beef-123456/api") 253 c.Assert(err, gc.IsNil) 254 conn.Close() 255 256 // '/randompath' is not ok 257 conn, err = dialWebsocket(c, addr, "/randompath") 258 // Unfortunately go.net/websocket just returns Bad Status, it doesn't 259 // give us any information (whether this was a 404 Not Found, Internal 260 // Server Error, 200 OK, etc.) 261 c.Assert(err, gc.ErrorMatches, `websocket.Dial wss://localhost:\d+/randompath: bad status`) 262 c.Assert(conn, gc.IsNil) 263 }