sauce 0.12.9 → 0.12.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|