Browse Source

Add support for per-relay network directories

Steven Engler 4 years ago
parent
commit
66e1adc6f3
1 changed files with 121 additions and 82 deletions
  1. 121 82
      lib/chutney/TorNet.py

+ 121 - 82
lib/chutney/TorNet.py

@@ -664,7 +664,7 @@ class LocalNodeBuilder(NodeBuilder):
     def _makeDataDir(self):
     def _makeDataDir(self):
         """Create the data directory (with keys subdirectory) for this node.
         """Create the data directory (with keys subdirectory) for this node.
         """
         """
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         make_datadir_subdirectory(datadir, "keys")
         make_datadir_subdirectory(datadir, "keys")
 
 
     def _makeHiddenServiceDir(self):
     def _makeHiddenServiceDir(self):
@@ -674,13 +674,13 @@ class LocalNodeBuilder(NodeBuilder):
           key. It is combined with the 'dir' data directory key to yield the
           key. It is combined with the 'dir' data directory key to yield the
           path to the hidden service directory.
           path to the hidden service directory.
         """
         """
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         make_datadir_subdirectory(datadir, self._env['hs_directory'])
         make_datadir_subdirectory(datadir, self._env['hs_directory'])
 
 
     def _genAuthorityKey(self):
     def _genAuthorityKey(self):
         """Generate an authority identity and signing key for this authority,
         """Generate an authority identity and signing key for this authority,
            if they do not already exist."""
            if they do not already exist."""
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         tor_gencert = self._env['tor_gencert']
         tor_gencert = self._env['tor_gencert']
         lifetime = self._env['auth_cert_lifetime']
         lifetime = self._env['auth_cert_lifetime']
         idfile = os.path.join(datadir, 'keys', "authority_identity_key")
         idfile = os.path.join(datadir, 'keys', "authority_identity_key")
