Skip to content
This repository was archived by the owner on Jan 20, 2020. It is now read-only.

Commit fa9a969

Browse files
author
Feng Honglin
authored
Merge pull request #31 from docker/staging
1.0.10
2 parents e9658aa + 19c5bd6 commit fa9a969

33 files changed

+370
-220
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,7 @@ target/
5858
# IDE
5959
.idea/
6060

61+
# vim
62+
*.swp
63+
6164
venv/

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ The authentication can be configured in the following ways:
2929
export DOCKERCLOUD_USER=username
3030
export DOCKERCLOUD_APIKEY=apikey
3131

32+
## Optional parameters
33+
34+
You may set the reconnection interval (Integer, in seconds) using the variable DOCKERCLOUD_RECONNECTION_INTERVAL:
35+
36+
export DOCKERCLOUD_RECONNECTION_INTERVAL=240
37+
38+
Session uses a socket that may be closed by some peer. To prevent the "Read timed out" issue you should use this option.
39+
40+
Possible values:
41+
42+
* `-1` (by default) means no reconnect (as usually it works)
43+
* `0` means reconnect on each request
44+
* any positive value means that the connection will be reopened if the time diff between last 2 requests is more than that value
45+
3246
## Namespace
3347

3448
To support teams and orgs, you can specify the namespace in the following ways:

dockercloud/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from dockercloud.api.events import Events
2626
from dockercloud.api.nodeaz import AZ
2727

28-
__version__ = '1.0.9'
28+
__version__ = '1.0.10'
2929

3030
dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH')
3131
basic_auth = auth.load_from_file("~/.docker/config.json")
@@ -40,8 +40,14 @@
4040

4141
namespace = os.environ.get('DOCKERCLOUD_NAMESPACE')
4242

43+
# in seconds, if the connection is inactive more than that value it will be recreated
44+
reconnection_interval = int(os.environ.get('DOCKERCLOUD_RECONNECTION_INTERVAL', '-1'))
45+
4346
user_agent = None
4447

48+
# in seconds, make the api call timeout after X seconds, None usually is 15 mins
49+
api_timeout = None
50+
4551
logging.basicConfig()
4652
logger = logging.getLogger("python-dockercloud")
4753

dockercloud/api/action.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class Action(Immutable):
77
subsystem = 'audit'
88
endpoint = "/action"
9-
namespaced = False
9+
is_namespaced = False
1010

1111
@classmethod
1212
def _pk_key(cls):

dockercloud/api/auth.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
HUB_INDEX = "https://index.docker.io/v1/"
1414

15+
1516
def authenticate(username, password):
1617
verify_credential(username, password)
1718
dockercloud.basic_auth = base64.b64encode("%s:%s" % (username, password))
@@ -55,7 +56,8 @@ def load_from_file(f="~/.docker/config.json"):
5556
p = subprocess.Popen([cmd, 'get'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
5657
out = p.communicate(input=HUB_INDEX)[0]
5758
except:
58-
raise dockercloud.AuthError('error getting credentials - err: exec: "%s": executable file not found in $PATH, out: ``' % cmd)
59+
raise dockercloud.AuthError(
60+
'error getting credentials - err: exec: "%s": executable file not found in $PATH, out: ``' % cmd)
5961

6062
try:
6163
credential = json.loads(out)

dockercloud/api/base.py

+50-46
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@
1616
class BasicObject(object):
1717
_api_version = 'v1'
1818

19-
def __init__(self, **kwargs):
20-
pass
21-
2219

2320
class Restful(BasicObject):
24-
_detail_uri = None
25-
namespaced = True
21+
is_namespaced = True
2622

27-
def __init__(self, **kwargs):
23+
def __init__(self, namespace="", **kwargs):
2824
"""Simply reflect all the values in kwargs"""
2925
for k, v in list(kwargs.items()):
3026
setattr(self, k, v)
27+
if self.is_namespaced and namespace:
28+
self._namespace = namespace
29+
else:
30+
self._namespace = dockercloud.namespace
31+
self._resource_uri = ""
3132

3233
def __addchanges__(self, name):
3334
changed_attrs = self.__getchanges__()
@@ -38,7 +39,7 @@ def __addchanges__(self, name):
3839
def __setattr__(self, name, value):
3940
"""Keeps track of what attributes have been set"""
4041
current_value = getattr(self, name, None)
41-
if value != current_value:
42+
if value != current_value and not name.startswith("_"):
4243
self.__addchanges__(name)
4344
super(Restful, self).__setattr__(name, value)
4445

@@ -53,17 +54,10 @@ def __setchanges__(self, val):
5354

5455
def _loaddict(self, dict):
5556
"""Internal. Sets the model attributes to the dictionary values passed"""
56-
endpoint = getattr(self, 'endpoint', None)
57-
subsystem = getattr(self, 'subsystem', None)
58-
assert endpoint, "Endpoint not specified for %s" % self.__class__.__name__
59-
assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__
6057
for k, v in list(dict.items()):
6158
setattr(self, k, v)
62-
if self.namespaced and dockercloud.namespace:
63-
self._detail_uri = "/".join(["api", subsystem, self._api_version, dockercloud.namespace,
64-
endpoint.strip("/"), self.pk])
65-
else:
66-
self._detail_uri = "/".join(["api", subsystem, self._api_version, endpoint.strip("/"), self.pk])
59+
60+
self._resource_uri = getattr(self, "resource_uri", None)
6761
self.__setchanges__([])
6862

6963
@property
@@ -93,9 +87,9 @@ def is_dirty(self):
9387
def _perform_action(self, action, params=None, data={}):
9488
"""Internal. Performs the specified action on the object remotely"""
9589
success = False
96-
if not self._detail_uri:
90+
if not self._resource_uri:
9791
raise ApiError("You must save the object before performing this operation")
98-
path = "/".join([self._detail_uri.rstrip("/"), action.lstrip("/")])
92+
path = "/".join([self._resource_uri.rstrip("/"), action.lstrip("/")])
9993
json = send_request("POST", path, params=params, data=data)
10094
if json:
10195
self._loaddict(json)
@@ -104,9 +98,9 @@ def _perform_action(self, action, params=None, data={}):
10498

10599
def _expand_attribute(self, attribute):
106100
"""Internal. Expands the given attribute from remote information"""
107-
if not self._detail_uri:
101+
if not self._resource_uri:
108102
raise ApiError("You must save the object before performing this operation")
109-
path = "/".join([self._detail_uri, attribute])
103+
path = "/".join([self._resource_uri, attribute])
110104
json = send_request("GET", path)
111105
if json:
112106
return json[attribute]
@@ -125,39 +119,43 @@ def get_all_attributes(self):
125119

126120
class Immutable(Restful):
127121
@classmethod
128-
def fetch(cls, pk):
129-
instance = None
122+
def fetch(cls, pk, namespace=""):
130123
endpoint = getattr(cls, 'endpoint', None)
131124
subsystem = getattr(cls, 'subsystem', None)
132125
assert endpoint, "Endpoint not specified for %s" % cls.__name__
133126
assert subsystem, "Subsystem not specified for %s" % cls.__name__
134-
if cls.namespaced and dockercloud.namespace:
135-
detail_uri = "/".join(["api", subsystem, cls._api_version, dockercloud.namespace, endpoint.strip("/"), pk])
127+
128+
if not namespace:
129+
namespace = dockercloud.namespace
130+
if cls.is_namespaced and namespace:
131+
resource_uri = "/".join(["api", subsystem, cls._api_version, namespace, endpoint.strip("/"), pk])
136132
else:
137-
detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/"), pk])
138-
json = send_request('GET', detail_uri)
133+
resource_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/"), pk])
134+
json = send_request('GET', resource_uri)
139135
if json:
140136
instance = cls()
141137
instance._loaddict(json)
142138
return instance
143139

144140
@classmethod
145-
def list(cls, limit=None, **kwargs):
141+
def list(cls, limit=None, namespace="", **kwargs):
146142
restful = []
147143
endpoint = getattr(cls, 'endpoint', None)
148144
subsystem = getattr(cls, 'subsystem', None)
149145
assert endpoint, "Endpoint not specified for %s" % cls.__name__
150146
assert subsystem, "Subsystem not specified for %s" % cls.__name__
151147

152-
if cls.namespaced and dockercloud.namespace:
153-
detail_uri = "/".join(["api", subsystem, cls._api_version, dockercloud.namespace, endpoint.strip("/")])
148+
if not namespace:
149+
namespace = dockercloud.namespace
150+
if cls.is_namespaced and namespace:
151+
resource_uri = "/".join(["api", subsystem, cls._api_version, namespace, endpoint.strip("/")])
154152
else:
155-
detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/")])
153+
resource_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/")])
156154
objects = []
157155
while True:
158156
if limit and len(objects) >= limit:
159157
break
160-
json = send_request('GET', detail_uri, params=kwargs)
158+
json = send_request('GET', resource_uri, params=kwargs)
161159
objs = json.get('objects', [])
162160
meta = json.get('meta', {})
163161
next_url = meta.get('next', '')
@@ -182,10 +180,10 @@ def refresh(self, force=False):
182180
if self.is_dirty and not force:
183181
# We have local non-committed changes - rejecting the refresh
184182
success = False
185-
elif not self._detail_uri:
183+
elif not self._resource_uri:
186184
raise ApiError("You must save the object before performing this operation")
187185
else:
188-
json = send_request("GET", self._detail_uri)
186+
json = send_request("GET", self._resource_uri)
189187
if json:
190188
self._loaddict(json)
191189
success = True
@@ -202,16 +200,17 @@ def create(cls, **kwargs):
202200
return cls(**kwargs)
203201

204202
def delete(self):
205-
if not self._detail_uri:
203+
if not self._resource_uri:
206204
raise ApiError("You must save the object before performing this operation")
207205
action = "DELETE"
208-
url = self._detail_uri
206+
url = self._resource_uri
209207
json = send_request(action, url)
210208
if json:
211209
self._loaddict(json)
210+
self._resource_uri = None
212211
else:
213212
# Object deleted successfully and nothing came back - deleting PK reference.
214-
self._detail_uri = None
213+
self._resource_uri = None
215214
# setattr(self, self._pk_key(), None) -- doesn't work
216215
self.__setchanges__([])
217216
return True
@@ -228,15 +227,15 @@ def save(self):
228227
assert endpoint, "Endpoint not specified for %s" % self.__class__.__name__
229228
assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__
230229
# Figure out whether we should do a create or update
231-
if not self._detail_uri:
230+
if not self._resource_uri:
232231
action = "POST"
233-
if cls.namespaced and dockercloud.namespace:
234-
path = "/".join(["api", subsystem, self._api_version, dockercloud.namespace, endpoint.lstrip("/")])
232+
if cls.is_namespaced and self._namespace:
233+
path = "/".join(["api", subsystem, self._api_version, self._namespace, endpoint.lstrip("/")])
235234
else:
236235
path = "/".join(["api", subsystem, self._api_version, endpoint.lstrip("/")])
237236
else:
238237
action = "PATCH"
239-
path = self._detail_uri
238+
path = self._resource_uri
240239
# Construct the necessary params
241240
params = {}
242241
for attr in self.__getchanges__():
@@ -322,13 +321,16 @@ def run_forever(self, *args, **kwargs):
322321

323322

324323
class StreamingLog(StreamingAPI):
325-
def __init__(self, subsystem, resource, uuid, tail, follow):
324+
def __init__(self, subsystem, resource, uuid, tail, follow, namespace=""):
326325
endpoint = "%s/%s/logs/?follow=%s" % (resource, uuid, str(follow).lower())
327326
if tail:
328327
endpoint = "%s&tail=%d" % (endpoint, tail)
329-
if dockercloud.namespace:
328+
329+
if not namespace:
330+
namespace = dockercloud.namespace
331+
if namespace:
330332
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", subsystem, self._api_version,
331-
dockercloud.namespace, endpoint.lstrip("/")])
333+
self._namespace, endpoint.lstrip("/")])
332334
else:
333335
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", subsystem, self._api_version,
334336
endpoint.lstrip("/")])
@@ -348,11 +350,13 @@ def run_forever(self, *args, **kwargs):
348350

349351

350352
class Exec(StreamingAPI):
351-
def __init__(self, uuid, cmd='sh'):
353+
def __init__(self, uuid, cmd='sh', namespace=""):
352354
endpoint = "container/%s/exec/?command=%s" % (uuid, urllib.quote_plus(cmd))
353-
if dockercloud.namespace:
355+
if not namespace:
356+
namespace = dockercloud.namespace
357+
if namespace:
354358
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "app", self._api_version,
355-
dockercloud.namespace, endpoint.lstrip("/")])
359+
namespace, endpoint.lstrip("/")])
356360
else:
357361
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "app", self._api_version, endpoint.lstrip("/")])
358362
super(self.__class__, self).__init__(url)

dockercloud/api/events.py

+21-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import logging
5+
import signal
56

67
import websocket
78

@@ -13,11 +14,14 @@
1314

1415

1516
class Events(StreamingAPI):
16-
def __init__(self):
17+
def __init__(self, namespace=""):
1718
endpoint = "events"
18-
if dockercloud.namespace:
19+
20+
if not namespace:
21+
namespace = dockercloud.namespace
22+
if namespace:
1923
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version,
20-
dockercloud.namespace, endpoint.lstrip("/")])
24+
namespace, endpoint.lstrip("/")])
2125
else:
2226
url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version,
2327
endpoint.lstrip("/")])
@@ -41,15 +45,22 @@ def _on_error(self, ws, e):
4145

4246
super(self.__class__, self)._on_error(ws, e)
4347

48+
def _on_stop(self, signal, frame):
49+
self.ws.close()
50+
self.run_forever_flag = not self.run_forever_flag
51+
4452
def run_forever(self, *args, **kwargs):
45-
while True:
53+
54+
self.run_forever_flag = True
55+
while self.run_forever_flag:
4656
if self.auth_error:
4757
self.auth_error = False
4858
raise AuthError("Not Authorized")
4959

50-
ws = websocket.WebSocketApp(self.url, header=self.header,
51-
on_open=self._on_open,
52-
on_message=self._on_message,
53-
on_error=self._on_error,
54-
on_close=self._on_close)
55-
ws.run_forever(ping_interval=10, ping_timeout=5, *args, **kwargs)
60+
self.ws = websocket.WebSocketApp(self.url, header=self.header,
61+
on_open=self._on_open,
62+
on_message=self._on_message,
63+
on_error=self._on_error,
64+
on_close=self._on_close)
65+
signal.signal(signal.SIGINT, self._on_stop)
66+
self.ws.run_forever(ping_interval=10, ping_timeout=5, *args, **kwargs)

dockercloud/api/http.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22

33
import logging
4+
import time
45

56
from requests import Request, Session
67
from requests import utils
@@ -12,9 +13,15 @@
1213
logger = logging.getLogger("python-dockercloud")
1314

1415
global_session = Session()
16+
last_connection_time = time.time()
1517

1618

17-
def get_session():
19+
def get_session(time=time):
20+
if (dockercloud.reconnection_interval >= 0):
21+
global last_connection_time
22+
if (time.time() - last_connection_time > dockercloud.reconnection_interval):
23+
new_session()
24+
last_connection_time = time.time()
1825
return global_session
1926

2027

@@ -55,7 +62,12 @@ def send_request(method, path, inject_header=True, **kwargs):
5562
# make the request
5663
req = s.prepare_request(request)
5764
logger.info("Prepared Request: %s, %s, %s, %s" % (req.method, req.url, req.headers, kwargs))
58-
response = s.send(req, **kw_args)
65+
66+
if dockercloud.api_timeout:
67+
response = s.send(req, timeout=dockercloud.api_timeout, **kw_args)
68+
else:
69+
response = s.send(req, **kw_args)
70+
5971
status_code = getattr(response, 'status_code', None)
6072
logger.info("Response: Status %s, %s, %s" % (str(status_code), response.headers, response.text))
6173

0 commit comments

Comments
 (0)