7ebc0d4f0905de5aca5d4dba8100336a5acf5c0b
[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>] [-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 no argument is provided to @XREMOTE_HOST, and the -h option is
73   not specified, the environment variable \$XREMOTE_HOST is used
74   instead
75
76   Contact <francois@fleuret.org> for comments.
77
78 EOF
79     return 0
80 }
81
82 function cleanup_remote_tmp () {
83     if [[ "${REMOTE_HOST}" ]] && [[ "${REMOTE_DIR}" ]]
84     then
85         if [[ "${ARG_DIR}" ]]
86         then
87             echo "xremote: Keeping remote workdir."
88         else
89             echo "xremote: Cleaning up temporary remote workdir."
90             ssh "${REMOTE_HOST}" rm -rf "${REMOTE_DIR}"
91         fi
92     fi
93 }
94
95 ######################################################################
96
97 while [[ "$1" =~ ^- ]]
98 do
99     case "$1"
100     in
101         -h)
102             shift
103             ARG_HOST="$1"
104             [[ ${ARG_HOST} ]] || (echo "xremote: Hostname missing." && exit 1)
105             echo "xremote: remote forced to ${ARG_HOST}"
106             ;;
107
108         -d)
109             shift
110             ARG_DIR="$1"
111             [[ ${ARG_DIR} ]] || (echo "xremote: Directory missing." && exit 1)
112             echo "xremote: remote dir set to ${ARG_DIR}"
113             ;;
114
115         -i)
116             NO_RUN=1
117             echo "xremote: no run"
118             ;;
119
120         --help)
121             help
122             exit 0
123             ;;
124
125         *)
126             echo "xremote: Unknown option $1"
127             exit 1
128             ;;
129     esac
130     shift
131 done
132
133 ######################################################################
134
135 [[ "$1" ]] || (echo "xremote: Script name missing" && exit 1)
136
137 [[ -a "$1" ]] || (help && echo >&2 "xremote: Cannot find script \`$1'" && exit 1)
138
139 cd "$(dirname "$1")"
140
141 main="$(basename "$1")"
142 main_config="${main}.xremote"
143
144 if [[ -f "${main_config}" ]]
145 then
146     echo "xremote: found ${main_config}"
147 else
148     main_config="${main}"
149 fi
150
151 shift
152
153 trap cleanup_remote_tmp EXIT
154
155 ######################################################################
156
157 while read line
158 do
159
160     if [[ "${line}" =~ '@XREMOTE' ]]
161     then
162
163         label=$(echo "${line}" | sed -e 's/^.*@XREMOTE_\([^:]*\):.*$/\1/')
164         value=$(echo "${line}" | sed -e 's/^.*@XREMOTE_[^:]*: *\(.*\)$/\1/')
165
166         case "${label}" in
167
168             EXEC)
169                 check_remote_is_defined
170                 [[ "${REMOTE_EXEC}" ]] && (exit "Remote executable already defined!" >&2 && exit 1)
171                 REMOTE_EXEC="${value}"
172                 ;;
173
174             PRE)
175                 check_remote_is_defined
176                 echo "xremote: ${value}"
177                 ssh < /dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && ${value}"
178                 ;;
179
180             SEND)
181                 check_remote_is_defined
182                 echo "xremote: -- sending files --------------------------------------------"
183                 tar ch ${value} | ssh "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && tar mxv"
184                 ;;
185
186             HOST)
187                 [[ "${REMOTE_DIR}" ]] && (exit "Remote host already defined!" >&2 && exit 1)
188                 REMOTE_HOST="${ARG_HOST}" # Host given in argument has priority
189                 [[ "${REMOTE_HOST}" ]] || REMOTE_HOST="${value}"
190                 [[ "${REMOTE_HOST}" ]] || REMOTE_HOST="${XREMOTE_HOST}"
191                 [[ "${REMOTE_HOST}" ]] || (echo "xremote: No remote host specified." >&2 && exit 1)
192                 if [[ "${ARG_DIR}" ]]
193                 then
194                     ssh </dev/null "${REMOTE_HOST}" "mkdir -p \"${ARG_DIR}\""
195                     REMOTE_DIR="${ARG_DIR}"
196                 else
197                     REMOTE_DIR="$(ssh </dev/null "${REMOTE_HOST}" mktemp -d /tmp/xremote_\$\(whoami\)_from_"$(hostname)_$(date +%Y%m%d_%H%M%S)".XXXXXX)"
198                 fi
199                 echo "xremote: target is ${REMOTE_HOST}"
200                 ;;
201         esac
202     fi
203
204 done < "${main_config}"
205
206 ######################################################################
207
208 check_remote_is_defined
209
210 tar c "${main}" | ssh "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && tar mx"
211
212 if [[ "${NO_RUN}" ]]
213 then
214     echo "xremote: everything has been set up in ${REMOTE_HOST}:${ARG_DIR}"
215     exit 0
216 fi
217
218 echo "xremote: -- running the executable -----------------------------------"
219
220 if [[ "${REMOTE_EXEC}" ]]
221 then
222     REMOTE_COMMAND="${REMOTE_EXEC} ${main}"
223 else
224     REMOTE_COMMAND="./${main}"
225 fi
226
227 ######################################################################
228
229 # I find this slightly ugly ...
230
231 for s in "$@"
232 do
233   quoted_args="${quoted_args} \"${s}\""
234 done
235
236 ssh </dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && ${REMOTE_COMMAND} ${quoted_args}"
237
238 ######################################################################
239
240 # Disable globbing to keep wildcards for the remote side
241
242 echo "xremote: -- retrieving files -----------------------------------------"
243
244 set -f
245
246 while read line
247 do
248     if [[ "${line}" =~ '@XREMOTE' ]]
249     then
250         label=$(echo "${line}" | sed -e 's/^.*@XREMOTE_\([^:]*\):.*$/\1/')
251         value=$(echo "${line}" | sed -e 's/^.*@XREMOTE_[^:]*: *\(.*\)$/\1/')
252         case "${label}" in
253             POST)
254                 check_remote_is_defined
255                 echo "xremote: ${value}"
256                 ssh < /dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && ${value}"
257                 ;;
258
259             GET)
260                 check_remote_is_defined
261                 ssh </dev/null "${REMOTE_HOST}" "cd \"${REMOTE_DIR}\" && tar 2>/dev/null c ${value}" | tar mxv
262                 ;;
263         esac
264     fi
265
266 done < "${main_config}"
267
268 set +f
269
270 echo "xremote: -- finished -------------------------------------------------"
271
272 ######################################################################