-
Notifications
You must be signed in to change notification settings - Fork 5.5k
/
dockermod.py
6976 lines (5497 loc) · 225 KB
/
dockermod.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
'''
Management of Docker Containers
.. versionadded:: 2015.8.0
.. versionchanged:: 2017.7.0
This module has replaced the legacy docker execution module.
:depends: docker_ Python module
.. _`create_container()`: http://docker-py.readthedocs.io/en/stable/api.html#docker.api.container.ContainerApiMixin.create_container
.. _`create_host_config()`: http://docker-py.readthedocs.io/en/stable/api.html#docker.api.container.ContainerApiMixin.create_host_config
.. _`connect_container_to_network()`: http://docker-py.readthedocs.io/en/stable/api.html#docker.api.network.NetworkApiMixin.connect_container_to_network
.. _`create_network()`: http://docker-py.readthedocs.io/en/stable/api.html#docker.api.network.NetworkApiMixin.create_network
.. _`logs()`: http://docker-py.readthedocs.io/en/stable/api.html#docker.api.container.ContainerApiMixin.logs
.. _`IPAM pool`: http://docker-py.readthedocs.io/en/stable/api.html#docker.types.IPAMPool
.. _docker: https://pypi.python.org/pypi/docker
.. _docker-py: https://pypi.python.org/pypi/docker-py
.. _lxc-attach: https://linuxcontainers.org/lxc/manpages/man1/lxc-attach.1.html
.. _nsenter: http://man7.org/linux/man-pages/man1/nsenter.1.html
.. _docker-exec: http://docs.docker.com/reference/commandline/cli/#exec
.. _`docker-py Low-level API`: http://docker-py.readthedocs.io/en/stable/api.html
.. _timelib: https://pypi.python.org/pypi/timelib
.. _`trusted builds`: https://blog.docker.com/2013/11/introducing-trusted-builds/
.. _`Docker Engine API`: https://docs.docker.com/engine/api/v1.33/#operation/ContainerCreate
.. note::
Older releases of the Python bindings for Docker were called docker-py_ in
PyPI. All releases of docker_, and releases of docker-py_ >= 1.6.0 are
supported. These python bindings can easily be installed using
:py:func:`pip.install <salt.modules.pip.install>`:
.. code-block:: bash
salt myminion pip.install docker
To upgrade from docker-py_ to docker_, you must first uninstall docker-py_,
and then install docker_:
.. code-block:: bash
salt myminion pip.uninstall docker-py
salt myminion pip.install docker
.. _docker-authentication:
Authentication
--------------
If you have previously performed a ``docker login`` from the minion, then the
credentials saved in ``~/.docker/config.json`` will be used for any actions
which require authentication. If not, then credentials can be configured in
Pillar data. The configuration schema is as follows:
.. code-block:: yaml
docker-registries:
<registry_url>:
username: <username>
password: <password>
For example:
.. code-block:: yaml
docker-registries:
hub:
username: foo
password: s3cr3t
.. note::
As of the 2016.3.7, 2016.11.4, and 2017.7.0 releases of Salt, credentials
for the Docker Hub can be configured simply by specifying ``hub`` in place
of the registry URL. In earlier releases, it is necessary to specify the
actual registry URL for the Docker Hub (i.e.
``https://index.docker.io/v1/``).
More than one registry can be configured. Salt will look for Docker credentials
in the ``docker-registries`` Pillar key, as well as any key ending in
``-docker-registries``. For example:
.. code-block:: yaml
docker-registries:
'https://mydomain.tld/registry:5000':
username: foo
password: s3cr3t
foo-docker-registries:
https://index.foo.io/v1/:
username: foo
password: s3cr3t
bar-docker-registries:
https://index.bar.io/v1/:
username: foo
password: s3cr3t
To login to the configured registries, use the :py:func:`docker.login
<salt.modules.dockermod.login>` function. This only needs to be done once for a
given registry, and it will store/update the credentials in
``~/.docker/config.json``.
.. note::
For Salt releases before 2016.3.7 and 2016.11.4, :py:func:`docker.login
<salt.modules.dockermod.login>` is not available. Instead, Salt will try to
authenticate using each of your configured registries for each push/pull,
behavior which is not correct and has been resolved in newer releases.
Configuration Options
---------------------
The following configuration options can be set to fine-tune how Salt uses
Docker:
- ``docker.url``: URL to the docker service (default: local socket).
- ``docker.version``: API version to use (should not need to be set manually in
the vast majority of cases)
- ``docker.exec_driver``: Execution driver to use, one of ``nsenter``,
``lxc-attach``, or ``docker-exec``. See the :ref:`Executing Commands Within a
Running Container <docker-execution-driver>` section for more details on how
this config parameter is used.
These configuration options are retrieved using :py:mod:`config.get
<salt.modules.config.get>` (click the link for further information).
.. _docker-execution-driver:
Executing Commands Within a Running Container
---------------------------------------------
.. note::
With the release of Docker 1.13.1, the Execution Driver has been removed.
Starting in versions 2016.3.6, 2016.11.4, and 2017.7.0, Salt defaults to
using ``docker exec`` to run commands in containers, however for older Salt
releases it will be necessary to set the ``docker.exec_driver`` config
option to either ``docker-exec`` or ``nsenter`` for Docker versions 1.13.1
and newer.
Multiple methods exist for executing commands within Docker containers:
- lxc-attach_: Default for older versions of docker
- nsenter_: Enters container namespace to run command
- docker-exec_: Native support for executing commands in Docker containers
(added in Docker 1.3)
Adding a configuration option (see :py:func:`config.get
<salt.modules.config.get>`) called ``docker.exec_driver`` will tell Salt which
execution driver to use:
.. code-block:: yaml
docker.exec_driver: docker-exec
If this configuration option is not found, Salt will use the appropriate
interface (either nsenter_ or lxc-attach_) based on the ``Execution Driver``
value returned from ``docker info``. docker-exec_ will not be used by default,
as it is presently (as of version 1.6.2) only able to execute commands as the
effective user of the container. Thus, if a ``USER`` directive was used to run
as a non-privileged user, docker-exec_ would be unable to perform the action as
root. Salt can still use docker-exec_ as an execution driver, but must be
explicitly configured (as in the example above) to do so at this time.
If possible, try to manually specify the execution driver, as it will save Salt
a little work.
This execution module provides functions that shadow those from the :mod:`cmd
<salt.modules.cmdmod>` module. They are as follows:
- :py:func:`docker.retcode <salt.modules.dockermod.retcode>`
- :py:func:`docker.run <salt.modules.dockermod.run>`
- :py:func:`docker.run_all <salt.modules.dockermod.run_all>`
- :py:func:`docker.run_stderr <salt.modules.dockermod.run_stderr>`
- :py:func:`docker.run_stdout <salt.modules.dockermod.run_stdout>`
- :py:func:`docker.script <salt.modules.dockermod.script>`
- :py:func:`docker.script_retcode <salt.modules.dockermod.script_retcode>`
Detailed Function Documentation
-------------------------------
'''
# Import Python Futures
from __future__ import absolute_import
__docformat__ = 'restructuredtext en'
# Import Python libs
import bz2
import copy
# Remove unused-import from disabled pylint checks when we uncomment the logic
# in _get_exec_driver() which checks the docker version
import fnmatch
import functools
import gzip
import json
import logging
import os
import pipes
import re
import shutil
import string
import sys
import time
import uuid
import subprocess
# Import Salt libs
from salt.exceptions import CommandExecutionError, SaltInvocationError
from salt.ext import six
from salt.ext.six.moves import map # pylint: disable=import-error,redefined-builtin
import salt.utils.docker.translate.container
import salt.utils.docker.translate.network
import salt.utils.functools
import salt.utils.json
import salt.utils.path
import salt.pillar
import salt.exceptions
import salt.fileclient
from salt.state import HighState
import salt.client.ssh.state
# pylint: disable=import-error
try:
import docker
HAS_DOCKER_PY = True
except ImportError:
HAS_DOCKER_PY = False
try:
if six.PY2:
import backports.lzma as lzma
else:
import lzma
HAS_LZMA = True
except ImportError:
HAS_LZMA = False
try:
import timelib
HAS_TIMELIB = True
except ImportError:
HAS_TIMELIB = False
# pylint: enable=import-error
HAS_NSENTER = bool(salt.utils.path.which('nsenter'))
# Set up logging
log = logging.getLogger(__name__)
# Don't shadow built-in's.
__func_alias__ = {
'import_': 'import',
'ps_': 'ps',
'rm_': 'rm',
'signal_': 'signal',
'start_': 'start',
'tag_': 'tag',
'apply_': 'apply'
}
# Minimum supported versions
MIN_DOCKER = (1, 9, 0)
MIN_DOCKER_PY = (1, 6, 0)
VERSION_RE = r'([\d.]+)'
NOTSET = object()
# Define the module's virtual name and alias
__virtualname__ = 'docker'
__virtual_aliases__ = ('dockerng', 'moby')
__proxyenabled__ = ['docker']
__outputter__ = {
'sls': 'highstate',
'apply_': 'highstate',
'highstate': 'highstate',
}
def __virtual__():
'''
Only load if docker libs are present
'''
if HAS_DOCKER_PY:
try:
docker_py_versioninfo = _get_docker_py_versioninfo()
except Exception:
# May fail if we try to connect to a docker daemon but can't
return (False, 'Docker module found, but no version could be'
' extracted')
# Don't let a failure to interpret the version keep this module from
# loading. Log a warning (log happens in _get_docker_py_versioninfo()).
if docker_py_versioninfo is None:
return (False, 'Docker module found, but no version could be'
' extracted')
if docker_py_versioninfo >= MIN_DOCKER_PY:
try:
docker_versioninfo = version().get('VersionInfo')
except Exception:
docker_versioninfo = None
if docker_versioninfo is None or docker_versioninfo >= MIN_DOCKER:
return __virtualname__
else:
return (False,
'Insufficient Docker version (required: {0}, '
'installed: {1})'.format(
'.'.join(map(str, MIN_DOCKER)),
'.'.join(map(str, docker_versioninfo))))
return (False,
'Insufficient docker-py version (required: {0}, '
'installed: {1})'.format(
'.'.join(map(str, MIN_DOCKER_PY)),
'.'.join(map(str, docker_py_versioninfo))))
return (False, 'Could not import docker module, is docker-py installed?')
class DockerJSONDecoder(json.JSONDecoder):
def decode(self, s, _w=None):
objs = []
for line in s.splitlines():
if not line:
continue
obj, _ = self.raw_decode(line)
objs.append(obj)
return objs
def _get_docker_py_versioninfo():
'''
Returns the version_info tuple from docker-py
'''
try:
return docker.version_info
except AttributeError:
pass
def _get_client(timeout=NOTSET, **kwargs):
client_kwargs = {}
if timeout is not NOTSET:
client_kwargs['timeout'] = timeout
for key, val in (('base_url', 'docker.url'),
('version', 'docker.version')):
param = __salt__['config.get'](val, NOTSET)
if param is not NOTSET:
client_kwargs[key] = param
if 'base_url' not in client_kwargs and 'DOCKER_HOST' in os.environ:
# Check if the DOCKER_HOST environment variable has been set
client_kwargs['base_url'] = os.environ.get('DOCKER_HOST')
if 'version' not in client_kwargs:
# Let docker-py auto detect docker version incase
# it's not defined by user.
client_kwargs['version'] = 'auto'
docker_machine = __salt__['config.get']('docker.machine', NOTSET)
if docker_machine is not NOTSET:
docker_machine_json = __salt__['cmd.run'](
['docker-machine', 'inspect', docker_machine],
python_shell=False)
try:
docker_machine_json = \
salt.utils.json.loads(docker_machine_json)
docker_machine_tls = \
docker_machine_json['HostOptions']['AuthOptions']
docker_machine_ip = docker_machine_json['Driver']['IPAddress']
client_kwargs['base_url'] = \
'https://' + docker_machine_ip + ':2376'
client_kwargs['tls'] = docker.tls.TLSConfig(
client_cert=(docker_machine_tls['ClientCertPath'],
docker_machine_tls['ClientKeyPath']),
ca_cert=docker_machine_tls['CaCertPath'],
assert_hostname=False,
verify=True)
except Exception as exc:
raise CommandExecutionError(
'Docker machine {0} failed: {1}'.format(docker_machine, exc))
try:
# docker-py 2.0 renamed this client attribute
ret = docker.APIClient(**client_kwargs)
except AttributeError:
ret = docker.Client(**client_kwargs)
log.debug('docker-py API version: %s', getattr(ret, 'api_version', None))
return ret
def _get_state(inspect_results):
'''
Helper for deriving the current state of the container from the inspect
results.
'''
if inspect_results.get('State', {}).get('Paused', False):
return 'paused'
elif inspect_results.get('State', {}).get('Running', False):
return 'running'
else:
return 'stopped'
# Decorators
def _docker_client(wrapped):
'''
Decorator to run a function that requires the use of a docker.Client()
instance.
'''
@functools.wraps(wrapped)
def wrapper(*args, **kwargs):
'''
Ensure that the client is present
'''
kwargs = __utils__['args.clean_kwargs'](**kwargs)
timeout = kwargs.pop('client_timeout', NOTSET)
if 'docker.client' not in __context__ \
or not hasattr(__context__['docker.client'], 'timeout'):
__context__['docker.client'] = _get_client(
timeout=timeout, **kwargs)
orig_timeout = None
if timeout is not NOTSET \
and hasattr(__context__['docker.client'], 'timeout') \
and __context__['docker.client'].timeout != timeout:
# Temporarily override timeout
orig_timeout = __context__['docker.client'].timeout
__context__['docker.client'].timeout = timeout
ret = wrapped(*args, **kwargs)
if orig_timeout is not None:
__context__['docker.client'].timeout = orig_timeout
return ret
return wrapper
def _refresh_mine_cache(wrapped):
'''
Decorator to trigger a refresh of salt mine data.
'''
@functools.wraps(wrapped)
def wrapper(*args, **kwargs):
'''
refresh salt mine on exit.
'''
returned = wrapped(*args, **__utils__['args.clean_kwargs'](**kwargs))
if _check_update_mine():
__salt__['mine.send'](
'docker.ps', verbose=True, all=True, host=True)
return returned
return wrapper
def _check_update_mine():
try:
ret = __context__['docker.update_mine']
except KeyError:
ret = __context__['docker.update_mine'] = __salt__[
'config.get']('docker.update_mine', default=True)
return ret
# Helper functions
def _change_state(name, action, expected, *args, **kwargs):
'''
Change the state of a container
'''
pre = state(name)
if action != 'restart' and pre == expected:
return {'result': False,
'state': {'old': expected, 'new': expected},
'comment': ('Container \'{0}\' already {1}'
.format(name, expected))}
_client_wrapper(action, name, *args, **kwargs)
_clear_context()
try:
post = state(name)
except CommandExecutionError:
# Container doesn't exist anymore
post = None
ret = {'result': post == expected,
'state': {'old': pre, 'new': post}}
return ret
def _clear_context():
'''
Clear the state/exists values stored in context
'''
# Can't use 'for key in __context__' or six.iterkeys(__context__) because
# an exception will be raised if the size of the dict is modified during
# iteration.
keep_context = (
'docker.client', 'docker.exec_driver', 'docker._pull_status',
'docker.docker_version', 'docker.docker_py_version'
)
for key in list(__context__):
try:
if key.startswith('docker.') and key not in keep_context:
__context__.pop(key)
except AttributeError:
pass
def _get_md5(name, path):
'''
Get the MD5 checksum of a file from a container
'''
output = run_stdout(name,
'md5sum {0}'.format(pipes.quote(path)),
ignore_retcode=True)
try:
return output.split()[0]
except IndexError:
# Destination file does not exist or could not be accessed
return None
def _get_exec_driver():
'''
Get the method to be used in shell commands
'''
contextkey = 'docker.exec_driver'
if contextkey not in __context__:
from_config = __salt__['config.get'](contextkey, None)
# This if block can be removed once we make docker-exec a default
# option, as it is part of the logic in the commented block above.
if from_config is not None:
__context__[contextkey] = from_config
return from_config
# The execution driver was removed in Docker 1.13.1, docker-exec is now
# the default.
driver = info().get('ExecutionDriver', 'docker-exec')
if driver == 'docker-exec':
__context__[contextkey] = driver
elif driver.startswith('lxc-'):
__context__[contextkey] = 'lxc-attach'
elif driver.startswith('native-') and HAS_NSENTER:
__context__[contextkey] = 'nsenter'
elif not driver.strip() and HAS_NSENTER:
log.warning(
'ExecutionDriver from \'docker info\' is blank, falling '
'back to using \'nsenter\'. To squelch this warning, set '
'docker.exec_driver. See the Salt documentation for the '
'docker module for more information.'
)
__context__[contextkey] = 'nsenter'
else:
raise NotImplementedError(
'Unknown docker ExecutionDriver \'{0}\', or didn\'t find '
'command to attach to the container'.format(driver)
)
return __context__[contextkey]
def _get_top_level_images(imagedata, subset=None):
'''
Returns a list of the top-level images (those which are not parents). If
``subset`` (an iterable) is passed, the top-level images in the subset will
be returned, otherwise all top-level images will be returned.
'''
try:
parents = [imagedata[x]['ParentId'] for x in imagedata]
filter_ = subset if subset is not None else imagedata
return [x for x in filter_ if x not in parents]
except (KeyError, TypeError):
raise CommandExecutionError(
'Invalid image data passed to _get_top_level_images(). Please '
'report this issue. Full image data: {0}'.format(imagedata)
)
def _prep_pull():
'''
Populate __context__ with the current (pre-pull) image IDs (see the
docstring for _pull_status for more information).
'''
__context__['docker._pull_status'] = [x[:12] for x in images(all=True)]
def _scrub_links(links, name):
'''
Remove container name from HostConfig:Links values to enable comparing
container configurations correctly.
'''
if isinstance(links, list):
ret = []
for l in links:
ret.append(l.replace('/{0}/'.format(name), '/', 1))
else:
ret = links
return ret
def _ulimit_sort(ulimit_val):
if isinstance(ulimit_val, list):
return sorted(ulimit_val,
key=lambda x: (x.get('Name'),
x.get('Hard', 0),
x.get('Soft', 0)))
return ulimit_val
def _size_fmt(num):
'''
Format bytes as human-readable file sizes
'''
try:
num = int(num)
if num < 1024:
return '{0} bytes'.format(num)
num /= 1024.0
for unit in ('KiB', 'MiB', 'GiB', 'TiB', 'PiB'):
if num < 1024.0:
return '{0:3.1f} {1}'.format(num, unit)
num /= 1024.0
except Exception:
log.error('Unable to format file size for \'%s\'', num)
return 'unknown'
@_docker_client
def _client_wrapper(attr, *args, **kwargs):
'''
Common functionality for running low-level API calls
'''
catch_api_errors = kwargs.pop('catch_api_errors', True)
func = getattr(__context__['docker.client'], attr, None)
if func is None or not hasattr(func, '__call__'):
raise SaltInvocationError('Invalid client action \'{0}\''.format(attr))
if attr in ('push', 'pull'):
try:
# Refresh auth config from config.json
__context__['docker.client'].reload_config()
except AttributeError:
pass
err = ''
try:
log.debug(
'Attempting to run docker-py\'s "%s" function '
'with args=%s and kwargs=%s', attr, args, kwargs
)
ret = func(*args, **kwargs)
except docker.errors.APIError as exc:
if catch_api_errors:
# Generic handling of Docker API errors
raise CommandExecutionError(
'Error {0}: {1}'.format(exc.response.status_code,
exc.explanation)
)
else:
# Allow API errors to be caught further up the stack
raise
except docker.errors.DockerException as exc:
# More general docker exception (catches InvalidVersion, etc.)
raise CommandExecutionError(exc.__str__())
except Exception as exc:
err = exc.__str__()
else:
return ret
# If we're here, it's because an exception was caught earlier, and the
# API command failed.
msg = 'Unable to perform {0}'.format(attr)
if err:
msg += ': {0}'.format(err)
raise CommandExecutionError(msg)
def _build_status(data, item):
'''
Process a status update from a docker build, updating the data structure
'''
stream = item['stream']
if 'Running in' in stream:
data.setdefault('Intermediate_Containers', []).append(
stream.rstrip().split()[-1])
if 'Successfully built' in stream:
data['Id'] = stream.rstrip().split()[-1]
def _import_status(data, item, repo_name, repo_tag):
'''
Process a status update from docker import, updating the data structure
'''
status = item['status']
try:
if 'Downloading from' in status:
return
elif all(x in string.hexdigits for x in status):
# Status is an image ID
data['Image'] = '{0}:{1}'.format(repo_name, repo_tag)
data['Id'] = status
except (AttributeError, TypeError):
pass
def _pull_status(data, item):
'''
Process a status update from a docker pull, updating the data structure.
For containers created with older versions of Docker, there is no
distinction in the status updates between layers that were already present
(and thus not necessary to download), and those which were actually
downloaded. Because of this, any function that needs to invoke this
function needs to pre-fetch the image IDs by running _prep_pull() in any
function that calls _pull_status(). It is important to grab this
information before anything is pulled so we aren't looking at the state of
the images post-pull.
We can't rely on the way that __context__ is utilized by the images()
function, because by design we clear the relevant context variables once
we've made changes to allow the next call to images() to pick up any
changes that were made.
'''
def _already_exists(id_):
'''
Layer already exists
'''
already_pulled = data.setdefault('Layers', {}).setdefault(
'Already_Pulled', [])
if id_ not in already_pulled:
already_pulled.append(id_)
def _new_layer(id_):
'''
Pulled a new layer
'''
pulled = data.setdefault('Layers', {}).setdefault(
'Pulled', [])
if id_ not in pulled:
pulled.append(id_)
if 'docker._pull_status' not in __context__:
log.warning(
'_pull_status context variable was not populated, information on '
'downloaded layers may be inaccurate. Please report this to the '
'SaltStack development team, and if possible include the image '
'(and tag) that was being pulled.'
)
__context__['docker._pull_status'] = NOTSET
status = item['status']
if status == 'Already exists':
_already_exists(item['id'])
elif status in 'Pull complete':
_new_layer(item['id'])
elif status.startswith('Status: '):
data['Status'] = status[8:]
elif status == 'Download complete':
if __context__['docker._pull_status'] is not NOTSET:
id_ = item['id']
if id_ in __context__['docker._pull_status']:
_already_exists(id_)
else:
_new_layer(id_)
def _push_status(data, item):
'''
Process a status update from a docker push, updating the data structure
'''
status = item['status'].lower()
if 'id' in item:
if 'already pushed' in status or 'already exists' in status:
# Layer already exists
already_pushed = data.setdefault('Layers', {}).setdefault(
'Already_Pushed', [])
already_pushed.append(item['id'])
elif 'successfully pushed' in status or status == 'pushed':
# Pushed a new layer
pushed = data.setdefault('Layers', {}).setdefault(
'Pushed', [])
pushed.append(item['id'])
def _error_detail(data, item):
'''
Process an API error, updating the data structure
'''
err = item['errorDetail']
if 'code' in err:
try:
msg = ': '.join((
item['errorDetail']['code'],
item['errorDetail']['message']
))
except TypeError:
msg = '{0}: {1}'.format(
item['errorDetail']['code'],
item['errorDetail']['message'],
)
else:
msg = item['errorDetail']['message']
data.append(msg)
# Functions to handle docker-py client args
def get_client_args(limit=None):
'''
.. versionadded:: 2016.3.6,2016.11.4,2017.7.0
.. versionchanged:: 2017.7.0
Replaced the container config args with the ones from the API's
``create_container`` function.
.. versionchanged:: 2018.3.0
Added ability to limit the input to specific client functions
Many functions in Salt have been written to support the full list of
arguments for a given function in the `docker-py Low-level API`_. However,
depending on the version of docker-py installed on the minion, the
available arguments may differ. This function will get the arguments for
various functions in the installed version of docker-py, to be used as a
reference.
limit
An optional list of categories for which to limit the return. This is
useful if only a specific set of arguments is desired, and also keeps
other function's argspecs from needlessly being examined.
**AVAILABLE LIMITS**
- ``create_container`` - arguments accepted by `create_container()`_ (used
by :py:func:`docker.create <salt.modules.dockermod.create>`)
- ``host_config`` - arguments accepted by `create_host_config()`_ (used to
build the host config for :py:func:`docker.create
<salt.modules.dockermod.create>`)
- ``connect_container_to_network`` - arguments used by
`connect_container_to_network()`_ to construct an endpoint config when
connecting to a network (used by
:py:func:`docker.connect_container_to_network
<salt.modules.dockermod.connect_container_to_network>`)
- ``create_network`` - arguments accepted by `create_network()`_ (used by
:py:func:`docker.create_network <salt.modules.dockermod.create_network>`)
- ``ipam_config`` - arguments used to create an `IPAM pool`_ (used by
:py:func:`docker.create_network <salt.modules.dockermod.create_network>`
in the process of constructing an IPAM config dictionary)
CLI Example:
.. code-block:: bash
salt myminion docker.get_client_args
salt myminion docker.get_client_args logs
salt myminion docker.get_client_args create_container,connect_container_to_network
'''
return __utils__['docker.get_client_args'](limit=limit)
def _get_create_kwargs(skip_translate=None,
ignore_collisions=False,
validate_ip_addrs=True,
client_args=None,
**kwargs):
'''
Take input kwargs and return a kwargs dict to pass to docker-py's
create_container() function.
'''
kwargs = __utils__['docker.translate_input'](
salt.utils.docker.translate.container,
skip_translate=skip_translate,
ignore_collisions=ignore_collisions,
validate_ip_addrs=validate_ip_addrs,
**__utils__['args.clean_kwargs'](**kwargs))
if client_args is None:
try:
client_args = get_client_args(['create_container', 'host_config'])
except CommandExecutionError as exc:
log.error('docker.create: Error getting client args: \'%s\'',
exc.__str__(), exc_info=True)
raise CommandExecutionError(
'Failed to get client args: {0}'.format(exc))
full_host_config = {}
host_kwargs = {}
create_kwargs = {}
# Using list() becausee we'll be altering kwargs during iteration
for arg in list(kwargs):
if arg in client_args['host_config']:
host_kwargs[arg] = kwargs.pop(arg)
continue
if arg in client_args['create_container']:
if arg == 'host_config':
full_host_config.update(kwargs.pop(arg))
else:
create_kwargs[arg] = kwargs.pop(arg)
continue
create_kwargs['host_config'] = \
_client_wrapper('create_host_config', **host_kwargs)
# In the event that a full host_config was passed, overlay it on top of the
# one we just created.
create_kwargs['host_config'].update(full_host_config)
# The "kwargs" dict at this point will only contain unused args
return create_kwargs, kwargs
def compare_containers(first, second, ignore=None):
'''
.. versionadded:: 2017.7.0
.. versionchanged:: 2018.3.0
Renamed from ``docker.compare_container`` to
``docker.compare_containers`` (old function name remains as an alias)
Compare two containers' Config and and HostConfig and return any
differences between the two.
first
Name or ID of first container
second
Name or ID of second container
ignore
A comma-separated list (or Python list) of keys to ignore when
comparing. This is useful when comparing two otherwise identical
containers which have different hostnames.
CLI Examples:
.. code-block:: bash
salt myminion docker.compare_containers foo bar
salt myminion docker.compare_containers foo bar ignore=Hostname
'''
ignore = __utils__['args.split_input'](ignore or [])
result1 = inspect_container(first)
result2 = inspect_container(second)
ret = {}
for conf_dict in ('Config', 'HostConfig'):
for item in result1[conf_dict]:
if item in ignore:
continue
val1 = result1[conf_dict][item]
val2 = result2[conf_dict].get(item)
if item in ('OomKillDisable',) or (val1 is None or val2 is None):
if bool(val1) != bool(val2):
ret.setdefault(conf_dict, {})[item] = {
'old': val1, 'new': val2}
elif item == 'Image':
image1 = inspect_image(val1)['Id']
image2 = inspect_image(val2)['Id']
if image1 != image2:
ret.setdefault(conf_dict, {})[item] = {
'old': image1, 'new': image2}
else:
if item == 'Links':
val1 = sorted(_scrub_links(val1, first))
val2 = sorted(_scrub_links(val2, second))
if item == 'Ulimits':
val1 = _ulimit_sort(val1)
val2 = _ulimit_sort(val2)
if item == 'Env':
val1 = sorted(val1)
val2 = sorted(val2)
if val1 != val2:
ret.setdefault(conf_dict, {})[item] = {
'old': val1, 'new': val2}
# Check for optionally-present items that were in the second container
# and not the first.
for item in result2[conf_dict]:
if item in ignore or item in ret.get(conf_dict, {}):
# We're either ignoring this or we already processed this
# when iterating through result1. Either way, skip it.
continue
val1 = result1[conf_dict].get(item)
val2 = result2[conf_dict][item]
if item in ('OomKillDisable',) or (val1 is None or val2 is None):
if bool(val1) != bool(val2):
ret.setdefault(conf_dict, {})[item] = {
'old': val1, 'new': val2}
elif item == 'Image':
image1 = inspect_image(val1)['Id']
image2 = inspect_image(val2)['Id']
if image1 != image2:
ret.setdefault(conf_dict, {})[item] = {
'old': image1, 'new': image2}
else:
if item == 'Links':
val1 = sorted(_scrub_links(val1, first))
val2 = sorted(_scrub_links(val2, second))
if item == 'Ulimits':
val1 = _ulimit_sort(val1)
val2 = _ulimit_sort(val2)
if item == 'Env':
val1 = sorted(val1)
val2 = sorted(val2)
if val1 != val2:
ret.setdefault(conf_dict, {})[item] = {
'old': val1, 'new': val2}
return ret
compare_container = salt.utils.functools.alias_function(
compare_containers,
'compare_container')