Repository: ansible/ansible

Issue:
Inventory parsing performance with high number of groups
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
core

##### ANSIBLE VERSION
```
ansible 2.4.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/kwoodson/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.13 (default, Sep  5 2017, 08:53:59) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
```

##### CONFIGURATION
```
ACCELERATE_CONNECT_TIMEOUT(/etc/ansible/ansible.cfg) = 5.0
ACCELERATE_DAEMON_TIMEOUT(/etc/ansible/ansible.cfg) = 30
ACCELERATE_PORT(/etc/ansible/ansible.cfg) = 5099
ACCELERATE_TIMEOUT(/etc/ansible/ansible.cfg) = 30
ANSIBLE_SSH_ARGS(/etc/ansible/ansible.cfg) = -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes
CACHE_PLUGIN(/etc/ansible/ansible.cfg) = memory
DEFAULT_ACTION_PLUGIN_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/action_plugins']
DEFAULT_CALLBACK_PLUGIN_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/callback_plugins']
DEFAULT_CALLBACK_WHITELIST(/etc/ansible/ansible.cfg) = ['profile_tasks']
DEFAULT_CONNECTION_PLUGIN_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/connection_plugins']
DEFAULT_FILTER_PLUGIN_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/filter_plugins']
DEFAULT_FORKS(/etc/ansible/ansible.cfg) = 5
DEFAULT_GATHERING(/etc/ansible/ansible.cfg) = implicit
DEFAULT_HOST_LIST(/etc/ansible/ansible.cfg) = [u'/etc/ansible/hosts']
DEFAULT_LOOKUP_PLUGIN_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/lookup_plugins']
DEFAULT_MANAGED_STR(/etc/ansible/ansible.cfg) = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
DEFAULT_MODULE_LANG(/etc/ansible/ansible.cfg) = C
DEFAULT_POLL_INTERVAL(/etc/ansible/ansible.cfg) = 15
DEFAULT_REMOTE_TMP(/etc/ansible/ansible.cfg) = $HOME/.ansible/tmp
DEFAULT_SUDO_EXE(/etc/ansible/ansible.cfg) = sudo
DEFAULT_SUDO_FLAGS(/etc/ansible/ansible.cfg) = -HE
DEFAULT_SUDO_USER(/etc/ansible/ansible.cfg) = root
DEFAULT_TIMEOUT(/etc/ansible/ansible.cfg) = 10
DEFAULT_TRANSPORT(/etc/ansible/ansible.cfg) = smart
DEFAULT_VARS_PLUGIN_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/vars_plugins']
```
##### OS / ENVIRONMENT
fedora 26
##### SUMMARY
<!--- Explain the problem briefly -->
When calling ansible with a custom dynamic inventory the startup time is taking ~40 seconds.
```
time ansible -u root opstest-master-ip-172-31-62-207.ec2.internal -m shell -i multi_inventory.py -a date -vvvv   42.26s user 0.30s system 94% cpu 44.831 total
```

The python inventory script returns in subseconds.
```
time ./multi_inventory.py > /dev/null
./multi_inventory.py > /dev/null  0.90s user 0.03s system 98% cpu 0.947 total
```
##### STEPS TO REPRODUCE
Our environment has a lot of prebuilt groups.  These groups are the ones returned from ec2.py and some additional ones that we have defined.  This is the inventory loaded into a python interpreter showing the number of keys or approximately the number of groups:
```
>>> len(inventory.keys())
6436
```

##### EXPECTED RESULTS
I would expect the Ansible start up time would be substantially less than 40 seconds.  JQ, python, and other tooling can parse the inventory in less than 1s.

