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