qb 0.3.25 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/ansible.cfg +10 -1
- data/exe/.qb_interop_receive +3 -10
- data/exe/qb +8 -2
- data/lib/python/qb/__init__.py +6 -0
- data/{roles/qb/ruby/rspec/setup/tasks/persistence.yml → lib/python/qb/ansible/__init__.py} +0 -0
- data/lib/python/qb/ansible/modules/__init__.py +0 -0
- data/lib/python/qb/ansible/modules/docker/__init__.py +0 -0
- data/lib/python/qb/ansible/modules/docker/client.py +177 -0
- data/lib/python/qb/ansible/modules/docker/image_manager.py +754 -0
- data/lib/python/qb/ipc/__init__.py +0 -0
- data/lib/python/qb/ipc/stdio/__init__.py +99 -0
- data/lib/python/qb/ipc/stdio/logging.py +151 -0
- data/lib/qb.rb +3 -3
- data/lib/qb/ansible/cmds/playbook.rb +5 -14
- data/lib/qb/ansible/env.rb +36 -6
- data/lib/qb/ansible/module.rb +396 -152
- data/lib/qb/ansible/module/response.rb +195 -0
- data/lib/qb/ansible/modules.rb +42 -0
- data/lib/qb/ansible/modules/docker/image.rb +273 -0
- data/lib/qb/cli.rb +5 -18
- data/lib/qb/cli/run.rb +2 -2
- data/lib/qb/data.rb +22 -0
- data/lib/qb/data/immutable.rb +39 -0
- data/lib/qb/docker.rb +2 -0
- data/lib/qb/docker/cli.rb +430 -0
- data/lib/qb/docker/image.rb +207 -0
- data/lib/qb/docker/image/name.rb +309 -0
- data/lib/qb/docker/image/tag.rb +113 -0
- data/lib/qb/docker/repo.rb +0 -0
- data/lib/qb/errors.rb +17 -3
- data/lib/qb/execution.rb +83 -0
- data/lib/qb/ipc.rb +48 -0
- data/lib/qb/ipc/stdio.rb +32 -0
- data/lib/qb/ipc/stdio/client.rb +267 -0
- data/lib/qb/ipc/stdio/server.rb +229 -0
- data/lib/qb/ipc/stdio/server/in_service.rb +18 -0
- data/lib/qb/ipc/stdio/server/log_service.rb +168 -0
- data/lib/qb/ipc/stdio/server/out_service.rb +20 -0
- data/lib/qb/ipc/stdio/server/service.rb +229 -0
- data/lib/qb/options.rb +360 -502
- data/lib/qb/options/option.rb +293 -115
- data/lib/qb/options/option/option_parser_concern.rb +228 -0
- data/lib/qb/options/types.rb +73 -0
- data/lib/qb/package.rb +0 -1
- data/lib/qb/package/version.rb +179 -58
- data/lib/qb/package/version/from.rb +192 -51
- data/lib/qb/package/version/leveled.rb +1 -1
- data/lib/qb/path.rb +3 -2
- data/lib/qb/repo/git.rb +9 -85
- data/lib/qb/role/default_dir.rb +2 -2
- data/lib/qb/role/errors.rb +2 -8
- data/lib/qb/util.rb +1 -2
- data/lib/qb/util/bundler.rb +73 -43
- data/lib/qb/util/decorators.rb +99 -0
- data/lib/qb/util/interop.rb +7 -8
- data/lib/qb/util/resource.rb +12 -13
- data/lib/qb/version.rb +10 -0
- data/library/path_facts +5 -10
- data/library/qb.module.rb +105 -0
- data/library/stream +6 -26
- data/load/ansible/module/autorun.rb +25 -0
- data/load/ansible/module/script.rb +123 -0
- data/load/rebundle.rb +39 -0
- data/plugins/filter/dict_filters.py +56 -0
- data/plugins/{filter_plugins/path_plugins.py → filter/path_filters.py} +0 -0
- data/plugins/{filter_plugins/ruby_interop_plugins.py → filter/ruby_interop_filters.py} +1 -17
- data/plugins/{filter_plugins/string_plugins.py → filter/string_filters.py} +1 -20
- data/plugins/{filter_plugins/version_plugins.py → filter/version_filters.py} +3 -18
- data/plugins/{lookup_plugins/every.py → lookup/every_lookups.py} +0 -0
- data/plugins/{lookup_plugins/resolve.py → lookup/resolve_lookups.py} +0 -0
- data/plugins/{lookup_plugins/version.py → lookup/version_lookups.py} +0 -16
- data/plugins/test/dict_tests.py +36 -0
- data/plugins/test/string_tests.py +36 -0
- data/qb.gemspec +7 -3
- data/roles/nrser.rb/library/set_fact_with_ruby.rb +3 -9
- data/roles/nrser.state_mate/library/state +3 -17
- data/roles/qb/call/meta/qb.yml +1 -1
- data/roles/qb/dev/ref/repo/git/meta/qb.yml +1 -1
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/defaults/main.yml +1 -1
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/main.yml +3 -2
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/qb.yml +12 -7
- data/roles/qb/docker/mac/kubernetes/tasks/main.yml +45 -0
- data/roles/qb/git/check/clean/meta/qb.yml +1 -1
- data/roles/qb/git/ignore/meta/qb +10 -3
- data/roles/qb/git/submodule/update/library/git_submodule_update +17 -27
- data/roles/qb/github/pages/setup/meta/qb.yml +1 -1
- data/roles/qb/labs/atom/apm/meta/qb.yml +1 -1
- data/roles/qb/osx/git/change_case/meta/qb.yml +1 -1
- data/roles/qb/osx/notif/meta/qb.yml +1 -1
- data/roles/qb/pkg/bump/library/bump +4 -16
- data/roles/qb/role/qb/defaults/main.yml +2 -0
- data/roles/qb/role/qb/meta/qb.yml +10 -5
- data/roles/qb/role/qb/templates/qb.yml.j2 +7 -2
- data/roles/qb/role/templates/library/module.rb.j2 +12 -23
- data/roles/qb/role/templates/meta/main.yml.j2 +14 -1
- data/roles/qb/ruby/bundler/meta/qb.yml +1 -1
- data/roles/qb/ruby/dependency/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/bin_stubs/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/bin_stubs/templates/console +8 -2
- data/roles/qb/ruby/gem/build/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/new/meta/qb.yml +1 -1
- data/roles/qb/ruby/nrser/rspex/generate/meta/qb.yml +5 -5
- data/roles/qb/ruby/nrser/rspex/issue/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/clean/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/config/library/yard.get_output_dir +5 -15
- data/roles/qb/ruby/yard/config/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/setup/meta/qb.yml +1 -1
- metadata +71 -22
- data/lib/qb/ansible_module.rb +0 -5
- data/lib/qb/util/stdio.rb +0 -187
- data/roles/qb/ruby/rspec/setup/tasks/main.yml +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dd927b2c784f288a1bdadf4f4c4a6f06882bdc6
|
4
|
+
data.tar.gz: e182c25d31078f16d5942735f0f178b916e14067
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 49174eac9d628957755b1eb620e03e49fd3b17eed4cb85727559c232948b80afea0ae4552551955da22ab4bf4cc46933272b5644cd65cd6170a400fa28e8f0f7
|
7
|
+
data.tar.gz: 4ea4f7d244e93d482de5f87d6da86b6658a087b9830b3101f2a9458c7909a83affa4546cf4314d0f6105591c9ec61e57d07f0f0e1fb0c87b9e5c8f7c3ba7cefc
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/ansible.cfg
CHANGED
@@ -1,6 +1,15 @@
|
|
1
|
+
##
|
2
|
+
# This is the Ansible config used in local dev, when I'm running `qb` from
|
3
|
+
# the project root.
|
4
|
+
#
|
5
|
+
# It should probably be at `//dev/ansible.cfg` but I tried moving it there and
|
6
|
+
# it didn't work and I'm not gonna investigate now.
|
7
|
+
#
|
8
|
+
##
|
9
|
+
|
1
10
|
[defaults]
|
2
11
|
|
3
|
-
roles_path = ./dev/scratch
|
12
|
+
roles_path = ./dev/scratch/roles
|
4
13
|
retry_files_enabled = False
|
5
14
|
|
6
15
|
# Will cause Ansible to cache facts at `//tmp/facts_cache/localhost` in JSON
|
data/exe/.qb_interop_receive
CHANGED
@@ -8,15 +8,8 @@
|
|
8
8
|
# not gonna right now. At least it's not a string in the Python file anymore.
|
9
9
|
#
|
10
10
|
|
11
|
-
#
|
12
|
-
if ENV['
|
13
|
-
ENV.each {|k, v|
|
14
|
-
if k.start_with? 'QB_DEV_ENV_'
|
15
|
-
ENV[k.sub('QB_DEV_ENV_', '')] = v
|
16
|
-
end
|
17
|
-
}
|
18
|
-
require 'bundler/setup'
|
19
|
-
end
|
11
|
+
# Reinstate Bundler ENV vars if they have been moved
|
12
|
+
load ENV['QB_REBUNDLE_PATH'] if ENV['QB_REBUNDLE_PATH']
|
20
13
|
|
21
14
|
# Set the thread name so that logs make sense.
|
22
15
|
require 'thread'
|
@@ -29,7 +22,7 @@ require 'qb'
|
|
29
22
|
if ENV['QB_STDIO_ERR']
|
30
23
|
$stderr = UNIXSocket.new ENV['QB_STDIO_ERR']
|
31
24
|
|
32
|
-
NRSER::
|
25
|
+
NRSER::Log.setup_for_cli! application: 'qb'
|
33
26
|
|
34
27
|
QB.debug "Connected to QB stderr stream at #{ ENV['QB_STDIO_ERR'] } #{ $stderr.path }."
|
35
28
|
end
|
data/exe/qb
CHANGED
@@ -26,6 +26,9 @@ require 'cmds'
|
|
26
26
|
require 'qb'
|
27
27
|
|
28
28
|
|
29
|
+
# QB::IPC.is_master_process!
|
30
|
+
|
31
|
+
|
29
32
|
# Refinements
|
30
33
|
# =======================================================================
|
31
34
|
|
@@ -37,10 +40,10 @@ using NRSER
|
|
37
40
|
|
38
41
|
def main *args
|
39
42
|
Thread.current.name = 'main'
|
40
|
-
logger = NRSER::
|
43
|
+
logger = NRSER::Log['qb/exe/qb#main']
|
41
44
|
|
42
45
|
QB::CLI.set_debug! args
|
43
|
-
NRSER::
|
46
|
+
NRSER::Log.setup_for_cli! application: 'qb'
|
44
47
|
|
45
48
|
logger.debug args: args
|
46
49
|
|
@@ -59,6 +62,9 @@ def main *args
|
|
59
62
|
[:setup, args.rest]
|
60
63
|
when 'list', 'ls'
|
61
64
|
[:list, *args.rest]
|
65
|
+
when 'root'
|
66
|
+
puts QB::ROOT.to_s
|
67
|
+
exit true
|
62
68
|
else
|
63
69
|
# default to `run` on the full args
|
64
70
|
[:run, args]
|
data/lib/python/qb/__init__.py
CHANGED
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,177 @@
|
|
1
|
+
#!/usr/bin/python
|
2
|
+
#
|
3
|
+
# Copyright 2016 Red Hat | Ansible
|
4
|
+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
5
|
+
|
6
|
+
from __future__ import absolute_import, division, print_function
|
7
|
+
__metaclass__ = type
|
8
|
+
|
9
|
+
import json
|
10
|
+
|
11
|
+
from ansible.module_utils.docker_common import AnsibleDockerClient
|
12
|
+
|
13
|
+
import qb.ipc.stdio
|
14
|
+
import qb.ipc.stdio.logging
|
15
|
+
|
16
|
+
|
17
|
+
class QBAnsibleDockerClient(AnsibleDockerClient):
|
18
|
+
|
19
|
+
# Construction
|
20
|
+
# ========================================================================
|
21
|
+
|
22
|
+
def __init__(self, *args, **kwds):
|
23
|
+
self.logger = qb.ipc.stdio.logging.getLogger(
|
24
|
+
'qb.ansible.modules.docker.client.QBAnsibleDockerClient'
|
25
|
+
)
|
26
|
+
|
27
|
+
AnsibleDockerClient.__init__(self, *args, **kwds)
|
28
|
+
|
29
|
+
|
30
|
+
# Instance Methods
|
31
|
+
# ============================================================================
|
32
|
+
|
33
|
+
# Logging and Output
|
34
|
+
# ----------------------------------------------------------------------------
|
35
|
+
|
36
|
+
def out(self, msg):
|
37
|
+
'''
|
38
|
+
Write output from the Docker daemon to STDOUT for the user to see
|
39
|
+
what's going on.
|
40
|
+
|
41
|
+
This used to be called `log` and was used for more than just logging
|
42
|
+
output from Docker, and it didn't do anything... though there was some
|
43
|
+
commented out code to write to a file.
|
44
|
+
|
45
|
+
Now it writes to the QB master process' STDOUT through
|
46
|
+
`QB::IPC::STDOUT`, assuming that's present.
|
47
|
+
|
48
|
+
:param msg - A string or dict.
|
49
|
+
|
50
|
+
:return: None
|
51
|
+
'''
|
52
|
+
|
53
|
+
# Bail unless QB STDOUT is connected
|
54
|
+
if not qb.ipc.stdio.client.stdout.connected:
|
55
|
+
return None
|
56
|
+
|
57
|
+
# What we're gonna write
|
58
|
+
string = None
|
59
|
+
|
60
|
+
if isinstance(msg, str):
|
61
|
+
# If the message is just a string, write that
|
62
|
+
string = msg
|
63
|
+
|
64
|
+
elif isinstance( msg, dict ):
|
65
|
+
# Dicts come for the Docker daemon/API, so we want to extract the
|
66
|
+
# relevant output and display that nicely... work in progress
|
67
|
+
|
68
|
+
if 'stream' in msg:
|
69
|
+
# This is part of an output 'stream' from a build,
|
70
|
+
# so just grab that that variable.
|
71
|
+
string = msg['stream']
|
72
|
+
|
73
|
+
elif 'status' in msg:
|
74
|
+
# This is part of an output from a pull (and maybe more?)
|
75
|
+
|
76
|
+
if 'id' in msg:
|
77
|
+
if 'progress' in msg:
|
78
|
+
string = "{id} {status} {progress}".format(**msg)
|
79
|
+
else:
|
80
|
+
string = "{id} {status}".format(**msg)
|
81
|
+
else:
|
82
|
+
string = msg['status']
|
83
|
+
|
84
|
+
else:
|
85
|
+
# Structures we're not sure how to deal with yet... just
|
86
|
+
# just pretty-dump them
|
87
|
+
string = json.dumps(
|
88
|
+
msg,
|
89
|
+
sort_keys=True,
|
90
|
+
indent=4,
|
91
|
+
separators=(',', ': ')
|
92
|
+
)
|
93
|
+
else:
|
94
|
+
self.logger.warning(
|
95
|
+
"Unregonized `msg` type {} in .out", type(msg),
|
96
|
+
payload=dict(msg=msg)
|
97
|
+
)
|
98
|
+
|
99
|
+
if string is not None:
|
100
|
+
qb.ipc.stdio.client.stdout.println(string)
|
101
|
+
|
102
|
+
|
103
|
+
def log(self, msg, pretty_print=False):
|
104
|
+
'''
|
105
|
+
Override :class:`AnsibleDockerClient.log` to actually do something.
|
106
|
+
|
107
|
+
It's used in :class:`AnsibleDockerClient` to do *both* logging and
|
108
|
+
relaying Docker API/daemon output, but we try to split it up and
|
109
|
+
send logging to :attr:`logger` and output to :meth:`out` - output seems
|
110
|
+
to always be `dict`, though this might be wrong.
|
111
|
+
|
112
|
+
:param msg: A stirng or dict.
|
113
|
+
:param pretty_print: Boolean, but not used - part of super API.
|
114
|
+
|
115
|
+
:return: None
|
116
|
+
'''
|
117
|
+
|
118
|
+
if isinstance(msg, dict):
|
119
|
+
self.out(msg)
|
120
|
+
elif isinstance(msg, str):
|
121
|
+
self.logger.info(msg)
|
122
|
+
else:
|
123
|
+
self.logger.warning(
|
124
|
+
"Unregonized `msg` type {} in .log".format(type(msg)),
|
125
|
+
payload=dict(msg=msg)
|
126
|
+
)
|
127
|
+
|
128
|
+
|
129
|
+
def fail(self, msg, **values):
|
130
|
+
'''
|
131
|
+
Overrides :class:`AnsibleDockerClient.fail` to log the failure first
|
132
|
+
(as `critical`/`fatal`).
|
133
|
+
|
134
|
+
Also adds feature to accept a dict of values which will be
|
135
|
+
:meth:`str.format` into the `msg` and also logged as the payload.
|
136
|
+
|
137
|
+
:param msg: String message, which may have `{key}` template markers
|
138
|
+
in it to be subsititued from `values`.
|
139
|
+
:param values: Optional dict of values to interpolate and log.
|
140
|
+
|
141
|
+
:return: See :class:`AnsibleDockerClient.fail`
|
142
|
+
'''
|
143
|
+
|
144
|
+
self.logger.critical(msg, payload=values)
|
145
|
+
|
146
|
+
return super(QBAnsibleDockerClient, self).fail(msg)
|
147
|
+
|
148
|
+
|
149
|
+
# Actions
|
150
|
+
# ------------------------------------------------------------------------
|
151
|
+
|
152
|
+
def try_pull_image(self, name, tag="latest"):
|
153
|
+
'''
|
154
|
+
Try to pull an image (before building or loading)
|
155
|
+
'''
|
156
|
+
|
157
|
+
self.logger.info(
|
158
|
+
"Attempting to pull image {}:{}".format(name, tag)
|
159
|
+
)
|
160
|
+
|
161
|
+
try:
|
162
|
+
for line in self.pull(name, tag=tag, stream=True, decode=True):
|
163
|
+
self.out(line)
|
164
|
+
|
165
|
+
if line.get('error'):
|
166
|
+
self.logger.info(
|
167
|
+
"Attempt to pull {}:{} failed".format(name, tag)
|
168
|
+
)
|
169
|
+
return None
|
170
|
+
|
171
|
+
except Exception as exc:
|
172
|
+
self.logger.warning(
|
173
|
+
"Error pulling image {}:{} - {}".format(name, tag, str(exc))
|
174
|
+
)
|
175
|
+
return None
|
176
|
+
|
177
|
+
return self.find_image(name=name, tag=tag)
|
@@ -0,0 +1,754 @@
|
|
1
|
+
#!/usr/bin/python
|
2
|
+
#
|
3
|
+
# Copyright 2016 Red Hat | Ansible
|
4
|
+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
5
|
+
|
6
|
+
|
7
|
+
# Imports
|
8
|
+
# ============================================================================
|
9
|
+
|
10
|
+
from __future__ import absolute_import, division, print_function
|
11
|
+
__metaclass__ = type
|
12
|
+
|
13
|
+
import os
|
14
|
+
import re
|
15
|
+
import json
|
16
|
+
import logging
|
17
|
+
|
18
|
+
from ansible.module_utils.docker_common import (
|
19
|
+
HAS_DOCKER_PY_2,
|
20
|
+
AnsibleDockerClient,
|
21
|
+
DockerBaseClass
|
22
|
+
)
|
23
|
+
|
24
|
+
from ansible.module_utils._text import to_native
|
25
|
+
|
26
|
+
try:
|
27
|
+
if HAS_DOCKER_PY_2:
|
28
|
+
from docker.auth import resolve_repository_name
|
29
|
+
else:
|
30
|
+
from docker.auth.auth import resolve_repository_name
|
31
|
+
from docker.utils.utils import parse_repository_tag
|
32
|
+
except ImportError:
|
33
|
+
# missing docker-py handled in docker_common
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
import qb.ipc.stdio
|
38
|
+
import qb.ipc.stdio.logging
|
39
|
+
|
40
|
+
|
41
|
+
# Globals
|
42
|
+
# ============================================================================
|
43
|
+
|
44
|
+
logger = qb.ipc.stdio.logging.getLogger('qb_docker_image')
|
45
|
+
|
46
|
+
|
47
|
+
# Casses
|
48
|
+
# ============================================================================
|
49
|
+
|
50
|
+
class ImageManager(DockerBaseClass):
|
51
|
+
'''
|
52
|
+
Adaptation and extension of the `ImageManager` class from Ansible's
|
53
|
+
`docker_image` module for QB's `qb_docker_image` module.
|
54
|
+
'''
|
55
|
+
|
56
|
+
# Construction
|
57
|
+
# ========================================================================
|
58
|
+
|
59
|
+
def __init__(self, client, results):
|
60
|
+
|
61
|
+
super(ImageManager, self).__init__()
|
62
|
+
|
63
|
+
self.client = client
|
64
|
+
self.results = results
|
65
|
+
parameters = self.client.module.params
|
66
|
+
self.check_mode = self.client.check_mode
|
67
|
+
|
68
|
+
self.archive_path = parameters.get('archive_path')
|
69
|
+
self.container_limits = parameters.get('container_limits')
|
70
|
+
self.dockerfile = parameters.get('dockerfile')
|
71
|
+
self.force = parameters.get('force')
|
72
|
+
self.load_path = parameters.get('load_path')
|
73
|
+
self.name = parameters.get('name')
|
74
|
+
self.nocache = parameters.get('nocache')
|
75
|
+
self.path = parameters.get('path')
|
76
|
+
self.pull = parameters.get('pull')
|
77
|
+
self.repository = parameters.get('repository')
|
78
|
+
self.rm = parameters.get('rm')
|
79
|
+
self.state = parameters.get('state')
|
80
|
+
self.tag = parameters.get('tag')
|
81
|
+
self.http_timeout = parameters.get('http_timeout')
|
82
|
+
self.push = parameters.get('push')
|
83
|
+
self.buildargs = parameters.get('buildargs')
|
84
|
+
|
85
|
+
# QB additions
|
86
|
+
self.try_to_pull = parameters.get('try_to_pull')
|
87
|
+
|
88
|
+
self.logger = qb.ipc.stdio.logging.getLogger(
|
89
|
+
'qb_docker_image:ImageManager',
|
90
|
+
)
|
91
|
+
|
92
|
+
# If name contains a tag, it takes precedence over tag parameter.
|
93
|
+
repo, repo_tag = parse_repository_tag(self.name)
|
94
|
+
if repo_tag:
|
95
|
+
self.name = repo
|
96
|
+
self.tag = repo_tag
|
97
|
+
|
98
|
+
if self.state in ['present', 'build']:
|
99
|
+
self.present()
|
100
|
+
elif self.state == 'absent':
|
101
|
+
self.absent()
|
102
|
+
|
103
|
+
# END __init__
|
104
|
+
|
105
|
+
|
106
|
+
# Instance Methods
|
107
|
+
# ========================================================================
|
108
|
+
|
109
|
+
# Helpers
|
110
|
+
# ------------------------------------------------------------------------
|
111
|
+
|
112
|
+
def out(self, msg):
|
113
|
+
'''
|
114
|
+
Just proxies to :attr:`client.out`, which writes to the master QB
|
115
|
+
process' STDOUT (if available).
|
116
|
+
|
117
|
+
:param msg: String or dict with output, but should usually be a
|
118
|
+
dict with Docker API/daemon output in this case; log
|
119
|
+
messages using the :attr:`logger`.
|
120
|
+
|
121
|
+
:return: None
|
122
|
+
'''
|
123
|
+
self.client.out(msg)
|
124
|
+
|
125
|
+
|
126
|
+
def warn(self, warning, **values):
|
127
|
+
'''
|
128
|
+
Append a warning message to the `warnings` array of :attr:`results`
|
129
|
+
that will be returned to Ansible and log it as a warning.
|
130
|
+
'''
|
131
|
+
warning = str(warning)
|
132
|
+
self.results['warnings'].append(warning.format(**values))
|
133
|
+
self.logger.warning(warning, payload=values)
|
134
|
+
|
135
|
+
|
136
|
+
def fail(self, msg, **values):
|
137
|
+
'''
|
138
|
+
Proxy to :attr:`client.fail`.
|
139
|
+
'''
|
140
|
+
self.client.fail(msg, **values)
|
141
|
+
|
142
|
+
|
143
|
+
def append_action(self, msg, **values):
|
144
|
+
'''
|
145
|
+
Add a message to the `actions` array in :attr:`results` and log it
|
146
|
+
(as `info`).
|
147
|
+
|
148
|
+
I'm not sure what 'actions' are for, but they were here when I adapted
|
149
|
+
it... maybe something to do with "check mode"?
|
150
|
+
'''
|
151
|
+
|
152
|
+
formatted = msg.format(**values)
|
153
|
+
self.logger.info(formatted, payload=values)
|
154
|
+
self.results['actions'].append(formatted)
|
155
|
+
|
156
|
+
|
157
|
+
def image_summary(self, image):
|
158
|
+
return dict(
|
159
|
+
Id = image['Id'],
|
160
|
+
RepoTags = image['RepoTags'],
|
161
|
+
Created = image['Created'],
|
162
|
+
Metadata = image['Metadata'],
|
163
|
+
)
|
164
|
+
|
165
|
+
|
166
|
+
# States
|
167
|
+
# ------------------------------------------------------------------------
|
168
|
+
|
169
|
+
def present(self):
|
170
|
+
'''
|
171
|
+
Handles state = 'present', which includes building, loading or pulling
|
172
|
+
an image, depending on user provided parameters.
|
173
|
+
|
174
|
+
:return: None
|
175
|
+
'''
|
176
|
+
|
177
|
+
self.logger.debug(
|
178
|
+
"Starting state `present`...",
|
179
|
+
payload = dict(
|
180
|
+
name = self.name,
|
181
|
+
tag = self.tag,
|
182
|
+
)
|
183
|
+
)
|
184
|
+
|
185
|
+
existing_image = self.client.find_image(name=self.name, tag=self.tag)
|
186
|
+
|
187
|
+
if existing_image:
|
188
|
+
self.logger.info(
|
189
|
+
"Found existing image `{find_name}` in local daemon",
|
190
|
+
payload = dict(
|
191
|
+
find_name = "{}:{}".format(self.name, self.tag),
|
192
|
+
**self.image_summary(existing_image)
|
193
|
+
)
|
194
|
+
)
|
195
|
+
|
196
|
+
# Keep track of what images we get from where
|
197
|
+
pulled_image = None
|
198
|
+
built_image = None
|
199
|
+
loaded_image = None
|
200
|
+
|
201
|
+
if not existing_image or self.force:
|
202
|
+
# Try to pull if we're not forcing (which means we want to
|
203
|
+
# re-build/load regardless) and `self.try_to_pull` is `True`
|
204
|
+
if not self.force and self.try_to_pull:
|
205
|
+
self.append_action(
|
206
|
+
'Tried to pull image `{name}:{tag}`',
|
207
|
+
name = self.name,
|
208
|
+
tag = self.tag
|
209
|
+
)
|
210
|
+
|
211
|
+
self.results['changed'] = True
|
212
|
+
|
213
|
+
if not self.check_mode:
|
214
|
+
pulled_image = self.client.try_pull_image(
|
215
|
+
self.name,
|
216
|
+
tag=self.tag
|
217
|
+
)
|
218
|
+
|
219
|
+
if pulled_image:
|
220
|
+
self.append_action(
|
221
|
+
'Pulled image `{name}:{tag}`',
|
222
|
+
name = self.name,
|
223
|
+
tag = self.tag
|
224
|
+
)
|
225
|
+
self.results['image'] = pulled_image
|
226
|
+
# END if not self.force and self.try_to_pull:
|
227
|
+
|
228
|
+
if pulled_image is None:
|
229
|
+
if self.path:
|
230
|
+
# Build the image
|
231
|
+
if not os.path.isdir(self.path):
|
232
|
+
self.fail(
|
233
|
+
"Requested build path `{path}` could not be " +
|
234
|
+
"found or you do not have access.",
|
235
|
+
path = self.path,
|
236
|
+
)
|
237
|
+
|
238
|
+
image_name = self.name
|
239
|
+
if self.tag:
|
240
|
+
image_name = "%s:%s" % (self.name, self.tag)
|
241
|
+
|
242
|
+
self.logger.info(
|
243
|
+
"Building image `{image_name}`",
|
244
|
+
payload = dict(
|
245
|
+
image_name = image_name,
|
246
|
+
)
|
247
|
+
)
|
248
|
+
|
249
|
+
self.append_action(
|
250
|
+
"Built image `{image_name}` from `{path}`",
|
251
|
+
image_name = image_name,
|
252
|
+
path = self.path,
|
253
|
+
)
|
254
|
+
|
255
|
+
self.results['changed'] = True
|
256
|
+
|
257
|
+
if not self.check_mode:
|
258
|
+
built_image = self.build_image()
|
259
|
+
self.results['image'] = built_image
|
260
|
+
|
261
|
+
elif self.load_path:
|
262
|
+
|
263
|
+
# Load the image from an archive
|
264
|
+
if not os.path.isfile(self.load_path):
|
265
|
+
self.fail(
|
266
|
+
"Error loading image `{name}`. " +
|
267
|
+
"Specified load path `{load_path}` does not exist.",
|
268
|
+
name = self.name,
|
269
|
+
load_path = self.load_path,
|
270
|
+
)
|
271
|
+
|
272
|
+
image_name = self.name
|
273
|
+
|
274
|
+
if self.tag:
|
275
|
+
image_name = "%s:%s" % (self.name, self.tag)
|
276
|
+
|
277
|
+
self.append_action(
|
278
|
+
"Loaded image `{image_name}` from `{load_path}`",
|
279
|
+
image_name = image_name,
|
280
|
+
load_path = self.load_path,
|
281
|
+
)
|
282
|
+
|
283
|
+
self.results['changed'] = True
|
284
|
+
|
285
|
+
if not self.check_mode:
|
286
|
+
loaded_image = self.load_image()
|
287
|
+
self.results['image'] = loaded_image
|
288
|
+
|
289
|
+
else:
|
290
|
+
# pull the image
|
291
|
+
self.append_action(
|
292
|
+
'Pulled image `{name}:{tag}`',
|
293
|
+
name = self.name,
|
294
|
+
tag = self.tag,
|
295
|
+
)
|
296
|
+
|
297
|
+
self.results['changed'] = True
|
298
|
+
|
299
|
+
if not self.check_mode:
|
300
|
+
pulled_image = self.client.pull_image(
|
301
|
+
self.name,
|
302
|
+
tag = self.tag,
|
303
|
+
)
|
304
|
+
|
305
|
+
self.results['image'] = pulled_image
|
306
|
+
|
307
|
+
if (
|
308
|
+
existing_image and
|
309
|
+
existing_image == self.results['image']
|
310
|
+
):
|
311
|
+
self.results['changed'] = False
|
312
|
+
# END if pulled_image is None:
|
313
|
+
# END if not image or self.force:
|
314
|
+
|
315
|
+
# Archive the image if we have an archive path
|
316
|
+
if self.archive_path:
|
317
|
+
self.archive_image(self.name, self.tag)
|
318
|
+
|
319
|
+
# Tag the image to a repository if we have one
|
320
|
+
if self.repository:
|
321
|
+
self.tag_image(
|
322
|
+
self.name,
|
323
|
+
self.tag,
|
324
|
+
self.repository,
|
325
|
+
force = self.force,
|
326
|
+
push = self.push,
|
327
|
+
)
|
328
|
+
|
329
|
+
# This is weird to me logically, but I'm attempting to stick to the
|
330
|
+
# Ansible `docker_image` module behavior...
|
331
|
+
#
|
332
|
+
# We only push to the default repository (Docker Hub) if we didn't
|
333
|
+
# receive a `self.respository`. I guess this is for when you're using
|
334
|
+
# another repo than Docker Hub that you provide as `repository` and
|
335
|
+
# then it assumes you would never want to push to Docker Hub *too*.
|
336
|
+
#
|
337
|
+
# OK, that kinda makes sense to me...
|
338
|
+
#
|
339
|
+
elif self.push:
|
340
|
+
if pulled_image is not None:
|
341
|
+
# Regadless of anything, we never want to push an image we
|
342
|
+
# just pulled... makes no sense.
|
343
|
+
self.logger.debug(
|
344
|
+
"Image was pulled from repo, not pushing",
|
345
|
+
payload=self.image_summary(pulled_image)
|
346
|
+
)
|
347
|
+
|
348
|
+
else:
|
349
|
+
# Ok, now we can look at pushing...
|
350
|
+
if self.force:
|
351
|
+
# We're forcing, so force the push
|
352
|
+
self.logger.info(
|
353
|
+
"FORCING push of image `{name}:{tag}`...",
|
354
|
+
payload = dict(
|
355
|
+
name = self.name,
|
356
|
+
tag = self.tag,
|
357
|
+
)
|
358
|
+
)
|
359
|
+
self.push_image(self.name, self.tag)
|
360
|
+
|
361
|
+
else:
|
362
|
+
# We only want to push if we built or loaded an image
|
363
|
+
if built_image is not None:
|
364
|
+
self.logger.info(
|
365
|
+
"Pushing built image `{name}:{tag}`",
|
366
|
+
payload = dict(
|
367
|
+
name = self.name,
|
368
|
+
tag = self.tag,
|
369
|
+
**self.image_summary(built_image)
|
370
|
+
)
|
371
|
+
)
|
372
|
+
self.push_image(self.name, self.tag)
|
373
|
+
|
374
|
+
elif loaded_image is not None:
|
375
|
+
self.logger.info(
|
376
|
+
"Pushing loaded image `{name}:{tag}`",
|
377
|
+
payload = dict(
|
378
|
+
name = self.name,
|
379
|
+
tag = self.tag,
|
380
|
+
**self.image_summary(loaded_image)
|
381
|
+
)
|
382
|
+
)
|
383
|
+
self.push_image(self.name, self.tag)
|
384
|
+
|
385
|
+
else:
|
386
|
+
self.logger.info(
|
387
|
+
"No image built or loaded, not pushing"
|
388
|
+
)
|
389
|
+
|
390
|
+
# END if self.force / else
|
391
|
+
# END if pulled_image is not None / else
|
392
|
+
# END if self.repository / elif self.push
|
393
|
+
|
394
|
+
# QB addition - set the existing image as the result. Not sure why
|
395
|
+
# the Ansible version doesn't do this..?
|
396
|
+
if not self.results['image'] and existing_image is not None:
|
397
|
+
self.results['image'] = existing_image
|
398
|
+
|
399
|
+
self.logger.debug(
|
400
|
+
"State `present` done",
|
401
|
+
payload = dict(
|
402
|
+
results = self.results,
|
403
|
+
)
|
404
|
+
)
|
405
|
+
# END present()
|
406
|
+
|
407
|
+
|
408
|
+
def absent(self):
|
409
|
+
'''
|
410
|
+
Handles state = 'absent', which removes an image.
|
411
|
+
|
412
|
+
:return None
|
413
|
+
'''
|
414
|
+
image = self.client.find_image(self.name, self.tag)
|
415
|
+
if image:
|
416
|
+
name = self.name
|
417
|
+
if self.tag:
|
418
|
+
name = "%s:%s" % (self.name, self.tag)
|
419
|
+
if not self.check_mode:
|
420
|
+
try:
|
421
|
+
self.client.remove_image(name, force=self.force)
|
422
|
+
except Exception as exc:
|
423
|
+
self.fail("Error removing image %s - %s" % (name, str(exc)))
|
424
|
+
|
425
|
+
self.results['changed'] = True
|
426
|
+
self.append_action(
|
427
|
+
"Removed image `{name}`",
|
428
|
+
name = name,
|
429
|
+
)
|
430
|
+
self.results['image']['state'] = 'Deleted'
|
431
|
+
|
432
|
+
|
433
|
+
# Actions
|
434
|
+
# ------------------------------------------------------------------------
|
435
|
+
|
436
|
+
def archive_image(self, name, tag):
|
437
|
+
'''
|
438
|
+
Archive an image to a .tar file. Called when archive_path is passed.
|
439
|
+
|
440
|
+
:param name - name of the image. Type: str
|
441
|
+
:return None
|
442
|
+
'''
|
443
|
+
|
444
|
+
if not tag:
|
445
|
+
tag = "latest"
|
446
|
+
|
447
|
+
image = self.client.find_image(name=name, tag=tag)
|
448
|
+
if not image:
|
449
|
+
self.logger.info(
|
450
|
+
"archive image: image {name}:{tag} not found",
|
451
|
+
payload = dict(
|
452
|
+
name = name,
|
453
|
+
tag = tag,
|
454
|
+
)
|
455
|
+
)
|
456
|
+
return
|
457
|
+
|
458
|
+
image_name = "%s:%s" % (name, tag)
|
459
|
+
|
460
|
+
self.append_action(
|
461
|
+
'Archived image `{image_name}` to `{archive_path}`',
|
462
|
+
image_name = image_name,
|
463
|
+
archive_path = self.archive_path
|
464
|
+
)
|
465
|
+
|
466
|
+
self.results['changed'] = True
|
467
|
+
|
468
|
+
if not self.check_mode:
|
469
|
+
|
470
|
+
self.logger.info(
|
471
|
+
"Getting archive of image `{image_name}`",
|
472
|
+
payload = dict(
|
473
|
+
image_name = image_name
|
474
|
+
)
|
475
|
+
)
|
476
|
+
|
477
|
+
try:
|
478
|
+
image = self.client.get_image(image_name)
|
479
|
+
except Exception as exc:
|
480
|
+
self.fail(
|
481
|
+
"Error getting image `%s` - %s" % (image_name, str(exc))
|
482
|
+
)
|
483
|
+
|
484
|
+
try:
|
485
|
+
with open(self.archive_path, 'w') as fd:
|
486
|
+
for chunk in image.stream(2048, decode_content=False):
|
487
|
+
fd.write(chunk)
|
488
|
+
except Exception as exc:
|
489
|
+
self.fail(
|
490
|
+
"Error writing image archive `%s` - %s" % (
|
491
|
+
self.archive_path,
|
492
|
+
str(exc)
|
493
|
+
)
|
494
|
+
)
|
495
|
+
|
496
|
+
image = self.client.find_image(name=name, tag=tag)
|
497
|
+
if image:
|
498
|
+
self.results['image'] = image
|
499
|
+
|
500
|
+
|
501
|
+
def push_image(self, name, tag=None):
|
502
|
+
'''
|
503
|
+
If the name of the image contains a repository path, then push the image.
|
504
|
+
|
505
|
+
:param name Name of the image to push.
|
506
|
+
:param tag Use a specific tag.
|
507
|
+
:return: None
|
508
|
+
'''
|
509
|
+
|
510
|
+
repository = name
|
511
|
+
if not tag:
|
512
|
+
repository, tag = parse_repository_tag(name)
|
513
|
+
registry, repo_name = resolve_repository_name(repository)
|
514
|
+
|
515
|
+
self.logger.info(
|
516
|
+
"push `{name}` to `{registry}/{repo_name}:{tag}`",
|
517
|
+
payload = dict(
|
518
|
+
name = self.name,
|
519
|
+
registry = registry,
|
520
|
+
repo_name = repo_name,
|
521
|
+
tag = tag
|
522
|
+
)
|
523
|
+
)
|
524
|
+
|
525
|
+
if registry:
|
526
|
+
|
527
|
+
self.append_action(
|
528
|
+
"Pushed image `{name}` to `{registry}/{repo_name}:{tag}`",
|
529
|
+
name = self.name,
|
530
|
+
registry = registry,
|
531
|
+
repo_name = repo_name,
|
532
|
+
tag = tag
|
533
|
+
)
|
534
|
+
|
535
|
+
self.results['changed'] = True
|
536
|
+
|
537
|
+
if not self.check_mode:
|
538
|
+
status = None
|
539
|
+
try:
|
540
|
+
for line in self.client.push(
|
541
|
+
repository,
|
542
|
+
tag = tag,
|
543
|
+
stream = True,
|
544
|
+
decode = True
|
545
|
+
):
|
546
|
+
self.out(line)
|
547
|
+
|
548
|
+
if line.get('errorDetail'):
|
549
|
+
raise Exception(line['errorDetail']['message'])
|
550
|
+
|
551
|
+
status = line.get('status')
|
552
|
+
|
553
|
+
except Exception as exc:
|
554
|
+
if re.search('unauthorized', str(exc)):
|
555
|
+
if re.search('authentication required', str(exc)):
|
556
|
+
self.fail(
|
557
|
+
"Error pushing image %s/%s:%s - %s. Try logging into %s first." % (
|
558
|
+
registry,
|
559
|
+
repo_name,
|
560
|
+
tag,
|
561
|
+
str(exc),
|
562
|
+
registry
|
563
|
+
)
|
564
|
+
)
|
565
|
+
else:
|
566
|
+
self.fail(
|
567
|
+
"Error pushing image %s/%s:%s - %s. Does the repository exist?" % (
|
568
|
+
registry,
|
569
|
+
repo_name,
|
570
|
+
tag,
|
571
|
+
str(exc)
|
572
|
+
)
|
573
|
+
)
|
574
|
+
|
575
|
+
self.fail(
|
576
|
+
"Error pushing image %s: %s" % (repository, str(exc))
|
577
|
+
)
|
578
|
+
|
579
|
+
self.results['image'] = self.client.find_image(
|
580
|
+
name = repository,
|
581
|
+
tag = tag
|
582
|
+
)
|
583
|
+
|
584
|
+
if not self.results['image']:
|
585
|
+
self.results['image'] = dict()
|
586
|
+
|
587
|
+
self.results['image']['push_status'] = status
|
588
|
+
|
589
|
+
|
590
|
+
def tag_image(self, name, tag, repository, force=False, push=False):
|
591
|
+
'''
|
592
|
+
Tag an image into a repository.
|
593
|
+
|
594
|
+
:param name: name of the image. required.
|
595
|
+
:param tag: image tag.
|
596
|
+
:param repository: path to the repository. required.
|
597
|
+
:param force: bool. force tagging, even it image already exists with the repository path.
|
598
|
+
:param push: bool. push the image once it's tagged.
|
599
|
+
:return: None
|
600
|
+
'''
|
601
|
+
repo, repo_tag = parse_repository_tag(repository)
|
602
|
+
|
603
|
+
if not repo_tag:
|
604
|
+
repo_tag = "latest"
|
605
|
+
if tag:
|
606
|
+
repo_tag = tag
|
607
|
+
|
608
|
+
image = self.client.find_image(name=repo, tag=repo_tag)
|
609
|
+
|
610
|
+
found = 'found' if image else 'not found'
|
611
|
+
|
612
|
+
self.logger.info(
|
613
|
+
"image `{repo}` was `{found}`",
|
614
|
+
payload = dict(
|
615
|
+
repo = repo,
|
616
|
+
found = found
|
617
|
+
)
|
618
|
+
)
|
619
|
+
|
620
|
+
if not image or force:
|
621
|
+
self.logger.info(
|
622
|
+
"tagging {name}:{tag} to {repo}:{repo_tag}",
|
623
|
+
payload=dict(name=name, tag=tag, repo=repo, repo_tag=repo_tag)
|
624
|
+
)
|
625
|
+
|
626
|
+
self.results['changed'] = True
|
627
|
+
|
628
|
+
append_action(
|
629
|
+
"Tagged image {name}:{tag} to {repo}:{repo_tag}",
|
630
|
+
name=name, tag=tag, repo=repo, repo_tag=repo_tag
|
631
|
+
)
|
632
|
+
|
633
|
+
if not self.check_mode:
|
634
|
+
try:
|
635
|
+
# Finding the image does not always work, especially running a localhost registry. In those
|
636
|
+
# cases, if we don't set force=True, it errors.
|
637
|
+
image_name = name
|
638
|
+
if tag and not re.search(tag, name):
|
639
|
+
image_name = "%s:%s" % (name, tag)
|
640
|
+
tag_status = self.client.tag(image_name, repo, tag=repo_tag, force=True)
|
641
|
+
if not tag_status:
|
642
|
+
raise Exception("Tag operation failed.")
|
643
|
+
except Exception as exc:
|
644
|
+
self.fail("Error: failed to tag image - %s" % str(exc))
|
645
|
+
self.results['image'] = self.client.find_image(name=repo, tag=repo_tag)
|
646
|
+
if push:
|
647
|
+
self.push_image(repo, repo_tag)
|
648
|
+
|
649
|
+
|
650
|
+
def build_image(self):
|
651
|
+
'''
|
652
|
+
Build an image
|
653
|
+
|
654
|
+
:return: image dict
|
655
|
+
'''
|
656
|
+
params = dict(
|
657
|
+
path=self.path,
|
658
|
+
tag=self.name,
|
659
|
+
rm=self.rm,
|
660
|
+
nocache=self.nocache,
|
661
|
+
# Docker Pythong client v3 doesn't support
|
662
|
+
# stream=True,
|
663
|
+
timeout=self.http_timeout,
|
664
|
+
pull=self.pull,
|
665
|
+
forcerm=self.rm,
|
666
|
+
dockerfile=self.dockerfile,
|
667
|
+
decode=True
|
668
|
+
)
|
669
|
+
build_output = []
|
670
|
+
if self.tag:
|
671
|
+
params['tag'] = "%s:%s" % (self.name, self.tag)
|
672
|
+
if self.container_limits:
|
673
|
+
params['container_limits'] = self.container_limits
|
674
|
+
if self.buildargs:
|
675
|
+
for key, value in self.buildargs.items():
|
676
|
+
self.buildargs[key] = to_native(value)
|
677
|
+
params['buildargs'] = self.buildargs
|
678
|
+
|
679
|
+
self.logger.info(
|
680
|
+
"Building",
|
681
|
+
payload=params,
|
682
|
+
)
|
683
|
+
|
684
|
+
logs = self.client.build(**params)
|
685
|
+
|
686
|
+
# self.logger.info("build result", payload=dict(result=result))
|
687
|
+
|
688
|
+
for log in logs:
|
689
|
+
|
690
|
+
self.out(log)
|
691
|
+
|
692
|
+
if "stream" in log:
|
693
|
+
build_output.append(log["stream"])
|
694
|
+
|
695
|
+
if log.get('error'):
|
696
|
+
if log.get('errorDetail'):
|
697
|
+
errorDetail = log.get('errorDetail')
|
698
|
+
self.fail(
|
699
|
+
"Error building %s - code: %s, message: %s, logs: %s" % (
|
700
|
+
self.name,
|
701
|
+
errorDetail.get('code'),
|
702
|
+
errorDetail.get('message'),
|
703
|
+
build_output
|
704
|
+
)
|
705
|
+
)
|
706
|
+
else:
|
707
|
+
self.fail(
|
708
|
+
"Error building %s - message: %s, logs: %s" % (
|
709
|
+
self.name,
|
710
|
+
log.get('error'),
|
711
|
+
build_output
|
712
|
+
)
|
713
|
+
)
|
714
|
+
return self.client.find_image(name=self.name, tag=self.tag)
|
715
|
+
|
716
|
+
|
717
|
+
def load_image(self):
|
718
|
+
'''
|
719
|
+
Load an image from a .tar archive
|
720
|
+
|
721
|
+
:return: image dict
|
722
|
+
'''
|
723
|
+
try:
|
724
|
+
self.logger.info(
|
725
|
+
"Opening image `{load_path}`",
|
726
|
+
payload = dict(load_path=self.load_path)
|
727
|
+
)
|
728
|
+
|
729
|
+
image_tar = open(self.load_path, 'r')
|
730
|
+
|
731
|
+
except Exception as exc:
|
732
|
+
self.fail(
|
733
|
+
"Error opening image `{load_path}` - `{error}`",
|
734
|
+
load_path = self.load_path,
|
735
|
+
error = str(exc)
|
736
|
+
)
|
737
|
+
|
738
|
+
try:
|
739
|
+
self.logger.info(
|
740
|
+
"Loading image from `{load_path}`",
|
741
|
+
payload = dict(load_path=self.load_path)
|
742
|
+
)
|
743
|
+
|
744
|
+
self.client.load_image(image_tar)
|
745
|
+
|
746
|
+
except Exception as exc:
|
747
|
+
self.fail("Error loading image %s - %s" % (self.name, str(exc)))
|
748
|
+
|
749
|
+
try:
|
750
|
+
image_tar.close()
|
751
|
+
except Exception as exc:
|
752
|
+
self.fail("Error closing image %s - %s" % (self.name, str(exc)))
|
753
|
+
|
754
|
+
return self.client.find_image(self.name, self.tag)
|