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

This commit is contained in:
Jakob Borg 2014-09-23 16:04:20 +02:00
parent 24e5000c37
commit e596a45e9f
9 changed files with 133 additions and 17 deletions

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -260,6 +260,11 @@
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
</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]">
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{connections[nodeCfg.NodeID].ClientVersion}}</td>
@ -388,6 +393,15 @@
<label>
<input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
</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>
</form>

View File

@ -8,10 +8,12 @@
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
"Announce Server": "Announce Server",
"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",
"CPU Utilization": "CPU Utilization",
"Close": "Close",
"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",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
"Delete": "Delete",
@ -42,6 +44,7 @@
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignore Permissions",
"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)",
"Keep Versions": "Keep Versions",
"Last seen": "Last seen",

View File

@ -437,18 +437,18 @@ func (m *Model) repoSharedWith(repo string, nodeID protocol.NodeID) bool {
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()
if config.ClientName == "syncthing" {
m.nodeVer[nodeID] = config.ClientVersion
if cm.ClientName == "syncthing" {
m.nodeVer[nodeID] = cm.ClientVersion
} else {
m.nodeVer[nodeID] = config.ClientName + " " + config.ClientVersion
m.nodeVer[nodeID] = cm.ClientName + " " + cm.ClientVersion
}
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)
node := m.cfg.GetNodeConfiguration(nodeID)
if node != nil && node.Name == "" {
@ -456,6 +456,73 @@ func (m *Model) ClusterConfig(nodeID protocol.NodeID, config protocol.ClusterCon
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.
@ -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[:]
node := node
// TODO: Set read only bit when relevant
cr.Nodes = append(cr.Nodes, protocol.Node{
cn := protocol.Node{
ID: node[:],
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)
}

View File

@ -306,7 +306,8 @@ func TestClusterConfig(t *testing.T) {
cfg := config.New("/tmp/test", node1)
cfg.Nodes = []config.NodeConfiguration{
{
NodeID: node1,
NodeID: node1,
Introducer: true,
},
{
NodeID: node2,
@ -351,9 +352,15 @@ func TestClusterConfig(t *testing.T) {
if id := r.Nodes[0].ID; bytes.Compare(id, node1[:]) != 0 {
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 {
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]
if r.ID != "repo2" {
@ -365,9 +372,15 @@ func TestClusterConfig(t *testing.T) {
if id := r.Nodes[0].ID; bytes.Compare(id, node1[:]) != 0 {
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 {
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) {

View File

@ -249,7 +249,7 @@ The Node Flags field contains the following single bit flags:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |Pri| Reserved |R|T|
| Reserved |Pri| Reserved |I|R|T|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 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
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 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.
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
version number of the files already known to be in the index sent by

View File

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