##### ACTUAL RESULTS
```
time ansible -u root opstest-master-ip-172-31-62-207.ec2.internal -m shell -i multi_inventory.py -a date -vvvv
ansible 2.4.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/kwoodson/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.13 (default, Sep  5 2017, 08:53:59) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
Using /etc/ansible/ansible.cfg as config file
[DEPRECATION WARNING]: ACCELERATE_TIMEOUT option, Removing accelerate as a connection method, settings not needed either. . This feature 
will be removed in version 2.5. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_FLAGS option, In favor of become which is a generic framework . This feature will be removed in version
 2.8. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: ACCELERATE_DAEMON_TIMEOUT option, Removing accelerate as a connection method, settings not needed either. . This 
feature will be removed in version 2.5. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: ACCELERATE_CONNECT_TIMEOUT option, Removing accelerate as a connection method, settings not needed either. . This 
feature will be removed in version 2.5. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_EXE option, In favor of become which is a generic framework . This feature will be removed in version 
2.8. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_USER option, In favor of become which is a generic framework . This feature will be removed in version 
2.8. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: ACCELERATE_PORT option, Removing accelerate as a connection method, settings not needed either. . This feature will 
be removed in version 2.5. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
setting up inventory plugins
Parsed /home/kwoodson/git/openshift-tools/ansible/inventory/multi_inventory.py inventory source with script plugin
Loading callback plugin minimal of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc
META: ran handlers
Using module file /usr/lib/python2.7/site-packages/ansible/modules/commands/command.py
<52.55.251.196> ESTABLISH SSH CONNECTION FOR USER: root
<52.55.251.196> SSH: EXEC ssh -vvv -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/kwoodson/.ansible/cp/6b98e26715 52.55.251.196 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209 `" && echo ansible-tmp-1508168965.18-39768536728209="` echo $HOME/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209 `" ) && sleep 0'"'"''
<52.55.251.196> (0, 'ansible-tmp-1508168965.18-39768536728209=/root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209\n', 'OpenSSH_7.5p1, OpenSSL 1.1.0f-fips  25 May 2017\r\ndebug1: Reading configuration data /home/kwoodson/.ssh/config\r\ndebug1: /home/kwoodson/.ssh/config line 46: Applying options for *\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug3: /etc/ssh/ssh_config line 56: Including file /etc/ssh/ssh_config.d/05-redhat.conf depth 0\r\ndebug1: Reading configuration data /etc/ssh/ssh_config.d/05-redhat.conf\r\ndebug3: /etc/ssh/ssh_config.d/05-redhat.conf line 2: Including file /etc/crypto-policies/back-ends/openssh.config depth 1\r\ndebug1: Reading configuration data /etc/crypto-policies/back-ends/openssh.config\r\ndebug3: gss kex names ok: [gss-gex-sha1-,gss-group14-sha1-]\r\ndebug3: kex names ok: [curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1]\r\ndebug1: /etc/ssh/ssh_config.d/05-redhat.conf line 8: Applying options for *\r\ndebug1: auto-mux: Trying existing master\r\ndebug1: Control socket "/home/kwoodson/.ansible/cp/6b98e26715" does not exist\r\ndebug2: resolving "52.55.251.196" port 22\r\ndebug2: ssh_connect_direct: needpriv 0\r\ndebug1: Connecting to 52.55.251.196 [52.55.251.196] port 22.\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug1: fd 3 clearing O_NONBLOCK\r\ndebug1: Connection established.\r\ndebug3: timeout: 9979 ms remain after connect\r\ndebug1: identity file /home/kwoodson/.ssh/id_rsa type 1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_rsa-cert type -1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_dsa type -1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_dsa-cert type -1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_ecdsa type -1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_ecdsa-cert type -1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_ed25519 type -1\r\ndebug1: key_load_public: No such file or directory\r\ndebug1: identity file /home/kwoodson/.ssh/id_ed25519-cert type -1\r\ndebug1: Enabling compatibility mode for protocol 2.0\r\ndebug1: Local version string SSH-2.0-OpenSSH_7.5\r\ndebug1: Remote protocol version 2.0, remote software version OpenSSH_6.6.1\r\ndebug1: match: OpenSSH_6.6.1 pat OpenSSH_6.6.1* compat 0x04000000\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug1: Authenticating to 52.55.251.196:22 as \'root\'\r\ndebug3: hostkeys_foreach: reading file "/home/kwoodson/.ssh/known_hosts"\r\ndebug3: record_hostkey: found key type ECDSA in file /home/kwoodson/.ssh/known_hosts:2362\r\ndebug3: load_hostkeys: loaded 1 keys from 52.55.251.196\r\ndebug3: order_hostkeyalgs: prefer hostkeyalgs: ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521\r\ndebug3: send packet: type 20\r\ndebug1: SSH2_MSG_KEXINIT sent\r\ndebug3: receive packet: type 20\r\ndebug1: SSH2_MSG_KEXINIT received\r\ndebug2: local client KEXINIT proposal\r\ndebug2: KEX algorithms: curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,ext-info-c\r\ndebug2: host key algorithms: ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa\r\ndebug2: ciphers ctos: aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctr,aes256-cbc,aes128-gcm@openssh.com,aes128-ctr,aes128-cbc,3des-cbc\r\ndebug2: ciphers stoc: aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctr,aes256-cbc,aes128-gcm@openssh.com,aes128-ctr,aes128-cbc,3des-cbc\r\ndebug2: MACs ctos: umac-128-etm@openssh.com,umac-128@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha1,hmac-sha2-256-etm@openssh.com,hmac-sha2-256,hmac-sha2-512-etm@openssh.com,hmac-sha2-512\r\ndebug2: MACs stoc: umac-128-etm@openssh.com,umac-128@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha1,hmac-sha2-256-etm@openssh.com,hmac-sha2-256,hmac-sha2-512-etm@openssh.com,hmac-sha2-512\r\ndebug2: compression ctos: none,zlib@openssh.com,zlib\r\ndebug2: compression stoc: none,zlib@openssh.com,zlib\r\ndebug2: languages ctos: \r\ndebug2: languages stoc: \r\ndebug2: first_kex_follows 0 \r\ndebug2: reserved 0 \r\ndebug2: peer server KEXINIT proposal\r\ndebug2: KEX algorithms: curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1\r\ndebug2: host key algorithms: ssh-rsa,ecdsa-sha2-nistp256,ssh-ed25519\r\ndebug2: ciphers ctos: aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se\r\ndebug2: ciphers stoc: aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se\r\ndebug2: MACs ctos: hmac-md5-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,hmac-sha1-96-etm@openssh.com,hmac-md5-96-etm@openssh.com,hmac-md5,hmac-sha1,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96\r\ndebug2: MACs stoc: hmac-md5-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,hmac-sha1-96-etm@openssh.com,hmac-md5-96-etm@openssh.com,hmac-md5,hmac-sha1,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96\r\ndebug2: compression ctos: none,zlib@openssh.com\r\ndebug2: compression stoc: none,zlib@openssh.com\r\ndebug2: languages ctos: \r\ndebug2: languages stoc: \r\ndebug2: first_kex_follows 0 \r\ndebug2: reserved 0 \r\ndebug1: kex: algorithm: curve25519-sha256@libssh.org\r\ndebug1: kex: host key algorithm: ecdsa-sha2-nistp256\r\ndebug1: kex: server->client cipher: aes256-gcm@openssh.com MAC: <implicit> compression: none\r\ndebug1: kex: client->server cipher: aes256-gcm@openssh.com MAC: <implicit> compression: none\r\ndebug1: kex: curve25519-sha256@libssh.org need=32 dh_need=32\r\ndebug1: kex: curve25519-sha256@libssh.org need=32 dh_need=32\r\ndebug3: send packet: type 30\r\ndebug1: expecting SSH2_MSG_KEX_ECDH_REPLY\r\ndebug3: receive packet: type 31\r\ndebug1: Server host key: ecdsa-sha2-nistp256 SHA256:Ra+8OsGJRu62XImKyoZzuQ8R/+nNaXDy6KADGA4z+40\r\ndebug3: hostkeys_foreach: reading file "/home/kwoodson/.ssh/known_hosts"\r\ndebug3: record_hostkey: found key type ECDSA in file /home/kwoodson/.ssh/known_hosts:2362\r\ndebug3: load_hostkeys: loaded 1 keys from 52.55.251.196\r\ndebug1: Host \'52.55.251.196\' is known and matches the ECDSA host key.\r\ndebug1: Found key in /home/kwoodson/.ssh/known_hosts:2362\r\ndebug3: send packet: type 21\r\ndebug2: set_newkeys: mode 1\r\ndebug1: rekey after 4294967296 blocks\r\ndebug1: SSH2_MSG_NEWKEYS sent\r\ndebug1: expecting SSH2_MSG_NEWKEYS\r\ndebug3: receive packet: type 21\r\ndebug1: SSH2_MSG_NEWKEYS received\r\ndebug2: set_newkeys: mode 0\r\ndebug1: rekey after 4294967296 blocks\r\ndebug2: key: /home/kwoodson/.ssh/id_rsa (0x555d6f507530), agent\r\ndebug2: key: /home/kwoodson/ec2/mmcgrath_admin.pem (0x555d6f507d90), agent\r\ndebug2: key: /home/kwoodson/.ssh/openshift_id_rsa (0x555d6f5083a0), agent\r\ndebug2: key: /home/kwoodson/.ssh/libra.pem (0x555d6f50c510), agent\r\ndebug2: key: kwoodson@flash.rdu.redhat.com (0x555d6f5082f0), agent\r\ndebug2: key: /home/kwoodson/.ssh/id_dsa ((nil))\r\ndebug2: key: /home/kwoodson/.ssh/id_ecdsa ((nil))\r\ndebug2: key: /home/kwoodson/.ssh/id_ed25519 ((nil))\r\ndebug3: send packet: type 5\r\ndebug3: receive packet: type 6\r\ndebug2: service_accept: ssh-userauth\r\ndebug1: SSH2_MSG_SERVICE_ACCEPT received\r\ndebug3: send packet: type 50\r\ndebug3: receive packet: type 51\r\ndebug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic\r\ndebug3: start over, passed a different list publickey,gssapi-keyex,gssapi-with-mic\r\ndebug3: preferred gssapi-with-mic,gssapi-keyex,hostbased,publickey\r\ndebug3: authmethod_lookup gssapi-with-mic\r\ndebug3: remaining preferred: gssapi-keyex,hostbased,publickey\r\ndebug3: authmethod_is_enabled gssapi-with-mic\r\ndebug1: Next authentication method: gssapi-with-mic\r\ndebug1: Unspecified GSS failure.  Minor code may provide more information\nNo Kerberos credentials available (default cache: FILE:/tmp/krb5cc_1000)\n\r\ndebug1: Unspecified GSS failure.  Minor code may provide more information\nNo Kerberos credentials available (default cache: FILE:/tmp/krb5cc_1000)\n\r\ndebug2: we did not send a packet, disable method\r\ndebug3: authmethod_lookup gssapi-keyex\r\ndebug3: remaining preferred: hostbased,publickey\r\ndebug3: authmethod_is_enabled gssapi-keyex\r\ndebug1: Next authentication method: gssapi-keyex\r\ndebug1: No valid Key exchange context\r\ndebug2: we did not send a packet, disable method\r\ndebug3: authmethod_lookup publickey\r\ndebug3: remaining preferred: ,publickey\r\ndebug3: authmethod_is_enabled publickey\r\ndebug1: Next authentication method: publickey\r\ndebug1: Offering RSA public key: /home/kwoodson/.ssh/id_rsa\r\ndebug3: send_pubkey_test\r\ndebug3: send packet: type 50\r\ndebug2: we sent a publickey packet, wait for reply\r\ndebug3: receive packet: type 51\r\ndebug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic\r\ndebug1: Offering RSA public key: /home/kwoodson/ec2/mmcgrath_admin.pem\r\ndebug3: send_pubkey_test\r\ndebug3: send packet: type 50\r\ndebug2: we sent a publickey packet, wait for reply\r\ndebug3: receive packet: type 51\r\ndebug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic\r\ndebug1: Offering RSA public key: /home/kwoodson/.ssh/openshift_id_rsa\r\ndebug3: send_pubkey_test\r\ndebug3: send packet: type 50\r\ndebug2: we sent a publickey packet, wait for reply\r\ndebug3: receive packet: type 60\r\ndebug1: Server accepts key: pkalg ssh-rsa blen 535\r\ndebug2: input_userauth_pk_ok: fp SHA256:016exF6yx82IDUQAsrOIKOn3cNzAKNqI2LdDN70ehb8\r\ndebug3: sign_and_send_pubkey: RSA SHA256:016exF6yx82IDUQAsrOIKOn3cNzAKNqI2LdDN70ehb8\r\ndebug3: send packet: type 50\r\ndebug3: receive packet: type 52\r\ndebug1: Authentication succeeded (publickey).\r\nAuthenticated to 52.55.251.196 ([52.55.251.196]:22).\r\ndebug1: setting up multiplex master socket\r\ndebug3: muxserver_listen: temporary control path /home/kwoodson/.ansible/cp/6b98e26715.865HgPYJc8ZpHHSB\r\ndebug2: fd 5 setting O_NONBLOCK\r\ndebug3: fd 5 is O_NONBLOCK\r\ndebug3: fd 5 is O_NONBLOCK\r\ndebug1: channel 0: new [/home/kwoodson/.ansible/cp/6b98e26715]\r\ndebug3: muxserver_listen: mux listener channel 0 fd 5\r\ndebug2: fd 3 setting TCP_NODELAY\r\ndebug3: ssh_packet_set_tos: set IP_TOS 0x08\r\ndebug1: control_persist_detach: backgrounding master process\r\ndebug2: control_persist_detach: background process is 5531\r\ndebug2: fd 5 setting O_NONBLOCK\r\ndebug1: forking to background\r\ndebug1: Entering interactive session.\r\ndebug1: pledge: id\r\ndebug2: set_control_persist_exit_time: schedule exit in 60 seconds\r\ndebug1: multiplexing control connection\r\ndebug2: fd 6 setting O_NONBLOCK\r\ndebug3: fd 6 is O_NONBLOCK\r\ndebug1: channel 1: new [mux-control]\r\ndebug3: channel_post_mux_listener: new mux channel 1 fd 6\r\ndebug3: mux_master_read_cb: channel 1: hello sent\r\ndebug2: set_control_persist_exit_time: cancel scheduled exit\r\ndebug3: mux_master_read_cb: channel 1 packet type 0x00000001 len 4\r\ndebug2: process_mux_master_hello: channel 1 slave version 4\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_master_read_cb: channel 1 packet type 0x10000004 len 4\r\ndebug2: process_mux_alive_check: channel 1: alive check\r\ndebug3: mux_client_request_alive: done pid = 5534\r\ndebug3: mux_client_request_session: session request sent\r\ndebug3: mux_master_read_cb: channel 1 packet type 0x10000002 len 332\r\ndebug2: process_mux_new_session: channel 1: request tty 0, X 0, agent 1, subsys 0, term "xterm-256color", cmd "/bin/sh -c \'( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209 `" && echo ansible-tmp-1508168965.18-39768536728209="` echo $HOME/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209 `" ) && sleep 0\'", env 2\r\ndebug3: process_mux_new_session: got fds stdin 7, stdout 8, stderr 9\r\ndebug2: fd 8 setting O_NONBLOCK\r\ndebug2: fd 9 setting O_NONBLOCK\r\ndebug1: channel 2: new [client-session]\r\ndebug2: process_mux_new_session: channel_new: 2 linked to control channel 1\r\ndebug2: channel 2: send open\r\ndebug3: send packet: type 90\r\ndebug3: receive packet: type 91\r\ndebug2: callback start\r\ndebug1: Requesting authentication agent forwarding.\r\ndebug2: channel 2: request auth-agent-req@openssh.com confirm 0\r\ndebug3: send packet: type 98\r\ndebug2: client_session2_setup: id 2\r\ndebug1: Sending environment.\r\ndebug1: Sending env LANG = en_US.UTF-8\r\ndebug2: channel 2: request env confirm 0\r\ndebug3: send packet: type 98\r\ndebug1: Sending env XMODIFIERS = @im=ibus\r\ndebug2: channel 2: request env confirm 0\r\ndebug3: send packet: type 98\r\ndebug1: Sending command: /bin/sh -c \'( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209 `" && echo ansible-tmp-1508168965.18-39768536728209="` echo $HOME/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209 `" ) && sleep 0\'\r\ndebug2: channel 2: request exec confirm 1\r\ndebug3: send packet: type 98\r\ndebug3: mux_session_confirm: sending success reply\r\ndebug2: callback done\r\ndebug2: channel 2: open confirm rwindow 0 rmax 32768\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug2: channel 2: rcvd adjust 2097152\r\ndebug3: receive packet: type 99\r\ndebug2: channel_input_status_confirm: type 99 id 2\r\ndebug2: exec request accepted on channel 2\r\ndebug3: receive packet: type 98\r\ndebug1: client_input_channel_req: channel 2 rtype exit-status reply 0\r\ndebug3: mux_exit_message: channel 2: exit message, exitval 0\r\ndebug3: receive packet: type 98\r\ndebug1: client_input_channel_req: channel 2 rtype eow@openssh.com reply 0\r\ndebug2: channel 2: rcvd eow\r\ndebug2: channel 2: close_read\r\ndebug2: channel 2: input open -> closed\r\ndebug3: receive packet: type 96\r\ndebug2: channel 2: rcvd eof\r\ndebug2: channel 2: output open -> drain\r\ndebug2: channel 2: obuf empty\r\ndebug2: channel 2: close_write\r\ndebug2: channel 2: output drain -> closed\r\ndebug3: receive packet: type 97\r\ndebug2: channel 2: rcvd close\r\ndebug3: channel 2: will not send data after close\r\ndebug2: channel 2: send close\r\ndebug3: send packet: type 97\r\ndebug2: channel 2: is dead\r\ndebug2: channel 2: gc: notify user\r\ndebug3: mux_master_session_cleanup_cb: entering for channel 2\r\ndebug2: channel 1: rcvd close\r\ndebug2: channel 1: output open -> drain\r\ndebug2: channel 1: close_read\r\ndebug2: channel 1: input open -> closed\r\ndebug2: channel 2: gc: user detached\r\ndebug2: channel 2: is dead\r\ndebug2: channel 2: garbage collecting\r\ndebug1: channel 2: free: client-session, nchannels 3\r\ndebug3: channel 2: status: The following connections are open:\r\n  #1 mux-control (t16 r-1 i3/0 o1/16 fd 6/6 cc -1)\r\n  #2 client-session (t4 r0 i3/0 o3/0 fd -1/-1 cc -1)\r\n\r\ndebug2: channel 1: obuf empty\r\ndebug2: channel 1: close_write\r\ndebug2: channel 1: output drain -> closed\r\ndebug2: channel 1: is dead (local)\r\ndebug2: channel 1: gc: notify user\r\ndebug3: mux_master_control_cleanup_cb: entering for channel 1\r\ndebug2: channel 1: gc: user detached\r\ndebug2: channel 1: is dead (local)\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: channel 1: garbage collecting\r\ndebug1: channel 1: free: mux-control, nchannels 2\r\ndebug3: channel 1: status: The following connections are open:\r\n  #1 mux-control (t16 r-1 i3/0 o3/0 fd 6/6 cc -1)\r\n\r\ndebug2: Received exit status from master 0\r\ndebug2: set_control_persist_exit_time: schedule exit in 60 seconds\r\n')
<52.55.251.196> PUT /tmp/tmpEF5U_O TO /root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/command.py
<52.55.251.196> SSH: EXEC sftp -b - -vvv -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/kwoodson/.ansible/cp/6b98e26715 '[52.55.251.196]'
<52.55.251.196> (0, 'sftp> put /tmp/tmpEF5U_O /root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/command.py\n', 'OpenSSH_7.5p1, OpenSSL 1.1.0f-fips  25 May 2017\r\ndebug1: Reading configuration data /home/kwoodson/.ssh/config\r\ndebug1: /home/kwoodson/.ssh/config line 46: Applying options for *\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug3: /etc/ssh/ssh_config line 56: Including file /etc/ssh/ssh_config.d/05-redhat.conf depth 0\r\ndebug1: Reading configuration data /etc/ssh/ssh_config.d/05-redhat.conf\r\ndebug3: /etc/ssh/ssh_config.d/05-redhat.conf line 2: Including file /etc/crypto-policies/back-ends/openssh.config depth 1\r\ndebug1: Reading configuration data /etc/crypto-policies/back-ends/openssh.config\r\ndebug3: gss kex names ok: [gss-gex-sha1-,gss-group14-sha1-]\r\ndebug3: kex names ok: [curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1]\r\ndebug1: /etc/ssh/ssh_config.d/05-redhat.conf line 8: Applying options for *\r\ndebug1: auto-mux: Trying existing master\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 5534\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug2: Remote version: 3\r\ndebug2: Server supports extension "posix-rename@openssh.com" revision 1\r\ndebug2: Server supports extension "statvfs@openssh.com" revision 2\r\ndebug2: Server supports extension "fstatvfs@openssh.com" revision 2\r\ndebug2: Server supports extension "hardlink@openssh.com" revision 1\r\ndebug2: Server supports extension "fsync@openssh.com" revision 1\r\ndebug3: Sent message fd 5 T:16 I:1\r\ndebug3: SSH_FXP_REALPATH . -> /root size 0\r\ndebug3: Looking up /tmp/tmpEF5U_O\r\ndebug3: Sent message fd 5 T:17 I:2\r\ndebug3: Received stat reply T:101 I:2\r\ndebug1: Couldn\'t stat remote file: No such file or directory\r\ndebug3: Sent message SSH2_FXP_OPEN I:3 P:/root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/command.py\r\ndebug3: Sent message SSH2_FXP_WRITE I:4 O:0 S:32768\r\ndebug3: SSH2_FXP_STATUS 0\r\ndebug3: In write loop, ack for 4 32768 bytes at 0\r\ndebug3: Sent message SSH2_FXP_WRITE I:5 O:32768 S:32768\r\ndebug3: Sent message SSH2_FXP_WRITE I:6 O:65536 S:977\r\ndebug3: SSH2_FXP_STATUS 0\r\ndebug3: In write loop, ack for 5 32768 bytes at 32768\r\ndebug3: SSH2_FXP_STATUS 0\r\ndebug3: In write loop, ack for 6 977 bytes at 65536\r\ndebug3: Sent message SSH2_FXP_CLOSE I:4\r\ndebug3: SSH2_FXP_STATUS 0\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\n')
<52.55.251.196> ESTABLISH SSH CONNECTION FOR USER: root
<52.55.251.196> SSH: EXEC ssh -vvv -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/kwoodson/.ansible/cp/6b98e26715 52.55.251.196 '/bin/sh -c '"'"'chmod u+x /root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/ /root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/command.py && sleep 0'"'"''
<52.55.251.196> (0, '', 'OpenSSH_7.5p1, OpenSSL 1.1.0f-fips  25 May 2017\r\ndebug1: Reading configuration data /home/kwoodson/.ssh/config\r\ndebug1: /home/kwoodson/.ssh/config line 46: Applying options for *\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug3: /etc/ssh/ssh_config line 56: Including file /etc/ssh/ssh_config.d/05-redhat.conf depth 0\r\ndebug1: Reading configuration data /etc/ssh/ssh_config.d/05-redhat.conf\r\ndebug3: /etc/ssh/ssh_config.d/05-redhat.conf line 2: Including file /etc/crypto-policies/back-ends/openssh.config depth 1\r\ndebug1: Reading configuration data /etc/crypto-policies/back-ends/openssh.config\r\ndebug3: gss kex names ok: [gss-gex-sha1-,gss-group14-sha1-]\r\ndebug3: kex names ok: [curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1]\r\ndebug1: /etc/ssh/ssh_config.d/05-redhat.conf line 8: Applying options for *\r\ndebug1: auto-mux: Trying existing master\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 5534\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\n')
<52.55.251.196> ESTABLISH SSH CONNECTION FOR USER: root
<52.55.251.196> SSH: EXEC ssh -vvv -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/kwoodson/.ansible/cp/6b98e26715 -tt 52.55.251.196 '/bin/sh -c '"'"'/usr/bin/python /root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/command.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1508168965.18-39768536728209/" > /dev/null 2>&1 && sleep 0'"'"''
<52.55.251.196> (0, '\r\n{"changed": true, "end": "2017-10-16 15:49:27.675624", "stdout": "Mon Oct 16 15:49:27 UTC 2017", "cmd": "date", "rc": 0, "start": "2017-10-16 15:49:27.671696", "stderr": "", "delta": "0:00:00.003928", "invocation": {"module_args": {"warn": true, "executable": null, "_uses_shell": true, "_raw_params": "date", "removes": null, "creates": null, "chdir": null, "stdin": null}}}\r\n', 'OpenSSH_7.5p1, OpenSSL 1.1.0f-fips  25 May 2017\r\ndebug1: Reading configuration data /home/kwoodson/.ssh/config\r\ndebug1: /home/kwoodson/.ssh/config line 46: Applying options for *\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug3: /etc/ssh/ssh_config line 56: Including file /etc/ssh/ssh_config.d/05-redhat.conf depth 0\r\ndebug1: Reading configuration data /etc/ssh/ssh_config.d/05-redhat.conf\r\ndebug3: /etc/ssh/ssh_config.d/05-redhat.conf line 2: Including file /etc/crypto-policies/back-ends/openssh.config depth 1\r\ndebug1: Reading configuration data /etc/crypto-policies/back-ends/openssh.config\r\ndebug3: gss kex names ok: [gss-gex-sha1-,gss-group14-sha1-]\r\ndebug3: kex names ok: [curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1]\r\ndebug1: /etc/ssh/ssh_config.d/05-redhat.conf line 8: Applying options for *\r\ndebug1: auto-mux: Trying existing master\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 5534\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\nShared connection to 52.55.251.196 closed.\r\n')
opstest-master-ip-172-31-62-207.ec2.internal | SUCCESS | rc=0 >>
Mon Oct 16 15:49:27 UTC 2017

META: ran handlers
META: ran handlers
```

Manually reducing the group count reduced run time to around 5 seconds from 42.



Discussion:
Ansible 2.3
```
ansible --version
ansible 2.3.3.0 (stable-2.3 60d9513efb) last updated 2017/10/05 15:19:27 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Sep  5 2017, 08:53:59) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
```

```
 time ansible -u root opstest-master-ip-172-31-62-207.ec2.internal -m shell -i ./multi_inventory.py -a date -vvvv

ansible -u root opstest-master-ip-172-31-62-207.ec2.internal -m shell -i  -a   2.67s user 0.27s system 85% cpu 6.960 total

```
Could you test this with devel/ or the latest 2.4.1 release candidate builds? (either stable-2.4/ branch from git or 
2.4.1.0-0.3.rc1 from http://releases.ansible.com/ansible/ansible-2.4.1.0-0.3.rc1.tar.gz) 

There have been an assortment of inventory related fixes and speed ups that may fix this.
@alikins, it looks like the 2.4.0.0 is the one taking ~45 seconds. 

stabe-2.4:
```
ansible 2.4.1.0 (stable-2.4 5d2f07aebb) last updated 2017/10/16 13:48:25 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/kwoodson/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/kwoodson/git/ansible/lib/ansible
  executable location = /home/kwoodson/git/ansible/bin/ansible
  python version = 2.7.13 (default, Sep  5 2017, 08:53:59) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
 
