miscell cleanup and prettyfication in collector and application
[nepi.git] / nepi / resources / linux / application.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License version 2 as
7 #    published by the Free Software Foundation;
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
18
19 from nepi.execution.attribute import Attribute, Flags, Types
20 from nepi.execution.trace import Trace, TraceAttr
21 from nepi.execution.resource import ResourceManager, clsinit_copy, \
22         ResourceState
23 from nepi.resources.linux.node import LinuxNode
24 from nepi.util.sshfuncs import ProcStatus, STDOUT
25 from nepi.util.timefuncs import tnow, tdiffsec
26
27 import os
28 import subprocess
29
30 # TODO: Resolve wildcards in commands!!
31 # TODO: When a failure occurs during deployment, scp and ssh processes are left running behind!!
32
33 @clsinit_copy
34 class LinuxApplication(ResourceManager):
35     """
36     .. class:: Class Args :
37       
38         :param ec: The Experiment controller
39         :type ec: ExperimentController
40         :param guid: guid of the RM
41         :type guid: int
42
43     .. note::
44
45         A LinuxApplication RM represents a process that can be executed in
46         a remote Linux host using SSH.
47
48         The LinuxApplication RM takes care of uploadin sources and any files
49         needed to run the experiment, to the remote host. 
50         It also allows to provide source compilation (build) and installation 
51         instructions, and takes care of automating the sources build and 
52         installation tasks for the user.
53
54         It is important to note that files uploaded to the remote host have
55         two possible scopes: single-experiment or multi-experiment.
56         Single experiment files are those that will not be re-used by other 
57         experiments. Multi-experiment files are those that will.
58         Sources and shared files are always made available to all experiments.
59
60         Directory structure:
61
62         The directory structure used by LinuxApplication RM at the Linux
63         host is the following:
64
65         ${HOME}/.nepi/nepi-usr --> Base directory for multi-experiment files
66                       |
67         ${LIB}        |- /lib --> Base directory for libraries
68         ${BIN}        |- /bin --> Base directory for binary files
69         ${SRC}        |- /src --> Base directory for sources
70         ${SHARE}      |- /share --> Base directory for other files
71
72         ${HOME}/.nepi/nepi-exp --> Base directory for single-experiment files
73                       |
74         ${EXP_HOME}   |- /<exp-id>  --> Base directory for experiment exp-id
75                           |
76         ${APP_HOME}       |- /<app-guid> --> Base directory for application 
77                                |     specific files (e.g. command.sh, input)
78                                | 
79         ${RUN_HOME}            |- /<run-id> --> Base directory for run specific
80
81     """
82
83     _rtype = "linux::Application"
84     _help = "Runs an application on a Linux host with a BASH command "
85     _platform = "linux"
86
87     @classmethod
88     def _register_attributes(cls):
89         cls._register_attribute(
90             Attribute("command", "Command to execute at application start. "
91                       "Note that commands will be executed in the ${RUN_HOME} directory, "
92                       "make sure to take this into account when using relative paths. ", 
93                       flags = Flags.Design))
94         cls._register_attribute(
95             Attribute("forwardX11",
96                       "Enables X11 forwarding for SSH connections", 
97                       flags = Flags.Design))
98         cls._register_attribute(
99             Attribute("env",
100                       "Environment variables string for command execution",
101                       flags = Flags.Design))
102         cls._register_attribute(
103             Attribute("sudo",
104                       "Run with root privileges", 
105                       flags = Flags.Design))
106         cls._register_attribute(
107             Attribute("depends", 
108                       "Space-separated list of packages required to run the application",
109                       flags = Flags.Design))
110         cls._register_attribute(
111             Attribute("sources", 
112                       "semi-colon separated list of regular files to be uploaded to ${SRC} "
113                       "directory prior to building. Archives won't be expanded automatically. "
114                       "Sources are globally available for all experiments unless "
115                       "cleanHome is set to True (This will delete all sources). ",
116                       flags = Flags.Design))
117         cls._register_attribute(
118             Attribute("files", 
119                       "semi-colon separated list of regular miscellaneous files to be uploaded "
120                       "to ${SHARE} directory. "
121                       "Files are globally available for all experiments unless "
122                       "cleanHome is set to True (This will delete all files). ",
123                       flags = Flags.Design))
124         cls._register_attribute(
125             Attribute("libs", 
126                       "semi-colon separated list of libraries (e.g. .so files) to be uploaded "
127                       "to ${LIB} directory. "
128                       "Libraries are globally available for all experiments unless "
129                       "cleanHome is set to True (This will delete all files). ",
130                       flags = Flags.Design))
131         cls._register_attribute(
132             Attribute("bins", 
133                       "semi-colon separated list of binary files to be uploaded "
134                       "to ${BIN} directory. "
135                       "Binaries are globally available for all experiments unless "
136                       "cleanHome is set to True (This will delete all files). ",
137                       flags = Flags.Design))
138         cls._register_attribute(
139             Attribute("code", 
140                       "Plain text source code to be uploaded to the ${APP_HOME} directory. ",
141                       flags = Flags.Design))
142         cls._register_attribute(
143             Attribute("build", 
144                       "Build commands to execute after deploying the sources. "
145                       "Sources are uploaded to the ${SRC} directory and code "
146                       "is uploaded to the ${APP_HOME} directory. \n"
147                       "Usage example: tar xzf ${SRC}/my-app.tgz && cd my-app && "
148                       "./configure && make && make clean.\n"
149                       "Make sure to make the build commands return with a nonzero exit "
150                       "code on error.",
151                       flags = Flags.Design))
152         cls._register_attribute(
153             Attribute("install", 
154                       "Commands to transfer built files to their final destinations. "
155                       "Install commands are executed after build commands. ",
156                       flags = Flags.Design))
157         cls._register_attribute(
158             Attribute("stdin", "Standard input for the 'command'", 
159                       flags = Flags.Design))
160         cls._register_attribute(
161             Attribute("tearDown",
162                       "Command to be executed just before releasing the resource", 
163                       flags = Flags.Design))
164         cls._register_attribute(
165             Attribute("splitStderr", 
166                       "requests stderr to be retrieved separately",
167                       default = False))
168
169     @classmethod
170     def _register_traces(cls):
171         cls._register_trace(
172             Trace("stdout", "Standard output stream", enabled = True))
173         cls._register_trace(
174             Trace("stderr", "Standard error stream", enabled = True))
175
176     def __init__(self, ec, guid):
177         super(LinuxApplication, self).__init__(ec, guid)
178         self._pid = None
179         self._ppid = None
180         self._node = None
181         self._home = "app-{}".format(self.guid)
182
183         # whether the command should run in foreground attached
184         # to a terminal
185         self._in_foreground = False
186
187         # whether to use sudo to kill the application process
188         self._sudo_kill = False
189
190         # keep a reference to the running process handler when 
191         # the command is not executed as remote daemon in background
192         self._proc = None
193
194         # timestamp of last state check of the application
195         self._last_state_check = tnow()
196         
197     def log_message(self, msg):
198         return " guid {} - host {} - {} "\
199             .format(self.guid, self.node.get("hostname"), msg)
200
201     @property
202     def node(self):
203         if not self._node:
204             node = self.get_connected(LinuxNode.get_rtype())
205             if not node: 
206                 msg = "Application {} guid {} NOT connected to Node"\
207                       .format(self._rtype, self.guid)
208                 raise RuntimeError(msg)
209
210             self._node = node[0]
211
212         return self._node
213
214     @property
215     def app_home(self):
216         return os.path.join(self.node.exp_home, self._home)
217
218     @property
219     def run_home(self):
220         return os.path.join(self.app_home, self.ec.run_id)
221
222     @property
223     def pid(self):
224         return self._pid
225
226     @property
227     def ppid(self):
228         return self._ppid
229
230     @property
231     def in_foreground(self):
232         """
233         Returns True if the command needs to be executed in foreground.
234         This means that command will be executed using 'execute' instead of
235         'run' ('run' executes a command in background and detached from the 
236         terminal)
237         
238         When using X11 forwarding option, the command can not run in background
239         and detached from a terminal, since we need to keep the terminal attached 
240         to interact with it.
241         """
242         return self.get("forwardX11") or self._in_foreground
243
244     def trace_filepath(self, filename):
245         return os.path.join(self.run_home, filename)
246
247     def trace(self, name, attr = TraceAttr.ALL, block = 512, offset = 0):
248         self.info("Retrieving '{}' trace {} ".format(name, attr))
249
250         path = self.trace_filepath(name)
251         
252         command = "(test -f {} && echo 'success') || echo 'error'".format(path)
253         (out, err), proc = self.node.execute(command)
254
255         if (err and proc.poll()) or out.find("error") != -1:
256             msg = " Couldn't find trace {} ".format(name)
257             self.error(msg, out, err)
258             return None
259     
260         if attr == TraceAttr.PATH:
261             return path
262
263         if attr == TraceAttr.ALL:
264             (out, err), proc = self.node.check_output(self.run_home, name)
265             
266             if proc.poll():
267                 msg = " Couldn't read trace {} ".format(name)
268                 self.error(msg, out, err)
269                 return None
270
271             return out
272
273         if attr == TraceAttr.STREAM:
274             cmd = "dd if={} bs={} count=1 skip={}".format(path, block, offset)
275         elif attr == TraceAttr.SIZE:
276             cmd = "stat -c {} ".format(path)
277
278         (out, err), proc = self.node.execute(cmd)
279
280         if proc.poll():
281             msg = " Couldn't find trace {} ".format(name)
282             self.error(msg, out, err)
283             return None
284         
285         if attr == TraceAttr.SIZE:
286             out = int(out.strip())
287
288         return out
289
290     def do_provision(self):
291         # take a snapshot of the system if user is root
292         # to ensure that cleanProcess will not kill
293         # pre-existent processes
294         if self.node.get("username") == 'root':
295             import pickle
296             procs = dict()
297             ps_aux = "ps aux | awk '{print $2,$11}'"
298             (out, err), proc = self.node.execute(ps_aux)
299             if len(out) != 0:
300                 for line in out.strip().split("\n"):
301                     parts = line.strip().split(" ")
302                     procs[parts[0]] = parts[1]
303                 with open("/tmp/save.proc", "wb") as pickle_file:
304                     pickle.dump(procs, pickle_file)
305             
306         # create run dir for application
307         self.node.mkdir(self.run_home)
308    
309         # List of all the provision methods to invoke
310         steps = [
311             # upload sources
312             self.upload_sources,
313             # upload files
314             self.upload_files,
315             # upload binaries
316             self.upload_binaries,
317             # upload libraries
318             self.upload_libraries,
319             # upload code
320             self.upload_code,
321             # upload stdin
322             self.upload_stdin,
323             # install dependencies
324             self.install_dependencies,
325             # build
326             self.build,
327             # Install
328             self.install,
329         ]
330
331         command = []
332
333         # Since provisioning takes a long time, before
334         # each step we check that the EC is still 
335         for step in steps:
336             if self.ec.abort:
337                 self.debug("Interrupting provisioning. EC says 'ABORT")
338                 return
339             
340             ret = step()
341             if ret:
342                 command.append(ret)
343
344         # upload deploy script
345         deploy_command = ";".join(command)
346         self.execute_deploy_command(deploy_command)
347
348         # upload start script
349         self.upload_start_command()
350        
351         self.info("Provisioning finished")
352
353         super(LinuxApplication, self).do_provision()
354
355     def upload_start_command(self, overwrite = False):
356         # Upload command to remote bash script
357         # - only if command can be executed in background and detached
358         command = self.get("command")
359
360         if command and not self.in_foreground:
361 #            self.info("Uploading command '{}'".format(command))
362
363             # replace application specific paths in the command
364             command = self.replace_paths(command)
365             # replace application specific paths in the environment
366             env = self.get("env")
367             env = env and self.replace_paths(env)
368
369             shfile = os.path.join(self.app_home, "start.sh")
370
371             self.node.upload_command(command, 
372                                      shfile = shfile,
373                                      env = env,
374                                      overwrite = overwrite)
375
376     def execute_deploy_command(self, command, prefix="deploy"):
377         if command:
378             # replace application specific paths in the command
379             command = self.replace_paths(command)
380             
381             # replace application specific paths in the environment
382             env = self.get("env")
383             env = env and self.replace_paths(env)
384
385             # Upload the command to a bash script and run it
386             # in background ( but wait until the command has
387             # finished to continue )
388             shfile = os.path.join(self.app_home, "{}.sh".format(prefix))
389             # low-level spawn tools in both sshfuncs and execfuncs
390             # expect stderr=sshfuncs.STDOUT to mean std{out,err} are merged
391             stderr = "{}_stderr".format(prefix) \
392                      if self.get("splitStderr") \
393                         else STDOUT
394             print("{} : prefix = {}, command={}, stderr={}"
395                   .format(self, prefix, command, stderr))
396             self.node.run_and_wait(command, self.run_home,
397                                    shfile = shfile, 
398                                    overwrite = False,
399                                    pidfile = "{}_pidfile".format(prefix), 
400                                    ecodefile = "{}_exitcode".format(prefix), 
401                                    stdout = "{}_stdout".format(prefix),
402                                    stderr = stderr)
403
404     def upload_sources(self, sources = None, src_dir = None):
405         if not sources:
406             sources = self.get("sources")
407    
408         command = ""
409
410         if not src_dir:
411             src_dir = self.node.src_dir
412
413         if sources:
414             self.info("Uploading sources ")
415
416             sources = [str.strip(source) for source in sources.split(";")]
417
418             # Separate sources that should be downloaded from 
419             # the web, from sources that should be uploaded from
420             # the local machine
421             command = []
422             for source in list(sources):
423                 if source.startswith("http") or source.startswith("https"):
424                     # remove the hhtp source from the sources list
425                     sources.remove(source)
426
427                     command.append(
428                         " ( " 
429                         # Check if the source already exists
430                         " ls {src_dir}/{basename} "
431                         " || ( "
432                         # If source doesn't exist, download it and check
433                         # that it it downloaded ok
434                         "   wget -c --directory-prefix={src_dir} {source} && "
435                         "   ls {src_dir}/{basename} "
436                         " ) ) ".format(
437                             basename = os.path.basename(source),
438                             source = source,
439                             src_dir = src_dir
440                         ))
441
442             command = " && ".join(command)
443
444             # replace application specific paths in the command
445             command = self.replace_paths(command)
446        
447             if sources:
448                 sources = ';'.join(sources)
449                 self.node.upload(sources, src_dir, overwrite = False)
450
451         return command
452
453     def upload_files(self, files = None):
454         if not files:
455             files = self.get("files")
456
457         if files:
458             self.info("Uploading files {} ".format(files))
459             self.node.upload(files, self.node.share_dir, overwrite = False)
460
461     def upload_libraries(self, libs = None):
462         if not libs:
463             libs = self.get("libs")
464
465         if libs:
466             self.info("Uploading libraries {} ".format(libs))
467             self.node.upload(libs, self.node.lib_dir, overwrite = False)
468
469     def upload_binaries(self, bins = None):
470         if not bins:
471             bins = self.get("bins")
472
473         if bins:
474             self.info("Uploading binaries {} ".format(bins))
475             self.node.upload(bins, self.node.bin_dir, overwrite = False)
476
477     def upload_code(self, code = None):
478         if not code:
479             code = self.get("code")
480
481         if code:
482             self.info("Uploading code")
483             dst = os.path.join(self.app_home, "code")
484             self.node.upload(code, dst, overwrite = False, text = True, executable = True)
485
486     def upload_stdin(self, stdin = None):
487         if not stdin:
488            stdin = self.get("stdin")
489
490         if stdin:
491             # create dir for sources
492             self.info("Uploading stdin")
493             
494             # upload stdin file to ${SHARE_DIR} directory
495             if os.path.isfile(stdin):
496                 basename = os.path.basename(stdin)
497                 dst = os.path.join(self.node.share_dir, basename)
498             else:
499                 dst = os.path.join(self.app_home, "stdin")
500
501             self.node.upload(stdin, dst, overwrite = False, text = True)
502
503             # create "stdin" symlink on ${APP_HOME} directory
504             command = "( cd {app_home} ; [ ! -f stdin ] &&  ln -s {stdin} stdin )"\
505                       .format(app_home = self.app_home, stdin = dst)
506             return command
507
508     def install_dependencies(self, depends = None):
509         if not depends:
510             depends = self.get("depends")
511
512         if depends:
513             self.info("Installing dependencies {}".format(depends))
514             return self.node.install_packages_command(depends)
515
516     def build(self, build = None):
517         if not build:
518             build = self.get("build")
519
520         if build:
521             self.info("Building sources ")
522             
523             # replace application specific paths in the command
524             return self.replace_paths(build)
525
526     def install(self, install = None):
527         if not install:
528             install = self.get("install")
529
530         if install:
531             self.info("Installing sources ")
532
533             # replace application specific paths in the command
534             return self.replace_paths(install)
535
536     def do_deploy(self):
537         # Wait until node is associated and deployed
538         node = self.node
539         if not node or node.state < ResourceState.READY:
540             self.debug("---- RESCHEDULING DEPLOY ---- node state {} ".format(self.node.state))
541             self.ec.schedule(self.reschedule_delay, self.deploy)
542         else:
543             command = self.get("command") or ""
544             self.info("Deploying command '{}' ".format(command))
545             self.do_discover()
546             self.do_provision()
547
548             super(LinuxApplication, self).do_deploy()
549    
550     def do_start(self):
551         command = self.get("command")
552
553         self.info("Starting command '{}'".format(command))
554
555         if not command:
556             # If no command was given (i.e. Application was used for dependency
557             # installation), then the application is directly marked as STOPPED
558             super(LinuxApplication, self).set_stopped()
559         else:
560             if self.in_foreground:
561                 self._run_in_foreground()
562             else:
563                 self._run_in_background()
564
565             super(LinuxApplication, self).do_start()
566
567     def _run_in_foreground(self):
568         command = self.get("command")
569         sudo = self.get("sudo") or False
570         x11 = self.get("forwardX11")
571         env = self.get("env")
572
573         # Command will be launched in foreground and attached to the
574         # terminal using the node 'execute' in non blocking mode.
575
576         # We save the reference to the process in self._proc 
577         # to be able to kill the process from the stop method.
578         # We also set blocking = False, since we don't want the
579         # thread to block until the execution finishes.
580         (out, err), self._proc = self.execute_command(command, 
581                                                       env = env,
582                                                       sudo = sudo,
583                                                       forward_x11 = x11,
584                                                       blocking = False)
585
586         if self._proc.poll():
587             self.error(msg, out, err)
588             raise RuntimeError(msg)
589
590     def _run_in_background(self):
591         command = self.get("command")
592         env = self.get("env")
593         sudo = self.get("sudo") or False
594
595         stdout = "stdout"
596         # low-level spawn tools in both sshfuncs and execfuncs
597         # expect stderr=sshfuncs.STDOUT to mean std{out,err} are merged
598         stderr = "stderr" \
599                  if self.get("splitStderr") \
600                     else STDOUT
601         stdin = os.path.join(self.app_home, "stdin") if self.get("stdin") \
602                 else None
603
604         # Command will be run as a daemon in baground and detached from any
605         # terminal.
606         # The command to run was previously uploaded to a bash script
607         # during deployment, now we launch the remote script using 'run'
608         # method from the node.
609         cmd = "bash {}".format(os.path.join(self.app_home, "start.sh"))
610         (out, err), proc = self.node.run(cmd, self.run_home, 
611                                          stdin = stdin, 
612                                          stdout = stdout,
613                                          stderr = stderr,
614                                          sudo = sudo)
615
616         # check if execution errors occurred
617         msg = " Failed to start command '{}' ".format(command)
618         
619         if proc.poll():
620             self.error(msg, out, err)
621             raise RuntimeError(msg)
622     
623         # Wait for pid file to be generated
624         pid, ppid = self.node.wait_pid(self.run_home)
625         if pid: self._pid = int(pid)
626         if ppid: self._ppid = int(ppid)
627
628         # If the process is not running, check for error information
629         # on the remote machine
630         if not self.pid or not self.ppid:
631             (out, err), proc = self.node.check_errors(self.run_home,
632                                                       stderr = stderr) 
633
634             # Out is what was written in the stderr file
635             if err:
636                 msg = " Failed to start command '{}' ".format(command)
637                 self.error(msg, out, err)
638                 raise RuntimeError(msg)
639     
640     def do_stop(self):
641         """ Stops application execution
642         """
643         command = self.get('command') or ''
644
645         if self.state == ResourceState.STARTED:
646         
647             self.info("Stopping command '{}' ".format(command))
648         
649             # If the command is running in foreground (it was launched using
650             # the node 'execute' method), then we use the handler to the Popen
651             # process to kill it. Else we send a kill signal using the pid and ppid
652             # retrieved after running the command with the node 'run' method
653             if self._proc:
654                 self._proc.kill()
655             else:
656                 # Only try to kill the process if the pid and ppid
657                 # were retrieved
658                 if self.pid and self.ppid:
659                     (out, err), proc = self.node.kill(self.pid, self.ppid,
660                                                       sudo = self._sudo_kill)
661
662                     """
663                     # TODO: check if execution errors occurred
664                     if (proc and proc.poll()) or err:
665                         msg = " Failed to STOP command '{}' ".format(self.get("command"))
666                         self.error(msg, out, err)
667                     """
668
669             super(LinuxApplication, self).do_stop()
670
671     def do_release(self):
672         self.info("Releasing resource")
673
674         self.do_stop()
675         
676         tear_down = self.get("tearDown")
677         if tear_down:
678             self.node.execute(tear_down)
679
680         hard_release = self.get("hardRelease")
681         if hard_release:
682             self.node.rmdir(self.app_home)
683
684         super(LinuxApplication, self).do_release()
685         
686     @property
687     def state(self):
688         """ Returns the state of the application
689         """
690         if self._state == ResourceState.STARTED:
691             if self.in_foreground:
692                 # Check if the process we used to execute the command
693                 # is still running ...
694                 retcode = self._proc.poll()
695
696                 # retcode == None -> running
697                 # retcode > 0 -> error
698                 # retcode == 0 -> finished
699                 if retcode:
700                     out = ""
701                     msg = " Failed to execute command '{}'".format(self.get("command"))
702                     err = self._proc.stderr.read()
703                     self.error(msg, out, err)
704                     self.do_fail()
705
706                 elif retcode == 0:
707                     self.set_stopped()
708             else:
709                 # We need to query the status of the command we launched in 
710                 # background. In order to avoid overwhelming the remote host and
711                 # the local processor with too many ssh queries, the state is only
712                 # requested every 'state_check_delay' seconds.
713                 state_check_delay = 0.5
714                 if tdiffsec(tnow(), self._last_state_check) > state_check_delay:
715                     if self.pid and self.ppid:
716                         # Make sure the process is still running in background
717                         status = self.node.status(self.pid, self.ppid)
718
719                         if status == ProcStatus.FINISHED:
720                             # If the program finished, check if execution
721                             # errors occurred
722                             (out, err), proc \
723                                 = self.node.check_errors(self.run_home)
724
725                             if err:
726                                 msg = "Failed to execute command '{}'"\
727                                       .format(self.get("command"))
728                                 self.error(msg, out, err)
729                                 self.do_fail()
730                             else:
731                                 self.set_stopped()
732
733                     self._last_state_check = tnow()
734
735         return self._state
736
737     def execute_command(self, command, 
738                         env = None,
739                         sudo = False,
740                         tty = False,
741                         forward_x11 = False,
742                         blocking = False):
743
744         environ = ""
745         if env:
746             environ = self.node.format_environment(env, inline = True)
747         command = environ + command
748         command = self.replace_paths(command)
749
750         return self.node.execute(command,
751                                  sudo = sudo,
752                                  tty = tty,
753                                  forward_x11 = forward_x11,
754                                  blocking = blocking)
755
756     def replace_paths(self, command, node = None, app_home = None, run_home = None):
757         """
758         Replace all special path tags with shell-escaped actual paths.
759         """
760         if not node:
761             node = self.node
762
763         if not app_home:
764             app_home = self.app_home
765
766         if not run_home:
767             run_home = self.run_home
768
769         return ( command
770                  .replace("${USR}", node.usr_dir)
771                  .replace("${LIB}", node.lib_dir)
772                  .replace("${BIN}", node.bin_dir)
773                  .replace("${SRC}", node.src_dir)
774                  .replace("${SHARE}", node.share_dir)
775                  .replace("${EXP}", node.exp_dir)
776                  .replace("${EXP_HOME}", node.exp_home)
777                  .replace("${APP_HOME}", app_home)
778                  .replace("${RUN_HOME}", run_home)
779                  .replace("${NODE_HOME}", node.node_home)
780                  .replace("${HOME}", node.home_dir)
781                  # a shortcut to refer to the file uploaded as 'code = '
782                  .replace("${CODE}", "{}/code".format(app_home))
783              )
784
785     def valid_connection(self, guid):
786         # TODO: Validate!
787         return True