client.py 31 KB

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