ansible -u root opstest-master-ip-172-31-62-207.ec2.internal -m shell -i  -a   5.11s user 0.30s system 86% cpu 6.273 total
```
devel branch:
```
ansible 2.5.0 (devel 1e85facf36) last updated 2017/10/16 13:46:35 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/kwoodson/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/kwoodson/git/ansible.pr/lib/ansible
  executable location = /home/kwoodson/git/ansible.pr/bin/ansible
  python version = 2.7.13 (default, Sep  5 2017, 08:53:59) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]

ansible -u root opstest-master-ip-172-31-62-207.ec2.internal -m shell -i  -a   5.35s user 0.40s system 48% cpu 11.773 total
```

@alikins, as long as its documented that 2.4.0.0 has known slow downs I'm fine closing this issue.

Resolving patch:
diff --git a/lib/ansible/inventory/group.py b/lib/ansible/inventory/group.py
--- a/lib/ansible/inventory/group.py
+++ b/lib/ansible/inventory/group.py
@@ -19,6 +19,8 @@
 
 from ansible.errors import AnsibleError
 
+from itertools import chain
+
 
 class Group:
     ''' a group of ansible hosts '''
@@ -80,6 +82,38 @@ def deserialize(self, data):
             g.deserialize(parent_data)
             self.parent_groups.append(g)
 
+    def _walk_relationship(self, rel):
+        '''
+        Given `rel` that is an iterable property of Group,
+        consitituting a directed acyclic graph among all groups,
+        Returns a set of all groups in full tree
+        A   B    C
+        |  / |  /
+        | /  | /
+        D -> E
+        |  /    vertical connections
+        | /     are directed upward
+        F
+        Called on F, returns set of (A, B, C, D, E)
+        '''
+        seen = set([])
+        unprocessed = set(getattr(self, rel))
+
+        while unprocessed:
+            seen.update(unprocessed)
+            unprocessed = set(chain.from_iterable(
+                getattr(g, rel) for g in unprocessed
+            ))
+            unprocessed.difference_update(seen)
+
+        return seen
+
+    def get_ancestors(self):
+        return self._walk_relationship('parent_groups')
+
+    def get_descendants(self):
+        return self._walk_relationship('child_groups')
+
     @property
     def host_names(self):
         if self._hosts is None:
@@ -96,6 +130,17 @@ def add_child_group(self, group):
 
         # don't add if it's already there
         if group not in self.child_groups:
+
+            # prepare list of group's new ancestors this edge creates
+            start_ancestors = group.get_ancestors()
+            new_ancestors = self.get_ancestors()
+            if group in new_ancestors:
+                raise AnsibleError(
+                    "Adding group '%s' as child to '%s' creates a recursive "
+                    "dependency loop." % (group.name, self.name))
+            new_ancestors.add(self)
+            new_ancestors.difference_update(start_ancestors)
+
             self.child_groups.append(group)
 
             # update the depth of the child
@@ -109,18 +154,28 @@ def add_child_group(self, group):
             if self.name not in [g.name for g in group.parent_groups]:
                 group.parent_groups.append(self)
                 for h in group.get_hosts():
-                    h.populate_ancestors()
+                    h.populate_ancestors(additions=new_ancestors)
 
             self.clear_hosts_cache()
 
     def _check_children_depth(self):
 
-        try:
-            for group in self.child_groups:
-                group.depth = max([self.depth + 1, group.depth])
-                group._check_children_depth()
-        except RuntimeError:
-            raise AnsibleError("The group named '%s' has a recursive dependency loop." % self.name)
+        depth = self.depth
+        start_depth = self.depth  # self.depth could change over loop
+        seen = set([])
+        unprocessed = set(self.child_groups)
+
+        while unprocessed:
+            seen.update(unprocessed)
+            depth += 1
+            to_process = unprocessed.copy()
+            unprocessed = set([])
+            for g in to_process:
+                if g.depth < depth:
+                    g.depth = depth
+                    unprocessed.update(g.child_groups)
+            if depth - start_depth > len(seen):
+                raise AnsibleError("The group named '%s' has a recursive dependency loop." % self.name)
 
     def add_host(self, host):
         if host.name not in self.host_names:
@@ -147,8 +202,8 @@ def set_variable(self, key, value):
     def clear_hosts_cache(self):
 
         self._hosts_cache = None
-        for g in self.parent_groups:
-            g.clear_hosts_cache()
+        for g in self.get_ancestors():
+            g._hosts_cache = None
 
     def get_hosts(self):
 
@@ -160,8 +215,8 @@ def _get_hosts(self):
 
         hosts = []
         seen = {}
-        for kid in self.child_groups:
-            kid_hosts = kid.get_hosts()
+        for kid in self.get_descendants():
+            kid_hosts = kid.hosts
             for kk in kid_hosts:
                 if kk not in seen:
                     seen[kk] = 1
@@ -179,18 +234,6 @@ def _get_hosts(self):
     def get_vars(self):
         return self.vars.copy()
 
-    def _get_ancestors(self):
-
-        results = {}
-        for g in self.parent_groups:
-            results[g.name] = g
-            results.update(g._get_ancestors())
-        return results
-
-    def get_ancestors(self):
-
-        return self._get_ancestors().values()
-
     def set_priority(self, priority):
         try:
             self.priority = int(priority)
diff --git a/lib/ansible/inventory/host.py b/lib/ansible/inventory/host.py
--- a/lib/ansible/inventory/host.py
+++ b/lib/ansible/inventory/host.py
@@ -101,17 +101,22 @@ def __init__(self, name=None, port=None, gen_uuid=True):
     def get_name(self):
         return self.name
 
-    def populate_ancestors(self):
+    def populate_ancestors(self, additions=None):
         # populate ancestors
-        for group in self.groups:
-            self.add_group(group)
+        if additions is None:
+            for group in self.groups:
+                self.add_group(group)
+        else:
+            for group in additions:
+                if group not in self.groups:
+                    self.groups.append(group)
 
     def add_group(self, group):
 
         # populate ancestors first
         for oldg in group.get_ancestors():
             if oldg not in self.groups:
-                self.add_group(oldg)
+                self.groups.append(oldg)
 
         # actually add group
         if group not in self.groups:


Test patch:
diff --git a/test/units/plugins/inventory/test_group.py b/test/units/plugins/inventory/test_group.py
new file mode 100644
--- /dev/null
+++ b/test/units/plugins/inventory/test_group.py
@@ -0,0 +1,125 @@
+# Copyright 2018 Alan Rominger <arominge@redhat.com>
+#
+# 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/>.
+
+from ansible.compat.tests import unittest
+
+from ansible.inventory.group import Group
+from ansible.inventory.host import Host
+from ansible.errors import AnsibleError
+
+
+class TestGroup(unittest.TestCase):
+
+    def test_depth_update(self):
+        A = Group('A')
+        B = Group('B')
+        Z = Group('Z')
+        A.add_child_group(B)
+        A.add_child_group(Z)
+        self.assertEqual(A.depth, 0)
+        self.assertEqual(Z.depth, 1)
+        self.assertEqual(B.depth, 1)
+
+    def test_depth_update_dual_branches(self):
+        alpha = Group('alpha')
+        A = Group('A')
+        alpha.add_child_group(A)
+        B = Group('B')
+        A.add_child_group(B)
+        Z = Group('Z')
+        alpha.add_child_group(Z)
+        beta = Group('beta')
+        B.add_child_group(beta)
+        Z.add_child_group(beta)
+
+        self.assertEqual(alpha.depth, 0)  # apex
+        self.assertEqual(beta.depth, 3)  # alpha -> A -> B -> beta
+
+        omega = Group('omega')
+        omega.add_child_group(alpha)
+
+        # verify that both paths are traversed to get the max depth value
+        self.assertEqual(B.depth, 3)  # omega -> alpha -> A -> B
+        self.assertEqual(beta.depth, 4)  # B -> beta
+
+    def test_depth_recursion(self):
+        A = Group('A')
+        B = Group('B')
+        A.add_child_group(B)
+        # hypothetical of adding B as child group to A
+        A.parent_groups.append(B)
+        B.child_groups.append(A)
+        # can't update depths of groups, because of loop
+        with self.assertRaises(AnsibleError):
+            B._check_children_depth()
+
+    def test_loop_detection(self):
+        A = Group('A')
+        B = Group('B')
+        C = Group('C')
+        A.add_child_group(B)
+        B.add_child_group(C)
+        with self.assertRaises(AnsibleError):
+            C.add_child_group(A)
+
+    def test_populates_descendant_hosts(self):
+        A = Group('A')
+        B = Group('B')
+        C = Group('C')
+        h = Host('h')
+        C.add_host(h)
+        A.add_child_group(B)  # B is child of A
+        B.add_child_group(C)  # C is descendant of A
+        A.add_child_group(B)
+        self.assertEqual(set(h.groups), set([C, B, A]))
+        h2 = Host('h2')
+        C.add_host(h2)
+        self.assertEqual(set(h2.groups), set([C, B, A]))
+
+    def test_ancestor_example(self):
+        # see docstring for Group._walk_relationship
+        groups = {}
+        for name in ['A', 'B', 'C', 'D', 'E', 'F']:
+            groups[name] = Group(name)
+        # first row
+        groups['A'].add_child_group(groups['D'])
+        groups['B'].add_child_group(groups['D'])
+        groups['B'].add_child_group(groups['E'])
+        groups['C'].add_child_group(groups['D'])
+        # second row
+        groups['D'].add_child_group(groups['E'])
+        groups['D'].add_child_group(groups['F'])
+        groups['E'].add_child_group(groups['F'])
+
+        self.assertEqual(
+            set(groups['F'].get_ancestors()),
+            set([
+                groups['A'], groups['B'], groups['C'], groups['D'], groups['E']
+            ])
+        )
+
+    def test_ancestors_recursive_loop_safe(self):
+        '''
+        The get_ancestors method may be referenced before circular parenting
+        checks, so the method is expected to be stable even with loops
+        '''
+        A = Group('A')
+        B = Group('B')
+        A.parent_groups.append(B)
+        B.parent_groups.append(A)
+        # finishes in finite time
+        self.assertEqual(A.get_ancestors(), set([A, B]))