pec2 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,108 @@
1
+ # Copyright (c) 2009-2012, Andrew McNabb
2
+ # Copyright (c) 2003-2008, Brent N. Chun
3
+
4
+ import fcntl
5
+ import string
6
+ import sys
7
+
8
+ HOST_FORMAT = 'Host format is [user@]host[:port] [user]'
9
+
10
+
11
+ def read_host_files(paths, default_user=None, default_port=None):
12
+ """Reads the given host files.
13
+
14
+ Returns a list of (host, port, user) triples.
15
+ """
16
+ hosts = []
17
+ if paths:
18
+ for path in paths:
19
+ hosts.extend(read_host_file(path, default_user=default_user))
20
+ return hosts
21
+
22
+
23
+ def read_host_file(path, default_user=None, default_port=None):
24
+ """Reads the given host file.
25
+
26
+ Lines are of the form: host[:port] [login].
27
+ Returns a list of (host, port, user) triples.
28
+ """
29
+ lines = []
30
+ f = open(path)
31
+ for line in f:
32
+ lines.append(line.strip())
33
+ f.close()
34
+
35
+ hosts = []
36
+ for line in lines:
37
+ # Skip blank lines or lines starting with #
38
+ line = line.strip()
39
+ if not line or line.startswith('#'):
40
+ continue
41
+ host, port, user = parse_host_entry(line, default_user, default_port)
42
+ if host:
43
+ hosts.append((host, port, user))
44
+ return hosts
45
+
46
+
47
+ # TODO: deprecate the second host field and standardize on the
48
+ # [user@]host[:port] format.
49
+ def parse_host_entry(line, default_user, default_port):
50
+ """Parses a single host entry.
51
+
52
+ This may take either the of the form [user@]host[:port] or
53
+ host[:port][ user].
54
+
55
+ Returns a (host, port, user) triple.
56
+ """
57
+ fields = line.split()
58
+ if len(fields) > 2:
59
+ sys.stderr.write('Bad line: "%s". Format should be'
60
+ ' [user@]host[:port] [user]\n' % line)
61
+ return None, None, None
62
+ host_field = fields[0]
63
+ host, port, user = parse_host(host_field, default_port=default_port)
64
+ if len(fields) == 2:
65
+ if user is None:
66
+ user = fields[1]
67
+ else:
68
+ sys.stderr.write('User specified twice in line: "%s"\n' % line)
69
+ return None, None, None
70
+ if user is None:
71
+ user = default_user
72
+ return host, port, user
73
+
74
+
75
+ def parse_host_string(host_string, default_user=None, default_port=None):
76
+ """Parses a whitespace-delimited string of "[user@]host[:port]" entries.
77
+
78
+ Returns a list of (host, port, user) triples.
79
+ """
80
+ hosts = []
81
+ entries = host_string.split()
82
+ for entry in entries:
83
+ hosts.append(parse_host(entry, default_user, default_port))
84
+ return hosts
85
+
86
+
87
+ def parse_host(host, default_user=None, default_port=None):
88
+ """Parses host entries of the form "[user@]host[:port]".
89
+
90
+ Returns a (host, port, user) triple.
91
+ """
92
+ # TODO: when we stop supporting Python 2.4, switch to using str.partition.
93
+ user = default_user
94
+ port = default_port
95
+ if '@' in host:
96
+ user, host = host.split('@', 1)
97
+ if ':' in host:
98
+ host, port = host.rsplit(':', 1)
99
+ return (host, port, user)
100
+
101
+
102
+ def set_cloexec(filelike):
103
+ """Sets the underlying filedescriptor to automatically close on exec.
104
+
105
+ If set_cloexec is called for all open files, then subprocess.Popen does
106
+ not require the close_fds option.
107
+ """
108
+ fcntl.fcntl(filelike.fileno(), fcntl.FD_CLOEXEC, 1)
@@ -0,0 +1,288 @@
1
+ # Copyright (c) 2009-2012, Andrew McNabb
2
+
3
+ from errno import EINTR
4
+ from subprocess import Popen, PIPE
5
+ import os
6
+ import signal
7
+ import sys
8
+ import time
9
+ import traceback
10
+
11
+ from psshlib import askpass_client
12
+ from psshlib import color
13
+
14
+ BUFFER_SIZE = 1 << 16
15
+
16
+ try:
17
+ bytes
18
+ except NameError:
19
+ bytes = str
20
+
21
+
22
+ class Task(object):
23
+ """Starts a process and manages its input and output.
24
+
25
+ Upon completion, the `exitstatus` attribute is set to the exit status
26
+ of the process.
27
+ """
28
+ def __init__(self, host, port, user, cmd, opts, stdin=None):
29
+ self.exitstatus = None
30
+
31
+ self.host = host
32
+ self.pretty_host = host
33
+ self.port = port
34
+ self.cmd = cmd
35
+
36
+ if user != opts.user:
37
+ self.pretty_host = '@'.join((user, self.pretty_host))
38
+ if port:
39
+ self.pretty_host = ':'.join((self.pretty_host, port))
40
+
41
+ self.proc = None
42
+ self.writer = None
43
+ self.timestamp = None
44
+ self.failures = []
45
+ self.killed = False
46
+ self.inputbuffer = stdin
47
+ self.byteswritten = 0
48
+ self.outputbuffer = bytes()
49
+ self.errorbuffer = bytes()
50
+
51
+ self.stdin = None
52
+ self.stdout = None
53
+ self.stderr = None
54
+ self.outfile = None
55
+ self.errfile = None
56
+
57
+ # Set options.
58
+ self.verbose = opts.verbose
59
+ try:
60
+ self.print_out = bool(opts.print_out)
61
+ except AttributeError:
62
+ self.print_out = False
63
+ try:
64
+ self.inline = bool(opts.inline)
65
+ except AttributeError:
66
+ self.inline = False
67
+ try:
68
+ self.inline_stdout = bool(opts.inline_stdout)
69
+ except AttributeError:
70
+ self.inline_stdout = False
71
+
72
+ def start(self, nodenum, iomap, writer, askpass_socket=None):
73
+ """Starts the process and registers files with the IOMap."""
74
+ self.writer = writer
75
+
76
+ if writer:
77
+ self.outfile, self.errfile = writer.open_files(self.pretty_host)
78
+
79
+ # Set up the environment.
80
+ environ = dict(os.environ)
81
+ environ['PSSH_NODENUM'] = str(nodenum)
82
+ environ['PSSH_HOST'] = self.host
83
+ # Disable the GNOME pop-up password dialog and allow ssh to use
84
+ # askpass.py to get a provided password. If the module file is
85
+ # askpass.pyc, we replace the extension.
86
+ environ['SSH_ASKPASS'] = askpass_client.executable_path()
87
+ if askpass_socket:
88
+ environ['PSSH_ASKPASS_SOCKET'] = askpass_socket
89
+ if self.verbose:
90
+ environ['PSSH_ASKPASS_VERBOSE'] = '1'
91
+ # Work around a mis-feature in ssh where it won't call SSH_ASKPASS
92
+ # if DISPLAY is unset.
93
+ if 'DISPLAY' not in environ:
94
+ environ['DISPLAY'] = 'pssh-gibberish'
95
+
96
+ # Create the subprocess. Since we carefully call set_cloexec() on
97
+ # all open files, we specify close_fds=False.
98
+ self.proc = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE,
99
+ close_fds=False, preexec_fn=os.setsid, env=environ)
100
+ self.timestamp = time.time()
101
+ if self.inputbuffer:
102
+ self.stdin = self.proc.stdin
103
+ iomap.register_write(self.stdin.fileno(), self.handle_stdin)
104
+ else:
105
+ self.proc.stdin.close()
106
+ self.stdout = self.proc.stdout
107
+ iomap.register_read(self.stdout.fileno(), self.handle_stdout)
108
+ self.stderr = self.proc.stderr
109
+ iomap.register_read(self.stderr.fileno(), self.handle_stderr)
110
+
111
+ def _kill(self):
112
+ """Signals the process to terminate."""
113
+ if self.proc:
114
+ try:
115
+ os.kill(-self.proc.pid, signal.SIGKILL)
116
+ except OSError:
117
+ # If the kill fails, then just assume the process is dead.
118
+ pass
119
+ self.killed = True
120
+
121
+ def timedout(self):
122
+ """Kills the process and registers a timeout error."""
123
+ if not self.killed:
124
+ self._kill()
125
+ self.failures.append('Timed out')
126
+
127
+ def interrupted(self):
128
+ """Kills the process and registers an keyboard interrupt error."""
129
+ if not self.killed:
130
+ self._kill()
131
+ self.failures.append('Interrupted')
132
+
133
+ def cancel(self):
134
+ """Stops a task that has not started."""
135
+ self.failures.append('Cancelled')
136
+
137
+ def elapsed(self):
138
+ """Finds the time in seconds since the process was started."""
139
+ return time.time() - self.timestamp
140
+
141
+ def running(self):
142
+ """Finds if the process has terminated and saves the return code."""
143
+ if self.stdin or self.stdout or self.stderr:
144
+ return True
145
+ if self.proc:
146
+ self.exitstatus = self.proc.poll()
147
+ if self.exitstatus is None:
148
+ if self.killed:
149
+ # Set the exitstatus to what it would be if we waited.
150
+ self.exitstatus = -signal.SIGKILL
151
+ return False
152
+ else:
153
+ return True
154
+ else:
155
+ if self.exitstatus < 0:
156
+ message = 'Killed by signal %s' % (-self.exitstatus)
157
+ self.failures.append(message)
158
+ elif self.exitstatus > 0:
159
+ message = 'Exited with error code %s' % self.exitstatus
160
+ self.failures.append(message)
161
+ self.proc = None
162
+ return False
163
+
164
+ def handle_stdin(self, fd, iomap):
165
+ """Called when the process's standard input is ready for writing."""
166
+ try:
167
+ start = self.byteswritten
168
+ if start < len(self.inputbuffer):
169
+ chunk = self.inputbuffer[start:start+BUFFER_SIZE]
170
+ self.byteswritten = start + os.write(fd, chunk)
171
+ else:
172
+ self.close_stdin(iomap)
173
+ except (OSError, IOError):
174
+ _, e, _ = sys.exc_info()
175
+ if e.errno != EINTR:
176
+ self.close_stdin(iomap)
177
+ self.log_exception(e)
178
+
179
+ def close_stdin(self, iomap):
180
+ if self.stdin:
181
+ iomap.unregister(self.stdin.fileno())
182
+ self.stdin.close()
183
+ self.stdin = None
184
+
185
+ def handle_stdout(self, fd, iomap):
186
+ """Called when the process's standard output is ready for reading."""
187
+ try:
188
+ buf = os.read(fd, BUFFER_SIZE)
189
+ if buf:
190
+ if self.inline or self.inline_stdout:
191
+ self.outputbuffer += buf
192
+ if self.outfile:
193
+ self.writer.write(self.outfile, buf)
194
+ if self.print_out:
195
+ sys.stdout.write('%s: %s' % (self.host, buf))
196
+ if buf[-1] != '\n':
197
+ sys.stdout.write('\n')
198
+ else:
199
+ self.close_stdout(iomap)
200
+ except (OSError, IOError):
201
+ _, e, _ = sys.exc_info()
202
+ if e.errno != EINTR:
203
+ self.close_stdout(iomap)
204
+ self.log_exception(e)
205
+
206
+ def close_stdout(self, iomap):
207
+ if self.stdout:
208
+ iomap.unregister(self.stdout.fileno())
209
+ self.stdout.close()
210
+ self.stdout = None
211
+ if self.outfile:
212
+ self.writer.close(self.outfile)
213
+ self.outfile = None
214
+
215
+ def handle_stderr(self, fd, iomap):
216
+ """Called when the process's standard error is ready for reading."""
217
+ try:
218
+ buf = os.read(fd, BUFFER_SIZE)
219
+ if buf:
220
+ if self.inline:
221
+ self.errorbuffer += buf
222
+ if self.errfile:
223
+ self.writer.write(self.errfile, buf)
224
+ else:
225
+ self.close_stderr(iomap)
226
+ except (OSError, IOError):
227
+ _, e, _ = sys.exc_info()
228
+ if e.errno != EINTR:
229
+ self.close_stderr(iomap)
230
+ self.log_exception(e)
231
+
232
+ def close_stderr(self, iomap):
233
+ if self.stderr:
234
+ iomap.unregister(self.stderr.fileno())
235
+ self.stderr.close()
236
+ self.stderr = None
237
+ if self.errfile:
238
+ self.writer.close(self.errfile)
239
+ self.errfile = None
240
+
241
+ def log_exception(self, e):
242
+ """Saves a record of the most recent exception for error reporting."""
243
+ if self.verbose:
244
+ exc_type, exc_value, exc_traceback = sys.exc_info()
245
+ exc = ("Exception: %s, %s, %s" %
246
+ (exc_type, exc_value, traceback.format_tb(exc_traceback)))
247
+ else:
248
+ exc = str(e)
249
+ self.failures.append(exc)
250
+
251
+ def report(self, n):
252
+ """Pretty prints a status report after the Task completes."""
253
+ error = ', '.join(self.failures)
254
+ tstamp = time.asctime().split()[3] # Current time
255
+ if color.has_colors(sys.stdout):
256
+ progress = color.c("[%s]" % color.B(n))
257
+ success = color.g("[%s]" % color.B("SUCCESS"))
258
+ failure = color.r("[%s]" % color.B("FAILURE"))
259
+ stderr = color.r("Stderr: ")
260
+ error = color.r(color.B(error))
261
+ else:
262
+ progress = "[%s]" % n
263
+ success = "[SUCCESS]"
264
+ failure = "[FAILURE]"
265
+ stderr = "Stderr: "
266
+ host = self.pretty_host
267
+ if self.failures:
268
+ print(' '.join((progress, tstamp, failure, host, error)))
269
+ else:
270
+ print(' '.join((progress, tstamp, success, host)))
271
+ # NOTE: The extra flushes are to ensure that the data is output in
272
+ # the correct order with the C implementation of io.
273
+ if self.outputbuffer:
274
+ sys.stdout.flush()
275
+ try:
276
+ sys.stdout.buffer.write(self.outputbuffer)
277
+ sys.stdout.flush()
278
+ except AttributeError:
279
+ sys.stdout.write(self.outputbuffer)
280
+ if self.errorbuffer:
281
+ sys.stdout.write(stderr)
282
+ # Flush the TextIOWrapper before writing to the binary buffer.
283
+ sys.stdout.flush()
284
+ try:
285
+ sys.stdout.buffer.write(self.errorbuffer)
286
+ except AttributeError:
287
+ sys.stdout.write(self.errorbuffer)
288
+
@@ -0,0 +1 @@
1
+ VERSION = '2.3.1'
data/lib/pec2/cli.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require "thor"
2
2
  require "tempfile"
