client.py 27 KB

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