@@ -711,7 +711,7 @@ class LocalNodeBuilder(NodeBuilder):
         """Generate an identity key for this router, unless we already have,
         """Generate an identity key for this router, unless we already have,
            and set up the 'fingerprint' entry in the Environ.
            and set up the 'fingerprint' entry in the Environ.
         """
         """
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         tor = self._env['tor']
         tor = self._env['tor']
         torrc = self._getTorrcFname()
         torrc = self._getTorrcFname()
         cmdline = [
         cmdline = [
@@ -737,7 +737,7 @@ class LocalNodeBuilder(NodeBuilder):
         if not self._env['authority']:
         if not self._env['authority']:
             return ""
             return ""
 
 
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         certfile = os.path.join(datadir, 'keys', "authority_certificate")
         certfile = os.path.join(datadir, 'keys', "authority_certificate")
         v3id = None
         v3id = None
         with open(certfile, 'r') as f:
         with open(certfile, 'r') as f:
@@ -812,56 +812,58 @@ class LocalNodeBuilder(NodeBuilder):
         return bridgelines
         return bridgelines
 
 
 
 
-def scp_file(abs_filepath, host):
-    if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
-        # this check for '/tmp' is in no way secure, but helps prevent me from shooting
-        # myself in the foot
-        raise Exception('SCP path must be absolute and must be in /tmp')
-    assert(':' not in host)
-    assert(':' not in abs_filepath)
-    remote_filepath = os.path.dirname(abs_filepath)
-    cmd = ['scp', abs_filepath, ':'.join([host, remote_filepath])]
-    print('Transferring file: {}'.format(cmd))
-    subprocess.check_output(cmd, stderr=subprocess.STDOUT)
-
-def scp_dir(abs_dirpath, host):
-    if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
-        # this check for '/tmp' is in no way secure, but helps prevent me from shooting
-        # myself in the foot
-        raise Exception('SCP path must be absolute and must be in /tmp')
-    assert(':' not in host)
-    assert(':' not in abs_dirpath)
-    remote_dirpath = os.path.dirname(abs_dirpath)
+#def scp_file(abs_filepath, host):
+#    if not os.path.isabs(abs_filepath) or abs_filepath[0:5] != '/tmp/':
+#        # this check for '/tmp' is in no way secure, but helps prevent me from shooting
+#        # myself in the foot
+#        raise Exception('SCP path must be absolute and must be in /tmp')
+#    assert ':' not in host
+#    assert ':' not in abs_filepath
+#    remote_filepath = os.path.dirname(abs_filepath)
+#    cmd = ['scp', abs_filepath, ':'.join([host, remote_filepath])]
+#    print('Transferring file: {}'.format(cmd))
+#    subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+
+def scp_dir(abs_dirpath, abs_remote_dirpath, host):
+    #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
+    #    # this check for '/tmp' is in no way secure, but helps prevent me from shooting
+    #    # myself in the foot
+    #    raise Exception('SCP path must be absolute and must be in /tmp')
+    assert ':' not in host
+    assert ':' not in abs_dirpath
+    assert ':' not in abs_remote_dirpath
+    remote_dirpath = os.path.dirname(abs_remote_dirpath)
     cmd = ['scp', '-r', abs_dirpath, ':'.join([host, remote_dirpath])]
     cmd = ['scp', '-r', abs_dirpath, ':'.join([host, remote_dirpath])]
     print('Transferring files: {}'.format(cmd))
     print('Transferring files: {}'.format(cmd))
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
 
 
-def scp_dir_backwards(abs_dirpath, host):
-    if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
-        # this check for '/tmp' is in no way secure, but helps prevent me from shooting
-        # myself in the foot
-        raise Exception('SCP path must be absolute and must be in /tmp')
-    assert(':' not in host)
-    assert(':' not in abs_dirpath)
-    remote_dirpath = os.path.dirname(abs_dirpath)
-    cmd = ['scp', '-r', ':'.join([host, abs_dirpath]), remote_dirpath]
+def scp_dir_backwards(abs_remote_dirpath, abs_dirpath, host):
+    #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
+    #    # this check for '/tmp' is in no way secure, but helps prevent me from shooting
+    #    # myself in the foot
+    #    raise Exception('SCP path must be absolute and must be in /tmp')
+    assert ':' not in host
+    assert ':' not in abs_dirpath
+    assert ':' not in abs_remote_dirpath
+    local_dirpath = os.path.dirname(abs_dirpath)
+    cmd = ['scp', '-r', ':'.join([host, abs_remote_dirpath]), local_dirpath]
     print('Transferring files backwards: {}'.format(cmd))
     print('Transferring files backwards: {}'.format(cmd))
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
 
 
 def ssh_mkdir_p(abs_dirpath, remote_hostname):
 def ssh_mkdir_p(abs_dirpath, remote_hostname):
-    if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
-        # this check for '/tmp' is in no way secure, but helps prevent me from shooting
-        # myself in the foot
-        raise Exception('Path must be absolute and must be in /tmp')
+    #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
+    #    # this check for '/tmp' is in no way secure, but helps prevent me from shooting
+    #    # myself in the foot
+    #    raise Exception('Path must be absolute and must be in /tmp')
     cmd = ['ssh', remote_hostname, 'mkdir', '-p', abs_dirpath]
     cmd = ['ssh', remote_hostname, 'mkdir', '-p', abs_dirpath]
     print('Making directory: {}'.format(cmd))
     print('Making directory: {}'.format(cmd))
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
 
 
 def ssh_rm_if_exists(abs_dirpath, remote_hostname):
 def ssh_rm_if_exists(abs_dirpath, remote_hostname):
-    if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
-        # this check for '/tmp' is in no way secure, but helps prevent me from shooting
-        # myself in the foot
-        raise Exception('Path must be absolute and must be in /tmp')
+    #if not os.path.isabs(abs_dirpath) or abs_dirpath[0:5] != '/tmp/':
+    #    # this check for '/tmp' is in no way secure, but helps prevent me from shooting
+    #    # myself in the foot
+    #    raise Exception('Path must be absolute and must be in /tmp')
     cmd = ['ssh', remote_hostname, 'rm', '-f', abs_dirpath]
     cmd = ['ssh', remote_hostname, 'rm', '-f', abs_dirpath]
     print('Removing: {}'.format(cmd))
     print('Removing: {}'.format(cmd))
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
     subprocess.check_output(cmd, stderr=subprocess.STDOUT)
@@ -1056,8 +1058,8 @@ class RemoteNodeBuilder(NodeBuilder):
     def postConfig(self, net):
     def postConfig(self, net):
         """Called on each nodes after all nodes configure."""
         """Called on each nodes after all nodes configure."""
         # self.net.addNode(self)
         # self.net.addNode(self)
-        scp_dir(os.path.abspath(self._env['dir']), self._env['remote_hostname'])
-        shutil.rmtree(self._env['dir'])
+        scp_dir(os.path.abspath(self._env['local_dir']), os.path.abspath(self._env['remote_dir']), self._env['remote_hostname'])
+        shutil.rmtree(self._env['local_dir'])
 
 
     def isSupported(self, net):
     def isSupported(self, net):
         """Return true if this node appears to have everything it needs;
         """Return true if this node appears to have everything it needs;
@@ -1077,7 +1079,7 @@ class RemoteNodeBuilder(NodeBuilder):
     def _makeDataDir(self):
     def _makeDataDir(self):
         """Create the data directory (with keys subdirectory) for this node.
         """Create the data directory (with keys subdirectory) for this node.
         """
         """
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         make_datadir_subdirectory(datadir, "keys")
         make_datadir_subdirectory(datadir, "keys")
 
 
     def _makeHiddenServiceDir(self):
     def _makeHiddenServiceDir(self):
@@ -1087,13 +1089,13 @@ class RemoteNodeBuilder(NodeBuilder):
           key. It is combined with the 'dir' data directory key to yield the
           key. It is combined with the 'dir' data directory key to yield the
           path to the hidden service directory.
           path to the hidden service directory.
         """
         """
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         make_datadir_subdirectory(datadir, self._env['hs_directory'])
         make_datadir_subdirectory(datadir, self._env['hs_directory'])
 
 
     def _genAuthorityKey(self):
     def _genAuthorityKey(self):
         """Generate an authority identity and signing key for this authority,
         """Generate an authority identity and signing key for this authority,
            if they do not already exist."""
            if they do not already exist."""
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         tor_gencert = self._env['tor_gencert']
         tor_gencert = self._env['tor_gencert']
         lifetime = self._env['auth_cert_lifetime']
         lifetime = self._env['auth_cert_lifetime']
         idfile = os.path.join(datadir, 'keys', "authority_identity_key")
         idfile = os.path.join(datadir, 'keys', "authority_identity_key")
@@ -1124,7 +1126,7 @@ class RemoteNodeBuilder(NodeBuilder):
         """Generate an identity key for this router, unless we already have,
         """Generate an identity key for this router, unless we already have,
            and set up the 'fingerprint' entry in the Environ.
            and set up the 'fingerprint' entry in the Environ.
         """
         """
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         tor = self._env['tor']
         tor = self._env['tor']
         torrc = self._getTorrcFname()
         torrc = self._getTorrcFname()
         cmdline = [
         cmdline = [
@@ -1150,7 +1152,7 @@ class RemoteNodeBuilder(NodeBuilder):
         if not self._env['authority']:
         if not self._env['authority']:
             return ""
             return ""
 
 
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         certfile = os.path.join(datadir, 'keys', "authority_certificate")
         certfile = os.path.join(datadir, 'keys', "authority_certificate")
         v3id = None
         v3id = None
         with open(certfile, 'r') as f:
         with open(certfile, 'r') as f:
@@ -1240,7 +1242,7 @@ class LocalNodeController(NodeController):
            the pid of the running process, or None if there is no pid in the
            the pid of the running process, or None if there is no pid in the
            file.
            file.
         """
         """
-        pidfile = os.path.join(self._env['dir'], 'pid')
+        pidfile = os.path.join(self._env['local_dir'], 'pid')
         if not os.path.exists(pidfile):
         if not os.path.exists(pidfile):
             return None
             return None
 
 
@@ -1281,7 +1283,7 @@ class LocalNodeController(NodeController):
         # XXX Split this into "check" and "print" parts.
         # XXX Split this into "check" and "print" parts.
         pid = self.getPid()
         pid = self.getPid()
         nick = self._env['nick']
         nick = self._env['nick']
-        datadir = self._env['dir']
+        datadir = self._env['local_dir']
         corefile = "core.%s" % pid
         corefile = "core.%s" % pid
         tor_version = get_tor_version(self._env['tor'])
         tor_version = get_tor_version(self._env['tor'])
         if self.isRunning(pid):
         if self.isRunning(pid):
@@ -1343,7 +1345,7 @@ class LocalNodeController(NodeController):
         if self._env['google_cpu_profiler'] is True:
         if self._env['google_cpu_profiler'] is True:
             if add_environ_vars is None:
             if add_environ_vars is None:
                 add_environ_vars = {}
                 add_environ_vars = {}
-            add_environ_vars['CPUPROFILE'] = os.path.join(self._env['dir'], 'cpu-prof.out')
+            add_environ_vars['CPUPROFILE'] = os.path.join(self._env['local_dir'], 'cpu-prof.out')
         #
         #
         cmdline.extend([
         cmdline.extend([
             tor_path,
             tor_path,
@@ -1431,13 +1433,14 @@ class LocalNodeController(NodeController):
                     self._env['poll_launch_time_default']
                     self._env['poll_launch_time_default']
             return False
             return False
 
 
-    def getLogfile(self, info=False):
+    def getLogfile(self):
         """Return the expected path to the logfile for this instance."""
         """Return the expected path to the logfile for this instance."""
-        datadir = self._env['dir']
-        if info:
-            logname = "info.log"
-        else:
-            logname = "notice.log"
+        datadir = self._env['local_dir']
+        logfile_priority = ['notice', 'info', 'debug']
+        for p in logfile_priority:
+            if p in self._env['log_files']:
+                logname = p + '.log'
+                break
         return os.path.join(datadir, logname)
         return os.path.join(datadir, logname)
 
 
     def getLastBootstrapStatus(self):
     def getLastBootstrapStatus(self):
@@ -1482,8 +1485,8 @@ class RemoteNodeController(NodeController):
            the pid of the running process, or None if there is no pid in the
            the pid of the running process, or None if there is no pid in the
            file.
            file.
         """
         """
-        pidfile = os.path.join(self._env['dir'], 'pid')
         if self._env['remote_hostname'] is None:
         if self._env['remote_hostname'] is None:
+            pidfile = os.path.join(self._env['local_dir'], 'pid')
             if not os.path.exists(pidfile):
             if not os.path.exists(pidfile):
                 return None
                 return None
             with open(pidfile, 'r') as f:
             with open(pidfile, 'r') as f:
@@ -1492,6 +1495,7 @@ class RemoteNodeController(NodeController):
                 except ValueError:
                 except ValueError:
                     return None
                     return None
         else:
         else:
+            pidfile = os.path.join(self._env['remote_dir'], 'pid')
             pid = ssh_read_file(pidfile, self._env['remote_hostname'])
             pid = ssh_read_file(pidfile, self._env['remote_hostname'])
             if pid is None:
             if pid is None:
                 return None
                 return None
@@ -1533,7 +1537,10 @@ class RemoteNodeController(NodeController):
         # XXX Split this into "check" and "print" parts.
         # XXX Split this into "check" and "print" parts.
         pid = self.getPid()
         pid = self.getPid()
         nick = self._env['nick']
         nick = self._env['nick']
-        datadir = self._env['dir']
+        if self._env['remote_hostname'] is None:
+            datadir = self._env['local_dir']
+        else:
+            datadir = self._env['remote_dir']
         corefile = os.path.join(datadir, "core.%s" % pid)
         corefile = os.path.join(datadir, "core.%s" % pid)
         tor_version = get_tor_version(self._env['tor'], remote_hostname=self._env['remote_hostname'])
         tor_version = get_tor_version(self._env['tor'], remote_hostname=self._env['remote_hostname'])
 
 
@@ -1586,7 +1593,10 @@ class RemoteNodeController(NodeController):
             print("{:12} is already running".format(self._env['nick']))
             print("{:12} is already running".format(self._env['nick']))
             return True
             return True
         tor_path = self._env['tor']
         tor_path = self._env['tor']
-        torrc = self._getTorrcFname()
+        if self._env['remote_hostname'] is None:
+            torrc = self._getTorrcFname()
+        else:
+            torrc = os.path.join(self._env['remote_dir'], 'torrc')
         #
         #
         add_environ_vars = self._env['add_environ_vars']
         add_environ_vars = self._env['add_environ_vars']
         if add_environ_vars is not None:
         if add_environ_vars is not None:
@@ -1595,7 +1605,10 @@ class RemoteNodeController(NodeController):
         if self._env['google_cpu_profiler'] is True:
         if self._env['google_cpu_profiler'] is True:
             if add_environ_vars is None:
             if add_environ_vars is None:
                 add_environ_vars = {}
                 add_environ_vars = {}
-            add_environ_vars['CPUPROFILE'] = os.path.join(self._env['dir'], 'cpu-prof.out')
+            if self._env['remote_hostname'] is None:
+                add_environ_vars['CPUPROFILE'] = os.path.join(self._env['local_dir'], 'cpu-prof.out')
+            else:
+                add_environ_vars['CPUPROFILE'] = os.path.join(self._env['remote_dir'], 'cpu-prof.out')
         #
         #
         cmdline = []
         cmdline = []
         if self._env['remote_hostname'] is not None:
         if self._env['remote_hostname'] is not None:
@@ -1712,13 +1725,17 @@ class RemoteNodeController(NodeController):
                     self._env['poll_launch_time_default']
                     self._env['poll_launch_time_default']
             return False
             return False
 
 
-    def getLogfile(self, info=False):
+    def getLogfile(self):
         """Return the expected path to the logfile for this instance."""
         """Return the expected path to the logfile for this instance."""
-        datadir = self._env['dir']
-        if info:
-            logname = "info.log"
+        if self._env['remote_hostname'] is None:
+            datadir = self._env['local_dir']
         else:
         else:
-            logname = "notice.log"
+            datadir = self._env['remote_dir']
+        logfile_priority = ['notice', 'info', 'debug']
+        for p in logfile_priority:
+            if p in self._env['log_files']:
+                logname = p + '.log'
+                break
         return os.path.join(datadir, logname)
         return os.path.join(datadir, logname)
 
 
     def getLastBootstrapStatus(self):
     def getLastBootstrapStatus(self):
@@ -1757,9 +1774,10 @@ class RemoteNodeController(NodeController):
 
 
     def getRemoteFiles(self):
     def getRemoteFiles(self):
         if self._env['remote_hostname'] is not None:
         if self._env['remote_hostname'] is not None:
-            path = os.path.abspath(self._env['dir'])
-            ssh_rm_if_exists(os.path.join(path, 'control'), self._env['remote_hostname'])
-            scp_dir_backwards(path, self._env['remote_hostname'])
+            local_path = os.path.abspath(self._env['local_dir'])
+            remote_path = os.path.abspath(self._env['remote_dir'])
+            ssh_rm_if_exists(os.path.join(remote_path, 'control'), self._env['remote_hostname'])
+            scp_dir_backwards(remote_path, local_path, self._env['remote_hostname'])
 
 
 # XXX: document these options
 # XXX: document these options
 DEFAULTS = {
 DEFAULTS = {
@@ -1786,7 +1804,7 @@ DEFAULTS = {
     'ipv6_addr': os.environ.get('CHUTNEY_LISTEN_ADDRESS_V6', None),
     'ipv6_addr': os.environ.get('CHUTNEY_LISTEN_ADDRESS_V6', None),
     'dirserver_flags': 'no-v2',
     'dirserver_flags': 'no-v2',
     'chutney_dir': get_absolute_chutney_path(),
     'chutney_dir': get_absolute_chutney_path(),
-    'torrc_fname': '${dir}/torrc',
+    'torrc_fname': '${local_dir}/torrc',
     'orport_base': 5000,
     'orport_base': 5000,
     'dirport_base': 7000,
     'dirport_base': 7000,
     'controlport_base': 8000,
     'controlport_base': 8000,
@@ -1846,6 +1864,7 @@ DEFAULTS = {
     'log_files': ['notice', 'info', 'debug'],
     'log_files': ['notice', 'info', 'debug'],
     'google_cpu_profiler': False,
     'google_cpu_profiler': False,
     'remote_hostname': None,
     'remote_hostname': None,
+    'remote_net_dir': None,
     'num_additional_eventloops': None,
     'num_additional_eventloops': None,
     'log_throughput': False,
     'log_throughput': False,
 }
 }
@@ -1925,12 +1944,25 @@ class TorEnviron(chutney.Templating.Environ):
     def _get_ptport(self, my):
     def _get_ptport(self, my):
         return my['ptport_base'] + my['nodenum']
         return my['ptport_base'] + my['nodenum']
 
 
-    def _get_dir(self, my):
+    def _get_local_dir(self, my):
         return os.path.abspath(os.path.join(my['net_base_dir'],
         return os.path.abspath(os.path.join(my['net_base_dir'],
                                             "nodes",
                                             "nodes",
                                             "%03d%s" % (
                                             "%03d%s" % (
                                                 my['nodenum'], my['tag'])))
                                                 my['nodenum'], my['tag'])))
 
 
+    def _get_remote_dir(self, my):
+        if my['remote_net_dir'] is None or my['remote_hostname'] is None:
+            return None
+        return os.path.abspath(os.path.join(my['remote_net_dir'],
+                                            "nodes",
+                                            "%03d%s" % (
+                                                my['nodenum'], my['tag'])))
+
+    def _get_dir(self, my):
+        if self['remote_dir'] is not None:
+            return self['remote_dir']
+        return self['local_dir']
+
     def _get_nick(self, my):
     def _get_nick(self, my):
         return "%s%03d%s" % (my['nick_base'], my['nodenum'], my['tag'])
         return "%s%03d%s" % (my['nick_base'], my['nodenum'], my['tag'])
 
 
@@ -2104,10 +2136,12 @@ class Network(object):
         print("NOTE: creating %r, linking to %r" % (newnodesdir, nodeslink))
         print("NOTE: creating %r, linking to %r" % (newnodesdir, nodeslink))
         # this gets created with mode 0700, that's probably ok
         # this gets created with mode 0700, that's probably ok
         mkdir_p(newnodesdir)
         mkdir_p(newnodesdir)
-        remote_hostnames = list(set([x._env['remote_hostname'] for x in self._nodes if x._env['remote_hostname'] is not None]))
-        if len(remote_hostnames) != 0:
-            for x in remote_hostnames:
-                ssh_mkdir_p(newnodesdir, x)
+        remotes = list(set([(x._env['remote_hostname'],x._env['remote_net_dir']) for x in self._nodes if x._env['remote_hostname'] is not None]))
+        if len(remotes) != 0:
+            for (hostname, remote_net_dir) in remotes:
+                assert remote_net_dir is not None
+                remote_newnodesdir = os.path.join(remote_net_dir, os.path.basename(newnodesdir))
+                ssh_mkdir_p(remote_newnodesdir, hostname)
         try:
         try:
             os.unlink(nodeslink)
             os.unlink(nodeslink)
         except OSError as e:
         except OSError as e:
@@ -2116,13 +2150,18 @@ class Network(object):
                 pass
                 pass
             else:
             else:
                 raise
                 raise
-        if len(remote_hostnames) != 0:
-            for x in remote_hostnames:
-                ssh_rm_if_exists(nodeslink, x)
+        if len(remotes) != 0:
+            for (hostname, remote_net_dir) in remotes:
+                assert remote_net_dir is not None
+                remote_nodeslink = os.path.join(remote_net_dir, 'nodes')
+                ssh_rm_if_exists(remote_nodeslink, hostname)
         os.symlink(newnodesdir, nodeslink)
         os.symlink(newnodesdir, nodeslink)
-        if len(remote_hostnames) != 0:
-            for x in remote_hostnames:
-                ssh_symlink(newnodesdir, nodeslink, x)
+        if len(remotes) != 0:
+            for (hostname, remote_net_dir) in remotes:
+                assert remote_net_dir is not None
+                remote_newnodesdir = os.path.join(remote_net_dir, os.path.basename(newnodesdir))
+                remote_nodeslink = os.path.join(remote_net_dir, 'nodes')
+                ssh_symlink(remote_newnodesdir, remote_nodeslink, hostname)
 
 
     def _checkConfig(self):
     def _checkConfig(self):
         for n in self._nodes:
         for n in self._nodes: