/plugins/inventory/docker.py

https://github.com/ajanthanm/ansible · Python · 359 lines · 183 code · 40 blank · 136 comment · 52 complexity · 34b2f04e653d01872e7aedc2ccc416d0 MD5 · raw file

  1. #!/usr/bin/env python
  2. # (c) 2013, Paul Durivage <paul.durivage@gmail.com>
  3. #
  4. # This file is part of Ansible.
  5. #
  6. # Ansible is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Ansible is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. #
  20. # Author: Paul Durivage <paul.durivage@gmail.com>
  21. #
  22. # Description:
  23. # This module queries local or remote Docker daemons and generates
  24. # inventory information.
  25. #
  26. # This plugin does not support targeting of specific hosts using the --host
  27. # flag. Instead, it it queries the Docker API for each container, running
  28. # or not, and returns this data all once.
  29. #
  30. # The plugin returns the following custom attributes on Docker containers:
  31. # docker_args
  32. # docker_config
  33. # docker_created
  34. # docker_driver
  35. # docker_exec_driver
  36. # docker_host_config
  37. # docker_hostname_path
  38. # docker_hosts_path
  39. # docker_id
  40. # docker_image
  41. # docker_name
  42. # docker_network_settings
  43. # docker_path
  44. # docker_resolv_conf_path
  45. # docker_state
  46. # docker_volumes
  47. # docker_volumes_rw
  48. #
  49. # Requirements:
  50. # The docker-py module: https://github.com/dotcloud/docker-py
  51. #
  52. # Notes:
  53. # A config file can be used to configure this inventory module, and there
  54. # are several environment variables that can be set to modify the behavior
  55. # of the plugin at runtime:
  56. # DOCKER_CONFIG_FILE
  57. # DOCKER_HOST
  58. # DOCKER_VERSION
  59. # DOCKER_TIMEOUT
  60. # DOCKER_PRIVATE_SSH_PORT
  61. # DOCKER_DEFAULT_IP
  62. #
  63. # Environment Variables:
  64. # environment variable: DOCKER_CONFIG_FILE
  65. # description:
  66. # - A path to a Docker inventory hosts/defaults file in YAML format
  67. # - A sample file has been provided, colocated with the inventory
  68. # file called 'docker.yml'
  69. # required: false
  70. # default: Uses docker.docker.Client constructor defaults
  71. # environment variable: DOCKER_HOST
  72. # description:
  73. # - The socket on which to connect to a Docker daemon API
  74. # required: false
  75. # default: Uses docker.docker.Client constructor defaults
  76. # environment variable: DOCKER_VERSION
  77. # description:
  78. # - Version of the Docker API to use
  79. # default: Uses docker.docker.Client constructor defaults
  80. # required: false
  81. # environment variable: DOCKER_TIMEOUT
  82. # description:
  83. # - Timeout in seconds for connections to Docker daemon API
  84. # default: Uses docker.docker.Client constructor defaults
  85. # required: false
  86. # environment variable: DOCKER_PRIVATE_SSH_PORT
  87. # description:
  88. # - The private port (container port) on which SSH is listening
  89. # for connections
  90. # default: 22
  91. # required: false
  92. # environment variable: DOCKER_DEFAULT_IP
  93. # description:
  94. # - This environment variable overrides the container SSH connection
  95. # IP address (aka, 'ansible_ssh_host')
  96. #
  97. # This option allows one to override the ansible_ssh_host whenever
  98. # Docker has exercised its default behavior of binding private ports
  99. # to all interfaces of the Docker host. This behavior, when dealing
  100. # with remote Docker hosts, does not allow Ansible to determine
  101. # a proper host IP address on which to connect via SSH to containers.
  102. # By default, this inventory module assumes all 0.0.0.0-exposed
  103. # ports to be bound to localhost:<port>. To override this
  104. # behavior, for example, to bind a container's SSH port to the public
  105. # interface of its host, one must manually set this IP.
  106. #
  107. # It is preferable to begin to launch Docker containers with
  108. # ports exposed on publicly accessible IP addresses, particularly
  109. # if the containers are to be targeted by Ansible for remote
  110. # configuration, not accessible via localhost SSH connections.
  111. #
  112. # Docker containers can be explicitly exposed on IP addresses by
  113. # a) starting the daemon with the --ip argument
  114. # b) running containers with the -P/--publish ip::containerPort
  115. # argument
  116. # default: 127.0.0.1 if port exposed on 0.0.0.0 by Docker
  117. # required: false
  118. #
  119. # Examples:
  120. # Use the config file:
  121. # DOCKER_CONFIG_FILE=./docker.yml docker.py --list
  122. #
  123. # Connect to docker instance on localhost port 4243
  124. # DOCKER_HOST=tcp://localhost:4243 docker.py --list
  125. #
  126. # Any container's ssh port exposed on 0.0.0.0 will mapped to
  127. # another IP address (where Ansible will attempt to connect via SSH)
  128. # DOCKER_DEFAULT_IP=1.2.3.4 docker.py --list
  129. import os
  130. import sys
  131. import json
  132. import argparse
  133. from UserDict import UserDict
  134. from collections import defaultdict
  135. import yaml
  136. from requests import HTTPError, ConnectionError
  137. # Manipulation of the path is needed because the docker-py
  138. # module is imported by the name docker, and because this file
  139. # is also named docker
  140. for path in [os.getcwd(), '', os.path.dirname(os.path.abspath(__file__))]:
  141. try:
  142. del sys.path[sys.path.index(path)]
  143. except:
  144. pass
  145. try:
  146. import docker
  147. except ImportError:
  148. print('docker-py is required for this module')
  149. sys.exit(1)
  150. class HostDict(UserDict):
  151. def __setitem__(self, key, value):
  152. if value is not None:
  153. self.data[key] = value
  154. def update(self, dict=None, **kwargs):
  155. if dict is None:
  156. pass
  157. elif isinstance(dict, UserDict):
  158. for k, v in dict.data.items():
  159. self[k] = v
  160. else:
  161. for k, v in dict.items():
  162. self[k] = v
  163. if len(kwargs):
  164. for k, v in kwargs.items():
  165. self[k] = v
  166. def write_stderr(string):
  167. sys.stderr.write('%s\n' % string)
  168. def setup():
  169. config = dict()
  170. config_file = os.environ.get('DOCKER_CONFIG_FILE')
  171. if config_file:
  172. try:
  173. config_file = os.path.abspath(config_file)
  174. except Exception as e:
  175. write_stderr(e)
  176. sys.exit(1)
  177. with open(config_file) as f:
  178. try:
  179. config = yaml.safe_load(f.read())
  180. except Exception as e:
  181. write_stderr(e)
  182. sys.exit(1)
  183. # Enviroment Variables
  184. env_base_url = os.environ.get('DOCKER_HOST')
  185. env_version = os.environ.get('DOCKER_VERSION')
  186. env_timeout = os.environ.get('DOCKER_TIMEOUT')
  187. env_ssh_port = os.environ.get('DOCKER_PRIVATE_SSH_PORT', '22')
  188. env_default_ip = os.environ.get('DOCKER_DEFAULT_IP', '127.0.0.1')
  189. # Config file defaults
  190. defaults = config.get('defaults', dict())
  191. def_host = defaults.get('host')
  192. def_version = defaults.get('version')
  193. def_timeout = defaults.get('timeout')
  194. def_default_ip = defaults.get('default_ip')
  195. def_ssh_port = defaults.get('private_ssh_port')
  196. hosts = list()
  197. if config:
  198. hosts_list = config.get('hosts', list())
  199. # Look to the config file's defined hosts
  200. if hosts_list:
  201. for host in hosts_list:
  202. baseurl = host.get('host') or def_host or env_base_url
  203. version = host.get('version') or def_version or env_version
  204. timeout = host.get('timeout') or def_timeout or env_timeout
  205. default_ip = host.get('default_ip') or def_default_ip or env_default_ip
  206. ssh_port = host.get('private_ssh_port') or def_ssh_port or env_ssh_port
  207. hostdict = HostDict(
  208. base_url=baseurl,
  209. version=version,
  210. timeout=timeout,
  211. default_ip=default_ip,
  212. private_ssh_port=ssh_port,
  213. )
  214. hosts.append(hostdict)
  215. # Look to the defaults
  216. else:
  217. hostdict = HostDict(
  218. base_url=def_host,
  219. version=def_version,
  220. timeout=def_timeout,
  221. default_ip=def_default_ip,
  222. private_ssh_port=def_ssh_port,
  223. )
  224. hosts.append(hostdict)
  225. # Look to the environment
  226. else:
  227. hostdict = HostDict(
  228. base_url=env_base_url,
  229. version=env_version,
  230. timeout=env_timeout,
  231. default_ip=env_default_ip,
  232. private_ssh_port=env_ssh_port,
  233. )
  234. hosts.append(hostdict)
  235. return hosts
  236. def list_groups():
  237. hosts = setup()
  238. groups = defaultdict(list)
  239. hostvars = defaultdict(dict)
  240. for host in hosts:
  241. ssh_port = host.pop('private_ssh_port', None)
  242. default_ip = host.pop('default_ip', None)
  243. hostname = host.get('base_url')
  244. try:
  245. client = docker.Client(**host)
  246. containers = client.containers(all=True)
  247. except (HTTPError, ConnectionError) as e:
  248. write_stderr(e)
  249. sys.exit(1)
  250. for container in containers:
  251. id = container.get('Id')
  252. short_id = id[:13]
  253. try:
  254. name = container.get('Names', list()).pop(0).lstrip('/')
  255. except IndexError:
  256. name = short_id
  257. if not id:
  258. continue
  259. inspect = client.inspect_container(id)
  260. running = inspect.get('State', dict()).get('Running')
  261. groups[id].append(name)
  262. groups[name].append(name)
  263. if not short_id in groups.keys():
  264. groups[short_id].append(name)
  265. groups[hostname].append(name)
  266. if running is True:
  267. groups['running'].append(name)
  268. else:
  269. groups['stopped'].append(name)
  270. try:
  271. port = client.port(container, ssh_port)[0]
  272. except (IndexError, AttributeError, TypeError):
  273. port = dict()
  274. try:
  275. ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp']
  276. except KeyError:
  277. ip = ''
  278. container_info = dict(
  279. ansible_ssh_host=ip,
  280. ansible_ssh_port=port.get('HostPort', int()),
  281. docker_args=inspect.get('Args'),
  282. docker_config=inspect.get('Config'),
  283. docker_created=inspect.get('Created'),
  284. docker_driver=inspect.get('Driver'),
  285. docker_exec_driver=inspect.get('ExecDriver'),
  286. docker_host_config=inspect.get('HostConfig'),
  287. docker_hostname_path=inspect.get('HostnamePath'),
  288. docker_hosts_path=inspect.get('HostsPath'),
  289. docker_id=inspect.get('ID'),
  290. docker_image=inspect.get('Image'),
  291. docker_name=name,
  292. docker_network_settings=inspect.get('NetworkSettings'),
  293. docker_path=inspect.get('Path'),
  294. docker_resolv_conf_path=inspect.get('ResolvConfPath'),
  295. docker_state=inspect.get('State'),
  296. docker_volumes=inspect.get('Volumes'),
  297. docker_volumes_rw=inspect.get('VolumesRW'),
  298. )
  299. hostvars[name].update(container_info)
  300. groups['docker_hosts'] = [host.get('base_url') for host in hosts]
  301. groups['_meta'] = dict()
  302. groups['_meta']['hostvars'] = hostvars
  303. print json.dumps(groups, sort_keys=True, indent=4)
  304. sys.exit(0)
  305. def parse_args():
  306. parser = argparse.ArgumentParser()
  307. group = parser.add_mutually_exclusive_group(required=True)
  308. group.add_argument('--list', action='store_true')
  309. group.add_argument('--host', action='store_true')
  310. return parser.parse_args()
  311. def main():
  312. args = parse_args()
  313. if args.list:
  314. list_groups()
  315. elif args.host:
  316. write_stderr('This option is not supported.')
  317. sys.exit(1)
  318. sys.exit(0)
  319. main()