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  }