github.com/rochacon/deis@v1.0.2-0.20150903015341-6839b592a1ff/builder/etcd/etcd.go (about) 1 /*Package etcd is a library for performing common Etcd tasks. 2 */ 3 package etcd 4 5 import ( 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "os" 11 "os/exec" 12 "strings" 13 "time" 14 15 "github.com/Masterminds/cookoo" 16 "github.com/Masterminds/cookoo/log" 17 "github.com/Masterminds/cookoo/safely" 18 19 "github.com/coreos/go-etcd/etcd" 20 ) 21 22 var ( 23 retryCycles = 2 24 retrySleep = 200 * time.Millisecond 25 ) 26 27 // Getter describes the Get behavior of an Etcd client. 28 // 29 // Usually you will want to use go-etcd/etcd.Client to satisfy this. 30 // 31 // We use an interface because it is more testable. 32 type Getter interface { 33 Get(string, bool, bool) (*etcd.Response, error) 34 } 35 36 // DirCreator describes etcd's CreateDir behavior. 37 // 38 // Usually you will want to use go-etcd/etcd.Client to satisfy this. 39 type DirCreator interface { 40 CreateDir(string, uint64) (*etcd.Response, error) 41 } 42 43 // Watcher watches an etcd entry. 44 type Watcher interface { 45 Watch(string, uint64, bool, chan *etcd.Response, chan bool) (*etcd.Response, error) 46 } 47 48 // Setter sets a value in Etcd. 49 type Setter interface { 50 Set(string, string, uint64) (*etcd.Response, error) 51 } 52 53 // GetterSetter performs get and set operations. 54 type GetterSetter interface { 55 Getter 56 Setter 57 } 58 59 // CreateClient creates a new Etcd client and prepares it for work. 60 // 61 // Params: 62 // - url (string): A server to connect to. 63 // - retries (int): Number of times to retry a connection to the server 64 // - retrySleep (time.Duration): How long to sleep between retries 65 // 66 // Returns: 67 // This puts an *etcd.Client into the context. 68 func CreateClient(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 69 url := p.Get("url", "http://localhost:4001").(string) 70 71 // Backed this out because it's unnecessary so far. 72 //hosts := p.Get("urls", []string{"http://localhost:4001"}).([]string) 73 hosts := []string{url} 74 retryCycles = p.Get("retries", retryCycles).(int) 75 retrySleep = p.Get("retrySleep", retrySleep).(time.Duration) 76 77 // Support `host:port` format, too. 78 for i, host := range hosts { 79 if !strings.Contains(host, "://") { 80 hosts[i] = "http://" + host 81 } 82 } 83 84 client := etcd.NewClient(hosts) 85 client.CheckRetry = checkRetry 86 87 return client, nil 88 } 89 90 // Get performs an etcd Get operation. 91 // 92 // Params: 93 // - client (EtcdGetter): Etcd client 94 // - path (string): The path/key to fetch 95 // 96 // Returns: 97 // - This puts an `etcd.Response` into the context, and returns an error 98 // if the client could not connect. 99 func Get(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 100 cli, ok := p.Has("client") 101 if !ok { 102 return nil, errors.New("No Etcd client found.") 103 } 104 client := cli.(Getter) 105 path := p.Get("path", "/").(string) 106 107 res, err := client.Get(path, false, false) 108 if err != nil { 109 return res, err 110 } 111 112 if !res.Node.Dir { 113 return res, fmt.Errorf("Expected / to be a dir.") 114 } 115 return res, nil 116 } 117 118 // IsRunning checks to see if etcd is running. 119 // 120 // It will test `count` times before giving up. 121 // 122 // Params: 123 // - client (EtcdGetter) 124 // - count (int): Number of times to try before giving up. 125 // 126 // Returns: 127 // boolean true if etcd is listening. 128 func IsRunning(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 129 client := p.Get("client", nil).(Getter) 130 count := p.Get("count", 20).(int) 131 for i := 0; i < count; i++ { 132 _, err := client.Get("/", false, false) 133 if err == nil { 134 return true, nil 135 } 136 log.Infof(c, "Waiting for etcd to come online.") 137 time.Sleep(250 * time.Millisecond) 138 } 139 log.Errf(c, "Etcd is not answering after %d attempts.", count) 140 return false, &cookoo.FatalError{"Could not connect to Etcd."} 141 } 142 143 // Set sets a value in etcd. 144 // 145 // Params: 146 // - key (string): The key 147 // - value (string): The value 148 // - ttl (uint64): Time to live 149 // - client (EtcdGetter): Client, usually an *etcd.Client. 150 // 151 // Returns: 152 // - *etcd.Result 153 func Set(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 154 key := p.Get("key", "").(string) 155 value := p.Get("value", "").(string) 156 ttl := p.Get("ttl", uint64(20)).(uint64) 157 client := p.Get("client", nil).(Setter) 158 159 res, err := client.Set(key, value, ttl) 160 if err != nil { 161 log.Infof(c, "Failed to set %s=%s", key, value) 162 return res, err 163 } 164 165 return res, nil 166 } 167 168 // FindSSHUser finds an SSH user by public key. 169 // 170 // Some parts of the system require that we know not only the SSH key, but also 171 // the name of the user. That information is stored in etcd. 172 // 173 // Params: 174 // - client (EtcdGetter) 175 // - fingerprint (string): The fingerprint of the SSH key. 176 // 177 // Returns: 178 // - username (string) 179 func FindSSHUser(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 180 client := p.Get("client", nil).(Getter) 181 fingerprint := p.Get("fingerprint", nil).(string) 182 183 res, err := client.Get("/deis/builder/users", false, true) 184 if err != nil { 185 log.Warnf(c, "Error querying etcd: %s", err) 186 return "", err 187 } else if res.Node == nil || !res.Node.Dir { 188 log.Warnf(c, "No users found in etcd.") 189 return "", errors.New("Users not found") 190 } 191 for _, user := range res.Node.Nodes { 192 log.Infof(c, "Checking user %s", user.Key) 193 for _, keyprint := range user.Nodes { 194 if strings.HasSuffix(keyprint.Key, fingerprint) { 195 parts := strings.Split(user.Key, "/") 196 username := parts[len(parts)-1] 197 log.Infof(c, "Found user %s for fingerprint %s", username, fingerprint) 198 return username, nil 199 } 200 } 201 } 202 203 return "", fmt.Errorf("User not found for fingerprint %s", fingerprint) 204 } 205 206 // StoreHostKeys stores SSH hostkeys locally. 207 // 208 // First it tries to fetch them from etcd. If the keys are not present there, 209 // it generates new ones and then puts them into etcd. 210 // 211 // Params: 212 // - client(EtcdGetterSetter) 213 // - ciphers([]string): A list of ciphers to generate. Defaults are dsa, 214 // ecdsa, ed25519 and rsa. 215 // - basepath (string): Base path in etcd (ETCD_PATH). 216 // Returns: 217 // 218 func StoreHostKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 219 defaultCiphers := []string{"rsa", "dsa", "ecdsa", "ed25519"} 220 client := p.Get("client", nil).(GetterSetter) 221 ciphers := p.Get("ciphers", defaultCiphers).([]string) 222 basepath := p.Get("basepath", "/deis/builder").(string) 223 224 res, err := client.Get("sshHostKey", false, false) 225 if err != nil || res.Node == nil { 226 log.Infof(c, "Could not get SSH host key from etcd. Generating new ones.") 227 if err := genSSHKeys(c); err != nil { 228 log.Err(c, "Failed to generate SSH keys. Aborting.") 229 return nil, err 230 } 231 if err := keysToEtcd(c, client, ciphers, basepath); err != nil { 232 return nil, err 233 } 234 } else if err := keysToLocal(c, client, ciphers, basepath); err != nil { 235 log.Infof(c, "Fetching SSH host keys from etcd.") 236 return nil, err 237 } 238 239 return nil, nil 240 } 241 242 // keysToLocal copies SSH host keys from etcd to the local file system. 243 // 244 // This only fails if the main key, sshHostKey cannot be stored or retrieved. 245 func keysToLocal(c cookoo.Context, client Getter, ciphers []string, etcdPath string) error { 246 lpath := "/etc/ssh/ssh_host_%s_key" 247 privkey := "%s/sshHost%sKey" 248 for _, cipher := range ciphers { 249 path := fmt.Sprintf(lpath, cipher) 250 key := fmt.Sprintf(privkey, etcdPath, cipher) 251 res, err := client.Get(key, false, false) 252 if err != nil || res.Node == nil { 253 continue 254 } 255 256 content := res.Node.Value 257 if err := ioutil.WriteFile(path, []byte(content), 0600); err != nil { 258 log.Errf(c, "Error writing ssh host key file: %s", err) 259 } 260 } 261 262 // Now get generic key. 263 res, err := client.Get("sshHostKey", false, false) 264 if err != nil || res.Node == nil { 265 return fmt.Errorf("Failed to get sshHostKey from etcd. %v", err) 266 } 267 268 content := res.Node.Value 269 if err := ioutil.WriteFile("/etc/ssh/ssh_host_key", []byte(content), 0600); err != nil { 270 log.Errf(c, "Error writing ssh host key file: %s", err) 271 return err 272 } 273 return nil 274 } 275 276 // keysToEtcd copies local keys into etcd. 277 // 278 // It only fails if it cannot copy ssh_host_key to sshHostKey. All other 279 // abnormal conditions are logged, but not considered to be failures. 280 func keysToEtcd(c cookoo.Context, client Setter, ciphers []string, etcdPath string) error { 281 lpath := "/etc/ssh/ssh_host_%s_key" 282 privkey := "%s/sshHost%sKey" 283 for _, cipher := range ciphers { 284 path := fmt.Sprintf(lpath, cipher) 285 key := fmt.Sprintf(privkey, etcdPath, cipher) 286 content, err := ioutil.ReadFile(path) 287 if err != nil { 288 log.Infof(c, "No key named %s", path) 289 } else if _, err := client.Set(key, string(content), 0); err != nil { 290 log.Errf(c, "Could not store ssh key in etcd: %s", err) 291 } 292 } 293 // Now we set the generic key: 294 if content, err := ioutil.ReadFile("/etc/ssh/ssh_host_key"); err != nil { 295 log.Errf(c, "Could not read the ssh_host_key file.") 296 return err 297 } else if _, err := client.Set("sshHostKey", string(content), 0); err != nil { 298 log.Errf(c, "Failed to set sshHostKey in etcd.") 299 return err 300 } 301 return nil 302 } 303 304 // genSshKeys generates the default set of SSH host keys. 305 func genSSHKeys(c cookoo.Context) error { 306 // Generate a new key 307 out, err := exec.Command("ssh-keygen", "-A").CombinedOutput() 308 if err != nil { 309 log.Infof(c, "ssh-keygen: %s", out) 310 log.Errf(c, "Failed to generate SSH keys: %s", err) 311 return err 312 } 313 return nil 314 } 315 316 // UpdateHostPort intermittently notifies etcd of the builder's address. 317 // 318 // If `port` is specified, this will notify etcd at 10 second intervals that 319 // the builder is listening at $HOST:$PORT, setting the TTL to 20 seconds. 320 // 321 // This will notify etcd as long as the local sshd is running. 322 // 323 // Params: 324 // - base (string): The base path to write the data: $base/host and $base/port. 325 // - host (string): The hostname 326 // - port (string): The port 327 // - client (Setter): The client to use to write the data to etcd. 328 // - sshPid (int): The PID for SSHD. If SSHD dies, this stops notifying. 329 func UpdateHostPort(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 330 331 base := p.Get("base", "").(string) 332 host := p.Get("host", "").(string) 333 port := p.Get("port", "").(string) 334 client := p.Get("client", nil).(Setter) 335 sshd := p.Get("sshdPid", 0).(int) 336 337 // If no port is specified, we don't do anything. 338 if len(port) == 0 { 339 log.Infof(c, "No external port provided. Not publishing details.") 340 return false, nil 341 } 342 343 var ttl uint64 = 20 344 345 if err := setHostPort(client, base, host, port, ttl); err != nil { 346 log.Errf(c, "Etcd error setting host/port: %s", err) 347 return false, err 348 } 349 350 // Update etcd every ten seconds with this builder's host/port. 351 safely.GoDo(c, func() { 352 ticker := time.NewTicker(10 * time.Second) 353 for range ticker.C { 354 //log.Infof(c, "Setting SSHD host/port") 355 if _, err := os.FindProcess(sshd); err != nil { 356 log.Errf(c, "Lost SSHd process: %s", err) 357 break 358 } else { 359 if err := setHostPort(client, base, host, port, ttl); err != nil { 360 log.Errf(c, "Etcd error setting host/port: %s", err) 361 break 362 } 363 } 364 } 365 ticker.Stop() 366 }) 367 368 return true, nil 369 } 370 371 func setHostPort(client Setter, base, host, port string, ttl uint64) error { 372 if _, err := client.Set(base+"/host", host, ttl); err != nil { 373 return err 374 } 375 if _, err := client.Set(base+"/port", port, ttl); err != nil { 376 return err 377 } 378 return nil 379 } 380 381 // MakeDir makes a directory in Etcd. 382 // 383 // Params: 384 // - client (EtcdDirCreator): Etcd client 385 // - path (string): The name of the directory to create. 386 // - ttl (uint64): Time to live. 387 // Returns: 388 // *etcd.Response 389 func MakeDir(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 390 name := p.Get("path", "").(string) 391 ttl := p.Get("ttl", uint64(0)).(uint64) 392 cli, ok := p.Has("client") 393 if !ok { 394 return nil, errors.New("No Etcd client found.") 395 } 396 client := cli.(DirCreator) 397 398 if len(name) == 0 { 399 return false, errors.New("Expected directory name to be more than zero characters.") 400 } 401 402 res, err := client.CreateDir(name, ttl) 403 if err != nil { 404 return res, &cookoo.RecoverableError{err.Error()} 405 } 406 407 return res, nil 408 409 } 410 411 // Watch watches a given path, and executes a git check-repos for each event. 412 // 413 // It starts the watcher and then returns. The watcher runs on its own 414 // goroutine. To stop the watching, send the returned channel a bool. 415 // 416 // Params: 417 // - client (Watcher): An Etcd client. 418 // - path (string): The path to watch 419 // 420 // Returns: 421 // - chan bool: Send this a message to stop the watcher. 422 func Watch(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 423 // etcdctl -C $ETCD watch --recursive /deis/services 424 path := p.Get("path", "/deis/services").(string) 425 cli, ok := p.Has("client") 426 if !ok { 427 return nil, errors.New("No etcd client found.") 428 } 429 client := cli.(Watcher) 430 431 // Stupid hack because etcd watch seems to be broken, constantly complaining 432 // that the JSON it received is malformed. 433 safely.GoDo(c, func() { 434 for { 435 response, err := client.Watch(path, 0, true, nil, nil) 436 if err != nil { 437 log.Errf(c, "Etcd Watch failed: %s", err) 438 time.Sleep(50 * time.Millisecond) 439 continue 440 } 441 442 if response.Node == nil { 443 log.Infof(c, "Unexpected Etcd message: %v", response) 444 } 445 git := exec.Command("/home/git/check-repos") 446 if out, err := git.CombinedOutput(); err != nil { 447 log.Errf(c, "Failed git check-repos: %s", err) 448 log.Infof(c, "Output: %s", out) 449 } 450 } 451 452 }) 453 454 return nil, nil 455 456 /* Watch seems to be broken. So we do this stupid watch loop instead. 457 receiver := make(chan *etcd.Response) 458 stop := make(chan bool) 459 // Buffer the channels so that we don't hang waiting for go-etcd to 460 // read off the channel. 461 stopetcd := make(chan bool, 1) 462 stopwatch := make(chan bool, 1) 463 464 465 // Watch for errors. 466 safely.GoDo(c, func() { 467 // When a receiver is passed in, no *Response is ever returned. Instead, 468 // Watch acts like an error channel, and receiver gets all of the messages. 469 _, err := client.Watch(path, 0, true, receiver, stopetcd) 470 if err != nil { 471 log.Infof(c, "Watcher stopped with error '%s'", err) 472 stopwatch <- true 473 //close(stopwatch) 474 } 475 }) 476 // Watch for events 477 safely.GoDo(c, func() { 478 for { 479 select { 480 case msg := <-receiver: 481 if msg.Node != nil { 482 log.Infof(c, "Received notification %s for %s", msg.Action, msg.Node.Key) 483 } else { 484 log.Infof(c, "Received unexpected etcd message: %v", msg) 485 } 486 git := exec.Command("/home/git/check-repos") 487 if out, err := git.CombinedOutput(); err != nil { 488 log.Errf(c, "Failed git check-repos: %s", err) 489 log.Infof(c, "Output: %s", out) 490 } 491 case <-stopwatch: 492 c.Logf("debug", "Received signal to stop watching events.") 493 return 494 } 495 } 496 }) 497 // Fan out stop requests. 498 safely.GoDo(c, func() { 499 <-stop 500 stopwatch <- true 501 stopetcd <- true 502 close(stopwatch) 503 close(stopetcd) 504 }) 505 506 return stop, nil 507 */ 508 } 509 510 // checkRetry overrides etcd.DefaultCheckRetry. 511 // 512 // It adds configurable number of retries and configurable timesouts. 513 func checkRetry(c *etcd.Cluster, numReqs int, last http.Response, err error) error { 514 515 if numReqs > retryCycles*len(c.Machines) { 516 return fmt.Errorf("Tried and failed %d cluster connections: %s", retryCycles, err) 517 } 518 519 switch last.StatusCode { 520 case 0: 521 return nil 522 case 500: 523 time.Sleep(retrySleep) 524 return nil 525 case 200: 526 return nil 527 default: 528 return fmt.Errorf("Unhandled HTTP Error: %s %d", last.Status, last.StatusCode) 529 } 530 }