Simplified a bit.
[xremote.git] / xremote.sh
1 #!/bin/bash
2
3 #########################################################################
4 # This program is free software: you can redistribute it and/or modify  #
5 # it under the terms of the version 3 of the GNU General Public License #
6 # as published by the Free Software Foundation.                         #
7 #                                                                       #
8 # This program is distributed in the hope that it will be useful, but   #
9 # WITHOUT ANY WARRANTY; without even the implied warranty of            #
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      #
11 # General Public License for more details.                              #
12 #                                                                       #
13 # You should have received a copy of the GNU General Public License     #
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.  #
15 #                                                                       #
16 # Written by and Copyright (C) Francois Fleuret                         #
17 # Contact <francois@fleuret.org> for comments & bug reports             #
18 #########################################################################
19
20 set -e
21
22 # set -o pipefail
23
24 ######################################################################
25
26 function check_remote_is_defined () {
27     if [[ "${REMOTE_HOST}" ]] && [[ "${REMOTE_DIR}" ]]
28     then
29         return 0
30     else
31         echo "@XREMOTE_HOST should come first." >&2
32         exit 1
33     fi
34 }
35
36 function help () {
37     cat <<EOF
38 xremote.sh [--help] [-h <remote_host>] [-d <remote_dir>] [-r <local_result_dir>] [-i] <script> [script arguments]
39
40   This script takes a script as argument and executes it remotely in a
41   temporary directory on a ssh-accessible server.
42
43   It parses the script first to find embedded arguments which define
44   the hostname on which to run, the files to send, the files to get
45   back when the execution is over, and commands to execute before
46   running the executable remotely.
47
48   These arguments can appear multiple times, except the one that
49   specifies the remote host.
50
51   Example:
52
53     @XREMOTE_HOST: elk.fleuret.org
54     @XREMOTE_EXEC: python3
55     @XREMOTE_SEND: main.cf
56     @XREMOTE_GET: *.dat
57     @XREMOTE_PRE: ln -s /home/fleuret/data/pytorch ./data
58
59   If a file with the same name as the script with the .xremote
60   extension appended to it exists, arguments will be read from it by
61   default.
62
63   If the -h option is provided @XREMOTE_HOST is ignored.
64
65   If the -d option is provided, the provided directory is used and
66   kept, instead of a temporary one
67
68   If the -i option is provided, all the files are installed and
69   scripts run in the specified directory on the remote host, but the
70   main executable and post-run commands are ignored
71
72   If the -r option is provided, the result files specified with
73   @XREMOTE_GET will be downloaded there instead of the current
74   directory.
75
76   If no argument is provided to @XREMOTE_HOST, and the -h option is
77   not specified, the environment variable \$XREMOTE_HOST is used
78   instead
79
80   Contact <francois@fleuret.org> for comments.
81
82 EOF
83     return 0
84 }
85
86 function cleanup_remote_tmp () {
87     if [[ "${REMOTE_HOST}" ]] && [[ "${REMOTE_DIR}" ]]
88     then
89         if [[ "${ARG_DIR}" ]]
90         then
91             echo "xremote: Keeping remote workdir."
92         else
93             echo "xremote: Cleaning up temporary remote workdir."
94             ssh "${REMOTE_HOST}" rm -rf "${REMOTE_DIR}"
95         fi
96     fi
97 }
98
99 ######################################################################
100
101 while [[ "$1" =~ ^- ]]
102 do
103     case "$1"
104     in
105         -h)
106             shift
107             ARG_HOST="$1"
108             [[ ${ARG_HOST} ]] || (echo "xremote: Hostname missing." && exit 1)
109             echo "xremote: remote forced to ${ARG_HOST}"
110             ;;
111
112         -d)
113             shift
114             ARG_DIR="$1"
115             [[ ${ARG_DIR} ]] || (echo "xremote: Directory missing." && exit 1)
116             echo "xremote: remote dir set to ${ARG_DIR}"
117             ;;
118
119         -i)
120             NO_RUN=1
121             echo "xremote: no run"
122             ;;
123
124         -r)
125             shift
126             ARG_RESULT_DIR="$1"
127             [[ ${ARG_RESULT_DIR} ]] || (echo "xremote: Directory missing." && exit 1)
128             echo "xremote: result dir set to ${ARG_RESULT_DIR}"
129             ;;
130
131         --help)
132             help
133             exit 0
134             ;;
135
136         *)
137             echo "xremote: Unknown option $1"
138             exit 1
139             ;;
140     esac
141     shift
142 done
143
144 ######################################################################
145
146 [[ "$1" ]] || (echo "xremote: Script name missing" && exit 1)
147
148 [[ -a "$1" ]] || (help && echo >&2 "xremote: Cannot find script \`$1'" && exit 1)
149
150 cd "$(dirname "$1")"
151
152 main="$(basename "$1")"
153 main_config="${main}.xremote"
154
155 if [[ -f "${main_config}" ]]
156 then
157     echo "xremote: found ${main_config}"
158 else
159     main_config="${main}"
160 fi
161
162 shift
163
164 trap cleanup_remote_tmp EXIT
165
166 ######################################################################
167
168 while read line
169 do
170
171     if [[ "${line}" =~ '@XREMOTE' ]]
172     then
173
174         label=$(sed -e 's/^.*@XREMOTE_\([^:]*\):.*$/\1/' <<<"${line}")
175         value=$(sed -e 's/^.*@XREMOTE_[^:]*: *\(.*\)$/\1/' <<<"${line}")
176
177         case "${label}" in
178
179             EXEC)
180                 check_remote_is_defined
181                 [[ "${REMOTE_EXEC}" ]] && (exit "Remote executable already defined!" >&2 && exit 1)
182                 REMOTE_EXEC="${value}"
183                 ;;
184
185             PRE)
186                 check_remote_is_defined
187                 echo "xremote: ${value}"
188                 ssh < /dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && ${value}"
189                 ;;
190
191             SEND)
192                 check_remote_is_defined
193                 echo "xremote: -- sending files --------------------------------------------"
194                 tar ch ${value} | ssh "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && tar mxv"
195                 ;;
196
197             HOST)
198                 [[ "${REMOTE_DIR}" ]] && (exit "Remote host already defined!" >&2 && exit 1)
199                 REMOTE_HOST="${ARG_HOST}" # Host given in argument has priority
200                 [[ "${REMOTE_HOST}" ]] || REMOTE_HOST="${value}"
201                 [[ "${REMOTE_HOST}" ]] || REMOTE_HOST="${XREMOTE_HOST}"
202                 [[ "${REMOTE_HOST}" ]] || (echo "xremote: No remote host specified." >&2 && exit 1)
203                 if [[ "${ARG_DIR}" ]]
204                 then
205                     ssh </dev/null "${REMOTE_HOST}" "mkdir -p \"${ARG_DIR}\""
206                     REMOTE_DIR="${ARG_DIR}"
207                 else
208                     REMOTE_DIR="$(ssh </dev/null "${REMOTE_HOST}" mktemp -d /tmp/xremote_\$\(whoami\)_from_"$(hostname)_$(date +%Y%m%d_%H%M%S)".XXXXXX)"
209                 fi
210                 echo "xremote: target is ${REMOTE_HOST}"
211                 ;;
212         esac
213     fi
214
215 done < "${main_config}"
216
217 ######################################################################
218
219 check_remote_is_defined
220
221 tar c "${main}" | ssh "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && tar mx"
222
223 if [[ "${NO_RUN}" ]]
224 then
225     echo "xremote: everything has been set up in ${REMOTE_HOST}:${ARG_DIR}"
226     exit 0
227 fi
228
229 echo "xremote: -- running the executable -----------------------------------"
230
231 if [[ "${REMOTE_EXEC}" ]]
232 then
233     REMOTE_COMMAND="${REMOTE_EXEC} ${main}"
234 else
235     REMOTE_COMMAND="./${main}"
236 fi
237
238 ######################################################################
239
240 # I find this slightly ugly ...
241
242 for s in "$@"
243 do
244   quoted_args="${quoted_args} \"${s}\""
245 done
246
247 ssh </dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && ${REMOTE_COMMAND} ${quoted_args}"
248
249 ######################################################################
250
251 # Disable globbing to keep wildcards for the remote side
252
253 echo "xremote: -- retrieving files -----------------------------------------"
254
255 set -f
256
257 if [[ "${ARG_RESULT_DIR}" ]]
258 then
259     RESULT_DIR="${ARG_RESULT_DIR}"
260 else
261     RESULT_DIR="."
262 fi
263
264 [[ "${ARG_RESULT_DIR}" ]] && mkdir -p "${ARG_RESULT_DIR}"
265
266 while read line
267 do
268     if [[ "${line}" =~ '@XREMOTE' ]]
269     then
270         label=$(sed -e 's/^.*@XREMOTE_\([^:]*\):.*$/\1/' <<<"${line}")
271         value=$(sed -e 's/^.*@XREMOTE_[^:]*: *\(.*\)$/\1/' <<<"${line}")
272         case "${label}" in
273             POST)
274                 check_remote_is_defined
275                 echo "xremote: ${value}"
276                 ssh < /dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && ${value}"
277                 ;;
278
279             GET)
280                 check_remote_is_defined
281                 ssh </dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && tar 2>/dev/null c ${value}" | tar mxv -C "${RESULT_DIR}"
282                 ;;
283         esac
284     fi
285
286 done < "${main_config}"
287
288 set +f
289
290 echo "xremote: -- finished -------------------------------------------------"
291
292 ######################################################################