sauce 0.12.9 → 0.12.10
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/support/sauce_connect +48 -37
- metadata +4 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.12.
|
1
|
+
0.12.10
|
data/support/sauce_connect
CHANGED
@@ -27,6 +27,7 @@ import time
|
|
27
27
|
import platform
|
28
28
|
import tempfile
|
29
29
|
import string
|
30
|
+
from base64 import b64encode
|
30
31
|
from collections import defaultdict
|
31
32
|
from contextlib import closing
|
32
33
|
from functools import wraps
|
@@ -37,7 +38,7 @@ except ImportError:
|
|
37
38
|
import simplejson as json # Python 2.5 dependency
|
38
39
|
|
39
40
|
NAME = "sauce_connect"
|
40
|
-
RELEASE =
|
41
|
+
RELEASE = 25
|
41
42
|
DISPLAY_VERSION = "%s release %s" % (NAME, RELEASE)
|
42
43
|
PRODUCT_NAME = u"Sauce Connect"
|
43
44
|
VERSIONS_URL = "http://saucelabs.com/versions.json"
|
@@ -59,6 +60,12 @@ is_openbsd = platform.system().lower() == "openbsd"
|
|
59
60
|
logger = logging.getLogger(NAME)
|
60
61
|
|
61
62
|
|
63
|
+
class DeleteRequest(urllib2.Request):
|
64
|
+
|
65
|
+
def get_method(self):
|
66
|
+
return "DELETE"
|
67
|
+
|
68
|
+
|
62
69
|
class HTTPResponseError(Exception):
|
63
70
|
|
64
71
|
def __init__(self, msg):
|
@@ -84,18 +91,19 @@ class TunnelMachine(object):
|
|
84
91
|
|
85
92
|
_host_search = re.compile("//([^/]+)").search
|
86
93
|
|
87
|
-
def __init__(self, rest_url, user, password, domains, metadata=None):
|
94
|
+
def __init__(self, rest_url, user, password, domains, ssh_port, metadata=None):
|
88
95
|
self.user = user
|
89
96
|
self.password = password
|
90
97
|
self.domains = set(domains)
|
98
|
+
self.ssh_port = ssh_port
|
91
99
|
self.metadata = metadata or dict()
|
92
100
|
|
93
101
|
self.reverse_ssh = None
|
94
102
|
self.is_shutdown = False
|
95
103
|
self.base_url = "%(rest_url)s/%(user)s/tunnels" % locals()
|
96
104
|
self.rest_host = self._host_search(rest_url).group(1)
|
97
|
-
self.basic_auth_header = {"Authorization": "Basic %s"
|
98
|
-
|
105
|
+
self.basic_auth_header = {"Authorization": "Basic %s"
|
106
|
+
% b64encode("%s:%s" % (user, password))}
|
99
107
|
|
100
108
|
self._set_urlopen(user, password)
|
101
109
|
|
@@ -111,9 +119,7 @@ class TunnelMachine(object):
|
|
111
119
|
"help@saucelabs.com.")
|
112
120
|
|
113
121
|
def _set_urlopen(self, user, password):
|
114
|
-
# always send Basic Auth header
|
115
|
-
# NOTE: we directly construct the header because it is more reliable
|
116
|
-
# and more efficient than HTTPBasicAuthHandler and we always need it
|
122
|
+
# always send Basic Auth header (HTTPBasicAuthHandler was unreliable)
|
117
123
|
opener = urllib2.build_opener()
|
118
124
|
opener.addheaders = self.basic_auth_header.items()
|
119
125
|
self.urlopen = opener.open
|
@@ -155,21 +161,6 @@ class TunnelMachine(object):
|
|
155
161
|
raise HTTPResponseError(resp.msg)
|
156
162
|
return json.loads(resp.read())
|
157
163
|
|
158
|
-
@_retry_rest_api
|
159
|
-
def _get_delete_doc(self, url):
|
160
|
-
# urllib2 doesn support the DELETE method (lame), so we build our own
|
161
|
-
if self.base_url.startswith("https"):
|
162
|
-
make_conn = httplib.HTTPSConnection
|
163
|
-
else:
|
164
|
-
make_conn = httplib.HTTPConnection
|
165
|
-
with closing(make_conn(self.rest_host)) as conn:
|
166
|
-
conn.request(method="DELETE", url=url,
|
167
|
-
headers=self.basic_auth_header)
|
168
|
-
resp = conn.getresponse()
|
169
|
-
if resp.reason != "OK":
|
170
|
-
raise HTTPResponseError(resp.reason)
|
171
|
-
return json.loads(resp.read())
|
172
|
-
|
173
164
|
def _provision_tunnel(self):
|
174
165
|
# Shutdown any tunnel using a requested domain
|
175
166
|
kill_list = set()
|
@@ -186,7 +177,7 @@ class TunnelMachine(object):
|
|
186
177
|
logger.debug(
|
187
178
|
"Shutting down old tunnel host: %s" % tunnel_id)
|
188
179
|
url = "%s/%s" % (self.base_url, tunnel_id)
|
189
|
-
doc = self.
|
180
|
+
doc = self._get_doc(DeleteRequest(url=url))
|
190
181
|
if not doc.get('ok'):
|
191
182
|
logger.warning("Old tunnel host failed to shutdown?")
|
192
183
|
continue
|
@@ -201,7 +192,8 @@ class TunnelMachine(object):
|
|
201
192
|
# Request a tunnel machine
|
202
193
|
headers = {"Content-Type": "application/json"}
|
203
194
|
data = json.dumps(dict(DomainNames=list(self.domains),
|
204
|
-
Metadata=self.metadata
|
195
|
+
Metadata=self.metadata,
|
196
|
+
SSHPort=self.ssh_port))
|
205
197
|
req = urllib2.Request(url=self.base_url, headers=headers, data=data)
|
206
198
|
doc = self._get_doc(req)
|
207
199
|
if doc.get('error'):
|
@@ -243,9 +235,10 @@ class TunnelMachine(object):
|
|
243
235
|
logger.debug("Tunnel host ID: %s" % self.id)
|
244
236
|
|
245
237
|
try:
|
246
|
-
doc = self.
|
247
|
-
except TunnelMachineError:
|
238
|
+
doc = self._get_doc(DeleteRequest(url=self.url))
|
239
|
+
except TunnelMachineError, e:
|
248
240
|
logger.warning("Unable to shut down tunnel host")
|
241
|
+
logger.debug("Shut down failed because: %s", str(e))
|
249
242
|
self.is_shutdown = True # fuhgeddaboudit
|
250
243
|
return
|
251
244
|
assert doc.get('ok')
|
@@ -350,13 +343,14 @@ class ReverseSSHError(Exception):
|
|
350
343
|
|
351
344
|
class ReverseSSH(object):
|
352
345
|
|
353
|
-
def __init__(self, tunnel, host, ports, tunnel_ports,
|
346
|
+
def __init__(self, tunnel, host, ports, tunnel_ports, ssh_port,
|
354
347
|
use_ssh_config=False, debug=False):
|
355
348
|
self.tunnel = tunnel
|
356
349
|
self.host = host
|
357
350
|
self.ports = ports
|
358
351
|
self.tunnel_ports = tunnel_ports
|
359
352
|
self.use_ssh_config = use_ssh_config
|
353
|
+
self.ssh_port = ssh_port
|
360
354
|
self.debug = debug
|
361
355
|
|
362
356
|
self.proc = None
|
@@ -388,8 +382,8 @@ class ReverseSSH(object):
|
|
388
382
|
def get_plink_command(self):
|
389
383
|
"""Return the Windows SSH command."""
|
390
384
|
verbosity = "-v" if self.debug else ""
|
391
|
-
return ("plink\plink %s -l %s -pw %s -N %s %s"
|
392
|
-
% (verbosity, self.tunnel.user, self.tunnel.password,
|
385
|
+
return ("plink\plink %s -P %s -l %s -pw %s -N %s %s"
|
386
|
+
% (verbosity, self.ssh_port, self.tunnel.user, self.tunnel.password,
|
393
387
|
self._dash_Rs, self.tunnel.host))
|
394
388
|
|
395
389
|
def get_expect_script(self):
|
@@ -402,8 +396,8 @@ class ReverseSSH(object):
|
|
402
396
|
config_file = "" if self.use_ssh_config else "-F /dev/null"
|
403
397
|
host_ip = socket.gethostbyname(self.tunnel.host)
|
404
398
|
script = (
|
405
|
-
"spawn ssh %s %s -p
|
406
|
-
% (verbosity, config_file, self.tunnel.user,
|
399
|
+
"spawn ssh %s %s -p %s -l %s -o ServerAliveInterval=%s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -N %s %s;"
|
400
|
+
% (verbosity, config_file, self.ssh_port, self.tunnel.user,
|
407
401
|
HEALTH_CHECK_INTERVAL, self._dash_Rs, self.tunnel.host) +
|
408
402
|
"expect *password:;send -- %s\\r;" % self.tunnel.password +
|
409
403
|
"expect -timeout -1 timeout")
|
@@ -433,9 +427,9 @@ class ReverseSSH(object):
|
|
433
427
|
|
434
428
|
# setup recurring healthchecks
|
435
429
|
forwarded_health = HealthChecker(self.host, self.ports)
|
436
|
-
tunnel_health = HealthChecker(host=self.tunnel.host, ports=[
|
430
|
+
tunnel_health = HealthChecker(host=self.tunnel.host, ports=[self.ssh_port],
|
437
431
|
fail_msg="!! Your tests may fail because your network can not get "
|
438
|
-
"to the tunnel host (%s:%d)." % (self.tunnel.host,
|
432
|
+
"to the tunnel host (%s:%d)." % (self.tunnel.host, self.ssh_port))
|
439
433
|
|
440
434
|
start_time = int(time.time())
|
441
435
|
while self.proc.poll() is None:
|
@@ -719,6 +713,8 @@ Performance tip:
|
|
719
713
|
help=optparse.SUPPRESS_HELP)
|
720
714
|
og.add_option("--allow-unclean-exit", action="store_true", default=False,
|
721
715
|
help=optparse.SUPPRESS_HELP)
|
716
|
+
og.add_option("--ssh-port", default=22, type="int",
|
717
|
+
help=optparse.SUPPRESS_HELP)
|
722
718
|
op.add_option_group(og)
|
723
719
|
|
724
720
|
og = optparse.OptionGroup(op, "Script debugging options")
|
@@ -730,6 +726,20 @@ Performance tip:
|
|
730
726
|
|
731
727
|
(options, args) = op.parse_args()
|
732
728
|
|
729
|
+
# check ports are numbers
|
730
|
+
try:
|
731
|
+
map(int, options.ports)
|
732
|
+
map(int, options.tunnel_ports)
|
733
|
+
except ValueError:
|
734
|
+
sys.stderr.write("Error: Ports must be integers\n\n")
|
735
|
+
print "Help with options -t and -p:"
|
736
|
+
print " All ports must be integers. You used:"
|
737
|
+
if options.ports:
|
738
|
+
print " -p", " -p ".join(options.ports)
|
739
|
+
if options.tunnel_ports:
|
740
|
+
print " -t", " -t ".join(options.tunnel_ports)
|
741
|
+
raise SystemExit(1)
|
742
|
+
|
733
743
|
# default to 80 and default to matching host ports with tunnel ports
|
734
744
|
if not options.ports and not options.tunnel_ports:
|
735
745
|
options.ports = ["80"]
|
@@ -830,6 +840,7 @@ def run(options, dependency_versions=None):
|
|
830
840
|
print "| Contact us: http://saucelabs.com/forums |"
|
831
841
|
print "-----------------------------------------------------"
|
832
842
|
logger.info("/ Starting \\")
|
843
|
+
logger.info('Please wait for "You may start your tests" to start your tests.')
|
833
844
|
logger.info("%s" % DISPLAY_VERSION)
|
834
845
|
check_version()
|
835
846
|
|
@@ -861,7 +872,8 @@ def run(options, dependency_versions=None):
|
|
861
872
|
for attempt in xrange(1, RETRY_BOOT_MAX + 1):
|
862
873
|
try:
|
863
874
|
tunnel = TunnelMachine(options.rest_url, options.user,
|
864
|
-
options.api_key, options.domains,
|
875
|
+
options.api_key, options.domains,
|
876
|
+
options.ssh_port, metadata)
|
865
877
|
except TunnelMachineError, e:
|
866
878
|
logger.error(e)
|
867
879
|
peace_out(returncode=1) # exits
|
@@ -881,6 +893,7 @@ def run(options, dependency_versions=None):
|
|
881
893
|
|
882
894
|
ssh = ReverseSSH(tunnel=tunnel, host=options.host,
|
883
895
|
ports=options.ports, tunnel_ports=options.tunnel_ports,
|
896
|
+
ssh_port=options.ssh_port,
|
884
897
|
use_ssh_config=options.use_ssh_config,
|
885
898
|
debug=options.debug_ssh)
|
886
899
|
try:
|
@@ -891,9 +904,7 @@ def run(options, dependency_versions=None):
|
|
891
904
|
|
892
905
|
|
893
906
|
def main():
|
894
|
-
|
895
|
-
pyver = float("%s.%s" % tuple(platform.python_version().split('.')[:2]))
|
896
|
-
if pyver < 2.5:
|
907
|
+
if map(int, platform.python_version_tuple ()) < [2, 5]:
|
897
908
|
print "%s requires Python 2.5 (2006) or newer." % PRODUCT_NAME
|
898
909
|
raise SystemExit(1)
|
899
910
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sauce
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 59
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 12
|
9
|
-
-
|
10
|
-
version: 0.12.
|
9
|
+
- 10
|
10
|
+
version: 0.12.10
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Sean Grove
|
@@ -17,7 +17,7 @@ autorequire:
|
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
19
|
|
20
|
-
date: 2011-01-
|
20
|
+
date: 2011-01-21 00:00:00 -03:00
|
21
21
|
default_executable: sauce
|
22
22
|
dependencies:
|
23
23
|
- !ruby/object:Gem::Dependency
|