12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036 |
- #!/usr/bin/python
- # pylint: skip-file
- # flake8: noqa
- # TODO: remove this file once openshift-ansible requires ansible >= 2.3.
- # This file is a copy of
- # https://github.com/ansible/ansible/blob/20bf02f/lib/ansible/modules/cloud/docker/docker_container.py.
- # It has been temporarily vendored here due to issue https://github.com/ansible/ansible/issues/22323.
- # Copyright 2016 Red Hat | Ansible
- #
- # This file is part of Ansible
- #
- # Ansible is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Ansible is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
- ANSIBLE_METADATA = {'status': ['preview'],
- 'supported_by': 'committer',
- 'version': '1.0'}
- DOCUMENTATION = '''
- ---
- module: docker_container
- short_description: manage docker containers
- description:
- - Manage the life cycle of docker containers.
- - Supports check mode. Run with --check and --diff to view config difference and list of actions to be taken.
- version_added: "2.1"
- options:
- blkio_weight:
- description:
- - Block IO (relative weight), between 10 and 1000.
- default: null
- required: false
- capabilities:
- description:
- - List of capabilities to add to the container.
- default: null
- required: false
- cleanup:
- description:
- - Use with I(detach) to remove the container after successful execution.
- default: false
- required: false
- version_added: "2.2"
- command:
- description:
- - Command to execute when the container starts.
- default: null
- required: false
- cpu_period:
- description:
- - Limit CPU CFS (Completely Fair Scheduler) period
- default: 0
- required: false
- cpu_quota:
- description:
- - Limit CPU CFS (Completely Fair Scheduler) quota
- default: 0
- required: false
- cpuset_cpus:
- description:
- - CPUs in which to allow execution C(1,3) or C(1-3).
- default: null
- required: false
- cpuset_mems:
- description:
- - Memory nodes (MEMs) in which to allow execution C(0-3) or C(0,1)
- default: null
- required: false
- cpu_shares:
- description:
- - CPU shares (relative weight).
- default: null
- required: false
- detach:
- description:
- - Enable detached mode to leave the container running in background.
- If disabled, the task will reflect the status of the container run (failed if the command failed).
- default: true
- required: false
- devices:
- description:
- - "List of host device bindings to add to the container. Each binding is a mapping expressed
- in the format: <path_on_host>:<path_in_container>:<cgroup_permissions>"
- default: null
- required: false
- dns_servers:
- description:
- - List of custom DNS servers.
- default: null
- required: false
- dns_search_domains:
- description:
- - List of custom DNS search domains.
- default: null
- required: false
- env:
- description:
- - Dictionary of key,value pairs.
- default: null
- required: false
- env_file:
- version_added: "2.2"
- description:
- - Path to a file containing environment variables I(FOO=BAR).
- - If variable also present in C(env), then C(env) value will override.
- - Requires docker-py >= 1.4.0.
- default: null
- required: false
- entrypoint:
- description:
- - Command that overwrites the default ENTRYPOINT of the image.
- default: null
- required: false
- etc_hosts:
- description:
- - Dict of host-to-IP mappings, where each host name is a key in the dictionary.
- Each host name will be added to the container's /etc/hosts file.
- default: null
- required: false
- exposed_ports:
- description:
- - List of additional container ports which informs Docker that the container
- listens on the specified network ports at runtime.
- If the port is already exposed using EXPOSE in a Dockerfile, it does not
- need to be exposed again.
- default: null
- required: false
- aliases:
- - exposed
- force_kill:
- description:
- - Use the kill command when stopping a running container.
- default: false
- required: false
- groups:
- description:
- - List of additional group names and/or IDs that the container process will run as.
- default: null
- required: false
- hostname:
- description:
- - Container hostname.
- default: null
- required: false
- ignore_image:
- description:
- - When C(state) is I(present) or I(started) the module compares the configuration of an existing
- container to requested configuration. The evaluation includes the image version. If
- the image version in the registry does not match the container, the container will be
- recreated. Stop this behavior by setting C(ignore_image) to I(True).
- default: false
- required: false
- version_added: "2.2"
- image:
- description:
- - Repository path and tag used to create the container. If an image is not found or pull is true, the image
- will be pulled from the registry. If no tag is included, 'latest' will be used.
- default: null
- required: false
- interactive:
- description:
- - Keep stdin open after a container is launched, even if not attached.
- default: false
- required: false
- ipc_mode:
- description:
- - Set the IPC mode for the container. Can be one of 'container:<name|id>' to reuse another
- container's IPC namespace or 'host' to use the host's IPC namespace within the container.
- default: null
- required: false
- keep_volumes:
- description:
- - Retain volumes associated with a removed container.
- default: true
- required: false
- kill_signal:
- description:
- - Override default signal used to kill a running container.
- default null:
- required: false
- kernel_memory:
- description:
- - "Kernel memory limit (format: <number>[<unit>]). Number is a positive integer.
- Unit can be one of b, k, m, or g. Minimum is 4M."
- default: 0
- required: false
- labels:
- description:
- - Dictionary of key value pairs.
- default: null
- required: false
- links:
- description:
- - List of name aliases for linked containers in the format C(container_name:alias)
- default: null
- required: false
- log_driver:
- description:
- - Specify the logging driver. Docker uses json-file by default.
- choices:
- - none
- - json-file
- - syslog
- - journald
- - gelf
- - fluentd
- - awslogs
- - splunk
- default: null
- required: false
- log_options:
- description:
- - Dictionary of options specific to the chosen log_driver. See https://docs.docker.com/engine/admin/logging/overview/
- for details.
- required: false
- default: null
- mac_address:
- description:
- - Container MAC address (e.g. 92:d0:c6:0a:29:33)
- default: null
- required: false
- memory:
- description:
- - "Memory limit (format: <number>[<unit>]). Number is a positive integer.
- Unit can be one of b, k, m, or g"
- default: 0
- required: false
- memory_reservation:
- description:
- - "Memory soft limit (format: <number>[<unit>]). Number is a positive integer.
- Unit can be one of b, k, m, or g"
- default: 0
- required: false
- memory_swap:
- description:
- - Total memory limit (memory + swap, format:<number>[<unit>]).
- Number is a positive integer. Unit can be one of b, k, m, or g.
- default: 0
- required: false
- memory_swappiness:
- description:
- - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100.
- default: 0
- required: false
- name:
- description:
- - Assign a name to a new container or match an existing container.
- - When identifying an existing container name may be a name or a long or short container ID.
- required: true
- network_mode:
- description:
- - Connect the container to a network.
- choices:
- - bridge
- - container:<name|id>
- - host
- - none
- default: null
- required: false
- networks:
- description:
- - List of networks the container belongs to.
- - Each network is a dict with keys C(name), C(ipv4_address), C(ipv6_address), C(links), C(aliases).
- - For each network C(name) is required, all other keys are optional.
- - If included, C(links) or C(aliases) are lists.
- - For examples of the data structure and usage see EXAMPLES below.
- - To remove a container from one or more networks, use the C(purge_networks) option.
- default: null
- required: false
- version_added: "2.2"
- oom_killer:
- description:
- - Whether or not to disable OOM Killer for the container.
- default: false
- required: false
- oom_score_adj:
- description:
- - An integer value containing the score given to the container in order to tune OOM killer preferences.
- default: 0
- required: false
- version_added: "2.2"
- paused:
- description:
- - Use with the started state to pause running processes inside the container.
- default: false
- required: false
- pid_mode:
- description:
- - Set the PID namespace mode for the container. Currently only supports 'host'.
- default: null
- required: false
- privileged:
- description:
- - Give extended privileges to the container.
- default: false
- required: false
- published_ports:
- description:
- - List of ports to publish from the container to the host.
- - "Use docker CLI syntax: C(8000), C(9000:8000), or C(0.0.0.0:9000:8000), where 8000 is a
- container port, 9000 is a host port, and 0.0.0.0 is a host interface."
- - Container ports must be exposed either in the Dockerfile or via the C(expose) option.
- - A value of all will publish all exposed container ports to random host ports, ignoring
- any other mappings.
- - If C(networks) parameter is provided, will inspect each network to see if there exists
- a bridge network with optional parameter com.docker.network.bridge.host_binding_ipv4.
- If such a network is found, then published ports where no host IP address is specified
- will be bound to the host IP pointed to by com.docker.network.bridge.host_binding_ipv4.
- Note that the first bridge network with a com.docker.network.bridge.host_binding_ipv4
- value encountered in the list of C(networks) is the one that will be used.
- aliases:
- - ports
- required: false
- default: null
- pull:
- description:
- - If true, always pull the latest version of an image. Otherwise, will only pull an image when missing.
- default: false
- required: false
- purge_networks:
- description:
- - Remove the container from ALL networks not included in C(networks) parameter.
- - Any default networks such as I(bridge), if not found in C(networks), will be removed as well.
- default: false
- required: false
- version_added: "2.2"
- read_only:
- description:
- - Mount the container's root file system as read-only.
- default: false
- required: false
- recreate:
- description:
- - Use with present and started states to force the re-creation of an existing container.
- default: false
- required: false
- restart:
- description:
- - Use with started state to force a matching container to be stopped and restarted.
- default: false
- required: false
- restart_policy:
- description:
- - Container restart policy. Place quotes around I(no) option.
- choices:
- - always
- - no
- - on-failure
- - unless-stopped
- default: on-failure
- required: false
- restart_retries:
- description:
- - Use with restart policy to control maximum number of restart attempts.
- default: 0
- required: false
- shm_size:
- description:
- - Size of `/dev/shm`. The format is `<number><unit>`. `number` must be greater than `0`.
- Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes).
- - Omitting the unit defaults to bytes. If you omit the size entirely, the system uses `64m`.
- default: null
- required: false
- security_opts:
- description:
- - List of security options in the form of C("label:user:User")
- default: null
- required: false
- state:
- description:
- - 'I(absent) - A container matching the specified name will be stopped and removed. Use force_kill to kill the container
- rather than stopping it. Use keep_volumes to retain volumes associated with the removed container.'
- - 'I(present) - Asserts the existence of a container matching the name and any provided configuration parameters. If no
- container matches the name, a container will be created. If a container matches the name but the provided configuration
- does not match, the container will be updated, if it can be. If it cannot be updated, it will be removed and re-created
- with the requested config. Image version will be taken into account when comparing configuration. To ignore image
- version use the ignore_image option. Use the recreate option to force the re-creation of the matching container. Use
- force_kill to kill the container rather than stopping it. Use keep_volumes to retain volumes associated with a removed
- container.'
- - 'I(started) - Asserts there is a running container matching the name and any provided configuration. If no container
- matches the name, a container will be created and started. If a container matching the name is found but the
- configuration does not match, the container will be updated, if it can be. If it cannot be updated, it will be removed
- and a new container will be created with the requested configuration and started. Image version will be taken into
- account when comparing configuration. To ignore image version use the ignore_image option. Use recreate to always
- re-create a matching container, even if it is running. Use restart to force a matching container to be stopped and
- restarted. Use force_kill to kill a container rather than stopping it. Use keep_volumes to retain volumes associated
- with a removed container.'
- - 'I(stopped) - Asserts that the container is first I(present), and then if the container is running moves it to a stopped
- state. Use force_kill to kill a container rather than stopping it.'
- required: false
- default: started
- choices:
- - absent
- - present
- - stopped
- - started
- stop_signal:
- description:
- - Override default signal used to stop the container.
- default: null
- required: false
- stop_timeout:
- description:
- - Number of seconds to wait for the container to stop before sending SIGKILL.
- required: false
- default: null
- trust_image_content:
- description:
- - If true, skip image verification.
- default: false
- required: false
- tty:
- description:
- - Allocate a psuedo-TTY.
- default: false
- required: false
- ulimits:
- description:
- - "List of ulimit options. A ulimit is specified as C(nofile:262144:262144)"
- default: null
- required: false
- user:
- description:
- - Sets the username or UID used and optionally the groupname or GID for the specified command.
- - "Can be [ user | user:group | uid | uid:gid | user:gid | uid:group ]"
- default: null
- required: false
- uts:
- description:
- - Set the UTS namespace mode for the container.
- default: null
- required: false
- volumes:
- description:
- - List of volumes to mount within the container.
- - "Use docker CLI-style syntax: C(/host:/container[:mode])"
- - You can specify a read mode for the mount with either C(ro) or C(rw).
- - SELinux hosts can additionally use C(z) or C(Z) to use a shared or
- private label for the volume.
- default: null
- required: false
- volume_driver:
- description:
- - The container volume driver.
- default: none
- required: false
- volumes_from:
- description:
- - List of container names or Ids to get volumes from.
- default: null
- required: false
- extends_documentation_fragment:
- - docker
- author:
- - "Cove Schneider (@cove)"
- - "Joshua Conner (@joshuaconner)"
- - "Pavel Antonov (@softzilla)"
- - "Thomas Steinbach (@ThomasSteinbach)"
- - "Philippe Jandot (@zfil)"
- - "Daan Oosterveld (@dusdanig)"
- - "James Tanner (@jctanner)"
- - "Chris Houseknecht (@chouseknecht)"
- requirements:
- - "python >= 2.6"
- - "docker-py >= 1.7.0"
- - "Docker API >= 1.20"
- '''
- EXAMPLES = '''
- - name: Create a data container
- docker_container:
- name: mydata
- image: busybox
- volumes:
- - /data
- - name: Re-create a redis container
- docker_container:
- name: myredis
- image: redis
- command: redis-server --appendonly yes
- state: present
- recreate: yes
- exposed_ports:
- - 6379
- volumes_from:
- - mydata
- - name: Restart a container
- docker_container:
- name: myapplication
- image: someuser/appimage
- state: started
- restart: yes
- links:
- - "myredis:aliasedredis"
- devices:
- - "/dev/sda:/dev/xvda:rwm"
- ports:
- - "8080:9000"
- - "127.0.0.1:8081:9001/udp"
- env:
- SECRET_KEY: ssssh
- - name: Container present
- docker_container:
- name: mycontainer
- state: present
- image: ubuntu:14.04
- command: sleep infinity
- - name: Stop a container
- docker_container:
- name: mycontainer
- state: stopped
- - name: Start 4 load-balanced containers
- docker_container:
- name: "container{{ item }}"
- recreate: yes
- image: someuser/anotherappimage
- command: sleep 1d
- with_sequence: count=4
- - name: remove container
- docker_container:
- name: ohno
- state: absent
- - name: Syslogging output
- docker_container:
- name: myservice
- image: busybox
- log_driver: syslog
- log_options:
- syslog-address: tcp://my-syslog-server:514
- syslog-facility: daemon
- # NOTE: in Docker 1.13+ the "syslog-tag" option was renamed to "tag" for
- # older docker installs, use "syslog-tag" instead
- tag: myservice
- - name: Create db container and connect to network
- docker_container:
- name: db_test
- image: "postgres:latest"
- networks:
- - name: "{{ docker_network_name }}"
- - name: Start container, connect to network and link
- docker_container:
- name: sleeper
- image: ubuntu:14.04
- networks:
- - name: TestingNet
- ipv4_address: "172.1.1.100"
- aliases:
- - sleepyzz
- links:
- - db_test:db
- - name: TestingNet2
- - name: Start a container with a command
- docker_container:
- name: sleepy
- image: ubuntu:14.04
- command: sleep infinity
- - name: Add container to networks
- docker_container:
- name: sleepy
- networks:
- - name: TestingNet
- ipv4_address: 172.1.1.18
- links:
- - sleeper
- - name: TestingNet2
- ipv4_address: 172.1.10.20
- - name: Update network with aliases
- docker_container:
- name: sleepy
- networks:
- - name: TestingNet
- aliases:
- - sleepyz
- - zzzz
- - name: Remove container from one network
- docker_container:
- name: sleepy
- networks:
- - name: TestingNet2
- purge_networks: yes
- - name: Remove container from all networks
- docker_container:
- name: sleepy
- purge_networks: yes
- '''
- RETURN = '''
- docker_container:
- description:
- - Before 2.3 this was 'ansible_docker_container' but was renamed due to conflicts with the connection plugin.
- - Facts representing the current state of the container. Matches the docker inspection output.
- - Note that facts are not part of registered vars but accessible directly.
- - Empty if C(state) is I(absent)
- - If detached is I(False), will include Output attribute containing any output from container run.
- returned: always
- type: dict
- sample: '{
- "AppArmorProfile": "",
- "Args": [],
- "Config": {
- "AttachStderr": false,
- "AttachStdin": false,
- "AttachStdout": false,
- "Cmd": [
- "/usr/bin/supervisord"
- ],
- "Domainname": "",
- "Entrypoint": null,
- "Env": [
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
- ],
- "ExposedPorts": {
- "443/tcp": {},
- "80/tcp": {}
- },
- "Hostname": "8e47bf643eb9",
- "Image": "lnmp_nginx:v1",
- "Labels": {},
- "OnBuild": null,
- "OpenStdin": false,
- "StdinOnce": false,
- "Tty": false,
- "User": "",
- "Volumes": {
- "/tmp/lnmp/nginx-sites/logs/": {}
- },
- ...
- }'
- '''
- import re
- from ansible.module_utils.docker_common import *
- try:
- from docker import utils
- if HAS_DOCKER_PY_2:
- from docker.types import Ulimit
- else:
- from docker.utils.types import Ulimit
- except:
- # missing docker-py handled in ansible.module_utils.docker
- pass
- REQUIRES_CONVERSION_TO_BYTES = [
- 'memory',
- 'memory_reservation',
- 'memory_swap',
- 'shm_size'
- ]
- VOLUME_PERMISSIONS = ('rw', 'ro', 'z', 'Z')
- class TaskParameters(DockerBaseClass):
- '''
- Access and parse module parameters
- '''
- def __init__(self, client):
- super(TaskParameters, self).__init__()
- self.client = client
- self.blkio_weight = None
- self.capabilities = None
- self.cleanup = None
- self.command = None
- self.cpu_period = None
- self.cpu_quota = None
- self.cpuset_cpus = None
- self.cpuset_mems = None
- self.cpu_shares = None
- self.detach = None
- self.debug = None
- self.devices = None
- self.dns_servers = None
- self.dns_opts = None
- self.dns_search_domains = None
- self.env = None
- self.env_file = None
- self.entrypoint = None
- self.etc_hosts = None
- self.exposed_ports = None
- self.force_kill = None
- self.groups = None
- self.hostname = None
- self.ignore_image = None
- self.image = None
- self.interactive = None
- self.ipc_mode = None
- self.keep_volumes = None
- self.kernel_memory = None
- self.kill_signal = None
- self.labels = None
- self.links = None
- self.log_driver = None
- self.log_options = None
- self.mac_address = None
- self.memory = None
- self.memory_reservation = None
- self.memory_swap = None
- self.memory_swappiness = None
- self.name = None
- self.network_mode = None
- self.networks = None
- self.oom_killer = None
- self.oom_score_adj = None
- self.paused = None
- self.pid_mode = None
- self.privileged = None
- self.purge_networks = None
- self.pull = None
- self.read_only = None
- self.recreate = None
- self.restart = None
- self.restart_retries = None
- self.restart_policy = None
- self.shm_size = None
- self.security_opts = None
- self.state = None
- self.stop_signal = None
- self.stop_timeout = None
- self.trust_image_content = None
- self.tty = None
- self.user = None
- self.uts = None
- self.volumes = None
- self.volume_binds = dict()
- self.volumes_from = None
- self.volume_driver = None
- for key, value in client.module.params.items():
- setattr(self, key, value)
- for param_name in REQUIRES_CONVERSION_TO_BYTES:
- if client.module.params.get(param_name):
- try:
- setattr(self, param_name, human_to_bytes(client.module.params.get(param_name)))
- except ValueError as exc:
- self.fail("Failed to convert %s to bytes: %s" % (param_name, exc))
- self.publish_all_ports = False
- self.published_ports = self._parse_publish_ports()
- if self.published_ports in ('all', 'ALL'):
- self.publish_all_ports = True
- self.published_ports = None
- self.ports = self._parse_exposed_ports(self.published_ports)
- self.log("expose ports:")
- self.log(self.ports, pretty_print=True)
- self.links = self._parse_links(self.links)
- if self.volumes:
- self.volumes = self._expand_host_paths()
- self.env = self._get_environment()
- self.ulimits = self._parse_ulimits()
- self.log_config = self._parse_log_config()
- self.exp_links = None
- self.volume_binds = self._get_volume_binds(self.volumes)
- self.log("volumes:")
- self.log(self.volumes, pretty_print=True)
- self.log("volume binds:")
- self.log(self.volume_binds, pretty_print=True)
- if self.networks:
- for network in self.networks:
- if not network.get('name'):
- self.fail("Parameter error: network must have a name attribute.")
- network['id'] = self._get_network_id(network['name'])
- if not network['id']:
- self.fail("Parameter error: network named %s could not be found. Does it exist?" % network['name'])
- if network.get('links'):
- network['links'] = self._parse_links(network['links'])
- def fail(self, msg):
- self.client.module.fail_json(msg=msg)
- @property
- def update_parameters(self):
- '''
- Returns parameters used to update a container
- '''
- update_parameters = dict(
- blkio_weight='blkio_weight',
- cpu_period='cpu_period',
- cpu_quota='cpu_quota',
- cpu_shares='cpu_shares',
- cpuset_cpus='cpuset_cpus',
- mem_limit='memory',
- mem_reservation='mem_reservation',
- memswap_limit='memory_swap',
- kernel_memory='kernel_memory'
- )
- result = dict()
- for key, value in update_parameters.items():
- if getattr(self, value, None) is not None:
- result[key] = getattr(self, value)
- return result
- @property
- def create_parameters(self):
- '''
- Returns parameters used to create a container
- '''
- create_params = dict(
- command='command',
- hostname='hostname',
- user='user',
- detach='detach',
- stdin_open='interactive',
- tty='tty',
- ports='ports',
- environment='env',
- name='name',
- entrypoint='entrypoint',
- cpu_shares='cpu_shares',
- mac_address='mac_address',
- labels='labels',
- stop_signal='stop_signal',
- volume_driver='volume_driver',
- )
- result = dict(
- host_config=self._host_config(),
- volumes=self._get_mounts(),
- )
- for key, value in create_params.items():
- if getattr(self, value, None) is not None:
- result[key] = getattr(self, value)
- return result
- def _expand_host_paths(self):
- new_vols = []
- for vol in self.volumes:
- if ':' in vol:
- if len(vol.split(':')) == 3:
- host, container, mode = vol.split(':')
- if re.match(r'[\.~]', host):
- host = os.path.abspath(host)
- new_vols.append("%s:%s:%s" % (host, container, mode))
- continue
- elif len(vol.split(':')) == 2:
- parts = vol.split(':')
- if parts[1] not in VOLUME_PERMISSIONS and re.match(r'[\.~]', parts[0]):
- host = os.path.abspath(parts[0])
- new_vols.append("%s:%s:rw" % (host, parts[1]))
- continue
- new_vols.append(vol)
- return new_vols
- def _get_mounts(self):
- '''
- Return a list of container mounts.
- :return:
- '''
- result = []
- if self.volumes:
- for vol in self.volumes:
- if ':' in vol:
- if len(vol.split(':')) == 3:
- host, container, _ = vol.split(':')
- result.append(container)
- continue
- if len(vol.split(':')) == 2:
- parts = vol.split(':')
- if parts[1] not in VOLUME_PERMISSIONS:
- result.append(parts[1])
- continue
- result.append(vol)
- self.log("mounts:")
- self.log(result, pretty_print=True)
- return result
- def _host_config(self):
- '''
- Returns parameters used to create a HostConfig object
- '''
- host_config_params=dict(
- port_bindings='published_ports',
- publish_all_ports='publish_all_ports',
- links='links',
- privileged='privileged',
- dns='dns_servers',
- dns_search='dns_search_domains',
- binds='volume_binds',
- volumes_from='volumes_from',
- network_mode='network_mode',
- cap_add='capabilities',
- extra_hosts='etc_hosts',
- read_only='read_only',
- ipc_mode='ipc_mode',
- security_opt='security_opts',
- ulimits='ulimits',
- log_config='log_config',
- mem_limit='memory',
- memswap_limit='memory_swap',
- mem_swappiness='memory_swappiness',
- oom_score_adj='oom_score_adj',
- shm_size='shm_size',
- group_add='groups',
- devices='devices',
- pid_mode='pid_mode'
- )
- params = dict()
- for key, value in host_config_params.items():
- if getattr(self, value, None) is not None:
- params[key] = getattr(self, value)
- if self.restart_policy:
- params['restart_policy'] = dict(Name=self.restart_policy,
- MaximumRetryCount=self.restart_retries)
- return self.client.create_host_config(**params)
- @property
- def default_host_ip(self):
- ip = '0.0.0.0'
- if not self.networks:
- return ip
- for net in self.networks:
- if net.get('name'):
- network = self.client.inspect_network(net['name'])
- if network.get('Driver') == 'bridge' and \
- network.get('Options', {}).get('com.docker.network.bridge.host_binding_ipv4'):
- ip = network['Options']['com.docker.network.bridge.host_binding_ipv4']
- break
- return ip
- def _parse_publish_ports(self):
- '''
- Parse ports from docker CLI syntax
- '''
- if self.published_ports is None:
- return None
- if 'all' in self.published_ports:
- return 'all'
- default_ip = self.default_host_ip
- binds = {}
- for port in self.published_ports:
- parts = str(port).split(':')
- container_port = parts[-1]
- if '/' not in container_port:
- container_port = int(parts[-1])
- p_len = len(parts)
- if p_len == 1:
- bind = (default_ip,)
- elif p_len == 2:
- bind = (default_ip, int(parts[0]))
- elif p_len == 3:
- bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],)
- if container_port in binds:
- old_bind = binds[container_port]
- if isinstance(old_bind, list):
- old_bind.append(bind)
- else:
- binds[container_port] = [binds[container_port], bind]
- else:
- binds[container_port] = bind
- return binds
- @staticmethod
- def _get_volume_binds(volumes):
- '''
- Extract host bindings, if any, from list of volume mapping strings.
- :return: dictionary of bind mappings
- '''
- result = dict()
- if volumes:
- for vol in volumes:
- host = None
- if ':' in vol:
- if len(vol.split(':')) == 3:
- host, container, mode = vol.split(':')
- if len(vol.split(':')) == 2:
- parts = vol.split(':')
- if parts[1] not in VOLUME_PERMISSIONS:
- host, container, mode = (vol.split(':') + ['rw'])
- if host is not None:
- result[host] = dict(
- bind=container,
- mode=mode
- )
- return result
- def _parse_exposed_ports(self, published_ports):
- '''
- Parse exposed ports from docker CLI-style ports syntax.
- '''
- exposed = []
- if self.exposed_ports:
- for port in self.exposed_ports:
- port = str(port).strip()
- protocol = 'tcp'
- match = re.search(r'(/.+$)', port)
- if match:
- protocol = match.group(1).replace('/', '')
- port = re.sub(r'/.+$', '', port)
- exposed.append((port, protocol))
- if published_ports:
- # Any published port should also be exposed
- for publish_port in published_ports:
- match = False
- if isinstance(publish_port, basestring) and '/' in publish_port:
- port, protocol = publish_port.split('/')
- port = int(port)
- else:
- protocol = 'tcp'
- port = int(publish_port)
- for exposed_port in exposed:
- if isinstance(exposed_port[0], basestring) and '-' in exposed_port[0]:
- start_port, end_port = exposed_port[0].split('-')
- if int(start_port) <= port <= int(end_port):
- match = True
- elif exposed_port[0] == port:
- match = True
- if not match:
- exposed.append((port, protocol))
- return exposed
- @staticmethod
- def _parse_links(links):
- '''
- Turn links into a dictionary
- '''
- if links is None:
- return None
- result = {}
- for link in links:
- parsed_link = link.split(':', 1)
- if len(parsed_link) == 2:
- result[parsed_link[0]] = parsed_link[1]
- else:
- result[parsed_link[0]] = parsed_link[0]
- return result
- def _parse_ulimits(self):
- '''
- Turn ulimits into an array of Ulimit objects
- '''
- if self.ulimits is None:
- return None
- results = []
- for limit in self.ulimits:
- limits = dict()
- pieces = limit.split(':')
- if len(pieces) >= 2:
- limits['name'] = pieces[0]
- limits['soft'] = int(pieces[1])
- limits['hard'] = int(pieces[1])
- if len(pieces) == 3:
- limits['hard'] = int(pieces[2])
- try:
- results.append(Ulimit(**limits))
- except ValueError as exc:
- self.fail("Error parsing ulimits value %s - %s" % (limit, exc))
- return results
- def _parse_log_config(self):
- '''
- Create a LogConfig object
- '''
- if self.log_driver is None:
- return None
- options = dict(
- Type=self.log_driver,
- Config = dict()
- )
- if self.log_options is not None:
- options['Config'] = self.log_options
- try:
- return LogConfig(**options)
- except ValueError as exc:
- self.fail('Error parsing logging options - %s' % (exc))
- def _get_environment(self):
- """
- If environment file is combined with explicit environment variables, the explicit environment variables
- take precedence.
- """
- final_env = {}
- if self.env_file:
- parsed_env_file = utils.parse_env_file(self.env_file)
- for name, value in parsed_env_file.items():
- final_env[name] = str(value)
- if self.env:
- for name, value in self.env.items():
- final_env[name] = str(value)
- return final_env
- def _get_network_id(self, network_name):
- network_id = None
- try:
- for network in self.client.networks(names=[network_name]):
- if network['Name'] == network_name:
- network_id = network['Id']
- break
- except Exception as exc:
- self.fail("Error getting network id for %s - %s" % (network_name, str(exc)))
- return network_id
- class Container(DockerBaseClass):
- def __init__(self, container, parameters):
- super(Container, self).__init__()
- self.raw = container
- self.Id = None
- self.container = container
- if container:
- self.Id = container['Id']
- self.Image = container['Image']
- self.log(self.container, pretty_print=True)
- self.parameters = parameters
- self.parameters.expected_links = None
- self.parameters.expected_ports = None
- self.parameters.expected_exposed = None
- self.parameters.expected_volumes = None
- self.parameters.expected_ulimits = None
- self.parameters.expected_etc_hosts = None
- self.parameters.expected_env = None
- def fail(self, msg):
- self.parameters.client.module.fail_json(msg=msg)
- @property
- def exists(self):
- return True if self.container else False
- @property
- def running(self):
- if self.container and self.container.get('State'):
- if self.container['State'].get('Running') and not self.container['State'].get('Ghost', False):
- return True
- return False
- def has_different_configuration(self, image):
- '''
- Diff parameters vs existing container config. Returns tuple: (True | False, List of differences)
- '''
- self.log('Starting has_different_configuration')
- self.parameters.expected_entrypoint = self._get_expected_entrypoint()
- self.parameters.expected_links = self._get_expected_links()
- self.parameters.expected_ports = self._get_expected_ports()
- self.parameters.expected_exposed = self._get_expected_exposed(image)
- self.parameters.expected_volumes = self._get_expected_volumes(image)
- self.parameters.expected_binds = self._get_expected_binds(image)
- self.parameters.expected_ulimits = self._get_expected_ulimits(self.parameters.ulimits)
- self.parameters.expected_etc_hosts = self._convert_simple_dict_to_list('etc_hosts')
- self.parameters.expected_env = self._get_expected_env(image)
- self.parameters.expected_cmd = self._get_expected_cmd()
- self.parameters.expected_devices = self._get_expected_devices()
- if not self.container.get('HostConfig'):
- self.fail("has_config_diff: Error parsing container properties. HostConfig missing.")
- if not self.container.get('Config'):
- self.fail("has_config_diff: Error parsing container properties. Config missing.")
- if not self.container.get('NetworkSettings'):
- self.fail("has_config_diff: Error parsing container properties. NetworkSettings missing.")
- host_config = self.container['HostConfig']
- log_config = host_config.get('LogConfig', dict())
- restart_policy = host_config.get('RestartPolicy', dict())
- config = self.container['Config']
- network = self.container['NetworkSettings']
- # The previous version of the docker module ignored the detach state by
- # assuming if the container was running, it must have been detached.
- detach = not (config.get('AttachStderr') and config.get('AttachStdout'))
- # "ExposedPorts": null returns None type & causes AttributeError - PR #5517
- if config.get('ExposedPorts') is not None:
- expected_exposed = [re.sub(r'/.+$', '', p) for p in config.get('ExposedPorts', dict()).keys()]
- else:
- expected_exposed = []
- # Map parameters to container inspect results
- config_mapping = dict(
- image=config.get('Image'),
- expected_cmd=config.get('Cmd'),
- hostname=config.get('Hostname'),
- user=config.get('User'),
- detach=detach,
- interactive=config.get('OpenStdin'),
- capabilities=host_config.get('CapAdd'),
- expected_devices=host_config.get('Devices'),
- dns_servers=host_config.get('Dns'),
- dns_opts=host_config.get('DnsOptions'),
- dns_search_domains=host_config.get('DnsSearch'),
- expected_env=(config.get('Env') or []),
- expected_entrypoint=config.get('Entrypoint'),
- expected_etc_hosts=host_config['ExtraHosts'],
- expected_exposed=expected_exposed,
- groups=host_config.get('GroupAdd'),
- ipc_mode=host_config.get("IpcMode"),
- labels=config.get('Labels'),
- expected_links=host_config.get('Links'),
- log_driver=log_config.get('Type'),
- log_options=log_config.get('Config'),
- mac_address=network.get('MacAddress'),
- memory_swappiness=host_config.get('MemorySwappiness'),
- network_mode=host_config.get('NetworkMode'),
- oom_killer=host_config.get('OomKillDisable'),
- oom_score_adj=host_config.get('OomScoreAdj'),
- pid_mode=host_config.get('PidMode'),
- privileged=host_config.get('Privileged'),
- expected_ports=host_config.get('PortBindings'),
- read_only=host_config.get('ReadonlyRootfs'),
- restart_policy=restart_policy.get('Name'),
- restart_retries=restart_policy.get('MaximumRetryCount'),
- # Cannot test shm_size, as shm_size is not included in container inspection results.
- # shm_size=host_config.get('ShmSize'),
- security_opts=host_config.get("SecuriytOpt"),
- stop_signal=config.get("StopSignal"),
- tty=config.get('Tty'),
- expected_ulimits=host_config.get('Ulimits'),
- uts=host_config.get('UTSMode'),
- expected_volumes=config.get('Volumes'),
- expected_binds=host_config.get('Binds'),
- volumes_from=host_config.get('VolumesFrom'),
- volume_driver=host_config.get('VolumeDriver')
- )
- differences = []
- for key, value in config_mapping.items():
- self.log('check differences %s %s vs %s' % (key, getattr(self.parameters, key), str(value)))
- if getattr(self.parameters, key, None) is not None:
- if isinstance(getattr(self.parameters, key), list) and isinstance(value, list):
- if len(getattr(self.parameters, key)) > 0 and isinstance(getattr(self.parameters, key)[0], dict):
- # compare list of dictionaries
- self.log("comparing list of dict: %s" % key)
- match = self._compare_dictionary_lists(getattr(self.parameters, key), value)
- else:
- # compare two lists. Is list_a in list_b?
- self.log("comparing lists: %s" % key)
- set_a = set(getattr(self.parameters, key))
- set_b = set(value)
- match = (set_a <= set_b)
- elif isinstance(getattr(self.parameters, key), dict) and isinstance(value, dict):
- # compare two dicts
- self.log("comparing two dicts: %s" % key)
- match = self._compare_dicts(getattr(self.parameters, key), value)
- else:
- # primitive compare
- self.log("primitive compare: %s" % key)
- match = (getattr(self.parameters, key) == value)
- if not match:
- # no match. record the differences
- item = dict()
- item[key] = dict(
- parameter=getattr(self.parameters, key),
- container=value
- )
- differences.append(item)
- has_differences = True if len(differences) > 0 else False
- return has_differences, differences
- def _compare_dictionary_lists(self, list_a, list_b):
- '''
- If all of list_a exists in list_b, return True
- '''
- if not isinstance(list_a, list) or not isinstance(list_b, list):
- return False
- matches = 0
- for dict_a in list_a:
- for dict_b in list_b:
- if self._compare_dicts(dict_a, dict_b):
- matches += 1
- break
- result = (matches == len(list_a))
- return result
- def _compare_dicts(self, dict_a, dict_b):
- '''
- If dict_a in dict_b, return True
- '''
- if not isinstance(dict_a, dict) or not isinstance(dict_b, dict):
- return False
- for key, value in dict_a.items():
- if isinstance(value, dict):
- match = self._compare_dicts(value, dict_b.get(key))
- elif isinstance(value, list):
- if len(value) > 0 and isinstance(value[0], dict):
- match = self._compare_dictionary_lists(value, dict_b.get(key))
- else:
- set_a = set(value)
- set_b = set(dict_b.get(key))
- match = (set_a == set_b)
- else:
- match = (value == dict_b.get(key))
- if not match:
- return False
- return True
- def has_different_resource_limits(self):
- '''
- Diff parameters and container resource limits
- '''
- if not self.container.get('HostConfig'):
- self.fail("limits_differ_from_container: Error parsing container properties. HostConfig missing.")
- host_config = self.container['HostConfig']
- config_mapping = dict(
- cpu_period=host_config.get('CpuPeriod'),
- cpu_quota=host_config.get('CpuQuota'),
- cpuset_cpus=host_config.get('CpusetCpus'),
- cpuset_mems=host_config.get('CpusetMems'),
- cpu_shares=host_config.get('CpuShares'),
- kernel_memory=host_config.get("KernelMemory"),
- memory=host_config.get('Memory'),
- memory_reservation=host_config.get('MemoryReservation'),
- memory_swap=host_config.get('MemorySwap'),
- oom_score_adj=host_config.get('OomScoreAdj'),
- )
- differences = []
- for key, value in config_mapping.items():
- if getattr(self.parameters, key, None) and getattr(self.parameters, key) != value:
- # no match. record the differences
- item = dict()
- item[key] = dict(
- parameter=getattr(self.parameters, key),
- container=value
- )
- differences.append(item)
- different = (len(differences) > 0)
- return different, differences
- def has_network_differences(self):
- '''
- Check if the container is connected to requested networks with expected options: links, aliases, ipv4, ipv6
- '''
- different = False
- differences = []
- if not self.parameters.networks:
- return different, differences
- if not self.container.get('NetworkSettings'):
- self.fail("has_missing_networks: Error parsing container properties. NetworkSettings missing.")
- connected_networks = self.container['NetworkSettings']['Networks']
- for network in self.parameters.networks:
- if connected_networks.get(network['name'], None) is None:
- different = True
- differences.append(dict(
- parameter=network,
- container=None
- ))
- else:
- diff = False
- if network.get('ipv4_address') and network['ipv4_address'] != connected_networks[network['name']].get('IPAddress'):
- diff = True
- if network.get('ipv6_address') and network['ipv6_address'] != connected_networks[network['name']].get('GlobalIPv6Address'):
- diff = True
- if network.get('aliases') and not connected_networks[network['name']].get('Aliases'):
- diff = True
- if network.get('aliases') and connected_networks[network['name']].get('Aliases'):
- for alias in network.get('aliases'):
- if alias not in connected_networks[network['name']].get('Aliases', []):
- diff = True
- if network.get('links') and not connected_networks[network['name']].get('Links'):
- diff = True
- if network.get('links') and connected_networks[network['name']].get('Links'):
- expected_links = []
- for link, alias in network['links'].items():
- expected_links.append("%s:%s" % (link, alias))
- for link in expected_links:
- if link not in connected_networks[network['name']].get('Links', []):
- diff = True
- if diff:
- different = True
- differences.append(dict(
- parameter=network,
- container=dict(
- name=network['name'],
- ipv4_address=connected_networks[network['name']].get('IPAddress'),
- ipv6_address=connected_networks[network['name']].get('GlobalIPv6Address'),
- aliases=connected_networks[network['name']].get('Aliases'),
- links=connected_networks[network['name']].get('Links')
- )
- ))
- return different, differences
- def has_extra_networks(self):
- '''
- Check if the container is connected to non-requested networks
- '''
- extra_networks = []
- extra = False
- if not self.container.get('NetworkSettings'):
- self.fail("has_extra_networks: Error parsing container properties. NetworkSettings missing.")
- connected_networks = self.container['NetworkSettings'].get('Networks')
- if connected_networks:
- for network, network_config in connected_networks.items():
- keep = False
- if self.parameters.networks:
- for expected_network in self.parameters.networks:
- if expected_network['name'] == network:
- keep = True
- if not keep:
- extra = True
- extra_networks.append(dict(name=network, id=network_config['NetworkID']))
- return extra, extra_networks
- def _get_expected_devices(self):
- if not self.parameters.devices:
- return None
- expected_devices = []
- for device in self.parameters.devices:
- parts = device.split(':')
- if len(parts) == 1:
- expected_devices.append(
- dict(
- CgroupPermissions='rwm',
- PathInContainer=parts[0],
- PathOnHost=parts[0]
- ))
- elif len(parts) == 2:
- parts = device.split(':')
- expected_devices.append(
- dict(
- CgroupPermissions='rwm',
- PathInContainer=parts[1],
- PathOnHost=parts[0]
- )
- )
- else:
- expected_devices.append(
- dict(
- CgroupPermissions=parts[2],
- PathInContainer=parts[1],
- PathOnHost=parts[0]
- ))
- return expected_devices
- def _get_expected_entrypoint(self):
- self.log('_get_expected_entrypoint')
- if not self.parameters.entrypoint:
- return None
- return shlex.split(self.parameters.entrypoint)
- def _get_expected_ports(self):
- if not self.parameters.published_ports:
- return None
- expected_bound_ports = {}
- for container_port, config in self.parameters.published_ports.items():
- if isinstance(container_port, int):
- container_port = "%s/tcp" % container_port
- if len(config) == 1:
- expected_bound_ports[container_port] = [{'HostIp': "0.0.0.0", 'HostPort': ""}]
- elif isinstance(config[0], tuple):
- expected_bound_ports[container_port] = []
- for host_ip, host_port in config:
- expected_bound_ports[container_port].append({'HostIp': host_ip, 'HostPort': str(host_port)})
- else:
- expected_bound_ports[container_port] = [{'HostIp': config[0], 'HostPort': str(config[1])}]
- return expected_bound_ports
- def _get_expected_links(self):
- if self.parameters.links is None:
- return None
- self.log('parameter links:')
- self.log(self.parameters.links, pretty_print=True)
- exp_links = []
- for link, alias in self.parameters.links.items():
- exp_links.append("/%s:%s/%s" % (link, ('/' + self.parameters.name), alias))
- return exp_links
- def _get_expected_binds(self, image):
- self.log('_get_expected_binds')
- image_vols = []
- if image:
- image_vols = self._get_image_binds(image['ContainerConfig'].get('Volumes'))
- param_vols = []
- if self.parameters.volumes:
- for vol in self.parameters.volumes:
- host = None
- if ':' in vol:
- if len(vol.split(':')) == 3:
- host, container, mode = vol.split(':')
- if len(vol.split(':')) == 2:
- parts = vol.split(':')
- if parts[1] not in VOLUME_PERMISSIONS:
- host, container, mode = vol.split(':') + ['rw']
- if host:
- param_vols.append("%s:%s:%s" % (host, container, mode))
- result = list(set(image_vols + param_vols))
- self.log("expected_binds:")
- self.log(result, pretty_print=True)
- return result
- def _get_image_binds(self, volumes):
- '''
- Convert array of binds to array of strings with format host_path:container_path:mode
- :param volumes: array of bind dicts
- :return: array of strings
- '''
- results = []
- if isinstance(volumes, dict):
- results += self._get_bind_from_dict(volumes)
- elif isinstance(volumes, list):
- for vol in volumes:
- results += self._get_bind_from_dict(vol)
- return results
- @staticmethod
- def _get_bind_from_dict(volume_dict):
- results = []
- if volume_dict:
- for host_path, config in volume_dict.items():
- if isinstance(config, dict) and config.get('bind'):
- container_path = config.get('bind')
- mode = config.get('mode', 'rw')
- results.append("%s:%s:%s" % (host_path, container_path, mode))
- return results
- def _get_expected_volumes(self, image):
- self.log('_get_expected_volumes')
- expected_vols = dict()
- if image and image['ContainerConfig'].get('Volumes'):
- expected_vols.update(image['ContainerConfig'].get('Volumes'))
- if self.parameters.volumes:
- for vol in self.parameters.volumes:
- container = None
- if ':' in vol:
- if len(vol.split(':')) == 3:
- host, container, mode = vol.split(':')
- if len(vol.split(':')) == 2:
- parts = vol.split(':')
- if parts[1] not in VOLUME_PERMISSIONS:
- host, container, mode = vol.split(':') + ['rw']
- new_vol = dict()
- if container:
- new_vol[container] = dict()
- else:
- new_vol[vol] = dict()
- expected_vols.update(new_vol)
- if not expected_vols:
- expected_vols = None
- self.log("expected_volumes:")
- self.log(expected_vols, pretty_print=True)
- return expected_vols
- def _get_expected_env(self, image):
- self.log('_get_expected_env')
- expected_env = dict()
- if image and image['ContainerConfig'].get('Env'):
- for env_var in image['ContainerConfig']['Env']:
- parts = env_var.split('=', 1)
- expected_env[parts[0]] = parts[1]
- if self.parameters.env:
- expected_env.update(self.parameters.env)
- param_env = []
- for key, value in expected_env.items():
- param_env.append("%s=%s" % (key, value))
- return param_env
- def _get_expected_exposed(self, image):
- self.log('_get_expected_exposed')
- image_ports = []
- if image:
- image_ports = [re.sub(r'/.+$', '', p) for p in (image['ContainerConfig'].get('ExposedPorts') or {}).keys()]
- param_ports = []
- if self.parameters.ports:
- param_ports = [str(p[0]) for p in self.parameters.ports]
- result = list(set(image_ports + param_ports))
- self.log(result, pretty_print=True)
- return result
- def _get_expected_ulimits(self, config_ulimits):
- self.log('_get_expected_ulimits')
- if config_ulimits is None:
- return None
- results = []
- for limit in config_ulimits:
- results.append(dict(
- Name=limit.name,
- Soft=limit.soft,
- Hard=limit.hard
- ))
- return results
- def _get_expected_cmd(self):
- self.log('_get_expected_cmd')
- if not self.parameters.command:
- return None
- return shlex.split(self.parameters.command)
- def _convert_simple_dict_to_list(self, param_name, join_with=':'):
- if getattr(self.parameters, param_name, None) is None:
- return None
- results = []
- for key, value in getattr(self.parameters, param_name).items():
- results.append("%s%s%s" % (key, join_with, value))
- return results
- class ContainerManager(DockerBaseClass):
- '''
- Perform container management tasks
- '''
- def __init__(self, client):
- super(ContainerManager, self).__init__()
- self.client = client
- self.parameters = TaskParameters(client)
- self.check_mode = self.client.check_mode
- self.results = {'changed': False, 'actions': []}
- self.diff = {}
- self.facts = {}
- state = self.parameters.state
- if state in ('stopped', 'started', 'present'):
- self.present(state)
- elif state == 'absent':
- self.absent()
- if not self.check_mode and not self.parameters.debug:
- self.results.pop('actions')
- if self.client.module._diff or self.parameters.debug:
- self.results['diff'] = self.diff
- if self.facts:
- self.results['ansible_facts'] = {'docker_container': self.facts}
- def present(self, state):
- container = self._get_container(self.parameters.name)
- image = self._get_image()
- if not container.exists:
- # New container
- self.log('No container found')
- new_container = self.container_create(self.parameters.image, self.parameters.create_parameters)
- if new_container:
- container = new_container
- else:
- # Existing container
- different, differences = container.has_different_configuration(image)
- image_different = False
- if not self.parameters.ignore_image:
- image_different = self._image_is_different(image, container)
- if image_different or different or self.parameters.recreate:
- self.diff['differences'] = differences
- if image_different:
- self.diff['image_different'] = True
- self.log("differences")
- self.log(differences, pretty_print=True)
- if container.running:
- self.container_stop(container.Id)
- self.container_remove(container.Id)
- new_container = self.container_create(self.parameters.image, self.parameters.create_parameters)
- if new_container:
- container = new_container
- if container and container.exists:
- container = self.update_limits(container)
- container = self.update_networks(container)
- if state == 'started' and not container.running:
- container = self.container_start(container.Id)
- elif state == 'started' and self.parameters.restart:
- self.container_stop(container.Id)
- container = self.container_start(container.Id)
- elif state == 'stopped' and container.running:
- self.container_stop(container.Id)
- container = self._get_container(container.Id)
- self.facts = container.raw
- def absent(self):
- container = self._get_container(self.parameters.name)
- if container.exists:
- if container.running:
- self.container_stop(container.Id)
- self.container_remove(container.Id)
- def fail(self, msg, **kwargs):
- self.client.module.fail_json(msg=msg, **kwargs)
- def _get_container(self, container):
- '''
- Expects container ID or Name. Returns a container object
- '''
- return Container(self.client.get_container(container), self.parameters)
- def _get_image(self):
- if not self.parameters.image:
- self.log('No image specified')
- return None
- repository, tag = utils.parse_repository_tag(self.parameters.image)
- if not tag:
- tag = "latest"
- image = self.client.find_image(repository, tag)
- if not self.check_mode:
- if not image or self.parameters.pull:
- self.log("Pull the image.")
- image, alreadyToLatest = self.client.pull_image(repository, tag)
- if alreadyToLatest:
- self.results['changed'] = False
- else:
- self.results['changed'] = True
- self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag)))
- self.log("image")
- self.log(image, pretty_print=True)
- return image
- def _image_is_different(self, image, container):
- if image and image.get('Id'):
- if container and container.Image:
- if image.get('Id') != container.Image:
- return True
- return False
- def update_limits(self, container):
- limits_differ, different_limits = container.has_different_resource_limits()
- if limits_differ:
- self.log("limit differences:")
- self.log(different_limits, pretty_print=True)
- if limits_differ and not self.check_mode:
- self.container_update(container.Id, self.parameters.update_parameters)
- return self._get_container(container.Id)
- return container
- def update_networks(self, container):
- has_network_differences, network_differences = container.has_network_differences()
- updated_container = container
- if has_network_differences:
- if self.diff.get('differences'):
- self.diff['differences'].append(dict(network_differences=network_differences))
- else:
- self.diff['differences'] = [dict(network_differences=network_differences)]
- self.results['changed'] = True
- updated_container = self._add_networks(container, network_differences)
- if self.parameters.purge_networks:
- has_extra_networks, extra_networks = container.has_extra_networks()
- if has_extra_networks:
- if self.diff.get('differences'):
- self.diff['differences'].append(dict(purge_networks=extra_networks))
- else:
- self.diff['differences'] = [dict(purge_networks=extra_networks)]
- self.results['changed'] = True
- updated_container = self._purge_networks(container, extra_networks)
- return updated_container
- def _add_networks(self, container, differences):
- for diff in differences:
- # remove the container from the network, if connected
- if diff.get('container'):
- self.results['actions'].append(dict(removed_from_network=diff['parameter']['name']))
- if not self.check_mode:
- try:
- self.client.disconnect_container_from_network(container.Id, diff['parameter']['id'])
- except Exception as exc:
- self.fail("Error disconnecting container from network %s - %s" % (diff['parameter']['name'],
- str(exc)))
- # connect to the network
- params = dict(
- ipv4_address=diff['parameter'].get('ipv4_address', None),
- ipv6_address=diff['parameter'].get('ipv6_address', None),
- links=diff['parameter'].get('links', None),
- aliases=diff['parameter'].get('aliases', None)
- )
- self.results['actions'].append(dict(added_to_network=diff['parameter']['name'], network_parameters=params))
- if not self.check_mode:
- try:
- self.log("Connecting container to network %s" % diff['parameter']['id'])
- self.log(params, pretty_print=True)
- self.client.connect_container_to_network(container.Id, diff['parameter']['id'], **params)
- except Exception as exc:
- self.fail("Error connecting container to network %s - %s" % (diff['parameter']['name'], str(exc)))
- return self._get_container(container.Id)
- def _purge_networks(self, container, networks):
- for network in networks:
- self.results['actions'].append(dict(removed_from_network=network['name']))
- if not self.check_mode:
- try:
- self.client.disconnect_container_from_network(container.Id, network['name'])
- except Exception as exc:
- self.fail("Error disconnecting container from network %s - %s" % (network['name'],
- str(exc)))
- return self._get_container(container.Id)
- def container_create(self, image, create_parameters):
- self.log("create container")
- self.log("image: %s parameters:" % image)
- self.log(create_parameters, pretty_print=True)
- self.results['actions'].append(dict(created="Created container", create_parameters=create_parameters))
- self.results['changed'] = True
- new_container = None
- if not self.check_mode:
- try:
- new_container = self.client.create_container(image, **create_parameters)
- except Exception as exc:
- self.fail("Error creating container: %s" % str(exc))
- return self._get_container(new_container['Id'])
- return new_container
- def container_start(self, container_id):
- self.log("start container %s" % (container_id))
- self.results['actions'].append(dict(started=container_id))
- self.results['changed'] = True
- if not self.check_mode:
- try:
- self.client.start(container=container_id)
- except Exception as exc:
- self.fail("Error starting container %s: %s" % (container_id, str(exc)))
- if not self.parameters.detach:
- status = self.client.wait(container_id)
- output = self.client.logs(container_id, stdout=True, stderr=True, stream=False, timestamps=False)
- if status != 0:
- self.fail(output, status=status)
- if self.parameters.cleanup:
- self.container_remove(container_id, force=True)
- insp = self._get_container(container_id)
- if insp.raw:
- insp.raw['Output'] = output
- else:
- insp.raw = dict(Output=output)
- return insp
- return self._get_container(container_id)
- def container_remove(self, container_id, link=False, force=False):
- volume_state = (not self.parameters.keep_volumes)
- self.log("remove container container:%s v:%s link:%s force%s" % (container_id, volume_state, link, force))
- self.results['actions'].append(dict(removed=container_id, volume_state=volume_state, link=link, force=force))
- self.results['changed'] = True
- response = None
- if not self.check_mode:
- try:
- response = self.client.remove_container(container_id, v=volume_state, link=link, force=force)
- except Exception as exc:
- self.fail("Error removing container %s: %s" % (container_id, str(exc)))
- return response
- def container_update(self, container_id, update_parameters):
- if update_parameters:
- self.log("update container %s" % (container_id))
- self.log(update_parameters, pretty_print=True)
- self.results['actions'].append(dict(updated=container_id, update_parameters=update_parameters))
- self.results['changed'] = True
- if not self.check_mode and callable(getattr(self.client, 'update_container')):
- try:
- self.client.update_container(container_id, **update_parameters)
- except Exception as exc:
- self.fail("Error updating container %s: %s" % (container_id, str(exc)))
- return self._get_container(container_id)
- def container_kill(self, container_id):
- self.results['actions'].append(dict(killed=container_id, signal=self.parameters.kill_signal))
- self.results['changed'] = True
- response = None
- if not self.check_mode:
- try:
- if self.parameters.kill_signal:
- response = self.client.kill(container_id, signal=self.parameters.kill_signal)
- else:
- response = self.client.kill(container_id)
- except Exception as exc:
- self.fail("Error killing container %s: %s" % (container_id, exc))
- return response
- def container_stop(self, container_id):
- if self.parameters.force_kill:
- self.container_kill(container_id)
- return
- self.results['actions'].append(dict(stopped=container_id, timeout=self.parameters.stop_timeout))
- self.results['changed'] = True
- response = None
- if not self.check_mode:
- try:
- if self.parameters.stop_timeout:
- response = self.client.stop(container_id, timeout=self.parameters.stop_timeout)
- else:
- response = self.client.stop(container_id)
- except Exception as exc:
- self.fail("Error stopping container %s: %s" % (container_id, str(exc)))
- return response
- def main():
- argument_spec = dict(
- blkio_weight=dict(type='int'),
- capabilities=dict(type='list'),
- cleanup=dict(type='bool', default=False),
- command=dict(type='str'),
- cpu_period=dict(type='int'),
- cpu_quota=dict(type='int'),
- cpuset_cpus=dict(type='str'),
- cpuset_mems=dict(type='str'),
- cpu_shares=dict(type='int'),
- detach=dict(type='bool', default=True),
- devices=dict(type='list'),
- dns_servers=dict(type='list'),
- dns_opts=dict(type='list'),
- dns_search_domains=dict(type='list'),
- env=dict(type='dict'),
- env_file=dict(type='path'),
- entrypoint=dict(type='str'),
- etc_hosts=dict(type='dict'),
- exposed_ports=dict(type='list', aliases=['exposed', 'expose']),
- force_kill=dict(type='bool', default=False, aliases=['forcekill']),
- groups=dict(type='list'),
- hostname=dict(type='str'),
- ignore_image=dict(type='bool', default=False),
- image=dict(type='str'),
- interactive=dict(type='bool', default=False),
- ipc_mode=dict(type='str'),
- keep_volumes=dict(type='bool', default=True),
- kernel_memory=dict(type='str'),
- kill_signal=dict(type='str'),
- labels=dict(type='dict'),
- links=dict(type='list'),
- log_driver=dict(type='str',
- choices=['none', 'json-file', 'syslog', 'journald', 'gelf', 'fluentd', 'awslogs', 'splunk'],
- default=None),
- log_options=dict(type='dict', aliases=['log_opt']),
- mac_address=dict(type='str'),
- memory=dict(type='str', default='0'),
- memory_reservation=dict(type='str'),
- memory_swap=dict(type='str'),
- memory_swappiness=dict(type='int'),
- name=dict(type='str', required=True),
- network_mode=dict(type='str'),
- networks=dict(type='list'),
- oom_killer=dict(type='bool'),
- oom_score_adj=dict(type='int'),
- paused=dict(type='bool', default=False),
- pid_mode=dict(type='str'),
- privileged=dict(type='bool', default=False),
- published_ports=dict(type='list', aliases=['ports']),
- pull=dict(type='bool', default=False),
- purge_networks=dict(type='bool', default=False),
- read_only=dict(type='bool', default=False),
- recreate=dict(type='bool', default=False),
- restart=dict(type='bool', default=False),
- restart_policy=dict(type='str', choices=['no', 'on-failure', 'always', 'unless-stopped']),
- restart_retries=dict(type='int', default=None),
- shm_size=dict(type='str'),
- security_opts=dict(type='list'),
- state=dict(type='str', choices=['absent', 'present', 'started', 'stopped'], default='started'),
- stop_signal=dict(type='str'),
- stop_timeout=dict(type='int'),
- trust_image_content=dict(type='bool', default=False),
- tty=dict(type='bool', default=False),
- ulimits=dict(type='list'),
- user=dict(type='str'),
- uts=dict(type='str'),
- volumes=dict(type='list'),
- volumes_from=dict(type='list'),
- volume_driver=dict(type='str'),
- )
- required_if = [
- ('state', 'present', ['image'])
- ]
- client = AnsibleDockerClient(
- argument_spec=argument_spec,
- required_if=required_if,
- supports_check_mode=True
- )
- cm = ContainerManager(client)
- client.module.exit_json(**cm.results)
- # import module snippets
- from ansible.module_utils.basic import *
- if __name__ == '__main__':
- main()
|