github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/system/login.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package system 5 6 import ( 7 "github.com/juju/cmd" 8 "github.com/juju/errors" 9 "github.com/juju/juju/api/usermanager" 10 "github.com/juju/names" 11 "github.com/juju/utils" 12 goyaml "gopkg.in/yaml.v1" 13 "launchpad.net/gnuflag" 14 15 "github.com/juju/juju/api" 16 "github.com/juju/juju/cmd/envcmd" 17 "github.com/juju/juju/environs/configstore" 18 "github.com/juju/juju/juju" 19 "github.com/juju/juju/network" 20 ) 21 22 // GetUserManagerFunc defines a function that takes an api connection 23 // and returns the (locally defined) UserManager interface. 24 type GetUserManagerFunc func(conn api.Connection) (UserManager, error) 25 26 // LoginCommand logs in to a Juju system and caches the connection 27 // information. 28 type LoginCommand struct { 29 cmd.CommandBase 30 apiOpen api.OpenFunc 31 getUserManager GetUserManagerFunc 32 // TODO (thumper): when we support local cert definitions 33 // allow the use to specify the user and server address. 34 // user string 35 // address string 36 Server cmd.FileVar 37 Name string 38 KeepPassword bool 39 } 40 41 var loginDoc = ` 42 login connects to a juju system and caches the information that juju 43 needs to connect to the api server in the $(JUJU_HOME)/environments directory. 44 45 In order to login to a system, you need to have a user already created for you 46 in that system. The way that this occurs is for an existing user on the system 47 to create you as a user. This will generate a file that contains the 48 information needed to connect. 49 50 If you have been sent one of these server files, you can login by doing the 51 following: 52 53 # if you have saved the server file as ~/erica.server 54 juju system login --server=~/erica.server test-system 55 56 A new strong random password is generated to replace the password defined in 57 the server file. The 'test-system' will also become the current system that 58 the juju command will talk to by default. 59 60 If you have used the 'api-info' command to generate a copy of your current 61 credentials for a system, you should use the --keep-password option as it will 62 mean that you will still be able to connect to the api server from the 63 computer where you ran api-info. 64 65 See Also: 66 juju help system environments 67 juju help system use-environment 68 juju help system create-environment 69 juju help user add 70 juju help switch 71 ` 72 73 // Info implements Command.Info 74 func (c *LoginCommand) Info() *cmd.Info { 75 return &cmd.Info{ 76 Name: "login", 77 // TODO(thumper): support user and address options 78 // Args: "<name> [<server address>[:<server port>]]" 79 Args: "<name>", 80 Purpose: "login to a Juju System", 81 Doc: loginDoc, 82 } 83 } 84 85 // SetFlags implements Command.SetFlags. 86 func (c *LoginCommand) SetFlags(f *gnuflag.FlagSet) { 87 f.Var(&c.Server, "server", "path to yaml-formatted server file") 88 f.BoolVar(&c.KeepPassword, "keep-password", false, "do not generate a new random password") 89 } 90 91 // SetFlags implements Command.Init. 92 func (c *LoginCommand) Init(args []string) error { 93 if c.apiOpen == nil { 94 c.apiOpen = apiOpen 95 } 96 if c.getUserManager == nil { 97 c.getUserManager = getUserManager 98 } 99 if len(args) == 0 { 100 return errors.New("no name specified") 101 } 102 103 c.Name, args = args[0], args[1:] 104 return cmd.CheckEmpty(args) 105 } 106 107 // Run implements Command.Run 108 func (c *LoginCommand) Run(ctx *cmd.Context) error { 109 // TODO(thumper): as we support the user and address 110 // change this check here. 111 if c.Server.Path == "" { 112 return errors.New("no server file specified") 113 } 114 115 serverYAML, err := c.Server.Read(ctx) 116 if err != nil { 117 return errors.Trace(err) 118 } 119 120 var serverDetails envcmd.ServerFile 121 if err := goyaml.Unmarshal(serverYAML, &serverDetails); err != nil { 122 return errors.Trace(err) 123 } 124 125 // Construct the api.Info struct from the provided values 126 // and attempt to connect to the remote server before we do anything else. 127 if !names.IsValidUser(serverDetails.Username) { 128 return errors.Errorf("%q is not a valid username", serverDetails.Username) 129 } 130 131 userTag := names.NewUserTag(serverDetails.Username) 132 if userTag.Provider() != names.LocalProvider { 133 // Remove users do not have their passwords stored in Juju 134 // so we never attempt to change them. 135 c.KeepPassword = true 136 } 137 138 info := api.Info{ 139 Addrs: serverDetails.Addresses, 140 CACert: serverDetails.CACert, 141 Tag: userTag, 142 Password: serverDetails.Password, 143 } 144 145 apiState, err := c.apiOpen(&info, api.DefaultDialOpts()) 146 if err != nil { 147 return errors.Trace(err) 148 } 149 defer apiState.Close() 150 151 // If we get to here, the credentials supplied were sufficient to connect 152 // to the Juju System and login. Now we cache the details. 153 serverInfo, err := c.cacheConnectionInfo(serverDetails, apiState) 154 if err != nil { 155 return errors.Trace(err) 156 } 157 ctx.Infof("cached connection details as system %q", c.Name) 158 159 // If we get to here, we have been able to connect to the API server, and 160 // also have been able to write the cached information. Now we can change 161 // the user's password to a new randomly generated strong password, and 162 // update the cached information knowing that the likelihood of failure is 163 // minimal. 164 if !c.KeepPassword { 165 if err := c.updatePassword(ctx, apiState, userTag, serverInfo); err != nil { 166 return errors.Trace(err) 167 } 168 } 169 170 return errors.Trace(envcmd.SetCurrentSystem(ctx, c.Name)) 171 } 172 173 func (c *LoginCommand) cacheConnectionInfo(serverDetails envcmd.ServerFile, apiState api.Connection) (configstore.EnvironInfo, error) { 174 store, err := configstore.Default() 175 if err != nil { 176 return nil, errors.Trace(err) 177 } 178 serverInfo := store.CreateInfo(c.Name) 179 180 serverTag, err := apiState.ServerTag() 181 if err != nil { 182 return nil, errors.Wrap(err, errors.New("juju system too old to support login")) 183 } 184 185 connectedAddresses, err := network.ParseHostPorts(apiState.Addr()) 186 if err != nil { 187 // Should never happen, since we've just connected with it. 188 return nil, errors.Annotatef(err, "invalid API address %q", apiState.Addr()) 189 } 190 addressConnectedTo := connectedAddresses[0] 191 192 addrs, hosts, changed := juju.PrepareEndpointsForCaching(serverInfo, apiState.APIHostPorts(), addressConnectedTo) 193 if !changed { 194 logger.Infof("api addresses: %v", apiState.APIHostPorts()) 195 logger.Infof("address connected to: %v", addressConnectedTo) 196 return nil, errors.New("no addresses returned from prepare for caching") 197 } 198 199 serverInfo.SetAPICredentials( 200 configstore.APICredentials{ 201 User: serverDetails.Username, 202 Password: serverDetails.Password, 203 }) 204 205 serverInfo.SetAPIEndpoint(configstore.APIEndpoint{ 206 Addresses: addrs, 207 Hostnames: hosts, 208 CACert: serverDetails.CACert, 209 ServerUUID: serverTag.Id(), 210 }) 211 212 if err = serverInfo.Write(); err != nil { 213 return nil, errors.Trace(err) 214 } 215 return serverInfo, nil 216 } 217 218 func (c *LoginCommand) updatePassword(ctx *cmd.Context, conn api.Connection, userTag names.UserTag, serverInfo configstore.EnvironInfo) error { 219 password, err := utils.RandomPassword() 220 if err != nil { 221 return errors.Annotate(err, "failed to generate random password") 222 } 223 224 userManager, err := c.getUserManager(conn) 225 if err != nil { 226 return errors.Trace(err) 227 } 228 if err := userManager.SetPassword(userTag.Name(), password); err != nil { 229 errors.Trace(err) 230 } 231 ctx.Infof("password updated\n") 232 creds := serverInfo.APICredentials() 233 creds.Password = password 234 serverInfo.SetAPICredentials(creds) 235 if err = serverInfo.Write(); err != nil { 236 return errors.Trace(err) 237 } 238 return nil 239 } 240 241 func apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 242 return api.Open(info, opts) 243 } 244 245 // UserManager defines the calls that the Login command makes to the user 246 // manager client. It is returned by a helper function that is overridden in 247 // tests. 248 type UserManager interface { 249 SetPassword(username, password string) error 250 } 251 252 func getUserManager(conn api.Connection) (UserManager, error) { 253 return usermanager.NewClient(conn), nil 254 }