Merge branch 'master' of git://github.com/dotcloud/docker-py

This commit is contained in:
shin- 2014-01-16 20:26:06 +01:00
commit 477831334c
7 changed files with 297 additions and 163 deletions

View File

@ -55,7 +55,7 @@ Identical to the `docker cp` command.
c.create_container(image, command=None, hostname=None, user=None,
detach=False, stdin_open=False, tty=False, mem_limit=0,
ports=None, environment=None, dns=None, volumes=None,
volumes_from=None, name=None)
volumes_from=None, network_disabled=False, name=None)
```
Creates a container that can then be `start`ed. Parameters are similar
@ -142,6 +142,14 @@ Identical to the `docker logs` command. The `stream` parameter makes the
`logs` function return a blocking generator you can iterate over to
retrieve log output as it happens.
```python
c.attach(container, stdout=True, stderr=True, stream=False, logs=False)
```
The `logs` function is a wrapper around this one, which you can use
instead if you want to fetch/stream container output without first
retrieving the entire backlog.
```python
c.port(container, private_port)
```

View File

@ -13,14 +13,16 @@
# limitations under the License.
import base64
import fileinput
import json
import os
import six
import docker.utils as utils
from ..utils import utils
INDEX_URL = 'https://index.docker.io/v1/'
DOCKER_CONFIG_FILENAME = '.dockercfg'
def swap_protocol(url):
@ -59,12 +61,15 @@ def resolve_repository_name(repo_name):
return expand_registry_url(parts[0]), parts[1]
def resolve_authconfig(authconfig, registry):
default = {}
if registry == INDEX_URL or registry == '':
# default to the index server
return authconfig['Configs'].get(INDEX_URL, default)
# if its not the index server there are three cases:
def resolve_authconfig(authconfig, registry=None):
"""Return the authentication data from the given auth configuration for a
specific registry. We'll do our best to infer the correct URL for the
registry, trying both http and https schemes. Returns an empty dictionnary
if no data exists."""
# Default to the public index server
registry = registry or INDEX_URL
# Ff its not the index server there are three cases:
#
# 1. this is a full config url -> it should be used as is
# 2. it could be a full url, but with the wrong protocol
@ -77,11 +82,9 @@ def resolve_authconfig(authconfig, registry):
if not registry.startswith('http:') and not registry.startswith('https:'):
registry = 'https://' + registry
if registry in authconfig['Configs']:
return authconfig['Configs'][registry]
elif swap_protocol(registry) in authconfig['Configs']:
return authconfig['Configs'][swap_protocol(registry)]
return default
if registry in authconfig:
return authconfig[registry]
return authconfig.get(swap_protocol(registry), None)
def decode_auth(auth):
@ -98,38 +101,53 @@ def encode_header(auth):
def load_config(root=None):
root = root or os.environ['HOME']
config = {
'Configs': {},
'rootPath': root
}
"""Loads authentication data from a Docker configuration file in the given
root directory."""
conf = {}
data = None
config_file = os.path.join(root, '.dockercfg')
if not os.path.exists(config_file):
return config
config_file = os.path.join(root or os.environ.get('HOME', '.'),
DOCKER_CONFIG_FILENAME)
f = open(config_file)
# First try as JSON
try:
config['Configs'] = json.load(f)
for k, conf in six.iteritems(config['Configs']):
conf['Username'], conf['Password'] = decode_auth(conf['auth'])
del conf['auth']
config['Configs'][k] = conf
except Exception:
f.seek(0)
buf = []
for line in f:
k, v = line.split(' = ')
buf.append(v)
if len(buf) < 2:
raise Exception("The Auth config file is empty")
user, pwd = decode_auth(buf[0])
config['Configs'][INDEX_URL] = {
'Username': user,
'Password': pwd,
'Email': buf[1]
}
finally:
f.close()
with open(config_file) as f:
conf = {}
for registry, entry in six.iteritems(json.load(f)):
username, password = decode_auth(entry['auth'])
conf[registry] = {
'username': username,
'password': password,
'email': entry['email'],
'serveraddress': registry,
}
return conf
except:
pass
return config
# If that fails, we assume the configuration file contains a single
# authentication token for the public registry in the following format:
#
# auth = AUTH_TOKEN
# email = email@domain.com
try:
data = []
for line in fileinput.input(config_file):
data.append(line.strip().split(' = ')[1])
if len(data) < 2:
# Not enough data
raise Exception('Invalid or empty configuration file!')
username, password = decode_auth(data[0])
conf[INDEX_URL] = {
'username': username,
'password': password,
'email': data[1],
'serveraddress': INDEX_URL,
}
return conf
except:
pass
# If all fails, return an empty config
return {}

View File

@ -21,9 +21,9 @@ import requests
import requests.exceptions
import six
import docker.auth as auth
import docker.unixconn as unixconn
import docker.utils as utils
from .auth import auth
from .unixconn import unixconn
from .utils import utils
if not six.PY3:
import websocket
@ -65,22 +65,23 @@ class APIError(requests.exceptions.HTTPError):
class Client(requests.Session):
def __init__(self, base_url="unix://var/run/docker.sock", version="1.6",
def __init__(self, base_url=None, version="1.6",
timeout=DEFAULT_TIMEOUT_SECONDS):
super(Client, self).__init__()
if base_url is None:
base_url = "unix://var/run/docker.sock"
if base_url.startswith('unix:///'):
base_url = base_url.replace('unix:/', 'unix:')
if base_url.startswith('tcp:'):
base_url = base_url.replace('tcp:', 'http:')
if base_url.endswith('/'):
base_url = base_url[:-1]
self.base_url = base_url
self._version = version
self._timeout = timeout
self._auth_configs = auth.load_config()
self.mount('unix://', unixconn.UnixAdapter(base_url, timeout))
try:
self._cfg = auth.load_config()
except Exception:
pass
def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout
@ -120,7 +121,8 @@ class Client(requests.Session):
def _container_config(self, image, command, hostname=None, user=None,
detach=False, stdin_open=False, tty=False,
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None):
volumes=None, volumes_from=None,
network_disabled=False):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
if isinstance(environment, dict):
@ -132,15 +134,12 @@ class Client(requests.Session):
exposed_ports = {}
for port_definition in ports:
port = port_definition
proto = None
proto = 'tcp'
if isinstance(port_definition, tuple):
if len(port_definition) == 2:
proto = port_definition[1]
port = port_definition[0]
exposed_ports['{0}{1}'.format(
port,
'/' + proto if proto else ''
)] = {}
exposed_ports['{0}/{1}'.format(port, proto)] = {}
ports = exposed_ports
if volumes and isinstance(volumes, list):
@ -176,6 +175,7 @@ class Client(requests.Session):
'Image': image,
'Volumes': volumes,
'VolumesFrom': volumes_from,
'NetworkDisabled': network_disabled
}
def _post_json(self, url, data, **kwargs):
@ -227,7 +227,9 @@ class Client(requests.Session):
def _stream_helper(self, response):
"""Generator for data coming from a chunked-encoded HTTP response."""
socket = self._stream_result_socket(response).makefile()
socket_fp = self._stream_result_socket(response)
socket_fp.setblocking(1)
socket = socket_fp.makefile()
while True:
size = int(socket.readline(), 16)
if size <= 0:
@ -280,15 +282,26 @@ class Client(requests.Session):
break
yield data
def attach(self, container):
socket = self.attach_socket(container)
def attach(self, container, stdout=True, stderr=True,
stream=False, logs=False):
if isinstance(container, dict):
container = container.get('Id')
params = {
'logs': logs and 1 or 0,
'stdout': stdout and 1 or 0,
'stderr': stderr and 1 or 0,
'stream': stream and 1 or 0,
}
u = self._url("/containers/{0}/attach".format(container))
response = self._post(u, params=params, stream=stream)
while True:
chunk = socket.recv(4096)
if chunk:
yield chunk
else:
break
# Stream multi-plexing was introduced in API v1.6.
if utils.compare_version('1.6', self._version) < 0:
return stream and self._stream_result(response) or \
self._result(response, binary=True)
return stream and self._multiplexed_socket_stream_helper(response) or \
''.join([x for x in self._multiplexed_buffer_helper(response)])
def attach_socket(self, container, params=None, ws=False):
if params is None:
@ -307,7 +320,7 @@ class Client(requests.Session):
u, None, params=self._attach_params(params), stream=True))
def build(self, path=None, tag=None, quiet=False, fileobj=None,
nocache=False, rm=False, stream=False):
nocache=False, rm=False, stream=False, timeout=None):
remote = context = headers = None
if path is None and fileobj is None:
raise Exception("Either path or fileobj needs to be provided.")
@ -331,7 +344,12 @@ class Client(requests.Session):
headers = {'Content-Type': 'application/tar'}
response = self._post(
u, data=context, params=params, headers=headers, stream=stream
u,
data=context,
params=params,
headers=headers,
stream=stream,
timeout=timeout,
)
if context is not None:
@ -387,11 +405,12 @@ class Client(requests.Session):
def create_container(self, image, command=None, hostname=None, user=None,
detach=False, stdin_open=False, tty=False,
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None, name=None):
volumes=None, volumes_from=None,
network_disabled=False, name=None):
config = self._container_config(
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
ports, environment, dns, volumes, volumes_from
ports, environment, dns, volumes, volumes_from, network_disabled
)
return self.create_container_from_config(config, name)
@ -518,44 +537,42 @@ class Client(requests.Session):
self._raise_for_status(res)
def login(self, username, password=None, email=None, registry=None):
url = self._url("/auth")
if registry is None:
registry = auth.INDEX_URL
if getattr(self, '_cfg', None) is None:
self._cfg = auth.load_config()
authcfg = auth.resolve_authconfig(self._cfg, registry)
if 'username' in authcfg and authcfg['username'] == username:
def login(self, username, password=None, email=None, registry=None,
reauth=False):
# If we don't have any auth data so far, try reloading the config file
# one more time in case anything showed up in there.
if not self._auth_configs:
self._auth_configs = auth.load_config()
registry = registry or auth.INDEX_URL
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == username \
and not reauth:
return authcfg
req_data = {
'username': username,
'password': password,
'email': email
'email': email,
'serveraddress': registry,
}
res = self._result(self._post_json(url, data=req_data), True)
if res['Status'] == 'Login Succeeded':
self._cfg['Configs'][registry] = req_data
return res
response = self._post_json(self._url('/auth'), data=req_data)
if response.status_code == 200:
self._auth_configs[registry] = req_data
return self._result(response, json=True)
def logs(self, container, stdout=True, stderr=True, stream=False):
if isinstance(container, dict):
container = container.get('Id')
params = {
'logs': 1,
'stdout': stdout and 1 or 0,
'stderr': stderr and 1 or 0,
'stream': stream and 1 or 0,
}
u = self._url("/containers/{0}/attach".format(container))
response = self._post(u, params=params, stream=stream)
# Stream multi-plexing was introduced in API v1.6.
if utils.compare_version('1.6', self._version) < 0:
return stream and self._stream_result(response) or \
self._result(response, binary=True)
return stream and self._multiplexed_socket_stream_helper(response) or \
''.join([x for x in self._multiplexed_buffer_helper(response)])
return self.attach(
container,
stdout=stdout,
stderr=stderr,
stream=stream,
logs=True
)
def port(self, container, private_port):
if isinstance(container, dict):
@ -564,13 +581,13 @@ class Client(requests.Session):
self._raise_for_status(res)
json_ = res.json()
s_port = str(private_port)
f_port = None
if s_port in json_['NetworkSettings']['PortMapping']['Udp']:
f_port = json_['NetworkSettings']['PortMapping']['Udp'][s_port]
elif s_port in json_['NetworkSettings']['PortMapping']['Tcp']:
f_port = json_['NetworkSettings']['PortMapping']['Tcp'][s_port]
h_ports = None
return f_port
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp')
if h_ports is None:
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp')
return h_ports
def pull(self, repository, tag=None, stream=False):
registry, repo_name = auth.resolve_repository_name(repository)
@ -584,16 +601,20 @@ class Client(requests.Session):
headers = {}
if utils.compare_version('1.5', self._version) >= 0:
if getattr(self, '_cfg', None) is None:
self._cfg = auth.load_config()
authcfg = auth.resolve_authconfig(self._cfg, registry)
# do not fail if no atuhentication exists
# for this specific registry as we can have a readonly pull
# If we don't have any auth data so far, try reloading the config
# file one more time in case anything showed up in there.
if not self._auth_configs:
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# Do not fail here if no atuhentication exists for this specific
# registry as we can have a readonly pull. Just put the header if
# we can.
if authcfg:
headers['X-Registry-Auth'] = auth.encode_header(authcfg)
u = self._url("/images/create")
response = self._post(u, params=params, headers=headers, stream=stream,
timeout=None)
response = self._post(self._url('/images/create'), params=params,
headers=headers, stream=stream, timeout=None)
if stream:
return self._stream_helper(response)
@ -604,26 +625,26 @@ class Client(requests.Session):
registry, repo_name = auth.resolve_repository_name(repository)
u = self._url("/images/{0}/push".format(repository))
headers = {}
if getattr(self, '_cfg', None) is None:
self._cfg = auth.load_config()
authcfg = auth.resolve_authconfig(self._cfg, registry)
if utils.compare_version('1.5', self._version) >= 0:
# do not fail if no atuhentication exists
# for this specific registry as we can have an anon push
# If we don't have any auth data so far, try reloading the config
# file one more time in case anything showed up in there.
if not self._auth_configs:
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# Do not fail here if no atuhentication exists for this specific
# registry as we can have a readonly pull. Just put the header if
# we can.
if authcfg:
headers['X-Registry-Auth'] = auth.encode_header(authcfg)
if stream:
return self._stream_helper(
self._post_json(u, None, headers=headers, stream=True))
else:
return self._result(
self._post_json(u, None, headers=headers, stream=False))
if stream:
return self._stream_helper(
self._post_json(u, authcfg, stream=True))
response = self._post_json(u, None, headers=headers, stream=stream)
else:
return self._result(self._post_json(u, authcfg, stream=False))
response = self._post_json(u, authcfg, stream=stream)
return stream and self._stream_helper(response) \
or self._result(response)
def remove_container(self, container, v=False, link=False):
if isinstance(container, dict):

View File

@ -17,12 +17,17 @@ setup(
install_requires=requirements + test_requirements,
zip_safe=False,
test_suite='tests',
classifiers=['Development Status :: 4 - Beta',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Utilities',
'License :: OSI Approved :: Apache Software License'
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Topic :: Utilities',
'License :: OSI Approved :: Apache Software License',
],
)

View File

@ -157,6 +157,34 @@ def get_fake_inspect_image():
return status_code, response
def get_fake_port():
status_code = 200
response = {
'HostConfig': {
'Binds': None,
'ContainerIDFile': '',
'Links': None,
'LxcConf': None,
'PortBindings': {
'1111': None,
'1111/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '4567'}],
'2222': None
},
'Privileged': False,
'PublishAllPorts': False
},
'NetworkSettings': {
'Bridge': 'docker0',
'PortMapping': None,
'Ports': {
'1111': None,
'1111/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '4567'}],
'2222': None}
}
}
return status_code, response
def get_fake_insert_image():
status_code = 200
response = {'StatusCode': 0}
@ -282,6 +310,8 @@ fake_responses = {
post_fake_stop_container,
'{1}/{0}/containers/3cc2351ab11b/kill'.format(CURRENT_VERSION, prefix):
post_fake_kill_container,
'{1}/{0}/containers/3cc2351ab11b/json'.format(CURRENT_VERSION, prefix):
get_fake_port,
'{1}/{0}/containers/3cc2351ab11b/restart'.format(CURRENT_VERSION, prefix):
post_fake_restart_container,
'{1}/{0}/containers/3cc2351ab11b'.format(CURRENT_VERSION, prefix):

View File

@ -422,6 +422,34 @@ class TestKillWithSignal(BaseTestCase):
self.assertEqual(state['Running'], False, state)
class TestPort(BaseTestCase):
def runTest(self):
port_bindings = {
1111: ('127.0.0.1', '4567'),
2222: ('192.168.0.100', '4568')
}
container = self.client.create_container(
'busybox', ['sleep', '60'], ports=port_bindings.keys()
)
id = container['Id']
self.client.start(container, port_bindings=port_bindings)
#Call the port function on each biding and compare expected vs actual
for port in port_bindings:
actual_bindings = self.client.port(container, port)
port_binding = actual_bindings.pop()
ip, host_port = port_binding['HostIp'], port_binding['HostPort']
self.assertEqual(ip, port_bindings[port][0])
self.assertEqual(host_port, port_bindings[port][1])
self.client.kill(id)
class TestRestart(BaseTestCase):
def runTest(self):
container = self.client.create_container('busybox', ['sleep', '9999'])
@ -775,11 +803,29 @@ class TestLoadConfig(BaseTestCase):
f.write('email = sakuya@scarlet.net')
f.close()
cfg = docker.auth.load_config(folder)
self.assertNotEqual(cfg['Configs'][docker.auth.INDEX_URL], None)
cfg = cfg['Configs'][docker.auth.INDEX_URL]
self.assertEqual(cfg['Username'], b'sakuya')
self.assertEqual(cfg['Password'], b'izayoi')
self.assertEqual(cfg['Email'], 'sakuya@scarlet.net')
self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['username'], b'sakuya')
self.assertEqual(cfg['password'], b'izayoi')
self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None)
class TestLoadJSONConfig(BaseTestCase):
def runTest(self):
folder = tempfile.mkdtemp()
f = open(os.path.join(folder, '.dockercfg'), 'w')
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
email_ = 'sakuya@scarlet.net'
f.write('{{"{}": {{"auth": "{}", "email": "{}"}}}}\n'.format(
docker.auth.INDEX_URL, auth_, email_))
f.close()
cfg = docker.auth.load_config(folder)
self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['username'], b'sakuya')
self.assertEqual(cfg['password'], b'izayoi')
self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None)

View File

@ -34,10 +34,6 @@ except ImportError:
import mock
# FIXME: missing tests for
# port;
def response(status_code=200, content='', headers=None, reason=None, elapsed=0,
request=None):
res = requests.Response()
@ -180,7 +176,7 @@ class DockerClientTest(unittest.TestCase):
{"Tty": false, "Image": "busybox", "Cmd": ["true"],
"AttachStdin": false, "Memory": 0,
"AttachStderr": true, "AttachStdout": true,
"OpenStdin": false}'''))
"OpenStdin": false, "NetworkDisabled": false}'''))
self.assertEqual(args[1]['headers'],
{'Content-Type': 'application/json'})
@ -203,7 +199,8 @@ class DockerClientTest(unittest.TestCase):
"Cmd": ["ls", "/mnt"], "AttachStdin": false,
"Volumes": {"/mnt": {}}, "Memory": 0,
"AttachStderr": true,
"AttachStdout": true, "OpenStdin": false}'''))
"AttachStdout": true, "OpenStdin": false,
"NetworkDisabled": false}'''))
self.assertEqual(args[1]['headers'],
{'Content-Type': 'application/json'})
@ -222,12 +219,13 @@ class DockerClientTest(unittest.TestCase):
{"Tty": false, "Image": "busybox",
"Cmd": ["ls"], "AttachStdin": false,
"Memory": 0, "ExposedPorts": {
"1111": {},
"1111/tcp": {},
"2222/udp": {},
"3333": {}
"3333/tcp": {}
},
"AttachStderr": true,
"AttachStdout": true, "OpenStdin": false}'''))
"AttachStdout": true, "OpenStdin": false,
"NetworkDisabled": false}'''))
self.assertEqual(args[1]['headers'],
{'Content-Type': 'application/json'})
@ -246,7 +244,7 @@ class DockerClientTest(unittest.TestCase):
{"Tty": false, "Image": "busybox", "Cmd": ["true"],
"AttachStdin": false, "Memory": 0,
"AttachStderr": true, "AttachStdout": true,
"OpenStdin": false}'''))
"OpenStdin": false, "NetworkDisabled": false}'''))
self.assertEqual(args[1]['headers'],
{'Content-Type': 'application/json'})
self.assertEqual(args[1]['params'], {'name': 'marisa-kirisame'})
@ -587,6 +585,17 @@ class DockerClientTest(unittest.TestCase):
timeout=docker.client.DEFAULT_TIMEOUT_SECONDS
)
def test_port(self):
try:
self.client.port({'Id': fake_api.FAKE_CONTAINER_ID}, 1111)
except Exception as e:
self.fail('Command should not raise exception: {0}'.format(e))
fake_request.assert_called_with(
'unix://var/run/docker.sock/v1.6/containers/3cc2351ab11b/json',
timeout=docker.client.DEFAULT_TIMEOUT_SECONDS
)
def test_stop_container(self):
try:
self.client.stop(fake_api.FAKE_CONTAINER_ID, timeout=2)
@ -1037,10 +1046,6 @@ class DockerClientTest(unittest.TestCase):
folder = tempfile.mkdtemp()
cfg = docker.auth.load_config(folder)
self.assertTrue(cfg is not None)
self.assertTrue('Configs' in cfg)
self.assertEqual(cfg['Configs'], {})
self.assertTrue('rootPath' in cfg)
self.assertEqual(cfg['rootPath'], folder)
def test_load_config(self):
folder = tempfile.mkdtemp()
@ -1050,12 +1055,13 @@ class DockerClientTest(unittest.TestCase):
f.write('email = sakuya@scarlet.net')
f.close()
cfg = docker.auth.load_config(folder)
self.assertNotEqual(cfg['Configs'][docker.auth.INDEX_URL], None)
cfg = cfg['Configs'][docker.auth.INDEX_URL]
self.assertEqual(cfg['Username'], 'sakuya')
self.assertEqual(cfg['Password'], 'izayoi')
self.assertEqual(cfg['Email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None)
self.assertTrue(docker.auth.INDEX_URL in cfg)
self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['username'], 'sakuya')
self.assertEqual(cfg['password'], 'izayoi')
self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('auth'), None)
if __name__ == '__main__':