Procházet zdrojové kódy

Add "cluster introducer" functionality to nodes (ref #120)

Jakob Borg před 11 roky
rodič
revize
e596a45e9f
9 změnil soubory, kde provedl 130 přidání a 14 odebrání
  1. 0 0
      auto/gui.files.go
  2. 12 2
      config/config.go
  3. 2 1
      gui/app.js
  4. 14 0
      gui/index.html
  5. 3 0
      gui/lang/lang-en.json
  6. 79 8
      model/model.go
  7. 14 1
      model/model_test.go
  8. 5 2
      protocol/PROTOCOL.md
  9. 1 0
      protocol/protocol.go

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
auto/gui.files.go


+ 12 - 2
config/config.go

@@ -101,6 +101,7 @@ type NodeConfiguration struct {
 	Addresses   []string        `xml:"address,omitempty"`
 	Addresses   []string        `xml:"address,omitempty"`
 	Compression bool            `xml:"compression,attr"`
 	Compression bool            `xml:"compression,attr"`
 	CertName    string          `xml:"certName,attr,omitempty"`
 	CertName    string          `xml:"certName,attr,omitempty"`
+	Introducer  bool            `xml:"introducer,attr"`
 }
 }
 
 
 type RepositoryNodeConfiguration struct {
 type RepositoryNodeConfiguration struct {
@@ -153,15 +154,24 @@ func (cfg *Configuration) NodeMap() map[protocol.NodeID]NodeConfiguration {
 	return m
 	return m
 }
 }
 
 
-func (cfg *Configuration) GetNodeConfiguration(nodeid protocol.NodeID) *NodeConfiguration {
+func (cfg *Configuration) GetNodeConfiguration(nodeID protocol.NodeID) *NodeConfiguration {
 	for i, node := range cfg.Nodes {
 	for i, node := range cfg.Nodes {
-		if node.NodeID == nodeid {
+		if node.NodeID == nodeID {
 			return &cfg.Nodes[i]
 			return &cfg.Nodes[i]
 		}
 		}
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
+func (cfg *Configuration) GetRepoConfiguration(repoID string) *RepositoryConfiguration {
+	for i, repo := range cfg.Repositories {
+		if repo.ID == repoID {
+			return &cfg.Repositories[i]
+		}
+	}
+	return nil
+}
+
 func (cfg *Configuration) RepoMap() map[string]RepositoryConfiguration {
 func (cfg *Configuration) RepoMap() map[string]RepositoryConfiguration {
 	m := make(map[string]RepositoryConfiguration, len(cfg.Repositories))
 	m := make(map[string]RepositoryConfiguration, len(cfg.Repositories))
 	for _, r := range cfg.Repositories {
 	for _, r := range cfg.Repositories {

+ 2 - 1
gui/app.js

@@ -666,7 +666,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
     $scope.addNode = function () {
     $scope.addNode = function () {
         $scope.currentNode = {
         $scope.currentNode = {
             AddressesStr: 'dynamic',
             AddressesStr: 'dynamic',
-            Compression: true
+            Compression: true,
+            Introducer: true
         };
         };
         $scope.editingExisting = false;
         $scope.editingExisting = false;
         $scope.editingSelf = false;
         $scope.editingSelf = false;

+ 14 - 0
gui/index.html

@@ -260,6 +260,11 @@
                       <td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
                       <td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
                       <td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
                       <td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
                     </tr>
                     </tr>
+                    <tr>
+                      <th><span class="glyphicon glyphicon-thumbs-up"></span>&emsp;<span translate>Introducer</span></th>
+                      <td translate ng-if="nodeCfg.Introducer" class="text-right">Yes</td>
+                      <td translate ng-if="!nodeCfg.Introducer" class="text-right">No</td>
+                    </tr>
                     <tr ng-if="connections[nodeCfg.NodeID]">
                     <tr ng-if="connections[nodeCfg.NodeID]">
                       <th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
                       <th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
                       <td class="text-right">{{connections[nodeCfg.NodeID].ClientVersion}}</td>
                       <td class="text-right">{{connections[nodeCfg.NodeID].ClientVersion}}</td>
@@ -388,6 +393,15 @@
                 <label>
                 <label>
                   <input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
                   <input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
                 </label>
                 </label>
+                <p translate class="help-block">Compression is recommended in most setups.</p>
+              </div>
+            </div>
+            <div ng-if="!editingSelf" class="form-group">
+              <div class="checkbox">
+                <label>
+                  <input type="checkbox" ng-model="currentNode.Introducer"> <span translate>Introducer</span>
+                </label>
+                <p translate class="help-block">Any nodes configured on an introducer node will be added to this node as well.</p>
               </div>
               </div>
             </div>
             </div>
           </form>
           </form>

+ 3 - 0
gui/lang/lang-en.json

@@ -8,10 +8,12 @@
    "Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
    "Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
    "Announce Server": "Announce Server",
    "Announce Server": "Announce Server",
    "Anonymous Usage Reporting": "Anonymous Usage Reporting",
    "Anonymous Usage Reporting": "Anonymous Usage Reporting",
+   "Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
    "Bugs": "Bugs",
    "Bugs": "Bugs",
    "CPU Utilization": "CPU Utilization",
    "CPU Utilization": "CPU Utilization",
    "Close": "Close",
    "Close": "Close",
    "Comment, when used at the start of a line": "Comment, when used at the start of a line",
    "Comment, when used at the start of a line": "Comment, when used at the start of a line",
+   "Compression is recommended in most setups.": "Compression is recommended in most setups.",
    "Connection Error": "Connection Error",
    "Connection Error": "Connection Error",
    "Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
    "Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
    "Delete": "Delete",
    "Delete": "Delete",
@@ -42,6 +44,7 @@
    "Ignore Patterns": "Ignore Patterns",
    "Ignore Patterns": "Ignore Patterns",
    "Ignore Permissions": "Ignore Permissions",
    "Ignore Permissions": "Ignore Permissions",
    "Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
    "Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
+   "Introducer": "Introducer",
    "Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
    "Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
    "Keep Versions": "Keep Versions",
    "Keep Versions": "Keep Versions",
    "Last seen": "Last seen",
    "Last seen": "Last seen",

+ 79 - 8
model/model.go

@@ -437,18 +437,18 @@ func (m *Model) repoSharedWith(repo string, nodeID protocol.NodeID) bool {
 	return false
 	return false
 }
 }
 
 
-func (m *Model) ClusterConfig(nodeID protocol.NodeID, config protocol.ClusterConfigMessage) {
+func (m *Model) ClusterConfig(nodeID protocol.NodeID, cm protocol.ClusterConfigMessage) {
 	m.pmut.Lock()
 	m.pmut.Lock()
-	if config.ClientName == "syncthing" {
-		m.nodeVer[nodeID] = config.ClientVersion
+	if cm.ClientName == "syncthing" {
+		m.nodeVer[nodeID] = cm.ClientVersion
 	} else {
 	} else {
-		m.nodeVer[nodeID] = config.ClientName + " " + config.ClientVersion
+		m.nodeVer[nodeID] = cm.ClientName + " " + cm.ClientVersion
 	}
 	}
 	m.pmut.Unlock()
 	m.pmut.Unlock()
 
 
-	l.Infof(`Node %s client is "%s %s"`, nodeID, config.ClientName, config.ClientVersion)
+	l.Infof(`Node %s client is "%s %s"`, nodeID, cm.ClientName, cm.ClientVersion)
 
 
-	if name := config.GetOption("name"); name != "" {
+	if name := cm.GetOption("name"); name != "" {
 		l.Infof("Node %s hostname is %q", nodeID, name)
 		l.Infof("Node %s hostname is %q", nodeID, name)
 		node := m.cfg.GetNodeConfiguration(nodeID)
 		node := m.cfg.GetNodeConfiguration(nodeID)
 		if node != nil && node.Name == "" {
 		if node != nil && node.Name == "" {
@@ -456,6 +456,73 @@ func (m *Model) ClusterConfig(nodeID protocol.NodeID, config protocol.ClusterCon
 			m.cfg.Save()
 			m.cfg.Save()
 		}
 		}
 	}
 	}
+
+	if m.cfg.GetNodeConfiguration(nodeID).Introducer {
+		// This node is an introducer. Go through the announced lists of repos
+		// and nodes and add what we are missing.
+
+		var changed bool
+		for _, repo := range cm.Repositories {
+			// If we don't have this repository yet, skip it. Ideally, we'd
+			// offer up something in the GUI to create the repo, but for the
+			// moment we only handle repos that we already have.
+			if _, ok := m.repoNodes[repo.ID]; !ok {
+				continue
+			}
+
+		nextNode:
+			for _, node := range repo.Nodes {
+				var id protocol.NodeID
+				copy(id[:], node.ID)
+
+				if _, ok := m.nodeRepos[id]; !ok {
+					// The node is currently unknown. Add it to the config.
+
+					l.Infof("Adding node %v to config (vouched for by introducer %v)", id, nodeID)
+					newNodeCfg := config.NodeConfiguration{
+						NodeID: id,
+					}
+
+					// The introducers' introducers are also our introducers.
+					if node.Flags&protocol.FlagIntroducer != 0 {
+						l.Infof("Node %v is now also an introducer", id)
+						newNodeCfg.Introducer = true
+					}
+
+					m.cfg.Nodes = append(m.cfg.Nodes, newNodeCfg)
+
+					changed = true
+				}
+
+				for _, er := range m.nodeRepos[id] {
+					if er == repo.ID {
+						// We already share the repo with this node, so
+						// nothing to do.
+						continue nextNode
+					}
+				}
+
+				// We don't yet share this repo with this node. Add the node
+				// to sharing list of the repo.
+
+				l.Infof("Adding node %v to share %q (vouched for by introducer %v)", id, repo.ID, nodeID)
+
+				m.nodeRepos[id] = append(m.nodeRepos[id], repo.ID)
+				m.repoNodes[repo.ID] = append(m.repoNodes[repo.ID], id)
+
+				repoCfg := m.cfg.GetRepoConfiguration(repo.ID)
+				repoCfg.Nodes = append(repoCfg.Nodes, config.RepositoryNodeConfiguration{
+					NodeID: id,
+				})
+
+				changed = true
+			}
+		}
+
+		if changed {
+			m.cfg.Save()
+		}
+	}
 }
 }
 
 
 // Close removes the peer from the model and closes the underlying connection if possible.
 // Close removes the peer from the model and closes the underlying connection if possible.
@@ -1030,10 +1097,14 @@ func (m *Model) clusterConfig(node protocol.NodeID) protocol.ClusterConfigMessag
 			// so we don't grab aliases to the same array later on in node[:]
 			// so we don't grab aliases to the same array later on in node[:]
 			node := node
 			node := node
 			// TODO: Set read only bit when relevant
 			// TODO: Set read only bit when relevant
-			cr.Nodes = append(cr.Nodes, protocol.Node{
+			cn := protocol.Node{
 				ID:    node[:],
 				ID:    node[:],
 				Flags: protocol.FlagShareTrusted,
 				Flags: protocol.FlagShareTrusted,
-			})
+			}
+			if nodeCfg := m.cfg.GetNodeConfiguration(node); nodeCfg.Introducer {
+				cn.Flags |= protocol.FlagIntroducer
+			}
+			cr.Nodes = append(cr.Nodes, cn)
 		}
 		}
 		cm.Repositories = append(cm.Repositories, cr)
 		cm.Repositories = append(cm.Repositories, cr)
 	}
 	}

+ 14 - 1
model/model_test.go

@@ -306,7 +306,8 @@ func TestClusterConfig(t *testing.T) {
 	cfg := config.New("/tmp/test", node1)
 	cfg := config.New("/tmp/test", node1)
 	cfg.Nodes = []config.NodeConfiguration{
 	cfg.Nodes = []config.NodeConfiguration{
 		{
 		{
-			NodeID: node1,
+			NodeID:     node1,
+			Introducer: true,
 		},
 		},
 		{
 		{
 			NodeID: node2,
 			NodeID: node2,
@@ -351,9 +352,15 @@ func TestClusterConfig(t *testing.T) {
 	if id := r.Nodes[0].ID; bytes.Compare(id, node1[:]) != 0 {
 	if id := r.Nodes[0].ID; bytes.Compare(id, node1[:]) != 0 {
 		t.Errorf("Incorrect node ID %x != %x", id, node1)
 		t.Errorf("Incorrect node ID %x != %x", id, node1)
 	}
 	}
+	if r.Nodes[0].Flags&protocol.FlagIntroducer == 0 {
+		t.Error("Node1 should be flagged as Introducer")
+	}
 	if id := r.Nodes[1].ID; bytes.Compare(id, node2[:]) != 0 {
 	if id := r.Nodes[1].ID; bytes.Compare(id, node2[:]) != 0 {
 		t.Errorf("Incorrect node ID %x != %x", id, node2)
 		t.Errorf("Incorrect node ID %x != %x", id, node2)
 	}
 	}
+	if r.Nodes[1].Flags&protocol.FlagIntroducer != 0 {
+		t.Error("Node2 should not be flagged as Introducer")
+	}
 
 
 	r = cm.Repositories[1]
 	r = cm.Repositories[1]
 	if r.ID != "repo2" {
 	if r.ID != "repo2" {
@@ -365,9 +372,15 @@ func TestClusterConfig(t *testing.T) {
 	if id := r.Nodes[0].ID; bytes.Compare(id, node1[:]) != 0 {
 	if id := r.Nodes[0].ID; bytes.Compare(id, node1[:]) != 0 {
 		t.Errorf("Incorrect node ID %x != %x", id, node1)
 		t.Errorf("Incorrect node ID %x != %x", id, node1)
 	}
 	}
+	if r.Nodes[0].Flags&protocol.FlagIntroducer == 0 {
+		t.Error("Node1 should be flagged as Introducer")
+	}
 	if id := r.Nodes[1].ID; bytes.Compare(id, node2[:]) != 0 {
 	if id := r.Nodes[1].ID; bytes.Compare(id, node2[:]) != 0 {
 		t.Errorf("Incorrect node ID %x != %x", id, node2)
 		t.Errorf("Incorrect node ID %x != %x", id, node2)
 	}
 	}
+	if r.Nodes[1].Flags&protocol.FlagIntroducer != 0 {
+		t.Error("Node2 should not be flagged as Introducer")
+	}
 }
 }
 
 
 func TestIgnores(t *testing.T) {
 func TestIgnores(t *testing.T) {

+ 5 - 2
protocol/PROTOCOL.md

@@ -249,7 +249,7 @@ The Node Flags field contains the following single bit flags:
      0                   1                   2                   3
      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-    |          Reserved         |Pri|          Reserved         |R|T|
+    |          Reserved         |Pri|          Reserved       |I|R|T|
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 
 
  - Bit 31 ("T", Trusted) is set for nodes that participate in trusted
  - Bit 31 ("T", Trusted) is set for nodes that participate in trusted
@@ -258,6 +258,9 @@ The Node Flags field contains the following single bit flags:
  - Bit 30 ("R", Read Only) is set for nodes that participate in read
  - Bit 30 ("R", Read Only) is set for nodes that participate in read
    only mode.
    only mode.
 
 
+ - Bit 29 ("I", Introducer) is set for nodes that are trusted as cluster
+   introducers.
+
  - Bits 16 through 28 are reserved and MUST be set to zero.
  - Bits 16 through 28 are reserved and MUST be set to zero.
 
 
  - Bits 14-15 ("Pri) indicate the node's upload priority for this
  - Bits 14-15 ("Pri) indicate the node's upload priority for this
@@ -276,7 +279,7 @@ The Node Flags field contains the following single bit flags:
 
 
  - Bits 0 through 14 are reserved and MUST be set to zero.
  - Bits 0 through 14 are reserved and MUST be set to zero.
 
 
-Exactly one of the T, R or S bits MUST be set.
+Exactly one of the T and R bits MUST be set.
 
 
 The per node Max Local Version field contains the highest local file
 The per node Max Local Version field contains the highest local file
 version number of the files already known to be in the index sent by
 version number of the files already known to be in the index sent by

+ 1 - 0
protocol/protocol.go

@@ -47,6 +47,7 @@ const (
 const (
 const (
 	FlagShareTrusted  uint32 = 1 << 0
 	FlagShareTrusted  uint32 = 1 << 0
 	FlagShareReadOnly        = 1 << 1
 	FlagShareReadOnly        = 1 << 1
+	FlagIntroducer           = 1 << 2
 	FlagShareBits            = 0x000000ff
 	FlagShareBits            = 0x000000ff
 )
 )
 
 

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů