client.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. # Copyright 2013 dotCloud inc.
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. # Unless required by applicable law or agreed to in writing, software
  7. # distributed under the License is distributed on an "AS IS" BASIS,
  8. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. # See the License for the specific language governing permissions and
  10. # limitations under the License.
  11. import json
  12. import re
  13. import shlex
  14. import struct
  15. import requests
  16. import requests.exceptions
  17. import six
  18. from .auth import auth
  19. from .unixconn import unixconn
  20. from .utils import utils
  21. if not six.PY3:
  22. import websocket
  23. DEFAULT_TIMEOUT_SECONDS = 60
  24. STREAM_HEADER_SIZE_BYTES = 8
  25. class APIError(requests.exceptions.HTTPError):
  26. def __init__(self, message, response, explanation=None):
  27. super(APIError, self).__init__(message, response=response)
  28. self.explanation = explanation
  29. if self.explanation is None and response.content:
  30. self.explanation = response.content.strip()
  31. def __str__(self):
  32. message = super(APIError, self).__str__()
  33. if self.is_client_error():
  34. message = '%s Client Error: %s' % (
  35. self.response.status_code, self.response.reason)
  36. elif self.is_server_error():
  37. message = '%s Server Error: %s' % (
  38. self.response.status_code, self.response.reason)
  39. if self.explanation:
  40. message = '%s ("%s")' % (message, self.explanation)
  41. return message
  42. def is_client_error(self):
  43. return 400 <= self.response.status_code < 500
  44. def is_server_error(self):
  45. return 500 <= self.response.status_code < 600
  46. class Client(requests.Session):
  47. def __init__(self, base_url=None, version="1.6",
  48. timeout=DEFAULT_TIMEOUT_SECONDS):
  49. super(Client, self).__init__()
  50. if base_url is None:
  51. base_url = "unix://var/run/docker.sock"
  52. if base_url.startswith('unix:///'):
  53. base_url = base_url.replace('unix:/', 'unix:')
  54. if base_url.startswith('tcp:'):
  55. base_url = base_url.replace('tcp:', 'http:')
  56. if base_url.endswith('/'):
  57. base_url = base_url[:-1]
  58. self.base_url = base_url
  59. self._version = version
  60. self._timeout = timeout
  61. self._auth_configs = auth.load_config()
  62. self.mount('unix://', unixconn.UnixAdapter(base_url, timeout))
  63. def _set_request_timeout(self, kwargs):
  64. """Prepare the kwargs for an HTTP request by inserting the timeout
  65. parameter, if not already present."""
  66. kwargs.setdefault('timeout', self._timeout)
  67. return kwargs
  68. def _post(self, url, **kwargs):
  69. return self.post(url, **self._set_request_timeout(kwargs))
  70. def _get(self, url, **kwargs):
  71. return self.get(url, **self._set_request_timeout(kwargs))
  72. def _delete(self, url, **kwargs):
  73. return self.delete(url, **self._set_request_timeout(kwargs))
  74. def _url(self, path):
  75. return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
  76. def _raise_for_status(self, response, explanation=None):
  77. """Raises stored :class:`APIError`, if one occurred."""
  78. try:
  79. response.raise_for_status()
  80. except requests.exceptions.HTTPError as e:
  81. raise APIError(e, response, explanation=explanation)
  82. def _result(self, response, json=False, binary=False):
  83. assert not (json and binary)
  84. self._raise_for_status(response)
  85. if json:
  86. return response.json()
  87. if binary:
  88. return response.content
  89. return response.text
  90. def _container_config(self, image, command, hostname=None, user=None,
  91. detach=False, stdin_open=False, tty=False,
  92. mem_limit=0, ports=None, environment=None, dns=None,
  93. volumes=None, volumes_from=None,
  94. network_disabled=False, entrypoint=None,
  95. cpu_shares=None, working_dir=None):
  96. if isinstance(command, six.string_types):
  97. command = shlex.split(str(command))
  98. if isinstance(environment, dict):
  99. environment = [
  100. '{0}={1}'.format(k, v) for k, v in environment.items()
  101. ]
  102. if ports and isinstance(ports, list):
  103. exposed_ports = {}
  104. for port_definition in ports:
  105. port = port_definition
  106. proto = 'tcp'
  107. if isinstance(port_definition, tuple):
  108. if len(port_definition) == 2:
  109. proto = port_definition[1]
  110. port = port_definition[0]
  111. exposed_ports['{0}/{1}'.format(port, proto)] = {}
  112. ports = exposed_ports
  113. if volumes and isinstance(volumes, list):
  114. volumes_dict = {}
  115. for vol in volumes:
  116. volumes_dict[vol] = {}
  117. volumes = volumes_dict
  118. attach_stdin = False
  119. attach_stdout = False
  120. attach_stderr = False
  121. stdin_once = False
  122. if not detach:
  123. attach_stdout = True
  124. attach_stderr = True
  125. if stdin_open:
  126. attach_stdin = True
  127. stdin_once = True
  128. return {
  129. 'Hostname': hostname,
  130. 'ExposedPorts': ports,
  131. 'User': user,
  132. 'Tty': tty,
  133. 'OpenStdin': stdin_open,
  134. 'StdinOnce': stdin_once,
  135. 'Memory': mem_limit,
  136. 'AttachStdin': attach_stdin,
  137. 'AttachStdout': attach_stdout,
  138. 'AttachStderr': attach_stderr,
  139. 'Env': environment,
  140. 'Cmd': command,
  141. 'Dns': dns,
  142. 'Image': image,
  143. 'Volumes': volumes,
  144. 'VolumesFrom': volumes_from,
  145. 'NetworkDisabled': network_disabled,
  146. 'Entrypoint': entrypoint,
  147. 'CpuShares': cpu_shares,
  148. 'WorkingDir': working_dir
  149. }
  150. def _post_json(self, url, data, **kwargs):
  151. # Go <1.1 can't unserialize null to a string
  152. # so we do this disgusting thing here.
  153. data2 = {}
  154. if data is not None:
  155. for k, v in six.iteritems(data):
  156. if v is not None:
  157. data2[k] = v
  158. if 'headers' not in kwargs:
  159. kwargs['headers'] = {}
  160. kwargs['headers']['Content-Type'] = 'application/json'
  161. return self._post(url, data=json.dumps(data2), **kwargs)
  162. def _attach_params(self, override=None):
  163. return override or {
  164. 'stdout': 1,
  165. 'stderr': 1,
  166. 'stream': 1
  167. }
  168. def _attach_websocket(self, container, params=None):
  169. if six.PY3:
  170. raise NotImplementedError("This method is not currently supported "
  171. "under python 3")
  172. url = self._url("/containers/{0}/attach/ws".format(container))
  173. req = requests.Request("POST", url, params=self._attach_params(params))
  174. full_url = req.prepare().url
  175. full_url = full_url.replace("http://", "ws://", 1)
  176. full_url = full_url.replace("https://", "wss://", 1)
  177. return self._create_websocket_connection(full_url)
  178. def _create_websocket_connection(self, url):
  179. return websocket.create_connection(url)
  180. def _stream_result(self, response):
  181. """Generator for straight-out, non chunked-encoded HTTP responses."""
  182. self._raise_for_status(response)
  183. for line in response.iter_lines(chunk_size=1):
  184. # filter out keep-alive new lines
  185. if line:
  186. yield line + '\n'
  187. def _stream_result_socket(self, response):
  188. self._raise_for_status(response)
  189. return response.raw._fp.fp._sock
  190. def _stream_helper(self, response):
  191. """Generator for data coming from a chunked-encoded HTTP response."""
  192. socket_fp = self._stream_result_socket(response)
  193. socket_fp.setblocking(1)
  194. socket = socket_fp.makefile()
  195. while True:
  196. size = int(socket.readline(), 16)
  197. if size <= 0:
  198. break
  199. data = socket.readline()
  200. if not data:
  201. break
  202. yield data
  203. def _multiplexed_buffer_helper(self, response):
  204. """A generator of multiplexed data blocks read from a buffered
  205. response."""
  206. buf = self._result(response, binary=True)
  207. walker = 0
  208. while True:
  209. if len(buf[walker:]) < 8:
  210. break
  211. _, length = struct.unpack_from('>BxxxL', buf[walker:])
  212. start = walker + STREAM_HEADER_SIZE_BYTES
  213. end = start + length
  214. walker = end
  215. yield str(buf[start:end])
  216. def _multiplexed_socket_stream_helper(self, response):
  217. """A generator of multiplexed data blocks coming from a response
  218. socket."""
  219. socket = self._stream_result_socket(response)
  220. def recvall(socket, size):
  221. data = ''
  222. while size > 0:
  223. block = socket.recv(size)
  224. if not block:
  225. return None
  226. data += block
  227. size -= len(block)
  228. return data
  229. while True:
  230. socket.settimeout(None)
  231. header = recvall(socket, STREAM_HEADER_SIZE_BYTES)
  232. if not header:
  233. break
  234. _, length = struct.unpack('>BxxxL', header)
  235. if not length:
  236. break
  237. data = recvall(socket, length)
  238. if not data:
  239. break
  240. yield data
  241. def attach(self, container, stdout=True, stderr=True,
  242. stream=False, logs=False):
  243. if isinstance(container, dict):
  244. container = container.get('Id')
  245. params = {
  246. 'logs': logs and 1 or 0,
  247. 'stdout': stdout and 1 or 0,
  248. 'stderr': stderr and 1 or 0,
  249. 'stream': stream and 1 or 0,
  250. }
  251. u = self._url("/containers/{0}/attach".format(container))
  252. response = self._post(u, params=params, stream=stream)
  253. # Stream multi-plexing was introduced in API v1.6.
  254. if utils.compare_version('1.6', self._version) < 0:
  255. return stream and self._stream_result(response) or \
  256. self._result(response, binary=True)
  257. return stream and self._multiplexed_socket_stream_helper(response) or \
  258. ''.join([x for x in self._multiplexed_buffer_helper(response)])
  259. def attach_socket(self, container, params=None, ws=False):
  260. if params is None:
  261. params = {
  262. 'stdout': 1,
  263. 'stderr': 1,
  264. 'stream': 1
  265. }
  266. if ws:
  267. return self._attach_websocket(container, params)
  268. if isinstance(container, dict):
  269. container = container.get('Id')
  270. u = self._url("/containers/{0}/attach".format(container))
  271. return self._stream_result_socket(self.post(
  272. u, None, params=self._attach_params(params), stream=True))
  273. def build(self, path=None, tag=None, quiet=False, fileobj=None,
  274. nocache=False, rm=False, stream=False, timeout=None):
  275. remote = context = headers = None
  276. if path is None and fileobj is None:
  277. raise Exception("Either path or fileobj needs to be provided.")
  278. if fileobj is not None:
  279. context = utils.mkbuildcontext(fileobj)
  280. elif path.startswith(('http://', 'https://', 'git://', 'github.com/')):
  281. remote = path
  282. else:
  283. context = utils.tar(path)
  284. u = self._url('/build')
  285. params = {
  286. 't': tag,
  287. 'remote': remote,
  288. 'q': quiet,
  289. 'nocache': nocache,
  290. 'rm': rm
  291. }
  292. if context is not None:
  293. headers = {'Content-Type': 'application/tar'}
  294. response = self._post(
  295. u,
  296. data=context,
  297. params=params,
  298. headers=headers,
  299. stream=stream,
  300. timeout=timeout,
  301. )
  302. if context is not None:
  303. context.close()
  304. if stream:
  305. return self._stream_result(response)
  306. else:
  307. output = self._result(response)
  308. srch = r'Successfully built ([0-9a-f]+)'
  309. match = re.search(srch, output)
  310. if not match:
  311. return None, output
  312. return match.group(1), output
  313. def commit(self, container, repository=None, tag=None, message=None,
  314. author=None, conf=None):
  315. params = {
  316. 'container': container,
  317. 'repo': repository,
  318. 'tag': tag,
  319. 'comment': message,
  320. 'author': author
  321. }
  322. u = self._url("/commit")
  323. return self._result(self._post_json(u, data=conf, params=params),
  324. json=True)
  325. def containers(self, quiet=False, all=False, trunc=True, latest=False,
  326. since=None, before=None, limit=-1):
  327. params = {
  328. 'limit': 1 if latest else limit,
  329. 'all': 1 if all else 0,
  330. 'trunc_cmd': 1 if trunc else 0,
  331. 'since': since,
  332. 'before': before
  333. }
  334. u = self._url("/containers/json")
  335. res = self._result(self._get(u, params=params), True)
  336. if quiet:
  337. return [{'Id': x['Id']} for x in res]
  338. return res
  339. def copy(self, container, resource):
  340. res = self._post_json(
  341. self._url("/containers/{0}/copy".format(container)),
  342. data={"Resource": resource},
  343. stream=True
  344. )
  345. self._raise_for_status(res)
  346. return res.raw
  347. def create_container(self, image, command=None, hostname=None, user=None,
  348. detach=False, stdin_open=False, tty=False,
  349. mem_limit=0, ports=None, environment=None, dns=None,
  350. volumes=None, volumes_from=None,
  351. network_disabled=False, name=None, entrypoint=None,
  352. cpu_shares=None, working_dir=None):
  353. config = self._container_config(
  354. image, command, hostname, user, detach, stdin_open, tty, mem_limit,
  355. ports, environment, dns, volumes, volumes_from, network_disabled,
  356. entrypoint, cpu_shares, working_dir
  357. )
  358. return self.create_container_from_config(config, name)
  359. def create_container_from_config(self, config, name=None):
  360. u = self._url("/containers/create")
  361. params = {
  362. 'name': name
  363. }
  364. res = self._post_json(u, data=config, params=params)
  365. return self._result(res, True)
  366. def diff(self, container):
  367. if isinstance(container, dict):
  368. container = container.get('Id')
  369. return self._result(self._get(self._url("/containers/{0}/changes".
  370. format(container))), True)
  371. def events(self):
  372. u = self._url("/events")
  373. socket = self._stream_result_socket(self.get(u, stream=True))
  374. while True:
  375. chunk = socket.recv(4096)
  376. if chunk:
  377. # Messages come in the format of length, data, newline.
  378. length, data = chunk.split("\n", 1)
  379. length = int(length, 16)
  380. if length > len(data):
  381. data += socket.recv(length - len(data))
  382. yield json.loads(data)
  383. else:
  384. break
  385. def export(self, container):
  386. if isinstance(container, dict):
  387. container = container.get('Id')
  388. res = self._get(self._url("/containers/{0}/export".format(container)),
  389. stream=True)
  390. self._raise_for_status(res)
  391. return res.raw
  392. def history(self, image):
  393. res = self._get(self._url("/images/{0}/history".format(image)))
  394. self._raise_for_status(res)
  395. return self._result(res)
  396. def images(self, name=None, quiet=False, all=False, viz=False):
  397. if viz:
  398. return self._result(self._get(self._url("images/viz")))
  399. params = {
  400. 'filter': name,
  401. 'only_ids': 1 if quiet else 0,
  402. 'all': 1 if all else 0,
  403. }
  404. res = self._result(self._get(self._url("/images/json"), params=params),
  405. True)
  406. if quiet:
  407. return [x['Id'] for x in res]
  408. return res
  409. def import_image(self, src=None, repository=None, tag=None, image=None):
  410. u = self._url("/images/create")
  411. params = {
  412. 'repo': repository,
  413. 'tag': tag
  414. }
  415. if src:
  416. try:
  417. # XXX: this is ways not optimal but the only way
  418. # for now to import tarballs through the API
  419. fic = open(src)
  420. data = fic.read()
  421. fic.close()
  422. src = "-"
  423. except IOError:
  424. # file does not exists or not a file (URL)
  425. data = None
  426. if isinstance(src, six.string_types):
  427. params['fromSrc'] = src
  428. return self._result(self._post(u, data=data, params=params))
  429. return self._result(self._post(u, data=src, params=params))
  430. if image:
  431. params['fromImage'] = image
  432. return self._result(self._post(u, data=None, params=params))
  433. raise Exception("Must specify a src or image")
  434. def info(self):
  435. return self._result(self._get(self._url("/info")),
  436. True)
  437. def insert(self, image, url, path):
  438. api_url = self._url("/images/" + image + "/insert")
  439. params = {
  440. 'url': url,
  441. 'path': path
  442. }
  443. return self._result(self._post(api_url, params=params))
  444. def inspect_container(self, container):
  445. if isinstance(container, dict):
  446. container = container.get('Id')
  447. return self._result(
  448. self._get(self._url("/containers/{0}/json".format(container))),
  449. True)
  450. def inspect_image(self, image_id):
  451. return self._result(
  452. self._get(self._url("/images/{0}/json".format(image_id))),
  453. True
  454. )
  455. def kill(self, container, signal=None):
  456. if isinstance(container, dict):
  457. container = container.get('Id')
  458. url = self._url("/containers/{0}/kill".format(container))
  459. params = {}
  460. if signal is not None:
  461. params['signal'] = signal
  462. res = self._post(url, params=params)
  463. self._raise_for_status(res)
  464. def login(self, username, password=None, email=None, registry=None,
  465. reauth=False):
  466. # If we don't have any auth data so far, try reloading the config file
  467. # one more time in case anything showed up in there.
  468. if not self._auth_configs:
  469. self._auth_configs = auth.load_config()
  470. registry = registry or auth.INDEX_URL
  471. authcfg = auth.resolve_authconfig(self._auth_configs, registry)
  472. # If we found an existing auth config for this registry and username
  473. # combination, we can return it immediately unless reauth is requested.
  474. if authcfg and authcfg.get('username', None) == username \
  475. and not reauth:
  476. return authcfg
  477. req_data = {
  478. 'username': username,
  479. 'password': password,
  480. 'email': email,
  481. 'serveraddress': registry,
  482. }
  483. response = self._post_json(self._url('/auth'), data=req_data)
  484. if response.status_code == 200:
  485. self._auth_configs[registry] = req_data
  486. return self._result(response, json=True)
  487. def logs(self, container, stdout=True, stderr=True, stream=False):
  488. return self.attach(
  489. container,
  490. stdout=stdout,
  491. stderr=stderr,
  492. stream=stream,
  493. logs=True
  494. )
  495. def port(self, container, private_port):
  496. if isinstance(container, dict):
  497. container = container.get('Id')
  498. res = self._get(self._url("/containers/{0}/json".format(container)))
  499. self._raise_for_status(res)
  500. json_ = res.json()
  501. s_port = str(private_port)
  502. h_ports = None
  503. h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp')
  504. if h_ports is None:
  505. h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp')
  506. return h_ports
  507. def pull(self, repository, tag=None, stream=False):
  508. registry, repo_name = auth.resolve_repository_name(repository)
  509. if repo_name.count(":") == 1:
  510. repository, tag = repository.rsplit(":", 1)
  511. params = {
  512. 'tag': tag,
  513. 'fromImage': repository
  514. }
  515. headers = {}
  516. if utils.compare_version('1.5', self._version) >= 0:
  517. # If we don't have any auth data so far, try reloading the config
  518. # file one more time in case anything showed up in there.
  519. if not self._auth_configs:
  520. self._auth_configs = auth.load_config()
  521. authcfg = auth.resolve_authconfig(self._auth_configs, registry)
  522. # Do not fail here if no atuhentication exists for this specific
  523. # registry as we can have a readonly pull. Just put the header if
  524. # we can.
  525. if authcfg:
  526. headers['X-Registry-Auth'] = auth.encode_header(authcfg)
  527. response = self._post(self._url('/images/create'), params=params,
  528. headers=headers, stream=stream, timeout=None)
  529. if stream:
  530. return self._stream_helper(response)
  531. else:
  532. return self._result(response)
  533. def push(self, repository, stream=False):
  534. registry, repo_name = auth.resolve_repository_name(repository)
  535. u = self._url("/images/{0}/push".format(repository))
  536. headers = {}
  537. if utils.compare_version('1.5', self._version) >= 0:
  538. # If we don't have any auth data so far, try reloading the config
  539. # file one more time in case anything showed up in there.
  540. if not self._auth_configs:
  541. self._auth_configs = auth.load_config()
  542. authcfg = auth.resolve_authconfig(self._auth_configs, registry)
  543. # Do not fail here if no atuhentication exists for this specific
  544. # registry as we can have a readonly pull. Just put the header if
  545. # we can.
  546. if authcfg:
  547. headers['X-Registry-Auth'] = auth.encode_header(authcfg)
  548. response = self._post_json(u, None, headers=headers, stream=stream)
  549. else:
  550. response = self._post_json(u, authcfg, stream=stream)
  551. return stream and self._stream_helper(response) \
  552. or self._result(response)
  553. def remove_container(self, container, v=False, link=False):
  554. if isinstance(container, dict):
  555. container = container.get('Id')
  556. params = {'v': v, 'link': link}
  557. res = self._delete(self._url("/containers/" + container),
  558. params=params)
  559. self._raise_for_status(res)
  560. def remove_image(self, image):
  561. res = self._delete(self._url("/images/" + image))
  562. self._raise_for_status(res)
  563. def restart(self, container, timeout=10):
  564. if isinstance(container, dict):
  565. container = container.get('Id')
  566. params = {'t': timeout}
  567. url = self._url("/containers/{0}/restart".format(container))
  568. res = self._post(url, params=params)
  569. self._raise_for_status(res)
  570. def search(self, term):
  571. return self._result(self._get(self._url("/images/search"),
  572. params={'term': term}),
  573. True)
  574. def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
  575. publish_all_ports=False, links=None, privileged=False):
  576. if isinstance(container, dict):
  577. container = container.get('Id')
  578. if isinstance(lxc_conf, dict):
  579. formatted = []
  580. for k, v in six.iteritems(lxc_conf):
  581. formatted.append({'Key': k, 'Value': str(v)})
  582. lxc_conf = formatted
  583. start_config = {
  584. 'LxcConf': lxc_conf
  585. }
  586. if binds:
  587. bind_pairs = [
  588. '{0}:{1}'.format(host, dest) for host, dest in binds.items()
  589. ]
  590. start_config['Binds'] = bind_pairs
  591. if port_bindings:
  592. start_config['PortBindings'] = utils.convert_port_bindings(
  593. port_bindings
  594. )
  595. start_config['PublishAllPorts'] = publish_all_ports
  596. if links:
  597. formatted_links = [
  598. '{0}:{1}'.format(k, v) for k, v in sorted(six.iteritems(links))
  599. ]
  600. start_config['Links'] = formatted_links
  601. start_config['Privileged'] = privileged
  602. url = self._url("/containers/{0}/start".format(container))
  603. res = self._post_json(url, data=start_config)
  604. self._raise_for_status(res)
  605. def stop(self, container, timeout=10):
  606. if isinstance(container, dict):
  607. container = container.get('Id')
  608. params = {'t': timeout}
  609. url = self._url("/containers/{0}/stop".format(container))
  610. res = self._post(url, params=params,
  611. timeout=max(timeout, self._timeout))
  612. self._raise_for_status(res)
  613. def tag(self, image, repository, tag=None, force=False):
  614. params = {
  615. 'tag': tag,
  616. 'repo': repository,
  617. 'force': 1 if force else 0
  618. }
  619. url = self._url("/images/{0}/tag".format(image))
  620. res = self._post(url, params=params)
  621. self._raise_for_status(res)
  622. return res.status_code == 201
  623. def top(self, container):
  624. u = self._url("/containers/{0}/top".format(container))
  625. return self._result(self._get(u), True)
  626. def version(self):
  627. return self._result(self._get(self._url("/version")), True)
  628. def wait(self, container):
  629. if isinstance(container, dict):
  630. container = container.get('Id')
  631. url = self._url("/containers/{0}/wait".format(container))
  632. res = self._post(url, timeout=None)
  633. self._raise_for_status(res)
  634. json_ = res.json()
  635. if 'StatusCode' in json_:
  636. return json_['StatusCode']
  637. return -1