├── .gitignore ├── Makefile ├── README.md ├── client └── client.go ├── common ├── common.go ├── config.go ├── etcd.go └── fleet.go └── scriptrock_etcd_conf └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .*.swo 3 | *~ 4 | client/peerdiscovery_query 5 | daemon/peerdiscovery_daemon 6 | build/ 7 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIR = build 2 | BINS = build/scriptrock_etcd_conf 3 | VERSION = 0.1.0 4 | OS = $(shell uname -s) 5 | ARCH = $(shell uname -m) 6 | PACKAGE_NAME = scriptrock_peerdiscovery-$(VERSION)-$(OS)-$(ARCH) 7 | TARBALL = $(PACKAGE_NAME).tar.gz 8 | 9 | GITHUB_RELEASE_URL = https://uploads.github.com/repos/ScriptRock/peerdiscovery/releases 10 | 11 | default: $(BINS) 12 | 13 | $(BINS): force 14 | 15 | $(BUILD_DIR)/%: %/main.go 16 | (cd ${ ${@F}) 25 | 26 | package: $(BUILD_DIR)/$(TARBALL) 27 | 28 | goxc_coreos: 29 | cd scriptrock_etcd_conf && goxc -bc="linux,amd64" -pv coreos -d=../build/ 30 | 31 | goxc: 32 | cd scriptrock_etcd_conf && goxc -bc="linux,!arm, darwin,!arm" -pv $(VERSION) -d=../build/ 33 | 34 | clean: force 35 | rm -rf $(BUILD_DIR)/ 36 | 37 | push_package_to_github: $(BUILD_DIR)/$(TARBALL) 38 | curl -H "Content-Type: application/x-compressed" --upload-file $< $(GITHUB_RELEASE_URL)/$(VERSION)/assets?name=${= 9 && fields[0] == "=" { 78 | a := &AvahiBrowseResult{ 79 | Type: fields[0], 80 | InterfaceName: fields[1], 81 | Protocol: fields[2], 82 | Name: fields[3], 83 | Service: fields[4], 84 | Domain: fields[5], 85 | Host: fields[6], 86 | IPString: fields[7], 87 | IPv4: nil, 88 | IPv6: nil, 89 | PortString: fields[8], 90 | Port: 0, 91 | } 92 | if v, err := strconv.Atoi(a.PortString); err == nil { 93 | a.Port = v 94 | } 95 | if a.Protocol == "IPv4" { 96 | a.IPv4 = net.ParseIP(a.IPString) 97 | } 98 | if a.Protocol == "IPv6" { 99 | a.IPv6 = net.ParseIP(a.IPString) 100 | } 101 | results = append(results, a) 102 | } 103 | } 104 | return results 105 | } 106 | 107 | func avahiPrefix() string { 108 | wrapperPath := "" 109 | 110 | wrapperPath = "/opt/usr/bin/" 111 | if _, err := os.Stat(wrapperPath); err == nil { 112 | return wrapperPath 113 | } 114 | wrapperPath = "/opt/scriptrock_utils/docker_avahi/docker_avahi_ssh.sh" 115 | if _, err := os.Stat(wrapperPath); err == nil { 116 | return wrapperPath + " " 117 | } 118 | return "" 119 | } 120 | 121 | func runAvahiPublish(id string, service string, port int) *os.Process { 122 | avahiWrapper := avahiPrefix() 123 | 124 | // poll loop until the daemon is up 125 | for { 126 | command := strings.TrimSpace(fmt.Sprintf("%savahi-browse --terminate -a", avahiWrapper)) 127 | commandParts := regexp.MustCompile("\\s+").Split(command, -1) 128 | if _, err := exec.Command(commandParts[0], commandParts[1:]...).Output(); err != nil { 129 | time.Sleep(time.Second) 130 | } else { 131 | break 132 | } 133 | } 134 | 135 | command := strings.TrimSpace(fmt.Sprintf("%savahi-publish-service %s %s %d", avahiWrapper, id, service, port)) 136 | commandParts := regexp.MustCompile("\\s+").Split(command, -1) 137 | 138 | cmd := exec.Command(commandParts[0], commandParts[1:]...) 139 | cmd.SysProcAttr = &syscall.SysProcAttr{} 140 | //cmd.SysProcAttr.Pdeathsig = syscall.SIGTERM 141 | cmd.SysProcAttr.Setsid = false 142 | cmd.SysProcAttr.Setpgid = false 143 | // start process 144 | if err := cmd.Start(); err != nil { 145 | fmt.Printf("Error starting avahi command: '%s' error '%s'\n", command, err.Error()) 146 | return nil 147 | } 148 | return cmd.Process 149 | } 150 | 151 | func runAvahiBrowse(service string, results chan *AvahiBrowseResult) { 152 | avahiWrapper := avahiPrefix() 153 | 154 | command := strings.TrimSpace(fmt.Sprintf("%savahi-browse --terminate --parsable --no-db-lookup --ignore-local --resolve %s", avahiWrapper, service)) 155 | commandParts := regexp.MustCompile("\\s+").Split(command, -1) 156 | 157 | out, err := exec.Command(commandParts[0], commandParts[1:]...).Output() 158 | if err != nil { 159 | fmt.Printf("Error running avahi: command '%s' error '%s'\n", command, err.Error()) 160 | } else { 161 | parsed := parseAvahiBrowse(out) 162 | for _, p := range parsed { 163 | results <- p 164 | } 165 | } 166 | } 167 | 168 | func (cs *ClientState) WriteAvahiServiceFile() { 169 | // wrap each peer in quotes 170 | conf := fmt.Sprintf( 171 | ` 172 | 173 | 174 | %s 175 | 176 | %s 177 | %d 178 | 179 | 180 | `, 181 | cs.cfg.UUID, 182 | cs.cfg.MDNSService, 183 | cs.etcd.PeerPort) 184 | 185 | fmt.Printf("Writing avahi conf file to '%s'\n", cs.cfg.AvahiConfPath) 186 | if err := ioutil.WriteFile(cs.cfg.AvahiConfPath, []byte(conf), 0644); err != nil { 187 | fmt.Printf("Could not write conf file '%s': %s\n", cs.cfg.AvahiConfPath, err.Error()) 188 | } 189 | } 190 | 191 | func (cs *ClientState) pollLoop() { 192 | for { 193 | runAvahiBrowse(cs.cfg.MDNSService, cs.mdnsPeerServerEntries) 194 | 195 | // run avahi browse to see nearby things 196 | cs.pollEvent <- 0 197 | time.Sleep(cs.cfg.PollInterval) 198 | } 199 | } 200 | 201 | func (cs *ClientState) checkEnt(ent *AvahiBrowseResult) (*net.Interface, net.IP, net.IP, error, error) { 202 | // only use IPv4 203 | peerIP := ent.IPv4 204 | if peerIP == nil { 205 | return nil, nil, nil, fmt.Errorf("No IPv4 address present"), nil 206 | } 207 | // This technically restricts valid configurations that go through routers. Will need to re-visit. 208 | // The objective is to prevent NATs where the return path will not work, not routers where the return path will. 209 | // Eventually, must set up a tcp/http server on the destination host that echoes back the connecting IP address, 210 | // and compare against that. 211 | // There is also an issue with multiple addresses on the same subnet on the same interface; but this is dumb anyway 212 | iface, _, myIP, err := common.LocalNetForIp(peerIP) 213 | if err == nil && myIP.Equal(peerIP) { 214 | return nil, nil, nil, fmt.Errorf("IP address is self (%s = %s)", myIP.String(), peerIP.String()), nil 215 | } 216 | if strings.HasPrefix(ent.Name, cs.cfg.UUID) { 217 | // This is bad; duplicate UUID from someone that isn't us. Presumably caused by a cloned VM. 218 | // In this case, panic, delete old id, die, and on the next respawn we'll regenerate the id 219 | common.DuplicateClusterInstanceUUID() 220 | fatalErr := fmt.Errorf("Prefix UUID is from self") 221 | return nil, nil, nil, fatalErr, fatalErr 222 | } 223 | return iface, myIP, peerIP, err, nil 224 | } 225 | 226 | func (cs *ClientState) peerMDNSHostname(ent *AvahiBrowseResult) string { 227 | return ent.Name 228 | } 229 | 230 | func (cs *ClientState) stateTask() (errOut error) { 231 | polls := 0 232 | lastPollWithHigherPeer := 0 233 | finished := false 234 | errOut = nil 235 | 236 | for !finished { 237 | select { 238 | case <-cs.pollEvent: 239 | // time to give up and write out a conf 240 | polls = polls + 1 241 | fmt.Printf("poll occurred\n") 242 | if polls >= lastPollWithHigherPeer+cs.cfg.MaxLoops { 243 | fmt.Printf("%d consecutive polls with no lower peer; exiting\n", cs.cfg.MaxLoops) 244 | finished = true 245 | } 246 | case ent := <-cs.mdnsPeerServerEntries: 247 | // peer etcd server. It may still be booting though. 248 | // do an HTTP request to the server to see if it truly exists 249 | if iface, localIP, peerIP, err, fatalErr := cs.checkEnt(ent); fatalErr != nil { 250 | finished = true 251 | fmt.Printf("Fatal error from peer server entry: %s\n", err.Error()) 252 | errOut = fatalErr 253 | } else if err != nil { 254 | fmt.Println("etcd server", ent, "invalid", err) 255 | } else { 256 | peerMDNSHostname := cs.peerMDNSHostname(ent) 257 | peerPort := ent.Port 258 | fmt.Printf("etcd server mDNS response: IP %s mDNS hostname %s\n", peerIP.String(), peerMDNSHostname) 259 | url := fmt.Sprintf("http://%s:%d/v2/keys/", peerIP.String(), cs.etcd.ClientPort) 260 | if _, err := http.Get(url); err != nil { 261 | fmt.Printf("Peer at '%s' not available yet: %s\n", url, err.Error()) 262 | cs.etcd.AddBootingPeer(iface, localIP, peerIP, peerPort) 263 | if localIP.String() < peerIP.String() { 264 | // we are lower; keep going 265 | } else { 266 | lastPollWithHigherPeer = polls 267 | } 268 | } else { 269 | fmt.Printf("Peer etcd server found on %s (%s); exiting\n", url, peerIP) 270 | cs.etcd.AddServerPeer(iface, localIP, peerIP, peerPort) 271 | cs.etcd.DiscoveryURL = "" 272 | finished = true 273 | } 274 | } 275 | case url := <-cs.discoveryURL: 276 | // url is already validated 277 | cs.etcd.DiscoveryURL = url 278 | finished = true 279 | } 280 | } 281 | 282 | return errOut 283 | } 284 | 285 | func (cs *ClientState) validateDiscoveryURL(url string) bool { 286 | if url != "" { 287 | if _, err := http.Get(url); err != nil { 288 | fmt.Printf("Poll discovery URL '%s' returns error: %s\n", url, err.Error()) 289 | } else { 290 | cs.discoveryURL <- url 291 | return true 292 | } 293 | } 294 | return false 295 | } 296 | 297 | func (cs *ClientState) checkDiscoveryURL() bool { 298 | if cs.validateDiscoveryURL(cs.etcd.DiscoveryURL) { 299 | return true 300 | } else if cs.validateDiscoveryURL(os.Getenv("ETCD_DISCOVERY")) { 301 | return true 302 | } else { 303 | // check file 304 | urlFile := "/etc/etcd/discovery_url" 305 | if fileData, err := ioutil.ReadFile(urlFile); err != nil { 306 | // no file; ignore 307 | } else { 308 | if cs.validateDiscoveryURL(strings.TrimSpace(string(fileData))) { 309 | return true 310 | } 311 | } 312 | } 313 | return false 314 | } 315 | 316 | func Client() { 317 | cfg, etcd, fleet, args, err := common.LoadConfigs() 318 | if err != nil || len(args) > 1 { 319 | fmt.Printf("Error parsing options; un-parsed options remain: %s\n", strings.Join(args[1:], ", ")) 320 | } else { 321 | cs := newClientState(cfg, etcd) 322 | 323 | cs.WriteAvahiServiceFile() 324 | 325 | // if a discovery URL is present, test it and publish if successful 326 | usingDiscoveryURL := cs.checkDiscoveryURL() 327 | 328 | // otherwise start mDNS polling 329 | if !usingDiscoveryURL { 330 | go cs.pollLoop() 331 | } 332 | 333 | err := cs.stateTask() 334 | if err == nil { 335 | etcd.WriteFile() 336 | fleet.WriteFile(etcd) 337 | } else { 338 | os.Exit(1) 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | func LoadConfigs() (*Config, *EtcdConfig, *FleetConfig, []string, error) { 12 | argv1 := os.Args 13 | cfg, argv2, err := NewConfig(argv1) 14 | if err != nil { 15 | return nil, nil, nil, nil, fmt.Errorf("Error parsing main options: %s\n", err.Error()) 16 | } 17 | etcd, argv3, err := NewEtcdConfig(argv2, cfg.UUID) 18 | if err != nil { 19 | return nil, nil, nil, nil, fmt.Errorf("Error parsing etcd options: %s\n", err.Error()) 20 | } 21 | fleet, argv4, err := NewFleetConfig(argv3) 22 | if err != nil { 23 | return nil, nil, nil, nil, fmt.Errorf("Error parsing fleet options: %s\n", err.Error()) 24 | } 25 | args := argv4 26 | 27 | return cfg, etcd, fleet, args, nil 28 | } 29 | 30 | func LocalNetForIp(fromIP net.IP) (*net.Interface, net.Addr, net.IP, error) { 31 | if ifaces, err := net.Interfaces(); err != nil { 32 | return nil, nil, nil, fmt.Errorf("LocalNetForIp: Error getting interfaces: %s\n", err.Error()) 33 | } else { 34 | for _, iface := range ifaces { 35 | if (iface.Flags & net.FlagLoopback) != 0 { 36 | continue 37 | } 38 | if iface_addrs, err := iface.Addrs(); err != nil { 39 | return nil, nil, nil, fmt.Errorf("LocalNetForIp: Error getting interface addresses: %s\n", err.Error()) 40 | } else { 41 | for _, iface_addr := range iface_addrs { 42 | ipstr := iface_addr.String() 43 | ip, ipnet, err := net.ParseCIDR(ipstr) 44 | if err != nil { 45 | return nil, nil, nil, 46 | fmt.Errorf("LocalNetForIp: Error parsing local address '%s': %s\n", 47 | ipstr, err.Error()) 48 | } 49 | //fmt.Println("LocalNetForIp", "iface", iface, "ifaceaddr", iface_addr, "ip", ip, "ipnet", ipnet, "from", from) 50 | //fromHost, _, err := net.SplitHostPort(from.String()) 51 | //if err != nil { 52 | // return nil, nil, nil, 53 | // fmt.Errorf("LocalNetForIp: Error parsing from address '%s': %s\n", 54 | // from.String(), err.Error()) 55 | //} 56 | // split away the IPv6 zone if present 57 | //fromHost = strings.Split(fromHost, "%")[0] 58 | //fromIP := net.ParseIP(fromHost) 59 | //if fromIP == nil { 60 | // return nil, nil, nil, 61 | // fmt.Errorf("LocalNetForIp: Error parsing from host '%s'\n", fromHost) 62 | //} 63 | if ipnet.Contains(fromIP) { 64 | return &iface, iface_addr, ip, nil 65 | } 66 | } 67 | } 68 | } 69 | } 70 | return nil, nil, nil, fmt.Errorf("LocalNetForIp: Could not locate local interface/address matching '%s'", fromIP.String()) 71 | } 72 | 73 | func InterfaceIsVirtual(iface *net.Interface) bool { 74 | // linux specific, but this is for CoreOS anyway... 75 | // Look at the interface symlink in /sys/ to see if it is a virtual device or not. 76 | sysPath := fmt.Sprintf("/sys/class/net/%s", iface.Name) 77 | if linkTarget, err := filepath.EvalSymlinks(sysPath); err != nil { 78 | // possibly not linux, be cautious. 79 | } else { 80 | return strings.Contains(linkTarget, "/virtual/") 81 | } 82 | return false 83 | } 84 | -------------------------------------------------------------------------------- /common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | go_flags "github.com/jessevdk/go-flags" 11 | "github.com/pborman/uuid" 12 | ) 13 | 14 | type Config struct { 15 | UUID string `long:"uuid" description:"UUID used for mDNS hostname and service instance"` 16 | MDNSInstance string `long:"mdns_instance" description:"mDNS instance name (default is uuid)"` 17 | MDNSService string `long:"mdns_service" description:"mDNS service name (default '_scriptrock_etcd._tcp')"` 18 | MDNSDomain string `long:"mdns_domain" description:"mDNS domain (default 'local')"` 19 | PollInterval time.Duration 20 | PollIntervalSetter func(int) `long:"poll_interval" description:"polling interval when trying to find peers (default 1s)"` 21 | MaxLoops int `long:"max_loops" description:"maximum number of loops to poll before writing etcd conf (default 10)"` 22 | AvahiConfPath string `long:"avahi_conf_path" description:"where to write avahi service definition to (default /etc/avahi/services/etcd.service)"` 23 | Debug bool `long:"debug" description:"Debug mode"` 24 | } 25 | 26 | var ClusterInstanceUUIDPath string = "/etc/machine-id" 27 | 28 | func DuplicateClusterInstanceUUID() { 29 | fmt.Printf("Duplicate cluster instance UUID encountered; must delete: '%s' and reboot\n", ClusterInstanceUUIDPath) 30 | if err := os.Remove(ClusterInstanceUUIDPath); err != nil { 31 | fmt.Printf("Could not delete '%s': %s\n", ClusterInstanceUUIDPath, err.Error()) 32 | } 33 | } 34 | 35 | func uuidValid(u string) bool { 36 | if len(u) == 32 { 37 | u = u[0:8] + "-" + u[8:12] + "-" + u[12:16] + "-" + u[16:20] + "-" + u[20:] 38 | } 39 | if uuid.Parse(u) != nil { 40 | return true 41 | } 42 | return false 43 | } 44 | 45 | func LoadClusterInstanceUUID() string { 46 | clusterUUID := "" 47 | if fileData, err := ioutil.ReadFile(ClusterInstanceUUIDPath); err != nil { 48 | clusterUUID = uuid.New() 49 | if err := ioutil.WriteFile(ClusterInstanceUUIDPath, []byte(clusterUUID), 0644); err != nil { 50 | fmt.Printf("Could not open '%s' for writing: %s\n", ClusterInstanceUUIDPath, err.Error()) 51 | } 52 | } else { 53 | clusterUUID = strings.TrimSpace(string(fileData)) 54 | if !uuidValid(clusterUUID) { 55 | fmt.Printf("UUID from file '%s' is invalid\n", ClusterInstanceUUIDPath) 56 | clusterUUID = uuid.New() 57 | clusterUUID = strings.Replace(clusterUUID, "-", "", -1) 58 | // write out a new valid one 59 | if err := ioutil.WriteFile(ClusterInstanceUUIDPath, []byte(clusterUUID), 0644); err != nil { 60 | fmt.Printf("Could not open '%s' for writing: %s\n", ClusterInstanceUUIDPath, err.Error()) 61 | } 62 | } 63 | } 64 | fmt.Printf("Cluster Instance UUID (machine-id): %s\n", clusterUUID) 65 | return clusterUUID 66 | } 67 | 68 | func (c *Config) load(argsin []string) ([]string, error) { 69 | c.UUID = LoadClusterInstanceUUID() 70 | c.MDNSInstance = c.UUID 71 | c.MDNSService = "_scriptrock_etcd._tcp" 72 | c.MDNSDomain = "local" 73 | c.PollInterval = 1 * time.Second 74 | c.MaxLoops = 10 75 | c.AvahiConfPath = "/etc/avahi/services/etcd.service" 76 | c.Debug = false 77 | 78 | c.PollIntervalSetter = func(i int) { 79 | c.PollInterval = time.Duration(i) * time.Second 80 | } 81 | return go_flags.NewParser(c, go_flags.IgnoreUnknown).ParseArgs(argsin) 82 | } 83 | 84 | func NewConfig(argsin []string) (*Config, []string, error) { 85 | c := new(Config) 86 | argsout, err := c.load(argsin) 87 | return c, argsout, err 88 | } 89 | -------------------------------------------------------------------------------- /common/etcd.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | go_flags "github.com/jessevdk/go-flags" 6 | "io/ioutil" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | type EtcdPeer struct { 12 | Interface *net.Interface 13 | LocalIP net.IP 14 | PeerIP net.IP 15 | PeerPort int 16 | } 17 | 18 | type EtcdConfig struct { 19 | Name string `long:"etcd_name" description:"etcd machine name, must be unique within cluster. Default is UUID"` 20 | ConfPath string `long:"etcd_conf" description:"etcd conf path (default /etc/etcd/etcd.conf)"` 21 | ClientAddr string `long:"etcd_client_addr" description:"etcd client address (default from $private_ipv4, or peers)"` 22 | ClientBindAddr string `long:"etcd_client_bind_addr" description:"etcd client bind address (default 0.0.0.0)"` 23 | ClientPort int `long:"etcd_client_port" description:"etcd client port (default 4001)"` 24 | PeerAddr string `long:"etcd_peer_addr" description:"etcd peer address (default 0.0.0.0)"` 25 | PeerBindAddr string `long:"etcd_peer_bind_addr" description:"etcd peer bind address (default 0.0.0.0)"` 26 | PeerPort int `long:"etcd_peer_port" description:"etcd peer port (default 7001)"` 27 | DiscoveryURL string `long:"etcd_discovery_url" description:"etcd peer discovery url"` 28 | Peers []string // found through mDNS etc 29 | ServerPeers map[string]EtcdPeer 30 | BootingPeers map[string]EtcdPeer 31 | AddrSource string `long:"addr_from" description:"where to obtain addr & peer_addr from. Options: private_ipv4, public_ipv4, or heuristics"` 32 | Interface *net.Interface 33 | } 34 | 35 | func (c *EtcdConfig) load(argsin []string, name string) ([]string, error) { 36 | // Set some defaults 37 | c.Name = name 38 | c.ConfPath = "/etc/etcd/etcd.conf" 39 | c.ClientAddr = "" // there is much more complex logic around this elsewhere 40 | c.ClientPort = 4001 41 | c.PeerAddr = "" 42 | c.ClientBindAddr = "0.0.0.0" 43 | c.PeerBindAddr = "0.0.0.0" 44 | c.PeerPort = 7001 45 | c.DiscoveryURL = "" 46 | c.Peers = make([]string, 0) 47 | c.ServerPeers = make(map[string]EtcdPeer) 48 | c.BootingPeers = make(map[string]EtcdPeer) 49 | c.AddrSource = "/etc/private_ipv4" 50 | 51 | // TODO FIXME one day, load some from an incoming config file 52 | // TODO FIXME one day, override defaults and incoming conf with environment variables 53 | 54 | // override defaults, incoming conf, env vars with command line arguments 55 | argsout, err := go_flags.NewParser(c, go_flags.IgnoreUnknown).ParseArgs(argsin) 56 | 57 | // Warning: go_flags parser will set pointer types to not nil.... bad. 58 | c.Interface = nil 59 | 60 | // load client address from /etc/ if present 61 | if true && c.ClientAddr == "" { 62 | if useAddr, err := ioutil.ReadFile(c.AddrSource); err == nil { 63 | if ip := net.ParseIP(strings.TrimSpace(string(useAddr))); ip == nil { 64 | fmt.Printf("Invalid IP address found in '%s'\n", c.AddrSource) 65 | } else { 66 | c.verifyIP(ip, c.AddrSource) 67 | } 68 | } 69 | } 70 | 71 | return argsout, err 72 | } 73 | 74 | func (c *EtcdConfig) AddServerPeer(iface *net.Interface, localIP net.IP, peerIP net.IP, peerPort int) { 75 | c.ServerPeers[peerIP.String()] = EtcdPeer{ 76 | Interface: iface, 77 | LocalIP: localIP, 78 | PeerIP: peerIP, 79 | PeerPort: peerPort, 80 | } 81 | } 82 | 83 | func (c *EtcdConfig) AddBootingPeer(iface *net.Interface, localIP net.IP, peerIP net.IP, peerPort int) { 84 | c.BootingPeers[peerIP.String()] = EtcdPeer{ 85 | Interface: iface, 86 | LocalIP: localIP, 87 | PeerIP: peerIP, 88 | PeerPort: peerPort, 89 | } 90 | } 91 | 92 | func (c *EtcdConfig) verifyIP(ip net.IP, source string) { 93 | fmt.Printf("IP '%s' found in '%s'\n", ip.String(), source) 94 | // valid ip address found from source. Verify that it exists 95 | if iface, ip_net, ipverify, err := LocalNetForIp(ip); err != nil { 96 | fmt.Printf("%s\n", err.Error()) 97 | } else if !ip.Equal(ipverify) { 98 | fmt.Printf("IP address on interface %s: %s does not match ip from %s: %s\n", 99 | iface.Name, ipverify, c.AddrSource, ip) 100 | } else { 101 | fmt.Printf("IP '%s' verified, on interface '%s' net '%s'\n", 102 | ip.String(), iface.Name, ip_net.String()) 103 | set := false 104 | // set local defaults appropriately 105 | if c.ClientAddr == "" { 106 | c.ClientAddr = ip.String() 107 | set = true 108 | } 109 | if c.PeerAddr == "" { 110 | c.PeerAddr = ip.String() 111 | set = true 112 | } 113 | if set { 114 | c.Interface = iface 115 | } 116 | } 117 | } 118 | 119 | func IsIPv4(ip net.IP) bool { 120 | return ip.DefaultMask() != nil 121 | } 122 | 123 | func (c *EtcdConfig) setupAddresses() { 124 | // Now that all load sources have been tested; set up local addresses for etcd config 125 | 126 | // If a peer was found, use our local address based on that 127 | if c.ClientAddr == "" { 128 | for _, v := range c.ServerPeers { 129 | c.ClientAddr = v.LocalIP.String() 130 | fmt.Printf("setupAddresses: heuristic client address from server peer: %s\n", c.ClientAddr) 131 | break 132 | } 133 | } 134 | 135 | // Otherwise use booting peer; we may be the first to boot 136 | if c.ClientAddr == "" { 137 | for _, v := range c.BootingPeers { 138 | c.ClientAddr = v.LocalIP.String() 139 | fmt.Printf("setupAddresses: heuristic client address from booting peer: %s\n", c.ClientAddr) 140 | break 141 | } 142 | } 143 | 144 | // Last resort; iterate over our interfaces and use the last non-virtual one (linux specific) 145 | if c.ClientAddr == "" { 146 | var lastIP net.IP = nil 147 | if ifaces, err := net.Interfaces(); err != nil { 148 | fmt.Printf("setupAddresses: Error getting network interfaces: '%s'\n", err.Error()) 149 | } else { 150 | for _, iface := range ifaces { 151 | if (iface.Flags & net.FlagLoopback) != 0 { 152 | continue 153 | } 154 | if (iface.Flags & net.FlagUp) == 0 { 155 | continue 156 | } 157 | if (iface.Flags & net.FlagMulticast) == 0 { 158 | continue 159 | } 160 | if InterfaceIsVirtual(&iface) { 161 | continue 162 | } 163 | 164 | if iface_addrs, err := iface.Addrs(); err != nil { 165 | fmt.Errorf("setupAddresses: Error getting interface addresses: %s\n", err.Error()) 166 | } else { 167 | for _, iface_addr := range iface_addrs { 168 | ipstr := iface_addr.String() 169 | ip, _, err := net.ParseCIDR(ipstr) 170 | if err != nil { 171 | fmt.Printf("Error parsing local address '%s': %s\n", 172 | ipstr, err.Error()) 173 | } else { 174 | // Use an IPv4 address only 175 | if IsIPv4(ip) { 176 | lastIP = ip 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } 183 | if lastIP != nil { 184 | c.ClientAddr = lastIP.String() 185 | fmt.Printf("setupAddresses: heuristic client address from last network interface: %s\n", c.ClientAddr) 186 | } else { 187 | c.ClientAddr = "127.0.0.1" 188 | fmt.Printf("setupAddresses: cannot find any valid addresses; using loopback interface: %s\n", c.ClientAddr) 189 | } 190 | } 191 | 192 | if c.PeerAddr == "" { 193 | c.PeerAddr = c.ClientAddr 194 | } 195 | } 196 | 197 | func NewEtcdConfig(argsin []string, name string) (*EtcdConfig, []string, error) { 198 | c := new(EtcdConfig) 199 | argsout, err := c.load(argsin, name) 200 | return c, argsout, err 201 | } 202 | 203 | func (cfg *EtcdConfig) WriteFile() { 204 | cfg.setupAddresses() 205 | 206 | peers := make([]string, 0) 207 | if cfg.DiscoveryURL == "" { 208 | for k, _ := range cfg.ServerPeers { 209 | peers = append(peers, fmt.Sprintf("\"%s:%d\"", k, cfg.PeerPort)) 210 | } 211 | } 212 | 213 | // wrap each peer in quotes 214 | conf := fmt.Sprintf( 215 | ` 216 | # 217 | # Generated by ScriptRock Config init 218 | # 219 | name = "%s" 220 | addr = "%s:%d" 221 | bind_addr = "%s:%d" 222 | #ca_file = "" 223 | #cert_file = "" 224 | #cors = [] 225 | #cpu_profile_file = "" 226 | #data_dir = "." 227 | discovery = "%s" 228 | #http_read_timeout = 10.0 229 | #http_write_timeout = 10.0 230 | #key_file = "" 231 | peers = [%s] 232 | #peers_file = "" 233 | #max_cluster_size = 9 234 | #max_result_buffer = 1024 235 | #max_retry_attempts = 3 236 | #snapshot = true 237 | #verbose = false 238 | #very_verbose = false 239 | # 240 | [peer] 241 | addr = "%s:%d" 242 | bind_addr = "%s:%d" 243 | #ca_file = "" 244 | #cert_file = "" 245 | #key_file = "" 246 | # 247 | #[cluster] 248 | #active_size = 9 249 | #remove_delay = 1800.0 250 | #sync_interval = 5.0 251 | # 252 | `, 253 | cfg.Name, // name 254 | cfg.ClientAddr, cfg.ClientPort, // addr 255 | cfg.ClientBindAddr, cfg.ClientPort, // bind_addr 256 | cfg.DiscoveryURL, // discovery 257 | strings.Join(peers, ","), // peers 258 | cfg.PeerAddr, cfg.PeerPort, // peer_addr 259 | cfg.PeerBindAddr, cfg.PeerPort) // peer_bind_addr 260 | 261 | fmt.Printf("Writing etcd conf file to '%s'\n", cfg.ConfPath) 262 | if err := ioutil.WriteFile(cfg.ConfPath, []byte(conf), 0644); err != nil { 263 | fmt.Printf("Could not write conf file '%s': %s\n", cfg.ConfPath, err.Error()) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /common/fleet.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | go_flags "github.com/jessevdk/go-flags" 6 | "io/ioutil" 7 | ) 8 | 9 | type FleetConfig struct { 10 | ConfPath string `long:"fleet_conf" description:"fleet conf path (default /etc/fleet/fleet.conf)"` 11 | } 12 | 13 | func (c *FleetConfig) load(argsin []string) ([]string, error) { 14 | // Set some defaults 15 | c.ConfPath = "/etc/fleet/fleet.conf" 16 | 17 | // override defaults, incoming conf, env vars with command line arguments 18 | argsout, err := go_flags.NewParser(c, go_flags.IgnoreUnknown).ParseArgs(argsin) 19 | 20 | return argsout, err 21 | } 22 | 23 | func NewFleetConfig(argsin []string) (*FleetConfig, []string, error) { 24 | c := new(FleetConfig) 25 | argsout, err := c.load(argsin) 26 | return c, argsout, err 27 | } 28 | 29 | func (cfg *FleetConfig) WriteFile(etcd *EtcdConfig) { 30 | 31 | // wrap each peer in quotes 32 | conf := fmt.Sprintf( 33 | ` 34 | # 35 | # Generated by ScriptRock Config init 36 | # 37 | public_ip = "%s" 38 | 39 | `, 40 | etcd.ClientAddr) // public_ip 41 | 42 | fmt.Printf("Writing fleet conf file to '%s'\n", cfg.ConfPath) 43 | if err := ioutil.WriteFile(cfg.ConfPath, []byte(conf), 0644); err != nil { 44 | fmt.Printf("Could not write conf file '%s': %s\n", cfg.ConfPath, err.Error()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scriptrock_etcd_conf/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/ScriptRock/peerdiscovery/client" 4 | 5 | func main() { 6 | client.Client() 7 | } 8 | --------------------------------------------------------------------------------