3
+ require "logger"
4
+ require 'shellwords'
3
5
 
4
6
  module Pec2
5
7
  class CLI < Thor
@@ -11,7 +13,8 @@ module Pec2
11
13
  super(args, options, config)
12
14
  @global_options = config[:shell].base.options
13
15
  @core = Core.new
14
- @pssh_path = `which pssh`.strip
16
+ @pssh_path = File.expand_path('../../../exe/bin/pssh', __FILE__)
17
+ @logger = Logger.new(STDOUT)
15
18
  end
16
19
 
17
20
  desc 'search_tag', 'search tag'
@@ -29,7 +32,8 @@ module Pec2
29
32
  end
30
33
 
31
34
  if addresses.empty?
32
- raise "no host."
35
+ @logger.error(%Q{no host tag #{options[:tag]}.})
36
+ raise
33
37
  end
34
38
 
35
39
  File.write(f.path, addresses.join("\n"))
@@ -52,9 +56,9 @@ module Pec2
52
56
  end
53
57
 
54
58
  if options[:sudo_password]
55
- cmd = %Q{(echo #{options[:sudo_password]}) | #{cmd} -I '#{options[:command]}'}
59
+ cmd = %Q{(echo #{options[:sudo_password]}) | #{cmd} -I #{Shellwords.escape(options[:command])}}
56
60
  else
57
- cmd = %Q{#{cmd} -i '#{options[:command]}'}
61
+ cmd = %Q{#{cmd} -i #{Shellwords.escape(options[:command])}}
58
62
  end
59
63
  system(cmd)
60
64
  end
data/lib/pec2/core.rb CHANGED
@@ -6,7 +6,7 @@ require 'json'
6
6
  require 'timeout'
7
7
 
8
8
  class Pec2Mash < ::Hashie::Mash
9
- disable_warnings
9
+ disable_warnings if respond_to?(:disable_warnings)
10
10
  end
11
11
 
12
12
  module Pec2